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