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);
}
}
}