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

Reply via email to