8144355: JDK 9 changes to ZipFileSystem to support multi-release jar files

JEP 238 Multi-Release JarFileSystem implementation

Reviewed-by: alanb, psandoz, sherman
This commit is contained in:
Steve Drach 2015-12-30 16:15:21 +00:00
parent bac6aa4f18
commit cdc0735c60
7 changed files with 793 additions and 17 deletions

View File

@ -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);
}
}

View File

@ -155,6 +155,10 @@ class ZipFileSystem extends FileSystem {
throw new ReadOnlyFileSystemException();
}
void setReadOnly() {
this.readOnly = true;
}
@Override
public Iterable<Path> getRootDirectories() {
ArrayList<Path> pathArr = new ArrayList<>();
@ -320,7 +324,7 @@ class ZipFileSystem extends FileSystem {
beginRead();
try {
ensureOpen();
e = getEntry0(path);
e = getEntry(path);
if (e == null) {
IndexNode inode = getInode(path);
if (inode == null)
@ -342,7 +346,7 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
beginRead();
try {
ensureOpen();
Entry e = getEntry0(path);
Entry e = getEntry(path);
if (forWrite) {
checkWritable();
if (e == null) {
@ -855,7 +859,7 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
//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 @@ class ZipFileSystem extends FileSystem {
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 @@ class ZipFileSystem extends FileSystem {
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;

View File

@ -100,7 +100,11 @@ public class ZipFileSystemProvider extends FileSystemProvider {
}
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 @@ public class ZipFileSystemProvider extends FileSystemProvider {
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"))

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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();
}
}