8165640: Enhance jar tool to allow module-info in versioned directories but not in base in modular multi-release jar files
authorsherman
Sat, 11 Feb 2017 21:31:43 -0800
changeset 43731 bc7110b230c1
parent 43730 b6847c320451
child 43732 358d327a8220
8165640: Enhance jar tool to allow module-info in versioned directories but not in base in modular multi-release jar files Reviewed-by: psandoz, mchung
jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java
jdk/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java
jdk/test/tools/jar/mmrjar/Basic.java
jdk/test/tools/jar/modularJar/Basic.java
jdk/test/tools/jar/modularJar/src/baz/jdk/test/baz/Baz.java
--- a/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Fri Feb 10 21:58:45 2017 +0000
+++ b/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Sat Feb 11 21:31:43 2017 -0800
@@ -158,7 +158,7 @@
     static final String MANIFEST_DIR = "META-INF/";
     static final String VERSIONS_DIR = MANIFEST_DIR + "versions/";
     static final String VERSION = "1.0";
-
+    static final int VERSIONS_DIR_LENGTH = VERSIONS_DIR.length();
     private static ResourceBundle rsrc;
 
     /**
@@ -681,7 +681,7 @@
     void addPackageIfNamed(Set<String> packages, String name) {
         if (name.startsWith(VERSIONS_DIR)) {
             // trim the version dir prefix
-            int i0 = VERSIONS_DIR.length();
+            int i0 = VERSIONS_DIR_LENGTH;
             int i = name.indexOf('/', i0);
             if (i <= 0) {
                 warn(formatMsg("warn.release.unexpected.versioned.entry", name));
@@ -1727,12 +1727,16 @@
     private boolean printModuleDescriptor(ZipFile zipFile)
         throws IOException
     {
-        ZipEntry entry = zipFile.getEntry(MODULE_INFO);
-        if (entry ==  null)
+        ZipEntry[] zes = zipFile.stream()
+            .filter(e -> isModuleInfoEntry(e.getName()))
+            .sorted(Validator.ENTRY_COMPARATOR)
+            .toArray(ZipEntry[]::new);
+        if (zes.length == 0)
             return false;
-
-        try (InputStream is = zipFile.getInputStream(entry)) {
-            printModuleDescriptor(is);
+        for (ZipEntry ze : zes) {
+            try (InputStream is = zipFile.getInputStream(ze)) {
+                printModuleDescriptor(is, ze.getName());
+            }
         }
         return true;
     }
@@ -1742,16 +1746,23 @@
     {
         try (BufferedInputStream bis = new BufferedInputStream(fis);
              ZipInputStream zis = new ZipInputStream(bis)) {
-
             ZipEntry e;
             while ((e = zis.getNextEntry()) != null) {
-                if (e.getName().equals(MODULE_INFO)) {
-                    printModuleDescriptor(zis);
-                    return true;
+                String ename = e.getName();
+                if (isModuleInfoEntry(ename)){
+                    moduleInfos.put(ename, zis.readAllBytes());
                 }
             }
         }
-        return false;
+        if (moduleInfos.size() == 0)
+            return false;
+        String[] names = moduleInfos.keySet().stream()
+            .sorted(Validator.ENTRYNAME_COMPARATOR)
+            .toArray(String[]::new);
+        for (String name : names) {
+            printModuleDescriptor(new ByteArrayInputStream(moduleInfos.get(name)), name);
+        }
+        return true;
     }
 
     static <T> String toString(Collection<T> set) {
@@ -1760,7 +1771,7 @@
                   .collect(joining(" "));
     }
 
-    private void printModuleDescriptor(InputStream entryInputStream)
+    private void printModuleDescriptor(InputStream entryInputStream, String ename)
         throws IOException
     {
         ModuleInfo.Attributes attrs = ModuleInfo.read(entryInputStream, null);
@@ -1768,10 +1779,12 @@
         ModuleHashes hashes = attrs.recordedHashes();
 
         StringBuilder sb = new StringBuilder();
-        sb.append("\n");
+        sb.append("\nmodule ")
+          .append(md.toNameAndVersion())
+          .append(" (").append(ename).append(")");
+
         if (md.isOpen())
-            sb.append("open ");
-        sb.append(md.toNameAndVersion());
+            sb.append("\n  open ");
 
         md.requires().stream()
             .sorted(Comparator.comparing(Requires::name))
@@ -1879,7 +1892,7 @@
             if (end == 0)
                 return true;
             if (name.startsWith(VERSIONS_DIR)) {
-                int off = VERSIONS_DIR.length();
+                int off = VERSIONS_DIR_LENGTH;
                 if (off == end)      // meta-inf/versions/module-info.class
                     return false;
                 while (off < end - 1) {
--- a/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java	Fri Feb 10 21:58:45 2017 +0000
+++ b/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java	Sat Feb 11 21:31:43 2017 -0800
@@ -49,6 +49,7 @@
 
 import static java.util.jar.JarFile.MANIFEST_NAME;
 import static sun.tools.jar.Main.VERSIONS_DIR;
+import static sun.tools.jar.Main.VERSIONS_DIR_LENGTH;
 import static sun.tools.jar.Main.MODULE_INFO;
 import static sun.tools.jar.Main.getMsg;
 import static sun.tools.jar.Main.formatMsg;
@@ -59,19 +60,19 @@
 final class Validator {
     private final static boolean DEBUG = Boolean.getBoolean("jar.debug");
     private final  Map<String,FingerPrint> fps = new HashMap<>();
-    private static final int vdlen = VERSIONS_DIR.length();
     private final Main main;
     private final JarFile jf;
     private int oldVersion = -1;
     private String currentTopLevelName;
     private boolean isValid = true;
-    private Set<String> concealedPkgs;
+    private Set<String> concealedPkgs = Collections.emptySet();
     private ModuleDescriptor md;
+    private String mdName;
 
     private Validator(Main main, JarFile jf) {
         this.main = main;
         this.jf = jf;
-        loadModuleDescriptor();
+        checkModuleDescriptor(MODULE_INFO);
     }
 
     static boolean validate(Main main, JarFile jf) throws IOException {
@@ -83,7 +84,7 @@
             jf.stream()
               .filter(e -> !e.isDirectory() &&
                       !e.getName().equals(MANIFEST_NAME))
-              .sorted(entryComparator)
+              .sorted(ENTRY_COMPARATOR)
               .forEachOrdered(e -> validate(e));
             return isValid;
         } catch (InvalidJarException e) {
@@ -102,9 +103,8 @@
     // sort base entries before versioned entries, and sort entry classes with
     // nested classes so that the top level class appears before the associated
     // nested class
-    private static Comparator<JarEntry> entryComparator = (je1, je2) ->  {
-        String s1 = je1.getName();
-        String s2 = je2.getName();
+    static Comparator<String> ENTRYNAME_COMPARATOR = (s1, s2) ->  {
+
         if (s1.equals(s2)) return 0;
         boolean b1 = s1.startsWith(VERSIONS_DIR);
         boolean b2 = s2.startsWith(VERSIONS_DIR);
@@ -140,6 +140,9 @@
         return l1 - l2;
     };
 
+    static Comparator<ZipEntry> ENTRY_COMPARATOR =
+        Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR);
+
     /*
      *  Validator has state and assumes entries provided to accept are ordered
      *  from base entries first and then through the versioned entries in
@@ -158,24 +161,25 @@
 
         // validate the versioned module-info
         if (isModuleInfoEntry(entryName)) {
-            if (entryName.length() != MODULE_INFO.length())
-                checkModuleDescriptor(je);
+            if (!entryName.equals(mdName))
+                checkModuleDescriptor(entryName);
             return;
         }
 
         // figure out the version and basename from the JarEntry
         int version;
         String basename;
+        String versionStr = null;;
         if (entryName.startsWith(VERSIONS_DIR)) {
-            int n = entryName.indexOf("/", vdlen);
+            int n = entryName.indexOf("/", VERSIONS_DIR_LENGTH);
             if (n == -1) {
                 error(formatMsg("error.validator.version.notnumber", entryName));
                 isValid = false;
                 return;
             }
-            String v = entryName.substring(vdlen, n);
+            versionStr = entryName.substring(VERSIONS_DIR_LENGTH, n);
             try {
-                version = Integer.parseInt(v);
+                version = Integer.parseInt(versionStr);
             } catch (NumberFormatException x) {
                 error(formatMsg("error.validator.version.notnumber", entryName));
                 isValid = false;
@@ -196,6 +200,11 @@
         if (oldVersion != version) {
             oldVersion = version;
             currentTopLevelName = null;
+            if (md == null && versionStr != null) {
+                // don't have a base module-info.class yet, try to see if
+                // a versioned one exists
+                checkModuleDescriptor(VERSIONS_DIR + versionStr + "/" + MODULE_INFO);
+            }
         }
 
         // analyze the entry, keeping key attributes
@@ -308,61 +317,52 @@
         return;
     }
 
-    private void loadModuleDescriptor() {
-        ZipEntry je = jf.getEntry(MODULE_INFO);
-        if (je != null) {
-            try (InputStream jis = jf.getInputStream(je)) {
-                md = ModuleDescriptor.read(jis);
-                concealedPkgs = new HashSet<>(md.packages());
-                md.exports().stream().map(Exports::source).forEach(concealedPkgs::remove);
-                md.opens().stream().map(Opens::source).forEach(concealedPkgs::remove);
-                return;
-            } catch (Exception x) {
-                error(x.getMessage() + " : " + je.getName());
-                this.isValid = false;
-            }
-        }
-        md = null;
-        concealedPkgs = Collections.emptySet();
-    }
-
-    private static boolean isPlatformModule(String name) {
-        return name.startsWith("java.") || name.startsWith("jdk.");
-    }
-
     /**
      * Checks whether or not the given versioned module descriptor's attributes
-     * are valid when compared against the root module descriptor.
+     * are valid when compared against the root/base module descriptor.
      *
-     * A versioned module descriptor must be identical to the root module
+     * A versioned module descriptor must be identical to the root/base module
      * descriptor, with two exceptions:
      *  - A versioned descriptor can have different non-public `requires`
      *    clauses of platform ( `java.*` and `jdk.*` ) modules, and
      *  - A versioned descriptor can have different `uses` clauses, even of
      *    service types defined outside of the platform modules.
      */
-    private void checkModuleDescriptor(JarEntry je) {
-        try (InputStream is = jf.getInputStream(je)) {
-            ModuleDescriptor root = this.md;
-            ModuleDescriptor md = null;
-            try {
-                md = ModuleDescriptor.read(is);
-            } catch (InvalidModuleDescriptorException x) {
-                error(x.getMessage());
-                isValid = false;
-                return;
-            }
-            if (root == null) {
-                this.md = md;
-            } else {
-                if (!root.name().equals(md.name())) {
+    private void checkModuleDescriptor(String miName) {
+        ZipEntry je = jf.getEntry(miName);
+        if (je != null) {
+            try (InputStream jis = jf.getInputStream(je)) {
+                ModuleDescriptor md = ModuleDescriptor.read(jis);
+                // Initialize the base md if it's not yet. A "base" md can be either the
+                // root module-info.class or the first versioned module-info.class
+                ModuleDescriptor base = this.md;
+
+                if (base == null) {
+                    concealedPkgs = new HashSet<>(md.packages());
+                    md.exports().stream().map(Exports::source).forEach(concealedPkgs::remove);
+                    md.opens().stream().map(Opens::source).forEach(concealedPkgs::remove);
+                    // must have the implementation class of the services it 'provides'.
+                    if (md.provides().stream().map(Provides::providers)
+                          .flatMap(List::stream)
+                          .filter(p -> jf.getEntry(toBinaryName(p)) == null)
+                          .peek(p -> error(formatMsg("error.missing.provider", p)))
+                          .count() != 0) {
+                        isValid = false;
+                        return;
+                    }
+                    this.md = md;
+                    this.mdName = miName;
+                    return;
+                }
+
+                if (!base.name().equals(md.name())) {
                     error(getMsg("error.validator.info.name.notequal"));
                     isValid = false;
                 }
-                if (!root.requires().equals(md.requires())) {
-                    Set<Requires> rootRequires = root.requires();
+                if (!base.requires().equals(md.requires())) {
+                    Set<Requires> baseRequires = base.requires();
                     for (Requires r : md.requires()) {
-                        if (rootRequires.contains(r))
+                        if (baseRequires.contains(r))
                             continue;
                         if (r.modifiers().contains(Requires.Modifier.TRANSITIVE)) {
                             error(getMsg("error.validator.info.requires.transitive"));
@@ -372,7 +372,7 @@
                             isValid = false;
                         }
                     }
-                    for (Requires r : rootRequires) {
+                    for (Requires r : baseRequires) {
                         Set<Requires> mdRequires = md.requires();
                         if (mdRequires.contains(r))
                             continue;
@@ -382,33 +382,37 @@
                         }
                     }
                 }
-                if (!root.exports().equals(md.exports())) {
+                if (!base.exports().equals(md.exports())) {
                     error(getMsg("error.validator.info.exports.notequal"));
                     isValid = false;
                 }
-                if (!root.opens().equals(md.opens())) {
+                if (!base.opens().equals(md.opens())) {
                     error(getMsg("error.validator.info.opens.notequal"));
                     isValid = false;
                 }
-                if (!root.provides().equals(md.provides())) {
+                if (!base.provides().equals(md.provides())) {
                     error(getMsg("error.validator.info.provides.notequal"));
                     isValid = false;
                 }
-                if (!root.mainClass().equals(md.mainClass())) {
+                if (!base.mainClass().equals(md.mainClass())) {
                     error(formatMsg("error.validator.info.manclass.notequal", je.getName()));
                     isValid = false;
                 }
-                if (!root.version().equals(md.version())) {
+                if (!base.version().equals(md.version())) {
                     error(formatMsg("error.validator.info.version.notequal", je.getName()));
                     isValid = false;
                 }
+            } catch (Exception x) {
+                error(x.getMessage() + " : " + miName);
+                this.isValid = false;
             }
-        } catch (IOException x) {
-            error(x.getMessage());
-            isValid = false;
         }
     }
 
+    private static boolean isPlatformModule(String name) {
+        return name.startsWith("java.") || name.startsWith("jdk.");
+    }
+
     private boolean checkInternalName(String entryName, String basename, String internalName) {
         String className = className(basename);
         if (internalName.equals(className)) {
--- a/jdk/test/tools/jar/mmrjar/Basic.java	Fri Feb 10 21:58:45 2017 +0000
+++ b/jdk/test/tools/jar/mmrjar/Basic.java	Sat Feb 11 21:31:43 2017 -0800
@@ -268,7 +268,7 @@
 
         actual = lines(outbytes);
         expected = Set.of(
-                "hi",
+                "module hi (module-info.class)",
                 "requires mandated java.base",
                 "contains p",
                 "contains p.internal"
@@ -304,7 +304,7 @@
 
         actual = lines(outbytes);
         expected = Set.of(
-                "hi",
+                "module hi (module-info.class)",
                 "requires mandated java.base",
                 "contains p",
                 "contains p.internal",
@@ -396,18 +396,18 @@
                    Paths.get("test7-v10", "module-info.class"));
 
         int rc = jar("--create --file mmr.jar --main-class=p.Main -C test7 . --release 9 -C test7-v9 . --release 10 -C test7-v10 .");
-
-System.out.println("-----------------------");
-System.out.println( new String(errbytes.toByteArray()));
-
-
         Assert.assertEquals(rc, 0);
 
-
-        jar("-tf mmr.jar");
-
-System.out.println("-----------------------");
-System.out.println( new String(outbytes.toByteArray()));
+        jar("-d --file=mmr.jar");
+        System.out.println("-----------------------");
+        System.out.println( new String(outbytes.toByteArray()));
+        Assert.assertEquals(lines(outbytes),
+                            Set.of(
+                           "module m1 (META-INF/versions/9/module-info.class)",
+                           "module m1 (META-INF/versions/10/module-info.class)",
+                           "requires mandated java.base",
+                           "exports p",
+                           "main-class p.Main"));
 
         Optional<String> exp = Optional.of("p.Main");
         try (ZipFile zf = new ZipFile("mmr.jar")) {
--- a/jdk/test/tools/jar/modularJar/Basic.java	Fri Feb 10 21:58:45 2017 +0000
+++ b/jdk/test/tools/jar/modularJar/Basic.java	Sat Feb 11 21:31:43 2017 -0800
@@ -46,7 +46,7 @@
 
 /*
  * @test
- * @bug 8167328 8171830
+ * @bug 8167328 8171830 8165640
  * @library /lib/testlibrary
  * @modules jdk.compiler
  *          jdk.jartool
@@ -241,11 +241,6 @@
 
         java(mp, FOO.moduleName + "/" + FOO.mainClass)
             .assertSuccess()
-.resultChecker(r -> {
-        System.out.println("===================================");
-        System.out.println(r.output);
-        System.out.println("===================================");
-})
             .resultChecker(r -> assertModuleData(r, FOO));
         try (InputStream fis = Files.newInputStream(modularJar);
              JarInputStream jis = new JarInputStream(fis)) {
@@ -484,7 +479,7 @@
             .resultChecker(r -> {
                 // Expect similar output: "bar, requires mandated foo, ...
                 // conceals jdk.test.foo, conceals jdk.test.foo.internal"
-                Pattern p = Pattern.compile("\\s+bar\\s+requires\\s++foo");
+                Pattern p = Pattern.compile("module bar \\(module-info.class\\)\\s+requires\\s++foo");
                 assertTrue(p.matcher(r.output).find(),
                            "Expecting to find \"bar, requires foo,...\"",
                            "in output, but did not: [" + r.output + "]");
@@ -740,6 +735,53 @@
     }
 
     @Test
+    public void servicesCreateWithoutFailureNonRootMRMJAR() throws IOException {
+        // without a root module-info.class
+        Path mp = Paths.get("servicesCreateWithoutFailureNonRootMRMJAR");
+        createTestDir(mp);
+        Path modClasses = MODULE_CLASSES.resolve("baz");
+        Path mrjarDir = MRJAR_DIR.resolve("baz");
+        Path modularJar = mp.resolve("baz.jar");
+
+        jar("--create",
+            "--file=" + modularJar.toString(),
+            "--main-class=" + "jdk.test.baz.Baz",
+            "-m", mrjarDir.resolve("META-INF/MANIFEST.MF").toRealPath().toString(),
+            "-C", mrjarDir.toString(), "META-INF/versions/9/module-info.class",
+            "-C", modClasses.toString(), "jdk/test/baz/BazService.class",
+            "-C", modClasses.toString(), "jdk/test/baz/Baz.class",
+            "-C", modClasses.toString(), "jdk/test/baz/internal/BazServiceImpl.class")
+            .assertSuccess();
+
+
+        for (String option : new String[]  {"--print-module-descriptor", "-d" }) {
+
+            jar(option,
+                "--file=" + modularJar.toString())
+                .assertSuccess()
+                .resultChecker(r ->
+                    assertTrue(r.output.contains("main-class jdk.test.baz.Baz"),
+                              "Expected to find ", "main-class jdk.test.baz.Baz",
+                               " in [", r.output, "]"));
+
+            jarWithStdin(modularJar.toFile(),  option)
+                .assertSuccess()
+                .resultChecker(r ->
+                    assertTrue(r.output.contains("main-class jdk.test.baz.Baz"),
+                              "Expected to find ", "main-class jdk.test.baz.Baz",
+                               " in [", r.output, "]"));
+
+        }
+        // run module maain class
+        java(mp, "baz/jdk.test.baz.Baz")
+            .assertSuccess()
+            .resultChecker(r ->
+               assertTrue(r.output.contains("mainClass:jdk.test.baz.Baz"),
+                          "Expected to find ", "mainClass:jdk.test.baz.Baz",
+                          " in [", r.output, "]"));
+    }
+
+    @Test
     public void exportCreateWithMissingPkg() throws IOException {
 
         Path foobar = TEST_SRC.resolve("src").resolve("foobar");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/tools/jar/modularJar/src/baz/jdk/test/baz/Baz.java	Sat Feb 11 21:31:43 2017 -0800
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.test.baz;
+
+import java.lang.module.ModuleDescriptor;
+
+public class Baz {
+    public static void main(String[] args) {
+        ModuleDescriptor md = Baz.class.getModule().getDescriptor();
+        System.out.println("nameAndVersion:" + md.toNameAndVersion());
+        md.mainClass().ifPresent(mc -> System.out.println("mainClass:" + mc));
+    }
+}