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 8146486 8172432
27 * @summary Fail to create a MR modular JAR with a versioned entry in
28 *          base-versioned empty package
29 * @modules java.base/jdk.internal.module
30 *          jdk.compiler
31 *          jdk.jartool
32 * @library /lib/testlibrary
33 * @build jdk.testlibrary.FileUtils
34 * @run testng Basic
35 */
36
37import org.testng.Assert;
38import org.testng.annotations.AfterClass;
39import org.testng.annotations.Test;
40
41import java.io.ByteArrayInputStream;
42import java.io.ByteArrayOutputStream;
43import java.io.IOException;
44import java.io.PrintStream;
45import java.io.UncheckedIOException;
46import java.lang.module.ModuleDescriptor;
47import java.lang.module.ModuleDescriptor.Version;
48import java.nio.file.Files;
49import java.nio.file.Path;
50import java.nio.file.Paths;
51import java.util.Arrays;
52import java.util.Optional;
53import java.util.Set;
54import java.util.spi.ToolProvider;
55import java.util.stream.Collectors;
56import java.util.stream.Stream;
57import java.util.zip.ZipFile;
58
59import jdk.internal.module.ModuleInfoExtender;
60import jdk.testlibrary.FileUtils;
61
62public class Basic {
63    private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
64           .orElseThrow(() -> new RuntimeException("jar tool not found"));
65    private static final ToolProvider JAVAC_TOOL = ToolProvider.findFirst("javac")
66            .orElseThrow(() -> new RuntimeException("javac tool not found"));
67    private final String linesep = System.lineSeparator();
68    private final Path testsrc;
69    private final Path userdir;
70    private final ByteArrayOutputStream outbytes = new ByteArrayOutputStream();
71    private final PrintStream out = new PrintStream(outbytes, true);
72    private final ByteArrayOutputStream errbytes = new ByteArrayOutputStream();
73    private final PrintStream err = new PrintStream(errbytes, true);
74
75    public Basic() throws IOException {
76        testsrc = Paths.get(System.getProperty("test.src"));
77        userdir = Paths.get(System.getProperty("user.dir", "."));
78
79        // compile the classes directory
80        Path source = testsrc.resolve("src").resolve("classes");
81        Path destination = Paths.get("classes");
82        javac(source, destination);
83
84        // compile the mr9 directory including module-info.java
85        source = testsrc.resolve("src").resolve("mr9");
86        destination = Paths.get("mr9");
87        javac(source, destination);
88
89        // move module-info.class for later use
90        Files.move(destination.resolve("module-info.class"),
91                Paths.get("module-info.class"));
92    }
93
94    private void javac(Path source, Path destination) throws IOException {
95        String[] args = Stream.concat(
96                Stream.of("-d", destination.toString()),
97                Files.walk(source)
98                        .map(Path::toString)
99                        .filter(s -> s.endsWith(".java"))
100        ).toArray(String[]::new);
101        JAVAC_TOOL.run(System.out, System.err, args);
102    }
103
104    private int jar(String cmd) {
105        outbytes.reset();
106        errbytes.reset();
107        return JAR_TOOL.run(out, err, cmd.split(" +"));
108    }
109
110    @AfterClass
111    public void cleanup() throws IOException {
112        Files.walk(userdir, 1)
113                .filter(p -> !p.equals(userdir))
114                .forEach(p -> {
115                    try {
116                        if (Files.isDirectory(p)) {
117                            FileUtils.deleteFileTreeWithRetry(p);
118                        } else {
119                            FileUtils.deleteFileIfExistsWithRetry(p);
120                        }
121                    } catch (IOException x) {
122                        throw new UncheckedIOException(x);
123                    }
124                });
125    }
126
127    // updates a valid multi-release jar with a new public class in
128    // versioned section and fails
129    @Test
130    public void test1() {
131        // successful build of multi-release jar
132        int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 p/Hi.class");
133        Assert.assertEquals(rc, 0);
134
135        jar("-tf mmr.jar");
136
137        Set<String> actual = lines(outbytes);
138        Set<String> expected = Set.of(
139                "META-INF/",
140                "META-INF/MANIFEST.MF",
141                "p/",
142                "p/Hi.class",
143                "META-INF/versions/9/p/Hi.class"
144        );
145        Assert.assertEquals(actual, expected);
146
147        // failed build because of new public class
148        rc = jar("-uf mmr.jar --release 9 -C mr9 p/internal/Bar.class");
149        Assert.assertEquals(rc, 1);
150
151        String s = new String(errbytes.toByteArray());
152        Assert.assertTrue(Message.NOT_FOUND_IN_BASE_ENTRY.match(s, "p/internal/Bar.class"));
153    }
154
155    // updates a valid multi-release jar with a module-info class and new
156    // concealed public class in versioned section and succeeds
157    @Test
158    public void test2() {
159        // successful build of multi-release jar
160        int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 p/Hi.class");
161        Assert.assertEquals(rc, 0);
162
163        // successful build because of module-info and new public class
164        rc = jar("-uf mmr.jar module-info.class --release 9 -C mr9 p/internal/Bar.class");
165        Assert.assertEquals(rc, 0);
166
167        String s = new String(errbytes.toByteArray());
168        Assert.assertTrue(Message.NEW_CONCEALED_PACKAGE_WARNING.match(s, "p/internal/Bar.class"));
169
170        jar("-tf mmr.jar");
171
172        Set<String> actual = lines(outbytes);
173        Set<String> expected = Set.of(
174                "META-INF/",
175                "META-INF/MANIFEST.MF",
176                "p/",
177                "p/Hi.class",
178                "META-INF/versions/9/p/Hi.class",
179                "META-INF/versions/9/p/internal/Bar.class",
180                "module-info.class"
181        );
182        Assert.assertEquals(actual, expected);
183    }
184
185    // jar tool fails building mmr.jar because of new public class
186    @Test
187    public void test3() {
188        int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 .");
189        Assert.assertEquals(rc, 1);
190
191        String s = new String(errbytes.toByteArray());
192        Assert.assertTrue(Message.NOT_FOUND_IN_BASE_ENTRY.match(s, "p/internal/Bar.class"));
193    }
194
195    // jar tool succeeds building mmr.jar because of concealed package
196    @Test
197    public void test4() {
198        int rc = jar("-cf mmr.jar module-info.class -C classes . " +
199                "--release 9 module-info.class -C mr9 .");
200        Assert.assertEquals(rc, 0);
201
202        String s = new String(errbytes.toByteArray());
203        Assert.assertTrue(Message.NEW_CONCEALED_PACKAGE_WARNING.match(s, "p/internal/Bar.class"));
204
205        jar("-tf mmr.jar");
206
207        Set<String> actual = lines(outbytes);
208        Set<String> expected = Set.of(
209                "META-INF/",
210                "META-INF/MANIFEST.MF",
211                "module-info.class",
212                "META-INF/versions/9/module-info.class",
213                "p/",
214                "p/Hi.class",
215                "META-INF/versions/9/",
216                "META-INF/versions/9/p/",
217                "META-INF/versions/9/p/Hi.class",
218                "META-INF/versions/9/p/internal/",
219                "META-INF/versions/9/p/internal/Bar.class"
220        );
221        Assert.assertEquals(actual, expected);
222    }
223
224    // jar tool does two updates, no exported packages, all concealed.
225    // Along with various --describe-module variants
226    @Test
227    public void test5() throws IOException {
228        // compile the mr10 directory
229        Path source = testsrc.resolve("src").resolve("mr10");
230        Path destination = Paths.get("mr10");
231        javac(source, destination);
232
233        // create a directory for this tests special files
234        Files.createDirectory(Paths.get("test5"));
235
236        // create an empty module-info.java
237        String hi = "module hi {" + linesep + "}" + linesep;
238        Path modinfo = Paths.get("test5", "module-info.java");
239        Files.write(modinfo, hi.getBytes());
240
241        // and compile it
242        javac(modinfo, Paths.get("test5"));
243
244        int rc = jar("--create --file mr.jar -C classes .");
245        Assert.assertEquals(rc, 0);
246
247        rc = jar("--update --file mr.jar -C test5 module-info.class"
248                + " --release 9 -C mr9 .");
249        Assert.assertEquals(rc, 0);
250
251        jar("tf mr.jar");
252
253        Set<String> actual = lines(outbytes);
254        Set<String> expected = Set.of(
255                "META-INF/",
256                "META-INF/MANIFEST.MF",
257                "p/",
258                "p/Hi.class",
259                "META-INF/versions/9/",
260                "META-INF/versions/9/p/",
261                "META-INF/versions/9/p/Hi.class",
262                "META-INF/versions/9/p/internal/",
263                "META-INF/versions/9/p/internal/Bar.class",
264                "module-info.class"
265        );
266        Assert.assertEquals(actual, expected);
267
268        jar("-d --file mr.jar");
269
270        String uri = (Paths.get("mr.jar")).toUri().toString();
271        uri = "jar:" + uri + "/!module-info.class";
272
273        actual = lines(outbytes);
274        expected = Set.of(
275                "hi " + uri,
276                "requires java.base mandated",
277                "contains p",
278                "contains p.internal"
279        );
280        Assert.assertEquals(actual, expected);
281
282        rc = jar("--update --file mr.jar --release 10 -C mr10 .");
283        Assert.assertEquals(rc, 0);
284
285        jar("tf mr.jar");
286
287        actual = lines(outbytes);
288        expected = Set.of(
289                "META-INF/",
290                "META-INF/MANIFEST.MF",
291                "p/",
292                "p/Hi.class",
293                "META-INF/versions/9/",
294                "META-INF/versions/9/p/",
295                "META-INF/versions/9/p/Hi.class",
296                "META-INF/versions/9/p/internal/",
297                "META-INF/versions/9/p/internal/Bar.class",
298                "META-INF/versions/10/",
299                "META-INF/versions/10/p/",
300                "META-INF/versions/10/p/internal/",
301                "META-INF/versions/10/p/internal/bar/",
302                "META-INF/versions/10/p/internal/bar/Gee.class",
303                "module-info.class"
304        );
305        Assert.assertEquals(actual, expected);
306
307        jar("-d --file mr.jar");
308
309        actual = lines(outbytes);
310        expected = Set.of(
311                "hi " + uri,
312                "requires java.base mandated",
313                "contains p",
314                "contains p.internal",
315                "contains p.internal.bar"
316        );
317        Assert.assertEquals(actual, expected);
318
319        for (String release : new String[] {"9" , "10", "100", "1000"}) {
320            jar("-d --file mr.jar --release " + release);
321            actual = lines(outbytes);
322            Assert.assertEquals(actual, expected);
323        }
324    }
325
326    // root and versioned module-info entries have different main-class, version
327    // attributes
328    @Test
329    public void test6() throws IOException {
330        // create a directory for this tests special files
331        Files.createDirectory(Paths.get("test6"));
332        Files.createDirectory(Paths.get("test6-v9"));
333
334        // compile the classes directory
335        Path src = testsrc.resolve("src").resolve("classes");
336        Path dst = Paths.get("test6");
337        javac(src, dst);
338
339        byte[] mdBytes = Files.readAllBytes(Paths.get("module-info.class"));
340
341        ModuleInfoExtender mie = ModuleInfoExtender.newExtender(
342            new ByteArrayInputStream(mdBytes));
343
344        mie.mainClass("p.Main");
345        mie.version(Version.parse("1.0"));
346
347        ByteArrayOutputStream baos = new ByteArrayOutputStream();
348        mie.write(baos);
349        Files.write(Paths.get("test6", "module-info.class"), baos.toByteArray());
350        Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray());
351
352        int rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 .");
353        Assert.assertEquals(rc, 0);
354
355
356        // different main-class
357        mie = ModuleInfoExtender.newExtender(new ByteArrayInputStream(mdBytes));
358        mie.mainClass("p.Main2");
359        mie.version(Version.parse("1.0"));
360        baos.reset();
361        mie.write(baos);
362        Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray());
363
364        rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 .");
365        Assert.assertEquals(rc, 1);
366
367        Assert.assertTrue(Message.CONTAINS_DIFFERENT_MAINCLASS.match(
368            new String(errbytes.toByteArray()),
369            "META-INF/versions/9/module-info.class"));
370
371        // different version
372        mie = ModuleInfoExtender.newExtender(new ByteArrayInputStream(mdBytes));
373        mie.mainClass("p.Main");
374        mie.version(Version.parse("2.0"));
375        baos.reset();
376        mie.write(baos);
377        Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray());
378
379        rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 .");
380        Assert.assertEquals(rc, 1);
381
382        Assert.assertTrue(Message.CONTAINS_DIFFERENT_VERSION.match(
383            new String(errbytes.toByteArray()),
384            "META-INF/versions/9/module-info.class"));
385
386    }
387
388    // versioned mmr without root module-info.class
389    @Test
390    public void test7() throws IOException {
391        // create a directory for this tests special files
392        Files.createDirectory(Paths.get("test7"));
393        Files.createDirectory(Paths.get("test7-v9"));
394        Files.createDirectory(Paths.get("test7-v10"));
395
396        // compile the classes directory
397        Path src = testsrc.resolve("src").resolve("classes");
398        Path dst = Paths.get("test7");
399        javac(src, dst);
400
401        // move module-info.class to v9 later use
402        Files.copy(Paths.get("module-info.class"),
403                   Paths.get("test7-v9", "module-info.class"));
404
405        Files.copy(Paths.get("test7-v9", "module-info.class"),
406                   Paths.get("test7-v10", "module-info.class"));
407
408        int rc = jar("--create --file mmr.jar --main-class=p.Main -C test7 . --release 9 -C test7-v9 . --release 10 -C test7-v10 .");
409        Assert.assertEquals(rc, 0);
410
411        jar("-d --file=mmr.jar");
412        Set<String> actual = lines(outbytes);
413        Set<String> expected = Set.of(
414                "releases: 9 10",
415                "No root module descriptor, specify --release"
416        );
417        Assert.assertEquals(actual, expected);
418
419        String uriPrefix = "jar:" + (Paths.get("mmr.jar")).toUri().toString();
420
421        jar("-d --file=mmr.jar --release 9");
422        actual = lines(outbytes);
423        expected = Set.of(
424                "releases: 9 10",
425                "m1 " + uriPrefix + "/!META-INF/versions/9/module-info.class",
426                "requires java.base mandated",
427                "exports p",
428                "main-class p.Main"
429        );
430        Assert.assertEquals(actual, expected);
431
432        jar("-d --file=mmr.jar --release 10");
433        actual = lines(outbytes);
434        expected = Set.of(
435                "releases: 9 10",
436                "m1 " + uriPrefix + "/!META-INF/versions/10/module-info.class",
437                "requires java.base mandated",
438                "exports p",
439                "main-class p.Main"
440        );
441        Assert.assertEquals(actual, expected);
442
443        for (String release : new String[] {"11", "12", "15", "100"}) {
444            jar("-d --file mmr.jar --release " + release);
445            actual = lines(outbytes);
446            Assert.assertEquals(actual, expected);
447        }
448
449        Optional<String> exp = Optional.of("p.Main");
450        try (ZipFile zf = new ZipFile("mmr.jar")) {
451            Assert.assertTrue(zf.getEntry("module-info.class") == null);
452
453            ModuleDescriptor md = ModuleDescriptor.read(
454                zf.getInputStream(zf.getEntry("META-INF/versions/9/module-info.class")));
455            Assert.assertEquals(md.mainClass(), exp);
456
457            md = ModuleDescriptor.read(
458                zf.getInputStream(zf.getEntry("META-INF/versions/10/module-info.class")));
459            Assert.assertEquals(md.mainClass(), exp);
460        }
461    }
462
463    private static Set<String> lines(ByteArrayOutputStream baos) {
464        String s = new String(baos.toByteArray());
465        return Arrays.stream(s.split("\\R"))
466                     .map(l -> l.trim())
467                     .filter(l -> l.length() > 0)
468                     .collect(Collectors.toSet());
469    }
470
471    static enum Message {
472        CONTAINS_DIFFERENT_MAINCLASS(
473          ": module-info.class in a versioned directory contains different \"main-class\""
474        ),
475        CONTAINS_DIFFERENT_VERSION(
476          ": module-info.class in a versioned directory contains different \"version\""
477        ),
478        NOT_FOUND_IN_BASE_ENTRY(
479          ", contains a new public class not found in base entries"
480        ),
481        NEW_CONCEALED_PACKAGE_WARNING(
482            " is a public class" +
483            " in a concealed package, placing this jar on the class path will result" +
484            " in incompatible public interfaces"
485        );
486
487        final String msg;
488        Message(String msg) {
489            this.msg = msg;
490        }
491
492        /*
493         * Test if the given output contains this message ignoring the line break.
494         */
495        boolean match(String output, String entry) {
496            System.out.println("Expected: " + entry + msg);
497            System.out.println("Found: " + output);
498            return Arrays.stream(output.split("\\R"))
499                         .collect(Collectors.joining(" "))
500                         .contains(entry + msg);
501        }
502    }
503}
504