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