This is an automated email from the ASF dual-hosted git repository. ppkarwasz pushed a commit to branch fix/2.x/xinclude-fixup-attributes in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 3dce0bc55328add2083c66f453a890ac57f9304e Author: Piotr P. Karwasz <[email protected]> AuthorDate: Fri Jun 26 13:08:51 2026 +0200 Revamp XML configuration testing and documentation Rework the testing and documentation of the XML configuration format so that each behavior of the DOM-to-Node conversion is covered by its own focused test. `constructHierarchy`, `getType` and `processAttributes` are now static and take the `PluginManager` and `strict` flag as parameters, so the conversion can be unit-tested with a mocked `PluginManager` instead of a full `LoggerContext`. The `StrictXmlConfigTest` integration test is replaced by `XmlConfigurationConstructHierarchyTest`, which exercises plugin resolution, attribute folding, text values, strict-mode `type` resolution and XInclude whole-file/positional/by-id fragment resolution individually. The unused `Status`/`ErrorType` bookkeeping is dropped in favor of logging the error directly. The XML format documentation is moved into a reusable AsciiDoc partial and gains XPointer fragment examples. Testing each behavior individually surfaced a bug: the XInclude `fixup-language` feature adds an `xml:lang` attribute that was never stripped (only `xml:base` was) and was not covered by any test, so it leaked through as a Log4j configuration attribute. Matching the reserved XML namespace instead of the literal attribute name now strips both `xml:base` and `xml:lang`, and any other reserved `xml:` attribute. `XmlConfiguration` also stops explicitly enabling the XInclude `fixup-base-uris`/`fixup-language` features: they default to `true` and only add the attributes that Log4j strips. Assisted-By: Claude Opus 4.8 (1M context) <[email protected]> --- .../logging/log4j/core/StrictXmlConfigTest.java | 131 ---------- .../XmlConfigurationConstructHierarchyTest.java | 279 +++++++++++++++++++++ .../included-fragment.xml | 21 ++ .../included-with-id.xml | 26 ++ .../included.xml | 18 ++ .../src/test/resources/log4j-strict1.xml | 65 ----- .../log4j/core/config/xml/XmlConfiguration.java | 61 +---- src/changelog/.2.x.x/xinclude_fixup_attributes.xml | 11 + .../configuration/xinclude-xpointer-appenders.xml | 30 +++ .../configuration/xinclude-xpointer-main.xml | 34 +++ .../modules/ROOT/pages/manual/configuration.adoc | 62 +---- .../partials/manual/configuration-xml-format.adoc | 111 ++++++++ 12 files changed, 544 insertions(+), 305 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/StrictXmlConfigTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/StrictXmlConfigTest.java deleted file mode 100644 index 0bd00104b1..0000000000 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/StrictXmlConfigTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Date; -import java.util.List; -import java.util.Locale; -import org.apache.logging.log4j.MarkerManager; -import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.core.test.appender.ListAppender; -import org.apache.logging.log4j.core.test.junit.LoggerContextSource; -import org.apache.logging.log4j.core.test.junit.Named; -import org.apache.logging.log4j.message.EntryMessage; -import org.apache.logging.log4j.message.StructuredDataMessage; -import org.junit.jupiter.api.Test; - -@LoggerContextSource("log4j-strict1.xml") -class StrictXmlConfigTest { - - org.apache.logging.log4j.Logger logger; - private ListAppender app; - - public StrictXmlConfigTest(final LoggerContext context, @Named("List") final ListAppender app) { - logger = context.getLogger("LoggerTest"); - this.app = app.clear(); - } - - @Test - void basicFlow() { - final EntryMessage entry = logger.traceEntry(); - logger.traceExit(entry); - final List<LogEvent> events = app.getEvents(); - assertEquals(2, events.size(), "Incorrect number of events. Expected 2, actual " + events.size()); - } - - @Test - void basicFlowDeprecated() { - logger.traceEntry(); - logger.traceExit(); - final List<LogEvent> events = app.getEvents(); - assertEquals(2, events.size(), "Incorrect number of events. Expected 2, actual " + events.size()); - } - - @Test - void simpleFlow() { - logger.traceEntry(); - logger.traceExit(0); - final List<LogEvent> events = app.getEvents(); - assertEquals(2, events.size(), "Incorrect number of events. Expected 2, actual " + events.size()); - } - - @Test - void throwing() { - logger.throwing(new IllegalArgumentException("Test Exception")); - final List<LogEvent> events = app.getEvents(); - assertEquals(1, events.size(), "Incorrect number of events. Expected 1, actual " + events.size()); - } - - @Test - void catching() { - try { - throw new NullPointerException(); - } catch (final Exception e) { - logger.catching(e); - } - final List<LogEvent> events = app.getEvents(); - assertEquals(1, events.size(), "Incorrect number of events. Expected 1, actual " + events.size()); - } - - @Test - void debug() { - logger.debug("Debug message"); - final List<LogEvent> events = app.getEvents(); - assertEquals(1, events.size(), "Incorrect number of events. Expected 1, actual " + events.size()); - } - - @Test - void debugObject() { - logger.debug(new Date()); - final List<LogEvent> events = app.getEvents(); - assertEquals(1, events.size(), "Incorrect number of events. Expected 1, actual " + events.size()); - } - - @Test - void debugWithParms() { - logger.debug("Hello, {}", "World"); - final List<LogEvent> events = app.getEvents(); - assertEquals(1, events.size(), "Incorrect number of events. Expected 1, actual " + events.size()); - } - - @Test - void mdc() { - ThreadContext.put("TestYear", "2010"); - logger.debug("Debug message"); - ThreadContext.clearMap(); - logger.debug("Debug message"); - final List<LogEvent> events = app.getEvents(); - assertEquals(2, events.size(), "Incorrect number of events. Expected 2, actual " + events.size()); - } - - @Test - void structuredData() { - ThreadContext.put("loginId", "JohnDoe"); - ThreadContext.put("ipAddress", "192.168.0.120"); - ThreadContext.put("locale", Locale.US.getDisplayName()); - final StructuredDataMessage msg = new StructuredDataMessage("Audit@18060", "Transfer Complete", "Transfer"); - msg.put("ToAccount", "123456"); - msg.put("FromAccount", "123457"); - msg.put("Amount", "200.00"); - logger.info(MarkerManager.getMarker("EVENT"), msg); - ThreadContext.clearMap(); - final List<LogEvent> events = app.getEvents(); - assertEquals(1, events.size(), "Incorrect number of events. Expected 1, actual " + events.size()); - } -} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java new file mode 100644 index 0000000000..a0fbb29324 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java @@ -0,0 +1,279 @@ +/* + * 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.xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +import java.io.StringReader; +import java.util.Objects; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.util.PluginManager; +import org.apache.logging.log4j.core.config.plugins.util.PluginType; +import org.apache.logging.log4j.test.ListStatusListener; +import org.apache.logging.log4j.test.junit.UsingStatusListener; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +/** + * Unit tests for {@link XmlConfiguration#constructHierarchy(Node, Element, PluginManager, boolean)}. + * <p> + * These exercise the DOM-to-{@link Node} conversion in isolation: the {@link PluginManager} is mocked, so the test + * decides which element names resolve to a plugin without pulling in the real plugin registry or a running + * {@code LoggerContext}. + * </p> + */ +@ExtendWith(MockitoExtension.class) +class XmlConfigurationConstructHierarchyTest { + + private static final String INCLUDED_URI = resourceUri("included.xml"); + private static final String FRAGMENT_URI = resourceUri("included-fragment.xml"); + private static final String WITH_ID_URI = resourceUri("included-with-id.xml"); + + @Mock + private PluginManager pluginManager; + + private static String resourceUri(final String name) { + return Objects.requireNonNull(XmlConfigurationConstructHierarchyTest.class.getResource( + "/XmlConfigurationConstructHierarchyTest/" + name)) + .toExternalForm(); + } + + /** Parses an inline XML snippet into its root {@link Element}, configuring the parser exactly as production does. */ + private static Element parse(final String xml) throws Exception { + return parse(xml, false); + } + + private static Element parse(final String xml, final boolean enableXInclude) throws Exception { + final DocumentBuilder builder = XmlConfiguration.newDocumentBuilder(enableXInclude); + return builder.parse(new InputSource(new StringReader(xml))).getDocumentElement(); + } + + /** Builds an {@code <Appenders>} document with a single {@code xi:include} of {@code href}, optionally pointed. */ + private static String including(final String href, final String xpointer) { + final String pointer = xpointer == null ? "" : " xpointer=\"" + xpointer + "\""; + return "<Appenders xmlns:xi=\"http://www.w3.org/2001/XInclude\"><xi:include href=\"" + href + "\"" + pointer + + "/></Appenders>"; + } + + @SuppressWarnings("unchecked") + private void givenPlugin(final String name) { + // `lenient`: `constructHierarchy` also looks up names that are intentionally left unresolved (returning + // `null`), which strict stubbing would otherwise flag as an argument mismatch. + lenient().when(pluginManager.getPluginType(name)).thenReturn(mock(PluginType.class)); + } + + /** A child element that resolves to a plugin becomes a child {@link Node} carrying that plugin type. */ + @Test + void resolvedElementBecomesChildNode() throws Exception { + givenPlugin("Console"); + final Element element = parse("<Appenders><Console name=\"STDOUT\"/></Appenders>"); + + final Node root = new Node(); + XmlConfiguration.constructHierarchy(root, element, pluginManager, false); + + assertThat(root.getChildren()).singleElement().satisfies(child -> { + assertThat(child.getName()).isEqualTo("Console"); + assertThat(child.getType()).isNotNull(); + assertThat(child.getAttributes()).containsEntry("name", "STDOUT"); + }); + } + + /** A leaf element with no plugin type and a text value is folded into its parent as an attribute. */ + @Test + void unresolvedLeafElementBecomesAttribute() throws Exception { + // `Pattern` has no plugin type, so `<PatternLayout>` absorbs it as a `Pattern` attribute. + final Element element = parse("<PatternLayout><Pattern>%m%n</Pattern></PatternLayout>"); + + final Node patternLayout = new Node(); + XmlConfiguration.constructHierarchy(patternLayout, element, pluginManager, false); + + assertThat(patternLayout.getChildren()).isEmpty(); + assertThat(patternLayout.getAttributes()).containsEntry("Pattern", "%m%n"); + } + + /** An element with no plugin type but with children cannot be folded: it is dropped and an error is logged. */ + @Test + @UsingStatusListener + void unresolvedElementWithChildrenIsDropped(final ListStatusListener listener) throws Exception { + givenPlugin("Console"); + final Element element = parse("<Appenders><Unknown><Console/></Unknown></Appenders>"); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, false); + + // The `Unknown` element (and the `Console` nested in it) are not added to the tree. + assertThat(appenders.getChildren()).isEmpty(); + assertThat(listener.findStatusData(Level.ERROR)) + .anyMatch(data -> data.getMessage().getFormattedMessage().contains("Error processing element Unknown")); + } + + /** The text content of a resolved element becomes the node value. */ + @Test + void textContentBecomesNodeValue() throws Exception { + givenPlugin("Property"); + final Element element = + parse("<Properties><Property name=\"filename\">target/test.log</Property></Properties>"); + + final Node properties = new Node(); + XmlConfiguration.constructHierarchy(properties, element, pluginManager, false); + + assertThat(properties.getChildren()).singleElement().satisfies(child -> { + assertThat(child.getName()).isEqualTo("Property"); + assertThat(child.getValue()).isEqualTo("target/test.log"); + assertThat(child.getAttributes()).containsEntry("name", "filename"); + }); + } + + /** In strict mode the plugin name comes from the {@code type} attribute, which is consumed in the process. */ + @Test + void strictModeResolvesPluginFromTypeAttribute() throws Exception { + givenPlugin("Console"); + final Element element = parse("<Appenders><Appender type=\"Console\" name=\"STDOUT\"/></Appenders>"); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, true); + + assertThat(appenders.getChildren()).singleElement().satisfies(child -> { + assertThat(child.getName()).isEqualTo("Console"); + // The `type` attribute is consumed by the plugin-name lookup and not exposed as a Log4j attribute. + assertThat(child.getAttributes()).containsEntry("name", "STDOUT").doesNotContainKey("type"); + }); + } + + /** Without strict mode the tag name is the plugin name and {@code type} stays an ordinary attribute. */ + @Test + void nonStrictModeResolvesPluginFromTagName() throws Exception { + // With the tag name used as the plugin name, `Console` (the `type` attribute) is never looked up. + givenPlugin("Appender"); + final Element element = parse("<Appenders><Appender type=\"Console\" name=\"STDOUT\"/></Appenders>"); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, false); + + assertThat(appenders.getChildren()).singleElement().satisfies(child -> { + assertThat(child.getName()).isEqualTo("Appender"); + assertThat(child.getAttributes()).containsEntry("type", "Console").containsEntry("name", "STDOUT"); + }); + } + + /** An {@code xml:base} attribute (as added by the XInclude {@code fixup-base-uris} feature) is not exposed. */ + @Test + void xmlBaseAttributeIsStripped() throws Exception { + givenPlugin("Console"); + final Element element = parse("<Appenders><Console xml:base=\"urn:somewhere\" name=\"STDOUT\"/></Appenders>"); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, false); + + assertThat(appenders.getChildren()).singleElement().satisfies(child -> assertThat(child.getAttributes()) + .containsEntry("name", "STDOUT") + .doesNotContainKey("xml:base")); + } + + /** An {@code xml:lang} attribute (as added by the XInclude {@code fixup-language} feature) is not exposed. */ + @Test + void xmlLangAttributeIsStripped() throws Exception { + givenPlugin("Console"); + final Element element = parse("<Appenders><Console xml:lang=\"en\" name=\"STDOUT\"/></Appenders>"); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, false); + + assertThat(appenders.getChildren()).singleElement().satisfies(child -> assertThat(child.getAttributes()) + .containsEntry("name", "STDOUT") + .doesNotContainKey("xml:lang")); + } + + /** + * Checks the relevance of Log4j's attribute stripping + * + * <p>The XInclude {@code fixup-base-uris} and {@code fixup-language} features default to {@code true}, so a normal + * XInclude parse really does add {@code xml:base} and {@code xml:lang} to the included element.</p> + */ + @Test + void fixupAttributesAreProducedByDefault() throws Exception { + // `xml:lang` on the parent gives `fixup-language` a value to reconcile on the included `<Console>`. + final String document = "<Appenders xmlns:xi=\"http://www.w3.org/2001/XInclude\" xml:lang=\"en\">" + + "<xi:include href=\"" + INCLUDED_URI + "\"/></Appenders>"; + final Element console = + (Element) parse(document, true).getElementsByTagName("Console").item(0); + + assertThat(console.hasAttributeNS(XMLConstants.XML_NS_URI, "base")).isTrue(); + assertThat(console.hasAttributeNS(XMLConstants.XML_NS_URI, "lang")).isTrue(); + } + + /** With XInclude enabled, an {@code xi:include} is resolved and its content spliced into the tree. */ + @Test + void xIncludeIsHonored() throws Exception { + givenPlugin("Console"); + final Element element = parse(including(INCLUDED_URI, null), true); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, false); + + // The `<Console>` from `included.xml` has replaced the `<xi:include>` element. + assertThat(appenders.getChildren()).singleElement().satisfies(child -> { + assertThat(child.getName()).isEqualTo("Console"); + assertThat(child.getAttributes()).containsEntry("name", "STDOUT"); + }); + } + + /** A positional {@code xpointer} ({@code element(/1/2)}) selects a single element by its position. */ + @Test + void xIncludeResolvesPositionalPointer() throws Exception { + givenPlugin("Console"); + // `element(/1/2)`: the second child element of the document root (`<Fragment>`). + final Element element = parse(including(FRAGMENT_URI, "element(/1/2)"), true); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, false); + + assertThat(appenders.getChildren()).singleElement().satisfies(child -> { + assertThat(child.getName()).isEqualTo("Console"); + assertThat(child.getAttributes()).containsEntry("name", "SECOND"); + }); + } + + /** + * A shorthand {@code xpointer} selects an element by an {@code ID}-typed attribute. + * + * <p>The {@code name} attribute is typed {@code ID} through the included document's internal DTD subset, + * which Log4j's hardening leaves enabled (only the <em>external</em> subset is disabled).</p> + */ + @Test + void xIncludeResolvesIdPointer() throws Exception { + givenPlugin("Console"); + final Element element = parse(including(WITH_ID_URI, "OTHER"), true); + + final Node appenders = new Node(); + XmlConfiguration.constructHierarchy(appenders, element, pluginManager, false); + + assertThat(appenders.getChildren()).singleElement().satisfies(child -> { + assertThat(child.getName()).isEqualTo("Console"); + assertThat(child.getAttributes()).containsEntry("name", "OTHER"); + }); + } +} diff --git a/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included-fragment.xml b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included-fragment.xml new file mode 100644 index 0000000000..47e5e0906b --- /dev/null +++ b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included-fragment.xml @@ -0,0 +1,21 @@ +<?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. + --> +<Fragment> + <Console name="FIRST"/> + <Console name="SECOND"/> +</Fragment> diff --git a/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included-with-id.xml b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included-with-id.xml new file mode 100644 index 0000000000..b2c4f05d61 --- /dev/null +++ b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included-with-id.xml @@ -0,0 +1,26 @@ +<?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. + --> +<!DOCTYPE Fragment [ + <!ELEMENT Fragment (Console*)> + <!ELEMENT Console EMPTY> + <!ATTLIST Console name ID #IMPLIED> +]> +<Fragment> + <Console name="STDOUT"/> + <Console name="OTHER"/> +</Fragment> diff --git a/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included.xml b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included.xml new file mode 100644 index 0000000000..d087f23b12 --- /dev/null +++ b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/included.xml @@ -0,0 +1,18 @@ +<?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. + --> +<Console name="STDOUT"/> diff --git a/log4j-core-test/src/test/resources/log4j-strict1.xml b/log4j-core-test/src/test/resources/log4j-strict1.xml deleted file mode 100644 index cb37026046..0000000000 --- a/log4j-core-test/src/test/resources/log4j-strict1.xml +++ /dev/null @@ -1,65 +0,0 @@ -<?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" strict="true" name="XMLConfigTest"> - <Properties> - <Property name="filename">target/test.log</Property> - </Properties> - <Filter type="ThresholdFilter" level="trace"/> - - <Appenders> - <Appender type="Console" name="STDOUT"> - <Layout type="PatternLayout" pattern="%m MDC%X%n"/> - <Filters> - <Filter type="MarkerFilter" marker="FLOW" onMatch="DENY" onMismatch="NEUTRAL"/> - <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="DENY" onMismatch="ACCEPT"/> - </Filters> - </Appender> - <Appender type="Console" name="FLOW"> - <Layout type="PatternLayout" pattern="%C{1}.%M %m %ex%n"/> - <Filters> - <Filter type="MarkerFilter" marker="FLOW" onMatch="ACCEPT" onMismatch="NEUTRAL"/> - <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="ACCEPT" onMismatch="DENY"/> - </Filters> - </Appender> - <Appender type="File" name="File" fileName="${filename}"> - <Layout type="PatternLayout"> - <Pattern>%d %p %C{1.} [%t] %m%n</Pattern> - </Layout> - </Appender> - <Appender type="List" name="List"> - </Appender> - </Appenders> - - <Loggers> - <Logger name="org.apache.logging.log4j.test1" level="debug" additivity="false"> - <Filter type="ThreadContextMapFilter"> - <KeyValuePair key="test" value="123"/> - </Filter> - <AppenderRef ref="STDOUT"/> - </Logger>> - - <Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false"> - <AppenderRef ref="File"/> - </Logger>> - - <Root level="trace"> - <AppenderRef ref="List"/> - </Root> - </Loggers> - -</Configuration> 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 3218589bb1..8861171899 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 @@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.time.Instant; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -38,6 +37,7 @@ import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.Node; import org.apache.logging.log4j.core.config.Reconfigurable; +import org.apache.logging.log4j.core.config.plugins.util.PluginManager; import org.apache.logging.log4j.core.config.plugins.util.PluginType; import org.apache.logging.log4j.core.config.status.StatusConfiguration; import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; @@ -60,10 +60,6 @@ import org.xml.sax.SAXException; */ public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable { - private static final String XINCLUDE_FIXUP_LANGUAGE = "http://apache.org/xml/features/xinclude/fixup-language"; - private static final String XINCLUDE_FIXUP_BASE_URIS = "http://apache.org/xml/features/xinclude/fixup-base-uris"; - - private final List<Status> status = new ArrayList<>(); private Element rootElement; private boolean strict; private String schemaResource; @@ -231,8 +227,6 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu LOGGER.warn( "The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory, err); } - setFeature(factory, XINCLUDE_FIXUP_BASE_URIS, true); - setFeature(factory, XINCLUDE_FIXUP_LANGUAGE, true); } @Override @@ -241,13 +235,7 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu LOGGER.error("No logging configuration"); return; } - constructHierarchy(rootNode, rootElement); - if (!status.isEmpty()) { - for (final Status s : status) { - LOGGER.error("Error processing element {} ({}): {}", s.name, s.element, s.errorType); - } - return; - } + constructHierarchy(rootNode, rootElement, pluginManager, strict); rootElement = null; } @@ -266,7 +254,9 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu return null; } - private void constructHierarchy(final Node node, final Element element) { + // Package-private for testing + static void constructHierarchy( + final Node node, final Element element, final PluginManager pluginManager, final boolean strict) { processAttributes(node, element); final StringBuilder buffer = new StringBuilder(); final NodeList list = element.getChildNodes(); @@ -275,16 +265,16 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu final org.w3c.dom.Node w3cNode = list.item(i); if (w3cNode instanceof Element) { final Element child = (Element) w3cNode; - final String name = getType(child); + final String name = getType(child, strict); final PluginType<?> type = pluginManager.getPluginType(name); final Node childNode = new Node(node, name, type); - constructHierarchy(childNode, child); + constructHierarchy(childNode, child, pluginManager, strict); if (type == null) { final String value = childNode.getValue(); if (!childNode.hasChildren() && value != null) { node.getAttributes().put(name, value); } else { - status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND)); + LOGGER.error("Error processing element {} ({}): CLASS_NOT_FOUND", name, element); } } else { children.add(childNode); @@ -301,7 +291,7 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu } } - private String getType(final Element element) { + private static String getType(final Element element, final boolean strict) { if (strict) { final NamedNodeMap attrs = element.getAttributes(); for (int i = 0; i < attrs.getLength(); ++i) { @@ -319,7 +309,7 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu return element.getTagName(); } - private Map<String, String> processAttributes(final Node node, final Element element) { + private static Map<String, String> processAttributes(final Node node, final Element element) { final NamedNodeMap attrs = element.getAttributes(); final Map<String, String> attributes = node.getAttributes(); @@ -327,7 +317,9 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu final org.w3c.dom.Node w3cNode = attrs.item(i); if (w3cNode instanceof Attr) { final Attr attr = (Attr) w3cNode; - if (attr.getName().equals("xml:base")) { + // The XInclude `fixup-base-uris` and `fixup-language` features (both enabled by default) add + // `xml:base` and `xml:lang` attributes to the top-level included elements. + if (XMLConstants.XML_NS_URI.equals(attr.getNamespaceURI())) { continue; } attributes.put(attr.getName(), attr.getValue()); @@ -341,31 +333,4 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu return getClass().getSimpleName() + "[location=" + getConfigurationSource() + ", lastModified=" + Instant.ofEpochMilli(getConfigurationSource().getLastModified()) + "]"; } - - /** - * The error that occurred. - */ - private enum ErrorType { - CLASS_NOT_FOUND - } - - /** - * Status for recording errors. - */ - private static class Status { - private final Element element; - private final String name; - private final ErrorType errorType; - - public Status(final String name, final Element element, final ErrorType errorType) { - this.name = name; - this.element = element; - this.errorType = errorType; - } - - @Override - public String toString() { - return "Status [name=" + name + ", element=" + element + ", errorType=" + errorType + "]"; - } - } } diff --git a/src/changelog/.2.x.x/xinclude_fixup_attributes.xml b/src/changelog/.2.x.x/xinclude_fixup_attributes.xml new file mode 100644 index 0000000000..525ecfeddd --- /dev/null +++ b/src/changelog/.2.x.x/xinclude_fixup_attributes.xml @@ -0,0 +1,11 @@ +<?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="fixed"> + <description format="asciidoc"> + Fix XML configurations exposing the reserved `xml:base` and `xml:lang` attributes as configuration attributes. + </description> +</entry> diff --git a/src/site/antora/modules/ROOT/examples/manual/configuration/xinclude-xpointer-appenders.xml b/src/site/antora/modules/ROOT/examples/manual/configuration/xinclude-xpointer-appenders.xml new file mode 100644 index 0000000000..9587c6f8b5 --- /dev/null +++ b/src/site/antora/modules/ROOT/examples/manual/configuration/xinclude-xpointer-appenders.xml @@ -0,0 +1,30 @@ +<?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. + --> +<!DOCTYPE Appenders [ + <!-- Typing `name` as `ID` lets an `xpointer` shorthand select an appender by its name. --> + <!ATTLIST Console name ID #IMPLIED> + <!ATTLIST File name ID #IMPLIED> +]> +<Appenders> + <Console name="CONSOLE"> + <PatternLayout pattern="%d %m%n"/> + </Console> + <File name="FILE" fileName="${filename}"> + <PatternLayout pattern="%d %p %C{1.} [%t] %m%n"/> + </File> +</Appenders> diff --git a/src/site/antora/modules/ROOT/examples/manual/configuration/xinclude-xpointer-main.xml b/src/site/antora/modules/ROOT/examples/manual/configuration/xinclude-xpointer-main.xml new file mode 100644 index 0000000000..c28a071fd8 --- /dev/null +++ b/src/site/antora/modules/ROOT/examples/manual/configuration/xinclude-xpointer-main.xml @@ -0,0 +1,34 @@ +<?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 xmlns="https://logging.apache.org/xml/ns" + xmlns:xi="http://www.w3.org/2001/XInclude" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + https://logging.apache.org/xml/ns + https://logging.apache.org/xml/ns/log4j-config-2.xsd"> + <Properties> + <Property name="filename" value="app.log"/> + </Properties> + <Appenders> + <!-- Pull only the `CONSOLE` appender from the shared library, selected by its name. --> + <!-- tag::inclusion[] --> + <xi:include href="xinclude-xpointer-appenders.xml" xpointer="CONSOLE"/> + <!-- end::inclusion[] --> + </Appenders> + <xi:include href="xinclude-loggers.xml"/> +</Configuration> diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration.adoc b/src/site/antora/modules/ROOT/pages/manual/configuration.adoc index 3b64deb3cb..e065d0ce9b 100644 --- a/src/site/antora/modules/ROOT/pages/manual/configuration.adoc +++ b/src/site/antora/modules/ROOT/pages/manual/configuration.adoc @@ -1165,67 +1165,7 @@ include::partial$manual/composite-configuration.adoc[tag=how] [id=format-specific-notes] == Format specific notes -[id=xml-features] -=== XML format - -[id=xml-global-configuration-attributes] -==== Global configuration attributes - -The XML format supports the following additional attributes on the `Configuration` element. - -[id=configuration-attribute-schema] -===== `schema` - -[cols="1h,5"] -|=== -| Type | classpath resource -| Default value | `null` -|=== - -Specifies the path to a classpath resource containing an XML schema. - -[id=configuration-attribute-strict] -===== `strict` - -[cols="1h,5"] -|=== -| Type | `boolean` -| Default value | `false` -|=== - -If set to `true,` all configuration files will be checked against the XML schema provided by the -<<configuration-attribute-schema>>. - -This setting also enables "XML strict mode" and allows one to specify an element's **plugin type** through a `type` attribute instead of the tag name. - -[id=xinclude] -==== [[XInlcude]] XInclude - -XML configuration files can include other files with -https://www.w3.org/TR/xinclude/[XInclude]. - -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]. - -Here is an example `log4j2.xml` file that includes two other files: - -.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-main.xml[`log4j2.xml`] -[source,xml] ----- -include::example$manual/configuration/xinclude-main.xml[lines=1;18..-1] ----- - -.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-appenders.xml[`xinclude-appenders.xml`] -[source,xml] ----- -include::example$manual/configuration/xinclude-appenders.xml[lines=1;18..-1] ----- - -.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-loggers.xml[`xinclude-loggers.xml`] -[source,xml] ----- -include::example$manual/configuration/xinclude-loggers.xml[lines=1;18..-1] ----- +include::partial$manual/configuration-xml-format.adoc[leveloffset=+2] [id=java-properties-features] === Java properties format 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 new file mode 100644 index 0000000000..5a19684e08 --- /dev/null +++ b/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc @@ -0,0 +1,111 @@ +//// + 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. +//// + +[id=xml-features] += XML format + +[id=xml-global-configuration-attributes] +== Global configuration attributes + +The XML format supports the following additional attributes on the `Configuration` element. + +[id=configuration-attribute-schema] +=== `schema` + +[cols="1h,5"] +|=== +| Type | classpath resource +| Default value | `null` +|=== + +Specifies the path to a classpath resource containing an XML schema. + +[id=configuration-attribute-strict] +=== `strict` + +[cols="1h,5"] +|=== +| Type | `boolean` +| Default value | `false` +|=== + +If set to `true,` all configuration files will be checked against the XML schema provided by the +<<configuration-attribute-schema>>. + +This setting also enables "XML strict mode" and allows one to specify an element's **plugin type** through a `type` attribute instead of the tag name. + +[id=xinclude] +== [[XInlcude]] XInclude + +XML configuration files can include other files with +https://www.w3.org/TR/xinclude/[XInclude]. + +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]. + +Here is an example `log4j2.xml` file that includes two other files: + +.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-main.xml[`log4j2.xml`] +[source,xml] +---- +include::example$manual/configuration/xinclude-main.xml[lines=1;18..-1] +---- + +.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-appenders.xml[`xinclude-appenders.xml`] +[source,xml] +---- +include::example$manual/configuration/xinclude-appenders.xml[lines=1;18..-1] +---- + +.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-loggers.xml[`xinclude-loggers.xml`] +[source,xml] +---- +include::example$manual/configuration/xinclude-loggers.xml[lines=1;18..-1] +---- + +Instead of a whole file, you can include a single element by appending an +https://www.w3.org/TR/xptr-framework/[XPointer] to the `href`. +The JDK's XInclude engine only implements the `element()` scheme and *shorthand* pointers, so two forms are available: + +* A *positional* pointer addresses an element by its position in the document. +For example `xpointer="element(/1/2)"` selects the second child element of the document root: ++ +[source,xml] +---- +<xi:include href="xinclude-appenders.xml" xpointer="element(/1/2)"/> +---- + +* A *shorthand* pointer addresses an element by an attribute of type `ID`. +The appender `name` is a natural identifier: ++ +.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-xpointer-appenders.xml[`xinclude-xpointer-appenders.xml`] +[source,xml] +---- +include::example$manual/configuration/xinclude-xpointer-appenders.xml[lines=1;18..-1] +---- ++ +.Snippet from an example {antora-examples-url}/manual/configuration/xinclude-xpointer-main.xml[`log4j2.xml`] +[source,xml] +---- +include::example$manual/configuration/xinclude-xpointer-main.xml[tag=inclusion,indent=0] +---- + +[NOTE] +==== +The JDK's XInclude engine does **not** recognize the `xml:id` attribute as an `ID`, and it does **not** implement the full `xpointer()` scheme. +Only an attribute typed `ID` through a DTD (in practice, the included file's internal subset) can be addressed by a shorthand pointer. +==== \ No newline at end of file
