This is an automated email from the ASF dual-hosted git repository.
ppkarwasz pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.x by this push:
new 359ee520e3 Revamp XML configuration testing and documentation (#4155)
359ee520e3 is described below
commit 359ee520e3a98558899ceda9e6a4005669c479ed
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Sun Jul 26 07:06:40 2026 +0200
Revamp XML configuration testing and documentation (#4155)
* 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]>
* fix: add reference to PR
* Add a test for nested XInclude resolution
Verify that an included document may itself contain an `xi:include`, and
that
the inner relative `href` resolves against the included document's own
location
rather than the top-level configuration's.
Assisted-By: Claude Opus 4.8 (1M context) <[email protected]>
* Fix formatting
---
.../logging/log4j/core/StrictXmlConfigTest.java | 131 ---------
.../XmlConfigurationConstructHierarchyTest.java | 307 +++++++++++++++++++++
.../included-fragment.xml | 21 ++
.../included-with-id.xml | 26 ++
.../included.xml | 18 ++
.../nested.xml | 21 ++
.../src/test/resources/log4j-strict1.xml | 65 -----
.../log4j/core/config/xml/XmlConfiguration.java | 61 +---
src/changelog/.2.x.x/xinclude_fixup_attributes.xml | 12 +
.../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 ++++++++
13 files changed, 594 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..73c2019b6c
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java
@@ -0,0 +1,307 @@
+/*
+ * 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");
+ private static final String NESTED_URI = resourceUri("nested.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");
+ });
+ }
+
+ /**
+ * XInclude resolution is recursive: an included document may itself
contain an {@code xi:include}. The inner
+ * {@code href} is relative and must resolve against the <em>included</em>
document's own location, not the
+ * top-level configuration's.
+ */
+ @Test
+ void xIncludeResolvesNestedIncludes() throws Exception {
+ givenPlugin("Appenders");
+ givenPlugin("Console");
+ // `nested.xml` (an `<Appenders>` fragment) is included by absolute
URI and itself includes `included.xml`
+ // (a `<Console>`) by a relative href.
+ final String document = "<Configuration
xmlns:xi=\"http://www.w3.org/2001/XInclude\">" + "<xi:include href=\""
+ + NESTED_URI + "\"/></Configuration>";
+ final Element element = parse(document, true);
+
+ final Node configuration = new Node();
+ XmlConfiguration.constructHierarchy(configuration, element,
pluginManager, false);
+
+
assertThat(configuration.getChildren()).singleElement().satisfies(appenders -> {
+ assertThat(appenders.getName()).isEqualTo("Appenders");
+
assertThat(appenders.getChildren()).singleElement().satisfies(console -> {
+ assertThat(console.getName()).isEqualTo("Console");
+ assertThat(console.getAttributes()).containsEntry("name",
"STDOUT");
+ });
+ });
+ }
+}
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/XmlConfigurationConstructHierarchyTest/nested.xml
b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/nested.xml
new file mode 100644
index 0000000000..b525f1a21d
--- /dev/null
+++
b/log4j-core-test/src/test/resources/XmlConfigurationConstructHierarchyTest/nested.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.
+ -->
+<Appenders xmlns:xi="http://www.w3.org/2001/XInclude">
+ <!-- Relative href: resolves against this file's location, exercising nested
base-URI resolution. -->
+ <xi:include href="included.xml"/>
+</Appenders>
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..f205162d3a
--- /dev/null
+++ b/src/changelog/.2.x.x/xinclude_fixup_attributes.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="fixed">
+ <issue id="4155" link="https://github.com/apache/logging-log4j2/pull/4155"/>
+ <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