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