< prev index next >

src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java

Print this page
rev 15454 : 8156499: Update jlink to support creating images with modules that are packaged as multi-release JARs
Reviewed-by:
Contributed-by: steve.drach@oracle.com

@@ -27,13 +27,17 @@
 
 import java.io.IOException;
 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;
 
 /**
  * An Archive backed by a jar file.

@@ -41,17 +45,17 @@
 public abstract class JarArchive implements Archive {
 
     /**
      * 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);
             size = entry.getSize();
         }

@@ -68,16 +72,21 @@
         public InputStream stream() throws IOException {
             return file.getInputStream(entry);
         }
     }
 
+    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);
         Objects.requireNonNull(file);
         this.moduleName = mn;

@@ -95,49 +104,133 @@
     }
 
     @Override
     public Stream<Entry> 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<JarEntry> 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<String,JarEntry> 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;
         }
 
         EntryType rt = toEntryType(name);
 
         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();
     }
 }
< prev index next >