You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/03/23 10:12:14 UTC
(maven) 01/01: Normalize redundant namespace prefixes on XML attributes (fixes #11760)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix/11760-namespace-prefix-normalization
in repository https://gitbox.apache.org/repos/asf/maven.git
commit f1a365a350c73b74176d05f884a00ca5c5a1fbd4
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Mar 23 11:12:01 2026 +0100
Normalize redundant namespace prefixes on XML attributes (fixes #11760)
When a POM uses a namespace prefix that maps to the same URI as the
element's default namespace (e.g., xmlns:mvn="http://maven.apache.org/POM/4.0.0"),
attributes like mvn:combine.children are now normalized to their
unprefixed form (combine.children) at read time. The redundant xmlns
declaration is also removed.
This fixes two issues:
- combine.children/combine.self merge directives were silently ignored
when using a namespace prefix, because the merge logic only looks up
the unprefixed attribute name
- The consumer POM transformation dropped the xmlns:mvn declaration
but kept the prefixed attributes, producing invalid XML
The write side is also improved to properly handle namespace
declarations (xmlns:prefix) and prefixed attributes for foreign
namespaces, ensuring valid XML output.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../maven/internal/xml/DefaultXmlService.java | 48 +++-
.../apache/maven/internal/xml/XmlNodeImplTest.java | 243 +++++++++++++++++++++
2 files changed, 289 insertions(+), 2 deletions(-)
diff --git a/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/DefaultXmlService.java b/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/DefaultXmlService.java
index e97a221380..205d6f4061 100644
--- a/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/DefaultXmlService.java
+++ b/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/DefaultXmlService.java
@@ -106,11 +106,28 @@ private XmlNode doBuild(XMLStreamReader parser, boolean trim, InputLocationBuild
String aValue = parser.getAttributeValue(i);
String aPrefix = parser.getAttributePrefix(i);
if (aPrefix != null && !aPrefix.isEmpty()) {
- aName = aPrefix + ":" + aName;
+ // If the attribute's namespace matches the element's namespace,
+ // strip the prefix since it's redundant (e.g., mvn:combine.children
+ // where xmlns:mvn="http://maven.apache.org/POM/4.0.0" is the same
+ // as the element's default namespace)
+ String aNamespaceUri = parser.getAttributeNamespace(i);
+ if (aNamespaceUri == null
+ || !aNamespaceUri.equals(lNamespaceUri)
+ || lNamespaceUri.isEmpty()) {
+ aName = aPrefix + ":" + aName;
+ }
}
attrs.put(aName, aValue);
spacePreserve = spacePreserve || ("xml:space".equals(aName) && "preserve".equals(aValue));
}
+ // Remove namespace declarations that are redundant with the element's
+ // default namespace (e.g., xmlns:mvn when mvn maps to the same URI)
+ if (lNamespaceUri != null && !lNamespaceUri.isEmpty()) {
+ String elementNsUri = lNamespaceUri;
+ attrs.entrySet()
+ .removeIf(
+ e -> e.getKey().startsWith("xmlns:") && elementNsUri.equals(e.getValue()));
+ }
}
} else {
if (children == null) {
@@ -162,8 +179,35 @@ public void doWrite(XmlNode node, Writer writer) throws IOException {
private void writeNode(XMLStreamWriter xmlWriter, XmlNode node) throws XMLStreamException {
xmlWriter.writeStartElement(node.prefix(), node.name(), node.namespaceUri());
+ // Write namespace declarations first, then regular attributes
+ for (Map.Entry<String, String> attr : node.attributes().entrySet()) {
+ String key = attr.getKey();
+ if ("xmlns".equals(key)) {
+ xmlWriter.writeDefaultNamespace(attr.getValue());
+ } else if (key.startsWith("xmlns:")) {
+ xmlWriter.writeNamespace(key.substring(6), attr.getValue());
+ }
+ }
for (Map.Entry<String, String> attr : node.attributes().entrySet()) {
- xmlWriter.writeAttribute(attr.getKey(), attr.getValue());
+ String key = attr.getKey();
+ String value = attr.getValue();
+ if (key.startsWith("xmlns:") || "xmlns".equals(key)) {
+ continue; // already written above
+ } else if (key.startsWith("xml:")) {
+ xmlWriter.writeAttribute("http://www.w3.org/XML/1998/namespace", key.substring(4), value);
+ } else if (key.contains(":")) {
+ int colon = key.indexOf(':');
+ String prefix = key.substring(0, colon);
+ String localName = key.substring(colon + 1);
+ String nsUri = node.attributes().get("xmlns:" + prefix);
+ if (nsUri != null) {
+ xmlWriter.writeAttribute(prefix, nsUri, localName, value);
+ } else {
+ xmlWriter.writeAttribute(key, value);
+ }
+ } else {
+ xmlWriter.writeAttribute(key, value);
+ }
}
for (XmlNode child : node.children()) {
diff --git a/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlNodeImplTest.java b/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlNodeImplTest.java
index e980517194..1c9a54404c 100644
--- a/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlNodeImplTest.java
+++ b/impl/maven-xml/src/test/java/org/apache/maven/internal/xml/XmlNodeImplTest.java
@@ -36,6 +36,7 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@@ -715,6 +716,248 @@ public Object toInputLocation(XMLStreamReader parser) {
}
}
+ /**
+ * Verifies that when a POM uses a namespace prefix that maps to the same URI
+ * as the element's default namespace (e.g., xmlns:mvn="http://maven.apache.org/POM/4.0.0"),
+ * the prefix is stripped from attributes at read time and the redundant namespace
+ * declaration is removed. This ensures combine.children/combine.self directives
+ * work correctly regardless of whether they use a prefix.
+ */
+ @Test
+ void testNamespacePrefixNormalization() throws Exception {
+ String xml = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:mvn="http://maven.apache.org/POM/4.0.0">
+ <configuration>
+ <compilerArgs mvn:combine.children="append">
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </configuration>
+ </project>
+ """;
+
+ XmlNode node = toXmlNode(xml);
+
+ // The xmlns:mvn declaration should be removed (same as default namespace)
+ assertNull(node.attribute("xmlns:mvn"), "Redundant xmlns:mvn should be removed");
+
+ // The mvn:combine.children attribute on <compilerArgs> should be normalized
+ XmlNode compilerArgs = node.child("configuration").child("compilerArgs");
+ assertNotNull(compilerArgs);
+ assertEquals(
+ "append",
+ compilerArgs.attribute("combine.children"),
+ "mvn:combine.children should be normalized to combine.children");
+ assertNull(
+ compilerArgs.attribute("mvn:combine.children"),
+ "mvn:combine.children should not remain as prefixed attribute");
+ }
+
+ /**
+ * Verifies that prefixed combine.children works correctly during merge
+ * when the prefix maps to the same namespace as the default.
+ */
+ @Test
+ void testPrefixedCombineChildrenMerge() throws Exception {
+ String dominant = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:mvn="http://maven.apache.org/POM/4.0.0">
+ <compilerArgs mvn:combine.children="append">
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </project>
+ """;
+
+ String recessive = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <compilerArgs>
+ <arg>-Xlint:unchecked</arg>
+ </compilerArgs>
+ </project>
+ """;
+
+ XmlNode dominantNode = toXmlNode(dominant);
+ XmlNode recessiveNode = toXmlNode(recessive);
+ XmlNode merged = XmlService.merge(dominantNode, recessiveNode);
+
+ // With combine.children="append", both args should be present
+ XmlNode compilerArgs = merged.child("compilerArgs");
+ assertNotNull(compilerArgs);
+ assertEquals(2, compilerArgs.children().size(), "Both args should be present after append merge");
+ }
+
+ /**
+ * Verifies that namespace prefixes mapping to a different namespace than the
+ * element's default are preserved (not stripped).
+ */
+ @Test
+ void testForeignNamespacePrefixPreserved() throws Exception {
+ String xml = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:custom="http://example.com/custom">
+ <compilerArgs custom:myattr="value">
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </project>
+ """;
+
+ XmlNode node = toXmlNode(xml);
+
+ // The xmlns:custom declaration should be preserved (different namespace)
+ assertEquals("http://example.com/custom", node.attribute("xmlns:custom"));
+
+ // The custom:myattr attribute should be preserved with prefix
+ XmlNode compilerArgs = node.child("compilerArgs");
+ assertNotNull(compilerArgs);
+ assertEquals("value", compilerArgs.attribute("custom:myattr"));
+ }
+
+ /**
+ * Verifies that the write side properly handles namespace declarations
+ * and prefixed attributes, producing valid XML.
+ */
+ @Test
+ void testWriteWithNamespaceAttributes() throws Exception {
+ String xml = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:custom="http://example.com/custom">
+ <compilerArgs custom:myattr="value">
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </project>
+ """;
+
+ XmlNode node = toXmlNode(xml);
+
+ // Write and re-read to verify round-trip
+ java.io.StringWriter writer = new java.io.StringWriter();
+ XmlService.write(node, writer);
+ String output = writer.toString();
+
+ // The output should be parseable XML
+ XmlNode reRead = toXmlNode(output);
+ assertNotNull(reRead);
+
+ // The foreign namespace attribute should survive the round-trip
+ XmlNode compilerArgs = reRead.child("compilerArgs");
+ assertNotNull(compilerArgs);
+ assertEquals("value", compilerArgs.attribute("custom:myattr"));
+ }
+
+ /**
+ * Verifies that prefixed combine.self="override" works correctly when
+ * the prefix maps to the same namespace as the default.
+ */
+ @Test
+ void testPrefixedCombineSelfMerge() throws Exception {
+ String dominant = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:mvn="http://maven.apache.org/POM/4.0.0">
+ <configuration>
+ <item mvn:combine.self="override">
+ <value>dominant</value>
+ </item>
+ </configuration>
+ </project>
+ """;
+
+ String recessive = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <configuration>
+ <item>
+ <value>recessive</value>
+ <extra>should-be-dropped</extra>
+ </item>
+ </configuration>
+ </project>
+ """;
+
+ XmlNode dominantNode = toXmlNode(dominant);
+ XmlNode recessiveNode = toXmlNode(recessive);
+ XmlNode merged = XmlService.merge(dominantNode, recessiveNode);
+
+ XmlNode item = merged.child("configuration").child("item");
+ assertNotNull(item);
+ assertEquals("dominant", item.child("value").value());
+ assertNull(item.child("extra"), "combine.self=override should drop recessive children");
+ }
+
+ /**
+ * Verifies that xml:space="preserve" still works correctly after the
+ * namespace normalization changes (xml: prefix maps to a different namespace).
+ */
+ @Test
+ void testXmlSpacePreservedAfterNormalization() throws Exception {
+ String xml = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:mvn="http://maven.apache.org/POM/4.0.0">
+ <configuration>
+ <value xml:space="preserve"> spaces </value>
+ </configuration>
+ </project>
+ """;
+
+ XmlNode node = toXmlNode(xml);
+ XmlNode value = node.child("configuration").child("value");
+ assertNotNull(value);
+ assertEquals(" spaces ", value.value(), "xml:space=preserve should keep whitespace");
+ assertEquals("preserve", value.attribute("xml:space"), "xml:space attribute should be preserved");
+ }
+
+ /**
+ * Verifies that namespace prefix normalization works in nested elements
+ * where the xmlns:mvn declaration is on a parent element.
+ */
+ @Test
+ void testNestedNamespacePrefixNormalization() throws Exception {
+ String xml = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:mvn="http://maven.apache.org/POM/4.0.0">
+ <configuration>
+ <plugins>
+ <plugin>
+ <compilerArgs mvn:combine.children="append">
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </plugin>
+ </plugins>
+ </configuration>
+ </project>
+ """;
+
+ XmlNode node = toXmlNode(xml);
+ XmlNode compilerArgs =
+ node.child("configuration").child("plugins").child("plugin").child("compilerArgs");
+ assertNotNull(compilerArgs);
+ assertEquals("append", compilerArgs.attribute("combine.children"));
+ assertNull(compilerArgs.attribute("mvn:combine.children"));
+ }
+
+ /**
+ * Verifies that the normalized output round-trips correctly through
+ * write and re-read, producing the same XmlNode.
+ */
+ @Test
+ void testNormalizedRoundTrip() throws Exception {
+ String xml = """
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:mvn="http://maven.apache.org/POM/4.0.0">
+ <compilerArgs mvn:combine.children="append">
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </project>
+ """;
+
+ XmlNode node = toXmlNode(xml);
+
+ // Write and re-read
+ java.io.StringWriter writer = new java.io.StringWriter();
+ XmlService.write(node, writer);
+ XmlNode reRead = toXmlNode(writer.toString());
+
+ // The normalized attribute should survive the round-trip
+ XmlNode compilerArgs = reRead.child("compilerArgs");
+ assertNotNull(compilerArgs);
+ assertEquals("append", compilerArgs.attribute("combine.children"));
+
+ // No xmlns:mvn or mvn: prefix should appear in the output
+ String output = writer.toString();
+ assertFalse(output.contains("xmlns:mvn"), "Output should not contain redundant xmlns:mvn");
+ assertFalse(output.contains("mvn:combine"), "Output should not contain mvn: prefixed attributes");
+ }
+
public static Xpp3Dom build(Reader reader) throws XmlPullParserException, IOException {
try (Reader closeMe = reader) {
return new Xpp3Dom(XmlNodeBuilder.build(reader, true, null));