View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.internal.xml;
20  
21  import javax.xml.stream.XMLStreamException;
22  
23  import java.io.Serial;
24  import java.io.Serializable;
25  import java.io.StringWriter;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.ListIterator;
31  import java.util.Map;
32  import java.util.Objects;
33  import java.util.Optional;
34  import java.util.Set;
35  import java.util.function.Function;
36  import java.util.stream.Collectors;
37  import java.util.stream.Stream;
38  
39  import org.apache.maven.api.xml.XmlNode;
40  
41  /**
42   *  NOTE: remove all the util code in here when separated, this class should be pure data.
43   */
44  public class XmlNodeImpl implements Serializable, XmlNode {
45      @Serial
46      private static final long serialVersionUID = 2567894443061173996L;
47  
48      protected final String prefix;
49  
50      protected final String namespaceUri;
51  
52      protected final String name;
53  
54      protected final String value;
55  
56      protected final Map<String, String> attributes;
57  
58      protected final List<XmlNode> children;
59  
60      protected final Object location;
61  
62      public XmlNodeImpl(String name) {
63          this(name, null, null, null, null);
64      }
65  
66      public XmlNodeImpl(String name, String value) {
67          this(name, value, null, null, null);
68      }
69  
70      public XmlNodeImpl(XmlNode from, String name) {
71          this(name, from.getValue(), from.getAttributes(), from.getChildren(), from.getInputLocation());
72      }
73  
74      public XmlNodeImpl(
75              String name, String value, Map<String, String> attributes, List<XmlNode> children, Object location) {
76          this("", "", name, value, attributes, children, location);
77      }
78  
79      public XmlNodeImpl(
80              String prefix,
81              String namespaceUri,
82              String name,
83              String value,
84              Map<String, String> attributes,
85              List<XmlNode> children,
86              Object location) {
87          this.prefix = prefix == null ? "" : prefix;
88          this.namespaceUri = namespaceUri == null ? "" : namespaceUri;
89          this.name = Objects.requireNonNull(name);
90          this.value = value;
91          this.attributes = ImmutableCollections.copy(attributes);
92          this.children = ImmutableCollections.copy(children);
93          this.location = location;
94      }
95  
96      @Override
97      public XmlNode merge(XmlNode source, Boolean childMergeOverride) {
98          return merge(this, source, childMergeOverride);
99      }
100 
101     // ----------------------------------------------------------------------
102     // Name handling
103     // ----------------------------------------------------------------------
104 
105     @Override
106     public String getPrefix() {
107         return prefix;
108     }
109 
110     @Override
111     public String getNamespaceUri() {
112         return namespaceUri;
113     }
114 
115     @Override
116     public String getName() {
117         return name;
118     }
119 
120     // ----------------------------------------------------------------------
121     // Value handling
122     // ----------------------------------------------------------------------
123 
124     public String getValue() {
125         return value;
126     }
127 
128     // ----------------------------------------------------------------------
129     // Attribute handling
130     // ----------------------------------------------------------------------
131 
132     @Override
133     public Map<String, String> getAttributes() {
134         return attributes;
135     }
136 
137     public String getAttribute(String name) {
138         return attributes.get(name);
139     }
140 
141     // ----------------------------------------------------------------------
142     // Child handling
143     // ----------------------------------------------------------------------
144 
145     public XmlNode getChild(String name) {
146         if (name != null) {
147             ListIterator<XmlNode> it = children.listIterator(children.size());
148             while (it.hasPrevious()) {
149                 XmlNode child = it.previous();
150                 if (name.equals(child.getName())) {
151                     return child;
152                 }
153             }
154         }
155         return null;
156     }
157 
158     public List<XmlNode> getChildren() {
159         return children;
160     }
161 
162     public int getChildCount() {
163         return children.size();
164     }
165 
166     // ----------------------------------------------------------------------
167     // Input location handling
168     // ----------------------------------------------------------------------
169 
170     /**
171      * @since 3.2.0
172      * @return input location
173      */
174     public Object getInputLocation() {
175         return location;
176     }
177 
178     // ----------------------------------------------------------------------
179     // Helpers
180     // ----------------------------------------------------------------------
181 
182     /**
183      * Merges one DOM into another, given a specific algorithm and possible override points for that algorithm.<p>
184      * The algorithm is as follows:
185      * <ol>
186      * <li> if the recessive DOM is null, there is nothing to do... return.</li>
187      * <li> Determine whether the dominant node will suppress the recessive one (flag=mergeSelf).
188      *   <ol type="A">
189      *   <li> retrieve the 'combine.self' attribute on the dominant node, and try to match against 'override'...
190      *        if it matches 'override', then set mergeSelf == false...the dominant node suppresses the recessive one
191      *        completely.</li>
192      *   <li> otherwise, use the default value for mergeSelf, which is true...this is the same as specifying
193      *        'combine.self' == 'merge' as an attribute of the dominant root node.</li>
194      *   </ol></li>
195      * <li> If mergeSelf == true
196      *   <ol type="A">
197      *   <li> Determine whether children from the recessive DOM will be merged or appended to the dominant DOM as
198      *        siblings (flag=mergeChildren).
199      *     <ol type="i">
200      *     <li> if childMergeOverride is set (non-null), use that value (true/false)</li>
201      *     <li> retrieve the 'combine.children' attribute on the dominant node, and try to match against
202      *          'append'...</li>
203      *     <li> if it matches 'append', then set mergeChildren == false...the recessive children will be appended as
204      *          siblings of the dominant children.</li>
205      *     <li> otherwise, use the default value for mergeChildren, which is true...this is the same as specifying
206      *         'combine.children' == 'merge' as an attribute on the dominant root node.</li>
207      *     </ol></li>
208      *   <li> Iterate through the recessive children, and:
209      *     <ol type="i">
210      *     <li> if mergeChildren == true and there is a corresponding dominant child (matched by element name),
211      *          merge the two.</li>
212      *     <li> otherwise, add the recessive child as a new child on the dominant root node.</li>
213      *     </ol></li>
214      *   </ol></li>
215      * </ol>
216      */
217     @SuppressWarnings("checkstyle:MethodLength")
218     public static XmlNode merge(XmlNode dominant, XmlNode recessive, Boolean childMergeOverride) {
219         // TODO: share this as some sort of assembler, implement a walk interface?
220         if (recessive == null) {
221             return dominant;
222         }
223         if (dominant == null) {
224             return recessive;
225         }
226 
227         boolean mergeSelf = true;
228 
229         String selfMergeMode = getSelfCombinationMode(dominant);
230 
231         if (SELF_COMBINATION_OVERRIDE.equals(selfMergeMode)) {
232             mergeSelf = false;
233         }
234 
235         if (mergeSelf) {
236 
237             String value = dominant.getValue();
238             Object location = dominant.getInputLocation();
239             Map<String, String> attrs = dominant.getAttributes();
240             List<XmlNode> children = null;
241 
242             for (Map.Entry<String, String> attr : recessive.getAttributes().entrySet()) {
243                 String key = attr.getKey();
244                 if (isEmpty(attrs.get(key))) {
245                     if (attrs == dominant.getAttributes()) {
246                         attrs = new HashMap<>(attrs);
247                     }
248                     attrs.put(key, attr.getValue());
249                 }
250             }
251 
252             if (!recessive.getChildren().isEmpty()) {
253                 boolean mergeChildren = true;
254                 if (childMergeOverride != null) {
255                     mergeChildren = childMergeOverride;
256                 } else {
257                     String childCombinationMode = getChildCombinationMode(attrs);
258                     if (CHILDREN_COMBINATION_APPEND.equals(childCombinationMode)) {
259                         mergeChildren = false;
260                     }
261                 }
262 
263                 Map<String, Iterator<XmlNode>> commonChildren = new HashMap<>();
264                 Set<String> names =
265                         recessive.getChildren().stream().map(XmlNode::getName).collect(Collectors.toSet());
266                 for (String name : names) {
267                     List<XmlNode> dominantChildren = dominant.getChildren().stream()
268                             .filter(n -> n.getName().equals(name))
269                             .toList();
270                     if (!dominantChildren.isEmpty()) {
271                         commonChildren.put(name, dominantChildren.iterator());
272                     }
273                 }
274 
275                 String keysValue = recessive.getAttribute(KEYS_COMBINATION_MODE_ATTRIBUTE);
276 
277                 int recessiveChildIndex = 0;
278                 for (XmlNode recessiveChild : recessive.getChildren()) {
279                     String idValue = recessiveChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE);
280 
281                     XmlNode childDom = null;
282                     if (!isEmpty(idValue)) {
283                         for (XmlNode dominantChild : dominant.getChildren()) {
284                             if (idValue.equals(dominantChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE))) {
285                                 childDom = dominantChild;
286                                 // we have a match, so don't append but merge
287                                 mergeChildren = true;
288                             }
289                         }
290                     } else if (!isEmpty(keysValue)) {
291                         String[] keys = keysValue.split(",");
292                         Map<String, Optional<String>> recessiveKeyValues = Stream.of(keys)
293                                 .collect(Collectors.toMap(
294                                         k -> k, k -> Optional.ofNullable(recessiveChild.getAttribute(k))));
295 
296                         for (XmlNode dominantChild : dominant.getChildren()) {
297                             Map<String, Optional<String>> dominantKeyValues = Stream.of(keys)
298                                     .collect(Collectors.toMap(
299                                             k -> k, k -> Optional.ofNullable(dominantChild.getAttribute(k))));
300 
301                             if (recessiveKeyValues.equals(dominantKeyValues)) {
302                                 childDom = dominantChild;
303                                 // we have a match, so don't append but merge
304                                 mergeChildren = true;
305                             }
306                         }
307                     } else {
308                         childDom = dominant.getChild(recessiveChild.getName());
309                     }
310 
311                     if (mergeChildren && childDom != null) {
312                         String name = recessiveChild.getName();
313                         Iterator<XmlNode> it =
314                                 commonChildren.computeIfAbsent(name, n1 -> Stream.of(dominant.getChildren().stream()
315                                                 .filter(n2 -> n2.getName().equals(n1))
316                                                 .collect(Collectors.toList()))
317                                         .filter(l -> !l.isEmpty())
318                                         .map(List::iterator)
319                                         .findFirst()
320                                         .orElse(null));
321                         if (it == null) {
322                             if (children == null) {
323                                 children = new ArrayList<>(dominant.getChildren());
324                             }
325                             children.add(recessiveChild);
326                         } else if (it.hasNext()) {
327                             XmlNode dominantChild = it.next();
328 
329                             String dominantChildCombinationMode = getSelfCombinationMode(dominantChild);
330                             if (SELF_COMBINATION_REMOVE.equals(dominantChildCombinationMode)) {
331                                 if (children == null) {
332                                     children = new ArrayList<>(dominant.getChildren());
333                                 }
334                                 children.remove(dominantChild);
335                             } else {
336                                 int idx = dominant.getChildren().indexOf(dominantChild);
337                                 XmlNode merged = merge(dominantChild, recessiveChild, childMergeOverride);
338                                 if (merged != dominantChild) {
339                                     if (children == null) {
340                                         children = new ArrayList<>(dominant.getChildren());
341                                     }
342                                     children.set(idx, merged);
343                                 }
344                             }
345                         }
346                     } else {
347                         if (children == null) {
348                             children = new ArrayList<>(dominant.getChildren());
349                         }
350                         int idx = mergeChildren ? children.size() : recessiveChildIndex;
351                         children.add(idx, recessiveChild);
352                     }
353                     recessiveChildIndex++;
354                 }
355             }
356 
357             if (value != null || attrs != dominant.getAttributes() || children != null) {
358                 if (children == null) {
359                     children = dominant.getChildren();
360                 }
361                 if (!Objects.equals(value, dominant.getValue())
362                         || !Objects.equals(attrs, dominant.getAttributes())
363                         || !Objects.equals(children, dominant.getChildren())
364                         || !Objects.equals(location, dominant.getInputLocation())) {
365                     return new XmlNodeImpl(
366                             dominant.getName(), value != null ? value : dominant.getValue(), attrs, children, location);
367                 } else {
368                     return dominant;
369                 }
370             }
371         }
372         return dominant;
373     }
374 
375     private static String getChildCombinationMode(Map<String, String> attrs) {
376         String attribute = attrs.get(CHILDREN_COMBINATION_MODE_ATTRIBUTE);
377         if (CHILDREN_COMBINATION_APPEND.equals(attribute) || CHILDREN_COMBINATION_MERGE.equals(attribute)) {
378             return attribute;
379         }
380         return DEFAULT_CHILDREN_COMBINATION_MODE;
381     }
382 
383     private static String getSelfCombinationMode(XmlNode node) {
384         String attribute = node.getAttribute(SELF_COMBINATION_MODE_ATTRIBUTE);
385         if (SELF_COMBINATION_OVERRIDE.equals(attribute)
386                 || SELF_COMBINATION_MERGE.equals(attribute)
387                 || SELF_COMBINATION_REMOVE.equals(attribute)) {
388             return attribute;
389         }
390         return DEFAULT_SELF_COMBINATION_MODE;
391     }
392 
393     /**
394      * Merge two DOMs, with one having dominance in the case of collision. Merge mechanisms (vs. override for nodes, or
395      * vs. append for children) is determined by attributes of the dominant root node.
396      *
397      * @see #CHILDREN_COMBINATION_MODE_ATTRIBUTE
398      * @see #SELF_COMBINATION_MODE_ATTRIBUTE
399      * @param dominant The dominant DOM into which the recessive value/attributes/children will be merged
400      * @param recessive The recessive DOM, which will be merged into the dominant DOM
401      * @return merged DOM
402      */
403     public static XmlNode merge(XmlNode dominant, XmlNode recessive) {
404         return merge(dominant, recessive, null);
405     }
406 
407     // ----------------------------------------------------------------------
408     // Standard object handling
409     // ----------------------------------------------------------------------
410 
411     @Override
412     public boolean equals(Object o) {
413         if (this == o) {
414             return true;
415         }
416         if (o == null || getClass() != o.getClass()) {
417             return false;
418         }
419         XmlNodeImpl that = (XmlNodeImpl) o;
420         return Objects.equals(this.name, that.name)
421                 && Objects.equals(this.value, that.value)
422                 && Objects.equals(this.attributes, that.attributes)
423                 && Objects.equals(this.children, that.children);
424     }
425 
426     @Override
427     public int hashCode() {
428         return Objects.hash(name, value, attributes, children);
429     }
430 
431     @Override
432     public String toString() {
433         try {
434             return toStringXml();
435         } catch (XMLStreamException e) {
436             return toStringObject();
437         }
438     }
439 
440     public String toStringXml() throws XMLStreamException {
441         StringWriter writer = new StringWriter();
442         XmlNodeWriter.write(writer, this);
443         return writer.toString();
444     }
445 
446     public String toStringObject() {
447         StringBuilder sb = new StringBuilder();
448         sb.append("XmlNode[");
449         boolean w = false;
450         w = addToStringField(sb, prefix, o -> !o.isEmpty(), "prefix", w);
451         w = addToStringField(sb, namespaceUri, o -> !o.isEmpty(), "namespaceUri", w);
452         w = addToStringField(sb, name, o -> !o.isEmpty(), "name", w);
453         w = addToStringField(sb, value, o -> !o.isEmpty(), "value", w);
454         w = addToStringField(sb, attributes, o -> !o.isEmpty(), "attributes", w);
455         w = addToStringField(sb, children, o -> !o.isEmpty(), "children", w);
456         w = addToStringField(sb, location, Objects::nonNull, "location", w);
457         sb.append("]");
458         return sb.toString();
459     }
460 
461     private static <T> boolean addToStringField(StringBuilder sb, T o, Function<T, Boolean> p, String n, boolean w) {
462         if (!p.apply(o)) {
463             if (w) {
464                 sb.append(", ");
465             } else {
466                 w = true;
467             }
468             sb.append(n).append("='").append(o).append('\'');
469         }
470         return w;
471     }
472 
473     private static boolean isEmpty(String str) {
474         return str == null || str.isEmpty();
475     }
476 }