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