8151542: URL resources for multi-release jar files have a #runtime fragment appended to them
authorsdrach
Mon, 02 May 2016 09:03:38 -0700
changeset 37785 daf6dcc73e9d
parent 37784 d1c957a6806f
child 37786 2752c031a620
8151542: URL resources for multi-release jar files have a #runtime fragment appended to them Reviewed-by: alanb, chegar, psandoz, sherman Contributed-by: steve.drach@oracle.com
jdk/src/java.base/share/classes/java/util/jar/JarFile.java
jdk/src/java.base/share/classes/java/util/jar/JavaUtilJarAccessImpl.java
jdk/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java
jdk/src/java.base/share/classes/jdk/internal/misc/JavaUtilJarAccess.java
jdk/test/java/util/jar/JarFile/MultiReleaseJarHttpProperties.java
jdk/test/lib/testlibrary/java/util/jar/SimpleHttpServer.java
jdk/test/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java
--- a/jdk/src/java.base/share/classes/java/util/jar/JarFile.java	Tue May 03 16:17:12 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/util/jar/JarFile.java	Mon May 02 09:03:38 2016 -0700
@@ -658,6 +658,28 @@
         return vze == null ? ze : vze;
     }
 
+    /**
+     * Returns the real name of a {@code JarEntry}.  If this {@code JarFile} is
+     * a multi-release jar file and is configured to be processed as such, the
+     * name returned by this method is the path name of the versioned entry
+     * that the {@code JarEntry} represents, rather than the path name of the
+     * base entry that {@link JarEntry#getName()} returns.  If the
+     * {@code JarEntry} does not represent a versioned entry, or the
+     * jar file is not a multi-release jar file or {@code JarFile} is not
+     * configured for processing a multi-release jar file, this method returns
+     * the same name that {@link JarEntry#getName()} returns.
+     *
+     * @param entry the JarEntry
+     * @return the real name of the JarEntry
+     * @since 9
+     */
+    String getRealName(JarEntry entry) {
+        if (entry instanceof JarFileEntry) {
+            return ((JarFileEntry)entry).realName();
+        }
+        return entry.getName();
+    }
+
     private class JarFileEntry extends JarEntry {
         final private String name;
 
@@ -684,7 +706,7 @@
                 throw new RuntimeException(e);
             }
             if (certs == null && jv != null) {
-                certs = jv.getCerts(JarFile.this, reifiedEntry());
+                certs = jv.getCerts(JarFile.this, realEntry());
             }
             return certs == null ? null : certs.clone();
         }
@@ -695,17 +717,20 @@
                 throw new RuntimeException(e);
             }
             if (signers == null && jv != null) {
-                signers = jv.getCodeSigners(JarFile.this, reifiedEntry());
+                signers = jv.getCodeSigners(JarFile.this, realEntry());
             }
             return signers == null ? null : signers.clone();
         }
-        JarFileEntry reifiedEntry() {
+        JarFileEntry realEntry() {
             if (isMultiRelease()) {
                 String entryName = super.getName();
                 return entryName.equals(this.name) ? this : new JarFileEntry(entryName, this);
             }
             return this;
         }
+        String realName() {
+            return super.getName();
+        }
 
         @Override
         public String getName() {
@@ -876,11 +901,11 @@
     private JarEntry verifiableEntry(ZipEntry ze) {
         if (ze instanceof JarFileEntry) {
             // assure the name and entry match for verification
-            return ((JarFileEntry)ze).reifiedEntry();
+            return ((JarFileEntry)ze).realEntry();
         }
         ze = getJarEntry(ze.getName());
         if (ze instanceof JarFileEntry) {
-            return ((JarFileEntry)ze).reifiedEntry();
+            return ((JarFileEntry)ze).realEntry();
         }
         return (JarEntry)ze;
     }
--- a/jdk/src/java.base/share/classes/java/util/jar/JavaUtilJarAccessImpl.java	Tue May 03 16:17:12 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/util/jar/JavaUtilJarAccessImpl.java	Mon May 02 09:03:38 2016 -0700
@@ -60,4 +60,8 @@
     public List<Object> getManifestDigests(JarFile jar) {
         return jar.getManifestDigests();
     }
+
+    public String getRealName(JarFile jar, JarEntry entry) {
+        return jar.getRealName(entry);
+    }
 }
--- a/jdk/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java	Tue May 03 16:17:12 2016 -0700
+++ b/jdk/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java	Mon May 02 09:03:38 2016 -0700
@@ -372,9 +372,15 @@
             return java.security.AccessController.doPrivileged(
                 new java.security.PrivilegedExceptionAction<>() {
                 public Loader run() throws IOException {
+                    String protocol = url.getProtocol();  // lower cased in URL
                     String file = url.getFile();
-                    if (file != null && file.endsWith("/")) {
-                        if ("file".equals(url.getProtocol())) {
+                    if ("jar".equals(protocol)
+                            && file != null && (file.indexOf("!/") == file.length() - 2)) {
+                        // extract the nested URL
+                        URL nestedUrl = new URL(file.substring(0, file.length() - 2));
+                        return new JarLoader(nestedUrl, jarHandler, lmap);
+                    } else if (file != null && file.endsWith("/")) {
+                        if ("file".equals(protocol)) {
                             return new FileLoader(url);
                         } else {
                             return new Loader(url);
@@ -718,13 +724,13 @@
 
             final URL url;
             try {
+                String nm;
                 if (jar.isMultiRelease()) {
-                    // add #runtime fragment to tell JarURLConnection to use
-                    // runtime versioning if the underlying jar file is multi-release
-                    url = new URL(getBaseURL(), ParseUtil.encodePath(name, false) + "#runtime");
+                    nm = SharedSecrets.javaUtilJarAccess().getRealName(jar, entry);
                 } else {
-                    url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));
+                    nm = name;
                 }
+                url = new URL(getBaseURL(), ParseUtil.encodePath(nm, false));
                 if (check) {
                     URLClassPath.check(url);
                 }
@@ -940,7 +946,8 @@
 
             ensureOpen();
 
-            if (SharedSecrets.javaUtilJarAccess().jarFileHasClassPathAttribute(jar)) { // Only get manifest when necessary
+            // Only get manifest when necessary
+            if (SharedSecrets.javaUtilJarAccess().jarFileHasClassPathAttribute(jar)) {
                 Manifest man = jar.getManifest();
                 if (man != null) {
                     Attributes attr = man.getMainAttributes();
--- a/jdk/src/java.base/share/classes/jdk/internal/misc/JavaUtilJarAccess.java	Tue May 03 16:17:12 2016 -0700
+++ b/jdk/src/java.base/share/classes/jdk/internal/misc/JavaUtilJarAccess.java	Mon May 02 09:03:38 2016 -0700
@@ -41,4 +41,5 @@
     public Enumeration<JarEntry> entries2(JarFile jar);
     public void setEagerValidation(JarFile jar, boolean eager);
     public List<Object> getManifestDigests(JarFile jar);
+    public String getRealName(JarFile jar, JarEntry entry);
 }
--- a/jdk/test/java/util/jar/JarFile/MultiReleaseJarHttpProperties.java	Tue May 03 16:17:12 2016 -0700
+++ b/jdk/test/java/util/jar/JarFile/MultiReleaseJarHttpProperties.java	Mon May 02 09:03:38 2016 -0700
@@ -26,7 +26,7 @@
  * @bug 8132734
  * @summary Test the System properties for JarFile that support multi-release jar files
  * @library /lib/testlibrary/java/util/jar
- * @build Compiler JarBuilder CreateMultiReleaseTestJars
+ * @build Compiler JarBuilder CreateMultiReleaseTestJars SimpleHttpServer
  * @run testng MultiReleaseJarHttpProperties
  * @run testng/othervm -Djdk.util.jar.version=0 MultiReleaseJarHttpProperties
  * @run testng/othervm -Djdk.util.jar.version=8 MultiReleaseJarHttpProperties
@@ -43,8 +43,6 @@
  * @run testng/othervm -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarHttpProperties
  */
 
-import com.sun.net.httpserver.*;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -73,7 +71,7 @@
     @Override
     protected void initializeClassLoader() throws Exception {
         URL[] urls = new URL[]{
-                new URL("http://localhost:" + server.getPort() + "/multi-release-jar")
+                new URL("http://localhost:" + server.getPort() + "/multi-release.jar")
         };
         cldr = new URLClassLoader(urls);
         // load any class, Main is convenient and in the root entries
@@ -112,45 +110,3 @@
         getResource(rootClass, resource);
     }
 }
-
-/**
- * Extremely simple server that only performs one task.  The server listens for
- * requests on the ephemeral port.  If it sees a request that begins with
- * "/multi-release-jar", it consumes the request and returns a stream of bytes
- * representing the jar file multi-release.jar found in "userdir".
- */
-class SimpleHttpServer {
-    private static final String userdir = System.getProperty("user.dir", ".");
-    private static final Path multirelease = Paths.get(userdir, "multi-release.jar");
-
-    private final HttpServer server;
-
-    public SimpleHttpServer() throws IOException {
-        server = HttpServer.create();
-    }
-
-    public void start() throws IOException {
-        server.bind(new InetSocketAddress(0), 0);
-        server.createContext("/multi-release-jar", t -> {
-            try (InputStream is = t.getRequestBody()) {
-                is.readAllBytes();  // probably not necessary to consume request
-                byte[] bytes = Files.readAllBytes(multirelease);
-                t.sendResponseHeaders(200, bytes.length);
-                try (OutputStream os = t.getResponseBody()) {
-                    os.write(bytes);
-                }
-            }
-        });
-        server.setExecutor(null); // creates a default executor
-        server.start();
-    }
-
-    public void stop() {
-        server.stop(0);
-    }
-
-    int getPort() {
-        return server.getAddress().getPort();
-    }
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/lib/testlibrary/java/util/jar/SimpleHttpServer.java	Mon May 02 09:03:38 2016 -0700
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2015, 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.
+ */
+
+import com.sun.net.httpserver.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * Extremely simple server that only performs one task.  The server listens for
+ * requests on the ephemeral port.  If it sees a request that begins with
+ * "/multi-release.jar", it consumes the request and returns a stream of bytes
+ * representing the jar file multi-release.jar found in "userdir".
+ */
+class SimpleHttpServer {
+    private static final String userdir = System.getProperty("user.dir", ".");
+    private static final Path multirelease = Paths.get(userdir, "multi-release.jar");
+
+    private final HttpServer server;
+
+    public SimpleHttpServer() throws IOException {
+        server = HttpServer.create();
+    }
+
+    public void start() throws IOException {
+        server.bind(new InetSocketAddress(0), 0);
+        server.createContext("/multi-release.jar", t -> {
+            try (InputStream is = t.getRequestBody()) {
+                is.readAllBytes();  // probably not necessary to consume request
+                byte[] bytes = Files.readAllBytes(multirelease);
+                t.sendResponseHeaders(200, bytes.length);
+                try (OutputStream os = t.getResponseBody()) {
+                    os.write(bytes);
+                }
+            }
+        });
+        server.setExecutor(null); // creates a default executor
+        server.start();
+    }
+
+    public void stop() {
+        server.stop(0);
+    }
+
+    int getPort() {
+        return server.getAddress().getPort();
+    }
+}
+
--- a/jdk/test/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java	Tue May 03 16:17:12 2016 -0700
+++ b/jdk/test/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java	Mon May 02 09:03:38 2016 -0700
@@ -26,19 +26,25 @@
  * @bug 8132734
  * @summary Test that URL connections to multi-release jars can be runtime versioned
  * @library /lib/testlibrary/java/util/jar
- * @build Compiler JarBuilder CreateMultiReleaseTestJars
+ * @build Compiler JarBuilder CreateMultiReleaseTestJars SimpleHttpServer
  * @run testng MultiReleaseJarURLConnection
  */
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.net.JarURLConnection;
 import java.net.URL;
+import java.net.URLClassLoader;
 import java.net.URLConnection;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.jar.JarFile;
 
+import jdk.Version;
+
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
@@ -47,46 +53,78 @@
 
 public class MultiReleaseJarURLConnection {
     String userdir = System.getProperty("user.dir",".");
-    String file = userdir + "/signed-multi-release.jar";
+    String unversioned = userdir + "/unversioned.jar";
+    String unsigned = userdir + "/multi-release.jar";
+    String signed = userdir + "/signed-multi-release.jar";
+    SimpleHttpServer server;
 
     @BeforeClass
     public void initialize() throws Exception {
         CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars();
         creator.compileEntries();
+        creator.buildUnversionedJar();
         creator.buildMultiReleaseJar();
         creator.buildSignedMultiReleaseJar();
+
+        server = new SimpleHttpServer();
+        server.start();
+
     }
 
     @AfterClass
     public void close() throws IOException {
-        Files.delete(Paths.get(userdir, "multi-release.jar"));
-        Files.delete(Paths.get(userdir, "signed-multi-release.jar"));
+        // Windows requires server to stop before file is deleted
+        if (server != null)
+            server.stop();
+        Files.delete(Paths.get(unversioned));
+        Files.delete(Paths.get(unsigned));
+        Files.delete(Paths.get(signed));
     }
 
     @DataProvider(name = "data")
     public Object[][] createData() {
         return new Object[][]{
-                {"unsigned file", userdir + "/multi-release.jar"},
-                {"signed file", userdir + "/signed-multi-release.jar"},
+                {"unversioned", unversioned},
+                {"unsigned", unsigned},
+                {"signed", signed}
         };
     }
 
     @Test(dataProvider = "data")
-    public void testRuntimeVersioning(String ignore, String file) throws Exception {
+    public void testRuntimeVersioning(String style, String file) throws Exception {
         String urlFile = "jar:file:" + file + "!/";
-        String urlEntry = urlFile + "version/Version.java";
+        String baseUrlEntry = urlFile + "version/Version.java";
+        String rtreturn = "return " + Version.current().major();
+
+        Assert.assertTrue(readAndCompare(new URL(baseUrlEntry), "return 8"));
+        // #runtime is "magic" for a multi-release jar, but not for unversioned jar
+        Assert.assertTrue(readAndCompare(new URL(baseUrlEntry + "#runtime"),
+                style.equals("unversioned") ? "return 8" : rtreturn));
+        // #fragment or any other fragment is not magic
+        Assert.assertTrue(readAndCompare(new URL(baseUrlEntry + "#fragment"), "return 8"));
+        // cached entities not affected
+        Assert.assertTrue(readAndCompare(new URL(baseUrlEntry), "return 8"));
 
-        Assert.assertTrue(readAndCompare(new URL(urlEntry), "return 8"));
-        // #runtime is "magic"
-        Assert.assertTrue(readAndCompare(new URL(urlEntry + "#runtime"), "return 9"));
-        // #fragment or any other fragment is not magic
-        Assert.assertTrue(readAndCompare(new URL(urlEntry + "#fragment"), "return 8"));
-        // cached entities not affected
-        Assert.assertTrue(readAndCompare(new URL(urlEntry), "return 8"));
+        // the following tests will not work with unversioned jars
+        if (style.equals("unversioned")) return;
+
+        // direct access to versioned entry
+        String versUrlEntry = urlFile + "META-INF/versions/" + Version.current().major()
+                + "/version/Version.java";
+        Assert.assertTrue(readAndCompare(new URL(versUrlEntry), rtreturn));
+        // adding any fragment does not change things
+        Assert.assertTrue(readAndCompare(new URL(versUrlEntry + "#runtime"), rtreturn));
+        Assert.assertTrue(readAndCompare(new URL(versUrlEntry + "#fragment"), rtreturn));
+
+        // it really doesn't change things
+        versUrlEntry = urlFile + "META-INF/versions/10/version/Version.java";
+        Assert.assertTrue(readAndCompare(new URL(versUrlEntry), "return 10"));
+        Assert.assertTrue(readAndCompare(new URL(versUrlEntry + "#runtime"), "return 10"));
+        Assert.assertTrue(readAndCompare(new URL(versUrlEntry + "#fragment"), "return 10"));
     }
 
     @Test(dataProvider = "data")
-    public void testCachedJars(String ignore, String file) throws Exception {
+    public void testCachedJars(String style, String file) throws Exception {
         String urlFile = "jar:file:" + file + "!/";
 
         URL rootUrl = new URL(urlFile);
@@ -98,7 +136,11 @@
         juc = (JarURLConnection)runtimeUrl.openConnection();
         JarFile runtimeJar = juc.getJarFile();
         JarFile.Release runtime = runtimeJar.getVersion();
-        Assert.assertNotEquals(root, runtime);
+        if (style.equals("unversioned")) {
+            Assert.assertEquals(root, runtime);
+        } else {
+            Assert.assertNotEquals(root, runtime);
+        }
 
         juc = (JarURLConnection)rootUrl.openConnection();
         JarFile jar = juc.getJarFile();
@@ -115,6 +157,63 @@
         jar.close(); // probably not needed
     }
 
+    @DataProvider(name = "resourcedata")
+    public Object[][] createResourceData() throws Exception {
+        return new Object[][]{
+                {"unversioned", Paths.get(unversioned).toUri().toURL()},
+                {"unsigned", Paths.get(unsigned).toUri().toURL()},
+                {"signed", Paths.get(signed).toUri().toURL()},
+                {"unversioned", new URL("file:" + unversioned)},
+                {"unsigned", new URL("file:" + unsigned)},
+                {"signed", new URL("file:" + signed)},
+                {"unversioned", new URL("jar:file:" + unversioned + "!/")},
+                {"unsigned", new URL("jar:file:" + unsigned + "!/")},
+                {"signed", new URL("jar:file:" + signed + "!/")},
+                // external jar received via http protocol
+                {"http", new URL("jar:http://localhost:" + server.getPort() + "/multi-release.jar!/")},
+                {"http", new URL("http://localhost:" + server.getPort() + "/multi-release.jar")},
+
+        };
+    }
+
+    @Test(dataProvider = "resourcedata")
+    public void testResources(String style, URL url) throws Throwable {
+        //System.out.println("  testing " + style + " url: " + url);
+        URL[] urls = {url};
+        URLClassLoader cldr = new URLClassLoader(urls);
+        Class<?> vcls = cldr.loadClass("version.Version");
+
+        // verify we are loading a runtime versioned class
+        MethodType mt = MethodType.methodType(int.class);
+        MethodHandle mh = MethodHandles.lookup().findVirtual(vcls, "getVersion", mt);
+        Assert.assertEquals((int)mh.invoke(vcls.newInstance()),
+                style.equals("unversioned") ? 8 : Version.current().major());
+
+        // now get a resource and verify that we don't have a fragment attached
+        URL vclsUrl = vcls.getResource("/version/Version.class");
+        String fragment = vclsUrl.getRef();
+        Assert.assertNull(fragment);
+
+        // and verify that the the url is a reified pointer to the runtime entry
+        String rep = vclsUrl.toString();
+        //System.out.println("    getResource(\"/version/Version.class\") returned: " + rep);
+        if (style.equals("http")) {
+            Assert.assertTrue(rep.startsWith("jar:http:"));
+        } else {
+            Assert.assertTrue(rep.startsWith("jar:file:"));
+        }
+        String suffix;
+        if (style.equals("unversioned")) {
+            suffix = ".jar!/version/Version.class";
+        } else {
+            suffix = ".jar!/META-INF/versions/" + Version.current().major()
+                    + "/version/Version.class";
+        }
+        Assert.assertTrue(rep.endsWith(suffix));
+        cldr.close();
+    }
+
+
     private boolean readAndCompare(URL url, String match) throws Exception {
         boolean result;
         // necessary to do it this way, instead of openStream(), so we can