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]