8114827: JDK 9 multi-release enabled jar tool
Reviewed-by: chegar
Contributed-by: steve.drach@oracle.com
--- 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() {
+ }
+}