View Javadoc
1   /**
2    *
3    * Copyright 2018 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.codehaus.plexus.archiver.jar;
18  
19  import java.io.File;
20  import java.io.InputStream;
21  import java.lang.reflect.Method;
22  import java.nio.file.attribute.FileTime;
23  import java.text.SimpleDateFormat;
24  import java.util.Arrays;
25  import java.util.Enumeration;
26  import java.util.HashSet;
27  import java.util.Set;
28  import java.util.zip.ZipEntry;
29  import java.util.zip.ZipFile;
30  
31  import org.codehaus.plexus.archiver.ArchiverException;
32  import org.junit.jupiter.api.BeforeEach;
33  import org.junit.jupiter.api.Test;
34  import org.junit.jupiter.api.condition.DisabledIf;
35  import org.junit.jupiter.api.condition.EnabledIf;
36  
37  import static org.junit.jupiter.api.Assertions.assertEquals;
38  import static org.junit.jupiter.api.Assertions.assertNotNull;
39  import static org.junit.jupiter.api.Assertions.assertThrows;
40  
41  class JarToolModularJarArchiverTest extends BaseJarArchiverTest {
42  
43      private ModularJarArchiver archiver;
44  
45      /*
46       * Configures the ModularJarArchiver for the test cases.
47       */
48      @BeforeEach
49      void setup() throws Exception {
50          File jarFile = new File("target/output/modular.jar");
51          jarFile.delete();
52  
53          archiver = getJarArchiver();
54          archiver.setDestFile(jarFile);
55          archiver.addDirectory(new File("src/test/resources/java-classes"));
56      }
57  
58      /*
59       * Verify that the main class and the version are properly set for a modular JAR file.
60       */
61      @Test
62      @EnabledIf("modulesAreSupported")
63      void testModularJarWithMainClassAndVersion() throws Exception {
64          archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
65          archiver.setModuleVersion("1.0.0");
66          archiver.setModuleMainClass("com.example.app.Main");
67  
68          archiver.createArchive();
69  
70          // verify that the proper version and main class are set
71          assertModularJarFile(
72                  archiver.getDestFile(), "1.0.0", "com.example.app.Main", "com.example.app", "com.example.resources");
73      }
74  
75      /*
76       * Verify that when both module main class is set and the
77       * manifest contains main class atribute, the manifest
78       * value is overridden
79       */
80      @Test
81      @EnabledIf("modulesAreSupported")
82      void testModularJarWithManifestAndModuleMainClass() throws Exception {
83          archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
84          Manifest manifest = new Manifest();
85          manifest.addConfiguredAttribute(new Manifest.Attribute("Main-Class", "com.example.app.Main2"));
86          archiver.addConfiguredManifest(manifest);
87          archiver.setModuleMainClass("com.example.app.Main");
88  
89          archiver.createArchive();
90  
91          // Verify that the explicitly set module main class
92          // overrides the manifest main
93          assertModularJarFile(
94                  archiver.getDestFile(), null, "com.example.app.Main", "com.example.app", "com.example.resources");
95          assertManifestMainClass(archiver.getDestFile(), "com.example.app.Main");
96      }
97  
98      /**
99       * Verify that when the module main class is not explicitly set,
100      * the manifest main class attribute (if present) is used instead
101      */
102     @Test
103     @EnabledIf("modulesAreSupported")
104     void testModularJarWithManifestMainClassAttribute() throws Exception {
105         archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
106         Manifest manifest = new Manifest();
107         manifest.addConfiguredAttribute(new Manifest.Attribute("Main-Class", "com.example.app.Main2"));
108         archiver.addConfiguredManifest(manifest);
109 
110         archiver.createArchive();
111 
112         // Verify that the the manifest main class attribute is used as module main class
113         assertModularJarFile(
114                 archiver.getDestFile(), null, "com.example.app.Main2", "com.example.app", "com.example.resources");
115         assertManifestMainClass(archiver.getDestFile(), "com.example.app.Main2");
116     }
117 
118     /*
119      * Verify that a modular JAR file is created even when no additional attributes are set.
120      */
121     @Test
122     @EnabledIf("modulesAreSupported")
123     void testModularJar() throws Exception {
124         archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
125         archiver.createArchive();
126 
127         // verify that the proper version and main class are set
128         assertModularJarFile(archiver.getDestFile(), null, null, "com.example.app", "com.example.resources");
129     }
130 
131     /*
132      * Verify that exception is thrown when the modular JAR is not valid.
133      */
134     @Test
135     @EnabledIf("modulesAreSupported")
136     void testInvalidModularJar() throws Exception {
137         archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
138         // Not a valid version
139         archiver.setModuleVersion("notAValidVersion");
140 
141         assertThrows(ArchiverException.class, () -> archiver.createArchive());
142     }
143 
144     /*
145      * Verify that modular JAR files could be created even
146      * if the Java version does not support modules.
147      */
148     @Test
149     @DisabledIf("modulesAreSupported")
150     void testModularJarPriorJava9() throws Exception {
151         archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
152         archiver.setModuleVersion("1.0.0");
153         archiver.setModuleMainClass("com.example.app.Main");
154 
155         archiver.createArchive();
156 
157         // verify that the modular jar is created
158         try (ZipFile resultingArchive = new ZipFile(archiver.getDestFile())) {
159             assertNotNull(resultingArchive.getEntry("module-info.class"));
160         }
161     }
162 
163     /*
164      * Verify that the compression flag is respected.
165      */
166     @Test
167     @EnabledIf("modulesAreSupported")
168     void testNoCompression() throws Exception {
169         archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
170         archiver.setCompress(false);
171 
172         archiver.createArchive();
173 
174         // verify that the entries are not compressed
175         try (ZipFile resultingArchive = new ZipFile(archiver.getDestFile())) {
176             Enumeration<? extends ZipEntry> entries = resultingArchive.entries();
177 
178             while (entries.hasMoreElements()) {
179                 ZipEntry entry = entries.nextElement();
180 
181                 assertEquals(ZipEntry.STORED, entry.getMethod());
182             }
183         }
184     }
185 
186     /*
187      * Verify that the compression set in the "plain" JAR file
188      * is kept after it is updated to modular JAR file.
189      */
190     @Test
191     @EnabledIf("modulesAreSupported")
192     void testCompression() throws Exception {
193         archiver.addDirectory(new File("src/test/resources/java-module-descriptor"));
194         archiver.addFile(new File("src/test/jars/test.jar"), "META-INF/lib/test.jar");
195         archiver.setRecompressAddedZips(false);
196 
197         archiver.createArchive();
198 
199         // verify that the compression is kept
200         try (ZipFile resultingArchive = new ZipFile(archiver.getDestFile())) {
201             Enumeration<? extends ZipEntry> entries = resultingArchive.entries();
202 
203             while (entries.hasMoreElements()) {
204                 ZipEntry entry = entries.nextElement();
205 
206                 int expectedMethod =
207                         entry.isDirectory() || entry.getName().endsWith(".jar") ? ZipEntry.STORED : ZipEntry.DEFLATED;
208                 assertEquals(expectedMethod, entry.getMethod());
209             }
210         }
211     }
212 
213     /*
214      * Verify that a module descriptor in the versioned area is handled correctly.
215      */
216     @Test
217     @EnabledIf("modulesAreSupported")
218     void testModularMultiReleaseJar() throws Exception {
219         // Add two module-info.class, one on the root and one on the multi-release dir.
220         archiver.addFile(
221                 new File("src/test/resources/java-module-descriptor/module-info.class"),
222                 "META-INF/versions/9/module-info.class");
223         archiver.addFile(new File("src/test/resources/java-module-descriptor/module-info.class"), "module-info.class");
224 
225         Manifest manifest = new Manifest();
226         manifest.addConfiguredAttribute(new Manifest.Attribute("Main-Class", "com.example.app.Main2"));
227         manifest.addConfiguredAttribute(new Manifest.Attribute("Multi-Release", "true"));
228         archiver.addConfiguredManifest(manifest);
229 
230         archiver.setModuleVersion("1.0.0");
231         // This attribute overwrites the one from the manifest.
232         archiver.setModuleMainClass("com.example.app.Main");
233 
234         SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
235         long dateTimeMillis = isoFormat.parse("2020-02-29T23:59:59Z").getTime();
236         FileTime lastModTime = FileTime.fromMillis(dateTimeMillis);
237 
238         archiver.configureReproducibleBuild(lastModTime);
239         archiver.createArchive();
240 
241         // Round-down two seconds precision
242         long roundedDown = lastModTime.toMillis() - (lastModTime.toMillis() % 2000);
243         // Normalize to UTC
244         long expectedLastModifiedTime = normalizeLastModifiedTime(roundedDown);
245 
246         // verify that the resulting modular jar has the proper version and main class set
247         try (ZipFile resultingArchive = new ZipFile(archiver.getDestFile())) {
248             ZipEntry moduleDescriptorEntry = resultingArchive.getEntry("META-INF/versions/9/module-info.class");
249             InputStream resultingModuleDescriptor = resultingArchive.getInputStream(moduleDescriptorEntry);
250             assertModuleDescriptor(
251                     resultingModuleDescriptor,
252                     "1.0.0",
253                     "com.example.app.Main",
254                     "com.example.app",
255                     "com.example.resources");
256 
257             ZipEntry rootModuleDescriptorEntry = resultingArchive.getEntry("module-info.class");
258             InputStream rootResultingModuleDescriptor = resultingArchive.getInputStream(rootModuleDescriptorEntry);
259             assertModuleDescriptor(
260                     rootResultingModuleDescriptor,
261                     "1.0.0",
262                     "com.example.app.Main",
263                     "com.example.app",
264                     "com.example.resources");
265 
266             // verify every entry has the correct last modified time
267             Enumeration<? extends ZipEntry> entries = resultingArchive.entries();
268             while (entries.hasMoreElements()) {
269                 ZipEntry element = entries.nextElement();
270                 assertEquals(
271                         expectedLastModifiedTime, element.getTime(), "Last Modified Time does not match with expected");
272                 FileTime expectedFileTime = FileTime.fromMillis(expectedLastModifiedTime);
273                 assertEquals(
274                         expectedFileTime,
275                         element.getLastModifiedTime(),
276                         "Last Modified Time does not match with expected");
277             }
278         }
279     }
280 
281     @Override
282     protected JarToolModularJarArchiver getJarArchiver() {
283         return new JarToolModularJarArchiver();
284     }
285 
286     private void assertModularJarFile(
287             File jarFile, String expectedVersion, String expectedMainClass, String... expectedPackages)
288             throws Exception {
289         try (ZipFile resultingArchive = new ZipFile(jarFile)) {
290             ZipEntry moduleDescriptorEntry = resultingArchive.getEntry("module-info.class");
291             InputStream resultingModuleDescriptor = resultingArchive.getInputStream(moduleDescriptorEntry);
292 
293             assertModuleDescriptor(resultingModuleDescriptor, expectedVersion, expectedMainClass, expectedPackages);
294         }
295     }
296 
297     private void assertModuleDescriptor(
298             InputStream moduleDescriptorInputStream,
299             String expectedVersion,
300             String expectedMainClass,
301             String... expectedPackages)
302             throws Exception {
303         // ModuleDescriptor methods are available from Java 9 so let's get by reflection
304         Class<?> moduleDescriptorClass = Class.forName("java.lang.module.ModuleDescriptor");
305         Class<?> optionalClass = Class.forName("java.util.Optional");
306         Method readMethod = moduleDescriptorClass.getMethod("read", InputStream.class);
307         Method mainClassMethod = moduleDescriptorClass.getMethod("mainClass");
308         Method rawVersionMethod = moduleDescriptorClass.getMethod("rawVersion");
309         Method packagesMethod = moduleDescriptorClass.getMethod("packages");
310         Method isPresentMethod = optionalClass.getMethod("isPresent");
311         Method getMethod = optionalClass.getMethod("get");
312 
313         // Read the module from the input stream
314         Object moduleDescriptor = readMethod.invoke(null, moduleDescriptorInputStream);
315 
316         // Get the module main class
317         Object mainClassOptional = mainClassMethod.invoke(moduleDescriptor);
318         String actualMainClass = null;
319         if ((boolean) isPresentMethod.invoke(mainClassOptional)) {
320             actualMainClass = (String) getMethod.invoke(mainClassOptional);
321         }
322 
323         // Get the module version
324         Object versionOptional = rawVersionMethod.invoke(moduleDescriptor);
325         String actualVersion = null;
326         if ((boolean) isPresentMethod.invoke(versionOptional)) {
327             actualVersion = (String) getMethod.invoke(versionOptional);
328         }
329 
330         // Get the module packages
331         Set<String> actualPackagesSet = (Set<String>) packagesMethod.invoke(moduleDescriptor);
332         Set<String> expectedPackagesSet = new HashSet<>(Arrays.asList(expectedPackages));
333 
334         assertEquals(expectedMainClass, actualMainClass);
335         assertEquals(expectedVersion, actualVersion);
336         assertEquals(expectedPackagesSet, actualPackagesSet);
337     }
338 
339     private void assertManifestMainClass(File jarFile, String expectedMainClass) throws Exception {
340         try (ZipFile resultingArchive = new ZipFile(jarFile)) {
341             ZipEntry manifestEntry = resultingArchive.getEntry("META-INF/MANIFEST.MF");
342             InputStream manifestInputStream = resultingArchive.getInputStream(manifestEntry);
343 
344             // Get the manifest main class attribute
345             Manifest manifest = new Manifest(manifestInputStream);
346             String actualManifestMainClass = manifest.getMainAttributes().getValue("Main-Class");
347 
348             assertEquals(expectedMainClass, actualManifestMainClass);
349         }
350     }
351 
352     /*
353      * Returns true if the current version of Java does support modules.
354      */
355     private boolean modulesAreSupported() {
356         try {
357             Class.forName("java.lang.module.ModuleDescriptor");
358         } catch (ClassNotFoundException e) {
359             return false;
360         }
361 
362         return true;
363     }
364 }