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