1/*
2 * Copyright (c) 2016, 2017, 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 * @bug 8156499
27 * @summary Test image creation from Multi-Release JAR
28 * @author Steve Drach
29 * @library /test/lib
30 * @modules java.base/jdk.internal.jimage
31 *          java.base/jdk.internal.module
32 * @build jdk.test.lib.Utils
33 *        jdk.test.lib.Asserts
34 *        jdk.test.lib.JDKToolFinder
35 *        jdk.test.lib.JDKToolLauncher
36 *        jdk.test.lib.Platform
37 *        jdk.test.lib.process.*
38 * @run testng JLinkMultiReleaseJarTest
39 */
40
41import java.io.ByteArrayInputStream;
42import java.io.IOException;
43import java.lang.invoke.MethodHandle;
44import java.lang.invoke.MethodHandles;
45import java.lang.invoke.MethodType;
46import java.lang.module.ModuleDescriptor;
47import java.nio.file.Files;
48import java.nio.file.Path;
49import java.nio.file.Paths;
50import java.nio.file.StandardCopyOption;
51import java.util.Arrays;
52import java.util.Set;
53import java.util.jar.JarFile;
54import java.util.spi.ToolProvider;
55import java.util.stream.Collectors;
56import java.util.stream.Stream;
57
58import jdk.internal.jimage.BasicImageReader;
59import jdk.test.lib.process.ProcessTools;
60import jdk.test.lib.process.OutputAnalyzer;
61
62import org.testng.Assert;
63import org.testng.annotations.BeforeClass;
64import org.testng.annotations.Test;
65
66public class JLinkMultiReleaseJarTest {
67    private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
68            .orElseThrow(() -> new RuntimeException("jar tool not found"));
69    private static final ToolProvider JAVAC_TOOL = ToolProvider.findFirst("javac")
70            .orElseThrow(() -> new RuntimeException("javac tool not found"));
71    private static final ToolProvider JLINK_TOOL = ToolProvider.findFirst("jlink")
72            .orElseThrow(() -> new RuntimeException("jlink tool not found"));
73
74    private final Path userdir = Paths.get(System.getProperty("user.dir", "."));
75    private final Path javahome = Paths.get(System.getProperty("java.home"));
76    private final Path jmodsdir = javahome.resolve("jmods");
77
78    private final String pathsep = System.getProperty("path.separator");
79
80    private byte[] resource = (Runtime.version().major() + " resource file").getBytes();
81
82    @BeforeClass
83    public void initialize() throws IOException {
84        Path srcdir = Paths.get(System.getProperty("test.src"));
85
86        // create class files from source
87        Path base = srcdir.resolve("base");
88        Path basemods = userdir.resolve("basemods");
89        javac(base, basemods, base.toString());
90
91        Path rt = srcdir.resolve("rt");
92        Path rtmods = userdir.resolve("rtmods");
93        javac(rt, rtmods, rt.toString());
94
95        // create resources in basemods and rtmods
96        Path dest = basemods.resolve("m1").resolve("resource.txt");
97        byte[] text = "base resource file".getBytes();
98        ByteArrayInputStream is = new ByteArrayInputStream(text);
99        Files.copy(is, dest);
100
101        dest = rtmods.resolve("m1").resolve("resource.txt");
102        is = new ByteArrayInputStream(resource);
103        Files.copy(is, dest);
104
105        // build multi-release jar file with different module-infos
106        String[] args = {
107                "-cf", "m1.jar",
108                "-C", basemods.resolve("m1").toString(), ".",
109                "--release ", String.valueOf(JarFile.runtimeVersion().major()),
110                "-C", rtmods.resolve("m1").toString(), "."
111        };
112        JAR_TOOL.run(System.out, System.err, args);
113
114        // now move the module-info that requires logging to temporary place
115        Files.move(rtmods.resolve("m1").resolve("module-info.class"),
116                userdir.resolve("module-info.class"));
117
118        // and build another jar
119        args[1] = "m1-no-logging.jar";
120        JAR_TOOL.run(System.out, System.err, args);
121
122        // replace the no logging module-info with the logging module-info
123        Files.move(userdir.resolve("module-info.class"),
124                basemods.resolve("m1").resolve("module-info.class"),
125                StandardCopyOption.REPLACE_EXISTING);
126
127        // and build another jar
128        args[1] = "m1-logging.jar";
129        JAR_TOOL.run(System.out, System.err, args);
130    }
131
132    private void javac(Path source, Path destination, String srcpath) throws IOException {
133        String[] args = Stream.concat(
134                Stream.of("-d", destination.toString(), "--module-source-path", srcpath),
135                Files.walk(source)
136                        .map(Path::toString)
137                        .filter(s -> s.endsWith(".java"))
138        ).toArray(String[]::new);
139        int rc = JAVAC_TOOL.run(System.out, System.err, args);
140        Assert.assertEquals(rc, 0);
141    }
142
143    @Test
144    public void basicTest() throws Throwable {
145        if (ignoreTest()) return;
146
147        // use jlink to build image from multi-release jar
148       jlink("m1.jar", "myimage");
149
150        // validate image
151        Path jimage = userdir.resolve("myimage").resolve("lib").resolve("modules");
152        try (BasicImageReader reader = BasicImageReader.open(jimage)) {
153
154            // do we have the right entry names?
155            Set<String> names = Arrays.stream(reader.getEntryNames())
156                    .filter(n -> n.startsWith("/m1"))
157                    .collect(Collectors.toSet());
158            Assert.assertEquals(names, Set.of(
159                    "/m1/module-info.class",
160                    "/m1/p/Main.class",
161                    "/m1/p/Type.class",
162                    "/m1/q/PublicClass.class",
163                    "/m1/META-INF/MANIFEST.MF",
164                    "/m1/resource.txt"));
165
166            // do we have the right module-info.class?
167            byte[] b = reader.getResource("/m1/module-info.class");
168            Set<String> requires = ModuleDescriptor
169                    .read(new ByteArrayInputStream(b))
170                    .requires()
171                    .stream()
172                    .map(mdr -> mdr.name())
173                    .filter(nm -> !nm.equals("java.base"))
174                    .collect(Collectors.toSet());
175            Assert.assertEquals(requires, Set.of("java.logging"));
176
177            // do we have the right resource?
178            b = reader.getResource("/m1/resource.txt");
179            Assert.assertEquals(b, resource);
180
181            // do we have the right class?
182            b = reader.getResource("/m1/p/Main.class");
183            Class<?> clazz = (new ByteArrayClassLoader()).loadClass("p.Main", b);
184            MethodHandle getVersion = MethodHandles.lookup()
185                    .findVirtual(clazz, "getVersion", MethodType.methodType(int.class));
186            int version = (int) getVersion.invoke(clazz.getConstructor().newInstance());
187            Assert.assertEquals(version, JarFile.runtimeVersion().major());
188        }
189    }
190
191    @Test
192    public void noLoggingTest() throws Throwable {
193        if (ignoreTest()) return;
194
195        jlink("m1-no-logging.jar", "no-logging-image");
196        runImage("no-logging-image", false);
197    }
198
199    @Test
200    public void loggingTest() throws Throwable {
201        if (ignoreTest()) return;
202
203        jlink("m1-logging.jar", "logging-image");
204        runImage("logging-image", true);
205
206    }
207
208    // java.base.jmod must exist for this test to make sense
209    private boolean ignoreTest() {
210        if (Files.isRegularFile(jmodsdir.resolve("java.base.jmod"))) {
211            return false;
212        }
213        System.err.println("Test skipped. NO jmods/java.base.jmod");
214        return true;
215    }
216
217
218    private void jlink(String jar, String image) {
219        String args = "--output " + image + " --add-modules m1 --module-path " +
220                jar + pathsep + jmodsdir.toString();
221        int exitCode = JLINK_TOOL.run(System.out, System.err, args.split(" +"));
222        Assert.assertEquals(exitCode, 0);
223    }
224
225    public void runImage(String image, boolean expected) throws Throwable {
226        Path java = Paths.get(image, "bin", "java");
227        OutputAnalyzer oa = ProcessTools.executeProcess(java.toString(), "-m", "m1/p.Main");
228        String sout = oa.getStdout();
229        boolean actual = sout.contains("logging found");
230        Assert.assertEquals(actual, expected);
231        System.out.println(sout);
232        System.err.println(oa.getStderr());
233        Assert.assertEquals(oa.getExitValue(), 0);
234    }
235
236    private static class ByteArrayClassLoader extends ClassLoader {
237        public Class<?> loadClass(String name, byte[] bytes) {
238            return defineClass(name, bytes, 0, bytes.length);
239        }
240    }
241}
242