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