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