org.apache.commons
commons-lang3
diff --git a/src/main/java/org/apache/commons/logging/jakarta/ServletContextCleaner.java b/src/main/java/org/apache/commons/logging/jakarta/ServletContextCleaner.java
new file mode 100644
index 000000000..14d02d94b
--- /dev/null
+++ b/src/main/java/org/apache/commons/logging/jakarta/ServletContextCleaner.java
@@ -0,0 +1,147 @@
+/*
+ * 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
+ *
+ * https://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.commons.logging.jakarta;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * This class is capable of receiving notifications about the undeployment of
+ * a webapp, and responds by ensuring that commons-logging releases all
+ * memory associated with the undeployed webapp.
+ *
+ * In general, the WeakHashtable support added in commons-logging release 1.1
+ * ensures that logging classes do not hold references that prevent an
+ * undeployed webapp's memory from being garbage-collected even when multiple
+ * copies of commons-logging are deployed via multiple class loaders (a
+ * situation that earlier versions had problems with). However there are
+ * some rare cases where the WeakHashtable approach does not work; in these
+ * situations specifying this class as a listener for the web application will
+ * ensure that all references held by commons-logging are fully released.
+ *
+ *
+ * To use this class, configure the webapp deployment descriptor to call
+ * this class on webapp undeploy; the contextDestroyed method will tell
+ * every accessible LogFactory class that the entry in its map for the
+ * current webapp's context class loader should be cleared.
+ *
+ *
+ * @since 1.4.0
+ */
+public class ServletContextCleaner implements ServletContextListener {
+
+ private static final Class>[] RELEASE_SIGNATURE = { ClassLoader.class };
+
+ /**
+ * Constructs a new instance.
+ */
+ public ServletContextCleaner() {
+ // empty
+ }
+
+ /**
+ * Invoked when a webapp is undeployed, this tells the LogFactory
+ * class to release any logging information related to the current
+ * contextClassloader.
+ */
+ @Override
+ public void contextDestroyed(final ServletContextEvent sce) {
+ final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+
+ final Object[] params = new Object[1];
+ params[0] = tccl;
+
+ // Walk up the tree of class loaders, finding all the available
+ // LogFactory classes and releasing any objects associated with
+ // the tccl (ie the webapp).
+ //
+ // When there is only one LogFactory in the classpath, and it
+ // is within the webapp being undeployed then there is no problem;
+ // garbage collection works fine.
+ //
+ // When there are multiple LogFactory classes in the classpath but
+ // parent-first classloading is used everywhere, this loop is really
+ // short. The first instance of LogFactory found will
+ // be the highest in the classpath, and then no more will be found.
+ // This is ok, as with this setup this will be the only LogFactory
+ // holding any data associated with the tccl being released.
+ //
+ // When there are multiple LogFactory classes in the classpath and
+ // child-first classloading is used in any class loader, then multiple
+ // LogFactory instances may hold info about this TCCL; whenever the
+ // webapp makes a call into a class loaded via an ancestor class loader
+ // and that class calls LogFactory the tccl gets registered in
+ // the LogFactory instance that is visible from the ancestor
+ // class loader. However the concrete logging library it points
+ // to is expected to have been loaded via the TCCL, so the
+ // underlying logging lib is only initialized/configured once.
+ // These references from ancestor LogFactory classes down to
+ // TCCL class loaders are held via weak references and so should
+ // be released but there are circumstances where they may not.
+ // Walking up the class loader ancestry ladder releasing
+ // the current tccl at each level tree, though, will definitely
+ // clear any problem references.
+ ClassLoader loader = tccl;
+ while (loader != null) {
+ // Load via the current loader. Note that if the class is not accessible
+ // via this loader, but is accessible via some ancestor then that class
+ // will be returned.
+ try {
+ @SuppressWarnings("unchecked")
+ final Class logFactoryClass = (Class) loader.loadClass("org.apache.commons.logging.LogFactory");
+ final Method releaseMethod = logFactoryClass.getMethod("release", RELEASE_SIGNATURE);
+ releaseMethod.invoke(null, params);
+ loader = logFactoryClass.getClassLoader().getParent();
+ } catch (final ClassNotFoundException ex) {
+ // Neither the current class loader nor any of its ancestors could find
+ // the LogFactory class, so we can stop now.
+ loader = null;
+ } catch (final NoSuchMethodException ex) {
+ // This is not expected; every version of JCL has this method
+ System.err.println("LogFactory instance found which does not support release method!");
+ loader = null;
+ } catch (final IllegalAccessException ex) {
+ // This is not expected; every ancestor class should be accessible
+ System.err.println("LogFactory instance found which is not accessible!");
+ loader = null;
+ } catch (final InvocationTargetException ex) {
+ // This is not expected
+ System.err.println("LogFactory instance release method failed!");
+ loader = null;
+ }
+ }
+
+ // Just to be sure, invoke release on the LogFactory that is visible from
+ // this ServletContextCleaner class too. This should already have been caught
+ // by the above loop but just in case...
+ LogFactory.release(tccl);
+ }
+
+ /**
+ * Invoked when a webapp is deployed. Nothing needs to be done here.
+ */
+ @Override
+ public void contextInitialized(final ServletContextEvent sce) {
+ // do nothing
+ }
+}
diff --git a/src/test/java/org/apache/commons/logging/servlet/BasicJakartaServletTestCase.java b/src/test/java/org/apache/commons/logging/servlet/BasicJakartaServletTestCase.java
new file mode 100644
index 000000000..e29456966
--- /dev/null
+++ b/src/test/java/org/apache/commons/logging/servlet/BasicJakartaServletTestCase.java
@@ -0,0 +1,70 @@
+/*
+ * 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
+ *
+ * https://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.commons.logging.servlet;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+
+import org.apache.commons.logging.PathableClassLoader;
+import org.apache.commons.logging.PathableTestSuite;
+import org.apache.commons.logging.jakarta.ServletContextCleaner;
+
+/**
+ * Tests for ServletContextCleaner utility class.
+ */
+public class BasicJakartaServletTestCase extends TestCase {
+
+ /**
+ * Return the tests included in this test suite.
+ */
+ public static Test suite() throws Exception {
+ // LogFactory in parent
+ // LogFactory in child (loads test)
+ // LogFactory in tccl
+ //
+ // Having the test loaded via a loader above the tccl emulates the situation
+ // where a web.xml file specifies ServletContextCleaner as a listener, and
+ // that class is deployed via a shared class loader.
+
+ final PathableClassLoader parent = new PathableClassLoader(null);
+ parent.useExplicitLoader("junit.", Test.class.getClassLoader());
+ parent.addLogicalLib("commons-logging");
+ parent.addLogicalLib("jakarta-servlet-api");
+
+ final PathableClassLoader child = new PathableClassLoader(parent);
+ child.setParentFirst(false);
+ child.addLogicalLib("commons-logging");
+ child.addLogicalLib("testclasses");
+
+ final PathableClassLoader tccl = new PathableClassLoader(child);
+ tccl.setParentFirst(false);
+ tccl.addLogicalLib("commons-logging");
+
+ final Class> testClass = child.loadClass(BasicJakartaServletTestCase.class.getName());
+ return new PathableTestSuite(testClass, tccl);
+ }
+
+ /**
+ * Test that calling ServletContextCleaner.contextDestroyed doesn't crash.
+ * Testing anything else is rather difficult...
+ */
+ public void testBasics() {
+ final ServletContextCleaner scc = new ServletContextCleaner();
+ scc.contextDestroyed(null);
+ }
+}