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