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