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