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