This is an automated email from the ASF dual-hosted git repository. ppkarwasz pushed a commit to branch feat/2.x/xinclude-opt-in in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit ab09d5f22c54dc0b2fd668ab6ab938dd5062caf5 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Fri Jun 26 14:34:44 2026 +0200 Make XInclude processing in XML configurations opt-in XInclude support in XML configuration files is now disabled by default and must be enabled with the `log4j2.configurationEnableXInclude` property. The constructor reads the property and configures the `DocumentBuilder` accordingly, dropping the previous always-on behavior and its LOG4J2-1127 "retry without XInclude" fallback. The new property is documented alongside the other configuration-factory properties, and the XInclude manual section notes that it is now opt-in. The `ConfigurationFactoryTest.xinclude` test enables the property, and `testGetConfigurationWithMultipleUris` now composes two plain configurations instead of incidentally loading an XInclude file. A new `ConfigurationFactoryUnitTest` loads the same logical configuration as XML, JSON, YAML, Java properties, an XInclude-composed file (with the property enabled) and a cross-format composite, asserting through the `Configuration` interface that each yields the same components. It also covers the XInclude-disabled fallback to the default configuration. Assisted-By: Claude Opus 4.8 (1M context) <[email protected]> --- .../core/config/ConfigurationFactoryTest.java | 4 +- .../core/config/ConfigurationFactoryUnitTest.java | 171 +++++++++++++++++++++ .../src/test/resources/log4j-test1-basic.xml | 29 ++++ .../src/test/resources/log4j-test1-extended.yaml | 53 +++++++ .../log4j/core/config/xml/XmlConfiguration.java | 28 ++-- src/changelog/.2.x.x/xinclude_opt_in.xml | 12 ++ .../partials/manual/configuration-xml-format.adoc | 8 + .../properties-configuration-factory.adoc | 14 ++ 8 files changed, 299 insertions(+), 20 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java index ee19ea7ea8..a7232d1f34 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java @@ -42,6 +42,7 @@ import org.apache.logging.log4j.core.appender.ConsoleAppender; import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; import org.apache.logging.log4j.core.filter.ThreadContextMapFilter; import org.apache.logging.log4j.core.test.junit.LoggerContextSource; +import org.apache.logging.log4j.test.junit.SetTestProperty; import org.apache.logging.log4j.test.junit.TempLoggingDir; import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.Tag; @@ -103,6 +104,7 @@ class ConfigurationFactoryTest { } @Test + @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = "true") @LoggerContextSource("log4j-xinclude.xml") void xinclude(final LoggerContext context) throws IOException { checkConfiguration(context); @@ -181,7 +183,7 @@ class ConfigurationFactoryTest { final URI configLocation1 = getClass().getResource("/log4j-test1.xml").toURI(); final URI configLocation2 = - getClass().getResource("/log4j-xinclude.xml").toURI(); + getClass().getResource("/log4j-test2.xml").toURI(); final List<URI> configLocations = Arrays.asList(configLocation1, configLocation2); final Configuration config = factory.getConfiguration(context, "test", configLocations); assertInstanceOf(CompositeConfiguration.class, config); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java new file mode 100644 index 0000000000..b34bf0483a --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java @@ -0,0 +1,171 @@ +/* + * 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.logging.log4j.core.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.appender.FileAppender; +import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; +import org.apache.logging.log4j.core.filter.ThreadContextMapFilter; +import org.apache.logging.log4j.core.test.appender.ListAppender; +import org.apache.logging.log4j.test.junit.SetTestProperty; +import org.apache.logging.log4j.test.junit.TempLoggingDir; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Verifies that the same logical configuration, expressed as XML, JSON, YAML, Java properties, an XInclude-composed + * XML file, or a merged configuration produces the same components when loaded through {@link ConfigurationFactory}. + * + * <p>Unlike {@code ConfigurationFactoryTest}, this exercises the {@link Configuration} directly.</p> + */ +class ConfigurationFactoryUnitTest { + + // The File appender creates the log file at construction time, not at startup. + // This directory will hold the created files and delete them after the test. + @TempLoggingDir + private static Path loggingDir; + + private static Configuration load(final String resource) throws Exception { + final URI uri = + ConfigurationFactoryUnitTest.class.getResource("/" + resource).toURI(); + final Configuration configuration = + ConfigurationFactory.getInstance().getConfiguration(new LoggerContext("test"), resource, uri); + configuration.initialize(); + return configuration; + } + + private static Configuration loadComposite(final String... resources) throws Exception { + final List<URI> uris = new ArrayList<>(); + for (final String resource : resources) { + uris.add(ConfigurationFactoryUnitTest.class + .getResource("/" + resource) + .toURI()); + } + final Configuration configuration = + ConfigurationFactory.getInstance().getConfiguration(new LoggerContext("test"), "composite", uris); + configuration.initialize(); + return configuration; + } + + @ParameterizedTest + @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = "true") + @ValueSource( + strings = { + "log4j-test1.xml", + "log4j-test1.json", + "log4j-test1.yaml", + "log4j-test1.properties", + "log4j-xinclude.xml" + }) + void producesEquivalentComponents(final String resource) throws Exception { + final Configuration configuration = load(resource); + try { + assertAppenders(configuration); + assertLoggers(configuration); + // Every representation declares a `filename` substitution property resolving to a per-format log file. + assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log"); + } finally { + configuration.stop(); + } + } + + /** + * Tests behavior with XInclude disabled. + * + * <p>With XInclude disabled (the default), the {@code xi:include} elements are not expanded, + * so Log4j ignores them and falls back to the default setup: + * an {@code ERROR}-level root logger with a default {@code Console} appender.</p> + * + * <p>The {@code Properties} declared directly in the file are still honored.</p> + */ + @Test + void xIncludeDisabledIgnoresIncludesAndUsesDefaults() throws Exception { + final Configuration configuration = load("log4j-xinclude.xml"); + try { + // None of the included appenders or loggers are present. + final Map<String, Appender> appenders = configuration.getAppenders(); + assertThat(appenders).doesNotContainKeys("STDOUT", "File", "List"); + assertThat(configuration.getLoggers()) + .doesNotContainKeys("org.apache.logging.log4j.test1", "org.apache.logging.log4j.test2"); + + // The default setup is used instead: a single default Console appender and an ERROR-level root logger. + assertThat(appenders.values()).singleElement().satisfies(appender -> { + assertThat(appender).isInstanceOf(ConsoleAppender.class); + assertThat(appender.getName()).startsWith("DefaultConsole-"); + }); + assertThat(configuration.getRootLogger().getLevel()).isEqualTo(Level.ERROR); + + // The `Properties` declared directly in the file (outside the includes) are still resolved. + assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log"); + } finally { + configuration.stop(); + } + } + + /** + * The same component graph can be assembled from several sources, possibly in different formats, merged into a + * {@link CompositeConfiguration}: here an XML base (one appender and the root logger) and a YAML remainder. + */ + @Test + void mergesMultipleSources() throws Exception { + final Configuration configuration = loadComposite("log4j-test1-basic.xml", "log4j-test1-extended.yaml"); + try { + assertThat(configuration).isInstanceOf(CompositeConfiguration.class); + assertAppenders(configuration); + assertLoggers(configuration); + assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log"); + } finally { + configuration.stop(); + } + } + + private static void assertAppenders(final Configuration configuration) { + final Map<String, Appender> appenders = configuration.getAppenders(); + assertThat(appenders).containsOnlyKeys("STDOUT", "File", "List"); + assertThat(appenders.get("STDOUT")).isInstanceOf(ConsoleAppender.class); + assertThat(appenders.get("File")).isInstanceOf(FileAppender.class); + assertThat(appenders.get("List")).isInstanceOf(ListAppender.class); + } + + private static void assertLoggers(final Configuration configuration) { + final LoggerConfig test1 = configuration.getLoggerConfig("org.apache.logging.log4j.test1"); + assertThat(test1.getLevel()).isEqualTo(Level.DEBUG); + assertThat(test1.isAdditive()).isFalse(); + assertThat(test1.getFilter()).isInstanceOf(ThreadContextMapFilter.class); + assertThat(test1.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("STDOUT"); + + final LoggerConfig test2 = configuration.getLoggerConfig("org.apache.logging.log4j.test2"); + assertThat(test2.getLevel()).isEqualTo(Level.DEBUG); + assertThat(test2.isAdditive()).isFalse(); + assertThat(test2.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("File"); + + final LoggerConfig root = configuration.getRootLogger(); + assertThat(root.getLevel()).isEqualTo(Level.ERROR); + assertThat(root.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("STDOUT"); + } +} diff --git a/log4j-core-test/src/test/resources/log4j-test1-basic.xml b/log4j-core-test/src/test/resources/log4j-test1-basic.xml new file mode 100644 index 0000000000..eff1f6b3de --- /dev/null +++ b/log4j-core-test/src/test/resources/log4j-test1-basic.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ 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. + --> +<Configuration status="OFF" name="XMLConfigTestBasic"> + <Appenders> + <Console name="STDOUT"> + <PatternLayout pattern="%m%n"/> + </Console> + </Appenders> + <Loggers> + <Root level="error"> + <AppenderRef ref="STDOUT"/> + </Root> + </Loggers> +</Configuration> diff --git a/log4j-core-test/src/test/resources/log4j-test1-extended.yaml b/log4j-core-test/src/test/resources/log4j-test1-extended.yaml new file mode 100644 index 0000000000..4c23eab343 --- /dev/null +++ b/log4j-core-test/src/test/resources/log4j-test1-extended.yaml @@ -0,0 +1,53 @@ +# +# 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. +# +## +# Remainder of a composite configuration, merged with `log4j-test1-basic.xml` (a different format). +Configuration: + status: OFF + name: YAMLConfigTestExtended + properties: + property: + name: filename + value: ${test:logging.path}/test-composite.log + appenders: + File: + name: File + fileName: ${filename} + bufferedIO: false + PatternLayout: + Pattern: "%d %p %C{1.} [%t] %m%n" + List: + name: List + Filters: + ThresholdFilter: + level: error + Loggers: + logger: + - name: org.apache.logging.log4j.test1 + level: debug + additivity: false + ThreadContextMapFilter: + KeyValuePair: + key: test + value: 123 + AppenderRef: + ref: STDOUT + - name: org.apache.logging.log4j.test2 + level: debug + additivity: false + AppenderRef: + ref: File diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java index 8861171899..e36025f6d7 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java @@ -45,7 +45,7 @@ import org.apache.logging.log4j.core.util.Closer; import org.apache.logging.log4j.core.util.Integers; import org.apache.logging.log4j.core.util.Loader; import org.apache.logging.log4j.core.util.Patterns; -import org.apache.logging.log4j.core.util.Throwables; +import org.apache.logging.log4j.util.PropertiesUtil; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -60,6 +60,11 @@ import org.xml.sax.SAXException; */ public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable { + /** + * Property that enables XInclude processing in XML configuration files. Disabled by default. + */ + private static final String ENABLE_XINCLUDE_PROP = "log4j2.configurationEnableXInclude"; + private Element rootElement; private boolean strict; private String schemaResource; @@ -80,24 +85,9 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu } final InputSource source = new InputSource(new ByteArrayInputStream(buffer)); source.setSystemId(configSource.getLocation()); - final DocumentBuilder documentBuilder = newDocumentBuilder(true); - Document document; - try { - document = documentBuilder.parse(source); - } catch (final Exception e) { - // LOG4J2-1127 - final Throwable throwable = Throwables.getRootCause(e); - if (throwable instanceof UnsupportedOperationException) { - LOGGER.warn( - "The DocumentBuilder {} does not support an operation: {}." - + "Trying again without XInclude...", - documentBuilder, - e); - document = newDocumentBuilder(false).parse(source); - } else { - throw e; - } - } + final boolean xIncludeAware = PropertiesUtil.getProperties().getBooleanProperty(ENABLE_XINCLUDE_PROP); + final DocumentBuilder documentBuilder = newDocumentBuilder(xIncludeAware); + final Document document = documentBuilder.parse(source); rootElement = document.getDocumentElement(); final Map<String, String> attrs = processAttributes(rootNode, rootElement); final StatusConfiguration statusConfig = new StatusConfiguration().withStatus(getDefaultStatus()); diff --git a/src/changelog/.2.x.x/xinclude_opt_in.xml b/src/changelog/.2.x.x/xinclude_opt_in.xml new file mode 100644 index 0000000000..b3ae06d871 --- /dev/null +++ b/src/changelog/.2.x.x/xinclude_opt_in.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<entry xmlns="https://logging.apache.org/xml/ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + https://logging.apache.org/xml/ns + https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" + type="changed"> + <issue id="4064" link="https://github.com/apache/logging-log4j2/issues/4064"/> + <description format="asciidoc"> + Make XInclude processing in XML configurations opt-in (disabled by default): it can be enabled with the `log4j2.configurationEnableXInclude` property. + </description> +</entry> diff --git a/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc b/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc index 5a19684e08..cd8d423232 100644 --- a/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc +++ b/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc @@ -54,6 +54,14 @@ This setting also enables "XML strict mode" and allows one to specify an element XML configuration files can include other files with https://www.w3.org/TR/xinclude/[XInclude]. +[IMPORTANT] +==== +Since version `2.27.0`, XInclude processing is **disabled by default**. +Set the +xref:manual/systemproperties.adoc#log4j2.configurationEnableXInclude[`log4j2.configurationEnableXInclude`] +configuration property to `true` to enable it. +==== + NOTE: The list of `XInclude` and `XPath` features supported depends upon your https://docs.oracle.com/javase/{java-target-version}/docs/technotes/guides/xml/jaxp/index.html[JAXP implementation]. diff --git a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc index 1841a4ee4d..e9f0f1f7b5 100644 --- a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc +++ b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc @@ -14,6 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. //// +[id=log4j2.configurationEnableXInclude] +== `log4j2.configurationEnableXInclude` + +[cols="1h,5"] +|=== +| Env. variable | `LOG4J_CONFIGURATION_ENABLE_XINCLUDE` +| Type | `boolean` +| Default value | `false` +|=== + +If set to `true`, enables https://www.w3.org/TR/xinclude/[XInclude] processing in XML configuration files. + +See xref:manual/configuration.adoc#xinclude[XInclude] for details. + [id=log4j2.configurationFactory] == `log4j2.configurationFactory`
