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