Author: markt Date: Mon Oct 16 19:29:37 2017 New Revision: 1812315 URL: http://svn.apache.org/viewvc?rev=1812315&view=rev Log: Complete the fix for https://bz.apache.org/bugzilla/show_bug.cgi?id=61601 Handle multi-release JARs for packed web applications
Modified: tomcat/trunk/java/org/apache/catalina/webresources/JarWarResourceSet.java tomcat/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java tomcat/trunk/java/org/apache/tomcat/util/compat/JreCompat.java tomcat/trunk/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java tomcat/trunk/webapps/docs/changelog.xml Modified: tomcat/trunk/java/org/apache/catalina/webresources/JarWarResourceSet.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/webresources/JarWarResourceSet.java?rev=1812315&r1=1812314&r2=1812315&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/webresources/JarWarResourceSet.java (original) +++ tomcat/trunk/java/org/apache/catalina/webresources/JarWarResourceSet.java Mon Oct 16 19:29:37 2017 @@ -21,7 +21,9 @@ import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.Map.Entry; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarInputStream; @@ -31,6 +33,7 @@ import org.apache.catalina.LifecycleExce import org.apache.catalina.WebResource; import org.apache.catalina.WebResourceRoot; import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.compat.JreCompat; /** * Represents a {@link org.apache.catalina.WebResourceSet} based on a JAR file @@ -101,6 +104,7 @@ public class JarWarResourceSet extends A JarFile warFile = null; InputStream jarFileIs = null; archiveEntries = new HashMap<>(); + boolean multiRelease = false; try { warFile = openJarFile(); JarEntry jarFileInWar = warFile.getJarEntry(archivePath); @@ -112,7 +116,14 @@ public class JarWarResourceSet extends A archiveEntries.put(entry.getName(), entry); entry = jarIs.getNextJarEntry(); } - setManifest(jarIs.getManifest()); + Manifest m = jarIs.getManifest(); + setManifest(m); + if (m != null && JreCompat.isJre9Available()) { + String value = m.getMainAttributes().getValue("Multi-Release"); + if (value != null) { + multiRelease = Boolean.parseBoolean(value); + } + } // Hack to work-around JarInputStream swallowing these // entries. TomcatJarInputStream is used above which // extends JarInputStream and the method that creates @@ -128,6 +139,9 @@ public class JarWarResourceSet extends A archiveEntries.put(entry.getName(), entry); } } + if (multiRelease) { + processArchivesEntriesForMultiRelease(); + } } catch (IOException ioe) { // Should never happen archiveEntries = null; @@ -150,6 +164,56 @@ public class JarWarResourceSet extends A } + protected void processArchivesEntriesForMultiRelease() { + + int targetVersion = JreCompat.getInstance().jarFileRuntimeMajorVersion(); + + Map<String,VersionedJarEntry> versionedEntries = new HashMap<>(); + Iterator<Entry<String,JarEntry>> iter = archiveEntries.entrySet().iterator(); + while (iter.hasNext()) { + Entry<String,JarEntry> entry = iter.next(); + String name = entry.getKey(); + if (name.startsWith("META-INF/versions/")) { + // Remove the multi-release version + iter.remove(); + + // Get the base name and version for this versioned entry + int i = name.indexOf('/', 18); + if (i > 0) { + String baseName = name.substring(i + 1); + int version = Integer.parseInt(name.substring(18, i)); + + // Ignore any entries targeting for a later version than + // the target for this runtime + if (version <= targetVersion) { + VersionedJarEntry versionedJarEntry = versionedEntries.get(baseName); + if (versionedJarEntry == null) { + // No versioned entry found for this name. Create + // one. + versionedEntries.put(baseName, + new VersionedJarEntry(version, entry.getValue())); + } else { + // Ignore any entry for which we have already found + // a later version + if (version > versionedJarEntry.getVersion()) { + // Replace the entry targeted at an earlier + // version + versionedEntries.put(baseName, + new VersionedJarEntry(version, entry.getValue())); + } + } + } + } + } + } + + for (Entry<String,VersionedJarEntry> versionedJarEntry : versionedEntries.entrySet()) { + archiveEntries.put(versionedJarEntry.getKey(), + versionedJarEntry.getValue().getJarEntry()); + } + } + + /** * {@inheritDoc} * <p> @@ -164,7 +228,8 @@ public class JarWarResourceSet extends A @Override protected boolean isMultiRelease() { - // TODO: multi-release support for packed WAR files + // This always returns false otherwise the superclass will call + // #getArchiveEntry(String) return false; } @@ -190,4 +255,25 @@ public class JarWarResourceSet extends A throw new IllegalArgumentException(e); } } + + + private static final class VersionedJarEntry { + private final int version; + private final JarEntry jarEntry; + + public VersionedJarEntry(int version, JarEntry jarEntry) { + this.version = version; + this.jarEntry = jarEntry; + } + + + public int getVersion() { + return version; + } + + + public JarEntry getJarEntry() { + return jarEntry; + } + } } Modified: tomcat/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java?rev=1812315&r1=1812314&r2=1812315&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java (original) +++ tomcat/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java Mon Oct 16 19:29:37 2017 @@ -57,6 +57,7 @@ class Jre9Compat extends JreCompat { private static final Method isMultiReleaseMethod; private static final Object RUNTIME_VERSION; + private static final int RUNTIME_MAJOR_VERSION; static { Class<?> c1 = null; @@ -73,6 +74,7 @@ class Jre9Compat extends JreCompat { Constructor<JarFile> c12 = null; Method m13 = null; Object o14 = null; + Object o15 = null; try { Class<?> moduleLayerClazz = Class.forName("java.lang.ModuleLayer"); @@ -81,7 +83,8 @@ class Jre9Compat extends JreCompat { Class<?> moduleReferenceClazz = Class.forName("java.lang.module.ModuleReference"); Class<?> optionalClazz = Class.forName("java.util.Optional"); Class<?> versionClazz = Class.forName("java.lang.Runtime$Version"); - Method versionMethod = JarFile.class.getMethod("runtimeVersion"); + Method runtimeVersionMethod = JarFile.class.getMethod("runtimeVersion"); + Method majorMethod = versionClazz.getMethod("major"); c1 = Class.forName("java.lang.reflect.InaccessibleObjectException"); m2 = SSLParameters.class.getMethod("setApplicationProtocols", String[].class); @@ -96,7 +99,9 @@ class Jre9Compat extends JreCompat { m11 = optionalClazz.getMethod("get"); c12 = JarFile.class.getConstructor(File.class, boolean.class, int.class, versionClazz); m13 = JarFile.class.getMethod("isMultiRelease"); - o14 = versionMethod.invoke(null); + o14 = runtimeVersionMethod.invoke(null); + o15 = majorMethod.invoke(o14); + } catch (ClassNotFoundException e) { // Must be Java 8 } catch (ReflectiveOperationException | IllegalArgumentException e) { @@ -118,6 +123,12 @@ class Jre9Compat extends JreCompat { isMultiReleaseMethod = m13; RUNTIME_VERSION = o14; + if (o15 != null) { + RUNTIME_MAJOR_VERSION = ((Integer) o15).intValue(); + } else { + // Must be Java 8 + RUNTIME_MAJOR_VERSION = 8; + } } @@ -211,4 +222,10 @@ class Jre9Compat extends JreCompat { return false; } } + + + @Override + public int jarFileRuntimeMajorVersion() { + return RUNTIME_MAJOR_VERSION; + } } Modified: tomcat/trunk/java/org/apache/tomcat/util/compat/JreCompat.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/compat/JreCompat.java?rev=1812315&r1=1812314&r2=1812315&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/compat/JreCompat.java (original) +++ tomcat/trunk/java/org/apache/tomcat/util/compat/JreCompat.java Mon Oct 16 19:29:37 2017 @@ -35,6 +35,8 @@ import org.apache.tomcat.util.res.String */ public class JreCompat { + private static final int RUNTIME_MAJOR_VERSION = 8; + private static final JreCompat instance; private static final boolean jre9Available; private static final StringManager sm = StringManager.getManager(JreCompat.class); @@ -175,4 +177,9 @@ public class JreCompat { // Java 8 doesn't support multi-release so default to false return false; } + + + public int jarFileRuntimeMajorVersion() { + return RUNTIME_MAJOR_VERSION; + } } Modified: tomcat/trunk/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java?rev=1812315&r1=1812314&r2=1812315&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java (original) +++ tomcat/trunk/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java Mon Oct 16 19:29:37 2017 @@ -19,10 +19,14 @@ package org.apache.tomcat.util.scan; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; import java.util.jar.JarEntry; import java.util.jar.Manifest; import org.apache.tomcat.Jar; +import org.apache.tomcat.util.compat.JreCompat; /** * Base implementation of Jar for implementations that use a JarInputStream to @@ -34,6 +38,8 @@ public abstract class AbstractInputStrea private NonClosingJarInputStream jarInputStream = null; private JarEntry entry = null; + private Boolean multiRelease = null; + private Map<String,String> mrMap = null; public AbstractInputStreamJar(URL jarFileUrl) { this.jarFileURL = jarFileUrl; @@ -50,7 +56,7 @@ public abstract class AbstractInputStrea public void nextEntry() { if (jarInputStream == null) { try { - jarInputStream = createJarInputStream(); + reset(); } catch (IOException e) { entry = null; return; @@ -58,6 +64,21 @@ public abstract class AbstractInputStrea } try { entry = jarInputStream.getNextJarEntry(); + if (multiRelease.booleanValue()) { + // Skip base entries where there is a multi-release entry + // Skip multi-release entries that are not being used + while (entry != null && + (mrMap.keySet().contains(entry.getName()) || + entry.getName().startsWith("META-INF/versions/") && + !mrMap.values().contains(entry.getName()))) { + entry = jarInputStream.getNextJarEntry(); + } + } else { + // Skip multi-release entries + while (entry != null && entry.getName().startsWith("META-INF/versions/")) { + entry = jarInputStream.getNextJarEntry(); + } + } } catch (IOException ioe) { entry = null; } @@ -66,6 +87,8 @@ public abstract class AbstractInputStrea @Override public String getEntryName() { + // Given how the entry name is used, there is no requirement to convert + // the name for a multi-release entry to the corresponding base name. if (entry == null) { return null; } else { @@ -129,6 +152,25 @@ public abstract class AbstractInputStrea closeStream(); entry = null; jarInputStream = createJarInputStream(); + // Only perform multi-release processing on first access + if (multiRelease == null) { + if (JreCompat.isJre9Available()) { + Manifest manifest = jarInputStream.getManifest(); + String mrValue = manifest.getMainAttributes().getValue("Multi-Release"); + if (mrValue == null) { + multiRelease = Boolean.FALSE; + } else { + multiRelease = Boolean.valueOf(mrValue); + } + } else { + multiRelease = Boolean.FALSE; + } + if (multiRelease.booleanValue()) { + if (mrMap == null) { + populateMrMap(); + } + } + } } @@ -147,10 +189,25 @@ public abstract class AbstractInputStrea private void gotoEntry(String name) throws IOException { + if (multiRelease == null) { + reset(); + } + + // Need to convert requested name to multi-release name (if one exists) + if (multiRelease.booleanValue()) { + String mrName = mrMap.get(name); + if (mrName != null) { + name = mrName; + } + } else if (name.startsWith("META-INF/versions/")) { + entry = null; + return; + } + if (entry != null && name.equals(entry.getName())) { return; } - reset(); + JarEntry jarEntry = jarInputStream.getNextJarEntry(); while (jarEntry != null) { if (name.equals(jarEntry.getName())) { @@ -160,4 +217,57 @@ public abstract class AbstractInputStrea jarEntry = jarInputStream.getNextJarEntry(); } } + + + private void populateMrMap() throws IOException { + int targetVersion = JreCompat.getInstance().jarFileRuntimeMajorVersion(); + + Map<String,Integer> mrVersions = new HashMap<>(); + + JarEntry jarEntry = jarInputStream.getNextJarEntry(); + + // Tracking the base name and the latest valid version found is + // sufficient to be able to create the renaming map required + while (jarEntry != null) { + String name = jarEntry.getName(); + if (name.startsWith("META-INF/versions/") && name.endsWith(".class")) { + + // Get the base name and version for this versioned entry + int i = name.indexOf('/', 18); + if (i > 0) { + String baseName = name.substring(i + 1); + int version = Integer.parseInt(name.substring(18, i)); + + // Ignore any entries targeting for a later version than + // the target for this runtime + if (version <= targetVersion) { + Integer mappedVersion = mrVersions.get(baseName); + if (mappedVersion == null) { + // No version found for this name. Create one. + mrVersions.put(baseName, Integer.valueOf(version)); + } else { + // Ignore any entry for which we have already found + // a later version + if (version > mappedVersion.intValue()) { + // Replace the earlier version + mrVersions.put(baseName, Integer.valueOf(version)); + } + } + } + } + } + jarEntry = jarInputStream.getNextJarEntry(); + } + + mrMap = new HashMap<>(); + + for (Entry<String,Integer> mrVersion : mrVersions.entrySet()) { + mrMap.put(mrVersion.getKey() , "META-INF/versions/" + mrVersion.getValue().toString() + "" + + "/" + mrVersion.getKey()); + } + + // Reset stream back to the beginning of the JAR + closeStream(); + jarInputStream = createJarInputStream(); + } } Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1812315&r1=1812314&r2=1812315&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Mon Oct 16 19:29:37 2017 @@ -56,6 +56,10 @@ JARs on the module path when running on Java 9 and class path scanning is enabled. (markt) </fix> + <fix> + <bug>61601</bug>: Add support for multi-release JARs in JAR scanning and + web application class loading. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org