You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by mi...@apache.org on 2024/06/01 19:40:46 UTC

(maven-shared-jar) branch master updated: [MSHARED-1256] Add support for Multi-Release JARs

This is an automated email from the ASF dual-hosted git repository.

michaelo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-shared-jar.git


The following commit(s) were added to refs/heads/master by this push:
     new 0095819  [MSHARED-1256] Add support for Multi-Release JARs
0095819 is described below

commit 009581918bd86d9bcff1b3db155f7589a9e91c16
Author: Gabriel Belingueres <be...@gmail.com>
AuthorDate: Thu May 25 00:25:00 2023 -0300

    [MSHARED-1256] Add support for Multi-Release JARs
    
    This closes #114
---
 .../org/apache/maven/shared/jar/JarAnalyzer.java   |  25 ++-
 .../java/org/apache/maven/shared/jar/JarData.java  |  45 +++++-
 .../shared/jar/classes/JarClassesAnalysis.java     | 176 +++++++++++++++-----
 .../shared/jar/classes/JarVersionedRuntime.java    |  51 ++++++
 .../shared/jar/classes/JarVersionedRuntimes.java   | 110 +++++++++++++
 .../apache/maven/shared/jar/JarAnalyzerTest.java   |  12 ++
 .../shared/jar/classes/JarClassesAnalyzerTest.java | 180 +++++++++++++++++++++
 .../resources/jars/module-info-only-test-0.0.1.jar | Bin 0 -> 3340 bytes
 .../resources/jars/multi-release-test-0.0.1.jar    | Bin 0 -> 10201 bytes
 src/test/resources/jars/tomcat-jni-9.0.75.jar      | Bin 0 -> 37906 bytes
 10 files changed, 548 insertions(+), 51 deletions(-)

diff --git a/src/main/java/org/apache/maven/shared/jar/JarAnalyzer.java b/src/main/java/org/apache/maven/shared/jar/JarAnalyzer.java
index 6c9a92c..74ac4f9 100644
--- a/src/main/java/org/apache/maven/shared/jar/JarAnalyzer.java
+++ b/src/main/java/org/apache/maven/shared/jar/JarAnalyzer.java
@@ -151,9 +151,20 @@ public class JarAnalyzer {
      * @return the list of files found, in {@link java.util.jar.JarEntry} elements
      */
     public List<JarEntry> filterEntries(Pattern pattern) {
+        return filterEntries(pattern, getEntries());
+    }
+
+    /**
+     * Filter a list of JAR entries against the pattern.
+     *
+     * @param pattern the pattern to filter against.
+     * @param entryList the list of entries to filter from.
+     * @return the filtered list of JarEntry.
+     */
+    private List<JarEntry> filterEntries(Pattern pattern, List<JarEntry> entryList) {
         List<JarEntry> ret = new ArrayList<>();
 
-        for (JarEntry entry : getEntries()) {
+        for (JarEntry entry : entryList) {
             Matcher mat = pattern.matcher(entry.getName());
             if (mat.find()) {
                 ret.add(entry);
@@ -171,6 +182,18 @@ public class JarAnalyzer {
         return filterEntries(CLASS_FILTER);
     }
 
+    /**
+     * Get all the classes in the entry list.
+     *
+     * @param entryList the entry list.
+     * @return the filtered entry list.
+     *
+     * @since 3.0.1
+     */
+    public List<JarEntry> getClassEntries(List<JarEntry> entryList) {
+        return filterEntries(CLASS_FILTER, entryList);
+    }
+
     /**
      * Get all the Maven POM entries in the JAR.
      *
diff --git a/src/main/java/org/apache/maven/shared/jar/JarData.java b/src/main/java/org/apache/maven/shared/jar/JarData.java
index 32cbad8..3ffa13e 100644
--- a/src/main/java/org/apache/maven/shared/jar/JarData.java
+++ b/src/main/java/org/apache/maven/shared/jar/JarData.java
@@ -22,16 +22,21 @@ import java.io.File;
 import java.util.Collections;
 import java.util.List;
 import java.util.jar.Attributes;
+import java.util.jar.Attributes.Name;
 import java.util.jar.JarEntry;
 import java.util.jar.Manifest;
 
 import org.apache.maven.shared.jar.classes.JarClasses;
+import org.apache.maven.shared.jar.classes.JarVersionedRuntimes;
 import org.apache.maven.shared.jar.identification.JarIdentification;
 
 /**
  * Class that contains details of a single JAR file and it's entries.
  */
 public final class JarData {
+
+    private static final Name ATTR_MULTI_RELEASE = new Attributes.Name("Multi-Release");
+
     /**
      * The JAR file.
      */
@@ -42,6 +47,11 @@ public final class JarData {
      */
     private final boolean aSealed;
 
+    /**
+     * Whether the JAR file is Multi-Release.
+     */
+    private boolean multiRelease;
+
     /**
      * The hashcode for the entire file's contents.
      */
@@ -72,6 +82,11 @@ public final class JarData {
      */
     private JarIdentification jarIdentification;
 
+    /**
+     * Information about the JAR's Multi-Release entries
+     */
+    private JarVersionedRuntimes versionedRuntimes;
+
     /**
      * Constructor.
      *
@@ -86,14 +101,8 @@ public final class JarData {
 
         this.entries = Collections.unmodifiableList(entries);
 
-        boolean aSealed = false;
-        if (this.manifest != null) {
-            String sval = this.manifest.getMainAttributes().getValue(Attributes.Name.SEALED);
-            if (sval != null && !sval.isEmpty()) {
-                aSealed = "true".equalsIgnoreCase(sval.trim());
-            }
-        }
-        this.aSealed = aSealed;
+        this.aSealed = isAttributePresent(Attributes.Name.SEALED);
+        this.multiRelease = isAttributePresent(ATTR_MULTI_RELEASE);
     }
 
     public List<JarEntry> getEntries() {
@@ -112,6 +121,10 @@ public final class JarData {
         return aSealed;
     }
 
+    public boolean isMultiRelease() {
+        return multiRelease;
+    }
+
     public void setFileHash(String fileHash) {
         this.fileHash = fileHash;
     }
@@ -163,4 +176,20 @@ public final class JarData {
     public JarClasses getJarClasses() {
         return jarClasses;
     }
+
+    public void setVersionedRuntimes(JarVersionedRuntimes versionedRuntimess) {
+        this.versionedRuntimes = versionedRuntimess;
+    }
+
+    public JarVersionedRuntimes getVersionedRuntimes() {
+        return this.versionedRuntimes;
+    }
+
+    private boolean isAttributePresent(Attributes.Name attrName) {
+        if (this.manifest != null) {
+            String sval = this.manifest.getMainAttributes().getValue(attrName);
+            return sval != null && "true".equalsIgnoreCase(sval.trim());
+        }
+        return false;
+    }
 }
diff --git a/src/main/java/org/apache/maven/shared/jar/classes/JarClassesAnalysis.java b/src/main/java/org/apache/maven/shared/jar/classes/JarClassesAnalysis.java
index 68e297a..bd9cb22 100644
--- a/src/main/java/org/apache/maven/shared/jar/classes/JarClassesAnalysis.java
+++ b/src/main/java/org/apache/maven/shared/jar/classes/JarClassesAnalysis.java
@@ -26,8 +26,13 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Optional;
+import java.util.TreeMap;
 import java.util.jar.JarEntry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.apache.bcel.classfile.ClassFormatException;
 import org.apache.bcel.classfile.ClassParser;
@@ -36,6 +41,7 @@ import org.apache.bcel.classfile.JavaClass;
 import org.apache.bcel.classfile.LineNumberTable;
 import org.apache.bcel.classfile.Method;
 import org.apache.maven.shared.jar.JarAnalyzer;
+import org.apache.maven.shared.jar.JarData;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,8 +57,17 @@ import org.slf4j.LoggerFactory;
 @Named
 @SuppressWarnings("checkstyle:MagicNumber")
 public class JarClassesAnalysis {
+
     private final Logger logger = LoggerFactory.getLogger(getClass());
 
+    /**
+     * Constant representing the classes in the root of a Multi-Release JAR file.
+     * Meaning outside of any given META-INF/versions/NN/... entry.
+     */
+    private static final Integer ROOT = 0;
+
+    private static final Pattern ENTRY_FILTER_MULTI_RELEASE = Pattern.compile("^META-INF/versions/(\\d{1,2})/.+$");
+
     private static final Map<Double, String> JAVA_CLASS_VERSIONS;
 
     static {
@@ -90,68 +105,145 @@ public class JarClassesAnalysis {
      * @return the details of the classes found
      */
     public JarClasses analyze(JarAnalyzer jarAnalyzer) {
-        JarClasses classes = jarAnalyzer.getJarData().getJarClasses();
+        JarData jarData = jarAnalyzer.getJarData();
+        JarClasses classes = jarData.getJarClasses();
         if (classes == null) {
-            String jarfilename = jarAnalyzer.getFile().getAbsolutePath();
-            classes = new JarClasses();
+            if (jarData.isMultiRelease()) {
+                classes = analyzeMultiRelease(jarAnalyzer);
+            } else {
+                classes = analyzeRoot(jarAnalyzer);
+            }
+        }
+        return classes;
+    }
 
-            List<JarEntry> classList = jarAnalyzer.getClassEntries();
+    private Integer jarEntryVersion(JarEntry entry) {
+        Matcher matcher = ENTRY_FILTER_MULTI_RELEASE.matcher(entry.getName());
+        if (matcher.matches()) {
+            return Integer.valueOf(matcher.group(1));
+        }
+        return ROOT;
+    }
 
-            classes.setDebugPresent(false);
+    private JarClasses analyzeMultiRelease(JarAnalyzer jarAnalyzer) {
+        String jarFilename = jarAnalyzer.getFile().getAbsolutePath();
 
-            double maxVersion = 0.0;
+        Map<Integer, List<JarEntry>> mapEntries =
+                jarAnalyzer.getEntries().stream().collect(Collectors.groupingBy(this::jarEntryVersion));
 
-            for (JarEntry entry : classList) {
-                String classname = entry.getName();
+        // ordered by increasing Java version
+        NavigableMap<Integer, JarVersionedRuntime> runtimeVersionsMap = new TreeMap<>();
 
-                try {
-                    ClassParser classParser = new ClassParser(jarfilename, classname);
+        for (Map.Entry<Integer, List<JarEntry>> mapEntry : mapEntries.entrySet()) {
+            Integer runtimeVersion = mapEntry.getKey();
+            List<JarEntry> runtimeVersionEntryList = mapEntry.getValue();
 
-                    JavaClass javaClass = classParser.parse();
+            List<JarEntry> classList = jarAnalyzer.getClassEntries(runtimeVersionEntryList);
 
-                    String classSignature = javaClass.getClassName();
+            JarClasses classes = analyze(jarFilename, classList);
 
-                    if (!classes.isDebugPresent()) {
-                        if (hasDebugSymbols(javaClass)) {
-                            classes.setDebugPresent(true);
-                        }
-                    }
+            runtimeVersionsMap.put(runtimeVersion, new JarVersionedRuntime(runtimeVersionEntryList, classes));
+        }
 
-                    double classVersion = javaClass.getMajor();
-                    if (javaClass.getMinor() > 0) {
-                        classVersion = classVersion + javaClass.getMinor() / 10.0;
-                    }
+        JarVersionedRuntime baseJarRelease = runtimeVersionsMap.remove(ROOT);
+        JarClasses baseJarClasses = baseJarRelease.getJarClasses();
 
-                    if (classVersion > maxVersion) {
-                        maxVersion = classVersion;
-                    }
+        jarAnalyzer.getJarData().setJarClasses(baseJarClasses);
 
-                    Method[] methods = javaClass.getMethods();
-                    for (Method method : methods) {
-                        classes.addMethod(classSignature + "." + method.getName() + method.getSignature());
-                    }
+        // Paranoid?
+        for (Map.Entry<Integer, JarVersionedRuntime> runtimeVersionEntry : runtimeVersionsMap.entrySet()) {
+            Integer version = runtimeVersionEntry.getKey();
+            String jdkRevision = runtimeVersionEntry.getValue().getJarClasses().getJdkRevision();
+            if (!version.equals(Integer.valueOf(jdkRevision))) {
+                logger.warn(
+                        "Multi-release version {} in JAR file '{}' has some class compiled for Jdk revision {}",
+                        version,
+                        jarFilename,
+                        jdkRevision);
+            }
+        }
+
+        jarAnalyzer.getJarData().setVersionedRuntimes(new JarVersionedRuntimes(runtimeVersionsMap));
+
+        return baseJarClasses;
+    }
+
+    private JarClasses analyzeRoot(JarAnalyzer jarAnalyzer) {
+        String jarFilename = jarAnalyzer.getFile().getAbsolutePath();
+
+        List<JarEntry> classList = jarAnalyzer.getClassEntries();
 
-                    String classPackageName = javaClass.getPackageName();
+        JarClasses classes = analyze(jarFilename, classList);
 
-                    classes.addClassName(classSignature);
-                    classes.addPackage(classPackageName);
+        jarAnalyzer.getJarData().setJarClasses(classes);
+        return classes;
+    }
+
+    private JarClasses analyze(String jarFilename, List<JarEntry> classList) {
+        JarClasses classes = new JarClasses();
+
+        classes.setDebugPresent(false);
+
+        double maxVersion = 0.0;
+        double moduleInfoVersion = 0.0;
+
+        for (JarEntry entry : classList) {
+            String classname = entry.getName();
 
-                    ImportVisitor importVisitor = new ImportVisitor(javaClass);
-                    DescendingVisitor descVisitor = new DescendingVisitor(javaClass, importVisitor);
-                    javaClass.accept(descVisitor);
+            try {
+                ClassParser classParser = new ClassParser(jarFilename, classname);
 
-                    classes.addImports(importVisitor.getImports());
-                } catch (ClassFormatException e) {
-                    logger.warn("Unable to process class " + classname + " in JarAnalyzer File " + jarfilename, e);
-                } catch (IOException e) {
-                    logger.warn("Unable to process JarAnalyzer File " + jarfilename, e);
+                JavaClass javaClass = classParser.parse();
+
+                String classSignature = javaClass.getClassName();
+
+                if (!classes.isDebugPresent()) {
+                    if (hasDebugSymbols(javaClass)) {
+                        classes.setDebugPresent(true);
+                    }
                 }
-            }
 
-            Optional.ofNullable(JAVA_CLASS_VERSIONS.get(maxVersion)).ifPresent(classes::setJdkRevision);
+                double classVersion = javaClass.getMajor();
+                if (javaClass.getMinor() > 0) {
+                    classVersion = classVersion + javaClass.getMinor() / 10.0;
+                }
 
-            jarAnalyzer.getJarData().setJarClasses(classes);
+                if ("module-info".equals(classSignature)) {
+                    // ignore the module-info.class for computing the maxVersion, since it will always be >= 9
+                    moduleInfoVersion = classVersion;
+                } else if (classVersion > maxVersion) {
+                    maxVersion = classVersion;
+                }
+
+                Method[] methods = javaClass.getMethods();
+                for (Method method : methods) {
+                    classes.addMethod(classSignature + "." + method.getName() + method.getSignature());
+                }
+
+                String classPackageName = javaClass.getPackageName();
+
+                classes.addClassName(classSignature);
+                classes.addPackage(classPackageName);
+
+                ImportVisitor importVisitor = new ImportVisitor(javaClass);
+                DescendingVisitor descVisitor = new DescendingVisitor(javaClass, importVisitor);
+                javaClass.accept(descVisitor);
+
+                classes.addImports(importVisitor.getImports());
+            } catch (ClassFormatException e) {
+                logger.warn("Unable to process class " + classname + " in JarAnalyzer File " + jarFilename, e);
+            } catch (IOException e) {
+                logger.warn("Unable to process JarAnalyzer File " + jarFilename, e);
+            }
+        }
+
+        if (maxVersion == 0.0 && moduleInfoVersion > 0.0) {
+            // the one and only class file was module-info.class
+            maxVersion = moduleInfoVersion;
         }
+
+        Optional.ofNullable(JAVA_CLASS_VERSIONS.get(maxVersion)).ifPresent(classes::setJdkRevision);
+
         return classes;
     }
 
diff --git a/src/main/java/org/apache/maven/shared/jar/classes/JarVersionedRuntime.java b/src/main/java/org/apache/maven/shared/jar/classes/JarVersionedRuntime.java
new file mode 100644
index 0000000..f76cd30
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/jar/classes/JarVersionedRuntime.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.shared.jar.classes;
+
+import java.util.List;
+import java.util.jar.JarEntry;
+
+public class JarVersionedRuntime {
+
+    /**
+     * The JAR entries.
+     */
+    private final List<JarEntry> entries;
+
+    private final JarClasses jarClasses;
+
+    public JarVersionedRuntime(List<JarEntry> entries, JarClasses jarClasses) {
+        this.entries = entries;
+        this.jarClasses = jarClasses;
+    }
+
+    /**
+     * @return the entries
+     */
+    public List<JarEntry> getEntries() {
+        return entries;
+    }
+
+    /**
+     * @return the jarClasses
+     */
+    public JarClasses getJarClasses() {
+        return jarClasses;
+    }
+}
diff --git a/src/main/java/org/apache/maven/shared/jar/classes/JarVersionedRuntimes.java b/src/main/java/org/apache/maven/shared/jar/classes/JarVersionedRuntimes.java
new file mode 100644
index 0000000..7afa7bd
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/jar/classes/JarVersionedRuntimes.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.shared.jar.classes;
+
+import java.util.Map.Entry;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Gathered facts about the runtime versions contained within a Multi-Release JAR file.
+ *
+ * @see org.apache.maven.shared.jar.classes.JarClassesAnalysis#analyze(org.apache.maven.shared.jar.JarAnalyzer)
+ */
+public class JarVersionedRuntimes {
+
+    /**
+     * Information about the JAR's Multi-Release entries
+     */
+    private NavigableMap<Integer, JarVersionedRuntime> versionedRuntimeMap;
+
+    public JarVersionedRuntimes(NavigableMap<Integer, JarVersionedRuntime> versionedRuntimeMap) {
+        this.versionedRuntimeMap = versionedRuntimeMap;
+    }
+
+    /**
+     * @return the versionedRuntimeMap
+     */
+    public NavigableMap<Integer, JarVersionedRuntime> getVersionedRuntimeMap() {
+        return versionedRuntimeMap;
+    }
+
+    public JarVersionedRuntime getJarVersionedRuntime(Integer version) {
+        return versionedRuntimeMap.get(version);
+    }
+
+    /**
+     * Return the JarClasses associated to the release.
+     * @param version the release version.
+     * @return the JarClasses.
+     */
+    public JarClasses getJarClasses(Integer version) {
+        return versionedRuntimeMap.get(version).getJarClasses();
+    }
+
+    /**
+     * Get a set of release versions included in the JAR file.
+     * @return a set with the Java versions as Strings.
+     */
+    public Set<Integer> getRuntimeVersionsAsSet() {
+        return versionedRuntimeMap.keySet();
+    }
+
+    /**
+     * Return the highest the JarClasses of the Jdk version that would be executed if they would be executed on a JVM given by the release parameter.
+     * @param version the Jdk version number of the executing JVM.
+     * @return The fittest JarClasses object matching if found one, or null otherwise.
+     * @throws NullPointerException if release is null.
+     */
+    public JarVersionedRuntime getBestFitJarVersionedRuntime(Integer version) {
+        Objects.requireNonNull(version, "version cannot be null");
+        Entry<Integer, JarVersionedRuntime> entry = versionedRuntimeMap.floorEntry(version);
+        if (entry == null) {
+            return null;
+        }
+        return entry.getValue();
+    }
+
+    /**
+     * Return the highest the JarClasses of the Jdk version that would be executed if they would be executed given a System property.
+     * Example values: "java.version.specification" or "java.vm.specification.version".
+     * @param key the System property.
+     * @return The best fitting JarClasses object matching if found one, or null otherwise.
+     * @throws NullPointerException if key is null
+     * @throws IllegalStateException if system property value of key is null
+     * @throws IllegalStateException if system property cannot be converted to Integer
+     */
+    public JarVersionedRuntime getBestFitJarVersionedRuntimeBySystemProperty(String key) {
+        Objects.requireNonNull(key, "key cannot null");
+        String property = System.getProperty(key);
+        if (property == null) {
+            throw new IllegalStateException("The value of the system property '" + key + "' is null");
+        }
+        try {
+            Integer release = Integer.parseInt(property);
+            return getBestFitJarVersionedRuntime(release);
+        } catch (NumberFormatException e) {
+            throw new IllegalStateException(
+                    "The value of the system property '" + key + "' [" + property
+                            + "] cannot be converted to an Integer",
+                    e);
+        }
+    }
+}
diff --git a/src/test/java/org/apache/maven/shared/jar/JarAnalyzerTest.java b/src/test/java/org/apache/maven/shared/jar/JarAnalyzerTest.java
index 8cac275..f342573 100644
--- a/src/test/java/org/apache/maven/shared/jar/JarAnalyzerTest.java
+++ b/src/test/java/org/apache/maven/shared/jar/JarAnalyzerTest.java
@@ -64,6 +64,18 @@ class JarAnalyzerTest extends AbstractJarAnalyzerTestCase {
         assertFalse(jarData.isSealed());
     }
 
+    @Test
+    void testMultiRelease() throws Exception {
+        JarData jarData = getJarData("multi-release-test-0.0.1.jar");
+        assertTrue(jarData.isMultiRelease());
+    }
+
+    @Test
+    void testNotMultiRelease() throws Exception {
+        JarData jarData = getJarData("codec.jar");
+        assertFalse(jarData.isMultiRelease());
+    }
+
     @Test
     void testMissingFile() {
         assertThrows(IOException.class, () -> new JarAnalyzer(new File("foo-bar-this-should-not-exist.jar")));
diff --git a/src/test/java/org/apache/maven/shared/jar/classes/JarClassesAnalyzerTest.java b/src/test/java/org/apache/maven/shared/jar/classes/JarClassesAnalyzerTest.java
index 8c7bb2d..dc7c1db 100644
--- a/src/test/java/org/apache/maven/shared/jar/classes/JarClassesAnalyzerTest.java
+++ b/src/test/java/org/apache/maven/shared/jar/classes/JarClassesAnalyzerTest.java
@@ -21,9 +21,13 @@ package org.apache.maven.shared.jar.classes;
 import javax.inject.Inject;
 
 import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarEntry;
 
 import org.apache.maven.shared.jar.AbstractJarAnalyzerTestCase;
 import org.apache.maven.shared.jar.JarAnalyzer;
+import org.apache.maven.shared.jar.JarData;
 import org.codehaus.plexus.testing.PlexusTest;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -34,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
 /**
  * JarAnalyzer Classes Test Case
@@ -137,6 +142,181 @@ class JarClassesAnalyzerTest extends AbstractJarAnalyzerTestCase {
         assertEquals(expectedRevision, jclass.getJdkRevision());
     }
 
+    @Test
+    void testAnalyzeJarWithModuleInfoClass() throws Exception {
+        JarData jarData = getJarData("tomcat-jni-9.0.75.jar");
+        JarClasses jclass = jarData.getJarClasses();
+        assertEquals("1.8", jclass.getJdkRevision());
+    }
+
+    @Test
+    void testAnalyzeJarWithOnlyModuleInfoClass() throws Exception {
+        JarData jarData = getJarData("module-info-only-test-0.0.1.jar");
+        // root level classes
+        JarClasses jclass = jarData.getJarClasses();
+        assertTrue(jclass.getImports().isEmpty());
+        assertTrue(jclass.getPackages().isEmpty());
+        assertTrue(jclass.getClassNames().isEmpty());
+        assertTrue(jclass.getMethods().isEmpty());
+        assertNull(jclass.getJdkRevision());
+
+        JarVersionedRuntimes jarVersionedRuntimes = jarData.getVersionedRuntimes();
+        assertNotNull(jarVersionedRuntimes);
+        Map<Integer, JarVersionedRuntime> jarVersionedRuntimeMap = jarVersionedRuntimes.getVersionedRuntimeMap();
+        assertNotNull(jarVersionedRuntimeMap);
+        assertEquals(1, jarVersionedRuntimeMap.size()); // 11
+
+        JarVersionedRuntime jarVersionedRuntime11 = jarVersionedRuntimes.getJarVersionedRuntime(11);
+        JarClasses jarClasses11 = jarVersionedRuntime11.getJarClasses();
+        assertEquals("11", jarClasses11.getJdkRevision());
+        assertTrue(jarClasses11.getImports().isEmpty());
+        assertEquals(1, jarClasses11.getPackages().size());
+        assertEquals("", jarClasses11.getPackages().get(0));
+        assertEquals(1, jarClasses11.getClassNames().size());
+        assertTrue(jarClasses11.getMethods().isEmpty());
+        assertEquals(1, jarVersionedRuntime11.getEntries().size());
+        assertEntriesContains(jarVersionedRuntime11.getEntries(), "META-INF/versions/11/module-info.class");
+    }
+
+    @Test
+    void testAnalyzeMultiReleaseJarVersion() throws Exception {
+        JarData jarData = getJarData("multi-release-test-0.0.1.jar");
+        JarClasses jclass = jarData.getJarClasses();
+
+        assertEquals("1.8", jclass.getJdkRevision());
+        assertFalse(jclass.getImports().isEmpty());
+        assertEquals(1, jclass.getPackages().size());
+        assertEquals(1, jclass.getClassNames().size());
+        assertFalse(jclass.getMethods().isEmpty());
+        assertEntriesContains(jarData.getEntries(), "resource.txt");
+
+        JarVersionedRuntimes jarVersionedRuntimes = jarData.getVersionedRuntimes();
+        assertNotNull(jarVersionedRuntimes);
+        Map<Integer, JarVersionedRuntime> jarVersionedRuntimeMap = jarVersionedRuntimes.getVersionedRuntimeMap();
+        assertNotNull(jarVersionedRuntimeMap);
+        assertEquals(2, jarVersionedRuntimeMap.size()); // 9 and 11
+
+        JarVersionedRuntime jarVersionedRuntime9 = jarVersionedRuntimes.getJarVersionedRuntime(9);
+        JarClasses jarClasses9 = jarVersionedRuntime9.getJarClasses();
+        assertEquals("9", jarClasses9.getJdkRevision());
+        assertFalse(jarClasses9.getImports().isEmpty());
+        assertEquals(1, jarClasses9.getPackages().size());
+        assertEquals(1, jarClasses9.getClassNames().size());
+        assertFalse(jarClasses9.getMethods().isEmpty());
+        assertEntriesContains(jarVersionedRuntime9.getEntries(), "META-INF/versions/9/resource.txt");
+
+        JarVersionedRuntime jarVersionedRuntime11 = jarVersionedRuntimes.getJarVersionedRuntime(11);
+        JarClasses jarClasses11 = jarVersionedRuntime11.getJarClasses();
+        assertEquals("11", jarClasses11.getJdkRevision());
+        assertFalse(jarClasses11.getImports().isEmpty());
+        assertEquals(1, jarClasses11.getPackages().size());
+        assertEquals(1, jarClasses11.getClassNames().size());
+        assertFalse(jarClasses11.getMethods().isEmpty());
+        assertEntriesContains(jarVersionedRuntime11.getEntries(), "META-INF/versions/11/resource.txt");
+
+        // test ordering
+        assertEquals("[9, 11]", jarVersionedRuntimeMap.keySet().toString());
+
+        // test best fit
+        try {
+            jarVersionedRuntimes.getBestFitJarVersionedRuntime(null);
+            fail("It should throw an NPE");
+        } catch (NullPointerException e) {
+            assertTrue(true);
+        }
+        assertNull(jarVersionedRuntimes.getBestFitJarVersionedRuntime(8)); // unreal value but good just for testing
+        assertEquals(
+                "9",
+                jarVersionedRuntimes
+                        .getBestFitJarVersionedRuntime(9)
+                        .getJarClasses()
+                        .getJdkRevision());
+        assertEquals(
+                "9",
+                jarVersionedRuntimes
+                        .getBestFitJarVersionedRuntime(10)
+                        .getJarClasses()
+                        .getJdkRevision());
+        assertEquals(
+                "11",
+                jarVersionedRuntimes
+                        .getBestFitJarVersionedRuntime(11)
+                        .getJarClasses()
+                        .getJdkRevision());
+        assertEquals(
+                "11",
+                jarVersionedRuntimes
+                        .getBestFitJarVersionedRuntime(20)
+                        .getJarClasses()
+                        .getJdkRevision());
+
+        try {
+            jarVersionedRuntimes.getBestFitJarVersionedRuntimeBySystemProperty(null);
+            fail("It should throw an NPE");
+        } catch (NullPointerException e) {
+            assertTrue(true);
+        }
+
+        try {
+            getBestFitReleaseBySystemProperty(jarVersionedRuntimes, null);
+            fail("It should throw an NPE");
+        } catch (NullPointerException e) {
+            assertTrue(true);
+        }
+
+        try {
+            getBestFitReleaseBySystemProperty(jarVersionedRuntimes, "xxx");
+            fail("It should throw an ISE");
+        } catch (IllegalStateException e) {
+            assertTrue(true);
+        }
+
+        assertNull(getBestFitReleaseBySystemProperty(jarVersionedRuntimes, "8"));
+        assertEquals(
+                "9",
+                getBestFitReleaseBySystemProperty(jarVersionedRuntimes, "9")
+                        .getJarClasses()
+                        .getJdkRevision());
+        assertEquals(
+                "9",
+                getBestFitReleaseBySystemProperty(jarVersionedRuntimes, "10")
+                        .getJarClasses()
+                        .getJdkRevision());
+        assertEquals(
+                "11",
+                getBestFitReleaseBySystemProperty(jarVersionedRuntimes, "11")
+                        .getJarClasses()
+                        .getJdkRevision());
+        assertEquals(
+                "11",
+                getBestFitReleaseBySystemProperty(jarVersionedRuntimes, "20")
+                        .getJarClasses()
+                        .getJdkRevision());
+    }
+
+    private void assertEntriesContains(List<JarEntry> list, final String entryToFind) {
+        assertTrue(list.stream().anyMatch(entry -> entry.getName().equals(entryToFind)));
+    }
+
+    private JarVersionedRuntime getBestFitReleaseBySystemProperty(
+            JarVersionedRuntimes jarVersionedRuntimes, String value) {
+        String key = "maven.shared.jar.test.vm";
+        System.setProperty(key, value);
+        return jarVersionedRuntimes.getBestFitJarVersionedRuntimeBySystemProperty(key);
+    }
+
+    private JarData getJarData(String filename) throws Exception {
+        File file = getSampleJar(filename);
+
+        JarAnalyzer jarAnalyzer = new JarAnalyzer(file);
+        JarClasses jclass = analyzer.analyze(jarAnalyzer);
+        JarData jarData = jarAnalyzer.getJarData();
+        assertNotNull(jclass, "JarClasses");
+        assertNotNull(jarData, "JarData");
+
+        return jarData;
+    }
+
     private JarClasses getJarClasses(String filename) throws Exception {
         File file = getSampleJar(filename);
 
diff --git a/src/test/resources/jars/module-info-only-test-0.0.1.jar b/src/test/resources/jars/module-info-only-test-0.0.1.jar
new file mode 100644
index 0000000..c6f5752
Binary files /dev/null and b/src/test/resources/jars/module-info-only-test-0.0.1.jar differ
diff --git a/src/test/resources/jars/multi-release-test-0.0.1.jar b/src/test/resources/jars/multi-release-test-0.0.1.jar
new file mode 100644
index 0000000..b0a03bc
Binary files /dev/null and b/src/test/resources/jars/multi-release-test-0.0.1.jar differ
diff --git a/src/test/resources/jars/tomcat-jni-9.0.75.jar b/src/test/resources/jars/tomcat-jni-9.0.75.jar
new file mode 100644
index 0000000..93d0d6b
Binary files /dev/null and b/src/test/resources/jars/tomcat-jni-9.0.75.jar differ