347 lines
16 KiB
Diff
347 lines
16 KiB
Diff
diff --git a/java/org/apache/catalina/storeconfig/LocalStrings.properties b/java/org/apache/catalina/storeconfig/LocalStrings.properties
|
|
index d3e8585df5..4c2aa53bee 100644
|
|
--- a/java/org/apache/catalina/storeconfig/LocalStrings.properties
|
|
+++ b/java/org/apache/catalina/storeconfig/LocalStrings.properties
|
|
@@ -28,8 +28,11 @@ factory.storeTag=store tag [{0}] ( Object: [{1}] )
|
|
globalNamingResourcesSF.noFactory=Cannot find NamingResources store factory
|
|
globalNamingResourcesSF.wrongElement=Wrong element [{0}]
|
|
|
|
+registry.interfacesLoaded=Loaded [{0}] interface classes for registry
|
|
registry.loadClassFailed=Failed to load class [{0}]
|
|
registry.noDescriptor=Can't find descriptor for key [{0}]
|
|
+registry.optionalClassLoaded=Loaded optional class [{0}]
|
|
+registry.optionalClassNotFound=Optional class [{0}] not found, skipping
|
|
|
|
standardContextSF.cannotWriteFile=Cannot write file at [{0}]
|
|
standardContextSF.canonicalPathError=Failed to obtain the canonical path of the configuration file [{0}]
|
|
diff --git a/java/org/apache/catalina/storeconfig/StandardEngineSF.java b/java/org/apache/catalina/storeconfig/StandardEngineSF.java
|
|
index 0f600ee5c1..b6bd5d7168 100644
|
|
--- a/java/org/apache/catalina/storeconfig/StandardEngineSF.java
|
|
+++ b/java/org/apache/catalina/storeconfig/StandardEngineSF.java
|
|
@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener;
|
|
import org.apache.catalina.Realm;
|
|
import org.apache.catalina.Valve;
|
|
import org.apache.catalina.core.StandardEngine;
|
|
-import org.apache.catalina.ha.ClusterValve;
|
|
|
|
/**
|
|
* Store server.xml Element Engine
|
|
*/
|
|
public class StandardEngineSF extends StoreFactoryBase {
|
|
|
|
+ private static final Class<?> clusterValveClass;
|
|
+ static {
|
|
+ Class<?> clazz = null;
|
|
+ try {
|
|
+ clazz = Class.forName("org.apache.catalina.ha.ClusterValve");
|
|
+ } catch (ClassNotFoundException e) {
|
|
+ // Expected when clustering JARs are not present
|
|
+ }
|
|
+ clusterValveClass = clazz;
|
|
+ }
|
|
+
|
|
/**
|
|
* Store the specified Engine properties.
|
|
* <p>
|
|
@@ -64,7 +74,7 @@ public class StandardEngineSF extends StoreFactoryBase {
|
|
if (valves != null && valves.length > 0) {
|
|
List<Valve> engineValves = new ArrayList<>();
|
|
for (Valve valve : valves) {
|
|
- if (!(valve instanceof ClusterValve)) {
|
|
+ if (clusterValveClass == null || !clusterValveClass.isInstance(valve)) {
|
|
engineValves.add(valve);
|
|
}
|
|
}
|
|
diff --git a/java/org/apache/catalina/storeconfig/StandardHostSF.java b/java/org/apache/catalina/storeconfig/StandardHostSF.java
|
|
index 60d473982b..1e8f2d3a58 100644
|
|
--- a/java/org/apache/catalina/storeconfig/StandardHostSF.java
|
|
+++ b/java/org/apache/catalina/storeconfig/StandardHostSF.java
|
|
@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener;
|
|
import org.apache.catalina.Realm;
|
|
import org.apache.catalina.Valve;
|
|
import org.apache.catalina.core.StandardHost;
|
|
-import org.apache.catalina.ha.ClusterValve;
|
|
|
|
/**
|
|
* Store server.xml Element Host
|
|
*/
|
|
public class StandardHostSF extends StoreFactoryBase {
|
|
|
|
+ private static final Class<?> clusterValveClass;
|
|
+ static {
|
|
+ Class<?> clazz = null;
|
|
+ try {
|
|
+ clazz = Class.forName("org.apache.catalina.ha.ClusterValve");
|
|
+ } catch (ClassNotFoundException e) {
|
|
+ // Expected when clustering JARs are not present
|
|
+ }
|
|
+ clusterValveClass = clazz;
|
|
+ }
|
|
+
|
|
/**
|
|
* Store the specified Host properties and children (Listener,Alias,Realm,Valve,Cluster, Context)
|
|
* <p>
|
|
@@ -68,7 +78,7 @@ public class StandardHostSF extends StoreFactoryBase {
|
|
if (valves != null && valves.length > 0) {
|
|
List<Valve> hostValves = new ArrayList<>();
|
|
for (Valve valve : valves) {
|
|
- if (!(valve instanceof ClusterValve)) {
|
|
+ if (clusterValveClass == null || !clusterValveClass.isInstance(valve)) {
|
|
hostValves.add(valve);
|
|
}
|
|
}
|
|
diff --git a/java/org/apache/catalina/storeconfig/StoreRegistry.java b/java/org/apache/catalina/storeconfig/StoreRegistry.java
|
|
index c17dd3817e..ee803abe36 100644
|
|
--- a/java/org/apache/catalina/storeconfig/StoreRegistry.java
|
|
+++ b/java/org/apache/catalina/storeconfig/StoreRegistry.java
|
|
@@ -16,7 +16,9 @@
|
|
*/
|
|
package org.apache.catalina.storeconfig;
|
|
|
|
+import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
+import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import javax.naming.directory.DirContext;
|
|
@@ -28,17 +30,6 @@ import org.apache.catalina.Realm;
|
|
import org.apache.catalina.Valve;
|
|
import org.apache.catalina.WebResourceRoot;
|
|
import org.apache.catalina.WebResourceSet;
|
|
-import org.apache.catalina.ha.CatalinaCluster;
|
|
-import org.apache.catalina.ha.ClusterDeployer;
|
|
-import org.apache.catalina.ha.ClusterListener;
|
|
-import org.apache.catalina.tribes.Channel;
|
|
-import org.apache.catalina.tribes.ChannelInterceptor;
|
|
-import org.apache.catalina.tribes.ChannelReceiver;
|
|
-import org.apache.catalina.tribes.ChannelSender;
|
|
-import org.apache.catalina.tribes.Member;
|
|
-import org.apache.catalina.tribes.MembershipService;
|
|
-import org.apache.catalina.tribes.MessageListener;
|
|
-import org.apache.catalina.tribes.transport.DataSender;
|
|
import org.apache.coyote.UpgradeProtocol;
|
|
import org.apache.juli.logging.Log;
|
|
import org.apache.juli.logging.LogFactory;
|
|
@@ -61,11 +52,74 @@ public class StoreRegistry {
|
|
private String version;
|
|
|
|
// Access Information
|
|
- private static final Class<?>[] interfaces = { CatalinaCluster.class, ChannelSender.class, ChannelReceiver.class,
|
|
- Channel.class, MembershipService.class, ClusterDeployer.class, Realm.class, Manager.class, DirContext.class,
|
|
- LifecycleListener.class, Valve.class, ClusterListener.class, MessageListener.class, DataSender.class,
|
|
- ChannelInterceptor.class, Member.class, WebResourceRoot.class, WebResourceSet.class,
|
|
- CredentialHandler.class, UpgradeProtocol.class, CookieProcessor.class };
|
|
+ // Lazily initialized to gracefully handle optional features like clustering
|
|
+ private static volatile Class<?>[] interfaces = null;
|
|
+
|
|
+ /**
|
|
+ * Initialize the interfaces array with all available classes.
|
|
+ * Uses dynamic loading for optional classes (e.g., clustering) to avoid
|
|
+ * ClassNotFoundException when those JARs are not present. This approach
|
|
+ * is consistent with how Catalina.addClusterRuleSet() handles clustering.
|
|
+ */
|
|
+ private static Class<?>[] getInterfaces() {
|
|
+ if (interfaces == null) {
|
|
+ synchronized (StoreRegistry.class) {
|
|
+ if (interfaces == null) {
|
|
+ // Required interfaces - always present
|
|
+ List<Class<?>> list = new ArrayList<>();
|
|
+ list.add(Realm.class);
|
|
+ list.add(Manager.class);
|
|
+ list.add(DirContext.class);
|
|
+ list.add(LifecycleListener.class);
|
|
+ list.add(Valve.class);
|
|
+ list.add(WebResourceRoot.class);
|
|
+ list.add(WebResourceSet.class);
|
|
+ list.add(CredentialHandler.class);
|
|
+ list.add(UpgradeProtocol.class);
|
|
+ list.add(CookieProcessor.class);
|
|
+
|
|
+ // Optional clustering interfaces - load dynamically to support
|
|
+ // deployments where clustering JARs may not be present
|
|
+ tryAddClass(list, "org.apache.catalina.ha.CatalinaCluster");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.ChannelSender");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.ChannelReceiver");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.Channel");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.MembershipService");
|
|
+ tryAddClass(list, "org.apache.catalina.ha.ClusterDeployer");
|
|
+ tryAddClass(list, "org.apache.catalina.ha.ClusterListener");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.MessageListener");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.transport.DataSender");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.ChannelInterceptor");
|
|
+ tryAddClass(list, "org.apache.catalina.tribes.Member");
|
|
+
|
|
+ interfaces = list.toArray(new Class<?>[0]);
|
|
+
|
|
+ if (log.isDebugEnabled()) {
|
|
+ log.debug(sm.getString("registry.interfacesLoaded", Integer.valueOf(interfaces.length)));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return interfaces;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Try to load a class by name and add it to the list if successful.
|
|
+ * Logs at TRACE level if the class is not available.
|
|
+ */
|
|
+ private static void tryAddClass(List<Class<?>> list, String className) {
|
|
+ try {
|
|
+ Class<?> clazz = Class.forName(className, false, StoreRegistry.class.getClassLoader());
|
|
+ list.add(clazz);
|
|
+ if (log.isTraceEnabled()) {
|
|
+ log.trace(sm.getString("registry.optionalClassLoaded", className));
|
|
+ }
|
|
+ } catch (ClassNotFoundException | NoClassDefFoundError e) {
|
|
+ if (log.isTraceEnabled()) {
|
|
+ log.trace(sm.getString("registry.optionalClassNotFound", className));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
|
|
/**
|
|
* @return the name
|
|
@@ -116,9 +170,10 @@ public class StoreRegistry {
|
|
}
|
|
if (aClass != null) {
|
|
desc = descriptors.get(aClass.getName());
|
|
- for (int i = 0; desc == null && i < interfaces.length; i++) {
|
|
- if (interfaces[i].isAssignableFrom(aClass)) {
|
|
- desc = descriptors.get(interfaces[i].getName());
|
|
+ Class<?>[] availableInterfaces = getInterfaces();
|
|
+ for (int i = 0; desc == null && i < availableInterfaces.length; i++) {
|
|
+ if (availableInterfaces[i].isAssignableFrom(aClass)) {
|
|
+ desc = descriptors.get(availableInterfaces[i].getName());
|
|
}
|
|
}
|
|
}
|
|
diff --git a/test/org/apache/catalina/storeconfig/TestStoreRegistry.java b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java
|
|
new file mode 100644
|
|
index 0000000000..e6869d3642
|
|
--- /dev/null
|
|
+++ b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java
|
|
@@ -0,0 +1,104 @@
|
|
+/*
|
|
+ * 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.catalina.storeconfig;
|
|
+
|
|
+import java.lang.reflect.Method;
|
|
+
|
|
+import org.junit.Assert;
|
|
+import org.junit.Test;
|
|
+
|
|
+import org.apache.catalina.Manager;
|
|
+import org.apache.catalina.Realm;
|
|
+import org.apache.catalina.Valve;
|
|
+
|
|
+/**
|
|
+ * Test StoreRegistry behavior, particularly dynamic loading of optional classes like clustering.
|
|
+ *
|
|
+ * Verifies StoreRegistry uses the same dynamic loading pattern.
|
|
+ */
|
|
+public class TestStoreRegistry {
|
|
+
|
|
+ /**
|
|
+ * Test that clustering classes are dynamically loaded like other Tomcat components.
|
|
+ *
|
|
+ * StoreRegistry should initialize successfully whether clustering is available or not.
|
|
+ * This matches the pattern used in Catalina.addClusterRuleSet().
|
|
+ */
|
|
+ @Test
|
|
+ public void testClusteringClassesOptional() throws Exception {
|
|
+ // Verify StoreRegistry initializes successfully with dynamic class loading
|
|
+ StoreRegistry registry = new StoreRegistry();
|
|
+ Assert.assertNotNull("Registry should initialize with dynamic loading", registry);
|
|
+
|
|
+ // Trigger lazy loading of interfaces array
|
|
+ Method getInterfacesMethod = StoreRegistry.class.getDeclaredMethod("getInterfaces");
|
|
+ getInterfacesMethod.setAccessible(true);
|
|
+
|
|
+ Class<?>[] interfaces = (Class<?>[]) getInterfacesMethod.invoke(null);
|
|
+ Assert.assertNotNull("Interfaces should load dynamically", interfaces);
|
|
+
|
|
+ // Test passes if we get here without ClassNotFoundException.
|
|
+ // The actual number of interfaces loaded depends on whether clustering is available,
|
|
+ // but we should always have at least the core 10 interfaces.
|
|
+ Assert.assertTrue("Should have at least 10 core interfaces",
|
|
+ interfaces.length >= 10);
|
|
+
|
|
+ // Verify required core interfaces are always present
|
|
+ boolean hasRealm = false;
|
|
+ boolean hasManager = false;
|
|
+ boolean hasValve = false;
|
|
+
|
|
+ for (Class<?> iface : interfaces) {
|
|
+ if (iface.equals(Realm.class)) {
|
|
+ hasRealm = true;
|
|
+ }
|
|
+ if (iface.equals(Manager.class)) {
|
|
+ hasManager = true;
|
|
+ }
|
|
+ if (iface.equals(Valve.class)) {
|
|
+ hasValve = true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Assert.assertTrue("Should contain Realm interface", hasRealm);
|
|
+ Assert.assertTrue("Should contain Manager interface", hasManager);
|
|
+ Assert.assertTrue("Should contain Valve interface", hasValve);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Test that findDescription works with interface inheritance and
|
|
+ * dynamically loaded interfaces.
|
|
+ */
|
|
+ @Test
|
|
+ public void testFindDescriptionWithDynamicInterfaces() throws Exception {
|
|
+ StoreRegistry registry = new StoreRegistry();
|
|
+
|
|
+ // Register a description for the Valve interface
|
|
+ StoreDescription valveDesc = new StoreDescription();
|
|
+ valveDesc.setId(Valve.class.getName());
|
|
+ valveDesc.setTag("Valve");
|
|
+ valveDesc.setTagClass(Valve.class.getName());
|
|
+ registry.registerDescription(valveDesc);
|
|
+
|
|
+ // AccessLogValve implements Valve interface - should find via dynamic interface matching
|
|
+ String accessLogValveClass = "org.apache.catalina.valves.AccessLogValve";
|
|
+ StoreDescription foundDesc = registry.findDescription(accessLogValveClass);
|
|
+
|
|
+ Assert.assertNotNull("Should find description via interface matching", foundDesc);
|
|
+ Assert.assertEquals("Should match Valve descriptor", "Valve", foundDesc.getTag());
|
|
+ }
|
|
+}
|
|
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
|
|
index 808165f044..792dad2fa4 100644
|
|
--- a/webapps/docs/changelog.xml
|
|
+++ b/webapps/docs/changelog.xml
|
|
@@ -113,6 +113,13 @@
|
|
in RFC 9110 that hash functions used to generate strong ETags should be
|
|
collision resistant. (markt)
|
|
</update>
|
|
+ <fix>
|
|
+ Update <code>StoreRegistry</code> to dynamically load optional clustering
|
|
+ classes rather than statically referencing them. This matches the pattern
|
|
+ used in <code>Catalina.addClusterRuleSet()</code> and prevents
|
|
+ <code>NoClassDefFoundError</code> when <code>StoreConfigLifecycleListener</code>
|
|
+ is configured but clustering classes are not available. (csutherl)
|
|
+ </fix>
|
|
<fix>
|
|
Correct a regression in the fix for <bug>69781</bug> that broke
|
|
<code>FileStore</code>. (markt)
|