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