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