diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiReleaseJarFile.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiReleaseJarFile.java index f98973f375e..a8720cefdf4 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiReleaseJarFile.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiReleaseJarFile.java @@ -20,44 +20,132 @@ package org.eclipse.jetty.util; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.stream.Stream; /** - *

Utility class to create a stream of Multi Release {@link JarEntry}s

- *

This is the java 8 version of this class. - * A java 9 version of this class is included as a Multi Release class in the - * jetty-util jar, that uses java 9 APIs to correctly handle Multi Release jars.

+ *

Utility class to handle a Multi Release Jar file

*/ public class MultiReleaseJarFile { private static final String META_INF_VERSIONS = "META-INF/versions/"; - public static JarFile open(File file) throws IOException + private final JarFile jarFile; + private final int majorVersion; + private final boolean multiRelease; + + /* Map to hold unversioned name to VersionedJarEntry */ + private final Map entries; + + /** + * Construct a multi release jar file for the current JVM version, ignoring directories. + * @param file The file to open + */ + public MultiReleaseJarFile(File file) throws IOException { - return new JarFile(file); + this(file,JavaVersion.VERSION.getMajor(),false); } - public static Stream streamVersioned(JarFile jf) + /** + * Construct a multi release jar file + * @param file The file to open + * @param majorVersion The major JVM version to apply when selecting a version. + * @param includeDirectories true if any directory entries should not be ignored + * @throws IOException if the jar file cannot be read + */ + public MultiReleaseJarFile(File file, int majorVersion, boolean includeDirectories) throws IOException { - return jf.stream() - .map(VersionedJarEntry::new); + if (file==null || !file.exists() || !file.canRead() || file.isDirectory()) + throw new IllegalArgumentException("bad jar file: "+file); + + jarFile = new JarFile(file,true,JarFile.OPEN_READ); + this.majorVersion = majorVersion; + + Manifest manifest = jarFile.getManifest(); + if (manifest==null) + multiRelease = false; + else + multiRelease = Boolean.valueOf(String.valueOf(manifest.getMainAttributes().getValue("Multi-Release"))); + + Map map = new TreeMap<>(); + jarFile.stream() + .map(VersionedJarEntry::new) + .filter(e->(includeDirectories||!e.isDirectory()) && e.isApplicable()) + .forEach(e->map.compute(e.name, (k, v) -> v==null || v.isReplacedBy(e) ? e : v)); + + for (Iterator> i = map.entrySet().iterator();i.hasNext();) + { + Map.Entry e = i.next(); + VersionedJarEntry entry = e.getValue(); + + if (entry.inner) + { + VersionedJarEntry outer = map.get(entry.outer); + + if (entry.outer==null || outer.version!= entry.version) + i.remove(); + } + } + + entries = Collections.unmodifiableMap(map); } - public static Stream stream(JarFile jf) + /** + * @return true IFF the jar is a multi release jar + */ + public boolean isMultiRelease() { - // Java 8 version of this class, ignores all versioned entries. - return streamVersioned(jf) - .filter(e->!e.isVersioned()) - .map(e->e.resolve(jf)); + return multiRelease; } - public static class VersionedJarEntry + /** + * @return The major version applied to this jar for the purposes of selecting entries + */ + public int getVersion() + { + return majorVersion; + } + + /** + * @return A stream of versioned entries from the jar, excluded any that are not applicable + */ + public Stream stream() + { + return entries.values().stream(); + } + + /** Get a versioned resource entry by name + * @param name The unversioned name of the resource + * @return The versioned entry of the resource + */ + public VersionedJarEntry getEntry(String name) + { + return entries.get(name); + } + + @Override + public String toString() + { + return String.format("%s[%b,%d]",jarFile.getName(),isMultiRelease(),getVersion()); + } + + /** + * A versioned Jar entry + */ + public class VersionedJarEntry { final JarEntry entry; final String name; final int version; + final boolean inner; + final String outer; VersionedJarEntry(JarEntry entry) { @@ -65,17 +153,18 @@ public class MultiReleaseJarFile String name = entry.getName(); if (name.startsWith(META_INF_VERSIONS)) { - v=-1; + v = -1; int index = name.indexOf('/', META_INF_VERSIONS.length()); - if (index >= 0 && index < name.length()) + if (index > META_INF_VERSIONS.length() && index < name.length()) { try { - v = Integer.parseInt(name.substring(META_INF_VERSIONS.length(), index), 10); + v = TypeUtil.parseInt(name, META_INF_VERSIONS.length(), index - META_INF_VERSIONS.length(), 10); name = name.substring(index + 1); } catch (NumberFormatException x) { + throw new RuntimeException("illegal version in "+jarFile,x); } } } @@ -83,27 +172,79 @@ public class MultiReleaseJarFile this.entry = entry; this.name = name; this.version = v; + this.inner = name.contains("$") && name.toLowerCase().endsWith(".class"); + this.outer = inner ? name.substring(0, name.indexOf('$')) + name.substring(name.length() - 6, name.length()) : null; } - public int version() + /** + * @return the unversioned name of the resource + */ + public String getName() + { + return name; + } + + /** + * @return The name of the resource within the jar, which could be versioned + */ + public String getNameInJar() + { + return entry.getName(); + } + + /** + * @return The version of the resource or 0 for a base version + */ + public int getVersion() { return version; } + /** + * + * @return True iff the entry is not from the base version + */ public boolean isVersioned() { return version > 0; } + /** + * + * @return True iff the entry is a directory + */ + public boolean isDirectory() + { + return entry.isDirectory(); + } + + /** + * @return An input stream of the content of the versioned entry. + * @throws IOException if something goes wrong! + */ + public InputStream getInputStream() throws IOException + { + return jarFile.getInputStream(entry); + } + + boolean isApplicable() + { + if (multiRelease) + return this.version>=0 && this.version <= majorVersion && name.length()>0; + return this.version==0; + } + + boolean isReplacedBy(VersionedJarEntry entry) + { + if (isDirectory()) + return entry.version==0; + return this.name.equals(entry.name) && entry.version>version; + } + @Override public String toString() { - return entry.toString() + (version==0?"[base]":("["+version+"]")); - } - - public JarEntry resolve(JarFile jf) - { - return entry; + return String.format("%s->%s[%d]",name,entry.getName(),version); } } }