8132734: JDK 9 runtime changes to support multi-release jar files
authorsdrach
Mon, 15 Feb 2016 17:47:53 -0800
changeset 36129 332b49163fc9
parent 36128 e17994ab030a
child 36130 5423259ef9ea
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
jdk/src/java.base/share/classes/java/net/JarURLConnection.java
jdk/src/java.base/share/classes/java/util/jar/Attributes.java
jdk/src/java.base/share/classes/java/util/jar/JarFile.java
jdk/src/java.base/share/classes/sun/misc/URLClassPath.java
jdk/src/java.base/share/classes/sun/net/www/protocol/jar/URLJarFile.java
jdk/src/java.base/unix/classes/sun/net/www/protocol/jar/JarFileFactory.java
jdk/src/java.base/windows/classes/sun/net/www/protocol/jar/JarFileFactory.java
jdk/test/java/util/jar/JarFile/MultiReleaseJarAPI.java
jdk/test/java/util/jar/JarFile/MultiReleaseJarHttpProperties.java
jdk/test/java/util/jar/JarFile/MultiReleaseJarIterators.java
jdk/test/java/util/jar/JarFile/MultiReleaseJarProperties.java
jdk/test/java/util/jar/JarFile/MultiReleaseJarSecurity.java
jdk/test/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.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 */
--- 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");
     }
 }
--- 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.
+ *
+ * <p>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}.
+ *
+ * <p>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)}).
+ *
+ * <p>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.
+ *
+ * <p>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.
  *
  * <p> 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
+ * <div class="block">
+ * 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.
+ * <ul>
+ * <li>
+ * {@code jdk.util.jar.version} can be assigned a value that is the
+ * {@code String} representation of a non-negative integer
+ * {@code <= Version.current().major()}.  The value is used to set the effective
+ * runtime version to something other than the default value obtained by
+ * evaluating {@code Version.current().major()}. The effective runtime version
+ * is the version that the {@link JarFile#JarFile(File, boolean, int, Release)}
+ * constructor uses when the value of the last argument is
+ * {@code Release.RUNTIME}.
+ * </li>
+ * <li>
+ * {@code jdk.util.jar.enableMultiRelease} can be assigned one of the three
+ * {@code String} values <em>true</em>, <em>false</em>, or <em>force</em>.  The
+ * value <em>true</em>, the default value, enables multi-release jar file
+ * processing.  The value <em>false</em> disables multi-release jar processing,
+ * ignoring the "Multi-Release" manifest attribute, and the versioned
+ * directories in a multi-release jar file if they exist.  Furthermore,
+ * the method {@link JarFile#isMultiRelease()} returns <em>false</em>. The value
+ * <em>force</em> causes the {@code JarFile} to be initialized to runtime
+ * versioning after construction.  It effectively does the same as this code:
+ * {@code (new JarFile(File, boolean, int, Release.RUNTIME)}.
+ * </li>
+ * </ul>
+ * </div>
  *
  * @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<Manifest> 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<Integer>() {
+                    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<String>() {
+                    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
+         * <div class="block">
+         * 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.
+         * </div>
+         */
+        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.
      *
+     * <p>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
+     * <div class="block">
+     * This implementation invokes {@link JarFile#getEntry(String)}.
+     * </div>
      */
     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.
      *
+     * <p>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
+     * <div class="block">
+     * 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.
+     * </div>
      */
     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<JarEntry>
     {
         final Enumeration<? extends ZipEntry> 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;
     }
--- 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);
                 }
--- 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) {
--- 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);
--- 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);
--- /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());
+    }
+}
--- /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();
+    }
+}
+
--- /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<String,JarEntry> uvEntries = new HashMap<>();
+    Map<String,JarEntry> mrEntries = new HashMap<>();
+    Map<String,JarEntry> baseEntries = new HashMap<>();
+    Map<String,JarEntry> v9Entries = new HashMap<>();
+    Map<String, JarEntry> 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<JarEntry> 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<String,JarEntry> 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<String,JarEntry> expectedEntries) {
+        Map<String, JarEntry> actualEntries = new HashMap<>();
+        for (Enumeration<JarEntry> e = jf.entries(); e.hasMoreElements(); ) {
+            JarEntry je = e.nextElement();
+            actualEntries.put(je.getName(), je);
+        }
+
+        testEntries(jf, actualEntries, expectedEntries);
+    }
+
+
+    private void testStream(JarFile jf, Map<String,JarEntry> expectedEntries) {
+        Map<String,JarEntry> actualEntries = jf.stream().collect(Collectors.toMap(je -> je.getName(), je -> je));
+
+        testEntries(jf, actualEntries, expectedEntries);
+    }
+
+    private void testEntries(JarFile jf, Map<String,JarEntry> actualEntries, Map<String,JarEntry> 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<String,JarEntry> 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);
+    }
+}
--- /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));
+    }
+}
--- /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;
+            }
+        }
+    }
+}
--- /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;
+    }
+}