1 /*
  2  * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /**
 25  * @test
 26  * @enablePreview
 27  * @modules java.base/jdk.internal.module
 28  * @library /test/lib
 29  * @build MultiReleaseJarTest
 30  *        jdk.test.lib.util.JarUtils
 31  *        jdk.test.lib.util.ModuleInfoWriter
 32  * @run testng MultiReleaseJarTest
 33  * @run testng/othervm -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarTest
 34  * @summary Basic test of modular JARs as multi-release JARs
 35  */
 36 
 37 import java.io.File;
 38 import java.io.InputStream;
 39 import java.io.OutputStream;
 40 import java.lang.module.ModuleDescriptor;
 41 import java.lang.module.ModuleFinder;
 42 import java.lang.module.ModuleReader;
 43 import java.lang.module.ModuleReference;
 44 import java.net.URI;
 45 import java.net.URLConnection;
 46 import java.nio.ByteBuffer;
 47 import java.nio.file.Files;
 48 import java.nio.file.Path;
 49 import java.nio.file.Paths;
 50 import java.util.ArrayList;
 51 import java.util.HashMap;
 52 import java.util.HashSet;
 53 import java.util.List;
 54 import java.util.Map;
 55 import java.util.Optional;
 56 import java.util.Set;
 57 import java.util.jar.Attributes;
 58 import java.util.jar.Manifest;
 59 
 60 import jdk.test.lib.util.ModuleInfoWriter;
 61 import jdk.test.lib.util.JarUtils;
 62 
 63 import org.testng.annotations.Test;
 64 import static org.testng.Assert.*;
 65 
 66 
 67 @Test
 68 public class MultiReleaseJarTest {
 69 
 70     private static final String MODULE_INFO = "module-info.class";
 71 
 72     private static final int VERSION = Runtime.version().major();
 73 
 74     // are multi-release JARs enabled?
 75     private static final boolean MULTI_RELEASE;
 76     static {
 77         String s = System.getProperty("jdk.util.jar.enableMultiRelease");
 78         MULTI_RELEASE = (s == null || Boolean.parseBoolean(s));
 79     }
 80 
 81     /**
 82      * Basic test of a multi-release JAR.
 83      */
 84     public void testBasic() throws Exception {
 85         String name = "m1";
 86 
 87         ModuleDescriptor descriptor = ModuleDescriptor.newModule(name)
 88                 .requires("java.base")
 89                 .build();
 90 
 91         Path jar = new JarBuilder(name)
 92                 .moduleInfo("module-info.class", descriptor)
 93                 .resource("p/Main.class")
 94                 .resource("p/Helper.class")
 95                 .resource("META-INF/versions/" + VERSION + "/p/Helper.class")
 96                 .resource("META-INF/versions/" + VERSION + "/p/internal/Helper.class")
 97                 .build();
 98 
 99         // find the module
100         ModuleFinder finder = ModuleFinder.of(jar);
101         Optional<ModuleReference> omref = finder.find(name);
102         assertTrue((omref.isPresent()));
103         ModuleReference mref = omref.get();
104 
105         // check module packages
106         descriptor = mref.descriptor();
107         Set<String> packages = descriptor.packages();
108         assertTrue(packages.contains("p"));
109         if (MULTI_RELEASE) {
110             assertTrue(packages.size() == 2);
111             assertTrue(packages.contains("p.internal"));
112         } else {
113             assertTrue(packages.size() == 1);
114         }
115     }
116 
117     /**
118      * Test a multi-release JAR with a module-info.class in the versioned
119      * section of the JAR.
120      */
121     public void testModuleInfoInVersionedSection() throws Exception {
122         String name = "m1";
123 
124         ModuleDescriptor descriptor1 = ModuleDescriptor.newModule(name)
125                 .requires("java.base")
126                 .build();
127 
128         // module descriptor for versioned section
129         ModuleDescriptor descriptor2 = ModuleDescriptor.newModule(name)
130                 .requires("java.base")
131                 .requires("jdk.unsupported")
132                 .build();
133 
134         Path jar = new JarBuilder(name)
135                 .moduleInfo(MODULE_INFO, descriptor1)
136                 .resource("p/Main.class")
137                 .resource("p/Helper.class")
138                 .moduleInfo("META-INF/versions/" + VERSION + "/" + MODULE_INFO, descriptor2)
139                 .resource("META-INF/versions/" + VERSION + "/p/Helper.class")
140                 .resource("META-INF/versions/" + VERSION + "/p/internal/Helper.class")
141                 .build();
142 
143         // find the module
144         ModuleFinder finder = ModuleFinder.of(jar);
145         Optional<ModuleReference> omref = finder.find(name);
146         assertTrue((omref.isPresent()));
147         ModuleReference mref = omref.get();
148 
149         // ensure that the right module-info.class is loaded
150         ModuleDescriptor descriptor = mref.descriptor();
151         assertEquals(descriptor.name(), name);
152         if (MULTI_RELEASE) {
153             assertEquals(descriptor.requires(), descriptor2.requires());
154         } else {
155             assertEquals(descriptor.requires(), descriptor1.requires());
156         }
157     }
158 
159     /**
160      * Test multi-release JAR as an automatic module.
161      */
162     public void testAutomaticModule() throws Exception {
163         String name = "m";
164 
165         Path jar = new JarBuilder(name)
166                 .resource("p/Main.class")
167                 .resource("p/Helper.class")
168                 .resource("META-INF/versions/" + VERSION + "/p/Helper.class")
169                 .resource("META-INF/versions/" + VERSION + "/p/internal/Helper.class")
170                 .build();
171 
172         // find the module
173         ModuleFinder finder = ModuleFinder.of(jar);
174         Optional<ModuleReference> omref = finder.find(name);
175         assertTrue((omref.isPresent()));
176         ModuleReference mref = omref.get();
177 
178         // check module packages
179         ModuleDescriptor descriptor = mref.descriptor();
180         Set<String> packages = descriptor.packages();
181         if (MULTI_RELEASE) {
182             assertTrue(packages.size() == 2);
183             assertTrue(packages.contains("p.internal"));
184         } else {
185             assertTrue(packages.size() == 1);
186         }
187     }
188 
189     /**
190      * Exercise ModuleReader on a multi-release JAR
191      */
192     public void testModuleReader() throws Exception {
193         String name = "m1";
194 
195         ModuleDescriptor descriptor1 = ModuleDescriptor.newModule(name)
196                 .requires("java.base")
197                 .build();
198 
199         // module descriptor for versioned section
200         ModuleDescriptor descriptor2 = ModuleDescriptor.newModule(name)
201                 .requires("java.base")
202                 .requires("jdk.unsupported")
203                 .build();
204 
205         Path jar = new JarBuilder(name)
206                 .moduleInfo(MODULE_INFO, descriptor1)
207                 .moduleInfo("META-INF/versions/" + VERSION + "/" + MODULE_INFO, descriptor2)
208                 .build();
209 
210         // find the module
211         ModuleFinder finder = ModuleFinder.of(jar);
212         Optional<ModuleReference> omref = finder.find(name);
213         assertTrue((omref.isPresent()));
214         ModuleReference mref = omref.get();
215 
216         ModuleDescriptor expected;
217         if (MULTI_RELEASE) {
218             expected = descriptor2;
219         } else {
220             expected = descriptor1;
221         }
222 
223         // test ModuleReader by reading module-info.class resource
224         try (ModuleReader reader = mref.open()) {
225 
226             // open resource
227             Optional<InputStream> oin = reader.open(MODULE_INFO);
228             assertTrue(oin.isPresent());
229             try (InputStream in = oin.get()) {
230                 checkRequires(ModuleDescriptor.read(in), expected);
231             }
232 
233             // read resource
234             Optional<ByteBuffer> obb = reader.read(MODULE_INFO);
235             assertTrue(obb.isPresent());
236             ByteBuffer bb = obb.get();
237             try {
238                 checkRequires(ModuleDescriptor.read(bb), expected);
239             } finally {
240                 reader.release(bb);
241             }
242 
243             // find resource
244             Optional<URI> ouri = reader.find(MODULE_INFO);
245             assertTrue(ouri.isPresent());
246             URI uri = ouri.get();
247 
248             String expectedTail = "!/";
249             if (MULTI_RELEASE)
250                 expectedTail += "META-INF/versions/" + VERSION + "/";
251             expectedTail += MODULE_INFO;
252             assertTrue(uri.toString().endsWith(expectedTail));
253 
254             URLConnection uc = uri.toURL().openConnection();
255             uc.setUseCaches(false);
256             try (InputStream in = uc.getInputStream()) {
257                 checkRequires(ModuleDescriptor.read(in), expected);
258             }
259 
260         }
261     }
262 
263     /**
264      * Check that two ModuleDescriptor have the same requires
265      */
266     static void checkRequires(ModuleDescriptor md1, ModuleDescriptor md2) {
267         assertEquals(md1.requires(), md2.requires());
268     }
269 
270     /**
271      * A builder of multi-release JAR files.
272      */
273     static class JarBuilder {
274         private String name;
275         private Set<String> resources = new HashSet<>();
276         private Map<String, ModuleDescriptor> descriptors = new HashMap<>();
277 
278         JarBuilder(String name) {
279             this.name = name;
280         }
281 
282         /**
283          * Adds a module-info.class to the JAR file.
284          */
285         JarBuilder moduleInfo(String name, ModuleDescriptor descriptor) {
286             descriptors.put(name, descriptor);
287             return this;
288         }
289 
290         /**
291          * Adds a dummy resource to the JAR file.
292          */
293         JarBuilder resource(String name) {
294             resources.add(name);
295             return this;
296         }
297 
298         /**
299          * Create the multi-release JAR, returning its file path.
300          */
301         Path build() throws Exception {
302             Path dir = Files.createTempDirectory(Paths.get(""), "jar");
303             List<Path> files = new ArrayList<>();
304 
305             // write the module-info.class
306             for (Map.Entry<String, ModuleDescriptor> e : descriptors.entrySet()) {
307                 String name = e.getKey();
308                 ModuleDescriptor descriptor = e.getValue();
309                 Path mi = Paths.get(name.replace('/', File.separatorChar));
310                 Path parent = dir.resolve(mi).getParent();
311                 if (parent != null)
312                     Files.createDirectories(parent);
313                 try (OutputStream out = Files.newOutputStream(dir.resolve(mi))) {
314                     ModuleInfoWriter.write(descriptor, out);
315                 }
316                 files.add(mi);
317             }
318 
319             // write the dummy resources
320             for (String name : resources) {
321                 Path file = Paths.get(name.replace('/', File.separatorChar));
322                 // create dummy resource
323                 Path parent = dir.resolve(file).getParent();
324                 if (parent != null)
325                     Files.createDirectories(parent);
326                 Files.createFile(dir.resolve(file));
327                 files.add(file);
328             }
329 
330             Manifest man = new Manifest();
331             Attributes attrs = man.getMainAttributes();
332             attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
333             attrs.put(Attributes.Name.MULTI_RELEASE, "true");
334 
335             Path jarfile = Paths.get(name + ".jar");
336             JarUtils.createJarFile(jarfile, man, dir, files.toArray(new Path[0]));
337             return jarfile;
338         }
339     }
340 }