1 package org.apache.maven.plugins.shade;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
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
101 Multimap<String, File> duplicates = HashMultimap.create( 10000, 3 );
102
103
104 shadeJars( shadeRequest, resources, transformers, remapper, out, duplicates );
105
106
107 Multimap<Collection<File>, String> overlapping = HashMultimap.create( 20, 15 );
108
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
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
174
175
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
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
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
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
330 getLogger().warn(
331 Joiner.on( ", " ).join( jarzS ) + " define " + classes.size() + " overlapping classes: " );
332
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
359
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
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
421
422
423
424
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
459 String mappedName = remapper.map( name.substring( 0, name.indexOf( '.' ) ) );
460
461 try
462 {
463
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 }