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