# HG changeset patch # User sdrach # Date 1473800135 25200 # Node ID 1413b2ff89e4489e92171de28f7f25fb94bc440c # Parent 7262d01be07b355877a2a1f1b1df598eb2278d29 8163798: Create a JarFile versionedStream method Reviewed-by: mchung, psandoz, redestad diff -r 7262d01be07b -r 1413b2ff89e4 jdk/src/java.base/share/classes/java/util/jar/JarFile.java --- a/jdk/src/java.base/share/classes/java/util/jar/JarFile.java Tue Sep 13 20:59:43 2016 +0530 +++ b/jdk/src/java.base/share/classes/java/util/jar/JarFile.java Tue Sep 13 13:55:35 2016 -0700 @@ -536,19 +536,6 @@ * @return an ordered {@code Stream} of entries in this jar file * @throws IllegalStateException if the jar file has been closed * @since 1.8 - * - * @apiNote A versioned view of the stream obtained from a {@code JarFile} - * configured to process a multi-release jar file can be created with code - * similar to the following: - *
-     * {@code
-     *     Stream versionedStream(JarFile jf) {
-     *         return jf.stream().map(JarEntry::getName)
-     *                  .filter(name -> !name.startsWith("META-INF/versions/"))
-     *                  .map(jf::getJarEntry);
-     *     }
-     * }
-     * 
*/ public Stream stream() { return StreamSupport.stream(Spliterators.spliterator( @@ -571,7 +558,7 @@ private ZipEntry getVersionedEntry(ZipEntry ze) { ZipEntry vze = null; - if (BASE_VERSION_MAJOR < versionMajor && !ze.isDirectory()) { + if (BASE_VERSION_MAJOR < versionMajor) { String name = ze.getName(); if (!name.startsWith(META_INF)) { vze = searchForVersionedEntry(versionMajor, name); diff -r 7262d01be07b -r 1413b2ff89e4 jdk/src/java.base/share/classes/jdk/internal/util/jar/VersionedStream.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/src/java.base/share/classes/jdk/internal/util/jar/VersionedStream.java Tue Sep 13 13:55:35 2016 -0700 @@ -0,0 +1,85 @@ +/* + * 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. 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.internal.util.jar; + +import java.util.Objects; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Stream; + +public class VersionedStream { + private static final String META_INF_VERSIONS = "META-INF/versions/"; + + /** + * Returns a stream of versioned entries, derived from the base names of + * all entries in a multi-release {@code JarFile} that are present either in + * the base directory or in any versioned directory with a version number + * less than or equal to the {@code Runtime.Version::major} that the + * {@code JarFile} was opened with. These versioned entries are aliases + * for the real entries -- i.e. the names are base names and the content + * may come from a versioned directory entry. If the {@code jarFile} is not + * a multi-release jar, a stream of all entries is returned. + * + * @param jf the input JarFile + * @return stream of entries + * @since 9 + */ + public static Stream stream(JarFile jf) { + if (jf.isMultiRelease()) { + int version = jf.getVersion().major(); + return jf.stream() + .map(je -> getBaseSuffix(je, version)) + .filter(Objects::nonNull) + .distinct() + .map(jf::getJarEntry); + } + return jf.stream(); + } + + private static String getBaseSuffix(JarEntry je, int version) { + String name = je.getName(); + if (name.startsWith(META_INF_VERSIONS)) { + int len = META_INF_VERSIONS.length(); + int index = name.indexOf('/', len); + if (index == -1 || index == (name.length() - 1)) { + // filter out META-INF/versions/* and META-INF/versions/*/ + return null; + } + try { + if (Integer.parseInt(name, len, index, 10) > version) { + // not an integer + return null; + } + } catch (NumberFormatException x) { + // silently remove malformed entries + return null; + } + // We know name looks like META-INF/versions/*/* + return name.substring(index + 1); + } + return name; + } +} diff -r 7262d01be07b -r 1413b2ff89e4 jdk/test/jdk/internal/util/jar/TestVersionedStream.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/jdk/internal/util/jar/TestVersionedStream.java Tue Sep 13 13:55:35 2016 -0700 @@ -0,0 +1,214 @@ +/* + * 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 8163798 + * @summary basic tests for multi-release jar versioned streams + * @modules jdk.jartool/sun.tools.jar java.base/jdk.internal.util.jar + * @run testng TestVersionedStream + */ + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URI; +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.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipFile; + +public class TestVersionedStream { + private String userdir; + + @BeforeClass + public void initialize() { + userdir = System.getProperty("user.dir", "."); + + // These are not real class files even though they end with .class. + // They are resource files so jar tool validation won't reject them. + // But they are what we want to test, especially q/Bar.class that + // could be in a concealed package if this was a modular multi-release + // jar. + createFiles( + "base/p/Foo.class", + "base/p/Main.class", + "v9/p/Foo.class", + "v10/p/Foo.class", + "v10/q/Bar.class", + "v11/p/Foo.class" + ); + + jar("cf mmr.jar -C base . --release 9 -C v9 . --release 10 -C v10 . --release 11 -C v11 ."); + + System.out.println("Contents of mmr.jar\n======="); + jar("tf mmr.jar"); + System.out.println("======="); + } + + @AfterClass + public void close() throws IOException { + Path root = Paths.get(userdir); + Files.walkFileTree(root, 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 exc) throws IOException { + if (!dir.equals(root)) { + Files.delete(dir); + } + return FileVisitResult.CONTINUE; + } + }); + } + + @DataProvider + public Object[][] data() { + List p = List.of( + "META-INF/", + "META-INF/MANIFEST.MF", + "p/", + "p/Foo.class", + "p/Main.class" + ); + List q = List.of( + "META-INF/", + "META-INF/MANIFEST.MF", + "p/", + "p/Foo.class", + "p/Main.class", + "q/", + "q/Bar.class" + ); + Runtime.Version rt = JarFile.runtimeVersion(); + return new Object[][] { + {Runtime.Version.parse("8"), p}, + {Runtime.Version.parse("9"), p}, + {Runtime.Version.parse("10"), q}, + {Runtime.Version.parse("11"), q}, + {JarFile.baseVersion(), p}, + {rt, rt.major() > 9 ? q : p} + }; + } + + @Test(dataProvider="data") + public void test(Runtime.Version version, List names) throws Exception { + try (JarFile jf = new JarFile(new File("mmr.jar"), false, ZipFile.OPEN_READ, version); + Stream jes = jdk.internal.util.jar.VersionedStream.stream(jf)) + { + Assert.assertNotNull(jes); + + List entries = jes.collect(Collectors.toList()); + + // verify the correct order + List enames = entries.stream() + .map(je -> je.getName()) + .collect(Collectors.toList()); + Assert.assertEquals(enames, names); + + // verify the contents + Map contents = new HashMap<>(); + contents.put("p/Main.class", "base/p/Main.class\n"); + if (version.major() > 9) { + contents.put("q/Bar.class", "v10/q/Bar.class\n"); + } + switch (version.major()) { + case 8: + contents.put("p/Foo.class", "base/p/Foo.class\n"); + break; + case 9: + contents.put("p/Foo.class", "v9/p/Foo.class\n"); + break; + case 10: + contents.put("p/Foo.class", "v10/p/Foo.class\n"); + break; + case 11: + contents.put("p/Foo.class", "v11/p/Foo.class\n"); + break; + default: + Assert.fail("Test out of date, please add more cases"); + } + + contents.entrySet().stream().forEach(e -> { + String name = e.getKey(); + int i = enames.indexOf(name); + Assert.assertTrue(i != -1, name + " not in enames"); + JarEntry je = entries.get(i); + try (InputStream is = jf.getInputStream(je)) { + String s = new String(is.readAllBytes()); + Assert.assertTrue(s.endsWith(e.getValue()), s); + } catch (IOException x) { + throw new UncheckedIOException(x); + } + }); + } + } + + private void createFiles(String... files) { + ArrayList list = new ArrayList(); + Arrays.stream(files) + .map(f -> "file:///" + userdir + "/" + f) + .map(f -> URI.create(f)) + .filter(u -> u != null) + .map(u -> Paths.get(u)) + .forEach(p -> { + try { + Files.createDirectories(p.getParent()); + Files.createFile(p); + list.clear(); + list.add(p.toString()); + Files.write(p, list); + } catch (IOException x) { + throw new UncheckedIOException(x); + }}); + } + + private void jar(String args) { + new sun.tools.jar.Main(System.out, System.err, "jar") + .run(args.split(" +")); + } + +}