8114827: JDK 9 multi-release enabled jar tool
authorsdrach
Fri, 10 Jun 2016 13:57:51 -0700
changeset 39313 0970a9a1de13
parent 39312 4bb639021aad
child 39314 779f1d11a746
8114827: JDK 9 multi-release enabled jar tool Reviewed-by: chegar Contributed-by: steve.drach@oracle.com
jdk/src/java.base/share/classes/jdk/internal/util/jar/JarIndex.java
jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java
jdk/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties
jdk/test/tools/jar/compat/CLICompatibility.java
jdk/test/tools/jar/multiRelease/Basic.java
jdk/test/tools/jar/multiRelease/data/test01/base/version/Main.java
jdk/test/tools/jar/multiRelease/data/test01/base/version/Version.java
jdk/test/tools/jar/multiRelease/data/test01/v10/version/Version.java
jdk/test/tools/jar/multiRelease/data/test01/v9/version/Version.java
--- a/jdk/src/java.base/share/classes/jdk/internal/util/jar/JarIndex.java	Wed Jun 29 08:30:49 2016 +0200
+++ b/jdk/src/java.base/share/classes/jdk/internal/util/jar/JarIndex.java	Fri Jun 10 13:57:51 2016 -0700
@@ -222,7 +222,8 @@
                 // Any files in META-INF/ will be indexed explicitly
                 if (fileName.equals("META-INF/") ||
                     fileName.equals(INDEX_NAME) ||
-                    fileName.equals(JarFile.MANIFEST_NAME))
+                    fileName.equals(JarFile.MANIFEST_NAME) ||
+                    fileName.startsWith("META-INF/versions/"))
                     continue;
 
                 if (!metaInfFilenames || !fileName.startsWith("META-INF/")) {
--- a/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Wed Jun 29 08:30:49 2016 +0200
+++ b/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Fri Jun 10 13:57:51 2016 -0700
@@ -48,6 +48,7 @@
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import java.util.zip.*;
 import java.util.jar.*;
 import java.util.jar.Pack200.*;
@@ -77,24 +78,82 @@
     PrintStream out, err;
     String fname, mname, ename;
     String zname = "";
-    String[] files;
     String rootjar = null;
 
-    // An entryName(path)->File map generated during "expand", it helps to
+    private static final int BASE_VERSION = 0;
+
+    class Entry {
+        final String basename;
+        final String entryname;
+        final File file;
+        final boolean isDir;
+
+        Entry(int version, File file) {
+            this.file = file;
+            String path = file.getPath();
+            if (file.isDirectory()) {
+                isDir = true;
+                path = path.endsWith(File.separator) ? path :
+                            path + File.separator;
+            } else {
+                isDir = false;
+            }
+            EntryName en = new EntryName(path, version);
+            basename = en.baseName;
+            entryname = en.entryName;
+        }
+    }
+
+    class EntryName {
+        final String baseName;
+        final String entryName;
+
+        EntryName(String name, int version) {
+            name = name.replace(File.separatorChar, '/');
+            String matchPath = "";
+            for (String path : pathsMap.get(version)) {
+                if (name.startsWith(path)
+                        && (path.length() > matchPath.length())) {
+                    matchPath = path;
+                }
+            }
+            name = safeName(name.substring(matchPath.length()));
+            // the old implementaton doesn't remove
+            // "./" if it was led by "/" (?)
+            if (name.startsWith("./")) {
+                name = name.substring(2);
+            }
+            this.baseName = name;
+            this.entryName = (version > BASE_VERSION)
+                    ? VERSIONS_DIR + version + "/" + this.baseName
+                    : this.baseName;
+        }
+    }
+
+    // An entryName(path)->Entry map generated during "expand", it helps to
     // decide whether or not an existing entry in a jar file needs to be
     // replaced, during the "update" operation.
-    Map<String, File> entryMap = new HashMap<String, File>();
+    Map<String, Entry> entryMap = new HashMap<>();
 
-    // All files need to be added/updated.
-    Set<File> entries = new LinkedHashSet<File>();
+    // All entries need to be added/updated.
+    Map<String, Entry> entries = new LinkedHashMap<>();
+
     // All packages.
     Set<String> packages = new HashSet<>();
     // All actual entries added, or existing, in the jar file ( excl manifest
     // and module-info.class ). Populated during create or update.
     Set<String> jarEntries = new HashSet<>();
 
-    // Directories specified by "-C" operation.
-    Set<String> paths = new HashSet<String>();
+    // A paths Set for each version, where each Set contains directories
+    // specified by the "-C" operation.
+    Map<Integer,Set<String>> pathsMap = new HashMap<>();
+
+    // There's also a files array per version
+    Map<Integer,String[]> filesMap = new HashMap<>();
+
+    // Do we think this is a multi-release jar?  Set to true
+    // if --release option found followed by at least file
+    boolean isMultiRelease;
 
     /*
      * cflag: create
@@ -241,10 +300,15 @@
                     if (ename != null) {
                         addMainClass(manifest, ename);
                     }
+                    if (isMultiRelease) {
+                        addMultiRelease(manifest);
+                    }
                 }
                 Map<String,Path> moduleInfoPaths = new HashMap<>();
-                expand(null, files, false, moduleInfoPaths);
-
+                for (int version : filesMap.keySet()) {
+                    String[] files = filesMap.get(version);
+                    expand(null, files, false, moduleInfoPaths, version);
+                }
                 Map<String,byte[]> moduleInfos = new LinkedHashMap<>();
                 if (!moduleInfoPaths.isEmpty()) {
                     if (!checkModuleInfos(moduleInfoPaths))
@@ -348,7 +412,10 @@
                     (new FileInputStream(mname)) : null;
 
                 Map<String,Path> moduleInfoPaths = new HashMap<>();
-                expand(null, files, true, moduleInfoPaths);
+                for (int version : filesMap.keySet()) {
+                    String[] files = filesMap.get(version);
+                    expand(null, files, true, moduleInfoPaths, version);
+                }
 
                 Map<String,byte[]> moduleInfos = new HashMap<>();
                 for (Map.Entry<String,Path> e : moduleInfoPaths.entrySet())
@@ -381,10 +448,11 @@
                     tmpFile.delete();
                 }
             } else if (tflag) {
-                replaceFSC(files);
+                replaceFSC(filesMap);
                 // For the "list table contents" action, access using the
                 // ZipFile class is always most efficient since only a
                 // "one-finger" scan through the central directory is required.
+                String[] files = filesMapToFiles(filesMap);
                 if (fname != null) {
                     list(fname, files);
                 } else {
@@ -396,7 +464,7 @@
                     }
                 }
             } else if (xflag) {
-                replaceFSC(files);
+                replaceFSC(filesMap);
                 // For the extract action, when extracting all the entries,
                 // access using the ZipInputStream class is most efficient,
                 // since only a single sequential scan through the zip file is
@@ -406,6 +474,8 @@
                 // "leading garbage", we fall back from the ZipInputStream
                 // implementation to the ZipFile implementation, since only the
                 // latter can handle it.
+
+                String[] files = filesMapToFiles(filesMap);
                 if (fname != null && files != null) {
                     extract(fname, files);
                 } else {
@@ -421,6 +491,7 @@
                     }
                 }
             } else if (iflag) {
+                String[] files = filesMap.get(BASE_VERSION);  // base entries only, can be null
                 genIndex(rootjar, files);
             } else if (printModuleDescriptor) {
                 boolean found;
@@ -449,6 +520,20 @@
         return ok;
     }
 
+    private String[] filesMapToFiles(Map<Integer,String[]> filesMap) {
+        if (filesMap.isEmpty()) return null;
+        return filesMap.entrySet()
+                .stream()
+                .flatMap(this::filesToEntryNames)
+                .toArray(String[]::new);
+    }
+
+    Stream<String> filesToEntryNames(Map.Entry<Integer,String[]> fileEntries) {
+        int version = fileEntries.getKey();
+        return Stream.of(fileEntries.getValue())
+                .map(f -> (new EntryName(f, version)).entryName);
+    }
+
     /**
      * Parses command line arguments.
      */
@@ -579,8 +664,10 @@
         /* parse file arguments */
         int n = args.length - count;
         if (n > 0) {
+            int version = BASE_VERSION;
             int k = 0;
             String[] nameBuf = new String[n];
+            pathsMap.put(version, new HashSet<>());
             try {
                 for (int i = count; i < args.length; i++) {
                     if (args[i].equals("-C")) {
@@ -592,8 +679,33 @@
                         while (dir.indexOf("//") > -1) {
                             dir = dir.replace("//", "/");
                         }
-                        paths.add(dir.replace(File.separatorChar, '/'));
+                        pathsMap.get(version).add(dir.replace(File.separatorChar, '/'));
                         nameBuf[k++] = dir + args[++i];
+                    } else if (args[i].startsWith("--release")) {
+                        int v = BASE_VERSION;
+                        try {
+                            v = Integer.valueOf(args[++i]);
+                        } catch (NumberFormatException x) {
+                            error(formatMsg("error.release.value.notnumber", args[i]));
+                            // this will fall into the next error, thus returning false
+                        }
+                        if (v < 9) {
+                            error(formatMsg("error.release.value.toosmall", String.valueOf(v)));
+                            usageError();
+                            return false;
+                        }
+                        // associate the files, if any, with the previous version number
+                        if (k > 0) {
+                            String[] files = new String[k];
+                            System.arraycopy(nameBuf, 0, files, 0, k);
+                            filesMap.put(version, files);
+                            isMultiRelease = version > BASE_VERSION;
+                        }
+                        // reset the counters and start with the new version number
+                        k = 0;
+                        nameBuf = new String[n];
+                        version = v;
+                        pathsMap.put(version, new HashSet<>());
                     } else {
                         nameBuf[k++] = args[i];
                     }
@@ -602,8 +714,13 @@
                 usageError();
                 return false;
             }
-            files = new String[k];
-            System.arraycopy(nameBuf, 0, files, 0, k);
+            // associate remaining files, if any, with a version
+            if (k > 0) {
+                String[] files = new String[k];
+                System.arraycopy(nameBuf, 0, files, 0, k);
+                filesMap.put(version, files);
+                isMultiRelease = version > BASE_VERSION;
+            }
         } else if (cflag && (mname == null)) {
             error(getMsg("error.bad.cflag"));
             usageError();
@@ -651,7 +768,8 @@
     void expand(File dir,
                 String[] files,
                 boolean isUpdate,
-                Map<String,Path> moduleInfoPaths)
+                Map<String,Path> moduleInfoPaths,
+                int version)
         throws IOException
     {
         if (files == null)
@@ -664,29 +782,29 @@
             else
                 f = new File(dir, files[i]);
 
+            Entry entry = new Entry(version, f);
+            String entryName = entry.entryname;
+
             if (f.isFile()) {
-                String path = f.getPath();
-                String entryName = entryName(path);
                 if (entryName.endsWith(MODULE_INFO)) {
                     moduleInfoPaths.put(entryName, f.toPath());
                     if (isUpdate)
-                        entryMap.put(entryName, f);
-                } else if (entries.add(f)) {
+                        entryMap.put(entryName, entry);
+                } else if (!entries.containsKey(entryName)) {
+                    entries.put(entryName, entry);
                     jarEntries.add(entryName);
-                    if (path.endsWith(".class") && !entryName.startsWith(VERSIONS_DIR))
-                        packages.add(toPackageName(entryName));
+                    if (entry.basename.endsWith(".class") && !entryName.startsWith(VERSIONS_DIR))
+                        packages.add(toPackageName(entry.basename));
                     if (isUpdate)
-                        entryMap.put(entryName, f);
+                        entryMap.put(entryName, entry);
                 }
             } else if (f.isDirectory()) {
-                if (entries.add(f)) {
+                if (!entries.containsKey(entryName)) {
+                    entries.put(entryName, entry);
                     if (isUpdate) {
-                        String dirPath = f.getPath();
-                        dirPath = (dirPath.endsWith(File.separator)) ? dirPath :
-                            (dirPath + File.separator);
-                        entryMap.put(entryName(dirPath), f);
+                        entryMap.put(entryName, entry);
                     }
-                    expand(f, f.list(), isUpdate, moduleInfoPaths);
+                    expand(f, f.list(), isUpdate, moduleInfoPaths, version);
                 }
             } else {
                 error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
@@ -740,8 +858,9 @@
             in.transferTo(zos);
             zos.closeEntry();
         }
-        for (File file: entries) {
-            addFile(zos, file);
+        for (String entryname : entries.keySet()) {
+            Entry entry = entries.get(entryname);
+            addFile(zos, entry);
         }
         zos.close();
     }
@@ -823,7 +942,7 @@
                 || (Mflag && isManifestEntry)) {
                 continue;
             } else if (isManifestEntry && ((newManifest != null) ||
-                        (ename != null))) {
+                        (ename != null) || isMultiRelease)) {
                 foundManifest = true;
                 if (newManifest != null) {
                     // Don't read from the newManifest InputStream, as we
@@ -862,21 +981,21 @@
                     zos.putNextEntry(e2);
                     copy(zis, zos);
                 } else { // replace with the new files
-                    File f = entryMap.get(name);
-                    addFile(zos, f);
+                    Entry ent = entryMap.get(name);
+                    addFile(zos, ent);
                     entryMap.remove(name);
-                    entries.remove(f);
+                    entries.remove(name);
                 }
 
                 jarEntries.add(name);
-                if (name.endsWith(".class"))
+                if (name.endsWith(".class") && !(name.startsWith(VERSIONS_DIR)))
                     packages.add(toPackageName(name));
             }
         }
 
         // add the remaining new files
-        for (File f: entries) {
-            addFile(zos, f);
+        for (String entryname : entries.keySet()) {
+            addFile(zos, entries.get(entryname));
         }
         if (!foundManifest) {
             if (newManifest != null) {
@@ -961,6 +1080,9 @@
         if (ename != null) {
             addMainClass(m, ename);
         }
+        if (isMultiRelease) {
+            addMultiRelease(m);
+        }
         ZipEntry e = new ZipEntry(MANIFEST_NAME);
         e.setTime(System.currentTimeMillis());
         if (flag0) {
@@ -1016,24 +1138,6 @@
         return name;
     }
 
-    private String entryName(String name) {
-        name = name.replace(File.separatorChar, '/');
-        String matchPath = "";
-        for (String path : paths) {
-            if (name.startsWith(path)
-                && (path.length() > matchPath.length())) {
-                matchPath = path;
-            }
-        }
-        name = safeName(name.substring(matchPath.length()));
-        // the old implementaton doesn't remove
-        // "./" if it was led by "/" (?)
-        if (name.startsWith("./")) {
-            name = name.substring(2);
-        }
-        return name;
-    }
-
     private void addVersion(Manifest m) {
         Attributes global = m.getMainAttributes();
         if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) {
@@ -1058,6 +1162,11 @@
         global.put(Attributes.Name.MAIN_CLASS, mainApp);
     }
 
+    private void addMultiRelease(Manifest m) {
+        Attributes global = m.getMainAttributes();
+        global.put(Attributes.Name.MULTI_RELEASE, "true");
+    }
+
     private boolean isAmbiguousMainClass(Manifest m) {
         if (ename != null) {
             Attributes global = m.getMainAttributes();
@@ -1073,14 +1182,10 @@
     /**
      * Adds a new file entry to the ZIP output stream.
      */
-    void addFile(ZipOutputStream zos, File file) throws IOException {
-        String name = file.getPath();
-        boolean isDir = file.isDirectory();
-        if (isDir) {
-            name = name.endsWith(File.separator) ? name :
-                (name + File.separator);
-        }
-        name = entryName(name);
+    void addFile(ZipOutputStream zos, Entry entry) throws IOException {
+        File file = entry.file;
+        String name = entry.entryname;
+        boolean isDir = entry.isDir;
 
         if (name.equals("") || name.equals(".") || name.equals(zname)) {
             return;
@@ -1221,12 +1326,15 @@
         os.updateEntry(e);
     }
 
-    void replaceFSC(String files[]) {
-        if (files != null) {
-            for (int i = 0; i < files.length; i++) {
-                files[i] = files[i].replace(File.separatorChar, '/');
+    void replaceFSC(Map<Integer, String []> filesMap) {
+        filesMap.keySet().forEach(version -> {
+            String[] files = filesMap.get(version);
+            if (files != null) {
+                for (int i = 0; i < files.length; i++) {
+                    files[i] = files[i].replace(File.separatorChar, '/');
+                }
             }
-        }
+        });
     }
 
     @SuppressWarnings("serial")
@@ -1566,7 +1674,7 @@
     /**
      * Print an error message; like something is broken
      */
-    protected void error(String s) {
+    void error(String s) {
         err.println(s);
     }
 
--- a/jdk/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties	Wed Jun 29 08:30:49 2016 +0200
+++ b/jdk/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties	Fri Jun 10 13:57:51 2016 -0700
@@ -63,24 +63,28 @@
 error.module.descriptor.not.found=\
         Module descriptor not found
 error.versioned.info.without.root=\
-        module-info.class found in versioned section without module-info.class \
+        module-info.class found in a versioned directory without module-info.class \
         in the root
 error.versioned.info.name.notequal=\
-        module-info.class in versioned section contains incorrect name
+        module-info.class in a versioned directory contains incorrect name
 error.versioned.info.requires.public=\
-        module-info.class in versioned section contains additional requires public
+        module-info.class in a versioned directory contains additional requires public
 error.versioned.info.requires.added=\
-        module-info.class in versioned section contains additional requires
+        module-info.class in a versioned directory contains additional requires
 error.versioned.info.requires.dropped=\
-        module-info.class in versioned section contains missing requires
+        module-info.class in a versioned directory contains missing requires
 error.versioned.info.exports.notequal=\
-        module-info.class in versioned section contains different exports
+        module-info.class in a versioned directory contains different exports
 error.versioned.info.provides.notequal=\
-        module-info.class in versioned section contains different provides
+        module-info.class in a versioned directory contains different provides
 error.invalid.versioned.module.attribute=\
         Invalid module descriptor attribute {0}
 error.missing.provider=\
         Service provider not found: {0}
+error.release.value.notnumber=\
+        release {0} not valid
+error.release.value.toosmall=\
+        release {0} not valid, must be >= 9
 out.added.manifest=\
         added manifest
 out.added.module-info=\
@@ -109,7 +113,7 @@
 usage.compat=\
 \Compatibility Interface:\
 \n\
-Usage: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...\n\
+Usage: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files] ...\n\
 Options:\n\
 \ \   -c  create new archive\n\
 \ \   -t  list table of contents for archive\n\
@@ -141,7 +145,7 @@
 Try `jar --help' for more information.
 
 main.help.preopt=\
-Usage: jar [OPTION...] [-C dir] files ...\n\
+Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files] ...\n\
 jar creates an archive for classes and resources, and can manipulate or\n\
 restore individual classes or resources from an archive.\n\
 \n\
@@ -156,7 +160,9 @@
 \     -C foo/ classes resources\n\
 \ # Update an existing non-modular jar to a modular jar:\n\
 \ jar --update --file foo.jar --main-class com.foo.Main --module-version 1.0\n\
-\     -C foo/ module-info.class
+\     -C foo/ module-info.class\n\
+\ # Create a multi-release jar, placing some files in the META-INF/versions/9 directory:\n\
+\ jar --create --file mr.jar -C foo classes --release 9 -C foo9 classes
 main.help.opt.main=\
 \ Main operation mode:\n
 main.help.opt.main.create=\
@@ -178,7 +184,9 @@
 \  -C DIR                     Change to the specified directory and include the\n\
 \                             following file
 main.help.opt.any.file=\
-\  -f, --file=FILE            The archive file name
+\  -f, --file=FILE            The archive file name\n\
+\      --release VERSION      Places all following files in a versioned directory\n\
+\                             of the jar (i.e. META-INF/versions/VERSION/)
 main.help.opt.any.verbose=\
 \  -v, --verbose              Generate verbose output on standard output
 main.help.opt.create.update=\
--- a/jdk/test/tools/jar/compat/CLICompatibility.java	Wed Jun 29 08:30:49 2016 +0200
+++ b/jdk/test/tools/jar/compat/CLICompatibility.java	Fri Jun 10 13:57:51 2016 -0700
@@ -415,14 +415,14 @@
         jar("-h")
             .assertSuccess()
             .resultChecker(r ->
-                assertTrue(r.output.startsWith("Usage: jar [OPTION...] [-C dir] files"),
+                assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
                            "Failed, got [" + r.output + "]")
             );
 
         jar("--help")
             .assertSuccess()
             .resultChecker(r ->
-                assertTrue(r.output.startsWith("Usage: jar [OPTION...] [-C dir] files"),
+                assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
                            "Failed, got [" + r.output + "]")
             );
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/tools/jar/multiRelease/Basic.java	Fri Jun 10 13:57:51 2016 -0700
@@ -0,0 +1,354 @@
+/*
+ * Copyright (c) 2016, 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
+ * @library /test/lib/share/classes
+ * @modules java.base/jdk.internal.misc
+ * @build jdk.test.lib.JDKToolFinder jdk.test.lib.Platform
+ * @run testng Basic
+ */
+
+import static org.testng.Assert.*;
+
+import org.testng.annotations.*;
+
+import java.io.*;
+import java.nio.file.*;
+import java.nio.file.attribute.*;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.jar.*;
+import java.util.stream.Stream;
+import java.util.zip.*;
+
+import jdk.test.lib.JDKToolFinder;
+
+import static java.lang.String.format;
+import static java.lang.System.out;
+
+public class Basic {
+    private final String src = System.getProperty("test.src", ".");
+    private final String usr = System.getProperty("user.dir", ".");
+
+    @Test
+    // create a regular, non-multi-release jar
+    public void test00() throws IOException {
+        String jarfile = "test.jar";
+
+        compile("test01");  //use same data as test01
+
+        Path classes = Paths.get("classes");
+        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".")
+                .assertSuccess();
+
+        checkMultiRelease(jarfile, false);
+
+        Map<String,String[]> names = Map.of(
+                "version/Main.class",
+                new String[] {"base", "version", "Main.class"},
+
+                "version/Version.class",
+                new String[] {"base", "version", "Version.class"}
+        );
+
+        compare(jarfile, names);
+
+        delete(jarfile);
+        deleteDir(Paths.get(usr, "classes"));
+    }
+
+    @Test
+    // create a multi-release jar
+    public void test01() throws IOException {
+        String jarfile = "test.jar";
+
+        compile("test01");
+
+        Path classes = Paths.get("classes");
+        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+                "--release", "9", "-C", classes.resolve("v9").toString(), ".",
+                "--release", "10", "-C", classes.resolve("v10").toString(), ".")
+                .assertSuccess();
+
+        checkMultiRelease(jarfile, true);
+
+        Map<String,String[]> names = Map.of(
+                "version/Main.class",
+                new String[] {"base", "version", "Main.class"},
+
+                "version/Version.class",
+                new String[] {"base", "version", "Version.class"},
+
+                "META-INF/versions/9/version/Version.class",
+                new String[] {"v9", "version", "Version.class"},
+
+                "META-INF/versions/10/version/Version.class",
+                new String[] {"v10", "version", "Version.class"}
+        );
+
+        compare(jarfile, names);
+
+        delete(jarfile);
+        deleteDir(Paths.get(usr, "classes"));
+    }
+
+    @Test
+    // update a regular jar to a multi-release jar
+    public void test02() throws IOException {
+        String jarfile = "test.jar";
+
+        compile("test01");  //use same data as test01
+
+        Path classes = Paths.get("classes");
+        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".")
+                .assertSuccess();
+
+        checkMultiRelease(jarfile, false);
+
+        jar("uf", jarfile, "--release", "9", "-C", classes.resolve("v9").toString(), ".")
+                .assertSuccess();
+
+        checkMultiRelease(jarfile, true);
+
+        Map<String,String[]> names = Map.of(
+                "version/Main.class",
+                new String[] {"base", "version", "Main.class"},
+
+                "version/Version.class",
+                new String[] {"base", "version", "Version.class"},
+
+                "META-INF/versions/9/version/Version.class",
+                new String[] {"v9", "version", "Version.class"}
+        );
+
+        compare(jarfile, names);
+
+        delete(jarfile);
+        deleteDir(Paths.get(usr, "classes"));
+    }
+
+    @Test
+    // replace a base entry and a versioned entry
+    public void test03() throws IOException {
+        String jarfile = "test.jar";
+
+        compile("test01");  //use same data as test01
+
+        Path classes = Paths.get("classes");
+        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+                "--release", "9", "-C", classes.resolve("v9").toString(), ".")
+                .assertSuccess();
+
+        checkMultiRelease(jarfile, true);
+
+        Map<String,String[]> names = Map.of(
+                "version/Main.class",
+                new String[] {"base", "version", "Main.class"},
+
+                "version/Version.class",
+                new String[] {"base", "version", "Version.class"},
+
+                "META-INF/versions/9/version/Version.class",
+                new String[] {"v9", "version", "Version.class"}
+        );
+
+        compare(jarfile, names);
+
+        // write the v9 version/Version.class entry in base and the v10
+        // version/Version.class entry in versions/9 section
+        jar("uf", jarfile, "-C", classes.resolve("v9").toString(), "version",
+                "--release", "9", "-C", classes.resolve("v10").toString(), ".")
+                .assertSuccess();
+
+        checkMultiRelease(jarfile, true);
+
+        names = Map.of(
+                "version/Main.class",
+                new String[] {"base", "version", "Main.class"},
+
+                "version/Version.class",
+                new String[] {"v9", "version", "Version.class"},
+
+                "META-INF/versions/9/version/Version.class",
+                new String[] {"v10", "version", "Version.class"}
+        );
+
+        delete(jarfile);
+        deleteDir(Paths.get(usr, "classes"));
+    }
+
+    /*
+     *  Test Infrastructure
+     */
+    private void compile(String test) throws IOException {
+        Path classes = Paths.get(usr, "classes", "base");
+        Files.createDirectories(classes);
+        Path source = Paths.get(src, "data", test, "base", "version");
+        javac(classes, source.resolve("Main.java"), source.resolve("Version.java"));
+
+        classes = Paths.get(usr, "classes", "v9");
+        Files.createDirectories(classes);
+        source = Paths.get(src, "data", test, "v9", "version");
+        javac(classes, source.resolve("Version.java"));
+
+        classes = Paths.get(usr, "classes", "v10");
+        Files.createDirectories(classes);
+        source = Paths.get(src, "data", test, "v10", "version");
+        javac(classes, source.resolve("Version.java"));
+    }
+
+    private void checkMultiRelease(String jarFile, boolean expected) throws IOException {
+        try (JarFile jf = new JarFile(new File(jarFile), true, ZipFile.OPEN_READ,
+                JarFile.Release.RUNTIME)) {
+            assertEquals(jf.isMultiRelease(), expected);
+        }
+    }
+
+    // compares the bytes found in the jar entries with the bytes found in the
+    // corresponding data files used to create the entries
+    private void compare(String jarfile, Map<String,String[]> names) throws IOException {
+        try (JarFile jf = new JarFile(jarfile)) {
+            for (String name : names.keySet()) {
+                Path path = Paths.get("classes", names.get(name));
+                byte[] b1 = Files.readAllBytes(path);
+                byte[] b2;
+                JarEntry je = jf.getJarEntry(name);
+                try (InputStream is = jf.getInputStream(je)) {
+                    b2 = is.readAllBytes();
+                }
+                assertEquals(b1,b2);
+            }
+        }
+    }
+
+    private void delete(String name) throws IOException {
+        Files.delete(Paths.get(usr, name));
+    }
+
+    private void deleteDir(Path dir) throws IOException {
+        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                Files.delete(file);
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+                Files.delete(dir);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+    /*
+     * The following methods were taken from modular jar and other jar tests
+     */
+
+    void javac(Path dest, Path... sourceFiles) throws IOException {
+        String javac = JDKToolFinder.getJDKTool("javac");
+
+        List<String> commands = new ArrayList<>();
+        commands.add(javac);
+        commands.add("-d");
+        commands.add(dest.toString());
+        Stream.of(sourceFiles).map(Object::toString).forEach(x -> commands.add(x));
+
+        quickFail(run(new ProcessBuilder(commands)));
+    }
+
+    Result jarWithStdin(File stdinSource, String... args) {
+        String jar = JDKToolFinder.getJDKTool("jar");
+        List<String> commands = new ArrayList<>();
+        commands.add(jar);
+        Stream.of(args).forEach(x -> commands.add(x));
+        ProcessBuilder p = new ProcessBuilder(commands);
+        if (stdinSource != null)
+            p.redirectInput(stdinSource);
+        return run(p);
+    }
+
+    Result jar(String... args) {
+        return jarWithStdin(null, args);
+    }
+
+    void quickFail(Result r) {
+        if (r.ec != 0)
+            throw new RuntimeException(r.output);
+    }
+
+    Result run(ProcessBuilder pb) {
+        Process p;
+        out.printf("Running: %s%n", pb.command());
+        try {
+            p = pb.start();
+        } catch (IOException e) {
+            throw new RuntimeException(
+                    format("Couldn't start process '%s'", pb.command()), e);
+        }
+
+        String output;
+        try {
+            output = toString(p.getInputStream(), p.getErrorStream());
+        } catch (IOException e) {
+            throw new RuntimeException(
+                    format("Couldn't read process output '%s'", pb.command()), e);
+        }
+
+        try {
+            p.waitFor();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(
+                    format("Process hasn't finished '%s'", pb.command()), e);
+        }
+        return new Result(p.exitValue(), output);
+    }
+
+    String toString(InputStream in1, InputStream in2) throws IOException {
+        try (ByteArrayOutputStream dst = new ByteArrayOutputStream();
+             InputStream concatenated = new SequenceInputStream(in1, in2)) {
+            concatenated.transferTo(dst);
+            return new String(dst.toByteArray(), "UTF-8");
+        }
+    }
+
+    static class Result {
+        final int ec;
+        final String output;
+
+        private Result(int ec, String output) {
+            this.ec = ec;
+            this.output = output;
+        }
+        Result assertSuccess() {
+            assertTrue(ec == 0, format("ec: %d, output: %s", ec, output));
+            return this;
+        }
+        Result assertFailure() {
+            assertTrue(ec != 0, format("ec: %d, output: %s", ec, output));
+            return this;
+        }
+        Result resultChecker(Consumer<Result> r) { r.accept(this); return this; }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/tools/jar/multiRelease/data/test01/base/version/Main.java	Fri Jun 10 13:57:51 2016 -0700
@@ -0,0 +1,8 @@
+package version;
+
+public class Main {
+    public static void main(String[] args) {
+        Version v = new Version();
+        System.out.println("I am running on version " + v.getVersion());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/tools/jar/multiRelease/data/test01/base/version/Version.java	Fri Jun 10 13:57:51 2016 -0700
@@ -0,0 +1,13 @@
+package version;
+
+public class Version {
+    public int getVersion() {
+        return 8;
+    }
+
+    protected void doNothing() {
+    }
+
+    private void reallyDoNothing() {
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/tools/jar/multiRelease/data/test01/v10/version/Version.java	Fri Jun 10 13:57:51 2016 -0700
@@ -0,0 +1,13 @@
+package version;
+
+public class Version {
+    public int getVersion() {
+        return 10;
+    }
+
+    protected void doNothing() {
+    }
+
+    private void someName() {
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/tools/jar/multiRelease/data/test01/v9/version/Version.java	Fri Jun 10 13:57:51 2016 -0700
@@ -0,0 +1,13 @@
+package version;
+
+public class Version {
+    public int getVersion() {
+        return 9;
+    }
+
+    protected void doNothing() {
+    }
+
+    private void anyName() {
+    }
+}