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