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 java.io.BufferedOutputStream;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.OutputStreamWriter;
29  import java.io.Writer;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Enumeration;
33  import java.util.HashSet;
34  import java.util.Iterator;
35  import java.util.LinkedList;
36  import java.util.List;
37  import java.util.Set;
38  import java.util.jar.JarEntry;
39  import java.util.jar.JarFile;
40  import java.util.jar.JarOutputStream;
41  import java.util.regex.Matcher;
42  import java.util.regex.Pattern;
43  import java.util.zip.ZipException;
44  
45  import org.apache.maven.plugin.MojoExecutionException;
46  import org.apache.maven.plugins.shade.filter.Filter;
47  import org.apache.maven.plugins.shade.relocation.Relocator;
48  import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer;
49  import org.apache.maven.plugins.shade.resource.ResourceTransformer;
50  import org.codehaus.plexus.component.annotations.Component;
51  import org.codehaus.plexus.logging.AbstractLogEnabled;
52  import org.codehaus.plexus.util.IOUtil;
53  import org.objectweb.asm.ClassReader;
54  import org.objectweb.asm.ClassVisitor;
55  import org.objectweb.asm.ClassWriter;
56  import org.objectweb.asm.commons.ClassRemapper;
57  import org.objectweb.asm.commons.Remapper;
58  
59  
60  import com.google.common.base.Joiner;
61  import com.google.common.collect.HashMultimap;
62  import com.google.common.collect.Multimap;
63  
64  /**
65   * @author Jason van Zyl
66   */
67  @Component( role = Shader.class, hint = "default" )
68  public class DefaultShader
69      extends AbstractLogEnabled
70      implements Shader
71  {
72  
73      public void shade( ShadeRequest shadeRequest )
74          throws IOException, MojoExecutionException
75      {
76          Set<String> resources = new HashSet<String>();
77  
78          ResourceTransformer manifestTransformer = null;
79          List<ResourceTransformer> transformers =
80              new ArrayList<ResourceTransformer>( shadeRequest.getResourceTransformers() );
81          for ( Iterator<ResourceTransformer> it = transformers.iterator(); it.hasNext(); )
82          {
83              ResourceTransformer transformer = it.next();
84              if ( transformer instanceof ManifestResourceTransformer )
85              {
86                  manifestTransformer = transformer;
87                  it.remove();
88              }
89          }
90  
91          RelocatorRemapper remapper = new RelocatorRemapper( shadeRequest.getRelocators() );
92  
93          // noinspection ResultOfMethodCallIgnored
94          shadeRequest.getUberJar().getParentFile().mkdirs();
95  
96          JarOutputStream out = null;
97          try
98          {
99              out = new JarOutputStream( new BufferedOutputStream( new FileOutputStream( shadeRequest.getUberJar() ) ) );
100             goThroughAllJarEntriesForManifestTransformer( shadeRequest, resources, manifestTransformer, out );
101 
102             // CHECKSTYLE_OFF: MagicNumber
103             Multimap<String, File> duplicates = HashMultimap.create( 10000, 3 );
104             // CHECKSTYLE_ON: MagicNumber
105 
106             shadeJars( shadeRequest, resources, transformers, remapper, out, duplicates );
107 
108             // CHECKSTYLE_OFF: MagicNumber
109             Multimap<Collection<File>, String> overlapping = HashMultimap.create( 20, 15 );
110             // CHECKSTYLE_ON: MagicNumber
111 
112             for ( String clazz : duplicates.keySet() )
113             {
114                 Collection<File> jarz = duplicates.get( clazz );
115                 if ( jarz.size() > 1 )
116                 {
117                     overlapping.put( jarz, clazz );
118                 }
119             }
120 
121             // Log a summary of duplicates
122             logSummaryOfDuplicates( overlapping );
123 
124             if ( overlapping.keySet().size() > 0 )
125             {
126                 showOverlappingWarning();
127             }
128 
129             for ( ResourceTransformer transformer : transformers )
130             {
131                 if ( transformer.hasTransformedResource() )
132                 {
133                     transformer.modifyOutputStream( out );
134                 }
135             }
136 
137             out.close();
138             out = null;
139         }
140         finally
141         {
142             IOUtil.close( out );
143         }
144 
145         for ( Filter filter : shadeRequest.getFilters() )
146         {
147             filter.finished();
148         }
149     }
150 
151     private void shadeJars( ShadeRequest shadeRequest, Set<String> resources, List<ResourceTransformer> transformers,
152                             RelocatorRemapper remapper, JarOutputStream jos, Multimap<String, File> duplicates )
153         throws IOException, MojoExecutionException
154     {
155         for ( File jar : shadeRequest.getJars() )
156         {
157 
158             getLogger().debug( "Processing JAR " + jar );
159 
160             List<Filter> jarFilters = getFilters( jar, shadeRequest.getFilters() );
161 
162             JarFile jarFile = newJarFile( jar );
163 
164             try
165             {
166 
167                 for ( Enumeration<JarEntry> j = jarFile.entries(); j.hasMoreElements(); )
168                 {
169                     JarEntry entry = j.nextElement();
170 
171                     String name = entry.getName();
172                     
173                     if ( entry.isDirectory() || isFiltered( jarFilters, name ) )
174                     {
175                         continue;
176                     }
177 
178 
179                     if ( "META-INF/INDEX.LIST".equals( name ) )
180                     {
181                         // we cannot allow the jar indexes to be copied over or the
182                         // jar is useless. Ideally, we could create a new one
183                         // later
184                         continue;
185                     }
186                     
187                     if ( "module-info.class".equals( name ) )
188                     {
189                         getLogger().warn( "Discovered module-info.class. "
190                             + "Shading will break its strong encapsulation." );
191                         continue;
192                     }
193 
194                     try
195                     {
196                         shadeSingleJar( shadeRequest, resources, transformers, remapper, jos, duplicates, jar,
197                                         jarFile, entry, name );
198                     }
199                     catch ( Exception e )
200                     {
201                         throw new IOException( String.format( "Problem shading JAR %s entry %s: %s", jar, name, e ),
202                                                e );
203                     }
204                 }
205 
206             }
207             finally
208             {
209                 jarFile.close();
210             }
211         }
212     }
213 
214     private void shadeSingleJar( ShadeRequest shadeRequest, Set<String> resources,
215                                  List<ResourceTransformer> transformers, RelocatorRemapper remapper,
216                                  JarOutputStream jos, Multimap<String, File> duplicates, File jar, JarFile jarFile,
217                                  JarEntry entry, String name )
218         throws IOException, MojoExecutionException
219     {
220         InputStream in = null;
221         try
222         {
223             in = jarFile.getInputStream( entry );
224             String mappedName = remapper.map( name );
225 
226             int idx = mappedName.lastIndexOf( '/' );
227             if ( idx != -1 )
228             {
229                 // make sure dirs are created
230                 String dir = mappedName.substring( 0, idx );
231                 if ( !resources.contains( dir ) )
232                 {
233                     addDirectory( resources, jos, dir );
234                 }
235             }
236 
237             if ( name.endsWith( ".class" ) )
238             {
239                 duplicates.put( name, jar );
240                 addRemappedClass( remapper, jos, jar, name, in );
241             }
242             else if ( shadeRequest.isShadeSourcesContent() && name.endsWith( ".java" ) )
243             {
244                 // Avoid duplicates
245                 if ( resources.contains( mappedName ) )
246                 {
247                     return;
248                 }
249 
250                 addJavaSource( resources, jos, mappedName, in, shadeRequest.getRelocators() );
251             }
252             else
253             {
254                 if ( !resourceTransformed( transformers, mappedName, in, shadeRequest.getRelocators() ) )
255                 {
256                     // Avoid duplicates that aren't accounted for by the resource transformers
257                     if ( resources.contains( mappedName ) )
258                     {
259                         return;
260                     }
261 
262                     addResource( resources, jos, mappedName, entry.getTime(), in );
263                 }
264             }
265 
266             in.close();
267             in = null;
268         }
269         finally
270         {
271             IOUtil.close( in );
272         }
273     }
274 
275     private void goThroughAllJarEntriesForManifestTransformer( ShadeRequest shadeRequest, Set<String> resources,
276                                                                ResourceTransformer manifestTransformer,
277                                                                JarOutputStream jos )
278         throws IOException
279     {
280         if ( manifestTransformer != null )
281         {
282             for ( File jar : shadeRequest.getJars() )
283             {
284                 JarFile jarFile = newJarFile( jar );
285                 try
286                 {
287                     for ( Enumeration<JarEntry> en = jarFile.entries(); en.hasMoreElements(); )
288                     {
289                         JarEntry entry = en.nextElement();
290                         String resource = entry.getName();
291                         if ( manifestTransformer.canTransformResource( resource ) )
292                         {
293                             resources.add( resource );
294                             InputStream inputStream = jarFile.getInputStream( entry );
295                             try
296                             {
297                                 manifestTransformer.processResource( resource, inputStream,
298                                                                      shadeRequest.getRelocators() );
299                             }
300                             finally
301                             {
302                                 inputStream.close();
303                             }
304                             break;
305                         }
306                     }
307                 }
308                 finally
309                 {
310                     jarFile.close();
311                 }
312             }
313             if ( manifestTransformer.hasTransformedResource() )
314             {
315                 manifestTransformer.modifyOutputStream( jos );
316             }
317         }
318     }
319 
320     private void showOverlappingWarning()
321     {
322         getLogger().warn( "maven-shade-plugin has detected that some class files are" );
323         getLogger().warn( "present in two or more JARs. When this happens, only one" );
324         getLogger().warn( "single version of the class is copied to the uber jar." );
325         getLogger().warn( "Usually this is not harmful and you can skip these warnings," );
326         getLogger().warn( "otherwise try to manually exclude artifacts based on" );
327         getLogger().warn( "mvn dependency:tree -Ddetail=true and the above output." );
328         getLogger().warn( "See http://maven.apache.org/plugins/maven-shade-plugin/" );
329     }
330 
331     private void logSummaryOfDuplicates( Multimap<Collection<File>, String> overlapping )
332     {
333         for ( Collection<File> jarz : overlapping.keySet() )
334         {
335             List<String> jarzS = new LinkedList<String>();
336 
337             for ( File jjar : jarz )
338             {
339                 jarzS.add( jjar.getName() );
340             }
341 
342             List<String> classes = new LinkedList<String>();
343 
344             for ( String clazz : overlapping.get( jarz ) )
345             {
346                 classes.add( clazz.replace( ".class", "" ).replace( "/", "." ) );
347             }
348 
349             //CHECKSTYLE_OFF: LineLength
350             getLogger().warn(
351                 Joiner.on( ", " ).join( jarzS ) + " define " + classes.size() + " overlapping classes: " );
352             //CHECKSTYLE_ON: LineLength
353 
354             int max = 10;
355 
356             for ( int i = 0; i < Math.min( max, classes.size() ); i++ )
357             {
358                 getLogger().warn( "  - " + classes.get( i ) );
359             }
360 
361             if ( classes.size() > max )
362             {
363                 getLogger().warn( "  - " + ( classes.size() - max ) + " more..." );
364             }
365 
366         }
367     }
368 
369     private JarFile newJarFile( File jar )
370         throws IOException
371     {
372         try
373         {
374             return new JarFile( jar );
375         }
376         catch ( ZipException zex )
377         {
378             // JarFile is not very verbose and doesn't tell the user which file it was
379             // so we will create a new Exception instead
380             throw new ZipException( "error in opening zip file " + jar );
381         }
382     }
383 
384     private List<Filter> getFilters( File jar, List<Filter> filters )
385     {
386         List<Filter> list = new ArrayList<Filter>();
387 
388         for ( Filter filter : filters )
389         {
390             if ( filter.canFilter( jar ) )
391             {
392                 list.add( filter );
393             }
394 
395         }
396 
397         return list;
398     }
399 
400     private void addDirectory( Set<String> resources, JarOutputStream jos, String name )
401         throws IOException
402     {
403         if ( name.lastIndexOf( '/' ) > 0 )
404         {
405             String parent = name.substring( 0, name.lastIndexOf( '/' ) );
406             if ( !resources.contains( parent ) )
407             {
408                 addDirectory( resources, jos, parent );
409             }
410         }
411 
412         // directory entries must end in "/"
413         JarEntry entry = new JarEntry( name + "/" );
414         jos.putNextEntry( entry );
415 
416         resources.add( name );
417     }
418 
419     private void addRemappedClass( RelocatorRemapper remapper, JarOutputStream jos, File jar, String name,
420                                    InputStream is )
421         throws IOException, MojoExecutionException
422     {
423         if ( !remapper.hasRelocators() )
424         {
425             try
426             {
427                 jos.putNextEntry( new JarEntry( name ) );
428                 IOUtil.copy( is, jos );
429             }
430             catch ( ZipException e )
431             {
432                 getLogger().debug( "We have a duplicate " + name + " in " + jar );
433             }
434 
435             return;
436         }
437 
438         ClassReader cr = new ClassReader( is );
439 
440         // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
441         // Copying the original constant pool should be avoided because it would keep references
442         // to the original class names. This is not a problem at runtime (because these entries in the
443         // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin
444         // that use the constant pool to determine the dependencies of a class.
445         ClassWriter cw = new ClassWriter( 0 );
446 
447         final String pkg = name.substring( 0, name.lastIndexOf( '/' ) + 1 );
448         ClassVisitor cv = new ClassRemapper( cw, remapper )
449         {
450             @Override
451             public void visitSource( final String source, final String debug )
452             {
453                 if ( source == null )
454                 {
455                     super.visitSource( source, debug );
456                 }
457                 else
458                 {
459                     final String fqSource = pkg + source;
460                     final String mappedSource = remapper.map( fqSource );
461                     final String filename = mappedSource.substring( mappedSource.lastIndexOf( '/' ) + 1 );
462                     super.visitSource( filename, debug );
463                 }
464             }
465         };
466 
467         try
468         {
469             cr.accept( cv, ClassReader.EXPAND_FRAMES );
470         }
471         catch ( Throwable ise )
472         {
473             throw new MojoExecutionException( "Error in ASM processing class " + name, ise );
474         }
475 
476         byte[] renamedClass = cw.toByteArray();
477 
478         // Need to take the .class off for remapping evaluation
479         String mappedName = remapper.map( name.substring( 0, name.indexOf( '.' ) ) );
480 
481         try
482         {
483             // Now we put it back on so the class file is written out with the right extension.
484             jos.putNextEntry( new JarEntry( mappedName + ".class" ) );
485 
486             IOUtil.copy( renamedClass, jos );
487         }
488         catch ( ZipException e )
489         {
490             getLogger().debug( "We have a duplicate " + mappedName + " in " + jar );
491         }
492     }
493 
494     private boolean isFiltered( List<Filter> filters, String name )
495     {
496         for ( Filter filter : filters )
497         {
498             if ( filter.isFiltered( name ) )
499             {
500                 return true;
501             }
502         }
503 
504         return false;
505     }
506 
507     private boolean resourceTransformed( List<ResourceTransformer> resourceTransformers, String name, InputStream is,
508                                          List<Relocator> relocators )
509         throws IOException
510     {
511         boolean resourceTransformed = false;
512 
513         for ( ResourceTransformer transformer : resourceTransformers )
514         {
515             if ( transformer.canTransformResource( name ) )
516             {
517                 getLogger().debug( "Transforming " + name + " using " + transformer.getClass().getName() );
518 
519                 transformer.processResource( name, is, relocators );
520 
521                 resourceTransformed = true;
522 
523                 break;
524             }
525         }
526         return resourceTransformed;
527     }
528 
529     private void addJavaSource( Set<String> resources, JarOutputStream jos, String name, InputStream is,
530                                 List<Relocator> relocators )
531         throws IOException
532     {
533         jos.putNextEntry( new JarEntry( name ) );
534 
535         String sourceContent = IOUtil.toString( new InputStreamReader( is, "UTF-8" ) );
536 
537         for ( Relocator relocator : relocators )
538         {
539             sourceContent = relocator.applyToSourceContent( sourceContent );
540         }
541 
542         final Writer writer = new OutputStreamWriter( jos, "UTF-8" );
543         IOUtil.copy( sourceContent, writer );
544         writer.flush();
545 
546         resources.add( name );
547     }
548 
549     private void addResource( Set<String> resources, JarOutputStream jos, String name, long lastModified,
550                               InputStream is )
551         throws IOException
552     {
553         final JarEntry jarEntry = new JarEntry( name );
554 
555         jarEntry.setTime( lastModified );
556 
557         jos.putNextEntry( jarEntry );
558 
559         IOUtil.copy( is, jos );
560 
561         resources.add( name );
562     }
563 
564     static class RelocatorRemapper
565         extends Remapper
566     {
567 
568         private final Pattern classPattern = Pattern.compile( "(\\[*)?L(.+);" );
569 
570         List<Relocator> relocators;
571 
572         RelocatorRemapper( List<Relocator> relocators )
573         {
574             this.relocators = relocators;
575         }
576 
577         public boolean hasRelocators()
578         {
579             return !relocators.isEmpty();
580         }
581 
582         public Object mapValue( Object object )
583         {
584             if ( object instanceof String )
585             {
586                 String name = (String) object;
587                 String value = name;
588 
589                 String prefix = "";
590                 String suffix = "";
591 
592                 Matcher m = classPattern.matcher( name );
593                 if ( m.matches() )
594                 {
595                     prefix = m.group( 1 ) + "L";
596                     suffix = ";";
597                     name = m.group( 2 );
598                 }
599 
600                 for ( Relocator r : relocators )
601                 {
602                     if ( r.canRelocateClass( name ) )
603                     {
604                         value = prefix + r.relocateClass( name ) + suffix;
605                         break;
606                     }
607                     else if ( r.canRelocatePath( name ) )
608                     {
609                         value = prefix + r.relocatePath( name ) + suffix;
610                         break;
611                     }
612                 }
613 
614                 return value;
615             }
616 
617             return super.mapValue( object );
618         }
619 
620         public String map( String name )
621         {
622             String value = name;
623 
624             String prefix = "";
625             String suffix = "";
626 
627             Matcher m = classPattern.matcher( name );
628             if ( m.matches() )
629             {
630                 prefix = m.group( 1 ) + "L";
631                 suffix = ";";
632                 name = m.group( 2 );
633             }
634 
635             for ( Relocator r : relocators )
636             {
637                 if ( r.canRelocatePath( name ) )
638                 {
639                     value = prefix + r.relocatePath( name ) + suffix;
640                     break;
641                 }
642             }
643 
644             return value;
645         }
646 
647     }
648 
649 }