8144355: JDK 9 changes to ZipFileSystem to support multi-release jar files
authorsdrach
Wed, 30 Dec 2015 16:15:21 +0000
changeset 34835 ee52702b8d1b
parent 34834 8baf154e0db4
child 34836 0fd9d2684eb6
8144355: JDK 9 changes to ZipFileSystem to support multi-release jar files Summary: JEP 238 Multi-Release JarFileSystem implementation Reviewed-by: alanb, psandoz, sherman
jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/JarFileSystem.java
jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java
jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java
jdk/test/jdk/nio/zipfs/MultiReleaseJarTest.java
jdk/test/lib/testlibrary/java/util/jar/Compiler.java
jdk/test/lib/testlibrary/java/util/jar/CreateMultiReleaseTestJars.java
jdk/test/lib/testlibrary/java/util/jar/JarBuilder.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/JarFileSystem.java	Wed Dec 30 16:15:21 2015 +0000
@@ -0,0 +1,182 @@
+/*
+ * 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.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.nio.zipfs;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+/**
+ * Adds aliasing to ZipFileSystem to support multi-release jar files.  An alias map
+ * is created by {@link JarFileSystem#createVersionedLinks(int)}.  The map is then
+ * consulted when an entry is looked up in {@link JarFileSystem#getEntry(byte[])}
+ * to determine if the entry has a corresponding versioned entry.  If so, the
+ * versioned entry is returned.
+ *
+ * @author Steve Drach
+ */
+
+class JarFileSystem extends ZipFileSystem {
+    private Function<byte[],byte[]> lookup;
+
+    @Override
+    Entry getEntry(byte[] path) throws IOException {
+        // check for an alias to a versioned entry
+        byte[] versionedPath = lookup.apply(path);
+        return versionedPath == null ? super.getEntry(path) : super.getEntry(versionedPath);
+    }
+
+    JarFileSystem(ZipFileSystemProvider provider, Path zfpath, Map<String,?> env)
+            throws IOException {
+        super(provider, zfpath, env);
+        lookup = path -> path;  // lookup needs to be set before isMultiReleaseJar is called
+                                // because it eventually calls getEntry
+        if (isMultiReleaseJar()) {
+            int version;
+            Object o = env.get("multi-release");
+            if (o instanceof String) {
+                String s = (String)o;
+                if (s.equals("runtime")) {
+                    version = sun.misc.Version.jdkMajorVersion();  // fixme waiting for jdk.util.Version
+                } else {
+                    version = Integer.parseInt(s);
+                }
+            } else if (o instanceof Integer) {
+                version = (Integer)o;
+            } else if (false /*o instanceof Version*/) {  // fixme waiting for jdk.util.Version
+//                version = ((Version)o).major();
+            } else {
+                throw new IllegalArgumentException("env parameter must be String, Integer, "
+                        + "or Version");
+            }
+            lookup = createVersionedLinks(version < 0 ? 0 : version);
+            setReadOnly();
+        }
+    }
+
+    private boolean isMultiReleaseJar() {
+        try (InputStream is = newInputStream(getBytes("META-INF/MANIFEST.MF"))) {
+            return (new Manifest(is)).getMainAttributes()
+                    .containsKey(new Attributes.Name("Multi-Release"));
+            // fixme change line above after JarFile integration to contain Attributes.Name.MULTI_RELEASE
+        } catch (IOException x) {
+            return false;
+        }
+    }
+
+    /**
+     * create a map of aliases for versioned entries, for example:
+     *   version/PackagePrivate.class -> META-INF/versions/9/version/PackagePrivate.class
+     *   version/PackagePrivate.java -> META-INF/versions/9/version/PackagePrivate.java
+     *   version/Version.class -> META-INF/versions/10/version/Version.class
+     *   version/Version.java -> META-INF/versions/10/version/Version.java
+     *
+     * then wrap the map in a function that getEntry can use to override root
+     * entry lookup for entries that have corresponding versioned entries
+     */
+    private Function<byte[],byte[]> createVersionedLinks(int version) {
+        HashMap<IndexNode,byte[]> aliasMap = new HashMap<>();
+        getVersionMap(version, getInode(getBytes("META-INF/versions"))).values()
+                .forEach(versionNode -> {   // for each META-INF/versions/{n} directory
+                    // put all the leaf inodes, i.e. entries, into the alias map
+                    // possibly shadowing lower versioned entries
+                    walk(versionNode, entryNode -> {
+                        byte[] rootName = getRootName(versionNode, entryNode);
+                        if (rootName != null) {
+                            IndexNode rootNode = getInode(rootName);
+                            if (rootNode == null) { // no matching root node, make a virtual one
+                                rootNode = IndexNode.keyOf(rootName);
+                            }
+                            aliasMap.put(rootNode, entryNode.name);
+                        }
+                    });
+                });
+        return path -> aliasMap.get(IndexNode.keyOf(path));
+    }
+
+    /**
+     * create a sorted version map of version -> inode, for inodes <= max version
+     *   9 -> META-INF/versions/9
+     *  10 -> META-INF/versions/10
+     */
+    private TreeMap<Integer, IndexNode> getVersionMap(int version, IndexNode metaInfVersions) {
+        TreeMap<Integer,IndexNode> map = new TreeMap<>();
+        IndexNode child = metaInfVersions.child;
+        while (child != null) {
+            Integer key = getVersion(child.name, metaInfVersions.name.length);
+            if (key != null && key <= version) {
+                map.put(key, child);
+            }
+            child = child.sibling;
+        }
+        return map;
+    }
+
+    /**
+     * extract the integer version number -- META-INF/versions/9 returns 9
+     */
+    private Integer getVersion(byte[] name, int offset) {
+        try {
+            return Integer.parseInt(getString(Arrays.copyOfRange(name, offset, name.length-1)));
+        } catch (NumberFormatException x) {
+            // ignore this even though it might indicate issues with the JAR structure
+            return null;
+        }
+    }
+
+    /**
+     * walk the IndexNode tree processing all leaf nodes
+     */
+    private void walk(IndexNode inode, Consumer<IndexNode> process) {
+        if (inode == null) return;
+        if (inode.isDir()) {
+            walk(inode.child, process);
+        } else {
+            process.accept(inode);
+            walk(inode.sibling, process);
+        }
+    }
+
+    /**
+     * extract the root name from a versioned entry name
+     *   given inode for META-INF/versions/9/foo/bar.class
+     *   and prefix META-INF/versions/9/
+     *   returns foo/bar.class
+     */
+    private byte[] getRootName(IndexNode prefix, IndexNode inode) {
+        int offset = prefix.name.length;
+        byte[] fullName = inode.name;
+        return Arrays.copyOfRange(fullName, offset, fullName.length);
+    }
+}
--- a/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java	Mon Dec 28 19:03:18 2015 -0800
+++ b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java	Wed Dec 30 16:15:21 2015 +0000
@@ -155,6 +155,10 @@
             throw new ReadOnlyFileSystemException();
     }
 
+    void setReadOnly() {
+        this.readOnly = true;
+    }
+
     @Override
     public Iterable<Path> getRootDirectories() {
         ArrayList<Path> pathArr = new ArrayList<>();
@@ -320,7 +324,7 @@
         beginRead();
         try {
             ensureOpen();
-            e = getEntry0(path);
+            e = getEntry(path);
             if (e == null) {
                 IndexNode inode = getInode(path);
                 if (inode == null)
@@ -342,7 +346,7 @@
         beginWrite();
         try {
             ensureOpen();
-            Entry e = getEntry0(path);    // ensureOpen checked
+            Entry e = getEntry(path);    // ensureOpen checked
             if (e == null)
                 throw new NoSuchFileException(getString(path));
             if (e.type == Entry.CEN)
@@ -445,7 +449,7 @@
         beginWrite();
         try {
             ensureOpen();
-            Entry eSrc = getEntry0(src);  // ensureOpen checked
+            Entry eSrc = getEntry(src);  // ensureOpen checked
             if (eSrc == null)
                 throw new NoSuchFileException(getString(src));
             if (eSrc.isDir()) {    // spec says to create dst dir
@@ -460,7 +464,7 @@
                 else if (opt == COPY_ATTRIBUTES)
                     hasCopyAttrs = true;
             }
-            Entry eDst = getEntry0(dst);
+            Entry eDst = getEntry(dst);
             if (eDst != null) {
                 if (!hasReplace)
                     throw new FileAlreadyExistsException(getString(dst));
@@ -521,7 +525,7 @@
         beginRead();                 // only need a readlock, the "update()" will
         try {                        // try to obtain a writelock when the os is
             ensureOpen();            // being closed.
-            Entry e = getEntry0(path);
+            Entry e = getEntry(path);
             if (e != null) {
                 if (e.isDir() || hasCreateNew)
                     throw new FileAlreadyExistsException(getString(path));
@@ -550,7 +554,7 @@
         beginRead();
         try {
             ensureOpen();
-            Entry e = getEntry0(path);
+            Entry e = getEntry(path);
             if (e == null)
                 throw new NoSuchFileException(getString(path));
             if (e.isDir())
@@ -592,7 +596,7 @@
                     newOutputStream(path, options.toArray(new OpenOption[0])));
                 long leftover = 0;
                 if (options.contains(StandardOpenOption.APPEND)) {
-                    Entry e = getEntry0(path);
+                    Entry e = getEntry(path);
                     if (e != null && e.size >= 0)
                         leftover = e.size;
                 }
@@ -644,7 +648,7 @@
             beginRead();
             try {
                 ensureOpen();
-                Entry e = getEntry0(path);
+                Entry e = getEntry(path);
                 if (e == null || e.isDir())
                     throw new NoSuchFileException(getString(path));
                 final ReadableByteChannel rbc =
@@ -714,7 +718,7 @@
         beginRead();
         try {
             ensureOpen();
-            Entry e = getEntry0(path);
+            Entry e = getEntry(path);
             if (forWrite) {
                 checkWritable();
                 if (e == null) {
@@ -855,7 +859,7 @@
     private Path getTempPathForEntry(byte[] path) throws IOException {
         Path tmpPath = createTempFileInSameDirectoryAs(zfpath);
         if (path != null) {
-            Entry e = getEntry0(path);
+            Entry e = getEntry(path);
             if (e != null) {
                 try (InputStream is = newInputStream(path)) {
                     Files.copy(is, tmpPath, REPLACE_EXISTING);
@@ -939,7 +943,7 @@
 
     private long getDataPos(Entry e) throws IOException {
         if (e.locoff == -1) {
-            Entry e2 = getEntry0(e.name);
+            Entry e2 = getEntry(e.name);
             if (e2 == null)
                 throw new ZipException("invalid loc for entry <" + e.name + ">");
             e.locoff = e2.locoff;
@@ -1325,7 +1329,7 @@
         //System.out.printf("->sync(%s) done!%n", toString());
     }
 
-    private IndexNode getInode(byte[] path) {
+    IndexNode getInode(byte[] path) {
         if (path == null)
             throw new NullPointerException("path");
         IndexNode key = IndexNode.keyOf(path);
@@ -1340,7 +1344,7 @@
         return inode;
     }
 
-    private Entry getEntry0(byte[] path) throws IOException {
+    Entry getEntry(byte[] path) throws IOException {
         IndexNode inode = getInode(path);
         if (inode instanceof Entry)
             return (Entry)inode;
@@ -2096,7 +2100,7 @@
             pos += (LOCHDR + nlen + elen);
             if ((flag & FLAG_DATADESCR) != 0) {
                 // Data Descriptor
-                Entry e = zipfs.getEntry0(name);  // get the size/csize from cen
+                Entry e = zipfs.getEntry(name);  // get the size/csize from cen
                 if (e == null)
                     throw new ZipException("loc: name not found in cen");
                 size = e.size;
--- a/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java	Mon Dec 28 19:03:18 2015 -0800
+++ b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java	Wed Dec 30 16:15:21 2015 +0000
@@ -100,7 +100,11 @@
             }
             ZipFileSystem zipfs = null;
             try {
-                zipfs = new ZipFileSystem(this, path, env);
+                if (env.containsKey("multi-release")) {
+                    zipfs = new JarFileSystem(this, path, env);
+                } else {
+                    zipfs = new ZipFileSystem(this, path, env);
+                }
             } catch (ZipException ze) {
                 String pname = path.toString();
                 if (pname.endsWith(".zip") || pname.endsWith(".jar"))
@@ -124,8 +128,14 @@
             throw new UnsupportedOperationException();
         }
         ensureFile(path);
-        try {
-            return new ZipFileSystem(this, path, env);
+         try {
+             ZipFileSystem zipfs;
+             if (env.containsKey("multi-release")) {
+                 zipfs = new JarFileSystem(this, path, env);
+             } else {
+                 zipfs = new ZipFileSystem(this, path, env);
+             }
+            return zipfs;
         } catch (ZipException ze) {
             String pname = path.toString();
             if (pname.endsWith(".zip") || pname.endsWith(".jar"))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/jdk/nio/zipfs/MultiReleaseJarTest.java	Wed Dec 30 16:15:21 2015 +0000
@@ -0,0 +1,195 @@
+/*
+ * 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 8144355
+ * @summary Test aliasing additions to ZipFileSystem for multi-release jar files
+ * @library /lib/testlibrary/java/util/jar
+ * @build Compiler JarBuilder CreateMultiReleaseTestJars
+ * @run testng MultiReleaseJarTest
+ */
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.net.URI;
+import java.nio.file.*;
+import java.util.HashMap;
+import java.util.Map;
+
+import static sun.misc.Version.jdkMajorVersion;
+
+import org.testng.Assert;
+import org.testng.annotations.*;
+
+public class MultiReleaseJarTest {
+    final private String userdir = System.getProperty("user.dir",".");
+    final private Map<String,String> stringEnv = new HashMap<>();
+    final private Map<String,Integer> integerEnv = new HashMap<>();
+    final private String className = "version.Version";
+    final private MethodType mt = MethodType.methodType(int.class);
+
+    private String entryName;
+    private URI uvuri;
+    private URI mruri;
+    private URI smruri;
+
+    @BeforeClass
+    public void initialize() throws Exception {
+        CreateMultiReleaseTestJars creator =  new CreateMultiReleaseTestJars();
+        creator.compileEntries();
+        creator.buildUnversionedJar();
+        creator.buildMultiReleaseJar();
+        creator.buildShortMultiReleaseJar();
+        String ssp = Paths.get(userdir, "unversioned.jar").toUri().toString();
+        uvuri = new URI("jar", ssp , null);
+        ssp = Paths.get(userdir, "multi-release.jar").toUri().toString();
+        mruri = new URI("jar", ssp, null);
+        ssp = Paths.get(userdir, "short-multi-release.jar").toUri().toString();
+        smruri = new URI("jar", ssp, null);
+        entryName = className.replace('.', '/') + ".class";
+    }
+
+    public void close() throws IOException {
+        Files.delete(Paths.get(userdir, "unversioned.jar"));
+        Files.delete(Paths.get(userdir, "multi-release.jar"));
+        Files.delete(Paths.get(userdir, "short-multi-release.jar"));
+    }
+
+    @DataProvider(name="strings")
+    public Object[][] createStrings() {
+        return new Object[][]{
+                {"runtime", jdkMajorVersion()},
+                {"-20", 8},
+                {"0", 8},
+                {"8", 8},
+                {"9", 9},
+                {"10", 10},
+                {"11", 10},
+                {"50", 10}
+        };
+    }
+
+    @DataProvider(name="integers")
+    public Object[][] createIntegers() {
+        return new Object[][] {
+                {new Integer(-5), 8},
+                {new Integer(0), 8},
+                {new Integer(8), 8},
+                {new Integer(9), 9},
+                {new Integer(10), 10},
+                {new Integer(11), 10},
+                {new Integer(100), 10}
+        };
+    }
+
+    // Not the best test but all I can do since ZipFileSystem and JarFileSystem
+    // are not public, so I can't use (fs instanceof ...)
+    @Test
+    public void testNewFileSystem() throws Exception {
+        Map<String,String> env = new HashMap<>();
+        // no configuration, treat multi-release jar as unversioned
+        try (FileSystem fs = FileSystems.newFileSystem(mruri, env)) {
+            Assert.assertTrue(readAndCompare(fs, 8));
+        }
+        env.put("multi-release", "runtime");
+        // a configuration and jar file is multi-release
+        try (FileSystem fs = FileSystems.newFileSystem(mruri, env)) {
+            Assert.assertTrue(readAndCompare(fs, jdkMajorVersion()));
+        }
+        // a configuration but jar file is unversioned
+        try (FileSystem fs = FileSystems.newFileSystem(uvuri, env)) {
+            Assert.assertTrue(readAndCompare(fs, 8));
+        }
+    }
+
+    private boolean readAndCompare(FileSystem fs, int expected) throws IOException {
+        Path path = fs.getPath("version/Version.java");
+        String src = new String(Files.readAllBytes(path));
+        return src.contains("return " + expected);
+    }
+
+    @Test(dataProvider="strings")
+    public void testStrings(String value, int expected) throws Throwable {
+        stringEnv.put("multi-release", value);
+        runTest(stringEnv, expected);
+    }
+
+    @Test(dataProvider="integers")
+    public void testIntegers(Integer value, int expected) throws Throwable {
+        integerEnv.put("multi-release", value);
+        runTest(integerEnv, expected);
+    }
+
+    @Test
+    public void testShortJar() throws Throwable {
+        integerEnv.put("multi-release", Integer.valueOf(10));
+        runTest(smruri, integerEnv, 10);
+        integerEnv.put("multi-release", Integer.valueOf(9));
+        runTest(smruri, integerEnv, 8);
+    }
+
+    private void runTest(Map<String,?> env, int expected) throws Throwable {
+        runTest(mruri, env, expected);
+    }
+
+    private void runTest(URI uri, Map<String,?> env, int expected) throws Throwable {
+        try (FileSystem fs = FileSystems.newFileSystem(uri, env)) {
+            Path version = fs.getPath(entryName);
+            byte [] bytes = Files.readAllBytes(version);
+            Class<?> vcls = (new ByteArrayClassLoader(fs)).defineClass(className, bytes);
+            MethodHandle mh = MethodHandles.lookup().findVirtual(vcls, "getVersion", mt);
+            Assert.assertEquals((int)mh.invoke(vcls.newInstance()), expected);
+        }
+    }
+
+    private static class ByteArrayClassLoader extends ClassLoader {
+        final private FileSystem fs;
+
+        ByteArrayClassLoader(FileSystem fs) {
+            super(null);
+            this.fs = fs;
+        }
+
+        @Override
+        public Class<?> loadClass(String name) throws ClassNotFoundException {
+            try {
+                return super.loadClass(name);
+            } catch (ClassNotFoundException x) {}
+            Path cls = fs.getPath(name.replace('.', '/') + ".class");
+            try {
+                byte[] bytes = Files.readAllBytes(cls);
+                return defineClass(name, bytes);
+            } catch (IOException x) {
+                throw new ClassNotFoundException(x.getMessage());
+            }
+        }
+
+        public Class<?> defineClass(String name, byte[] bytes) throws ClassNotFoundException {
+            if (bytes == null) throw new ClassNotFoundException("No bytes for " + name);
+            return defineClass(name, bytes, 0, bytes.length);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/lib/testlibrary/java/util/jar/Compiler.java	Wed Dec 30 16:15:21 2015 +0000
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.tools.*;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+class Compiler {
+    final private Map<String,String> input;
+    private List<String> options;
+
+    Compiler(Map<String,String> input) {
+        this.input = input;
+    }
+
+    Compiler setRelease(int release) {
+        // Setting the -release option does not work for some reason
+        // so do it the old fashioned way
+        // options = Arrays.asList("-release", String.valueOf(release));
+        String target = String.valueOf(release);
+        options = Arrays.asList("-source", target, "-target", target);
+        return this;
+    }
+
+    Map<String,byte[]> compile() {
+        List<SourceFileObject> cunits = createCompilationUnits();
+        Map<String,ClassFileObject> cfos = createClassFileObjects();
+        JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
+        JavaFileManager jfm = new CustomFileManager(jc.getStandardFileManager(null, null, null), cfos);
+        jc.getTask(null, jfm, null, options, null, cunits).call();
+        return createOutput(cfos);
+    }
+
+    private List<SourceFileObject> createCompilationUnits() {
+        return input.entrySet().stream()
+                .map(e -> new SourceFileObject(e.getKey(), e.getValue())).collect(Collectors.toList());
+    }
+
+    private Map<String,ClassFileObject> createClassFileObjects() {
+        return input.keySet().stream()
+                .collect(Collectors.toMap(k -> k, k -> new ClassFileObject(k)));
+    }
+
+    private Map<String,byte[]> createOutput(Map<String,ClassFileObject> cfos) {
+        return cfos.keySet().stream().collect(Collectors.toMap(k -> k, k -> cfos.get(k).getBytes()));
+    }
+
+    private static class SourceFileObject extends SimpleJavaFileObject {
+        private final String source;
+
+        SourceFileObject(String name, String source) {
+            super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
+            this.source = source;
+        }
+
+        @Override
+        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+            return source;
+        }
+    }
+
+    private static class ClassFileObject extends SimpleJavaFileObject {
+        private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        ClassFileObject(String className) {
+            super(URI.create(className), Kind.CLASS);
+        }
+
+        @Override
+        public OutputStream openOutputStream() throws IOException {
+            return baos;
+        }
+
+        public byte[] getBytes() {
+            return baos.toByteArray();
+        }
+    }
+
+    private static class CustomFileManager extends ForwardingJavaFileManager<JavaFileManager> {
+        private final Map<String,ClassFileObject> cfos;
+
+        CustomFileManager(JavaFileManager jfm, Map<String,ClassFileObject> cfos) {
+            super(jfm);
+            this.cfos = cfos;
+        }
+
+        @Override
+        public JavaFileObject getJavaFileForOutput(JavaFileManager.Location loc, String name,
+                                                   JavaFileObject.Kind kind, FileObject sibling) throws IOException {
+            ClassFileObject cfo = cfos.get(name);
+            return cfo;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/lib/testlibrary/java/util/jar/CreateMultiReleaseTestJars.java	Wed Dec 30 16:15:21 2015 +0000
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+public class CreateMultiReleaseTestJars {
+    final private String main =
+            "package version;\n\n"
+            + "public class Main {\n"
+            + "    public static void main(String[] args) {\n"
+            + "        Version v = new Version();\n"
+            + "        System.out.println(\"I am running on version \" + v.getVersion());\n"
+            + "    }\n"
+            + "}\n";
+    final private String java8 =
+            "package version;\n\n"
+            + "public class Version {\n"
+            + "    public int getVersion() {\n"
+            + "        return 8;\n"
+            + "    }\n"
+            + "}\n";
+    final private String java9 =
+            "package version;\n\n"
+            + "public class Version {\n"
+            + "    public int getVersion() {\n"
+            + "        int version = (new PackagePrivate()).getVersion();\n"
+            + "        if (version == 9) return 9;\n"  // strange I know, but easy to test
+            + "        return version;\n"
+            + "    }\n"
+            + "}\n";
+    final private String ppjava9 =
+            "package version;\n\n"
+            + "class PackagePrivate {\n"
+            + "    int getVersion() {\n"
+            + "        return 9;\n"
+            + "    }\n"
+            + "}\n";
+    final private String java10 = java8.replace("8", "10");
+    final String readme8 = "This is the root readme file";
+    final String readme9 = "This is the version nine readme file";
+    final String readme10 = "This is the version ten readme file";
+    private Map<String,byte[]> rootClasses;
+    private Map<String,byte[]> version9Classes;
+    private Map<String,byte[]> version10Classes;
+
+    public void buildUnversionedJar() throws IOException {
+        JarBuilder jb = new JarBuilder("unversioned.jar");
+        jb.addEntry("README", readme8.getBytes());
+        jb.addEntry("version/Main.java", main.getBytes());
+        jb.addEntry("version/Main.class", rootClasses.get("version.Main"));
+        jb.addEntry("version/Version.java", java8.getBytes());
+        jb.addEntry("version/Version.class", rootClasses.get("version.Version"));
+        jb.build();
+    }
+
+    public void buildMultiReleaseJar() throws IOException {
+        JarBuilder jb = new JarBuilder("multi-release.jar");
+        jb.addAttribute("Multi-Release", "true");
+        jb.addEntry("README", readme8.getBytes());
+        jb.addEntry("version/Main.java", main.getBytes());
+        jb.addEntry("version/Main.class", rootClasses.get("version.Main"));
+        jb.addEntry("version/Version.java", java8.getBytes());
+        jb.addEntry("version/Version.class", rootClasses.get("version.Version"));
+        jb.addEntry("META-INF/versions/9/README", readme9.getBytes());
+        jb.addEntry("META-INF/versions/9/version/Version.java", java9.getBytes());
+        jb.addEntry("META-INF/versions/9/version/PackagePrivate.java", ppjava9.getBytes());
+        jb.addEntry("META-INF/versions/9/version/Version.class", version9Classes.get("version.Version"));
+        jb.addEntry("META-INF/versions/9/version/PackagePrivate.class", version9Classes.get("version.PackagePrivate"));
+        jb.addEntry("META-INF/versions/10/README", readme10.getBytes());
+        jb.addEntry("META-INF/versions/10/version/Version.java", java10.getBytes());
+        jb.addEntry("META-INF/versions/10/version/Version.class", version10Classes.get("version.Version"));
+        jb.build();
+    }
+
+    public void buildShortMultiReleaseJar() throws IOException {
+        JarBuilder jb = new JarBuilder("short-multi-release.jar");
+        jb.addAttribute("Multi-Release", "true");
+        jb.addEntry("README", readme8.getBytes());
+        jb.addEntry("version/Main.java", main.getBytes());
+        jb.addEntry("version/Main.class", rootClasses.get("version.Main"));
+        jb.addEntry("version/Version.java", java8.getBytes());
+        jb.addEntry("version/Version.class", rootClasses.get("version.Version"));
+        jb.addEntry("META-INF/versions/9/README", readme9.getBytes());
+        jb.addEntry("META-INF/versions/9/version/Version.java", java9.getBytes());
+        jb.addEntry("META-INF/versions/9/version/PackagePrivate.java", ppjava9.getBytes());
+        // no entry for META-INF/versions/9/version/Version.class
+        jb.addEntry("META-INF/versions/9/version/PackagePrivate.class", version9Classes.get("version.PackagePrivate"));
+        jb.addEntry("META-INF/versions/10/README", readme10.getBytes());
+        jb.addEntry("META-INF/versions/10/version/Version.java", java10.getBytes());
+        jb.addEntry("META-INF/versions/10/version/Version.class", version10Classes.get("version.Version"));
+        jb.build();
+    }
+
+    public void buildSignedMultiReleaseJar() throws Exception {
+        String testsrc = System.getProperty("test.src",".");
+        String testdir = findTestDir(testsrc);
+        String keystore = testdir + "/sun/security/tools/jarsigner/JarSigning.keystore";
+        String[] jsArgs = {
+                "-keystore", keystore,
+                "-storepass", "bbbbbb",
+                "-signedJar", "signed-multi-release.jar",
+                "multi-release.jar", "b"
+        };
+        sun.security.tools.jarsigner.Main.main(jsArgs);
+
+    }
+
+    String findTestDir(String dir) throws IOException {
+        Path path = Paths.get(dir).toAbsolutePath();
+        while (path != null && !path.endsWith("test")) {
+            path = path.getParent();
+        }
+        if (path == null) {
+            throw new IllegalArgumentException(dir + " is not in a test directory");
+        }
+        if (!Files.isDirectory(path)) {
+            throw new IOException(path.toString() + " is not a directory");
+        }
+        return path.toString();
+    }
+
+    void compileEntries() {
+        Map<String,String> input = new HashMap<>();
+        input.put("version.Main", main);
+        input.put("version.Version", java8);
+        rootClasses = (new Compiler(input)).setRelease(8).compile();
+        input.clear();
+        input.put("version.Version", java9);
+        input.put("version.PackagePrivate", ppjava9);
+        version9Classes = (new Compiler(input)).setRelease(9).compile();
+        input.clear();
+        input.put("version.Version", java10);
+        version10Classes = (new Compiler(input)).setRelease(9).compile();  // fixme in JDK 10
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/lib/testlibrary/java/util/jar/JarBuilder.java	Wed Dec 30 16:15:21 2015 +0000
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+public class JarBuilder {
+    final private String name;
+    final private Attributes attributes = new Attributes();
+    final private List<Entry> entries = new ArrayList<>();
+
+    public JarBuilder(String name) {
+        this.name = name;
+        attributes.putValue("Manifest-Version", "1.0");
+        attributes.putValue("Created-By", "1.9.0-internal (Oracle Corporation)");
+    }
+
+    public JarBuilder addAttribute(String name, String value) {
+        attributes.putValue(name, value);
+        return this;
+    }
+
+    public JarBuilder addEntry(String name, byte[] bytes) {
+        entries.add(new Entry(name, bytes));
+        return this;
+    }
+
+    public void build() throws IOException {
+        try (OutputStream os = Files.newOutputStream(Paths.get(name));
+             JarOutputStream jos = new JarOutputStream(os)) {
+            JarEntry me = new JarEntry("META-INF/MANIFEST.MF");
+            jos.putNextEntry(me);
+            Manifest manifest = new Manifest();
+            manifest.getMainAttributes().putAll(attributes);
+            manifest.write(jos);
+            jos.closeEntry();
+            entries.forEach(e -> {
+                JarEntry je = new JarEntry(e.name);
+                try {
+                    jos.putNextEntry(je);
+                    jos.write(e.bytes);
+                    jos.closeEntry();
+                } catch (IOException iox) {
+                    throw new RuntimeException(iox);
+                }
+            });
+        } catch (RuntimeException x) {
+            Throwable t = x.getCause();
+            if (t instanceof IOException) {
+                IOException iox = (IOException)t;
+                throw iox;
+            }
+            throw x;
+        }
+    }
+
+    private static class Entry {
+        String name;
+        byte[] bytes;
+
+        Entry(String name, byte[] bytes) {
+            this.name = name;
+            this.bytes = bytes;
+        }
+    }
+
+    public static void main(String[] args) throws IOException {
+        JarBuilder jb = new JarBuilder("version.jar");
+        jb.addAttribute("Multi-Release", "true");
+        String s = "something to say";
+        byte[] bytes = s.getBytes();
+        jb.addEntry("version/Version.class", bytes);
+        jb.addEntry("README", bytes);
+        jb.addEntry("version/Version.java", bytes);
+        jb.build();
+    }
+}