View Javadoc
1   package org.apache.maven.plugins.shade;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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   * @author Jason van Zyl
76   * @author Mauro Talevi
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             // ASM processes all class files. In doing so, it modifies them, even when not relocating anything.
110             // Before MSHADE-391, the processed files were written to the uber JAR, which did no harm, but made it
111             // difficult to find out by simple file comparison, if a file was actually relocated or not. Now, Shade
112             // makes sure to always write the original file if the class neither was relocated itself nor references
113             // other, relocated classes. So we are checking for regressions here. 
114             assertTrue( areEqual( originalJar, shadedJar,
115                 "org/codehaus/plexus/util/Expand.class" ) );
116 
117             // Relocated files should always be different, because they contain different package names in their byte
118             // code. We should verify this anyway, in order to avoid an existing class file from simply being moved to
119             // another location without actually having been relocated internally.
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 /* 1 | 2 */ , result);
137     }
138 
139     @Test
140     public void testOverlappingResourcesAreLogged() throws IOException, MojoExecutionException {
141         final DefaultShader shader = newShader();
142 
143         // we will shade two jars and expect to see META-INF/MANIFEST.MF overlaps, this will always be true
144         // but this can lead to a broken deployment if intended for OSGi or so, so even this should be logged
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         // FIXME: shaded jar should not include references to org/codehaus/* (empty dirs) or org.codehaus.* META-INF
278         // files.
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           // first, ensure it works:
320           Object o = c.newInstance();
321           assertEquals( "", c.getMethod( "clean", String.class ).invoke( o, (String) null ) );
322 
323           // now, check that its source file was rewritten:
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         //After shading, entry compression method should not be changed.
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 }