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