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 <[email protected]>
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 <[email protected]>
---
 .../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));

Reply via email to