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.codehaus.plexus.util.xml;
20  
21  import java.io.IOException;
22  import java.io.Serializable;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import org.apache.maven.api.xml.Dom;
28  import org.codehaus.plexus.util.StringUtils;
29  import org.codehaus.plexus.util.xml.pull.XmlSerializer;
30  
31  /**
32   *  NOTE: remove all the util code in here when separated, this class should be pure data.
33   */
34  public class Xpp3Dom implements Serializable {
35      private static final String[] EMPTY_STRING_ARRAY = new String[0];
36  
37      private static final Xpp3Dom[] EMPTY_DOM_ARRAY = new Xpp3Dom[0];
38  
39      public static final String CHILDREN_COMBINATION_MODE_ATTRIBUTE = "combine.children";
40  
41      public static final String CHILDREN_COMBINATION_MERGE = "merge";
42  
43      public static final String CHILDREN_COMBINATION_APPEND = "append";
44  
45      /**
46       * This default mode for combining children DOMs during merge means that where element names match, the process will
47       * try to merge the element data, rather than putting the dominant and recessive elements (which share the same
48       * element name) as siblings in the resulting DOM.
49       */
50      public static final String DEFAULT_CHILDREN_COMBINATION_MODE = CHILDREN_COMBINATION_MERGE;
51  
52      public static final String SELF_COMBINATION_MODE_ATTRIBUTE = "combine.self";
53  
54      public static final String SELF_COMBINATION_OVERRIDE = "override";
55  
56      public static final String SELF_COMBINATION_MERGE = "merge";
57  
58      public static final String SELF_COMBINATION_REMOVE = "remove";
59  
60      /**
61       * This default mode for combining a DOM node during merge means that where element names match, the process will
62       * try to merge the element attributes and values, rather than overriding the recessive element completely with the
63       * dominant one. This means that wherever the dominant element doesn't provide the value or a particular attribute,
64       * that value or attribute will be set from the recessive DOM node.
65       */
66      public static final String DEFAULT_SELF_COMBINATION_MODE = SELF_COMBINATION_MERGE;
67  
68      private ChildrenTracking childrenTracking;
69      private Dom dom;
70  
71      public Xpp3Dom(String name) {
72          this.dom = new org.apache.maven.internal.xml.Xpp3Dom(name);
73      }
74  
75      /**
76       * @since 3.2.0
77       * @param inputLocation The input location.
78       * @param name The name of the Dom.
79       */
80      public Xpp3Dom(String name, Object inputLocation) {
81          this.dom = new org.apache.maven.internal.xml.Xpp3Dom(name, null, null, null, inputLocation);
82      }
83  
84      /**
85       * Copy constructor.
86       * @param src The source Dom.
87       */
88      public Xpp3Dom(Xpp3Dom src) {
89          this(src, src.getName());
90      }
91  
92      /**
93       * Copy constructor with alternative name.
94       * @param src The source Dom.
95       * @param name The name of the Dom.
96       */
97      public Xpp3Dom(Xpp3Dom src, String name) {
98          this.dom = new org.apache.maven.internal.xml.Xpp3Dom(src.dom, name);
99      }
100 
101     public Xpp3Dom(Dom dom) {
102         this.dom = dom;
103     }
104 
105     public Xpp3Dom(Dom dom, Xpp3Dom parent) {
106         this.dom = dom;
107         this.childrenTracking = parent::replace;
108     }
109 
110     public Xpp3Dom(Dom dom, ChildrenTracking childrenTracking) {
111         this.dom = dom;
112         this.childrenTracking = childrenTracking;
113     }
114 
115     public Dom getDom() {
116         return dom;
117     }
118 
119     // ----------------------------------------------------------------------
120     // Name handling
121     // ----------------------------------------------------------------------
122 
123     public String getName() {
124         return dom.getName();
125     }
126 
127     // ----------------------------------------------------------------------
128     // Value handling
129     // ----------------------------------------------------------------------
130 
131     public String getValue() {
132         return dom.getValue();
133     }
134 
135     public void setValue(String value) {
136         update(new org.apache.maven.internal.xml.Xpp3Dom(
137                 dom.getName(), value, dom.getAttributes(), dom.getChildren(), dom.getInputLocation()));
138     }
139 
140     // ----------------------------------------------------------------------
141     // Attribute handling
142     // ----------------------------------------------------------------------
143 
144     public String[] getAttributeNames() {
145         return dom.getAttributes().keySet().toArray(EMPTY_STRING_ARRAY);
146     }
147 
148     public String getAttribute(String name) {
149         return dom.getAttribute(name);
150     }
151 
152     /**
153      *
154      * @param name name of the attribute to be removed
155      * @return <code>true</code> if the attribute has been removed
156      * @since 3.4.0
157      */
158     public boolean removeAttribute(String name) {
159         if (!StringUtils.isEmpty(name)) {
160             Map<String, String> attrs = new HashMap<>(dom.getAttributes());
161             boolean ret = attrs.remove(name) != null;
162             if (ret) {
163                 update(new org.apache.maven.internal.xml.Xpp3Dom(
164                         dom.getName(), dom.getValue(), attrs, dom.getChildren(), dom.getInputLocation()));
165             }
166             return ret;
167         }
168         return false;
169     }
170 
171     /**
172      * Set the attribute value
173      *
174      * @param name String not null
175      * @param value String not null
176      */
177     public void setAttribute(String name, String value) {
178         if (null == value) {
179             throw new NullPointerException("Attribute value can not be null");
180         }
181         if (null == name) {
182             throw new NullPointerException("Attribute name can not be null");
183         }
184         Map<String, String> attrs = new HashMap<>(dom.getAttributes());
185         attrs.put(name, value);
186         update(new org.apache.maven.internal.xml.Xpp3Dom(
187                 dom.getName(), dom.getValue(), attrs, dom.getChildren(), dom.getInputLocation()));
188     }
189 
190     // ----------------------------------------------------------------------
191     // Child handling
192     // ----------------------------------------------------------------------
193 
194     public Xpp3Dom getChild(int i) {
195         return new Xpp3Dom(dom.getChildren().get(i), this);
196     }
197 
198     public Xpp3Dom getChild(String name) {
199         Dom child = dom.getChild(name);
200         return child != null ? new Xpp3Dom(child, this) : null;
201     }
202 
203     public void addChild(Xpp3Dom xpp3Dom) {
204         List<Dom> children = new ArrayList<>(dom.getChildren());
205         children.add(xpp3Dom.dom);
206         xpp3Dom.childrenTracking = this::replace;
207         update(new org.apache.maven.internal.xml.Xpp3Dom(
208                 dom.getName(), dom.getValue(), dom.getAttributes(), children, dom.getInputLocation()));
209     }
210 
211     public Xpp3Dom[] getChildren() {
212         return dom.getChildren().stream().map(d -> new Xpp3Dom(d, this)).toArray(Xpp3Dom[]::new);
213     }
214 
215     public Xpp3Dom[] getChildren(String name) {
216         return dom.getChildren().stream()
217                 .filter(c -> c.getName().equals(name))
218                 .map(d -> new Xpp3Dom(d, this))
219                 .toArray(Xpp3Dom[]::new);
220     }
221 
222     public int getChildCount() {
223         return dom.getChildren().size();
224     }
225 
226     public void removeChild(int i) {
227         List<Dom> children = new ArrayList<>(dom.getChildren());
228         children.remove(i);
229         update(new org.apache.maven.internal.xml.Xpp3Dom(
230                 dom.getName(), dom.getValue(), dom.getAttributes(), children, dom.getInputLocation()));
231     }
232 
233     public void removeChild(Xpp3Dom child) {
234         List<Dom> children = new ArrayList<>(dom.getChildren());
235         children.remove(child.dom);
236         update(new org.apache.maven.internal.xml.Xpp3Dom(
237                 dom.getName(), dom.getValue(), dom.getAttributes(), children, dom.getInputLocation()));
238     }
239 
240     // ----------------------------------------------------------------------
241     // Parent handling
242     // ----------------------------------------------------------------------
243 
244     public Xpp3Dom getParent() {
245         throw new UnsupportedOperationException();
246     }
247 
248     public void setParent(Xpp3Dom parent) {}
249 
250     // ----------------------------------------------------------------------
251     // Input location handling
252     // ----------------------------------------------------------------------
253 
254     /**
255      * @since 3.2.0
256      * @return input location
257      */
258     public Object getInputLocation() {
259         return dom.getInputLocation();
260     }
261 
262     /**
263      * @since 3.2.0
264      * @param inputLocation input location to set
265      */
266     public void setInputLocation(Object inputLocation) {
267         update(new org.apache.maven.internal.xml.Xpp3Dom(
268                 dom.getName(), dom.getValue(), dom.getAttributes(), dom.getChildren(), inputLocation));
269     }
270 
271     // ----------------------------------------------------------------------
272     // Helpers
273     // ----------------------------------------------------------------------
274 
275     public void writeToSerializer(String namespace, XmlSerializer serializer) throws IOException {
276         // TODO: WARNING! Later versions of plexus-utils psit out an <?xml ?> header due to thinking this is a new
277         // document - not the desired behaviour!
278         SerializerXMLWriter xmlWriter = new SerializerXMLWriter(namespace, serializer);
279         Xpp3DomWriter.write(xmlWriter, this);
280         if (xmlWriter.getExceptions().size() > 0) {
281             throw (IOException) xmlWriter.getExceptions().get(0);
282         }
283     }
284 
285     /**
286      * Merges one DOM into another, given a specific algorithm and possible override points for that algorithm.<p>
287      * The algorithm is as follows:
288      * <ol>
289      * <li> if the recessive DOM is null, there is nothing to do... return.</li>
290      * <li> Determine whether the dominant node will suppress the recessive one (flag=mergeSelf).
291      *   <ol type="A">
292      *   <li> retrieve the 'combine.self' attribute on the dominant node, and try to match against 'override'...
293      *        if it matches 'override', then set mergeSelf == false...the dominant node suppresses the recessive one
294      *        completely.</li>
295      *   <li> otherwise, use the default value for mergeSelf, which is true...this is the same as specifying
296      *        'combine.self' == 'merge' as an attribute of the dominant root node.</li>
297      *   </ol></li>
298      * <li> If mergeSelf == true
299      *   <ol type="A">
300      *   <li> if the dominant root node's value is empty, set it to the recessive root node's value</li>
301      *   <li> For each attribute in the recessive root node which is not set in the dominant root node, set it.</li>
302      *   <li> Determine whether children from the recessive DOM will be merged or appended to the dominant DOM as
303      *        siblings (flag=mergeChildren).
304      *     <ol type="i">
305      *     <li> if childMergeOverride is set (non-null), use that value (true/false)</li>
306      *     <li> retrieve the 'combine.children' attribute on the dominant node, and try to match against
307      *          'append'...</li>
308      *     <li> if it matches 'append', then set mergeChildren == false...the recessive children will be appended as
309      *          siblings of the dominant children.</li>
310      *     <li> otherwise, use the default value for mergeChildren, which is true...this is the same as specifying
311      *         'combine.children' == 'merge' as an attribute on the dominant root node.</li>
312      *     </ol></li>
313      *   <li> Iterate through the recessive children, and:
314      *     <ol type="i">
315      *     <li> if mergeChildren == true and there is a corresponding dominant child (matched by element name),
316      *          merge the two.</li>
317      *     <li> otherwise, add the recessive child as a new child on the dominant root node.</li>
318      *     </ol></li>
319      *   </ol></li>
320      * </ol>
321      */
322     private static void mergeIntoXpp3Dom(Xpp3Dom dominant, Xpp3Dom recessive, Boolean childMergeOverride) {
323         // TODO: share this as some sort of assembler, implement a walk interface?
324         if (recessive == null) {
325             return;
326         }
327         dominant.dom = dominant.dom.merge(recessive.dom, childMergeOverride);
328     }
329 
330     /**
331      * Merge two DOMs, with one having dominance in the case of collision.
332      *
333      * @see #CHILDREN_COMBINATION_MODE_ATTRIBUTE
334      * @see #SELF_COMBINATION_MODE_ATTRIBUTE
335      * @param dominant The dominant DOM into which the recessive value/attributes/children will be merged
336      * @param recessive The recessive DOM, which will be merged into the dominant DOM
337      * @param childMergeOverride Overrides attribute flags to force merging or appending of child elements into the
338      *            dominant DOM
339      * @return merged DOM
340      */
341     public static Xpp3Dom mergeXpp3Dom(Xpp3Dom dominant, Xpp3Dom recessive, Boolean childMergeOverride) {
342         if (dominant != null) {
343             mergeIntoXpp3Dom(dominant, recessive, childMergeOverride);
344             return dominant;
345         }
346         return recessive;
347     }
348 
349     /**
350      * Merge two DOMs, with one having dominance in the case of collision. Merge mechanisms (vs. override for nodes, or
351      * vs. append for children) is determined by attributes of the dominant root node.
352      *
353      * @see #CHILDREN_COMBINATION_MODE_ATTRIBUTE
354      * @see #SELF_COMBINATION_MODE_ATTRIBUTE
355      * @param dominant The dominant DOM into which the recessive value/attributes/children will be merged
356      * @param recessive The recessive DOM, which will be merged into the dominant DOM
357      * @return merged DOM
358      */
359     public static Xpp3Dom mergeXpp3Dom(Xpp3Dom dominant, Xpp3Dom recessive) {
360         if (dominant != null) {
361             mergeIntoXpp3Dom(dominant, recessive, null);
362             return dominant;
363         }
364         return recessive;
365     }
366 
367     // ----------------------------------------------------------------------
368     // Standard object handling
369     // ----------------------------------------------------------------------
370 
371     @Override
372     public boolean equals(Object obj) {
373         if (obj == this) {
374             return true;
375         }
376 
377         if (!(obj instanceof Xpp3Dom)) {
378             return false;
379         }
380 
381         Xpp3Dom dom = (Xpp3Dom) obj;
382         return this.dom.equals(dom.dom);
383     }
384 
385     @Override
386     public int hashCode() {
387         return dom.hashCode();
388     }
389 
390     @Override
391     public String toString() {
392         return dom.toString();
393     }
394 
395     public String toUnescapedString() {
396         return ((Xpp3Dom) dom).toUnescapedString();
397     }
398 
399     public static boolean isNotEmpty(String str) {
400         return ((str != null) && (str.length() > 0));
401     }
402 
403     public static boolean isEmpty(String str) {
404         return ((str == null) || (str.trim().length() == 0));
405     }
406 
407     private void update(Dom dom) {
408         if (childrenTracking != null) {
409             childrenTracking.replace(this.dom, dom);
410         }
411         this.dom = dom;
412     }
413 
414     private boolean replace(Object prevChild, Object newChild) {
415         List<Dom> children = new ArrayList<>(dom.getChildren());
416         children.replaceAll(d -> d == prevChild ? (Dom) newChild : d);
417         update(new org.apache.maven.internal.xml.Xpp3Dom(
418                 dom.getName(), dom.getValue(), dom.getAttributes(), children, dom.getInputLocation()));
419         return true;
420     }
421 
422     public void setChildrenTracking(ChildrenTracking childrenTracking) {
423         this.childrenTracking = childrenTracking;
424     }
425 
426     @FunctionalInterface
427     public interface ChildrenTracking {
428         boolean replace(Object oldDelegate, Object newDelegate);
429     }
430 }