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