# HG changeset patch # User sdrach # Date 1455587273 28800 # Node ID 332b49163fc919fcb3f729d56a9c1009e759bcec # Parent e17994ab030a664fc3ea39e61122156d37d6a6c4 8132734: JDK 9 runtime changes to support multi-release jar files Summary: JEP 238 Multi-Release JAR Files runtime support Reviewed-by: alanb, psandoz, sherman diff -r e17994ab030a -r 332b49163fc9 jdk/src/java.base/share/classes/java/net/JarURLConnection.java --- a/jdk/src/java.base/share/classes/java/net/JarURLConnection.java Mon Feb 15 16:59:56 2016 -0800 +++ b/jdk/src/java.base/share/classes/java/net/JarURLConnection.java Mon Feb 15 17:47:53 2016 -0800 @@ -173,6 +173,14 @@ } jarFileURL = new URL(spec.substring(0, separator++)); + /* + * The url argument may have had a runtime fragment appended, so + * we need to add a runtime fragment to the jarFileURL to enable + * runtime versioning when the underlying jar file is opened. + */ + if ("runtime".equals(url.getRef())) { + jarFileURL = new URL(jarFileURL, "#runtime"); + } entryName = null; /* if ! is the last letter of the innerURL, entryName is null */ diff -r e17994ab030a -r 332b49163fc9 jdk/src/java.base/share/classes/java/util/jar/Attributes.java --- a/jdk/src/java.base/share/classes/java/util/jar/Attributes.java Mon Feb 15 16:59:56 2016 -0800 +++ b/jdk/src/java.base/share/classes/java/util/jar/Attributes.java Mon Feb 15 17:47:53 2016 -0800 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 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 @@ -646,5 +646,13 @@ * manifest attribute used for package versioning. */ public static final Name SPECIFICATION_VENDOR = new Name("Specification-Vendor"); + + /** + * {@code Name} object for {@code Multi-Release} + * manifest attribute that indicates this is a multi-release JAR file. + * + * @since 9 + */ + public static final Name MULTI_RELEASE = new Name("Multi-Release"); } } diff -r e17994ab030a -r 332b49163fc9 jdk/src/java.base/share/classes/java/util/jar/JarFile.java --- a/jdk/src/java.base/share/classes/java/util/jar/JarFile.java Mon Feb 15 16:59:56 2016 -0800 +++ b/jdk/src/java.base/share/classes/java/util/jar/JarFile.java Mon Feb 15 17:47:53 2016 -0800 @@ -28,6 +28,7 @@ import java.io.*; import java.lang.ref.SoftReference; import java.net.URL; +import java.security.PrivilegedAction; import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -37,28 +38,91 @@ import java.security.AccessController; import java.security.CodeSource; import jdk.internal.misc.SharedSecrets; -import sun.security.action.GetPropertyAction; import sun.security.util.ManifestEntryVerifier; import sun.security.util.SignatureFileVerifier; +import static java.util.jar.Attributes.Name.MULTI_RELEASE; + /** * The {@code JarFile} class is used to read the contents of a jar file * from any file that can be opened with {@code java.io.RandomAccessFile}. * It extends the class {@code java.util.zip.ZipFile} with support - * for reading an optional {@code Manifest} entry. The - * {@code Manifest} can be used to specify meta-information about the - * jar file and its entries. + * for reading an optional {@code Manifest} entry, and support for + * processing multi-release jar files. The {@code Manifest} can be used + * to specify meta-information about the jar file and its entries. + * + *

A multi-release jar file is a jar file that contains + * a manifest with a main attribute named "Multi-Release", + * a set of "base" entries, some of which are public classes with public + * or protected methods that comprise the public interface of the jar file, + * and a set of "versioned" entries contained in subdirectories of the + * "META-INF/versions" directory. The versioned entries are partitioned by the + * major version of the Java platform. A versioned entry, with a version + * {@code n}, {@code 8 < n}, in the "META-INF/versions/{n}" directory overrides + * the base entry as well as any entry with a version number {@code i} where + * {@code 8 < i < n}. + * + *

By default, a {@code JarFile} for a multi-release jar file is configured + * to process the multi-release jar file as if it were a plain (unversioned) jar + * file, and as such an entry name is associated with at most one base entry. + * The {@code JarFile} may be configured to process a multi-release jar file by + * creating the {@code JarFile} with the + * {@link JarFile#JarFile(File, boolean, int, Release)} constructor. The + * {@code Release} object sets a maximum version used when searching for + * versioned entries. When so configured, an entry name + * can correspond with at most one base entry and zero or more versioned + * entries. A search is required to associate the entry name with the latest + * versioned entry whose version is less than or equal to the maximum version + * (see {@link #getEntry(String)}). + * + *

Class loaders that utilize {@code JarFile} to load classes from the + * contents of {@code JarFile} entries should construct the {@code JarFile} + * by invoking the {@link JarFile#JarFile(File, boolean, int, Release)} + * constructor with the value {@code Release.RUNTIME} assigned to the last + * argument. This assures that classes compatible with the major + * version of the running JVM are loaded from multi-release jar files. + * + *

If the verify flag is on when opening a signed jar file, the content of + * the file is verified against its signature embedded inside the file. Please + * note that the verification process does not include validating the signer's + * certificate. A caller should inspect the return value of + * {@link JarEntry#getCodeSigners()} to further determine if the signature + * can be trusted. * *

Unless otherwise noted, passing a {@code null} argument to a constructor * or method in this class will cause a {@link NullPointerException} to be * thrown. * - * If the verify flag is on when opening a signed jar file, the content of the - * file is verified against its signature embedded inside the file. Please note - * that the verification process does not include validating the signer's - * certificate. A caller should inspect the return value of - * {@link JarEntry#getCodeSigners()} to further determine if the signature - * can be trusted. + * @implNote + *

+ * If the API can not be used to configure a {@code JarFile} (e.g. to override + * the configuration of a compiled application or library), two {@code System} + * properties are available. + * + *
* * @author David Connelly * @see Manifest @@ -68,26 +132,126 @@ */ public class JarFile extends ZipFile { + private final static int BASE_VERSION; + private final static int RUNTIME_VERSION; + private final static boolean MULTI_RELEASE_ENABLED; + private final static boolean MULTI_RELEASE_FORCED; private SoftReference manRef; private JarEntry manEntry; private JarVerifier jv; private boolean jvInitialized; private boolean verify; + private final int version; + private boolean notVersioned; + private final boolean runtimeVersioned; // indicates if Class-Path attribute present (only valid if hasCheckedSpecialAttributes true) private boolean hasClassPathAttribute; // true if manifest checked for special attributes private volatile boolean hasCheckedSpecialAttributes; - // Set up JavaUtilJarAccess in SharedSecrets static { + // Set up JavaUtilJarAccess in SharedSecrets SharedSecrets.setJavaUtilJarAccess(new JavaUtilJarAccessImpl()); + + BASE_VERSION = 8; // one less than lowest version for versioned entries + RUNTIME_VERSION = AccessController.doPrivileged( + new PrivilegedAction() { + public Integer run() { + Integer v = sun.misc.Version.jdkMajorVersion(); // fixme when JEP 223 Version integrated + Integer i = Integer.getInteger("jdk.util.jar.version", v); + i = i < 0 ? 0 : i; + return i > v ? v : i; + } + } + ); + String multi_release = AccessController.doPrivileged( + new PrivilegedAction() { + public String run() { + return System.getProperty("jdk.util.jar.enableMultiRelease", "true"); + } + } + ); + switch (multi_release) { + case "true": + default: + MULTI_RELEASE_ENABLED = true; + MULTI_RELEASE_FORCED = false; + break; + case "false": + MULTI_RELEASE_ENABLED = false; + MULTI_RELEASE_FORCED = false; + break; + case "force": + MULTI_RELEASE_ENABLED = true; + MULTI_RELEASE_FORCED = true; + break; + } } /** + * A set of constants that represent the entries in either the base directory + * or one of the versioned directories in a multi-release jar file. It's + * possible for a multi-release jar file to contain versioned directories + * that are not represented by the constants of the {@code Release} enum. + * In those cases, the entries will not be located by this {@code JarFile} + * through the aliasing mechanism, but they can be directly accessed by + * specifying the full path name of the entry. + * + * @since 9 + */ + public enum Release { + /** + * Represents unversioned entries, or entries in "regular", as opposed + * to multi-release jar files. + */ + BASE(BASE_VERSION), + + /** + * Represents entries found in the META-INF/versions/9 directory of a + * multi-release jar file. + */ + VERSION_9(9), + + // fill in the "blanks" for future releases + + /** + * Represents entries found in the META-INF/versions/{n} directory of a + * multi-release jar file, where {@code n} is the effective runtime + * version of the jar file. + * + * @implNote + *
+ * The effective runtime version is determined + * by evaluating {@code Version.current().major()} or by using the value + * of the {@code jdk.util.jar.version} System property if it exists. + *
+ */ + RUNTIME(RUNTIME_VERSION); + + Release(int version) { + this.version = version; + } + + private static Release valueOf(int version) { + return version <= BASE.value() ? BASE : valueOf("VERSION_" + version); + } + + private final int version; + + private int value() { + return this.version; + } + } + + private static final String META_INF = "META-INF/"; + + private static final String META_INF_VERSIONS = META_INF + "versions/"; + + /** * The JAR manifest file name. */ - public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; + public static final String MANIFEST_NAME = META_INF + "MANIFEST.MF"; /** * Creates a new {@code JarFile} to read from the specified @@ -129,7 +293,6 @@ this(file, true, ZipFile.OPEN_READ); } - /** * Creates a new {@code JarFile} to read from the specified * {@code File} object. @@ -144,7 +307,6 @@ this(file, verify, ZipFile.OPEN_READ); } - /** * Creates a new {@code JarFile} to read from the specified * {@code File} object in the specified mode. The mode argument @@ -162,10 +324,104 @@ * @since 1.3 */ public JarFile(File file, boolean verify, int mode) throws IOException { + this(file, verify, mode, Release.BASE); + this.notVersioned = true; + } + + /** + * Creates a new {@code JarFile} to read from the specified + * {@code File} object in the specified mode. The mode argument + * must be either {@code OPEN_READ} or {@code OPEN_READ | OPEN_DELETE}. + * The version argument configures the {@code JarFile} for processing + * multi-release jar files. + * + * @param file the jar file to be opened for reading + * @param verify whether or not to verify the jar file if + * it is signed. + * @param mode the mode in which the file is to be opened + * @param version specifies the release version for a multi-release jar file + * @throws IOException if an I/O error has occurred + * @throws IllegalArgumentException + * if the {@code mode} argument is invalid + * @throws SecurityException if access to the file is denied + * by the SecurityManager + * @throws NullPointerException if {@code version} is {@code null} + * @since 9 + */ + public JarFile(File file, boolean verify, int mode, Release version) throws IOException { super(file, mode); + Objects.requireNonNull(version); this.verify = verify; + // version applies to multi-release jar files, ignored for regular jar files + this.version = MULTI_RELEASE_FORCED ? RUNTIME_VERSION : version.value(); + this.runtimeVersioned = version == Release.RUNTIME; + assert runtimeVersionExists(); } + private boolean runtimeVersionExists() { + int version = sun.misc.Version.jdkMajorVersion(); // fixme when JEP 223 integrated + try { + Release.valueOf(version); + return true; + } catch (IllegalArgumentException x) { + System.err.println("No JarFile.Release object for release " + version); + return false; + } + } + + /** + * Returns the maximum version used when searching for versioned entries. + * + * @return the maximum version, or {@code Release.BASE} if this jar file is + * processed as if it is an unversioned jar file or is not a + * multi-release jar file + * @since 9 + */ + public final Release getVersion() { + if (isMultiRelease()) { + return runtimeVersioned ? Release.RUNTIME : Release.valueOf(version); + } else { + return Release.BASE; + } + } + + /** + * Indicates whether or not this jar file is a multi-release jar file. + * + * @return true if this JarFile is a multi-release jar file + * @since 9 + */ + public final boolean isMultiRelease() { + // do not call this code in a constructor because some subclasses use + // lazy loading of manifest so it won't be available at construction time + if (MULTI_RELEASE_ENABLED) { + // Doubled-checked locking pattern + Boolean result = isMultiRelease; + if (result == null) { + synchronized (this) { + result = isMultiRelease; + if (result == null) { + Manifest man = null; + try { + man = getManifest(); + } catch (IOException e) { + //Ignored, manifest cannot be read + } + isMultiRelease = result = (man != null) + && man.getMainAttributes().containsKey(MULTI_RELEASE) + ? Boolean.TRUE : Boolean.FALSE; + } + } + } + return result == Boolean.TRUE; + } else { + return false; + } + } + // the following field, isMultiRelease, should only be used in the method + // isMultiRelease(), like a static local + private volatile Boolean isMultiRelease; // is jar multi-release? + /** * Returns the jar file manifest, or {@code null} if none. * @@ -209,40 +465,87 @@ } /** - * Returns the {@code JarEntry} for the given entry name or + * Returns the {@code JarEntry} for the given base entry name or * {@code null} if not found. * + *

If this {@code JarFile} is a multi-release jar file and is configured + * to be processed as such, then a search is performed to find and return + * a {@code JarEntry} that is the latest versioned entry associated with the + * given entry name. The returned {@code JarEntry} is the versioned entry + * corresponding to the given base entry name prefixed with the string + * {@code "META-INF/versions/{n}/"}, for the largest value of {@code n} for + * which an entry exists. If such a versioned entry does not exist, then + * the {@code JarEntry} for the base entry is returned, otherwise + * {@code null} is returned if no entries are found. The initial value for + * the version {@code n} is the maximum version as returned by the method + * {@link JarFile#getVersion()}. + * * @param name the jar file entry name - * @return the {@code JarEntry} for the given entry name or - * {@code null} if not found. + * @return the {@code JarEntry} for the given entry name, or + * the versioned entry name, or {@code null} if not found * * @throws IllegalStateException * may be thrown if the jar file has been closed * * @see java.util.jar.JarEntry + * + * @implSpec + *

+ * This implementation invokes {@link JarFile#getEntry(String)}. + *
*/ public JarEntry getJarEntry(String name) { return (JarEntry)getEntry(name); } /** - * Returns the {@code ZipEntry} for the given entry name or + * Returns the {@code ZipEntry} for the given base entry name or * {@code null} if not found. * + *

If this {@code JarFile} is a multi-release jar file and is configured + * to be processed as such, then a search is performed to find and return + * a {@code ZipEntry} that is the latest versioned entry associated with the + * given entry name. The returned {@code ZipEntry} is the versioned entry + * corresponding to the given base entry name prefixed with the string + * {@code "META-INF/versions/{n}/"}, for the largest value of {@code n} for + * which an entry exists. If such a versioned entry does not exist, then + * the {@code ZipEntry} for the base entry is returned, otherwise + * {@code null} is returned if no entries are found. The initial value for + * the version {@code n} is the maximum version as returned by the method + * {@link JarFile#getVersion()}. + * * @param name the jar file entry name * @return the {@code ZipEntry} for the given entry name or - * {@code null} if not found + * the versioned entry name or {@code null} if not found * * @throws IllegalStateException * may be thrown if the jar file has been closed * * @see java.util.zip.ZipEntry + * + * @implSpec + *

+ * This implementation may return a versioned entry for the requested name + * even if there is not a corresponding base entry. This can occur + * if there is a private or package-private versioned entry that matches. + * If a subclass overrides this method, assure that the override method + * invokes {@code super.getEntry(name)} to obtain all versioned entries. + *
*/ public ZipEntry getEntry(String name) { ZipEntry ze = super.getEntry(name); if (ze != null) { return new JarFileEntry(ze); } + // no matching base entry, but maybe there is a versioned entry, + // like a new private class + if (isMultiRelease()) { + ze = new ZipEntry(name); + ZipEntry vze = getVersionedEntry(ze); + if (ze != vze) { + return new JarFileEntry(name, vze); + } + } return null; } @@ -250,14 +553,42 @@ Iterator { final Enumeration e = JarFile.super.entries(); + ZipEntry ze; public boolean hasNext() { - return e.hasMoreElements(); + if (notVersioned) { + return e.hasMoreElements(); + } + if (ze != null) { + return true; + } + return findNext(); + } + + private boolean findNext() { + while (e.hasMoreElements()) { + ZipEntry ze2 = e.nextElement(); + if (!ze2.getName().startsWith(META_INF_VERSIONS)) { + ze = ze2; + return true; + } + } + return false; } public JarEntry next() { - ZipEntry ze = e.nextElement(); - return new JarFileEntry(ze); + ZipEntry ze2; + + if (notVersioned) { + ze2 = e.nextElement(); + return new JarFileEntry(ze2.getName(), ze2); + } + if (ze != null || findNext()) { + ze2 = ze; + ze = null; + return new JarFileEntry(ze2); + } + throw new NoSuchElementException(); } public boolean hasMoreElements() { @@ -274,7 +605,19 @@ } /** - * Returns an enumeration of the jar file entries. + * Returns an enumeration of the jar file entries. The set of entries + * returned depends on whether or not the jar file is a multi-release jar + * file, and on the constructor used to create the {@code JarFile}. If the + * jar file is not a multi-release jar file, all entries are returned, + * regardless of how the {@code JarFile} is created. If the constructor + * does not take a {@code Release} argument, all entries are returned. + * If the jar file is a multi-release jar file and the constructor takes a + * {@code Release} argument, then the set of entries returned is equivalent + * to the set of entries that would be returned if the set was built by + * invoking {@link JarFile#getEntry(String)} or + * {@link JarFile#getJarEntry(String)} with the name of each base entry in + * the jar file. A base entry is an entry whose path name does not start + * with "META-INF/versions/". * * @return an enumeration of the jar file entries * @throws IllegalStateException @@ -285,10 +628,21 @@ } /** - * Returns an ordered {@code Stream} over the jar file entries. + * Returns an ordered {@code Stream} over all the jar file entries. * Entries appear in the {@code Stream} in the order they appear in - * the central directory of the jar file. - * + * the central directory of the jar file. The set of entries + * returned depends on whether or not the jar file is a multi-release jar + * file, and on the constructor used to create the {@code JarFile}. If the + * jar file is not a multi-release jar file, all entries are returned, + * regardless of how the {@code JarFile} is created. If the constructor + * does not take a {@code Release} argument, all entries are returned. + * If the jar file is a multi-release jar file and the constructor takes a + * {@code Release} argument, then the set of entries returned is equivalent + * to the set of entries that would be returned if the set was built by + * invoking {@link JarFile#getEntry(String)} or + * {@link JarFile#getJarEntry(String)} with the name of each base entry in + * the jar file. A base entry is an entry whose path name does not start + * with "META-INF/versions/". * @return an ordered {@code Stream} of entries in this jar file * @throws IllegalStateException if the jar file has been closed * @since 1.8 @@ -300,14 +654,44 @@ Spliterator.IMMUTABLE | Spliterator.NONNULL), false); } + private ZipEntry searchForVersionedEntry(final int version, String name) { + ZipEntry vze = null; + String sname = "/" + name; + int i = version; + while (i > BASE_VERSION) { + vze = super.getEntry(META_INF_VERSIONS + i + sname); + if (vze != null) break; + i--; + } + return vze; + } + + private ZipEntry getVersionedEntry(ZipEntry ze) { + ZipEntry vze = null; + if (version > BASE_VERSION && !ze.isDirectory()) { + String name = ze.getName(); + if (!name.startsWith(META_INF)) { + vze = searchForVersionedEntry(version, name); + } + } + return vze == null ? ze : vze; + } + private class JarFileEntry extends JarEntry { + final private String name; + JarFileEntry(ZipEntry ze) { - super(ze); + super(isMultiRelease() ? getVersionedEntry(ze) : ze); + this.name = ze.getName(); + } + JarFileEntry(String name, ZipEntry vze) { + super(vze); + this.name = name; } public Attributes getAttributes() throws IOException { Manifest man = JarFile.this.getManifest(); if (man != null) { - return man.getAttributes(getName()); + return man.getAttributes(super.getName()); } else { return null; } @@ -319,7 +703,7 @@ throw new RuntimeException(e); } if (certs == null && jv != null) { - certs = jv.getCerts(JarFile.this, this); + certs = jv.getCerts(JarFile.this, reifiedEntry()); } return certs == null ? null : certs.clone(); } @@ -330,10 +714,22 @@ throw new RuntimeException(e); } if (signers == null && jv != null) { - signers = jv.getCodeSigners(JarFile.this, this); + signers = jv.getCodeSigners(JarFile.this, reifiedEntry()); } return signers == null ? null : signers.clone(); } + JarFileEntry reifiedEntry() { + if (isMultiRelease()) { + String entryName = super.getName(); + return entryName.equals(this.name) ? this : new JarFileEntry(entryName, this); + } + return this; + } + + @Override + public String getName() { + return name; + } } /* @@ -491,12 +887,19 @@ // wrap a verifier stream around the real stream return new JarVerifier.VerifierStream( getManifestFromReference(), - ze instanceof JarFileEntry ? - (JarEntry) ze : getJarEntry(ze.getName()), + verifiableEntry(ze), super.getInputStream(ze), jv); } + private JarEntry verifiableEntry(ZipEntry ze) { + if (!(ze instanceof JarFileEntry)) { + ze = getJarEntry(ze.getName()); + } + // assure the name and entry match for verification + return ze == null ? null : ((JarFileEntry)ze).reifiedEntry(); + } + // Statics for hand-coded Boyer-Moore search private static final char[] CLASSPATH_CHARS = {'c','l','a','s','s','-','p','a','t','h'}; // The bad character shift for "class-path" @@ -523,7 +926,7 @@ private JarEntry getManEntry() { if (manEntry == null) { // First look up manifest entry using standard name - manEntry = getJarEntry(MANIFEST_NAME); + ZipEntry manEntry = super.getEntry(MANIFEST_NAME); if (manEntry == null) { // If not found, then iterate through all the "META-INF/" // entries to find a match. @@ -531,12 +934,15 @@ if (names != null) { for (String name : names) { if (MANIFEST_NAME.equals(name.toUpperCase(Locale.ENGLISH))) { - manEntry = getJarEntry(name); + manEntry = super.getEntry(name); break; } } } } + this.manEntry = (manEntry == null) + ? null + : new JarFileEntry(manEntry.getName(), manEntry); } return manEntry; } diff -r e17994ab030a -r 332b49163fc9 jdk/src/java.base/share/classes/sun/misc/URLClassPath.java --- a/jdk/src/java.base/share/classes/sun/misc/URLClassPath.java Mon Feb 15 16:59:56 2016 -0800 +++ b/jdk/src/java.base/share/classes/sun/misc/URLClassPath.java Mon Feb 15 17:47:53 2016 -0800 @@ -63,6 +63,7 @@ import java.util.jar.Manifest; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; +import java.util.zip.ZipFile; import jdk.internal.jimage.ImageLocation; import jdk.internal.jimage.ImageReader; @@ -727,9 +728,10 @@ if (!p.exists()) { throw new FileNotFoundException(p.getPath()); } - return checkJar(new JarFile(p.getPath())); + return checkJar(new JarFile(new File(p.getPath()), true, ZipFile.OPEN_READ, + JarFile.Release.RUNTIME)); } - URLConnection uc = getBaseURL().openConnection(); + URLConnection uc = (new URL(getBaseURL(), "#runtime")).openConnection(); uc.setRequestProperty(USER_AGENT_JAVA_VERSION, JAVA_VERSION); JarFile jarFile = ((JarURLConnection)uc).getJarFile(); return checkJar(jarFile); @@ -756,7 +758,9 @@ final URL url; try { - url = new URL(getBaseURL(), ParseUtil.encodePath(name, false)); + // 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"); if (check) { URLClassPath.check(url); } diff -r e17994ab030a -r 332b49163fc9 jdk/src/java.base/share/classes/sun/net/www/protocol/jar/URLJarFile.java --- a/jdk/src/java.base/share/classes/sun/net/www/protocol/jar/URLJarFile.java Mon Feb 15 16:59:56 2016 -0800 +++ b/jdk/src/java.base/share/classes/sun/net/www/protocol/jar/URLJarFile.java Mon Feb 15 17:47:53 2016 -0800 @@ -65,9 +65,10 @@ } static JarFile getJarFile(URL url, URLJarFileCloseController closeController) throws IOException { - if (isFileURL(url)) - return new URLJarFile(url, closeController); - else { + if (isFileURL(url)) { + Release version = "runtime".equals(url.getRef()) ? Release.RUNTIME : Release.BASE; + return new URLJarFile(url, closeController, version); + } else { return retrieve(url, closeController); } } @@ -89,8 +90,13 @@ this.closeController = closeController; } - private URLJarFile(URL url, URLJarFileCloseController closeController) throws IOException { - super(ParseUtil.decode(url.getFile())); + private URLJarFile(File file, URLJarFileCloseController closeController, Release version) throws IOException { + super(file, true, ZipFile.OPEN_READ | ZipFile.OPEN_DELETE, version); + this.closeController = closeController; + } + + private URLJarFile(URL url, URLJarFileCloseController closeController, Release version) throws IOException { + super(new File(ParseUtil.decode(url.getFile())), true, ZipFile.OPEN_READ, version); this.closeController = closeController; } @@ -179,14 +185,6 @@ * Given a URL, retrieves a JAR file, caches it to disk, and creates a * cached JAR file object. */ - private static JarFile retrieve(final URL url) throws IOException { - return retrieve(url, null); - } - - /** - * Given a URL, retrieves a JAR file, caches it to disk, and creates a - * cached JAR file object. - */ private static JarFile retrieve(final URL url, final URLJarFileCloseController closeController) throws IOException { /* * See if interface is set, then call retrieve function of the class @@ -202,6 +200,7 @@ { JarFile result = null; + Release version = "runtime".equals(url.getRef()) ? Release.RUNTIME : Release.BASE; /* get the stream before asserting privileges */ try (final InputStream in = url.openConnection().getInputStream()) { @@ -211,7 +210,7 @@ Path tmpFile = Files.createTempFile("jar_cache", null); try { Files.copy(in, tmpFile, StandardCopyOption.REPLACE_EXISTING); - JarFile jarFile = new URLJarFile(tmpFile.toFile(), closeController); + JarFile jarFile = new URLJarFile(tmpFile.toFile(), closeController, version); tmpFile.toFile().deleteOnExit(); return jarFile; } catch (Throwable thr) { diff -r e17994ab030a -r 332b49163fc9 jdk/src/java.base/unix/classes/sun/net/www/protocol/jar/JarFileFactory.java --- a/jdk/src/java.base/unix/classes/sun/net/www/protocol/jar/JarFileFactory.java Mon Feb 15 16:59:56 2016 -0800 +++ b/jdk/src/java.base/unix/classes/sun/net/www/protocol/jar/JarFileFactory.java Mon Feb 15 17:47:53 2016 -0800 @@ -85,7 +85,7 @@ synchronized (instance) { result = getCachedJarFile(url); if (result == null) { - fileCache.put(URLUtil.urlNoFragString(url), local_result); + fileCache.put(urlKey(url), local_result); urlCache.put(local_result, url); result = local_result; } else { @@ -113,13 +113,13 @@ synchronized (instance) { URL urlRemoved = urlCache.remove(jarFile); if (urlRemoved != null) - fileCache.remove(URLUtil.urlNoFragString(urlRemoved)); + fileCache.remove(urlKey(urlRemoved)); } } private JarFile getCachedJarFile(URL url) { assert Thread.holdsLock(instance); - JarFile result = fileCache.get(URLUtil.urlNoFragString(url)); + JarFile result = fileCache.get(urlKey(url)); /* if the JAR file is cached, the permission will always be there */ if (result != null) { @@ -149,6 +149,12 @@ return result; } + private String urlKey(URL url) { + String urlstr = URLUtil.urlNoFragString(url); + if ("runtime".equals(url.getRef())) urlstr += "#runtime"; + return urlstr; + } + private Permission getPermission(JarFile jarFile) { try { URLConnection uc = getConnection(jarFile); diff -r e17994ab030a -r 332b49163fc9 jdk/src/java.base/windows/classes/sun/net/www/protocol/jar/JarFileFactory.java --- a/jdk/src/java.base/windows/classes/sun/net/www/protocol/jar/JarFileFactory.java Mon Feb 15 16:59:56 2016 -0800 +++ b/jdk/src/java.base/windows/classes/sun/net/www/protocol/jar/JarFileFactory.java Mon Feb 15 17:47:53 2016 -0800 @@ -95,7 +95,7 @@ synchronized (instance) { result = getCachedJarFile(url); if (result == null) { - fileCache.put(URLUtil.urlNoFragString(url), local_result); + fileCache.put(urlKey(url), local_result); urlCache.put(local_result, url); result = local_result; } else { @@ -123,13 +123,13 @@ synchronized (instance) { URL urlRemoved = urlCache.remove(jarFile); if (urlRemoved != null) - fileCache.remove(URLUtil.urlNoFragString(urlRemoved)); + fileCache.remove(urlKey(urlRemoved)); } } private JarFile getCachedJarFile(URL url) { assert Thread.holdsLock(instance); - JarFile result = fileCache.get(URLUtil.urlNoFragString(url)); + JarFile result = fileCache.get(urlKey(url)); /* if the JAR file is cached, the permission will always be there */ if (result != null) { @@ -159,6 +159,12 @@ return result; } + private String urlKey(URL url) { + String urlstr = URLUtil.urlNoFragString(url); + if ("runtime".equals(url.getRef())) urlstr += "#runtime"; + return urlstr; + } + private Permission getPermission(JarFile jarFile) { try { URLConnection uc = getConnection(jarFile); diff -r e17994ab030a -r 332b49163fc9 jdk/test/java/util/jar/JarFile/MultiReleaseJarAPI.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/util/jar/JarFile/MultiReleaseJarAPI.java Mon Feb 15 17:47:53 2016 -0800 @@ -0,0 +1,177 @@ +/* + * 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. + */ + +/* + * @test + * @bug 8132734 + * @summary Test the extended API and the aliasing additions in JarFile that + * support multi-release jar files + * @library /lib/testlibrary/java/util/jar + * @build Compiler JarBuilder CreateMultiReleaseTestJars + * @run testng MultiReleaseJarAPI + */ + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import static java.util.jar.JarFile.Release; +import static sun.misc.Version.jdkMajorVersion; // fixme JEP 223 Version + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + + +public class MultiReleaseJarAPI { + String userdir = System.getProperty("user.dir","."); + File unversioned = new File(userdir, "unversioned.jar"); + File multirelease = new File(userdir, "multi-release.jar"); + File signedmultirelease = new File(userdir, "signed-multi-release.jar"); + Release[] values = JarFile.Release.values(); + + + @BeforeClass + public void initialize() throws Exception { + CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars(); + creator.compileEntries(); + creator.buildUnversionedJar(); + creator.buildMultiReleaseJar(); + creator.buildSignedMultiReleaseJar(); + } + + @AfterClass + public void close() throws IOException { + Files.delete(unversioned.toPath()); + Files.delete(multirelease.toPath()); + Files.delete(signedmultirelease.toPath()); + } + + @Test + public void isMultiReleaseJar() throws Exception { + try (JarFile jf = new JarFile(unversioned)) { + Assert.assertFalse(jf.isMultiRelease()); + } + + try (JarFile jf = new JarFile(multirelease)) { + Assert.assertTrue(jf.isMultiRelease()); + } + } + + @Test + public void testVersioning() throws Exception { + // multi-release jar + JarFile jar = new JarFile(multirelease); + Assert.assertEquals(Release.BASE, jar.getVersion()); + jar.close(); + + for (Release value : values) { + System.err.println("test versioning for Release " + value); + try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, value)) { + Assert.assertEquals(value, jf.getVersion()); + } + } + + // regular, unversioned, jar + for (Release value : values) { + try (JarFile jf = new JarFile(unversioned, true, ZipFile.OPEN_READ, value)) { + Assert.assertEquals(Release.BASE, jf.getVersion()); + } + } + + // assure that we have a Release object corresponding to the actual runtime version + String version = "VERSION_" + jdkMajorVersion(); + boolean runtimeVersionExists = false; + for (Release value : values) { + if (version.equals(value.name())) runtimeVersionExists = true; + } + Assert.assertTrue(runtimeVersionExists); + } + + @Test + public void testAliasing() throws Exception { + for (Release value : values) { + System.err.println("test aliasing for Release " + value); + String name = value.name(); + String prefix; + if (name.equals("BASE")) { + prefix = ""; + } else if (name.equals("RUNTIME")) { + prefix = "META-INF/versions/" + jdkMajorVersion() + "/"; + } else { + prefix = "META-INF/versions/" + name.substring(8) + "/"; + } + // test both multi-release jars + readAndCompare(multirelease, value, "README", prefix + "README"); + readAndCompare(multirelease, value, "version/Version.class", prefix + "version/Version.class"); + // and signed multi-release jars + readAndCompare(signedmultirelease, value, "README", prefix + "README"); + readAndCompare(signedmultirelease, value, "version/Version.class", prefix + "version/Version.class"); + } + } + + private void readAndCompare(File jar, Release version, String name, String realName) throws Exception { + byte[] baseBytes; + byte[] versionedBytes; + try (JarFile jf = new JarFile(jar, true, ZipFile.OPEN_READ, Release.BASE)) { + ZipEntry ze = jf.getEntry(realName); + try (InputStream is = jf.getInputStream(ze)) { + baseBytes = is.readAllBytes(); + } + } + assert baseBytes.length > 0; + + try (JarFile jf = new JarFile(jar, true, ZipFile.OPEN_READ, version)) { + ZipEntry ze = jf.getEntry(name); + try (InputStream is = jf.getInputStream(ze)) { + versionedBytes = is.readAllBytes(); + } + } + assert versionedBytes.length > 0; + + Assert.assertTrue(Arrays.equals(baseBytes, versionedBytes)); + } + + @Test + public void testNames() throws Exception { + String rname = "version/Version.class"; + String vname = "META-INF/versions/9/version/Version.class"; + ZipEntry ze1; + ZipEntry ze2; + try (JarFile jf = new JarFile(multirelease)) { + ze1 = jf.getEntry(vname); + } + Assert.assertEquals(ze1.getName(), vname); + try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, Release.VERSION_9)) { + ze2 = jf.getEntry(rname); + } + Assert.assertEquals(ze2.getName(), rname); + Assert.assertNotEquals(ze1.getName(), ze2.getName()); + } +} diff -r e17994ab030a -r 332b49163fc9 jdk/test/java/util/jar/JarFile/MultiReleaseJarHttpProperties.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/util/jar/JarFile/MultiReleaseJarHttpProperties.java Mon Feb 15 17:47:53 2016 -0800 @@ -0,0 +1,156 @@ +/* + * 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. + */ + +/* + * @test + * @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 + * @run testng MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=0 MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=8 MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=9 MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=10 MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=100 MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=10 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.version=10 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarHttpProperties + * @run testng/othervm -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarHttpProperties + * @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; +import java.net.InetSocketAddress; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class MultiReleaseJarHttpProperties extends MultiReleaseJarProperties { + private SimpleHttpServer server; + + @BeforeClass + public void initialize() throws Exception { + server = new SimpleHttpServer(); + server.start(); + super.initialize(); + } + + @Override + protected void initializeClassLoader() throws Exception { + URL[] urls = new URL[]{ + new URL("http://localhost:" + server.getPort() + "/multi-release-jar") + }; + cldr = new URLClassLoader(urls); + // load any class, Main is convenient and in the root entries + rootClass = cldr.loadClass("version.Main"); + } + + @AfterClass + public void close() throws IOException { + // Windows requires server to stop before file is deleted + if (server != null) + server.stop(); + super.close(); + } + + /* + * jdk.util.jar.enableMultiRelease=force is a no-op for URLClassLoader + */ + + @Test + public void testURLClassLoader() throws Throwable { + Class vcls = cldr.loadClass("version.Version"); + invokeMethod(vcls, rtVersion); + } + + @Test + public void testGetResourceAsStream() throws Exception { + String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java"; + // use rootClass as a base for getting resources + getResourceAsStream(rootClass, resource); + } + + @Test + public void testGetResource() throws Exception { + String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java"; + // use rootClass as a base for getting resources + 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(); + } +} + diff -r e17994ab030a -r 332b49163fc9 jdk/test/java/util/jar/JarFile/MultiReleaseJarIterators.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/util/jar/JarFile/MultiReleaseJarIterators.java Mon Feb 15 17:47:53 2016 -0800 @@ -0,0 +1,228 @@ +/* + * 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. + */ + +/* + * @test + * @bug 8132734 + * @summary Test the extended API and the aliasing additions in JarFile that + * support multi-release jar files + * @library /lib/testlibrary/java/util/jar + * @build Compiler JarBuilder CreateMultiReleaseTestJars + * @run testng MultiReleaseJarIterators + */ + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.zip.ZipFile; + +import static java.util.jar.JarFile.Release; +import static sun.misc.Version.jdkMajorVersion; // fixme JEP 223 Version + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + + +public class MultiReleaseJarIterators { + String userdir = System.getProperty("user.dir", "."); + File unversioned = new File(userdir, "unversioned.jar"); + File multirelease = new File(userdir, "multi-release.jar"); + Map uvEntries = new HashMap<>(); + Map mrEntries = new HashMap<>(); + Map baseEntries = new HashMap<>(); + Map v9Entries = new HashMap<>(); + Map v10Entries = new HashMap<>(); + + @BeforeClass + public void initialize() throws Exception { + CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars(); + creator.compileEntries(); + creator.buildUnversionedJar(); + creator.buildMultiReleaseJar(); + + try (JarFile jf = new JarFile(multirelease)) { + for (Enumeration e = jf.entries(); e.hasMoreElements(); ) { + JarEntry je = e.nextElement(); + String name = je.getName(); + mrEntries.put(name, je); + if (name.startsWith("META-INF/versions/")) { + if (name.startsWith("META-INF/versions/9/")) { + v9Entries.put(name.substring(20), je); + } else if (name.startsWith("META-INF/versions/10/")) { + v10Entries.put(name.substring(21), je); + } + } else { + baseEntries.put(name, je); + } + } + } + Assert.assertEquals(mrEntries.size(), 14); + Assert.assertEquals(baseEntries.size(), 6); + Assert.assertEquals(v9Entries.size(), 5); + Assert.assertEquals(v10Entries.size(), 3); + + try (JarFile jf = new JarFile(unversioned)) { + jf.entries().asIterator().forEachRemaining(je -> uvEntries.put(je.getName(), je)); + } + Assert.assertEquals(uvEntries.size(), 6); + } + + @AfterClass + public void close() throws IOException { + Files.delete(unversioned.toPath()); + Files.delete(multirelease.toPath()); + } + + @Test + public void testMultiReleaseJar() throws IOException { + try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ)) { + testEnumeration(jf, mrEntries); + testStream(jf, mrEntries); + } + + try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, Release.BASE)) { + testEnumeration(jf, baseEntries); + testStream(jf, baseEntries); + } + + try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, Release.VERSION_9)) { + testEnumeration(jf, v9Entries); + testStream(jf, v9Entries); + } + + try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, Release.RUNTIME)) { + Map expectedEntries; + switch (jdkMajorVersion()) { + case 9: + expectedEntries = v9Entries; + break; + case 10: // won't get here until JDK 10 + expectedEntries = v10Entries; + break; + default: + expectedEntries = baseEntries; + break; + } + + testEnumeration(jf, expectedEntries); + testStream(jf, expectedEntries); + } + } + + @Test + public void testUnversionedJar() throws IOException { + try (JarFile jf = new JarFile(unversioned, true, ZipFile.OPEN_READ)) { + testEnumeration(jf, uvEntries); + testStream(jf, uvEntries); + } + + try (JarFile jf = new JarFile(unversioned, true, ZipFile.OPEN_READ, Release.BASE)) { + testEnumeration(jf, uvEntries); + testStream(jf, uvEntries); + } + + try (JarFile jf = new JarFile(unversioned, true, ZipFile.OPEN_READ, Release.VERSION_9)) { + testEnumeration(jf, uvEntries); + testStream(jf, uvEntries); + } + + try (JarFile jf = new JarFile(unversioned, true, ZipFile.OPEN_READ, Release.RUNTIME)) { + testEnumeration(jf, uvEntries); + testStream(jf, uvEntries); + } + } + + private void testEnumeration(JarFile jf, Map expectedEntries) { + Map actualEntries = new HashMap<>(); + for (Enumeration e = jf.entries(); e.hasMoreElements(); ) { + JarEntry je = e.nextElement(); + actualEntries.put(je.getName(), je); + } + + testEntries(jf, actualEntries, expectedEntries); + } + + + private void testStream(JarFile jf, Map expectedEntries) { + Map actualEntries = jf.stream().collect(Collectors.toMap(je -> je.getName(), je -> je)); + + testEntries(jf, actualEntries, expectedEntries); + } + + private void testEntries(JarFile jf, Map actualEntries, Map expectedEntries) { + /* For multi-release jar files constructed with a Release object, + * actualEntries contain versionedEntries that are considered part of the + * public API. They have a 1-1 correspondence with baseEntries, + * so entries that are not part of the public API won't be present, + * i.e. those entries with a name that starts with version/PackagePrivate + * in this particular jar file (multi-release.jar) + */ + + Map entries; + if (expectedEntries == mrEntries) { + Assert.assertEquals(actualEntries.size(), mrEntries.size()); + entries = mrEntries; + } else if (expectedEntries == uvEntries) { + Assert.assertEquals(actualEntries.size(), uvEntries.size()); + entries = uvEntries; + } else { + Assert.assertEquals(actualEntries.size(), baseEntries.size()); // this is correct + entries = baseEntries; + } + + entries.keySet().forEach(name -> { + JarEntry ee = expectedEntries.get(name); + if (ee == null) ee = entries.get(name); + JarEntry ae = actualEntries.get(name); + try { + compare(jf, ae, ee); + } catch (IOException x) { + throw new RuntimeException(x); + } + }); + } + + private void compare(JarFile jf, JarEntry actual, JarEntry expected) throws IOException { + byte[] abytes; + byte[] ebytes; + + try (InputStream is = jf.getInputStream(actual)) { + abytes = is.readAllBytes(); + } + + try (InputStream is = jf.getInputStream(expected)) { + ebytes = is.readAllBytes(); + } + + Assert.assertEquals(abytes, ebytes); + } +} diff -r e17994ab030a -r 332b49163fc9 jdk/test/java/util/jar/JarFile/MultiReleaseJarProperties.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/util/jar/JarFile/MultiReleaseJarProperties.java Mon Feb 15 17:47:53 2016 -0800 @@ -0,0 +1,200 @@ +/* + * 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. + */ + +/* + * @test + * @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 + * @run testng MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=0 MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=8 MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=9 MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=10 MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=100 MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=10 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.version=10 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarProperties + * @run testng/othervm -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarProperties + */ + +import java.io.File; +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.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import static sun.misc.Version.jdkMajorVersion; // fixme JEP 223 Version + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class MultiReleaseJarProperties { + final static int ROOTVERSION = 8; // magic number from knowledge of internals + final static String userdir = System.getProperty("user.dir", "."); + final static File multirelease = new File(userdir, "multi-release.jar"); + protected int rtVersion; + boolean force; + protected ClassLoader cldr; + protected Class rootClass; + + @BeforeClass + public void initialize() throws Exception { + CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars(); + creator.compileEntries(); + creator.buildMultiReleaseJar(); + + rtVersion = Integer.getInteger("jdk.util.jar.version", jdkMajorVersion()); + String mrprop = System.getProperty("jdk.util.jar.enableMultiRelease", ""); + if (mrprop.equals("false")) { + rtVersion = ROOTVERSION; + } else if (rtVersion < ROOTVERSION) { + rtVersion = ROOTVERSION; + } else if (rtVersion > jdkMajorVersion()) { + rtVersion = jdkMajorVersion(); + } + force = mrprop.equals("force"); + + initializeClassLoader(); + } + + protected void initializeClassLoader() throws Exception { + URL[] urls = new URL[]{multirelease.toURI().toURL()}; + cldr = new URLClassLoader(urls); + // load any class, Main is convenient and in the root entries + rootClass = cldr.loadClass("version.Main"); + } + + @AfterClass + public void close() throws IOException { + ((URLClassLoader)cldr).close(); + Files.delete(multirelease.toPath()); + } + + /* + * jdk.util.jar.enableMultiRelease=force is a no-op for URLClassLoader + */ + @Test + public void testURLClassLoader() throws Throwable { + Class vcls = cldr.loadClass("version.Version"); + invokeMethod(vcls, rtVersion); + } + + protected void invokeMethod(Class vcls, int expected) throws Throwable { + MethodType mt = MethodType.methodType(int.class); + MethodHandle mh = MethodHandles.lookup().findVirtual(vcls, "getVersion", mt); + Assert.assertEquals(expected, (int) mh.invoke(vcls.newInstance())); + } + + /* + * jdk.util.jar.enableMultiRelease=force should affect a custom class loader + */ + @Test + public void testClassLoader() throws Throwable { + try (JarFile jf = new JarFile(multirelease)) { // do not set runtime versioning + ClassLoader cldr = new CustomClassLoader(jf); + Class vcls = cldr.loadClass("version.Version"); + if (rtVersion == 9) { + try { + cldr.loadClass("version.PackagePrivate"); + } catch (ClassNotFoundException x) { + if (force) throw x; + } + } + invokeMethod(vcls, force ? rtVersion : ROOTVERSION); + } + } + + private static class CustomClassLoader extends ClassLoader { + private final JarFile jf; + + CustomClassLoader(JarFile jf) throws Exception { + super(null); + this.jf = jf; + } + + protected Class findClass(String name) throws ClassNotFoundException { + try { + byte[] b; + String entryName = name.replace(".", "/") + ".class"; + JarEntry je = jf.getJarEntry(entryName); + if (je != null) { + try (InputStream is = jf.getInputStream(je)) { + b = new byte[(int) je.getSize()]; + is.read(b); + } + return defineClass(name, b, 0, b.length); + } + throw new ClassNotFoundException(name); + } catch (IOException x) { + throw new ClassNotFoundException(x.getMessage()); + } + } + } + + @Test + public void testGetResourceAsStream() throws Exception { + String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java"; + // use fileRootClass as a base for getting resources + getResourceAsStream(rootClass, resource); + } + + protected void getResourceAsStream(Class rootClass, String resource) throws Exception { + try (InputStream is = rootClass.getResourceAsStream(resource)) { + byte[] bytes = is.readAllBytes(); + resource = new String(bytes); + } + String match = "return " + rtVersion + ";"; + Assert.assertTrue(resource.contains(match)); + } + + @Test + public void testGetResource() throws Exception { + String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java"; + // use rootClass as a base for getting resources + getResource(rootClass, resource); + } + + protected void getResource(Class rootClass, String resource) throws Exception { + URL url = rootClass.getResource(resource); + try (InputStream is = url.openStream()) { + byte[] bytes = is.readAllBytes(); + resource = new String(bytes); + } + String match = "return " + rtVersion + ";"; + Assert.assertTrue(resource.contains(match)); + } +} diff -r e17994ab030a -r 332b49163fc9 jdk/test/java/util/jar/JarFile/MultiReleaseJarSecurity.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/util/jar/JarFile/MultiReleaseJarSecurity.java Mon Feb 15 17:47:53 2016 -0800 @@ -0,0 +1,108 @@ +/* + * 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. + */ + +/* + * @test + * @bug 8132734 + * @summary Test potential security related issues + * @library /lib/testlibrary/java/util/jar + * @build Compiler JarBuilder CreateMultiReleaseTestJars + * @run testng MultiReleaseJarSecurity + */ + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.security.CodeSigner; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipFile; + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class MultiReleaseJarSecurity { + String userdir = System.getProperty("user.dir","."); + File multirelease = new File(userdir, "multi-release.jar"); + File signedmultirelease = new File(userdir, "signed-multi-release.jar"); + + @BeforeClass + public void initialize() throws Exception { + CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars(); + creator.compileEntries(); + creator.buildMultiReleaseJar(); + creator.buildSignedMultiReleaseJar(); + } + + @AfterClass + public void close() throws IOException { + Files.delete(multirelease.toPath()); + Files.delete(signedmultirelease.toPath()); + } + + @Test + public void testCertsAndSigners() throws IOException { + try (JarFile jf = new JarFile(signedmultirelease, true, ZipFile.OPEN_READ, JarFile.Release.RUNTIME)) { + int version = sun.misc.Version.jdkMajorVersion(); // fixme JEP 223 Version + CertsAndSigners vcas = new CertsAndSigners(jf, jf.getJarEntry("version/Version.class")); + CertsAndSigners rcas = new CertsAndSigners(jf, jf.getJarEntry("META-INF/versions/" + version + "/version/Version.class")); + Assert.assertTrue(Arrays.equals(rcas.getCertificates(), vcas.getCertificates())); + Assert.assertTrue(Arrays.equals(rcas.getCodeSigners(), vcas.getCodeSigners())); + } + } + + private static class CertsAndSigners { + final private JarFile jf; + final private JarEntry je; + private boolean readComplete; + + CertsAndSigners(JarFile jf, JarEntry je) { + this.jf = jf; + this.je = je; + } + + Certificate[] getCertificates() throws IOException { + readEntry(); + return je.getCertificates(); + } + + CodeSigner[] getCodeSigners() throws IOException { + readEntry(); + return je.getCodeSigners(); + } + + private void readEntry() throws IOException { + if (!readComplete) { + try (InputStream is = jf.getInputStream(je)) { + is.readAllBytes(); + } + readComplete = true; + } + } + } +} diff -r e17994ab030a -r 332b49163fc9 jdk/test/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java Mon Feb 15 17:47:53 2016 -0800 @@ -0,0 +1,117 @@ +/* + * 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. + */ + +/* + * @test + * @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 + * @run testng MultiReleaseJarURLConnection + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.jar.JarFile; + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class MultiReleaseJarURLConnection { + String userdir = System.getProperty("user.dir","."); + String urlFile = "jar:file:" + userdir + "/multi-release.jar!/"; + String urlEntry = urlFile + "version/Version.java"; + + @BeforeClass + public void initialize() throws Exception { + CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars(); + creator.compileEntries(); + creator.buildMultiReleaseJar(); + } + + @AfterClass + public void close() throws IOException { + Files.delete(Paths.get(userdir, "multi-release.jar")); + } + + @Test + public void testRuntimeVersioning() throws Exception { + 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")); + } + + @Test + public void testCachedJars() throws Exception { + URL rootUrl = new URL(urlFile); + JarURLConnection juc = (JarURLConnection)rootUrl.openConnection(); + JarFile rootJar = juc.getJarFile(); + JarFile.Release root = rootJar.getVersion(); + + URL runtimeUrl = new URL(urlFile + "#runtime"); + juc = (JarURLConnection)runtimeUrl.openConnection(); + JarFile runtimeJar = juc.getJarFile(); + JarFile.Release runtime = runtimeJar.getVersion(); + Assert.assertNotEquals(root, runtime); + + juc = (JarURLConnection)rootUrl.openConnection(); + JarFile jar = juc.getJarFile(); + Assert.assertEquals(jar.getVersion(), root); + Assert.assertEquals(jar, rootJar); + + juc = (JarURLConnection)runtimeUrl.openConnection(); + jar = juc.getJarFile(); + Assert.assertEquals(jar.getVersion(), runtime); + Assert.assertEquals(jar, runtimeJar); + + rootJar.close(); + runtimeJar.close(); + jar.close(); // probably not needed + } + + private boolean readAndCompare(URL url, String match) throws Exception { + boolean result; + // necessary to do it this way, instead of openStream(), so we can + // close underlying JarFile, otherwise windows can't delete the file + URLConnection conn = url.openConnection(); + try (InputStream is = conn.getInputStream()) { + byte[] bytes = is.readAllBytes(); + result = (new String(bytes)).contains(match); + } + if (conn instanceof JarURLConnection) { + ((JarURLConnection)conn).getJarFile().close(); + } + return result; + } +}