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