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