diff --git a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/Dom.java b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/Dom.java
index c796ff7110ad..f72d05591f71 100644
--- a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/Dom.java
+++ b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/Dom.java
@@ -57,6 +57,17 @@ public interface Dom {
String SELF_COMBINATION_REMOVE = "remove";
+ /**
+ * In case of complex XML structures, combining can be done based on id.
+ */
+ String ID_COMBINATION_MODE_ATTRIBUTE = "combine.id";
+
+ /**
+ * In case of complex XML structures, combining can be done based on keys.
+ * This is a comma separated list of attribute names.
+ */
+ String KEYS_COMBINATION_MODE_ATTRIBUTE = "combine.keys";
+
/**
* This default mode for combining a DOM node during merge means that where element names match, the process will
* try to merge the element attributes and values, rather than overriding the recessive element completely with the
diff --git a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3Dom.java b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3Dom.java
index 933a673664a4..4373f3988af8 100644
--- a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3Dom.java
+++ b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3Dom.java
@@ -29,6 +29,7 @@
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -180,8 +181,6 @@ public void writeToSerializer(String namespace, XmlSerializer serializer) throws
*
*
If mergeSelf == true
*
- * - if the dominant root node's value is empty, set it to the recessive root node's value
- * - For each attribute in the recessive root node which is not set in the dominant root node, set it.
* - Determine whether children from the recessive DOM will be merged or appended to the dominant DOM as
* siblings (flag=mergeChildren).
*
@@ -222,16 +221,11 @@ public static Dom merge(Dom dominant, Dom recessive, Boolean childMergeOverride)
if (mergeSelf) {
- String value = null;
- Object location = null;
+ String value = dominant.getValue();
+ Object location = dominant.getInputLocation();
Map attrs = null;
List children = null;
- if (isEmpty(dominant.getValue()) && !isEmpty(recessive.getValue())) {
- value = recessive.getValue();
- location = recessive.getInputLocation();
- }
-
for (Map.Entry attr : recessive.getAttributes().entrySet()) {
String key = attr.getKey();
if (isEmpty(dominant.getAttribute(key)) && !SELF_COMBINATION_MODE_ATTRIBUTE.equals(key)) {
@@ -253,25 +247,55 @@ public static Dom merge(Dom dominant, Dom recessive, Boolean childMergeOverride)
}
}
- if (!mergeChildren) {
- children = new ArrayList<>(recessive.getChildren().size()
- + dominant.getChildren().size());
- children.addAll(recessive.getChildren());
- children.addAll(dominant.getChildren());
- } else {
- Map> commonChildren = new HashMap<>();
- Set names =
- recessive.getChildren().stream().map(Dom::getName).collect(Collectors.toSet());
- for (String name : names) {
- List dominantChildren = dominant.getChildren().stream()
- .filter(n -> n.getName().equals(name))
- .collect(Collectors.toList());
- if (dominantChildren.size() > 0) {
- commonChildren.put(name, dominantChildren.iterator());
+ String keysValue = recessive.getAttribute(KEYS_COMBINATION_MODE_ATTRIBUTE);
+
+ for (Dom recessiveChild : recessive.getChildren()) {
+ String idValue = recessiveChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE);
+
+ Dom childDom = null;
+ if (isNotEmpty(idValue)) {
+ for (Dom dominantChild : dominant.getChildren()) {
+ if (idValue.equals(dominantChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE))) {
+ childDom = dominantChild;
+ // we have a match, so don't append but merge
+ mergeChildren = true;
+ }
}
+ } else if (isNotEmpty(keysValue)) {
+ String[] keys = keysValue.split(",");
+ Map> recessiveKeyValues = Stream.of(keys)
+ .collect(Collectors.toMap(
+ k -> k, k -> Optional.ofNullable(recessiveChild.getAttribute(k))));
+
+ for (Dom dominantChild : dominant.getChildren()) {
+ Map> dominantKeyValues = Stream.of(keys)
+ .collect(Collectors.toMap(
+ k -> k, k -> Optional.ofNullable(dominantChild.getAttribute(k))));
+
+ if (recessiveKeyValues.equals(dominantKeyValues)) {
+ childDom = dominantChild;
+ // we have a match, so don't append but merge
+ mergeChildren = true;
+ }
+ }
+ } else {
+ childDom = dominant.getChild(recessiveChild.getName());
}
- for (Dom recessiveChild : recessive.getChildren()) {
+ if (mergeChildren && childDom != null) {
+ Map> commonChildren = new HashMap<>();
+ Set names = recessive.getChildren().stream()
+ .map(Dom::getName)
+ .collect(Collectors.toSet());
+ for (String name : names) {
+ List dominantChildren = dominant.getChildren().stream()
+ .filter(n -> n.getName().equals(name))
+ .collect(Collectors.toList());
+ if (dominantChildren.size() > 0) {
+ commonChildren.put(name, dominantChildren.iterator());
+ }
+ }
+
String name = recessiveChild.getName();
Iterator it =
commonChildren.computeIfAbsent(name, n1 -> Stream.of(dominant.getChildren().stream()
@@ -297,7 +321,7 @@ public static Dom merge(Dom dominant, Dom recessive, Boolean childMergeOverride)
}
children.remove(dominantChild);
} else {
- int idx = (children != null ? children : dominant.getChildren()).indexOf(dominantChild);
+ int idx = dominant.getChildren().indexOf(dominantChild);
Dom merged = merge(dominantChild, recessiveChild, childMergeOverride);
if (merged != dominantChild) {
if (children == null) {
@@ -307,6 +331,14 @@ public static Dom merge(Dom dominant, Dom recessive, Boolean childMergeOverride)
}
}
}
+ } else {
+ if (children == null) {
+ children = new ArrayList<>(dominant.getChildren());
+ }
+ int idx = mergeChildren
+ ? children.size()
+ : recessive.getChildren().indexOf(recessiveChild);
+ children.add(idx, recessiveChild);
}
}
}
@@ -381,11 +413,11 @@ public String toUnescapedString() {
return writer.toString();
}
- public static boolean isNotEmpty(String str) {
+ private static boolean isNotEmpty(String str) {
return ((str != null) && (str.length() > 0));
}
- public static boolean isEmpty(String str) {
- return ((str == null) || (str.trim().length() == 0));
+ private static boolean isEmpty(String str) {
+ return ((str == null) || (str.length() == 0));
}
}
diff --git a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3DomBuilder.java b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3DomBuilder.java
index 4b7917528195..077efca5b655 100644
--- a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3DomBuilder.java
+++ b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/Xpp3DomBuilder.java
@@ -129,8 +129,10 @@ public static Xpp3Dom build(XmlPullParser parser, boolean trim, InputLocationBui
Map attrs = null;
List children = null;
int eventType = parser.getEventType();
+ boolean emptyTag = false;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
+ emptyTag = parser.isEmptyElementTag();
if (name == null) {
name = parser.getName();
location = locationBuilder != null ? locationBuilder.toInputLocation(parser) : null;
@@ -158,7 +160,12 @@ public static Xpp3Dom build(XmlPullParser parser, boolean trim, InputLocationBui
}
value = value != null ? value + text : text;
} else if (eventType == XmlPullParser.END_TAG) {
- return new Xpp3Dom(name, children == null ? value : null, attrs, children, location);
+ return new Xpp3Dom(
+ name,
+ children == null ? (value != null ? value : emptyTag ? null : "") : null,
+ attrs,
+ children,
+ location);
}
eventType = parser.next();
}
diff --git a/maven-xml-impl/src/test/java/org/apache/maven/internal/xml/Xpp3DomTest.java b/maven-xml-impl/src/test/java/org/apache/maven/internal/xml/Xpp3DomTest.java
new file mode 100644
index 000000000000..4abd156cc8e9
--- /dev/null
+++ b/maven-xml-impl/src/test/java/org/apache/maven/internal/xml/Xpp3DomTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.maven.internal.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.maven.api.xml.Dom;
+import org.codehaus.plexus.util.xml.pull.XmlPullParser;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+import org.junit.jupiter.api.Test;
+
+public class Xpp3DomTest {
+
+ /**
+ * testCombineId.
+ *
+ * @throws java.lang.Exception if any.
+ */
+ @Test
+ public void testCombineId() throws Exception {
+ String lhs = "" + "LHS-ONLYLHS"
+ + "TOOVERWRITELHS"
+ + "";
+
+ String rhs = "" + "RHS-ONLYRHS"
+ + "TOOVERWRITERHS"
+ + "";
+
+ Xpp3Dom leftDom = Xpp3DomBuilder.build(new StringReader(lhs), new FixedInputLocationBuilder("left"));
+ Xpp3Dom rightDom = Xpp3DomBuilder.build(new StringReader(rhs), new FixedInputLocationBuilder("right"));
+
+ Dom mergeResult = Xpp3Dom.merge(leftDom, rightDom, true);
+ assertEquals(3, getChildren(mergeResult, "property").size());
+
+ Dom p0 = getNthChild(mergeResult, "property", 0);
+ assertEquals("LHS-ONLY", p0.getChild("name").getValue());
+ assertEquals("left", p0.getChild("name").getInputLocation());
+ assertEquals("LHS", p0.getChild("value").getValue());
+ assertEquals("left", p0.getChild("value").getInputLocation());
+
+ Dom p1 = getNthChild(mergeResult, "property", 1);
+ assertEquals(
+ "TOOVERWRITE",
+ getNthChild(mergeResult, "property", 1).getChild("name").getValue());
+ assertEquals("left", p1.getChild("name").getInputLocation());
+ assertEquals(
+ "LHS", getNthChild(mergeResult, "property", 1).getChild("value").getValue());
+ assertEquals("left", p1.getChild("value").getInputLocation());
+
+ Dom p2 = getNthChild(mergeResult, "property", 2);
+ assertEquals(
+ "RHS-ONLY",
+ getNthChild(mergeResult, "property", 2).getChild("name").getValue());
+ assertEquals("right", p2.getChild("name").getInputLocation());
+ assertEquals(
+ "RHS", getNthChild(mergeResult, "property", 2).getChild("value").getValue());
+ assertEquals("right", p2.getChild("value").getInputLocation());
+ }
+
+ /**
+ * testCombineKeys.
+ *
+ * @throws java.lang.Exception if any.
+ */
+ @Test
+ public void testCombineKeys() throws Exception {
+ String lhs = ""
+ + "LHS-ONLYLHS"
+ + "TOOVERWRITELHS" + "";
+
+ String rhs = ""
+ + "RHS-ONLYRHS"
+ + "TOOVERWRITERHS" + "";
+
+ Xpp3Dom leftDom = Xpp3DomBuilder.build(new StringReader(lhs), new FixedInputLocationBuilder("left"));
+ Xpp3Dom rightDom = Xpp3DomBuilder.build(new StringReader(rhs), new FixedInputLocationBuilder("right"));
+
+ Dom mergeResult = Xpp3Dom.merge(leftDom, rightDom, true);
+ assertEquals(3, getChildren(mergeResult, "property").size());
+
+ Dom p0 = getNthChild(mergeResult, "property", 0);
+ assertEquals("LHS-ONLY", p0.getChild("name").getValue());
+ assertEquals("left", p0.getChild("name").getInputLocation());
+ assertEquals("LHS", p0.getChild("value").getValue());
+ assertEquals("left", p0.getChild("value").getInputLocation());
+
+ Dom p1 = getNthChild(mergeResult, "property", 1);
+ assertEquals(
+ "TOOVERWRITE",
+ getNthChild(mergeResult, "property", 1).getChild("name").getValue());
+ assertEquals("left", p1.getChild("name").getInputLocation());
+ assertEquals(
+ "LHS", getNthChild(mergeResult, "property", 1).getChild("value").getValue());
+ assertEquals("left", p1.getChild("value").getInputLocation());
+
+ Dom p2 = getNthChild(mergeResult, "property", 2);
+ assertEquals(
+ "RHS-ONLY",
+ getNthChild(mergeResult, "property", 2).getChild("name").getValue());
+ assertEquals("right", p2.getChild("name").getInputLocation());
+ assertEquals(
+ "RHS", getNthChild(mergeResult, "property", 2).getChild("value").getValue());
+ assertEquals("right", p2.getChild("value").getInputLocation());
+ }
+
+ @Test
+ public void testPreserveDominantBlankValue() throws XmlPullParserException, IOException {
+ String lhs = " ";
+
+ String rhs = "recessive";
+
+ Xpp3Dom leftDom = Xpp3DomBuilder.build(new StringReader(lhs), new FixedInputLocationBuilder("left"));
+ Xpp3Dom rightDom = Xpp3DomBuilder.build(new StringReader(rhs), new FixedInputLocationBuilder("right"));
+
+ Dom mergeResult = Xpp3Dom.merge(leftDom, rightDom, true);
+ assertEquals(" ", mergeResult.getValue());
+ }
+
+ @Test
+ public void testPreserveDominantEmptyNode() throws XmlPullParserException, IOException {
+ String lhs = "";
+
+ String rhs = "recessive";
+
+ Xpp3Dom leftDom = Xpp3DomBuilder.build(new StringReader(lhs), new FixedInputLocationBuilder("left"));
+ Xpp3Dom rightDom = Xpp3DomBuilder.build(new StringReader(rhs), new FixedInputLocationBuilder("right"));
+
+ Dom mergeResult = Xpp3Dom.merge(leftDom, rightDom, true);
+ assertEquals("", mergeResult.getValue());
+ }
+
+ @Test
+ public void testPreserveDominantEmptyNode2() throws XmlPullParserException, IOException {
+ String lhs = "";
+
+ String rhs = "recessive";
+
+ Xpp3Dom leftDom = Xpp3DomBuilder.build(new StringReader(lhs), new FixedInputLocationBuilder("left"));
+ Xpp3Dom rightDom = Xpp3DomBuilder.build(new StringReader(rhs), new FixedInputLocationBuilder("right"));
+
+ Dom mergeResult = Xpp3Dom.merge(leftDom, rightDom, true);
+ assertEquals(null, mergeResult.getValue());
+ }
+
+ private static List getChildren(Dom node, String name) {
+ return node.getChildren().stream().filter(n -> n.getName().equals(name)).collect(Collectors.toList());
+ }
+
+ private static Dom getNthChild(Dom node, String name, int nth) {
+ return node.getChildren().stream()
+ .filter(n -> n.getName().equals(name))
+ .skip(nth)
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static class FixedInputLocationBuilder implements Xpp3DomBuilder.InputLocationBuilder {
+ private final Object location;
+
+ public FixedInputLocationBuilder(Object location) {
+ this.location = location;
+ }
+
+ public Object toInputLocation(XmlPullParser parser) {
+ return location;
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 7bc7aff379ab..568cd01bf657 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,7 +157,7 @@ under the License.
2.1.0
1.26
4.0.0-alpha-3-SNAPSHOT
- 3.4.2
+ 3.5.0
5.1.0
30.1-jre
1.0.1