1+ /*
2+ * Licensed to the Apache Software Foundation (ASF) under one or more
3+ * contributor license agreements. See the NOTICE file distributed with
4+ * this work for additional information regarding copyright ownership.
5+ * The ASF licenses this file to You under the Apache License, Version 2.0
6+ * (the "License"); you may not use this file except in compliance with
7+ * the License. You may obtain a copy of the License at
8+ *
9+ * https://www.apache.org/licenses/LICENSE-2.0
10+ *
11+ * Unless required by applicable law or agreed to in writing, software
12+ * distributed under the License is distributed on an "AS IS" BASIS,
13+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ * See the License for the specific language governing permissions and
15+ * limitations under the License.
16+ */
17+ package org.apache.grails.buildsrc
18+
19+ import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
20+ import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
21+ import groovy.transform.CompileDynamic
22+ import groovy.transform.CompileStatic
23+ import groovy.util.logging.Slf4j
24+ import org.apache.tools.zip.ZipEntry
25+ import org.apache.tools.zip.ZipOutputStream
26+ import org.gradle.api.file.FileTreeElement
27+ import org.gradle.api.tasks.Input
28+
29+ import java.util.regex.Pattern
30+
31+ /**
32+ * supports combining into a single license file.
33+ */
34+ @Slf4j
35+ @CompileStatic
36+ class GrailsShadowLicenseTransform implements Transformer {
37+
38+ private static final List<Pattern > LICENSE_PATTERNS = [
39+ Pattern . compile(' META-INF/[^/]*LICENSE[^/]*' , Pattern . CASE_INSENSITIVE ),
40+ Pattern . compile(' META-INF/LICENSES/.*' , Pattern . CASE_INSENSITIVE ),
41+ Pattern . compile(' [^/]*LICENSE[^/]*' , Pattern . CASE_INSENSITIVE ),
42+ Pattern . compile(' LICENSES/.*' , Pattern . CASE_INSENSITIVE )
43+ ]
44+
45+ private static final String LICENSE_PATH = ' META-INF/LICENSE'
46+
47+ private LinkedHashMap<String , LicenseHolder > licenses = [:]
48+
49+ @Input
50+ String licenseAppendixEnding = ' LIMITATIONS UNDER THE LICENSE.'
51+
52+ @Input
53+ String licenseTermsEnding = ' END OF TERMS AND CONDITIONS'
54+
55+ @Input
56+ String licenseTermsStart = ' APACHE LICENSE VERSION 2.0'
57+
58+ @Input
59+ String licenseText // to be loaded by file
60+
61+ @Input
62+ Boolean separators = false
63+
64+ @Override
65+ boolean canTransformResource (FileTreeElement element ) {
66+ def path = element. relativePath. pathString
67+ LICENSE_PATTERNS . any { pattern -> pattern. matcher(path). matches() }
68+ }
69+
70+ @Override
71+ @CompileDynamic
72+ // Multiple assignments without list expressions on the right hand side are unsupported in static type checking mode
73+ void transform (TransformerContext context ) {
74+ if (! licenses) {
75+ // Add our license as previously seen so we can dedupe - this transformer only applies to the copy of other jars
76+ def (grailsLicense, grailsIndexMappings) = normalize(licenseText)
77+ licenses[grailsLicense] = new LicenseHolder (license : licenseText, indexMappings : grailsIndexMappings)
78+ }
79+
80+ context. is. withReader {
81+ BufferedReader reader = new BufferedReader (it)
82+
83+ def license = stripJavaBlockComment(reader. text)
84+ def (String normalized, List<Integer > indexMappings) = normalize(license)
85+
86+ // resect Apache License
87+ String resected = resectLicense(license, normalized, indexMappings)
88+ if (! resected. trim()) {
89+ return // only contained duplicated license terms with the ASF license
90+ }
91+
92+ def (String resectedNormalized, List<Integer > resectedIndexMappings) = normalize(resected)
93+ def previouslySeen = getVariations(resectedNormalized). any { licenses. containsKey(it) }
94+ if (! previouslySeen) {
95+ licenses[resectedNormalized] = new LicenseHolder (license : resected, indexMappings : resectedIndexMappings)
96+ }
97+ }
98+ }
99+
100+ private static String stripJavaBlockComment (String text ) {
101+ if (! text. startsWith(' /*' )) {
102+ return text
103+ }
104+
105+ return text
106+ .replaceAll(' ^/\\ *+|\\ *+/\\ s*$' , ' ' ) // opening & closing comment
107+ .readLines()
108+ .collect {
109+ it. replaceFirst(/ ^(\s *\* )?/ , ' ' ). trim()
110+ } // leading whitespace & *
111+ .join(' \n ' )
112+ }
113+
114+ private static Tuple2<String , List<Integer > > normalize (String license ) {
115+ def sb = new StringBuilder ()
116+ List<Integer > indexMappings = [] // each char in sb maps to original index
117+
118+ boolean previousWhitespace = false
119+ for (int i = 0 ; i < license. length(); i++ ) {
120+ char c = license. charAt(i)
121+ if (c. isWhitespace()) {
122+ if (! previousWhitespace) {
123+ sb. append(' ' )
124+ indexMappings << i
125+ previousWhitespace = true
126+ }
127+ } else {
128+ sb. append(Character . toUpperCase(c))
129+ indexMappings << i
130+ previousWhitespace = false
131+ }
132+ }
133+
134+ String normalized = sb. toString(). trim()
135+ int startTrim = sb. indexOf(normalized)
136+ int endTrim = startTrim + normalized. length()
137+ new Tuple2<String , List<Integer > > (normalized, indexMappings[startTrim.. < endTrim])
138+ }
139+
140+ private String resectLicense (String license , String normalized , List<Integer > indexMappings ) {
141+ if (! normalized. startsWith(licenseTermsStart. toUpperCase())) {
142+ return license // not ASF license, return as is
143+ }
144+
145+ // try to search on the appendix first
146+ String endOfLicenseMarker = normalize(licenseAppendixEnding). v1
147+ int end1Index = normalized. indexOf(endOfLicenseMarker)
148+ if (end1Index >= 0 ) {
149+ // license included the appendix
150+ def originalEnding = indexMappings[end1Index + endOfLicenseMarker. size() - 1 ] + 1
151+ if (originalEnding > license. length()) {
152+ // only the license is present
153+ return null
154+ }
155+
156+ return license. substring(originalEnding)
157+ }
158+
159+ // try to search on the terms ending
160+ String endMarker = normalize(licenseTermsEnding). v1
161+ int end2Index = normalized. indexOf(endMarker)
162+ if (end2Index >= 0 ) {
163+ // bare license
164+ def originalEnding = indexMappings[end2Index + endMarker. size() - 1 ] + 1
165+ if (originalEnding > license. length()) {
166+ // only the license is present
167+ return null
168+ }
169+
170+ return license. substring(originalEnding)
171+ }
172+
173+ license
174+ }
175+
176+ private static List<String > getVariations (String license ) {
177+ [license. trim()]. collectMany {
178+ [it, it. replace(' http://' , ' https://' ), it. replace(' https://' , ' http://' )]
179+ }
180+ }
181+
182+ @Override
183+ boolean hasTransformedResource () {
184+ true
185+ }
186+
187+ @Override
188+ void modifyOutputStream (ZipOutputStream os , boolean preserveFileTimestamps ) {
189+ ZipEntry zipEntry = new ZipEntry (LICENSE_PATH )
190+ zipEntry. time = TransformerContext . getEntryTimestamp(preserveFileTimestamps, zipEntry. time)
191+ os. putNextEntry(zipEntry)
192+
193+ os. withPrintWriter { writer ->
194+ licenses. entrySet(). withIndex(). each { license ->
195+ if (license. v1. value == null ) {
196+ return // skip the license that will be copied by shadow from our existing jars
197+ }
198+
199+ writer. println (license. v1. value. license)
200+ if (separators && license. v2 < licenses. size() - 1 ) {
201+ writer. println (" -------------------------${ license.v2} ---------------------------" )
202+ }
203+ }
204+
205+ writer. flush()
206+ }
207+
208+ licenses = [:]
209+ }
210+
211+ private static class LicenseHolder {
212+
213+ String license
214+ List<Integer > indexMappings
215+ }
216+ }
0 commit comments