# HG changeset patch # User sdrach # Date 1470788240 25200 # Tue Aug 09 17:17:20 2016 -0700 # Node ID 3f858ecec356b8f22d5242ac216aa661075be2f1 # Parent a670a92950d9e0a80c874b3d742ac9344bd04190 8156499: Update jlink to support creating images with modules that are packaged as multi-release JARs Reviewed-by: Contributed-by: steve.drach@oracle.com diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java @@ -29,9 +29,13 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Path; +import java.util.Comparator; +import java.util.Map; import java.util.Objects; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import jdk.tools.jlink.internal.Archive.Entry.EntryType; @@ -43,13 +47,13 @@ /** * An entry located in a jar file. */ - private class JarEntry extends Entry { + private class JarFileEntry extends Entry { private final long size; - private final ZipEntry entry; - private final ZipFile file; + private final JarEntry entry; + private final JarFile file; - JarEntry(String path, String name, EntryType type, ZipFile file, ZipEntry entry) { + JarFileEntry(String path, String name, EntryType type, JarFile file, JarEntry entry) { super(JarArchive.this, path, name, type); this.entry = Objects.requireNonNull(entry); this.file = Objects.requireNonNull(file); @@ -70,12 +74,17 @@ } } + private static final String MANIFEST = "META-INF/MANIFEST.MF"; private static final String MODULE_INFO = "module-info.class"; + private static final String VERSIONS_DIR = "META-INF/versions/"; + private static final int VERSIONS_DIR_LEN = VERSIONS_DIR.length(); private final Path file; private final String moduleName; - // currently processed ZipFile - private ZipFile zipFile; + // currently processed JarFile + private JarFile jarFile; + private int version; + private int offset; protected JarArchive(String mn, Path file) { Objects.requireNonNull(mn); @@ -97,24 +106,90 @@ @Override public Stream entries() { try { - if (zipFile == null) { + if (jarFile == null) { open(); } } catch (IOException ioe) { throw new UncheckedIOException(ioe); } - return zipFile.stream().map(this::toEntry).filter(n -> n != null); + if (jarFile.isMultiRelease()) { + // sort entries in ascending version order, extract the base name + // and add them to a map, then return the Entries as with regular jar + return jarFile.stream() + .filter(je -> !je.isDirectory()) + .filter(je -> !je.getName().equals(MANIFEST)) + .filter(this::versionAcceptable) + .sorted(entryComparator) + .collect(Collectors.toMap(this::extractBaseName, je -> je, (v1, v2) -> v2)) + .entrySet() + .stream().map(this::toEntry).filter(n -> n != null); + } + return jarFile.stream().map(this::toEntry).filter(n -> n != null); } abstract EntryType toEntryType(String entryName); abstract String getFileName(String entryName); - private Entry toEntry(ZipEntry ze) { - String name = ze.getName(); + // sort base entries before versioned entries + private Comparator entryComparator = (je1, je2) -> { + String s1 = je1.getName(); + String s2 = je2.getName(); + if (s1.equals(s2)) return 0; + boolean b1 = s1.startsWith(VERSIONS_DIR); + boolean b2 = s2.startsWith(VERSIONS_DIR); + if (b1 && !b2) return 1; + if (!b1 && b2) return -1; + int n = 0; // starting char for String compare + if (b1 && b2) { + // normally strings would be sorted so "10" goes before "9", but + // version number strings need to be sorted numerically + n = VERSIONS_DIR.length(); // skip the common prefix + int i1 = s1.indexOf('/', n); + int i2 = s1.indexOf('/', n); + if (i1 == -1) throw new RuntimeException(s1); // fixme, better message + if (i2 == -1) throw new RuntimeException(s2); // fixme, better message + // shorter version numbers go first + if (i1 != i2) return i1 - i2; + // otherwise, handle equal length numbers below + } + int l1 = s1.length(); + int l2 = s2.length(); + int lim = Math.min(l1, l2); + for (int k = n; k < lim; k++) { + char c1 = s1.charAt(k); + char c2 = s2.charAt(k); + if (c1 != c2) { + return c1 - c2; + } + } + return l1 - l2; + }; + + // must be invoked after versionAcceptable + private String extractBaseName(JarEntry je) { + String name = je.getName(); + if (name.startsWith(VERSIONS_DIR)) { + return name.substring(VERSIONS_DIR_LEN + offset + 1); + } + return name; + } + + private Entry toEntry(JarEntry je) { + String name = je.getName(); + return toEntry(name, je); + } + + private Entry toEntry(Map.Entry entry) { + String name = entry.getKey(); + JarEntry je = entry.getValue(); + return toEntry(name, je); + } + + private Entry toEntry(String name, JarEntry je) { String fn = getFileName(name); - if (ze.isDirectory() || fn.startsWith("_")) { + if (je.isDirectory() || fn.startsWith("_")) { return null; } @@ -123,21 +198,39 @@ if (fn.equals(MODULE_INFO)) { fn = moduleName + "/" + MODULE_INFO; } - return new JarEntry(ze.getName(), fn, rt, zipFile, ze); + return new JarFileEntry(name, fn, rt, jarFile, je); + } + + // must be invoked before extractBaseName + private boolean versionAcceptable(JarEntry je) { + String name = je.getName(); + if (name.startsWith(VERSIONS_DIR)) { + name = name.substring(VERSIONS_DIR_LEN); + offset = name.indexOf('/'); + if (offset == -1) + throw new RuntimeException("");// fixme, better message + if (Integer.parseInt(name.substring(0, offset)) <= version) { + return true; + } + return false; + } + return true; } @Override public void close() throws IOException { - if (zipFile != null) { - zipFile.close(); + if (jarFile != null) { + jarFile.close(); } } @Override public void open() throws IOException { - if (zipFile != null) { - zipFile.close(); + if (jarFile != null) { + jarFile.close(); } - zipFile = new ZipFile(file.toFile()); + // open this way to determine if it's a multi-release jar + jarFile = new JarFile(file.toFile(), true, ZipFile.OPEN_READ, JarFile.runtimeVersion()); + version = jarFile.getVersion().major(); } } diff --git a/test/tools/jlink/multireleasejar/JLinkMultiReleaseJarTest.java b/test/tools/jlink/multireleasejar/JLinkMultiReleaseJarTest.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/JLinkMultiReleaseJarTest.java @@ -0,0 +1,170 @@ +/* + * 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 + * @bug 8156499 + * @summary Test image creation from Multi-Release JAR + * @author Steve Drach + * @library /lib/testlibrary + * @modules java.base/jdk.internal.jimage + * jdk.jartool/sun.tools.jar + * jdk.jlink/jdk.tools.jlink.internal + * @build CompilerUtils + * @run testng JLinkMultiReleaseJarTest +*/ + +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.internal.jimage.BasicImageReader; + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class JLinkMultiReleaseJarTest { + private Path userdir; + private Path javahome; + + @BeforeClass + public void initialize() throws IOException { + Path srcdir = Paths.get(System.getProperty("test.src")); + userdir = Paths.get(System.getProperty("user.dir", ".")); + javahome = Paths.get(System.getProperty("java.home")); + + // create class files from source + Path base = srcdir.resolve("base"); + Path basemods = userdir.resolve("basemods"); + CompilerUtils.compile(base, basemods, "-modulesourcepath", base.toString()); + + Path rt = srcdir.resolve("rt"); + Path rtmods = userdir.resolve("rtmods"); + CompilerUtils.compile(rt, rtmods, "-modulesourcepath", rt.toString()); + + // copy resource into basemods + Path resource = base.resolve("m1").resolve("resources.txt"); + Path dest = basemods.resolve("m1").resolve("resources.txt"); + Files.copy(resource, dest); + + // build multi-release jar file + sun.tools.jar.Main jartool = new sun.tools.jar.Main(System.out, System.err, "jar"); + String args = "-cf m1.jar -C basemods/m1 . --release " + + JarFile.runtimeVersion().major() + " -C rtmods/m1 ."; + jartool.run(args.split(" +")); + } + + //@AfterClass + public void close() throws IOException { + Files.walkFileTree(userdir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException + { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException + { + if (dir.equals(userdir)) { + return FileVisitResult.CONTINUE; + } + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + } + + @Test + public void test() throws Throwable { + // check that there are only packaged modules (.jmod files) for modulepath + Path jmodsdir = javahome.resolve("jmods"); + try (Stream jmods = Files.walk(jmodsdir, 1)) { + if (!jmods + .filter(path -> !path.equals(jmodsdir)) + .filter(path -> !path.toString().endsWith(".jmod")) + .collect(Collectors.toSet()) + .isEmpty() + ) { + throw new Exception("Exploded modules found"); + } + } + + // use jlink to build image from multi-release jar + String args = "--output myimage --addmods m1 --modulepath m1.jar:" + + jmodsdir.toString(); + int exitCode = jdk.tools.jlink.internal.Main.run(args.split(" +"), + new PrintWriter(System.out)); + Assert.assertEquals(exitCode, 0); + + // validate image + Path jimage = userdir.resolve("myimage").resolve("lib").resolve("modules"); + BasicImageReader reader = BasicImageReader.open(jimage); + + // do we have the right entry names? + Set names = Arrays.stream(reader.getEntryNames()) + .filter(n -> n.startsWith("/m1")) + .collect(Collectors.toSet()); + Assert.assertEquals(names, Set.of( + "/m1/module-info.class", + "/m1/p/Main.class", + "/m1/p/Type.class", + "/m1/resources.txt")); + + // do we have the right code? + byte[] b = reader.getResource("/m1/p/Main.class"); + Class clazz = (new ByteArrayClassLoader()).loadClass("p.Main", b); + MethodHandle getVersion = MethodHandles.lookup() + .findVirtual(clazz, "getVersion", MethodType.methodType(int.class)); + int version = (int)getVersion.invoke(clazz.getConstructor().newInstance()); + Assert.assertEquals(version, JarFile.runtimeVersion().major()); + } + + private static class ByteArrayClassLoader extends ClassLoader { + public Class loadClass(String name, byte[] bytes) { + return defineClass(name, bytes, 0, bytes.length); + } + } +} diff --git a/test/tools/jlink/multireleasejar/base/m1/module-info.java b/test/tools/jlink/multireleasejar/base/m1/module-info.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/base/m1/module-info.java @@ -0,0 +1,3 @@ +module m1 { + exports p; +} diff --git a/test/tools/jlink/multireleasejar/base/m1/p/Main.java b/test/tools/jlink/multireleasejar/base/m1/p/Main.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/base/m1/p/Main.java @@ -0,0 +1,8 @@ +package p; + +public class Main { + private String msg = "something to give this a different size"; + public int getVersion() { + return 8; + } +} diff --git a/test/tools/jlink/multireleasejar/base/m1/resources.txt b/test/tools/jlink/multireleasejar/base/m1/resources.txt new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/base/m1/resources.txt @@ -0,0 +1,1 @@ +Sample resource file. diff --git a/test/tools/jlink/multireleasejar/rt/m1/module-info.java b/test/tools/jlink/multireleasejar/rt/m1/module-info.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/rt/m1/module-info.java @@ -0,0 +1,3 @@ +module m1 { + exports p; +} diff --git a/test/tools/jlink/multireleasejar/rt/m1/p/Main.java b/test/tools/jlink/multireleasejar/rt/m1/p/Main.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/rt/m1/p/Main.java @@ -0,0 +1,9 @@ +package p; + +import java.util.jar.JarFile; + +public class Main { + public int getVersion() { + return JarFile.runtimeVersion().major(); + } +} diff --git a/test/tools/jlink/multireleasejar/rt/m1/p/Type.java b/test/tools/jlink/multireleasejar/rt/m1/p/Type.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/rt/m1/p/Type.java @@ -0,0 +1,3 @@ +package p; + +class Type{}