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