From cdc0735c603890f6262873c01e6e5b8e22963ec0 Mon Sep 17 00:00:00 2001 From: Steve Drach Date: Wed, 30 Dec 2015 16:15:21 +0000 Subject: [PATCH] 8144355: JDK 9 changes to ZipFileSystem to support multi-release jar files JEP 238 Multi-Release JarFileSystem implementation Reviewed-by: alanb, psandoz, sherman --- .../classes/jdk/nio/zipfs/JarFileSystem.java | 182 ++++++++++++++++ .../classes/jdk/nio/zipfs/ZipFileSystem.java | 32 +-- .../jdk/nio/zipfs/ZipFileSystemProvider.java | 16 +- .../jdk/nio/zipfs/MultiReleaseJarTest.java | 195 ++++++++++++++++++ .../testlibrary/java/util/jar/Compiler.java | 120 +++++++++++ .../util/jar/CreateMultiReleaseTestJars.java | 160 ++++++++++++++ .../testlibrary/java/util/jar/JarBuilder.java | 105 ++++++++++ 7 files changed, 793 insertions(+), 17 deletions(-) create mode 100644 jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/JarFileSystem.java create mode 100644 jdk/test/jdk/nio/zipfs/MultiReleaseJarTest.java create mode 100644 jdk/test/lib/testlibrary/java/util/jar/Compiler.java create mode 100644 jdk/test/lib/testlibrary/java/util/jar/CreateMultiReleaseTestJars.java create mode 100644 jdk/test/lib/testlibrary/java/util/jar/JarBuilder.java diff --git a/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/JarFileSystem.java b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/JarFileSystem.java new file mode 100644 index 00000000000..ec874229fdc --- /dev/null +++ b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/JarFileSystem.java @@ -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 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 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 createVersionedLinks(int version) { + HashMap 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 getVersionMap(int version, IndexNode metaInfVersions) { + TreeMap 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 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); + } +} diff --git a/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java index e269216c081..ae01cb38460 100644 --- a/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java +++ b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java @@ -155,6 +155,10 @@ class ZipFileSystem extends FileSystem { throw new ReadOnlyFileSystemException(); } + void setReadOnly() { + this.readOnly = true; + } + @Override public Iterable getRootDirectories() { ArrayList 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; diff --git a/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java index 3cb7ca49d39..c721c2fe175 100644 --- a/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java +++ b/jdk/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java @@ -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")) diff --git a/jdk/test/jdk/nio/zipfs/MultiReleaseJarTest.java b/jdk/test/jdk/nio/zipfs/MultiReleaseJarTest.java new file mode 100644 index 00000000000..09d1c335d70 --- /dev/null +++ b/jdk/test/jdk/nio/zipfs/MultiReleaseJarTest.java @@ -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 stringEnv = new HashMap<>(); + final private Map 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 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 env, int expected) throws Throwable { + runTest(mruri, env, expected); + } + + private void runTest(URI uri, Map 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); + } + } +} diff --git a/jdk/test/lib/testlibrary/java/util/jar/Compiler.java b/jdk/test/lib/testlibrary/java/util/jar/Compiler.java new file mode 100644 index 00000000000..4b56cac5184 --- /dev/null +++ b/jdk/test/lib/testlibrary/java/util/jar/Compiler.java @@ -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 input; + private List options; + + Compiler(Map 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 compile() { + List cunits = createCompilationUnits(); + Map 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 createCompilationUnits() { + return input.entrySet().stream() + .map(e -> new SourceFileObject(e.getKey(), e.getValue())).collect(Collectors.toList()); + } + + private Map createClassFileObjects() { + return input.keySet().stream() + .collect(Collectors.toMap(k -> k, k -> new ClassFileObject(k))); + } + + private Map createOutput(Map 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 { + private final Map cfos; + + CustomFileManager(JavaFileManager jfm, Map 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; + } + } +} diff --git a/jdk/test/lib/testlibrary/java/util/jar/CreateMultiReleaseTestJars.java b/jdk/test/lib/testlibrary/java/util/jar/CreateMultiReleaseTestJars.java new file mode 100644 index 00000000000..299a9ca65db --- /dev/null +++ b/jdk/test/lib/testlibrary/java/util/jar/CreateMultiReleaseTestJars.java @@ -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 rootClasses; + private Map version9Classes; + private Map 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 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 + } +} diff --git a/jdk/test/lib/testlibrary/java/util/jar/JarBuilder.java b/jdk/test/lib/testlibrary/java/util/jar/JarBuilder.java new file mode 100644 index 00000000000..7f7449a8344 --- /dev/null +++ b/jdk/test/lib/testlibrary/java/util/jar/JarBuilder.java @@ -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 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(); + } +}