Author: markt Date: Wed Oct 18 20:53:55 2017 New Revision: 1812581 URL: http://svn.apache.org/viewvc?rev=1812581&view=rev Log: Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=61601 Handle multi-release JARs for packed and unpacked web applications
Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java Wed Oct 18 20:53:55 2017 @@ -3006,7 +3006,7 @@ public abstract class WebappClassLoaderB if (jarFiles[0] == null) { for (int i = 0; i < jarFiles.length; i++) { try { - jarFiles[i] = new JarFile(jarRealFiles[i]); + jarFiles[i] = JreCompat.getInstance().jarFileNewInstance(jarRealFiles[i]); } catch (IOException e) { log.warn(sm.getString("webappClassLoader.jarOpenFail", jarFiles[i]), e); closeJARs(true); @@ -3695,7 +3695,7 @@ public abstract class WebappClassLoaderB JarFile jarFile = null; try { - jarFile = new JarFile(file); + jarFile = JreCompat.getInstance().jarFileNewInstance(file); for (int i = 0; i < triggers.length; i++) { Class<?> clazz = null; try { Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java Wed Oct 18 20:53:55 2017 @@ -59,6 +59,7 @@ import org.apache.naming.resources.DirCo import org.apache.naming.resources.DirContextURLStreamHandlerFactory; import org.apache.naming.resources.Resource; import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.compat.JreCompat; import org.apache.tomcat.util.modeler.Registry; import org.apache.tomcat.util.res.StringManager; @@ -999,7 +1000,7 @@ public class WebappLoader extends Lifecy } try { - JarFile jarFile = new JarFile(destFile); + JarFile jarFile = JreCompat.getInstance().jarFileNewInstance(destFile); classLoader.addJar(filename, jarFile, destFile); } catch (Exception ex) { // Catch the exception if there is an empty jar file Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java Wed Oct 18 20:53:55 2017 @@ -22,6 +22,8 @@ import java.util.Locale; class Jre7Compat extends JreCompat { + private static final int RUNTIME_MAJOR_VERSION = 7; + private static final Method forLanguageTagMethod; @@ -55,4 +57,10 @@ class Jre7Compat extends JreCompat { return null; } } + + + @Override + public int jarFileRuntimeMajorVersion() { + return RUNTIME_MAJOR_VERSION; + } } Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java Wed Oct 18 20:53:55 2017 @@ -25,6 +25,8 @@ import javax.net.ssl.SSLServerSocket; class Jre8Compat extends Jre7Compat { + private static final int RUNTIME_MAJOR_VERSION = 8; + private static final Method getSSLParametersMethod; private static final Method setUseCipherSuitesOrderMethod; private static final Method setSSLParametersMethod; @@ -93,4 +95,10 @@ class Jre8Compat extends Jre7Compat { throw new UnsupportedOperationException(e); } } + + + @Override + public int jarFileRuntimeMajorVersion() { + return RUNTIME_MAJOR_VERSION; + } } Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java Wed Oct 18 20:53:55 2017 @@ -16,7 +16,9 @@ */ package org.apache.tomcat.util.compat; +import java.io.File; import java.io.IOException; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; @@ -25,6 +27,8 @@ import java.net.URL; import java.net.URLConnection; import java.util.Deque; import java.util.Set; +import java.util.jar.JarFile; +import java.util.zip.ZipFile; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -44,14 +48,13 @@ class Jre9Compat extends Jre8Compat { private static final Method locationMethod; private static final Method isPresentMethod; private static final Method getMethod; + private static final Constructor<JarFile> jarFileConstructor; + private static final Method isMultiReleaseMethod; - static { - Class<?> moduleLayerClazz = null; - Class<?> configurationClazz = null; - Class<?> resolvedModuleClazz = null; - Class<?> moduleReferenceClazz = null; - Class<?> optionalClazz = null; + private static final Object RUNTIME_VERSION; + private static final int RUNTIME_MAJOR_VERSION; + static { Class<?> c1 = null; Method m4 = null; Method m5 = null; @@ -61,13 +64,20 @@ class Jre9Compat extends Jre8Compat { Method m9 = null; Method m10 = null; Method m11 = null; + Constructor<JarFile> c12 = null; + Method m13 = null; + Object o14 = null; + Object o15 = null; try { - moduleLayerClazz = Class.forName("java.lang.ModuleLayer"); - configurationClazz = Class.forName("java.lang.module.Configuration"); - resolvedModuleClazz = Class.forName("java.lang.module.ResolvedModule"); - moduleReferenceClazz = Class.forName("java.lang.module.ModuleReference"); - optionalClazz = Class.forName("java.util.Optional"); + Class<?> moduleLayerClazz = Class.forName("java.lang.ModuleLayer"); + Class<?> configurationClazz = Class.forName("java.lang.module.Configuration"); + Class<?> resolvedModuleClazz = Class.forName("java.lang.module.ResolvedModule"); + Class<?> moduleReferenceClazz = Class.forName("java.lang.module.ModuleReference"); + Class<?> optionalClazz = Class.forName("java.util.Optional"); + Class<?> versionClazz = Class.forName("java.lang.Runtime$Version"); + Method runtimeVersionMethod = JarFile.class.getMethod("runtimeVersion"); + Method majorMethod = versionClazz.getMethod("major"); c1 = Class.forName("java.lang.reflect.InaccessibleObjectException"); m4 = URLConnection.class.getMethod("setDefaultUseCaches", String.class, boolean.class); @@ -78,13 +88,25 @@ class Jre9Compat extends Jre8Compat { m9 = moduleReferenceClazz.getMethod("location"); m10 = optionalClazz.getMethod("isPresent"); m11 = optionalClazz.getMethod("get"); + c12 = JarFile.class.getConstructor(File.class, boolean.class, int.class, versionClazz); + m13 = JarFile.class.getMethod("isMultiRelease"); + o14 = runtimeVersionMethod.invoke(null); + o15 = majorMethod.invoke(o14); + } catch (SecurityException e) { // Should never happen } catch (NoSuchMethodException e) { // Should never happen } catch (ClassNotFoundException e) { // Must be Java 8 + } catch (IllegalArgumentException e) { + // Should never happen + } catch (IllegalAccessException e) { + // Should never happen + } catch (InvocationTargetException e) { + // Should never happen } + inaccessibleObjectExceptionClazz = c1; setDefaultUseCachesMethod = m4; bootMethod = m5; @@ -94,6 +116,16 @@ class Jre9Compat extends Jre8Compat { locationMethod = m9; isPresentMethod = m10; getMethod = m11; + jarFileConstructor = c12; + isMultiReleaseMethod = m13; + + RUNTIME_VERSION = o14; + if (o15 != null) { + RUNTIME_MAJOR_VERSION = ((Integer) o15).intValue(); + } else { + // Must be Java 8 + RUNTIME_MAJOR_VERSION = 8; + } } @@ -154,4 +186,41 @@ class Jre9Compat extends Jre8Compat { throw new UnsupportedOperationException(e); } } + + + @Override + public JarFile jarFileNewInstance(File f) throws IOException { + try { + return jarFileConstructor.newInstance( + f, Boolean.TRUE, Integer.valueOf(ZipFile.OPEN_READ), RUNTIME_VERSION); + } catch (IllegalArgumentException e) { + throw new IOException(e); + } catch (InstantiationException e) { + throw new IOException(e); + } catch (IllegalAccessException e) { + throw new IOException(e); + } catch (InvocationTargetException e) { + throw new IOException(e); + } + } + + + @Override + public boolean jarFileIsMultiRelease(JarFile jarFile) { + try { + return ((Boolean) isMultiReleaseMethod.invoke(jarFile)).booleanValue(); + } catch (IllegalArgumentException e) { + return false; + } catch (IllegalAccessException e) { + return false; + } catch (InvocationTargetException e) { + return false; + } + } + + + @Override + public int jarFileRuntimeMajorVersion() { + return RUNTIME_MAJOR_VERSION; + } } Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java Wed Oct 18 20:53:55 2017 @@ -16,11 +16,13 @@ */ package org.apache.tomcat.util.compat; +import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.util.Deque; import java.util.Locale; +import java.util.jar.JarFile; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLServerSocket; @@ -34,6 +36,8 @@ import org.apache.tomcat.util.res.String */ public class JreCompat { + private static final int RUNTIME_MAJOR_VERSION = 6; + private static final JreCompat instance; private static StringManager sm = StringManager.getManager(JreCompat.class.getPackage().getName()); @@ -190,7 +194,40 @@ public class JreCompat { * added */ public void addBootModulePath(Deque<URL> classPathUrlsToProcess) { - // NO-OP for Java 8. There is no module path. + // NO-OP. There is no module path prior to Java 9. + } + + + /** + * Creates a new JarFile instance. When running on Java 9 and later, the + * JarFile will be multi-release JAR aware. + * + * @param f The JAR file to open + * + * @return A JarFile instance based on the provided file + * + * @throws IOException If an I/O error occurs creating the JarFile instance + */ + public JarFile jarFileNewInstance(File f) throws IOException { + return new JarFile(f); } + + /** + * Is this JarFile a multi-release JAR file. + * + * @param jarFile The JarFile to test + * + * @return {@code true} If it is a multi-release JAR file and is configured + * to behave as such. + */ + public boolean jarFileIsMultiRelease(JarFile jarFile) { + // There is no multi-release JAR support prior to Java 9 + return false; + } + + + public int jarFileRuntimeMajorVersion() { + return RUNTIME_MAJOR_VERSION; + } } Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java Wed Oct 18 20:53:55 2017 @@ -21,10 +21,14 @@ import java.io.InputStream; import java.net.JarURLConnection; import java.net.URL; import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.zip.ZipEntry; +import org.apache.tomcat.util.compat.JreCompat; + /** * Implementation of {@link Jar} that is optimised for file based JAR URLs (e.g * URLs of the form jar:file:...). @@ -32,13 +36,17 @@ import java.util.zip.ZipEntry; public class FileUrlJar implements Jar { private JarFile jarFile; + private final boolean multiRelease; private Enumeration<JarEntry> entries; + private Set<String> entryNamesSeen; private JarEntry entry = null; public FileUrlJar(URL url) throws IOException { JarURLConnection jarConn = (JarURLConnection) url.openConnection(); jarConn.setUseCaches(false); + // JarFile returned will be multi-release aware if the OS supports it. jarFile = jarConn.getJarFile(); + multiRelease = JreCompat.getInstance().jarFileIsMultiRelease(jarFile); } @Override @@ -49,6 +57,7 @@ public class FileUrlJar implements Jar { @Override public InputStream getInputStream(String name) throws IOException { + // JarFile#getEntry() is multi-release aware ZipEntry entry = jarFile.getEntry(name); if (entry == null) { return null; @@ -70,13 +79,58 @@ public class FileUrlJar implements Jar { @Override public void nextEntry() { + // JarFile#entries() is NOT multi-release aware if (entries == null) { entries = jarFile.entries(); + if (multiRelease) { + entryNamesSeen = new HashSet<String>(); + } } - if (entries.hasMoreElements()) { - entry = entries.nextElement(); + if (multiRelease) { + // Need to ensure that: + // - the one, correct entry is returned where multiple versions + // are available + // - that the order of entries in the JAR doesn't prevent the + // correct entries being returned + // - the case where an entry appears in the versions location + // but not in the the base location is handled correctly + + // Enumerate the entries until one is reached that represents an + // entry that has not been seen before. + String name = null; + while (true) { + if (entries.hasMoreElements()) { + entry = entries.nextElement(); + name = entry.getName(); + // Get 'base' name + if (name.startsWith("META-INF/versions/")) { + int i = name.indexOf('/', 18); + if (i == -1) { + continue; + } + name = name.substring(i + 1); + } + if (name.length() == 0 || entryNamesSeen.contains(name)) { + continue; + } + + entryNamesSeen.add(name); + + // JarFile.getJarEntry is version aware so use it + entry = jarFile.getJarEntry(entry.getName()); + break; + } else { + entry = null; + break; + } + } + } else { - entry = null; + if (entries.hasMoreElements()) { + entry = entries.nextElement(); + } else { + entry = null; + } } } @@ -101,6 +155,7 @@ public class FileUrlJar implements Jar { @Override public void reset() throws IOException { entries = null; + entryNamesSeen = null; entry = null; } } Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java Wed Oct 18 20:53:55 2017 @@ -21,7 +21,13 @@ import java.io.InputStream; import java.net.JarURLConnection; import java.net.URL; import java.net.URLConnection; +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.util.compat.JreCompat; /** * Implementation of {@link Jar} that is optimised for non-file based JAR URLs @@ -32,10 +38,23 @@ public class UrlJar implements Jar { private NonClosingJarInputStream jarInputStream = null; private URL url = null; private JarEntry entry = null; + private Map<String,String> mrMap = null; public UrlJar(URL url) throws IOException { this.url = url; this.jarInputStream = createJarInputStream(); + + boolean multiRelease = false; + if (JreCompat.isJre9Available()) { + Manifest manifest = jarInputStream.getManifest(); + String mrValue = manifest.getMainAttributes().getValue("Multi-Release"); + if (mrValue != null) { + multiRelease = Boolean.valueOf(mrValue).booleanValue(); + } + } + if (multiRelease) { + populateMrMap(); + } } @Override @@ -84,7 +103,22 @@ public class UrlJar implements Jar { public void nextEntry() { try { entry = jarInputStream.getNextJarEntry(); - } catch (IOException ioe) { + if (mrMap != null) { + // 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; } } @@ -106,6 +140,60 @@ public class UrlJar implements Jar { @Override public void reset() throws IOException { close(); + entry = null; + jarInputStream = createJarInputStream(); + } + + + private void populateMrMap() throws IOException { + int targetVersion = JreCompat.getInstance().jarFileRuntimeMajorVersion(); + + Map<String,Integer> mrVersions = new HashMap<String,Integer>(); + + 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<String,String>(); + + 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 + close(); jarInputStream = createJarInputStream(); } } Modified: tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml?rev=1812581&r1=1812580&r2=1812581&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml (original) +++ tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Wed Oct 18 20:53:55 2017 @@ -80,6 +80,10 @@ Fix the JMX descriptor for <code>Wrapper.findInitParameter()</code>. (rjung) </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="Jasper"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org