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.forge.buildlogic.shadowjar
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 org.apache.tools.zip.ZipEntry
24+ import org.apache.tools.zip.ZipOutputStream
25+ import org.gradle.api.file.FileTreeElement
26+ import org.gradle.api.tasks.Input
27+
28+ import java.util.regex.Pattern
29+
30+ /**
31+ * This transformer assists in combining all known licenses into a single META-INF/LICENSE file. Please note that the
32+ * shadow plugin will initially copy all dependencies that are in the local project and then it will copy all other external
33+ * dependencies. Transformers only apply to the copied jar file dependencies, and not to the local project dependencies.
34+ */
35+ @CompileStatic
36+ class GrailsShadowLicenseTransform implements Transformer {
37+
38+ private static final List<Pattern > LICENSE_PATTERNS = [
39+ ~' (?i)META-INF/[^/]*LICENSE[^/]*' ,
40+ ~' (?i)META-INF/LICENSES/.*' ,
41+ ~' (?i)[^/]*LICENSE[^/]*' ,
42+ ~' (?i)LICENSES/.*'
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+ /**
65+ * Whether this transformer can process the given resource. If set to true, it's expected the transformer will
66+ * write the resource.
67+ */
68+ @Override
69+ boolean canTransformResource (FileTreeElement element ) {
70+ def path = element. relativePath. pathString
71+ LICENSE_PATTERNS . any { pattern -> pattern. matcher(path). matches() }
72+ }
73+
74+ /**
75+ * Parses any file that matches the license patterns and extracts the license text, deduplicating & combining where
76+ * possible.
77+ *
78+ * @param context contains the input stream of the resource to transform
79+ */
80+ @Override
81+ // Multiple assignments without list expressions on the right hand side are unsupported in static type checking mode
82+ @CompileDynamic
83+ void transform (TransformerContext context ) {
84+ if (! licenses) {
85+ // Add our license as previously seen so we can dedupe - this transformer only applies to the copy of other jars
86+ def (grailsLicense, grailsIndexMappings) = normalize(licenseText)
87+ licenses[grailsLicense] = new LicenseHolder (license : licenseText, indexMappings : grailsIndexMappings)
88+ }
89+
90+ context. is. withReader {
91+ BufferedReader reader = new BufferedReader (it)
92+
93+ def license = stripJavaBlockComment(reader. text)
94+ def (String normalized, List<Integer > indexMappings) = normalize(license)
95+
96+ // resect Apache License
97+ String resected = resectLicense(license, normalized, indexMappings)
98+ if (! resected. trim()) {
99+ return // only contained duplicated license terms with the ASF license
100+ }
101+
102+ def (String resectedNormalized, List<Integer > resectedIndexMappings) = normalize(resected)
103+ def previouslySeen = getVariations(resectedNormalized). any { licenses. containsKey(it) }
104+ if (! previouslySeen) {
105+ licenses[resectedNormalized] = new LicenseHolder (license : resected, indexMappings : resectedIndexMappings)
106+ }
107+ }
108+ }
109+
110+ /**
111+ * Some libraries ship with a license.header file that contains a Java block comment. This method strips the
112+ * Java block comment syntax and returns the text without the comment.
113+ */
114+ private static String stripJavaBlockComment (String text ) {
115+ if (! text. startsWith(' /*' )) {
116+ return text
117+ }
118+
119+ return text
120+ .replaceAll(' ^/\\ *+|\\ *+/\\ s*$' , ' ' ) // opening & closing comment
121+ .readLines()
122+ .collect {
123+ it. replaceFirst(/ ^(\s *\* )?/ , ' ' ). trim()
124+ } // leading whitespace & *
125+ .join(' \n ' )
126+ }
127+
128+ /**
129+ * Normalizes the license text by collapsing whitespace and uppercasing all characters. It also returns a mapping
130+ * of the normalized text to the original license text, where each character in the normalized text maps to its
131+ * original index in the license text. This allows us to index into the normalized text from sections of the license text
132+ * for deduplication purposes.
133+ *
134+ * @param license the original license text
135+ * @return a tuple containing the normalized license text and a list of index mappings
136+ */
137+ private static Tuple2<String , List<Integer > > normalize (String license ) {
138+ def sb = new StringBuilder ()
139+ List<Integer > indexMappings = [] // each char in sb maps to original index
140+
141+ boolean previousWhitespace = false
142+ for (int i = 0 ; i < license. length(); i++ ) {
143+ char c = license. charAt(i)
144+ if (c. isWhitespace()) {
145+ if (! previousWhitespace) {
146+ sb. append(' ' )
147+ indexMappings << i
148+ previousWhitespace = true
149+ }
150+ } else {
151+ sb. append(Character . toUpperCase(c))
152+ indexMappings << i
153+ previousWhitespace = false
154+ }
155+ }
156+
157+ String normalized = sb. toString(). trim()
158+ int startTrim = sb. indexOf(normalized)
159+ int endTrim = startTrim + normalized. length()
160+ new Tuple2<String , List<Integer > > (normalized, indexMappings[startTrim.. < endTrim])
161+ }
162+
163+ /**
164+ * For a given license, this method will extract the duplicate license text and return the remaining text
165+ * @param license the original license text
166+ * @param normalized a normalized version of the license text, with all whitespace collapsed and all characters uppercased
167+ * @param indexMappings a mapping of the normalized text to the original license text, where each character in the normalized text maps to its original index in the license text
168+ * @return either null if no additional license text was found or the additional license text
169+ */
170+ private String resectLicense (String license , String normalized , List<Integer > indexMappings ) {
171+ if (! normalized. startsWith(licenseTermsStart. toUpperCase())) {
172+ return license // not ASF license, return as is
173+ }
174+
175+ // try to search on the appendix first
176+ String endOfLicenseMarker = normalize(licenseAppendixEnding). v1
177+ int end1Index = normalized. indexOf(endOfLicenseMarker)
178+ if (end1Index >= 0 ) {
179+ // license included the appendix
180+ def originalEnding = indexMappings[end1Index + endOfLicenseMarker. size() - 1 ] + 1
181+ if (originalEnding > license. length()) {
182+ // only the license is present
183+ return null
184+ }
185+
186+ return license. substring(originalEnding)
187+ }
188+
189+ // try to search on the terms ending
190+ String endMarker = normalize(licenseTermsEnding). v1
191+ int end2Index = normalized. indexOf(endMarker)
192+ if (end2Index >= 0 ) {
193+ // bare license
194+ def originalEnding = indexMappings[end2Index + endMarker. size() - 1 ] + 1
195+ if (originalEnding > license. length()) {
196+ // only the license is present
197+ return null
198+ }
199+
200+ return license. substring(originalEnding)
201+ }
202+
203+ license
204+ }
205+
206+ /**
207+ * Some licenses mix http & https links, handles simple variations of the license to ensure the license can be
208+ * deduplicated.
209+ *
210+ * @param license the license text
211+ * @return a list of variations of the license text
212+ */
213+ private static List<String > getVariations (String license ) {
214+ [license. trim()]. collectMany {
215+ [it, it. replace(' http://' , ' https://' ), it. replace(' https://' , ' http://' )]
216+ }
217+ }
218+
219+ /**
220+ * Whether this transformer will modify the output stream.
221+ */
222+ @Override
223+ boolean hasTransformedResource () {
224+ // Must always be true since we want to write the LICENSE file and all license files originate from jar files
225+ // after our project restructure
226+ true
227+ }
228+
229+ /**
230+ * Writes the combined license file to the output stream. The file will be written to
231+ * META-INF/LICENSE and will contain all licenses found in the project
232+ *
233+ * @param os the jar file output stream
234+ * @param preserveFileTimestamps whether to preserve file timestamps in the output jar
235+ */
236+ @Override
237+ void modifyOutputStream (ZipOutputStream os , boolean preserveFileTimestamps ) {
238+ ZipEntry zipEntry = new ZipEntry (LICENSE_PATH )
239+ zipEntry. time = TransformerContext . getEntryTimestamp(preserveFileTimestamps, zipEntry. time)
240+ os. putNextEntry(zipEntry)
241+
242+ os. withPrintWriter { writer ->
243+ licenses. entrySet(). withIndex(). each { license ->
244+ if (license. v1. value == null ) {
245+ return // skip the license that will be copied by shadow from our existing jars
246+ }
247+
248+ writer. println (license. v1. value. license)
249+ if (separators && license. v2 < licenses. size() - 1 ) {
250+ writer. println (" -------------------------${ license.v2} ---------------------------" )
251+ }
252+ }
253+
254+ writer. flush()
255+ }
256+
257+ licenses = [:]
258+ }
259+
260+ private static class LicenseHolder {
261+
262+ String license
263+ List<Integer > indexMappings
264+ }
265+ }
0 commit comments