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.BufferedReader;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.lang.reflect.Field;
30 import java.net.URL;
31 import java.net.URLClassLoader;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.Files;
34 import java.nio.file.attribute.FileTime;
35 import java.time.temporal.ChronoUnit;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.LinkedHashSet;
40 import java.util.List;
41 import java.util.Set;
42 import java.util.jar.JarEntry;
43 import java.util.jar.JarFile;
44 import java.util.jar.JarOutputStream;
45 import java.util.stream.Collectors;
46 import java.util.zip.CRC32;
47 import java.util.zip.ZipEntry;
48
49 import org.apache.maven.plugin.MojoExecutionException;
50 import org.apache.maven.plugins.shade.filter.Filter;
51 import org.apache.maven.plugins.shade.relocation.Relocator;
52 import org.apache.maven.plugins.shade.relocation.SimpleRelocator;
53 import org.apache.maven.plugins.shade.resource.AppendingTransformer;
54 import org.apache.maven.plugins.shade.resource.ComponentsXmlResourceTransformer;
55 import org.apache.maven.plugins.shade.resource.ResourceTransformer;
56 import org.apache.maven.plugins.shade.resource.ServicesResourceTransformer;
57 import org.codehaus.plexus.util.IOUtil;
58 import org.codehaus.plexus.util.Os;
59 import org.junit.Assert;
60 import org.junit.Test;
61 import org.junit.rules.TemporaryFolder;
62 import org.mockito.ArgumentCaptor;
63 import org.objectweb.asm.ClassReader;
64 import org.objectweb.asm.ClassVisitor;
65 import org.objectweb.asm.Opcodes;
66 import org.slf4j.Logger;
67
68 import static java.util.Objects.requireNonNull;
69 import static org.hamcrest.CoreMatchers.containsString;
70 import static org.hamcrest.CoreMatchers.hasItem;
71 import static org.hamcrest.CoreMatchers.hasItems;
72 import static org.hamcrest.CoreMatchers.is;
73 import static org.hamcrest.MatcherAssert.assertThat;
74 import static org.junit.Assert.assertEquals;
75 import static org.junit.Assert.assertFalse;
76 import static org.junit.Assert.assertTrue;
77 import static org.mockito.Mockito.doNothing;
78 import static org.mockito.Mockito.mock;
79 import static org.mockito.Mockito.when;
80
81
82
83
84
85 public class DefaultShaderTest
86 {
87 private static final String[] EXCLUDES = new String[] { "org/codehaus/plexus/util/xml/Xpp3Dom",
88 "org/codehaus/plexus/util/xml/pull.*" };
89
90 private final String NEWLINE = "\n";
91
92 @Test
93 public void testNoopWhenNotRelocated() throws IOException, MojoExecutionException {
94 final File plexusJar = new File("src/test/jars/plexus-utils-1.4.1.jar" );
95 final File shadedOutput = new File( "target/foo-custom_testNoopWhenNotRelocated.jar" );
96
97 final Set<File> jars = new LinkedHashSet<>();
98 jars.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
99 jars.add( plexusJar );
100
101 final Relocator relocator = new SimpleRelocator(
102 "org/codehaus/plexus/util/cli", "relocated/plexus/util/cli",
103 Collections.<String>emptyList(), Collections.<String>emptyList() );
104
105 final ShadeRequest shadeRequest = new ShadeRequest();
106 shadeRequest.setJars( jars );
107 shadeRequest.setRelocators( Collections.singletonList( relocator ) );
108 shadeRequest.setResourceTransformers( Collections.<ResourceTransformer>emptyList() );
109 shadeRequest.setFilters( Collections.<Filter>emptyList() );
110 shadeRequest.setUberJar( shadedOutput );
111
112 final DefaultShader shader = newShader();
113 shader.shade( shadeRequest );
114
115 try ( final JarFile originalJar = new JarFile( plexusJar );
116 final JarFile shadedJar = new JarFile( shadedOutput ) )
117 {
118
119
120
121
122
123 assertTrue( areEqual( originalJar, shadedJar,
124 "org/codehaus/plexus/util/Expand.class" ) );
125
126
127
128
129 assertFalse( areEqual(
130 originalJar, shadedJar,
131 "org/codehaus/plexus/util/cli/Arg.class", "relocated/plexus/util/cli/Arg.class" ) );
132 }
133 int result = 0;
134 for ( final String msg : debugMessages.getAllValues() )
135 {
136 if ( "Rewrote class bytecode: org/codehaus/plexus/util/cli/Arg.class".equals(msg) )
137 {
138 result |= 1;
139 }
140 else if ( "Keeping original class bytecode: org/codehaus/plexus/util/Expand.class".equals(msg) )
141 {
142 result |= 2;
143 }
144 }
145 assertEquals( 3 , result);
146 }
147
148 @Test
149 public void testOverlappingResourcesAreLogged() throws IOException, MojoExecutionException {
150 final DefaultShader shader = newShader();
151
152
153
154 final Set<File> set = new LinkedHashSet<>();
155 set.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
156 set.add( new File( "src/test/jars/plexus-utils-1.4.1.jar" ) );
157
158 final ShadeRequest shadeRequest = new ShadeRequest();
159 shadeRequest.setJars( set );
160 shadeRequest.setRelocators( Collections.<Relocator>emptyList() );
161 shadeRequest.setResourceTransformers( Collections.<ResourceTransformer>emptyList() );
162 shadeRequest.setFilters( Collections.<Filter>emptyList() );
163 shadeRequest.setUberJar( new File( "target/foo-custom_testOverlappingResourcesAreLogged.jar" ) );
164 shader.shade( shadeRequest );
165
166 assertThat(warnMessages.getAllValues(),
167 hasItem(containsString("plexus-utils-1.4.1.jar, test-project-1.0-SNAPSHOT.jar define 1 overlapping resource:")));
168 assertThat(warnMessages.getAllValues(),
169 hasItem(containsString("- META-INF/MANIFEST.MF")));
170 if (Os.isFamily(Os.FAMILY_WINDOWS)) {
171 assertThat(debugMessages.getAllValues(),
172 hasItem(containsString("We have a duplicate META-INF/MANIFEST.MF in src\\test\\jars\\plexus-utils-1.4.1.jar")));
173 }
174 else {
175 assertThat(debugMessages.getAllValues(),
176 hasItem(containsString("We have a duplicate META-INF/MANIFEST.MF in src/test/jars/plexus-utils-1.4.1.jar")));
177 }
178 }
179
180 @Test
181 public void testOverlappingResourcesAreLoggedExceptATransformerHandlesIt() throws Exception {
182 TemporaryFolder temporaryFolder = new TemporaryFolder();
183 try {
184 Set<File> set = new LinkedHashSet<>();
185 temporaryFolder.create();
186 File j1 = temporaryFolder.newFile("j1.jar");
187 try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(j1))) {
188 jos.putNextEntry(new JarEntry("foo.txt"));
189 jos.write("c1".getBytes(StandardCharsets.UTF_8));
190 jos.closeEntry();
191 }
192 File j2 = temporaryFolder.newFile("j2.jar");
193 try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(j2))) {
194 jos.putNextEntry(new JarEntry("foo.txt"));
195 jos.write("c2".getBytes(StandardCharsets.UTF_8));
196 jos.closeEntry();
197 }
198 set.add(j1);
199 set.add(j2);
200
201 AppendingTransformer transformer = new AppendingTransformer();
202 Field resource = AppendingTransformer.class.getDeclaredField("resource");
203 resource.setAccessible(true);
204 resource.set(transformer, "foo.txt");
205
206 ShadeRequest shadeRequest = new ShadeRequest();
207 shadeRequest.setJars(set);
208 shadeRequest.setRelocators(Collections.<Relocator>emptyList());
209 shadeRequest.setResourceTransformers(Collections.<ResourceTransformer>singletonList(transformer));
210 shadeRequest.setFilters(Collections.<Filter>emptyList());
211 shadeRequest.setUberJar(new File("target/foo-custom_testOverlappingResourcesAreLogged.jar"));
212
213 DefaultShader shaderWithTransformer = newShader();
214 shaderWithTransformer.shade(shadeRequest);
215
216 assertThat(warnMessages.getAllValues().size(), is(0) );
217
218 DefaultShader shaderWithoutTransformer = newShader();
219 shadeRequest.setResourceTransformers(Collections.<ResourceTransformer>emptyList());
220 shaderWithoutTransformer.shade(shadeRequest);
221
222 assertThat(warnMessages.getAllValues(),
223 hasItems(containsString("j1.jar, j2.jar define 1 overlapping resource:")));
224 assertThat(warnMessages.getAllValues(),
225 hasItems(containsString("- foo.txt")));
226 }
227 finally {
228 temporaryFolder.delete();
229 }
230 }
231
232 @Test
233 public void testShaderWithDefaultShadedPattern()
234 throws Exception
235 {
236 shaderWithPattern( null, new File( "target/foo-default.jar" ), EXCLUDES );
237 }
238
239 @Test
240 public void testShaderWithStaticInitializedClass()
241 throws Exception
242 {
243 Shader s = newShader();
244
245 Set<File> set = new LinkedHashSet<>();
246
247 set.add( new File( "src/test/jars/test-artifact-1.0-SNAPSHOT.jar" ) );
248
249 List<Relocator> relocators = new ArrayList<>();
250
251 relocators.add( new SimpleRelocator( "org.apache.maven.plugins.shade", null, null, null ) );
252
253 List<ResourceTransformer> resourceTransformers = new ArrayList<>();
254
255 List<Filter> filters = new ArrayList<>();
256
257 File file = new File( "target/testShaderWithStaticInitializedClass.jar" );
258
259 ShadeRequest shadeRequest = new ShadeRequest();
260 shadeRequest.setJars( set );
261 shadeRequest.setUberJar( file );
262 shadeRequest.setFilters( filters );
263 shadeRequest.setRelocators( relocators );
264 shadeRequest.setResourceTransformers( resourceTransformers );
265
266 s.shade( shadeRequest );
267
268 try ( URLClassLoader cl = new URLClassLoader( new URL[] { file.toURI().toURL() } ) ) {
269 Class<?> c = cl.loadClass( "hidden.org.apache.maven.plugins.shade.Lib" );
270 Object o = c.newInstance();
271 assertEquals( "foo.bar/baz", c.getDeclaredField( "CONSTANT" ).get( o ) );
272 }
273 }
274
275 @Test
276 public void testShaderWithCustomShadedPattern()
277 throws Exception
278 {
279 shaderWithPattern( "org/shaded/plexus/util", new File( "target/foo-custom.jar" ), EXCLUDES );
280 }
281
282 @Test
283 public void testShaderWithoutExcludesShouldRemoveReferencesOfOriginalPattern()
284 throws Exception
285 {
286
287
288 shaderWithPattern( "org/shaded/plexus/util", new File( "target/foo-custom-without-excludes.jar" ),
289 new String[] {} );
290 }
291
292 @Test
293 public void testShaderWithRelocatedClassname()
294 throws Exception
295 {
296 DefaultShader s = newShader();
297
298 Set<File> set = new LinkedHashSet<>();
299
300 set.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
301
302 set.add( new File( "src/test/jars/plexus-utils-1.4.1.jar" ) );
303
304 List<Relocator> relocators = new ArrayList<>();
305
306 relocators.add( new SimpleRelocator( "org/codehaus/plexus/util/", "_plexus/util/__", null,
307 Collections.<String>emptyList() ) );
308
309 List<ResourceTransformer> resourceTransformers = new ArrayList<>();
310
311 resourceTransformers.add( new ComponentsXmlResourceTransformer() );
312
313 List<Filter> filters = new ArrayList<>();
314
315 File file = new File( "target/foo-relocate-class.jar" );
316
317 ShadeRequest shadeRequest = new ShadeRequest();
318 shadeRequest.setJars( set );
319 shadeRequest.setUberJar( file );
320 shadeRequest.setFilters( filters );
321 shadeRequest.setRelocators( relocators );
322 shadeRequest.setResourceTransformers( resourceTransformers );
323
324 s.shade( shadeRequest );
325
326 try ( URLClassLoader cl = new URLClassLoader( new URL[] { file.toURI().toURL() } ) ) {
327 Class<?> c = cl.loadClass( "_plexus.util.__StringUtils" );
328
329 Object o = c.newInstance();
330 assertEquals( "", c.getMethod( "clean", String.class ).invoke( o, (String) null ) );
331
332
333 final String[] source = { null };
334 final ClassReader classReader = new ClassReader( cl.getResourceAsStream( "_plexus/util/__StringUtils.class" ) );
335 classReader.accept( new ClassVisitor( Opcodes.ASM4 )
336 {
337 @Override
338 public void visitSource( String arg0, String arg1 )
339 {
340 super.visitSource( arg0, arg1 );
341 source[0] = arg0;
342 }
343 }, ClassReader.SKIP_CODE );
344 assertEquals( "__StringUtils.java", source[0] );
345 }
346 }
347
348 @Test
349 public void testShaderWithNestedJar() throws Exception
350 {
351 TemporaryFolder temporaryFolder = new TemporaryFolder();
352
353 final String innerJarFileName = "inner.jar";
354
355 temporaryFolder.create();
356 File innerJar = temporaryFolder.newFile( innerJarFileName );
357 try ( JarOutputStream jos = new JarOutputStream( Files.newOutputStream( innerJar.toPath() ) ) )
358 {
359 jos.putNextEntry( new JarEntry( "foo.txt" ) );
360 jos.write( "c1".getBytes( StandardCharsets.UTF_8 ) );
361 jos.closeEntry();
362 }
363
364 ShadeRequest shadeRequest = new ShadeRequest();
365 shadeRequest.setJars( new LinkedHashSet<>( Collections.singleton( innerJar ) ) );
366 shadeRequest.setFilters( Collections.emptyList() );
367 shadeRequest.setRelocators( Collections.emptyList() );
368 shadeRequest.setResourceTransformers( Collections.emptyList() );
369 File shadedFile = temporaryFolder.newFile( "shaded.jar" );
370 shadeRequest.setUberJar( shadedFile );
371
372 DefaultShader shader = newShader();
373 shader.shade( shadeRequest );
374
375 FileTime lastModified = FileTime.from( Files.getLastModifiedTime( shadedFile.toPath() ).toInstant()
376 .minus( 5, ChronoUnit.SECONDS ) );
377
378 Files.setLastModifiedTime( shadedFile.toPath(), lastModified );
379
380 shader.shade(shadeRequest);
381 assertEquals( lastModified, Files.getLastModifiedTime( shadedFile.toPath() ) );
382
383 temporaryFolder.delete();
384 }
385
386 @Test
387 public void testShaderNoOverwrite() throws Exception
388 {
389 TemporaryFolder temporaryFolder = new TemporaryFolder();
390
391 final String innerJarFileName = "inner.jar";
392
393 temporaryFolder.create();
394 File innerJar = temporaryFolder.newFile( innerJarFileName );
395 try ( JarOutputStream jos = new JarOutputStream( new FileOutputStream( innerJar ) ) )
396 {
397 jos.putNextEntry( new JarEntry( "foo.txt" ) );
398 jos.write( "c1".getBytes( StandardCharsets.UTF_8 ) );
399 jos.closeEntry();
400 }
401
402 File outerJar = temporaryFolder.newFile( "outer.jar" );
403 try ( JarOutputStream jos = new JarOutputStream( new FileOutputStream( outerJar ) ) )
404 {
405 FileInputStream innerStream = new FileInputStream( innerJar );
406 byte[] bytes = IOUtil.toByteArray( innerStream, 32 * 1024 );
407 innerStream.close();
408 writeEntryWithoutCompression( innerJarFileName, bytes, jos );
409 }
410
411
412 ShadeRequest shadeRequest = new ShadeRequest();
413 shadeRequest.setJars( new LinkedHashSet<>( Collections.singleton( outerJar ) ) );
414 shadeRequest.setFilters( new ArrayList<Filter>() );
415 shadeRequest.setRelocators( new ArrayList<Relocator>() );
416 shadeRequest.setResourceTransformers( new ArrayList<ResourceTransformer>() );
417 File shadedFile = temporaryFolder.newFile( "shaded.jar" );
418 shadeRequest.setUberJar( shadedFile );
419
420 DefaultShader shader = newShader();
421 shader.shade( shadeRequest );
422
423 JarFile shadedJarFile = new JarFile( shadedFile );
424 JarEntry entry = shadedJarFile.getJarEntry( innerJarFileName );
425
426
427 Assert.assertEquals( entry.getMethod(), ZipEntry.STORED );
428
429 temporaryFolder.delete();
430 }
431
432 @Test
433 public void testShaderWithDuplicateService() throws Exception
434 {
435 TemporaryFolder temporaryFolder = new TemporaryFolder();
436 temporaryFolder.create();
437
438 String serviceEntryName = "META-INF/services/my.foo.Service";
439 String serviceEntryValue = "my.foo.impl.Service1";
440
441 File innerJar1 = temporaryFolder.newFile( "inner1.jar" );
442 try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream( innerJar1.toPath() ) ) )
443 {
444 jos.putNextEntry( new JarEntry(serviceEntryName) );
445 jos.write( ( serviceEntryValue + NEWLINE ).getBytes( StandardCharsets.UTF_8 ) );
446 jos.closeEntry();
447 }
448
449 File innerJar2 = temporaryFolder.newFile( "inner2.jar" );
450 try ( JarOutputStream jos = new JarOutputStream( Files.newOutputStream( innerJar2.toPath() ) ) )
451 {
452 jos.putNextEntry( new JarEntry(serviceEntryName) );
453 jos.write( ( serviceEntryValue + NEWLINE ).getBytes( StandardCharsets.UTF_8 ) );
454 jos.closeEntry();
455 }
456
457 ShadeRequest shadeRequest = new ShadeRequest();
458 shadeRequest.setJars( new LinkedHashSet<>( Arrays.asList( innerJar1, innerJar2 ) ) );
459 shadeRequest.setFilters( Collections.emptyList() );
460 shadeRequest.setRelocators( Collections.emptyList() );
461 shadeRequest.setResourceTransformers( Collections.singletonList( new ServicesResourceTransformer() ) );
462 File shadedFile = temporaryFolder.newFile( "shaded.jar" );
463 shadeRequest.setUberJar( shadedFile );
464
465 DefaultShader shader = newShader();
466 shader.shade( shadeRequest );
467
468 JarFile shadedJarFile = new JarFile( shadedFile );
469 JarEntry entry = shadedJarFile.getJarEntry(serviceEntryName);
470
471 List<String> lines = new BufferedReader( new InputStreamReader( shadedJarFile.getInputStream( entry ), StandardCharsets.UTF_8 ) )
472 .lines().collect( Collectors.toList() );
473
474
475 Assert.assertEquals( Collections.singletonList( serviceEntryValue ), lines );
476
477 temporaryFolder.delete();
478 }
479
480 @Test
481 public void testShaderWithSmallEntries() throws Exception
482 {
483 TemporaryFolder temporaryFolder = new TemporaryFolder();
484
485 final String innerJarFileName = "inner.jar";
486 int len;
487
488 temporaryFolder.create();
489 File innerJar = temporaryFolder.newFile( innerJarFileName );
490 try ( JarOutputStream jos = new JarOutputStream( new FileOutputStream( innerJar ) ) )
491 {
492 jos.putNextEntry( new JarEntry( "foo.txt" ) );
493 byte[] bytes = "c1".getBytes(StandardCharsets.UTF_8);
494 len = bytes.length;
495 jos.write( bytes );
496 jos.closeEntry();
497 }
498
499 ShadeRequest shadeRequest = new ShadeRequest();
500 shadeRequest.setJars( new LinkedHashSet<>( Collections.singleton( innerJar ) ) );
501 shadeRequest.setFilters( new ArrayList<Filter>() );
502 shadeRequest.setRelocators( new ArrayList<Relocator>() );
503 shadeRequest.setResourceTransformers( new ArrayList<ResourceTransformer>() );
504 File shadedFile = temporaryFolder.newFile( "shaded.jar" );
505 shadeRequest.setUberJar( shadedFile );
506
507 DefaultShader shader = newShader();
508 shader.shade( shadeRequest );
509
510 JarFile shadedJarFile = new JarFile( shadedFile );
511 JarEntry entry = shadedJarFile.getJarEntry( "foo.txt" );
512
513
514 Assert.assertEquals( entry.getSize(), len );
515
516 temporaryFolder.delete();
517 }
518
519 private void writeEntryWithoutCompression( String entryName, byte[] entryBytes, JarOutputStream jos ) throws IOException
520 {
521 final JarEntry entry = new JarEntry( entryName );
522 final int size = entryBytes.length;
523 final CRC32 crc = new CRC32();
524 crc.update( entryBytes, 0, size );
525 entry.setSize( size );
526 entry.setCompressedSize( size );
527 entry.setMethod( ZipEntry.STORED );
528 entry.setCrc( crc.getValue() );
529 jos.putNextEntry( entry );
530 jos.write( entryBytes );
531 jos.closeEntry();
532 }
533
534 private void shaderWithPattern( String shadedPattern, File jar, String[] excludes )
535 throws Exception
536 {
537 DefaultShader s = newShader();
538
539 Set<File> set = new LinkedHashSet<>();
540
541 set.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
542
543 set.add( new File( "src/test/jars/plexus-utils-1.4.1.jar" ) );
544
545 List<Relocator> relocators = new ArrayList<>();
546
547 relocators.add( new SimpleRelocator( "org/codehaus/plexus/util", shadedPattern, null, Arrays.asList( excludes ) ) );
548
549 List<ResourceTransformer> resourceTransformers = new ArrayList<>();
550
551 resourceTransformers.add( new ComponentsXmlResourceTransformer() );
552
553 List<Filter> filters = new ArrayList<>();
554
555 ShadeRequest shadeRequest = new ShadeRequest();
556 shadeRequest.setJars( set );
557 shadeRequest.setUberJar( jar );
558 shadeRequest.setFilters( filters );
559 shadeRequest.setRelocators( relocators );
560 shadeRequest.setResourceTransformers( resourceTransformers );
561
562 s.shade( shadeRequest );
563 }
564
565 private DefaultShader newShader()
566 {
567 return new DefaultShader(mockLogger());
568 }
569
570 private ArgumentCaptor<String> debugMessages;
571
572 private ArgumentCaptor<String> warnMessages;
573
574 private Logger mockLogger()
575 {
576 debugMessages = ArgumentCaptor.forClass(String.class);
577 warnMessages = ArgumentCaptor.forClass(String.class);
578 Logger logger = mock(Logger.class);
579 when(logger.isDebugEnabled()).thenReturn(true);
580 when(logger.isWarnEnabled()).thenReturn(true);
581 doNothing().when(logger).debug(debugMessages.capture());
582 doNothing().when(logger).warn(warnMessages.capture());
583 return logger;
584 }
585
586 private boolean areEqual( final JarFile jar1, final JarFile jar2, final String entry ) throws IOException
587 {
588 return areEqual( jar1, jar2, entry, entry );
589 }
590
591 private boolean areEqual( final JarFile jar1, final JarFile jar2, final String entry1, String entry2 )
592 throws IOException
593 {
594 try ( final InputStream s1 = jar1.getInputStream(
595 requireNonNull(jar1.getJarEntry(entry1), entry1 + " in " + jar1.getName() ) );
596 final InputStream s2 = jar2.getInputStream(
597 requireNonNull(jar2.getJarEntry(entry2), entry2 + " in " + jar2.getName() ) ))
598 {
599 return Arrays.equals( IOUtil.toByteArray( s1 ), IOUtil.toByteArray( s2 ) );
600 }
601 }
602 }