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
--- 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