diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java index bc892767746..c279fa2dd93 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java @@ -300,10 +300,16 @@ public LoggerContext getContext( final boolean currentContext, final List configLocations, final String name) { - final LoggerContext ctx = - selector.getContext(fqcn, loader, currentContext, null /*this probably needs to change*/); - if (externalContext != null && ctx.getExternalContext() == null) { - ctx.setExternalContext(externalContext); + final LoggerContext ctx; + if (externalContext instanceof Map.Entry) { + @SuppressWarnings("unchecked") + final Map.Entry entry = (Map.Entry) externalContext; + ctx = selector.getContext(fqcn, loader, entry, currentContext, null); + } else { + ctx = selector.getContext(fqcn, loader, currentContext, null); + if (externalContext != null && ctx.getExternalContext() == null) { + ctx.setExternalContext(externalContext); + } } if (name != null) { ctx.setName(name); diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java index 92874baa5ed..d14d6fb95a3 100644 --- a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java +++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java @@ -16,8 +16,26 @@ */ package org.apache.logging.log4j.web; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import jakarta.servlet.ServletContext; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.impl.ContextAnchor; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + public class WebLookupTest { + @AfterEach + void tearDown() { + ContextAnchor.THREAD_CONTEXT.remove(); + } + // TODO: re-enable when https://github.com/spring-projects/spring-framework/issues/25354 is fixed // @Test @@ -94,5 +112,63 @@ public class WebLookupTest { // initializer.stop(); // ContextAnchor.THREAD_CONTEXT.remove(); // } + /** + * Regression test for GitHub issue #2351: + * "Missing servlet context in web lookup when using composite configuration". + * + * When log4jConfiguration contains a comma-separated list of config files, + * the resulting composite LoggerContext must still expose the ServletContext + * via WebLoggerContextUtils.getServletContext() so that ${web:*} lookups resolve. + */ + @Test + void testCompositeConfigurationServletContextName() throws Exception { + ContextAnchor.THREAD_CONTEXT.remove(); + + final String expectedServletContextName = "CompositeTest"; + + // Use Mockito to create a minimal ServletContext (no Spring dependency needed) + final ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getServletContextName()).thenReturn(expectedServletContextName); + when(servletContext.getContextPath()).thenReturn("/composite-test"); + // Composite configuration: two comma-separated config files + when(servletContext.getInitParameter(Log4jWebSupport.LOG4J_CONFIG_LOCATION)) + .thenReturn("log4j2-combined.xml,log4j2-override.xml"); + // Let the initializer resolve each file via the servlet context resource lookup + when(servletContext.getResource("log4j2-combined.xml")) + .thenReturn(getClass().getResource("/log4j2-combined.xml")); + when(servletContext.getResource("log4j2-override.xml")) + .thenReturn(getClass().getResource("/log4j2-override.xml")); + + final Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext); + try { + initializer.start(); + initializer.setLoggerContext(); + + final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get(); + assertNotNull(ctx, "No LoggerContext"); + + // The servlet context MUST be reachable via the web lookup for composite config. + // Before the fix this returns null, breaking all ${web:*} lookups. + assertNotNull( + WebLoggerContextUtils.getServletContext(), + "ServletContext is null in composite configuration - " + + "${web:*} lookups will not resolve (issue #2351)"); + + final Configuration config = ctx.getConfiguration(); + assertNotNull(config, "No Configuration"); + + final StrSubstitutor substitutor = config.getStrSubstitutor(); + assertNotNull(substitutor, "No StrSubstitutor"); + // Core assertion: ${web:servletContextName} must resolve to the actual name + final String value = substitutor.replace("${web:servletContextName}"); + assertEquals( + expectedServletContextName, + value, + "${web:servletContextName} did not resolve in composite configuration (issue #2351)"); + } finally { + initializer.stop(); + ContextAnchor.THREAD_CONTEXT.remove(); + } + } }