View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.shade.relocation;
20  
21  import java.util.Collection;
22  import java.util.LinkedHashSet;
23  import java.util.List;
24  import java.util.Set;
25  import java.util.regex.Pattern;
26  
27  import org.codehaus.plexus.util.SelectorUtils;
28  
29  /**
30   * @author Jason van Zyl
31   * @author Mauro Talevi
32   */
33  public class SimpleRelocator implements Relocator {
34      /**
35       * Match dot, slash or space at end of string
36       */
37      private static final Pattern RX_ENDS_WITH_DOT_SLASH_SPACE = Pattern.compile("[./ ]$");
38  
39      /**
40       * Match <ul>
41       *     <li>certain Java keywords + space</li>
42       *     <li>beginning of Javadoc link + optional line breaks and continuations with '*'</li>
43       *     <li>(opening curly brace / opening parenthesis / comma / equals / semicolon) + space</li>
44       *     <li>(closing curly brace / closing multi-line comment) + space</li>
45       * </ul>
46       * at end of string
47       */
48      private static final Pattern RX_ENDS_WITH_JAVA_KEYWORD = Pattern.compile(
49              "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $"
50                      + "|"
51                      + "\\{@link( \\*)* $"
52                      + "|"
53                      + "([{}(=;,]|\\*/) $");
54  
55      private final String pattern;
56  
57      private final String pathPattern;
58  
59      private final String shadedPattern;
60  
61      private final String shadedPathPattern;
62  
63      private final Set<String> includes;
64  
65      private final Set<String> excludes;
66  
67      private final Set<String> sourcePackageExcludes = new LinkedHashSet<>();
68  
69      private final Set<String> sourcePathExcludes = new LinkedHashSet<>();
70  
71      private final boolean rawString;
72  
73      public SimpleRelocator(String patt, String shadedPattern, List<String> includes, List<String> excludes) {
74          this(patt, shadedPattern, includes, excludes, false);
75      }
76  
77      public SimpleRelocator(
78              String patt, String shadedPattern, List<String> includes, List<String> excludes, boolean rawString) {
79          this.rawString = rawString;
80  
81          if (rawString) {
82              this.pathPattern = patt;
83              this.shadedPathPattern = shadedPattern;
84  
85              this.pattern = null; // not used for raw string relocator
86              this.shadedPattern = null; // not used for raw string relocator
87          } else {
88              if (patt == null) {
89                  this.pattern = "";
90                  this.pathPattern = "";
91              } else {
92                  this.pattern = patt.replace('/', '.');
93                  this.pathPattern = patt.replace('.', '/');
94              }
95  
96              if (shadedPattern != null) {
97                  this.shadedPattern = shadedPattern.replace('/', '.');
98                  this.shadedPathPattern = shadedPattern.replace('.', '/');
99              } else {
100                 this.shadedPattern = "hidden." + this.pattern;
101                 this.shadedPathPattern = "hidden/" + this.pathPattern;
102             }
103         }
104 
105         this.includes = normalizePatterns(includes);
106         this.excludes = normalizePatterns(excludes);
107 
108         // Don't replace all dots to slashes, otherwise /META-INF/maven/${groupId} can't be matched.
109         if (includes != null && !includes.isEmpty()) {
110             this.includes.addAll(includes);
111         }
112 
113         if (excludes != null && !excludes.isEmpty()) {
114             this.excludes.addAll(excludes);
115         }
116 
117         if (!rawString && this.excludes != null) {
118             // Create exclude pattern sets for sources
119             for (String exclude : this.excludes) {
120                 // Excludes should be subpackages of the global pattern
121                 if (exclude.startsWith(pattern)) {
122                     sourcePackageExcludes.add(
123                             exclude.substring(pattern.length()).replaceFirst("[.][*]$", ""));
124                 }
125                 // Excludes should be subpackages of the global pattern
126                 if (exclude.startsWith(pathPattern)) {
127                     sourcePathExcludes.add(
128                             exclude.substring(pathPattern.length()).replaceFirst("[/][*]$", ""));
129                 }
130             }
131         }
132     }
133 
134     private static Set<String> normalizePatterns(Collection<String> patterns) {
135         Set<String> normalized = null;
136 
137         if (patterns != null && !patterns.isEmpty()) {
138             normalized = new LinkedHashSet<>();
139             for (String pattern : patterns) {
140                 String classPattern = pattern.replace('.', '/');
141                 normalized.add(classPattern);
142                 // Actually, class patterns should just use 'foo.bar.*' ending with a single asterisk, but some users
143                 // mistake them for path patterns like 'my/path/**', so let us be a bit more lenient here.
144                 if (classPattern.endsWith("/*") || classPattern.endsWith("/**")) {
145                     String packagePattern = classPattern.substring(0, classPattern.lastIndexOf('/'));
146                     normalized.add(packagePattern);
147                 }
148             }
149         }
150 
151         return normalized;
152     }
153 
154     private boolean isIncluded(String path) {
155         if (includes != null && !includes.isEmpty()) {
156             for (String include : includes) {
157                 if (SelectorUtils.matchPath(include, path, true)) {
158                     return true;
159                 }
160             }
161             return false;
162         }
163         return true;
164     }
165 
166     private boolean isExcluded(String path) {
167         if (excludes != null && !excludes.isEmpty()) {
168             for (String exclude : excludes) {
169                 if (SelectorUtils.matchPath(exclude, path, true)) {
170                     return true;
171                 }
172             }
173         }
174         return false;
175     }
176 
177     public boolean canRelocatePath(String path) {
178         if (rawString) {
179             return Pattern.compile(pathPattern).matcher(path).find();
180         }
181 
182         if (path.endsWith(".class")) {
183             path = path.substring(0, path.length() - 6);
184         }
185 
186         // Allow for annoying option of an extra / on the front of a path. See MSHADE-119; comes from
187         // getClass().getResource("/a/b/c.properties").
188         if (!path.isEmpty() && path.charAt(0) == '/') {
189             path = path.substring(1);
190         }
191 
192         return isIncluded(path) && !isExcluded(path) && path.startsWith(pathPattern);
193     }
194 
195     public boolean canRelocateClass(String clazz) {
196         return !rawString && clazz.indexOf('/') < 0 && canRelocatePath(clazz.replace('.', '/'));
197     }
198 
199     public String relocatePath(String path) {
200         if (rawString) {
201             return path.replaceAll(pathPattern, shadedPathPattern);
202         } else {
203             return path.replaceFirst(pathPattern, shadedPathPattern);
204         }
205     }
206 
207     public String relocateClass(String clazz) {
208         return rawString ? clazz : clazz.replaceFirst(pattern, shadedPattern);
209     }
210 
211     public String applyToSourceContent(String sourceContent) {
212         if (rawString) {
213             return sourceContent;
214         }
215         sourceContent = shadeSourceWithExcludes(sourceContent, pattern, shadedPattern, sourcePackageExcludes);
216         return shadeSourceWithExcludes(sourceContent, pathPattern, shadedPathPattern, sourcePathExcludes);
217     }
218 
219     private String shadeSourceWithExcludes(
220             String sourceContent, String patternFrom, String patternTo, Set<String> excludedPatterns) {
221         // Usually shading makes package names a bit longer, so make buffer 10% bigger than original source
222         StringBuilder shadedSourceContent = new StringBuilder(sourceContent.length() * 11 / 10);
223         boolean isFirstSnippet = true;
224         // Make sure that search pattern starts at word boundary and that we look for literal ".", not regex jokers
225         String[] snippets = sourceContent.split("\\b" + patternFrom.replace(".", "[.]") + "\\b");
226         for (int i = 0, snippetsLength = snippets.length; i < snippetsLength; i++) {
227             String snippet = snippets[i];
228             String previousSnippet = isFirstSnippet ? "" : snippets[i - 1];
229             boolean doExclude = false;
230             for (String excludedPattern : excludedPatterns) {
231                 if (snippet.startsWith(excludedPattern)) {
232                     doExclude = true;
233                     break;
234                 }
235             }
236             if (isFirstSnippet) {
237                 shadedSourceContent.append(snippet);
238                 isFirstSnippet = false;
239             } else {
240                 String previousSnippetOneLine = previousSnippet.replaceAll("\\s+", " ");
241                 boolean afterDotSlashSpace = RX_ENDS_WITH_DOT_SLASH_SPACE
242                         .matcher(previousSnippetOneLine)
243                         .find();
244                 boolean afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD
245                         .matcher(previousSnippetOneLine)
246                         .find();
247                 boolean shouldExclude = doExclude || afterDotSlashSpace && !afterJavaKeyWord;
248                 shadedSourceContent
249                         .append(shouldExclude ? patternFrom : patternTo)
250                         .append(snippet);
251             }
252         }
253         return shadedSourceContent.toString();
254     }
255 }