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.maven.plugin.MojoExecutionException;
48 import org.apache.maven.plugins.shade.filter.Filter;
49 import org.apache.maven.plugins.shade.relocation.Relocator;
50 import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer;
51 import org.apache.maven.plugins.shade.resource.ResourceTransformer;
52 import org.codehaus.plexus.component.annotations.Component;
53 import org.codehaus.plexus.logging.AbstractLogEnabled;
54 import org.codehaus.plexus.util.IOUtil;
55 import org.objectweb.asm.ClassReader;
56 import org.objectweb.asm.ClassVisitor;
57 import org.objectweb.asm.ClassWriter;
58 import org.objectweb.asm.commons.ClassRemapper;
59 import org.objectweb.asm.commons.Remapper;
60
61
62 import com.google.common.base.Joiner;
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 ResourceTransformer 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 = 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 {
250
251 if ( resources.contains( mappedName ) )
252 {
253 getLogger().debug( "We have a duplicate " + name + " in " + jar );
254 return;
255 }
256
257 addResource( resources, jos, mappedName, entry.getTime(), in );
258 }
259 }
260 }
261 }
262
263 private void goThroughAllJarEntriesForManifestTransformer( ShadeRequest shadeRequest, Set<String> resources,
264 ResourceTransformer manifestTransformer,
265 JarOutputStream jos )
266 throws IOException
267 {
268 if ( manifestTransformer != null )
269 {
270 for ( File jar : shadeRequest.getJars() )
271 {
272 try ( JarFile jarFile = newJarFile( jar ) )
273 {
274 for ( Enumeration<JarEntry> en = jarFile.entries(); en.hasMoreElements(); )
275 {
276 JarEntry entry = en.nextElement();
277 String resource = entry.getName();
278 if ( manifestTransformer.canTransformResource( resource ) )
279 {
280 resources.add( resource );
281 try ( InputStream inputStream = jarFile.getInputStream( entry ) )
282 {
283 manifestTransformer.processResource( resource, inputStream,
284 shadeRequest.getRelocators() );
285 }
286 break;
287 }
288 }
289 }
290 }
291 if ( manifestTransformer.hasTransformedResource() )
292 {
293 manifestTransformer.modifyOutputStream( jos );
294 }
295 }
296 }
297
298 private void showOverlappingWarning()
299 {
300 getLogger().warn( "maven-shade-plugin has detected that some class files are" );
301 getLogger().warn( "present in two or more JARs. When this happens, only one" );
302 getLogger().warn( "single version of the class is copied to the uber jar." );
303 getLogger().warn( "Usually this is not harmful and you can skip these warnings," );
304 getLogger().warn( "otherwise try to manually exclude artifacts based on" );
305 getLogger().warn( "mvn dependency:tree -Ddetail=true and the above output." );
306 getLogger().warn( "See http://maven.apache.org/plugins/maven-shade-plugin/" );
307 }
308
309 private void logSummaryOfDuplicates( Multimap<Collection<File>, String> overlapping )
310 {
311 for ( Collection<File> jarz : overlapping.keySet() )
312 {
313 List<String> jarzS = new ArrayList<>();
314
315 for ( File jjar : jarz )
316 {
317 jarzS.add( jjar.getName() );
318 }
319
320 Collections.sort( jarzS );
321
322 List<String> classes = new LinkedList<>();
323 List<String> resources = new LinkedList<>();
324
325 for ( String name : overlapping.get( jarz ) )
326 {
327 if ( name.endsWith( ".class" ) )
328 {
329 classes.add( name.replace( ".class", "" ).replace( "/", "." ) );
330 }
331 else
332 {
333 resources.add( name );
334 }
335 }
336
337
338 final Collection<String> overlaps = new ArrayList<>();
339 if ( !classes.isEmpty() )
340 {
341 overlaps.add( "classes" );
342 }
343 if ( !resources.isEmpty() )
344 {
345 overlaps.add( "resources" );
346 }
347
348 final List<String> all = new ArrayList<>( classes.size() + resources.size() );
349 all.addAll( classes );
350 all.addAll( resources );
351
352 getLogger().warn(
353 Joiner.on( ", " ).join( jarzS ) + " define " + all.size()
354 + " overlapping " + Joiner.on( " and " ).join( overlaps ) + ": " );
355
356
357 Collections.sort( all );
358
359 int max = 10;
360
361 for ( int i = 0; i < Math.min( max, all.size() ); i++ )
362 {
363 getLogger().warn( " - " + all.get( i ) );
364 }
365
366 if ( all.size() > max )
367 {
368 getLogger().warn( " - " + ( all.size() - max ) + " more..." );
369 }
370
371 }
372 }
373
374 private JarFile newJarFile( File jar )
375 throws IOException
376 {
377 try
378 {
379 return new JarFile( jar );
380 }
381 catch ( ZipException zex )
382 {
383
384
385 throw new ZipException( "error in opening zip file " + jar );
386 }
387 }
388
389 private List<Filter> getFilters( File jar, List<Filter> filters )
390 {
391 List<Filter> list = new ArrayList<>();
392
393 for ( Filter filter : filters )
394 {
395 if ( filter.canFilter( jar ) )
396 {
397 list.add( filter );
398 }
399
400 }
401
402 return list;
403 }
404
405 private void addDirectory( Set<String> resources, JarOutputStream jos, String name, long time )
406 throws IOException
407 {
408 if ( name.lastIndexOf( '/' ) > 0 )
409 {
410 String parent = name.substring( 0, name.lastIndexOf( '/' ) );
411 if ( !resources.contains( parent ) )
412 {
413 addDirectory( resources, jos, parent, time );
414 }
415 }
416
417
418 JarEntry entry = new JarEntry( name + "/" );
419 entry.setTime( time );
420 jos.putNextEntry( entry );
421
422 resources.add( name );
423 }
424
425 private void addRemappedClass( RelocatorRemapper remapper, JarOutputStream jos, File jar, String name,
426 long time, InputStream is )
427 throws IOException, MojoExecutionException
428 {
429 if ( !remapper.hasRelocators() )
430 {
431 try
432 {
433 JarEntry entry = new JarEntry( name );
434 entry.setTime( time );
435 jos.putNextEntry( entry );
436 IOUtil.copy( is, jos );
437 }
438 catch ( ZipException e )
439 {
440 getLogger().debug( "We have a duplicate " + name + " in " + jar );
441 }
442
443 return;
444 }
445
446 ClassReader cr = new ClassReader( is );
447
448
449
450
451
452
453 ClassWriter cw = new ClassWriter( 0 );
454
455 final String pkg = name.substring( 0, name.lastIndexOf( '/' ) + 1 );
456 ClassVisitor cv = new ClassRemapper( cw, remapper )
457 {
458 @Override
459 public void visitSource( final String source, final String debug )
460 {
461 if ( source == null )
462 {
463 super.visitSource( source, debug );
464 }
465 else
466 {
467 final String fqSource = pkg + source;
468 final String mappedSource = remapper.map( fqSource );
469 final String filename = mappedSource.substring( mappedSource.lastIndexOf( '/' ) + 1 );
470 super.visitSource( filename, debug );
471 }
472 }
473 };
474
475 try
476 {
477 cr.accept( cv, ClassReader.EXPAND_FRAMES );
478 }
479 catch ( Throwable ise )
480 {
481 throw new MojoExecutionException( "Error in ASM processing class " + name, ise );
482 }
483
484 byte[] renamedClass = cw.toByteArray();
485
486
487 String mappedName = remapper.map( name.substring( 0, name.indexOf( '.' ) ) );
488
489 try
490 {
491
492 JarEntry entry = new JarEntry( mappedName + ".class" );
493 entry.setTime( time );
494 jos.putNextEntry( entry );
495
496 jos.write( renamedClass );
497 }
498 catch ( ZipException e )
499 {
500 getLogger().debug( "We have a duplicate " + mappedName + " in " + jar );
501 }
502 }
503
504 private boolean isFiltered( List<Filter> filters, String name )
505 {
506 for ( Filter filter : filters )
507 {
508 if ( filter.isFiltered( name ) )
509 {
510 return true;
511 }
512 }
513
514 return false;
515 }
516
517 private boolean resourceTransformed( List<ResourceTransformer> resourceTransformers, String name, InputStream is,
518 List<Relocator> relocators )
519 throws IOException
520 {
521 boolean resourceTransformed = false;
522
523 for ( ResourceTransformer transformer : resourceTransformers )
524 {
525 if ( transformer.canTransformResource( name ) )
526 {
527 getLogger().debug( "Transforming " + name + " using " + transformer.getClass().getName() );
528
529 transformer.processResource( name, is, relocators );
530
531 resourceTransformed = true;
532
533 break;
534 }
535 }
536 return resourceTransformed;
537 }
538
539 private void addJavaSource( Set<String> resources, JarOutputStream jos, String name, long time, InputStream is,
540 List<Relocator> relocators )
541 throws IOException
542 {
543 JarEntry entry = new JarEntry( name );
544 entry.setTime( time );
545 jos.putNextEntry( entry );
546
547 String sourceContent = IOUtil.toString( new InputStreamReader( is, StandardCharsets.UTF_8 ) );
548
549 for ( Relocator relocator : relocators )
550 {
551 sourceContent = relocator.applyToSourceContent( sourceContent );
552 }
553
554 final Writer writer = new OutputStreamWriter( jos, StandardCharsets.UTF_8 );
555 writer.write( sourceContent );
556 writer.flush();
557
558 resources.add( name );
559 }
560
561 private void addResource( Set<String> resources, JarOutputStream jos, String name, long time,
562 InputStream is )
563 throws IOException
564 {
565 final JarEntry entry = new JarEntry( name );
566
567 entry.setTime( time );
568
569 jos.putNextEntry( entry );
570
571 IOUtil.copy( is, jos );
572
573 resources.add( name );
574 }
575
576 static class RelocatorRemapper
577 extends Remapper
578 {
579
580 private final Pattern classPattern = Pattern.compile( "(\\[*)?L(.+);" );
581
582 List<Relocator> relocators;
583
584 RelocatorRemapper( List<Relocator> relocators )
585 {
586 this.relocators = relocators;
587 }
588
589 public boolean hasRelocators()
590 {
591 return !relocators.isEmpty();
592 }
593
594 public Object mapValue( Object object )
595 {
596 if ( object instanceof String )
597 {
598 String name = (String) object;
599 String value = name;
600
601 String prefix = "";
602 String suffix = "";
603
604 Matcher m = classPattern.matcher( name );
605 if ( m.matches() )
606 {
607 prefix = m.group( 1 ) + "L";
608 suffix = ";";
609 name = m.group( 2 );
610 }
611
612 for ( Relocator r : relocators )
613 {
614 if ( r.canRelocateClass( name ) )
615 {
616 value = prefix + r.relocateClass( name ) + suffix;
617 break;
618 }
619 else if ( r.canRelocatePath( name ) )
620 {
621 value = prefix + r.relocatePath( name ) + suffix;
622 break;
623 }
624 }
625
626 return value;
627 }
628
629 return super.mapValue( object );
630 }
631
632 public String map( String name )
633 {
634 String value = name;
635
636 String prefix = "";
637 String suffix = "";
638
639 Matcher m = classPattern.matcher( name );
640 if ( m.matches() )
641 {
642 prefix = m.group( 1 ) + "L";
643 suffix = ";";
644 name = m.group( 2 );
645 }
646
647 for ( Relocator r : relocators )
648 {
649 if ( r.canRelocatePath( name ) )
650 {
651 value = prefix + r.relocatePath( name ) + suffix;
652 break;
653 }
654 }
655
656 return value;
657 }
658
659 }
660
661 }