8163798: Create a JarFile versionedStream method
authorsdrach
Tue, 13 Sep 2016 13:55:35 -0700
changeset 40940 1413b2ff89e4
parent 40939 7262d01be07b
child 40941 e9970aac41d9
8163798: Create a JarFile versionedStream method Reviewed-by: mchung, psandoz, redestad
jdk/src/java.base/share/classes/java/util/jar/JarFile.java
jdk/src/java.base/share/classes/jdk/internal/util/jar/VersionedStream.java
jdk/test/jdk/internal/util/jar/TestVersionedStream.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:
-     * <pre>
-     * {@code
-     *     Stream<JarEntry> versionedStream(JarFile jf) {
-     *         return jf.stream().map(JarEntry::getName)
-     *                  .filter(name -> !name.startsWith("META-INF/versions/"))
-     *                  .map(jf::getJarEntry);
-     *     }
-     * }
-     * </pre>
      */
     public Stream<JarEntry> 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);
--- /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<JarEntry> 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;
+    }
+}
--- /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<String> p = List.of(
+                "META-INF/",
+                "META-INF/MANIFEST.MF",
+                "p/",
+                "p/Foo.class",
+                "p/Main.class"
+        );
+        List<String> 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<String> names) throws Exception {
+        try (JarFile jf = new JarFile(new File("mmr.jar"), false, ZipFile.OPEN_READ, version);
+            Stream<JarEntry> jes = jdk.internal.util.jar.VersionedStream.stream(jf))
+        {
+            Assert.assertNotNull(jes);
+
+            List<JarEntry> entries = jes.collect(Collectors.toList());
+
+            // verify the correct order
+            List<String> enames = entries.stream()
+                    .map(je -> je.getName())
+                    .collect(Collectors.toList());
+            Assert.assertEquals(enames, names);
+
+            // verify the contents
+            Map<String,String> 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<String> 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(" +"));
+    }
+
+}