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

csutherl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 87dc4caebf Enhance version.sh and version.bat output to display APR, 
OpenSSL, and third-party library versions (#919)
87dc4caebf is described below

commit 87dc4caebf773c1ff221addca6eac86ad76c55ee
Author: Coty Sutherland <[email protected]>
AuthorDate: Thu Apr 30 12:05:53 2026 -0400

    Enhance version.sh and version.bat output to display APR, OpenSSL, and 
third-party library versions (#919)
---
 bin/catalina.bat                                   |   2 +-
 bin/catalina.sh                                    |   4 +-
 .../apache/catalina/core/AprLifecycleListener.java |  90 ++++
 .../catalina/core/OpenSSLLifecycleListener.java    |  23 ++
 java/org/apache/catalina/util/ServerInfo.java      | 215 ++++++++++
 .../util/net/openssl/panama/OpenSSLLibrary.java    |  12 +
 test/org/apache/catalina/util/TestServerInfo.java  | 454 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   6 +
 8 files changed, 804 insertions(+), 2 deletions(-)

diff --git a/bin/catalina.bat b/bin/catalina.bat
index 70601b6799..0b7d8ba8bd 100755
--- a/bin/catalina.bat
+++ b/bin/catalina.bat
@@ -305,7 +305,7 @@ set CATALINA_OPTS=
 goto execCmd
 
 :doVersion
-%_EXECJAVA% %JAVA_OPTS% -classpath "%CATALINA_HOME%\lib\catalina.jar" 
org.apache.catalina.util.ServerInfo
+%_EXECJAVA% %JAVA_OPTS% -classpath 
"%CATALINA_HOME%\bin\tomcat-juli.jar;%CATALINA_HOME%\lib\*" 
-Dcatalina.home="%CATALINA_HOME%" -Dcatalina.base="%CATALINA_BASE%" 
org.apache.catalina.util.ServerInfo
 goto end
 
 
diff --git a/bin/catalina.sh b/bin/catalina.sh
index ee679ad0c6..0e6f33cce9 100755
--- a/bin/catalina.sh
+++ b/bin/catalina.sh
@@ -564,7 +564,9 @@ elif [ "$1" = "configtest" ] ; then
 elif [ "$1" = "version" ] ; then
 
    eval "\"$_RUNJAVA\"" "$JAVA_OPTS" \
-         -classpath "\"$CATALINA_HOME/lib/catalina.jar\"" \
+         -classpath 
"\"$CATALINA_HOME/bin/tomcat-juli.jar:$CATALINA_HOME/lib/*\"" \
+         -Dcatalina.home="\"$CATALINA_HOME\"" \
+         -Dcatalina.base="\"$CATALINA_BASE\"" \
          org.apache.catalina.util.ServerInfo
 
 else
diff --git a/java/org/apache/catalina/core/AprLifecycleListener.java 
b/java/org/apache/catalina/core/AprLifecycleListener.java
index 9ff9f2b8bb..2e0bf01585 100644
--- a/java/org/apache/catalina/core/AprLifecycleListener.java
+++ b/java/org/apache/catalina/core/AprLifecycleListener.java
@@ -122,6 +122,96 @@ public class AprLifecycleListener implements 
LifecycleListener {
         return org.apache.tomcat.jni.AprStatus.isAprAvailable();
     }
 
+    /**
+     * Helper method to safely get a version string from APR/TCN.
+     * Checks APR availability and handles exceptions.
+     *
+     * @param versionSupplier supplier that returns the version string
+     * @return the version string, or null if APR is not available or an error 
occurs
+     */
+    private static String getVersionString(java.util.function.Supplier<String> 
versionSupplier) {
+        if (!isAprAvailable()) {
+            return null;
+        }
+
+        try {
+            return versionSupplier.get();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Get the installed Tomcat Native version string, if available.
+     *
+     * @return the version string, or null if APR is not available
+     */
+    public static String getInstalledTcnVersion() {
+        return getVersionString(org.apache.tomcat.jni.Library::versionString);
+    }
+
+    /**
+     * Get the installed APR version string, if available.
+     *
+     * @return the APR version string, or null if APR is not available
+     */
+    public static String getInstalledAprVersion() {
+        return 
getVersionString(org.apache.tomcat.jni.Library::aprVersionString);
+    }
+
+    /**
+     * Get the installed OpenSSL version string (via APR), if available.
+     *
+     * @return the OpenSSL version string, or null if not available
+     */
+    public static String getInstalledOpenSslVersion() {
+        return getVersionString(org.apache.tomcat.jni.SSL::versionString);
+    }
+
+    /**
+     * Helper method to convert version components to a comparable integer.
+     *
+     * @param major major version number
+     * @param minor minor version number
+     * @param patch patch version number
+     *
+     * @return comparable version integer
+     */
+    private static int versionToInt(int major, int minor, int patch) {
+        return major * 1000 + minor * 100 + patch;
+    }
+
+    /**
+     * Get a warning message if the installed Tomcat Native version is older 
than recommended.
+     * This performs the same version check used during Tomcat startup.
+     *
+     * @return a warning message if the installed version is outdated, or null 
if the version
+     *         is acceptable or APR is not available
+     */
+    public static String getTcnVersionWarning() {
+        if (!isAprAvailable()) {
+            return null;
+        }
+
+        try {
+            int installedVersion = versionToInt(
+                    org.apache.tomcat.jni.Library.TCN_MAJOR_VERSION,
+                    org.apache.tomcat.jni.Library.TCN_MINOR_VERSION,
+                    org.apache.tomcat.jni.Library.TCN_PATCH_VERSION);
+            int recommendedVersion = versionToInt(
+                    TCN_RECOMMENDED_MAJOR,
+                    TCN_RECOMMENDED_MINOR,
+                    TCN_RECOMMENDED_PV);
+            if (installedVersion < recommendedVersion) {
+                return "WARNING: Tomcat recommends a minimum version of " +
+                        TCN_RECOMMENDED_MAJOR + "." + TCN_RECOMMENDED_MINOR + 
"." + TCN_RECOMMENDED_PV;
+            }
+            return null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     public AprLifecycleListener() {
         org.apache.tomcat.jni.AprStatus.setInstanceCreated(true);
     }
diff --git a/java/org/apache/catalina/core/OpenSSLLifecycleListener.java 
b/java/org/apache/catalina/core/OpenSSLLifecycleListener.java
index 1e314ba87b..ba8c082b53 100644
--- a/java/org/apache/catalina/core/OpenSSLLifecycleListener.java
+++ b/java/org/apache/catalina/core/OpenSSLLifecycleListener.java
@@ -67,6 +67,29 @@ public class OpenSSLLifecycleListener implements 
LifecycleListener {
         return OpenSSLStatus.isAvailable();
     }
 
+    /**
+     * Get the installed OpenSSL version string (via FFM), if available.
+     *
+     * @return the OpenSSL version string (e.g., "OpenSSL 3.2.6 30 Sep 2025"), 
or null if not available
+     */
+    public static String getInstalledOpenSslVersion() {
+        if (!isAvailable()) {
+            return null;
+        }
+
+        if (JreCompat.isJre22Available()) {
+            try {
+                Class<?> openSSLLibraryClass =
+                        
Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary");
+                return (String) 
openSSLLibraryClass.getMethod("getVersionString").invoke(null);
+            } catch (Throwable t) {
+                Throwable throwable = 
ExceptionUtils.unwrapInvocationTargetException(t);
+                ExceptionUtils.handleThrowable(throwable);
+            }
+        }
+        return null;
+    }
+
     public OpenSSLLifecycleListener() {
         OpenSSLStatus.setInstanceCreated(true);
     }
diff --git a/java/org/apache/catalina/util/ServerInfo.java 
b/java/org/apache/catalina/util/ServerInfo.java
index ae74d176da..d88df98db7 100644
--- a/java/org/apache/catalina/util/ServerInfo.java
+++ b/java/org/apache/catalina/util/ServerInfo.java
@@ -17,8 +17,13 @@
 package org.apache.catalina.util;
 
 
+import java.io.File;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Properties;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
 
 import org.apache.tomcat.util.ExceptionUtils;
 
@@ -121,6 +126,10 @@ public class ServerInfo {
     }
 
     public static void main(String[] args) {
+        // Suppress INFO logging from library initialization
+        
java.util.logging.Logger.getLogger("org.apache.tomcat.util.net.openssl.panama").setLevel(java.util.logging.Level.WARNING);
+        
java.util.logging.Logger.getLogger("org.apache.catalina.core").setLevel(java.util.logging.Level.WARNING);
+
         System.out.println("Server version: " + getServerInfo());
         System.out.println("Server built:   " + getServerBuilt());
         System.out.println("Server number:  " + getServerNumber());
@@ -129,6 +138,212 @@ public class ServerInfo {
         System.out.println("Architecture:   " + System.getProperty("os.arch"));
         System.out.println("JVM Version:    " + 
System.getProperty("java.runtime.version"));
         System.out.println("JVM Vendor:     " + 
System.getProperty("java.vm.vendor"));
+
+        // Get CATALINA_HOME for library scanning (already displayed in 
catalina script output preface)
+        String catalinaHome = System.getProperty("catalina.home");
+
+        // Display APR/Tomcat Native information if available
+        boolean aprLoaded = false;
+        try {
+            // Try to initialize APR by creating an instance and calling 
isAprAvailable()
+            // Creating an instance sets the instance flag which allows 
initialization
+            Class<?> aprLifecycleListenerClass = 
Class.forName("org.apache.catalina.core.AprLifecycleListener");
+            aprLifecycleListenerClass.getConstructor().newInstance();
+            Boolean aprAvailable = (Boolean) 
aprLifecycleListenerClass.getMethod("isAprAvailable").invoke(null);
+            if (aprAvailable != null && aprAvailable.booleanValue()) {
+                // APR is available, get version information using public 
methods
+                String tcnVersion = (String) 
aprLifecycleListenerClass.getMethod("getInstalledTcnVersion").invoke(null);
+                String aprVersion = (String) 
aprLifecycleListenerClass.getMethod("getInstalledAprVersion").invoke(null);
+
+                System.out.println("APR loaded:     true");
+                System.out.println("APR Version:    " + aprVersion);
+                System.out.println("Tomcat Native:  " + tcnVersion);
+                aprLoaded = true;
+
+                // Check if installed version is older than recommended
+                try {
+                    String warning = (String) 
aprLifecycleListenerClass.getMethod("getTcnVersionWarning").invoke(null);
+
+                    if (warning != null) {
+                        System.out.println("                " + warning);
+                    }
+                } catch (Exception e) {
+                    // Failed to check version - ignore
+                }
+
+                // Display OpenSSL version if available
+                try {
+                    String openSSLVersion = (String) 
aprLifecycleListenerClass.getMethod("getInstalledOpenSslVersion").invoke(null);
+
+                    if (openSSLVersion != null && !openSSLVersion.isEmpty()) {
+                        System.out.println("OpenSSL (APR):  " + 
openSSLVersion);
+                    }
+                } catch (Exception e) {
+                    // SSL not initialized or not available
+                }
+            }
+        } catch (ClassNotFoundException | NoClassDefFoundError e) {
+            // APR/Tomcat Native classes not available on classpath
+        } catch (Exception e) {
+            // Error checking APR status
+        }
+
+        if (!aprLoaded) {
+            System.out.println("APR loaded:     false");
+        }
+
+        // Display FFM OpenSSL information if available
+        try {
+            // Try to initialize FFM OpenSSL by creating an instance and 
calling isAvailable()
+            // Creating an instance sets the instance flag which allows 
initialization
+            Class<?> openSSLLifecycleListenerClass = 
Class.forName("org.apache.catalina.core.OpenSSLLifecycleListener");
+            openSSLLifecycleListenerClass.getConstructor().newInstance();
+            Boolean ffmAvailable = (Boolean) 
openSSLLifecycleListenerClass.getMethod("isAvailable").invoke(null);
+
+            if (ffmAvailable != null && ffmAvailable.booleanValue()) {
+                // FFM OpenSSL is available, get version information using 
public method
+                String versionString = (String) 
openSSLLifecycleListenerClass.getMethod("getInstalledOpenSslVersion").invoke(null);
+
+                if (versionString != null && !versionString.isEmpty()) {
+                    System.out.println("OpenSSL (FFM):  " + versionString);
+                }
+            }
+        } catch (ClassNotFoundException | NoClassDefFoundError e) {
+            // FFM OpenSSL classes not available on classpath
+        } catch (Exception e) {
+            // Error checking FFM OpenSSL status
+        }
+
+        // Display third-party libraries in CATALINA_HOME/lib
+        if (catalinaHome != null) {
+            File libDir = new File(catalinaHome, "lib");
+            if (libDir.exists() && libDir.isDirectory()) {
+                File[] allJars = libDir.listFiles((dir, name) -> 
name.endsWith(".jar"));
+
+                if (allJars != null && allJars.length > 0) {
+                    // First pass: collect third-party JARs and find longest 
name
+                    List<File> thirdPartyJars = new ArrayList<>();
+                    int maxNameLength = 0;
+                    for (File jar : allJars) {
+                        if (!isTomcatCoreJar(jar)) {
+                            thirdPartyJars.add(jar);
+                            maxNameLength = Math.max(maxNameLength, 
jar.getName().length());
+                        }
+                    }
+
+                    // Second pass: print with aligned formatting
+                    if (!thirdPartyJars.isEmpty()) {
+                        System.out.println();
+                        System.out.println("Third-party libraries:");
+                        for (File jar : thirdPartyJars) {
+                            String version = getJarVersion(jar);
+                            String jarName = jar.getName();
+                            // Colon right after name, then pad to align 
version numbers
+                            String nameWithColon = jarName + ":";
+                            String paddedName = String.format("%-" + 
(maxNameLength + 1) + "s", nameWithColon);
+                            if (version != null) {
+                                System.out.println("  " + paddedName + " " + 
version);
+                            } else {
+                                System.out.println("  " + paddedName + " 
(unknown)");
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private static boolean isTomcatCoreJar(File jarFile) {
+        try (JarFile jar = new JarFile(jarFile)) {
+            Manifest manifest = jar.getManifest();
+
+            if (manifest != null) {
+                // Check Bundle-SymbolicName to identify Tomcat core JARs
+                String bundleName = 
manifest.getMainAttributes().getValue("Bundle-SymbolicName");
+                if (bundleName != null) {
+                    // Tomcat core JARs have Bundle-SymbolicName starting with 
org.apache.tomcat,
+                    // org.apache.catalina, or jakarta.
+                    if (bundleName.startsWith("org.apache.tomcat") ||
+                            bundleName.startsWith("org.apache.catalina") ||
+                            bundleName.startsWith("jakarta.")) {
+                        return true;
+                    }
+                }
+
+                // Fallback: Check Implementation-Vendor and 
Implementation-Title
+                String implVendor = 
manifest.getMainAttributes().getValue("Implementation-Vendor");
+                String implTitle = 
manifest.getMainAttributes().getValue("Implementation-Title");
+
+                if ("Apache Software Foundation".equals(implVendor) && "Apache 
Tomcat".equals(implTitle)) {
+                    return true;
+                }
+            }
+        } catch (Exception e) {
+            // Ignore errors reading JAR manifest
+        }
+
+        return false;
+    }
+
+    private static String getJarVersion(File jarFile) {
+        // First try manifest attributes
+        try (JarFile jar = new JarFile(jarFile)) {
+            Manifest manifest = jar.getManifest();
+
+            if (manifest != null) {
+                // Try different common version attributes
+                String[] versionAttrs = {"Bundle-Version", 
"Implementation-Version", "Specification-Version"};
+                for (String attr : versionAttrs) {
+                    String version = 
manifest.getMainAttributes().getValue(attr);
+                    if (version != null) {
+                        return version;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // Ignore errors reading JAR manifest
+        }
+
+        // Fallback: try to parse version from filename
+        return parseVersionFromFilename(jarFile.getName());
+    }
+
+    /**
+     * Attempt to extract a version number from a JAR filename.
+     * Common patterns include:
+     * - name-version.jar (e.g., commons-logging-1.2.jar)
+     * - name_version.jar (e.g., library_2.3.4.jar)
+     * - name-version-SNAPSHOT.jar (e.g., mylib-1.0.0-SNAPSHOT.jar)
+     *
+     * @param filename the JAR filename
+     * @return the extracted version string, or null if no version pattern is 
found
+     */
+    private static String parseVersionFromFilename(String filename) {
+        if (filename == null || !filename.endsWith(".jar")) {
+            return null;
+        }
+
+        // Remove .jar extension
+        String nameWithoutExt = filename.substring(0, filename.length() - 4);
+
+        // Try to find version pattern by looking for the first separator 
followed by a digit
+        // Search from right to left to find the start of the version string
+        String[] separators = {"-", "_"};
+        for (String sep : separators) {
+            // Find all occurrences of the separator
+            int index = nameWithoutExt.indexOf(sep);
+            while (index >= 0 && index < nameWithoutExt.length() - 1) {
+                String candidate = nameWithoutExt.substring(index + 1);
+                // Check if this looks like a version number (starts with 
digit)
+                if (!candidate.isEmpty() && 
Character.isDigit(candidate.charAt(0))) {
+                    return candidate;
+                }
+                // Move to next separator
+                index = nameWithoutExt.indexOf(sep, index + 1);
+            }
+        }
+
+        return null;
     }
 
 }
diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java 
b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
index 9eb8c0426f..2e51ad7141 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
+++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
@@ -428,6 +428,18 @@ public class OpenSSLLibrary {
         return fipsModeActive;
     }
 
+    public static String getVersionString() {
+        if (!OpenSSLStatus.isAvailable()) {
+            return null;
+        }
+
+        try {
+            return OpenSSL_version(0).getString(0);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     public static List<String> findCiphers(String ciphers) {
         ArrayList<String> ciphersList = new ArrayList<>();
         try (var localArena = Arena.ofConfined()) {
diff --git a/test/org/apache/catalina/util/TestServerInfo.java 
b/test/org/apache/catalina/util/TestServerInfo.java
index 3a61701730..0c36407650 100644
--- a/test/org/apache/catalina/util/TestServerInfo.java
+++ b/test/org/apache/catalina/util/TestServerInfo.java
@@ -16,8 +16,24 @@
  */
 package org.apache.catalina.util;
 
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Method;
+import java.util.function.Consumer;
+import java.util.jar.Attributes;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Test;
 
+import org.apache.catalina.core.AprLifecycleListener;
+import org.apache.catalina.core.OpenSSLLifecycleListener;
+import org.apache.tomcat.util.compat.JreCompat;
+
 public class TestServerInfo {
 
     /**
@@ -27,4 +43,442 @@ public class TestServerInfo {
     public void testServerInfo() {
         ServerInfo.main(new String[0]);
     }
+
+    /**
+     * Test that ServerInfo.main() outputs expected basic information.
+     */
+    @Test
+    public void testServerInfoOutput() throws Exception {
+        String output = captureServerInfoOutput();
+
+        // Check for expected output lines
+        Assert.assertTrue("Should contain server version", 
output.contains("Server version:"));
+        Assert.assertTrue("Should contain server built", 
output.contains("Server built:"));
+        Assert.assertTrue("Should contain server number", 
output.contains("Server number:"));
+        Assert.assertTrue("Should contain OS Name", output.contains("OS 
Name:"));
+        Assert.assertTrue("Should contain JVM Version", output.contains("JVM 
Version:"));
+        Assert.assertTrue("Should contain APR loaded status", 
output.contains("APR loaded:"));
+    }
+
+    /**
+     * Test isTomcatCoreJar() with Tomcat core JAR (Bundle-SymbolicName 
pattern).
+     */
+    @Test
+    public void testIsTomcatCoreJarWithBundleSymbolicName() throws Exception {
+        withTestJar("test-tomcat-core.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            manifest.getMainAttributes().putValue("Bundle-SymbolicName", 
"org.apache.tomcat-test");
+        }, jar -> Assert.assertTrue("Should identify org.apache.tomcat-* as 
core JAR",
+                invokeIsTomcatCoreJar(jar)));
+    }
+
+    /**
+     * Test isTomcatCoreJar() with Catalina core JAR (Bundle-SymbolicName 
pattern).
+     */
+    @Test
+    public void testIsTomcatCoreJarWithCatalinaSymbolicName() throws Exception 
{
+        withTestJar("test-catalina-core.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            manifest.getMainAttributes().putValue("Bundle-SymbolicName", 
"org.apache.catalina-ha");
+        }, jar -> Assert.assertTrue("Should identify org.apache.catalina-* as 
core JAR",
+                invokeIsTomcatCoreJar(jar)));
+    }
+
+    /**
+     * Test isTomcatCoreJar() with Jakarta API JAR (Bundle-SymbolicName 
pattern).
+     */
+    @Test
+    public void testIsTomcatCoreJarWithJakartaSymbolicName() throws Exception {
+        withTestJar("test-jakarta-api.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            manifest.getMainAttributes().putValue("Bundle-SymbolicName", 
"jakarta.servlet.api");
+        }, jar -> Assert.assertTrue("Should identify jakarta.* as core JAR",
+                invokeIsTomcatCoreJar(jar)));
+    }
+
+    /**
+     * Test isTomcatCoreJar() with Tomcat core JAR (Implementation-Vendor 
fallback).
+     */
+    @Test
+    public void testIsTomcatCoreJarWithImplementationVendor() throws Exception 
{
+        withTestJar("test-tomcat-i18n.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            
manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VENDOR, "Apache 
Software Foundation");
+            
manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_TITLE, "Apache 
Tomcat");
+        }, jar -> Assert.assertTrue("Should identify ASF/Tomcat as core JAR",
+                invokeIsTomcatCoreJar(jar)));
+    }
+
+    /**
+     * Test isTomcatCoreJar() with third-party JAR.
+     */
+    @Test
+    public void testIsTomcatCoreJarWithThirdParty() throws Exception {
+        withTestJar("test-third-party.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            manifest.getMainAttributes().putValue("Bundle-SymbolicName", 
"com.example.library");
+            
manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VENDOR, 
"Example Corp");
+        }, jar -> Assert.assertFalse("Should not identify third-party JAR as 
core",
+                invokeIsTomcatCoreJar(jar)));
+    }
+
+    /**
+     * Test getJarVersion() with Bundle-Version.
+     */
+    @Test
+    public void testGetJarVersionWithBundleVersion() throws Exception {
+        withTestJar("test-bundle-version.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            manifest.getMainAttributes().putValue("Bundle-Version", "1.2.3");
+        }, jar -> Assert.assertEquals("Should read Bundle-Version", "1.2.3",
+                invokeGetJarVersion(jar)));
+    }
+
+    /**
+     * Test getJarVersion() with Implementation-Version.
+     */
+    @Test
+    public void testGetJarVersionWithImplementationVersion() throws Exception {
+        withTestJar("test-impl-version.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            
manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VERSION, 
"2.3.4");
+        }, jar -> Assert.assertEquals("Should read Implementation-Version", 
"2.3.4",
+                invokeGetJarVersion(jar)));
+    }
+
+    /**
+     * Test getJarVersion() with Specification-Version.
+     */
+    @Test
+    public void testGetJarVersionWithSpecificationVersion() throws Exception {
+        withTestJar("test-spec-version.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            
manifest.getMainAttributes().put(Attributes.Name.SPECIFICATION_VERSION, 
"3.4.5");
+        }, jar -> Assert.assertEquals("Should read Specification-Version", 
"3.4.5",
+                invokeGetJarVersion(jar)));
+    }
+
+    /**
+     * Test getJarVersion() priority: Bundle-Version takes precedence.
+     */
+    @Test
+    public void testGetJarVersionPriority() throws Exception {
+        withTestJar("test-version-priority.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            manifest.getMainAttributes().putValue("Bundle-Version", "1.0.0");
+            
manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VERSION, 
"2.0.0");
+            
manifest.getMainAttributes().put(Attributes.Name.SPECIFICATION_VERSION, 
"3.0.0");
+        }, jar -> Assert.assertEquals("Should prioritize Bundle-Version", 
"1.0.0",
+                invokeGetJarVersion(jar)));
+    }
+
+    /**
+     * Test getJarVersion() with no version information.
+     */
+    @Test
+    public void testGetJarVersionWithNoVersion() throws Exception {
+        withTestJar("test-no-version.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+        }, jar -> Assert.assertNull("Should return null when no version found",
+                invokeGetJarVersion(jar)));
+    }
+
+    /**
+     * Test ServerInfo.main() output with APR/Tomcat Native when available.
+     */
+    @Test
+    public void testServerInfoOutputWithApr() throws Exception {
+        // Only run this test if APR is available
+        Assume.assumeTrue("APR not available", 
AprLifecycleListener.isAprAvailable());
+
+        String output = captureServerInfoOutput();
+
+        // Check for APR-specific output
+        Assert.assertTrue("Should contain 'APR loaded: true'", 
output.contains("APR loaded:     true"));
+        Assert.assertTrue("Should contain APR Version", output.contains("APR 
Version:"));
+        Assert.assertTrue("Should contain Tomcat Native version", 
output.contains("Tomcat Native:"));
+        // OpenSSL via APR should be present if SSL is initialized
+        // Note: May not always be present depending on initialization state
+    }
+
+    /**
+     * Test ServerInfo.main() output with FFM OpenSSL when available.
+     */
+    @Test
+    public void testServerInfoOutputWithFFM() throws Exception {
+        // Only run this test if JRE 22+ is available
+        Assume.assumeTrue("JRE 22+ not available", 
JreCompat.isJre22Available());
+
+        // Initialize FFM OpenSSL
+        boolean ffmAvailable = OpenSSLLifecycleListener.isAvailable();
+        Assume.assumeTrue("FFM OpenSSL not available", ffmAvailable);
+
+        String output = captureServerInfoOutput();
+
+        // Check for FFM OpenSSL output
+        Assert.assertTrue("Should contain OpenSSL (FFM) information", 
output.contains("OpenSSL (FFM):"));
+    }
+
+    /**
+     * Test ServerInfo.main() output when neither APR nor FFM is available.
+     */
+    @Test
+    public void testServerInfoOutputWithoutNativeLibraries() throws Exception {
+        // Skip if APR or FFM is available
+        boolean aprAvailable = AprLifecycleListener.isAprAvailable();
+        boolean ffmAvailable = JreCompat.isJre22Available() && 
OpenSSLLifecycleListener.isAvailable();
+
+        // Only run if neither is available (or force the test by not 
initializing them)
+        // This test validates the "not available" code path
+        if (!aprAvailable && !ffmAvailable) {
+            String output = captureServerInfoOutput();
+
+            // When no native libraries are available, should show APR loaded: 
false
+            Assert.assertTrue("Should contain 'APR loaded: false'", 
output.contains("APR loaded:     false"));
+            // Should NOT contain FFM or APR version information
+            Assert.assertFalse("Should not contain APR Version", 
output.contains("APR Version:"));
+            Assert.assertFalse("Should not contain Tomcat Native", 
output.contains("Tomcat Native:"));
+        }
+    }
+
+    /**
+     * Test that APR version info is displayed correctly.
+     */
+    @Test
+    public void testAprVersionInfo() throws Exception {
+        // Only run if APR is available
+        Assume.assumeTrue("APR not available", 
AprLifecycleListener.isAprAvailable());
+
+        String output = captureServerInfoOutput();
+
+        // Verify version info format (should contain version numbers)
+        String[] lines = output.split("\n");
+        boolean foundAprVersion = false;
+        boolean foundTcnVersion = false;
+
+        for (String line : lines) {
+            if (line.contains("APR Version:")) {
+                foundAprVersion = true;
+                // APR version should be in format like "1.7.0"
+                Assert.assertTrue("APR Version line should contain version 
number",
+                        line.matches(".*APR Version:\\s+\\d+\\.\\d+.*"));
+            }
+            if (line.contains("Tomcat Native:")) {
+                foundTcnVersion = true;
+                // Tomcat Native version should be in format like "2.0.5"
+                Assert.assertTrue("Tomcat Native line should contain version 
number",
+                        line.matches(".*Tomcat Native:\\s+\\d+\\.\\d+.*"));
+            }
+        }
+
+        Assert.assertTrue("Should have found APR Version line", 
foundAprVersion);
+        Assert.assertTrue("Should have found Tomcat Native line", 
foundTcnVersion);
+    }
+
+    /**
+     * Test that version warning is returned when APR is available but 
outdated.
+     * This tests the real version check using the installed APR library.
+     */
+    @Test
+    public void testTomcatNativeVersionWarningWithRealVersion() throws 
Exception {
+        // Only run if APR is available
+        Assume.assumeTrue("APR not available", 
AprLifecycleListener.isAprAvailable());
+
+        // If APR is available, getTcnVersionWarning() should return non-null 
if version is old,
+        // or null if version is current. We can't predict which, so just 
verify the method works.
+        String warning = AprLifecycleListener.getTcnVersionWarning();
+
+        // The warning should either be null (version is OK) or contain 
expected text
+        if (warning != null) {
+            Assert.assertTrue("Warning should mention 'WARNING'", 
warning.contains("WARNING"));
+            Assert.assertTrue("Warning should mention version", 
warning.matches(".*\\d+\\.\\d+\\.\\d+.*"));
+        }
+        // If warning is null, that's also valid (version is current)
+    }
+
+    /**
+     * Test that FFM OpenSSL version info is displayed correctly.
+     */
+    @Test
+    public void testFFMVersionInfo() throws Exception {
+        // Only run if JRE 22+ and FFM OpenSSL are available
+        Assume.assumeTrue("JRE 22+ not available", 
JreCompat.isJre22Available());
+
+        boolean ffmAvailable = OpenSSLLifecycleListener.isAvailable();
+        Assume.assumeTrue("FFM OpenSSL not available", ffmAvailable);
+
+        String output = captureServerInfoOutput();
+
+        // Verify FFM OpenSSL version info format
+        String[] lines = output.split("\n");
+        boolean foundFFMVersion = false;
+
+        for (String line : lines) {
+            if (line.contains("OpenSSL (FFM):")) {
+                foundFFMVersion = true;
+                // Should contain either version string or library name
+                Assert.assertTrue("OpenSSL (FFM) line should not be empty",
+                        line.length() > "OpenSSL (FFM):  ".length());
+            }
+        }
+
+        Assert.assertTrue("Should have found OpenSSL (FFM) line", 
foundFFMVersion);
+    }
+
+    /**
+     * Test that OpenSSLLibrary.getVersionString() returns the native version 
string.
+     * This ensures FFM output format matches APR output format.
+     */
+    @Test
+    public void testOpenSSLLibraryVersionString() throws Exception {
+        // Only run if JRE 22+ and FFM OpenSSL are available
+        Assume.assumeTrue("JRE 22+ not available", 
JreCompat.isJre22Available());
+
+        boolean ffmAvailable = OpenSSLLifecycleListener.isAvailable();
+        Assume.assumeTrue("FFM OpenSSL not available", ffmAvailable);
+
+        // Call OpenSSLLibrary.getVersionString() via reflection
+        Class<?> openSSLLibraryClass = 
Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary");
+        String versionString = (String) 
openSSLLibraryClass.getMethod("getVersionString").invoke(null);
+
+        // Verify the version string is in the expected format
+        Assert.assertNotNull("Version string should not be null", 
versionString);
+        Assert.assertTrue("Version string should start with 'OpenSSL' or 
library name",
+                versionString.matches("^(OpenSSL|LibreSSL|BoringSSL).*"));
+        Assert.assertTrue("Version string should contain version number",
+                versionString.matches(".*\\d+\\.\\d+.*"));
+    }
+
+    /**
+     * Test parseVersionFromFilename() with various filename patterns.
+     */
+    @Test
+    public void testParseVersionFromFilename() throws Exception {
+        // Test cases: [filename, expected version, description]
+        Object[][] testCases = {
+            {"commons-logging-1.2.jar", "1.2", "dash separator"},
+            {"library_2.3.4.jar", "2.3.4", "underscore separator"},
+            {"mylib-1.0.0-SNAPSHOT.jar", "1.0.0-SNAPSHOT", "SNAPSHOT version"},
+            {"spring-core-5.3.20.RELEASE.jar", "5.3.20.RELEASE", "RELEASE 
suffix"},
+            {"jackson-databind-2.15.0.jar", "2.15.0", "standard Maven 
version"},
+            {"library.jar", null, "no version in filename"},
+            {"library-core.jar", null, "non-numeric suffix"},
+            {"test-1.jar", "1", "minimal version"}
+        };
+
+        for (Object[] testCase : testCases) {
+            String filename = (String) testCase[0];
+            String expectedVersion = (String) testCase[1];
+            String description = (String) testCase[2];
+
+            String actualVersion = invokeParseVersionFromFilename(filename);
+            Assert.assertEquals("Failed for: " + description + " (" + filename 
+ ")",
+                    expectedVersion, actualVersion);
+        }
+    }
+
+    /**
+     * Test getJarVersion() fallback to filename parsing when manifest has no 
version.
+     */
+    @Test
+    public void testGetJarVersionFallbackToFilename() throws Exception {
+        withTestJar("test-library-3.2.1.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            // No version attributes in manifest
+        }, jar -> Assert.assertEquals("Should fallback to filename parsing", 
"3.2.1",
+                invokeGetJarVersion(jar)));
+    }
+
+    /**
+     * Test getJarVersion() prefers manifest version over filename.
+     */
+    @Test
+    public void testGetJarVersionPrefersManifest() throws Exception {
+        withTestJar("library-1.0.0.jar", manifest -> {
+            manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
+            manifest.getMainAttributes().putValue("Bundle-Version", "2.0.0");
+        }, jar -> Assert.assertEquals("Should prefer manifest version over 
filename", "2.0.0",
+                invokeGetJarVersion(jar)));
+    }
+
+    /**
+     * Functional interface for test logic that can throw exceptions.
+     */
+    @FunctionalInterface
+    private interface TestWithJar {
+        void test(File jarFile) throws Exception;
+    }
+
+    /**
+     * Helper method to run a test with a JAR file and ensure cleanup.
+     */
+    private void withTestJar(String filename, Consumer<Manifest> customizer, 
TestWithJar test) throws Exception {
+        File testJar = createTestJar(filename, customizer);
+        try {
+            test.test(testJar);
+        } finally {
+            testJar.delete();
+        }
+    }
+
+    /**
+     * Helper method to invoke the private isTomcatCoreJar() method via 
reflection.
+     */
+    private boolean invokeIsTomcatCoreJar(File jarFile) throws Exception {
+        Method method = ServerInfo.class.getDeclaredMethod("isTomcatCoreJar", 
File.class);
+        method.setAccessible(true);
+        return (Boolean) method.invoke(null, jarFile);
+    }
+
+    /**
+     * Helper method to invoke the private getJarVersion() method via 
reflection.
+     */
+    private String invokeGetJarVersion(File jarFile) throws Exception {
+        Method method = ServerInfo.class.getDeclaredMethod("getJarVersion", 
File.class);
+        method.setAccessible(true);
+        return (String) method.invoke(null, jarFile);
+    }
+
+    /**
+     * Helper method to invoke the private parseVersionFromFilename() method 
via reflection.
+     */
+    private String invokeParseVersionFromFilename(String filename) throws 
Exception {
+        Method method = 
ServerInfo.class.getDeclaredMethod("parseVersionFromFilename", String.class);
+        method.setAccessible(true);
+        return (String) method.invoke(null, filename);
+    }
+
+    /**
+     * Helper method to capture ServerInfo.main() output.
+     */
+    private String captureServerInfoOutput() throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        PrintStream ps = new PrintStream(baos);
+        PrintStream oldOut = System.out;
+        try {
+            System.setOut(ps);
+            ServerInfo.main(new String[0]);
+        } finally {
+            System.setOut(oldOut);
+        }
+        return baos.toString();
+    }
+
+    /**
+     * Helper method to create a test JAR file with custom manifest.
+     */
+    private File createTestJar(String filename, Consumer<Manifest> customizer) 
throws Exception {
+        File tempDir = new File(System.getProperty("java.io.tmpdir"));
+        File jarFile = new File(tempDir, filename);
+
+        Manifest manifest = new Manifest();
+        customizer.accept(manifest);
+
+        try (FileOutputStream fos = new FileOutputStream(jarFile);
+                JarOutputStream jos = new JarOutputStream(fos, manifest)) {
+            // Empty JAR with just manifest
+        }
+
+        return jarFile;
+    }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index d5640b8038..94808f56f9 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -192,6 +192,12 @@
         Add constants to <code>HttpServletResponse</code> for the HTTP status
         codes defined in RFC 6585. (markt)
       </scode>
+      <add>
+        Enhance <code>version.sh</code> and <code>version.bat</code> to display
+        APR, Tomcat Native, and OpenSSL version information (both APR and FFM
+        implementations), along with version compatibility warnings and
+        third-party library version information. (csutherl)
+      </add>
       <!-- Entries for backport and removal before 12.0.0-M1 below this line 
-->
       <scode>
         Refactor generation of the remote user element in the access log to


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to