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

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-logging.git


The following commit(s) were added to refs/heads/master by this push:
     new 3ed2daf  [LOGGING-192] Fix factory loading from TCCL (#281)
3ed2daf is described below

commit 3ed2daf47f17506651216ebc95638fcc3cabbbb3
Author: Piotr P. Karwasz <piotr.git...@karwasz.org>
AuthorDate: Thu Aug 15 00:56:20 2024 +0200

    [LOGGING-192] Fix factory loading from TCCL (#281)
    
    This fixes the loading of standard `LogFactory` implementations from the 
TCCL.
    
    The current code has a fallback clause in `newFactory`, so it can happen 
that the code tests for the presence of Log4j API and SLF4J in the TCCL, but it 
actually loads the factory from the current classloader (which lacks either of 
those libraries).
---
 pom.xml                                            |   1 +
 src/changes/changes.xml                            |   3 +-
 .../org/apache/commons/logging/LogFactory.java     | 115 ++++++++++++++-------
 .../security/SecurityForbiddenTestCase.java        |   6 +-
 .../tccl/logfactory/AdaptersTcclTestCase.java      |  76 ++++++++++++++
 .../tccl/logfactory/SiblingTcclTestCase.java       |  72 +++++++++++++
 6 files changed, 231 insertions(+), 42 deletions(-)

diff --git a/pom.xml b/pom.xml
index 1506572..a1d4457 100644
--- a/pom.xml
+++ b/pom.xml
@@ -283,6 +283,7 @@ under the License.
                   
<org.apache.commons.logging.diagnostics.dest>STDOUT</org.apache.commons.logging.diagnostics.dest>
                 -->
                 
<log4j12>${org.apache.logging.log4j:log4j-1.2-api:jar}</log4j12>
+                
<log4j-api>${org.apache.logging.log4j:log4j-api:jar}</log4j-api>
                 <logkit>${logkit:logkit:jar}</logkit>
                 
<servlet-api>${javax.servlet:javax.servlet-api:jar}</servlet-api>
                 
<commons-logging>target/${project.build.finalName}.jar</commons-logging>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c240e9c..83ef9ac 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -45,10 +45,11 @@ The <action> type attribute can be add,update,fix,remove.
   <body>
     <release version="1.3.4" date="YYYY-MM-DD" description="This is a feature 
and maintenance release. Java 8 or later is required.">
       <!-- FIX -->
+      <action dev="pkarwasz" issue="LOGGING-192" type="fix" due-to="Björn 
Kautler, Piotr Karwasz">Fix factory loading from context class loader.</action>
       <!-- ADD -->
       <!-- UPDATE -->
       <action dev="ggregory" type="update" due-to="Gary Gregory">Bump 
org.apache.commons:commons-parent from 71 to 72.</action>
-      <action dev="ggregory" type="update" due-to="Gary Gregory, 
Dependabot">Bump org.slf4j:slf4j-api from 2.0.13 to 2.0.15 #276.</action> 
+      <action dev="ggregory" type="update" due-to="Gary Gregory, 
Dependabot">Bump org.slf4j:slf4j-api from 2.0.13 to 2.0.15 #276.</action>
     </release>
     <release version="1.3.3" date="2024-06-30" description="This is a feature 
and maintenance release. Java 8 or later is required.">
       <!-- FIX -->
diff --git a/src/main/java/org/apache/commons/logging/LogFactory.java 
b/src/main/java/org/apache/commons/logging/LogFactory.java
index bcf0983..238ca9e 100644
--- a/src/main/java/org/apache/commons/logging/LogFactory.java
+++ b/src/main/java/org/apache/commons/logging/LogFactory.java
@@ -331,8 +331,8 @@ public abstract class LogFactory {
                                 + " does not extend '" + 
LogFactory.class.getName() + "' as loaded by this class loader.");
                         logHierarchy("[BAD CL TREE] ", classLoader);
                     }
-
-                    return logFactoryClass.getConstructor().newInstance();
+                    // Force a ClassCastException
+                    return 
LogFactory.class.cast(logFactoryClass.getConstructor().newInstance());
 
                 } catch (final ClassNotFoundException ex) {
                     if (classLoader == thisClassLoaderRef.get()) {
@@ -425,7 +425,8 @@ public abstract class LogFactory {
                         "Unable to load factory class via class loader " + 
objectId(classLoader) + " - trying the class loader associated with this 
LogFactory.");
             }
             logFactoryClass = Class.forName(factoryClassName);
-            return logFactoryClass.newInstance();
+            // Force a ClassCastException
+            return 
LogFactory.class.cast(logFactoryClass.getConstructor().newInstance());
         } catch (final Exception e) {
             // Check to see if we've got a bad configuration
             if (isDiagnosticsEnabled()) {
@@ -796,22 +797,31 @@ public abstract class LogFactory {
 
         // Determine whether we will be using the thread context class loader 
to
         // load logging classes or not by checking the loaded properties file 
(if any).
-        ClassLoader baseClassLoader = contextClassLoader;
+        boolean useTccl = contextClassLoader != null;
         if (props != null) {
             final String useTCCLStr = props.getProperty(TCCL_KEY);
-            // The Boolean.valueOf(useTCCLStr).booleanValue() formulation
-            // is required for Java 1.2 compatibility.
-            if (useTCCLStr != null && !Boolean.parseBoolean(useTCCLStr)) {
-                // Don't use current context class loader when locating any
-                // LogFactory or Log classes, just use the class that loaded
-                // this abstract class. When this class is deployed in a shared
-                // classpath of a container, it means webapps cannot deploy 
their
-                // own logging implementations. It also means that it is up to 
the
-                // implementation whether to load library-specific config files
-                // from the TCCL or not.
-                baseClassLoader = thisClassLoaderRef.get();
+            useTccl &= useTCCLStr == null || Boolean.parseBoolean(useTCCLStr);
+        }
+        // If TCCL is still enabled at this point, we check if it resolves 
this class
+        if (useTccl) {
+            try {
+                if (!LogFactory.class.equals(
+                        Class.forName(LogFactory.class.getName(), false, 
contextClassLoader))) {
+                    logDiagnostic("The class " + LogFactory.class.getName() + 
" loaded by the context class loader " + objectId(contextClassLoader)
+                            + " and this class differ. Disabling the usage of 
the context class loader."
+                            + "Background can be found in 
https://commons.apache.org/logging/tech.html. ");
+                    logHierarchy("[BAD CL TREE] ", contextClassLoader);
+                    useTccl = false;
+                }
+            } catch (ClassNotFoundException ignored) {
+                logDiagnostic("The class " + LogFactory.class.getName() + " is 
not present in the the context class loader " + objectId(contextClassLoader)
+                        + ". Disabling the usage of the context class loader."
+                        + "Background can be found in 
https://commons.apache.org/logging/tech.html. ");
+                logHierarchy("[BAD CL TREE] ", contextClassLoader);
+                useTccl = false;
             }
         }
+        final ClassLoader baseClassLoader = useTccl ? contextClassLoader : 
thisClassLoaderRef.get();
 
         // Determine which concrete LogFactory subclass to use.
         // First, try a global system property
@@ -864,7 +874,7 @@ public abstract class LogFactory {
                 logDiagnostic("[LOOKUP] Using ServiceLoader  to define the 
LogFactory subclass to use...");
             }
             try {
-                final ServiceLoader<LogFactory> serviceLoader = 
ServiceLoader.load(LogFactory.class);
+                final ServiceLoader<LogFactory> serviceLoader = 
ServiceLoader.load(LogFactory.class, baseClassLoader);
                 final Iterator<LogFactory> iterator = serviceLoader.iterator();
 
                 int i = MAX_BROKEN_SERVICES;
@@ -923,31 +933,19 @@ public abstract class LogFactory {
             }
         }
 
-        // Fourth, try one of the 3 provided factories
-
-        try {
-            // We prefer Log4j API, since it does not stringify objects.
-            if (factory == null && isClassAvailable(LOG4J_API_LOGGER, 
baseClassLoader)) {
-                // If the Log4j API is redirected to SLF4J, we use SLF4J 
directly.
-                if (isClassAvailable(LOG4J_TO_SLF4J_BRIDGE, baseClassLoader)) {
-                    logDiagnostic(
-                            "[LOOKUP] Log4j API to SLF4J redirection detected. 
Loading the SLF4J LogFactory implementation '" + FACTORY_SLF4J + "'.");
-                    factory = newFactory(FACTORY_SLF4J, baseClassLoader, 
contextClassLoader);
-                } else {
-                    logDiagnostic("[LOOKUP] Log4j API detected. Loading the 
Log4j API LogFactory implementation '" + FACTORY_LOG4J_API + "'.");
-                    factory = newFactory(FACTORY_LOG4J_API, baseClassLoader, 
contextClassLoader);
-                }
-            }
-
-            if (factory == null && isClassAvailable(SLF4J_API_LOGGER, 
baseClassLoader)) {
-                logDiagnostic("[LOOKUP] SLF4J detected. Loading the SLF4J 
LogFactory implementation '" + FACTORY_SLF4J + "'.");
-                factory = newFactory(FACTORY_SLF4J, baseClassLoader, 
contextClassLoader);
-            }
-        } catch (final Exception e) {
-            logDiagnostic("[LOOKUP] An exception occurred while creating 
LogFactory: " + e.getMessage());
+        // Fourth, try one of the three provided factories first from the 
specified classloader
+        // and then from the current one.
+        if (factory == null) {
+            factory = newStandardFactory(baseClassLoader);
         }
-
         if (factory == null) {
+            factory = newStandardFactory(thisClassLoaderRef.get());
+        }
+        if (factory != null) {
+            if (isDiagnosticsEnabled()) {
+                logDiagnostic("Created object " + objectId(factory) + " to 
manage class loader " + objectId(contextClassLoader));
+            }
+        } else {
             if (isDiagnosticsEnabled()) {
                 logDiagnostic(
                     "[LOOKUP] Loading the default LogFactory implementation '" 
+ FACTORY_DEFAULT +
@@ -1408,6 +1406,45 @@ public abstract class LogFactory {
         return newFactory(factoryClass, classLoader, null);
     }
 
+    /**
+     * Tries to load one of the standard three implementations from the given 
classloader.
+     * <p>
+     *     We assume that {@code classLoader} can load this class.
+     * </p>
+     * @param classLoader The classloader to use.
+     * @return An implementation of this class.
+     */
+    private static LogFactory newStandardFactory(final ClassLoader 
classLoader) {
+        if (isClassAvailable(LOG4J_TO_SLF4J_BRIDGE, classLoader)) {
+            try {
+                return (LogFactory) Class.forName(FACTORY_SLF4J, true, 
classLoader).getConstructor().newInstance();
+            } catch (final LinkageError | ReflectiveOperationException 
ignored) {
+            } finally {
+                logDiagnostic(
+                        "[LOOKUP] Log4j API to SLF4J redirection detected. 
Loading the SLF4J LogFactory implementation '" + FACTORY_SLF4J + "'.");
+            }
+        }
+        try {
+            return (LogFactory) Class.forName(FACTORY_LOG4J_API, true, 
classLoader).getConstructor().newInstance();
+        } catch (final LinkageError | ReflectiveOperationException ignored) {
+        } finally {
+            logDiagnostic("[LOOKUP] Loading the Log4j API LogFactory 
implementation '" + FACTORY_LOG4J_API + "'.");
+        }
+        try {
+            return (LogFactory) Class.forName(FACTORY_SLF4J, true, 
classLoader).getConstructor().newInstance();
+        } catch (final LinkageError | ReflectiveOperationException ignored) {
+        } finally {
+            logDiagnostic("[LOOKUP] Loading the SLF4J LogFactory 
implementation '" + FACTORY_SLF4J + "'.");
+        }
+        try {
+            return (LogFactory) Class.forName(FACTORY_DEFAULT, true, 
classLoader).getConstructor().newInstance();
+        } catch (final LinkageError | ReflectiveOperationException ignored) {
+        } finally {
+            logDiagnostic("[LOOKUP] Loading the legacy LogFactory 
implementation '" + FACTORY_DEFAULT + "'.");
+        }
+        return null;
+    }
+
     /**
      * Gets a new instance of the specified {@code LogFactory} implementation 
class, loaded by the specified class loader. If that fails, try the class loader
      * used to load this (abstract) LogFactory.
diff --git 
a/src/test/java/org/apache/commons/logging/security/SecurityForbiddenTestCase.java
 
b/src/test/java/org/apache/commons/logging/security/SecurityForbiddenTestCase.java
index 5e87db4..aa4005c 100644
--- 
a/src/test/java/org/apache/commons/logging/security/SecurityForbiddenTestCase.java
+++ 
b/src/test/java/org/apache/commons/logging/security/SecurityForbiddenTestCase.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotEquals;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Hashtable;
 
@@ -93,10 +94,11 @@ public class SecurityForbiddenTestCase extends TestCase {
             final Class<?> clazz = classLoader.loadClass(name);
             return clazz.getConstructor().newInstance();
         } catch (final Exception e) {
+            final Throwable wrapped = e instanceof InvocationTargetException ? 
((InvocationTargetException) e).getTargetException() : e;
             final StringWriter sw = new StringWriter();
             final PrintWriter pw = new PrintWriter(sw);
-            e.printStackTrace(pw);
-            fail("Unexpected exception:" + e.getMessage() + ":" + 
sw.toString());
+            wrapped.printStackTrace(pw);
+            fail("Unexpected exception:" + wrapped.getMessage() + ":" + sw);
         }
         return null;
     }
diff --git 
a/src/test/java/org/apache/commons/logging/tccl/logfactory/AdaptersTcclTestCase.java
 
b/src/test/java/org/apache/commons/logging/tccl/logfactory/AdaptersTcclTestCase.java
new file mode 100644
index 0000000..2d97056
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/logging/tccl/logfactory/AdaptersTcclTestCase.java
@@ -0,0 +1,76 @@
+/*
+ * 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.commons.logging.tccl.logfactory;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.logging.PathableClassLoader;
+import org.apache.commons.logging.PathableTestSuite;
+import org.apache.commons.logging.impl.Log4jApiLogFactory;
+
+/**
+ * Verifies that if the TCCL contains only `commons-logging-adapters`, it can 
load a web-application specific
+ * logging backend.
+ */
+public class AdaptersTcclTestCase extends TestCase {
+
+    /**
+     * Return the tests included in this test suite.
+     */
+    public static Test suite() throws Exception {
+        // The classloader running the test has access to `commons-logging`
+        final PathableClassLoader classLoader = new PathableClassLoader(null);
+        classLoader.useExplicitLoader("junit.", Test.class.getClassLoader());
+        classLoader.addLogicalLib("commons-logging");
+        classLoader.addLogicalLib("testclasses");
+
+        // The TCCL has access to both `commons-logging-adapters` and 
`log4j-api`
+        final PathableClassLoader tcclLoader = new 
PathableClassLoader(classLoader);
+        tcclLoader.addLogicalLib("commons-logging-adapters");
+        tcclLoader.addLogicalLib("log4j-api");
+        tcclLoader.setParentFirst(false);
+
+        final Class<?> testClass = 
classLoader.loadClass(AdaptersTcclTestCase.class.getName());
+        return new PathableTestSuite(testClass, tcclLoader);
+    }
+
+    /**
+     * Sets up instance variables required by this test case.
+     */
+    @Override
+    public void setUp() throws Exception {
+        LogFactory.releaseAll();
+    }
+
+    /**
+     * Tear down instance variables required by this test case.
+     */
+    @Override
+    public void tearDown() {
+        LogFactory.releaseAll();
+    }
+
+    public void testFactoryLoading() {
+        final LogFactory factory = LogFactory.getFactory();
+        // The implementation comes from the TCCL
+        assertEquals(Thread.currentThread().getContextClassLoader(), 
factory.getClass().getClassLoader());
+        // It uses the `log4j-api` only available in the TCCL
+        assertEquals(Log4jApiLogFactory.class.getName(), 
factory.getClass().getName());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/logging/tccl/logfactory/SiblingTcclTestCase.java
 
b/src/test/java/org/apache/commons/logging/tccl/logfactory/SiblingTcclTestCase.java
new file mode 100644
index 0000000..54e3279
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/logging/tccl/logfactory/SiblingTcclTestCase.java
@@ -0,0 +1,72 @@
+/*
+ * 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.commons.logging.tccl.logfactory;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.logging.PathableClassLoader;
+import org.apache.commons.logging.PathableTestSuite;
+
+/**
+ * Verifies that if the TCCL is a sibling of the classloader with 
`commons-logging`
+ */
+public class SiblingTcclTestCase extends TestCase {
+
+    /**
+     * Return the tests included in this test suite.
+     */
+    public static Test suite() throws Exception {
+        // The classloader running the test has access to `commons-logging`
+        final PathableClassLoader classLoader = new PathableClassLoader(null);
+        classLoader.useExplicitLoader("junit.", Test.class.getClassLoader());
+        classLoader.addLogicalLib("commons-logging");
+        classLoader.addLogicalLib("testclasses");
+
+        // The TCCL has only access to `log4j-api` and `slf4j-api`
+        // See https://issues.apache.org/jira/browse/LOGGING-192
+        final PathableClassLoader tcclLoader = new PathableClassLoader(null);
+        tcclLoader.addLogicalLib("log4j-api");
+
+        final Class<?> testClass = 
classLoader.loadClass(SiblingTcclTestCase.class.getName());
+        return new PathableTestSuite(testClass, tcclLoader);
+    }
+
+    /**
+     * Sets up instance variables required by this test case.
+     */
+    @Override
+    public void setUp() throws Exception {
+        LogFactory.releaseAll();
+    }
+
+    /**
+     * Tear down instance variables required by this test case.
+     */
+    @Override
+    public void tearDown() {
+        LogFactory.releaseAll();
+    }
+
+    public void testFactoryLoading() {
+        // Loading the factory does not fail as in LOGGING-192
+        final LogFactory factory = LogFactory.getFactory();
+        // The selected implementation comes from this classloader
+        assertEquals(getClass().getClassLoader(), 
factory.getClass().getClassLoader());
+    }
+}

Reply via email to