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