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