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