View Javadoc

1   package org.apache.maven.plugins.shade;
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.apache.maven.plugin.MojoExecutionException;
23  import org.apache.maven.plugins.shade.filter.Filter;
24  import org.apache.maven.plugins.shade.relocation.Relocator;
25  import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer;
26  import org.apache.maven.plugins.shade.resource.ResourceTransformer;
27  import org.codehaus.plexus.component.annotations.Component;
28  import org.codehaus.plexus.logging.AbstractLogEnabled;
29  import org.codehaus.plexus.util.IOUtil;
30  import org.objectweb.asm.ClassReader;
31  import org.objectweb.asm.ClassVisitor;
32  import org.objectweb.asm.ClassWriter;
33  import org.objectweb.asm.commons.Remapper;
34  import org.objectweb.asm.commons.RemappingClassAdapter;
35  
36  import java.io.File;
37  import java.io.FileOutputStream;
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.io.InputStreamReader;
41  import java.io.OutputStreamWriter;
42  import java.util.ArrayList;
43  import java.util.Enumeration;
44  import java.util.HashSet;
45  import java.util.Iterator;
46  import java.util.List;
47  import java.util.Set;
48  import java.util.jar.JarEntry;
49  import java.util.jar.JarFile;
50  import java.util.jar.JarOutputStream;
51  import java.util.regex.Matcher;
52  import java.util.regex.Pattern;
53  import java.util.zip.ZipException;
54  
55  /**
56   * @author Jason van Zyl
57   */
58  @Component( role = Shader.class, hint = "default" )
59  public class DefaultShader
60      extends AbstractLogEnabled
61      implements Shader
62  {
63  
64      public void shade( ShadeRequest shadeRequest )
65          throws IOException, MojoExecutionException
66      {
67          Set<String> resources = new HashSet<String>();
68  
69          ResourceTransformer manifestTransformer = null;
70          List<ResourceTransformer> transformers = new ArrayList<ResourceTransformer>( shadeRequest.getResourceTransformers() );
71          for ( Iterator<ResourceTransformer> it = transformers.iterator(); it.hasNext(); )
72          {
73              ResourceTransformer transformer = it.next();
74              if ( transformer instanceof ManifestResourceTransformer )
75              {
76                  manifestTransformer = transformer;
77                  it.remove();
78              }
79          }
80  
81          RelocatorRemapper remapper = new RelocatorRemapper( shadeRequest.getRelocators() );
82  
83          shadeRequest.getUberJar().getParentFile().mkdirs();
84          JarOutputStream jos = new JarOutputStream( new FileOutputStream( shadeRequest.getUberJar() ) );
85  
86          if ( manifestTransformer != null )
87          {
88              for ( File jar : shadeRequest.getJars() )
89              {
90                  JarFile jarFile = newJarFile( jar );
91                  for ( Enumeration<JarEntry> en = jarFile.entries(); en.hasMoreElements(); )
92                  {
93                      JarEntry entry = en.nextElement();
94                      String resource = entry.getName();
95                      if ( manifestTransformer.canTransformResource( resource ) )
96                      {
97                          resources.add( resource );
98                          manifestTransformer.processResource( resource, jarFile.getInputStream( entry ), shadeRequest.getRelocators() );
99                          break;
100                     }
101                 }
102             }
103             if ( manifestTransformer.hasTransformedResource() )
104             {
105                 manifestTransformer.modifyOutputStream( jos );
106             }
107         }
108 
109         for ( File jar : shadeRequest.getJars() )
110         {
111 
112             getLogger().debug( "Processing JAR " + jar );
113 
114             List<Filter> jarFilters = getFilters( jar, shadeRequest.getFilters() );
115 
116             JarFile jarFile = newJarFile( jar );
117 
118             for ( Enumeration<JarEntry> j = jarFile.entries(); j.hasMoreElements(); )
119             {
120                 JarEntry entry = j.nextElement();
121 
122                 String name = entry.getName();
123 
124                 if ( "META-INF/INDEX.LIST".equals( name ) )
125                 {
126                     // we cannot allow the jar indexes to be copied over or the
127                     // jar is useless. Ideally, we could create a new one
128                     // later
129                     continue;
130                 }
131 
132                 if ( !entry.isDirectory() && !isFiltered( jarFilters, name ) )
133                 {
134                     InputStream is = jarFile.getInputStream( entry );
135 
136                     String mappedName = remapper.map( name );
137 
138                     int idx = mappedName.lastIndexOf( '/' );
139                     if ( idx != -1 )
140                     {
141                         // make sure dirs are created
142                         String dir = mappedName.substring( 0, idx );
143                         if ( !resources.contains( dir ) )
144                         {
145                             addDirectory( resources, jos, dir );
146                         }
147                     }
148 
149                     if ( name.endsWith( ".class" ) )
150                     {
151                         addRemappedClass( remapper, jos, jar, name, is );
152                     }
153                     else if ( shadeRequest.isShadeSourcesContent() && name.endsWith( ".java" ) )
154                     {
155                         // Avoid duplicates
156                         if ( resources.contains( mappedName ) )
157                         {
158                             continue;
159                         }
160                         
161                         addJavaSource( resources, jos, mappedName, is, shadeRequest.getRelocators() );
162                     }
163                     else
164                     {
165                         if ( !resourceTransformed( transformers, mappedName, is, shadeRequest.getRelocators() ) )
166                         {
167                             // Avoid duplicates that aren't accounted for by the resource transformers
168                             if ( resources.contains( mappedName ) )
169                             {
170                                 continue;
171                             }
172 
173                             addResource( resources, jos, mappedName, is );
174                         }
175                     }
176 
177                     IOUtil.close( is );
178                 }
179             }
180 
181             jarFile.close();
182         }
183 
184         for ( ResourceTransformer transformer : transformers )
185         {
186             if ( transformer.hasTransformedResource() )
187             {
188                 transformer.modifyOutputStream( jos );
189             }
190         }
191 
192         IOUtil.close( jos );
193 
194         for ( Filter filter : shadeRequest.getFilters() )
195         {
196             filter.finished();
197         }
198     }
199 
200     private JarFile newJarFile( File jar )
201         throws IOException
202     {
203         try
204         {
205             return new JarFile( jar );
206         }
207         catch ( ZipException zex )
208         {
209             // JarFile is not very verbose and doesn't tell the user which file it was
210             // so we will create a new Exception instead
211             throw new ZipException( "error in opening zip file " + jar );
212         }
213     }
214 
215     private List<Filter> getFilters( File jar, List<Filter> filters )
216     {
217         List<Filter> list = new ArrayList<Filter>();
218 
219         for ( Filter filter : filters )
220         {
221             if ( filter.canFilter( jar ) )
222             {
223                 list.add( filter );
224             }
225 
226         }
227 
228         return list;
229     }
230 
231     private void addDirectory( Set<String> resources, JarOutputStream jos, String name )
232         throws IOException
233     {
234         if ( name.lastIndexOf( '/' ) > 0 )
235         {
236             String parent = name.substring( 0, name.lastIndexOf( '/' ) );
237             if ( !resources.contains( parent ) )
238             {
239                 addDirectory( resources, jos, parent );
240             }
241         }
242 
243         // directory entries must end in "/"
244         JarEntry entry = new JarEntry( name + "/" );
245         jos.putNextEntry( entry );
246 
247         resources.add( name );
248     }
249 
250     private void addRemappedClass( RelocatorRemapper remapper, JarOutputStream jos, File jar, String name,
251                                    InputStream is )
252         throws IOException, MojoExecutionException
253     {
254         if ( !remapper.hasRelocators() )
255         {
256             try
257             {
258                 jos.putNextEntry( new JarEntry( name ) );
259                 IOUtil.copy( is, jos );
260             }
261             catch ( ZipException e )
262             {
263                 getLogger().warn( "We have a duplicate " + name + " in " + jar );
264             }
265 
266             return;
267         }
268 
269         ClassReader cr = new ClassReader( is );
270 
271         // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
272         // Copying the original constant pool should be avoided because it would keep references
273         // to the original class names. This is not a problem at runtime (because these entries in the
274         // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin
275         // that use the constant pool to determine the dependencies of a class.
276         ClassWriter cw = new ClassWriter( 0 );
277 
278         ClassVisitor cv = new RemappingClassAdapter( cw, remapper );
279 
280         try
281         {
282             cr.accept( cv, ClassReader.EXPAND_FRAMES );
283         }
284         catch ( Throwable ise )
285         {
286             throw new MojoExecutionException( "Error in ASM processing class " + name, ise );
287         }
288 
289         byte[] renamedClass = cw.toByteArray();
290 
291         // Need to take the .class off for remapping evaluation
292         String mappedName = remapper.map( name.substring( 0, name.indexOf( '.' ) ) );
293 
294         try
295         {
296             // Now we put it back on so the class file is written out with the right extension.
297             jos.putNextEntry( new JarEntry( mappedName + ".class" ) );
298 
299             IOUtil.copy( renamedClass, jos );
300         }
301         catch ( ZipException e )
302         {
303             getLogger().warn( "We have a duplicate " + mappedName + " in " + jar );
304         }
305     }
306 
307     private boolean isFiltered( List<Filter> filters, String name )
308     {
309         for ( Filter filter : filters )
310         {
311             if ( filter.isFiltered( name ) )
312             {
313                 return true;
314             }
315         }
316 
317         return false;
318     }
319 
320     private boolean resourceTransformed( List<ResourceTransformer> resourceTransformers, String name, InputStream is,
321                                          List<Relocator> relocators )
322         throws IOException
323     {
324         boolean resourceTransformed = false;
325 
326         for ( ResourceTransformer transformer : resourceTransformers )
327         {
328             if ( transformer.canTransformResource( name ) )
329             {
330                 getLogger().debug( "Transforming " + name + " using " + transformer.getClass().getName() );
331 
332                 transformer.processResource( name, is, relocators );
333 
334                 resourceTransformed = true;
335 
336                 break;
337             }
338         }
339         return resourceTransformed;
340     }
341 
342     private void addJavaSource( Set<String> resources, JarOutputStream jos, String name, InputStream is,
343                                     List<Relocator> relocators )
344             throws IOException
345     {
346         jos.putNextEntry( new JarEntry( name ) );
347 
348         String sourceContent = IOUtil.toString( new InputStreamReader( is, "UTF-8" ) );
349         
350         for ( Relocator relocator : relocators ) {
351         	sourceContent = relocator.applyToSourceContent(sourceContent);
352         }
353         
354         OutputStreamWriter writer = new OutputStreamWriter( jos, "UTF-8" );
355         IOUtil.copy( sourceContent, writer );
356         writer.flush();
357 
358         resources.add( name );
359     }
360 
361     private void addResource( Set<String> resources, JarOutputStream jos, String name, InputStream is )
362         throws IOException
363     {
364         jos.putNextEntry( new JarEntry( name ) );
365 
366         IOUtil.copy( is, jos );
367 
368         resources.add( name );
369     }
370 
371     class RelocatorRemapper
372         extends Remapper
373     {
374 
375         private final Pattern classPattern = Pattern.compile( "(\\[*)?L(.+);" );
376 
377         List<Relocator> relocators;
378 
379         public RelocatorRemapper( List<Relocator> relocators )
380         {
381             this.relocators = relocators;
382         }
383 
384         public boolean hasRelocators()
385         {
386             return !relocators.isEmpty();
387         }
388 
389         public Object mapValue( Object object )
390         {
391             if ( object instanceof String )
392             {
393                 String name = (String) object;
394                 String value = name;
395 
396                 String prefix = "";
397                 String suffix = "";
398 
399                 Matcher m = classPattern.matcher( name );
400                 if ( m.matches() )
401                 {
402                     prefix = m.group( 1 ) + "L";
403                     suffix = ";";
404                     name = m.group( 2 );
405                 }
406 
407                 for ( Relocator r : relocators )
408                 {
409                     if ( r.canRelocateClass( name ) )
410                     {
411                         value = prefix + r.relocateClass( name ) + suffix;
412                         break;
413                     }
414                     else if ( r.canRelocatePath( name ) )
415                     {
416                         value = prefix + r.relocatePath( name ) + suffix;
417                         break;
418                     }
419                 }
420 
421                 return value;
422             }
423 
424             return super.mapValue( object );
425         }
426 
427         public String map( String name )
428         {
429             String value = name;
430 
431             String prefix = "";
432             String suffix = "";
433 
434             Matcher m = classPattern.matcher( name );
435             if ( m.matches() )
436             {
437                 prefix = m.group( 1 ) + "L";
438                 suffix = ";";
439                 name = m.group( 2 );
440             }
441 
442             for ( Relocator r : relocators )
443             {
444                 if ( r.canRelocatePath( name ) )
445                 {
446                     value = prefix + r.relocatePath( name ) + suffix;
447                     break;
448                 }
449             }
450 
451             return value;
452         }
453 
454     }
455 
456 }