View Javadoc
1   package org.apache.maven.internal.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 java.io.IOException;
23  import java.io.Serializable;
24  import java.io.StringWriter;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.ListIterator;
31  import java.util.Map;
32  import java.util.Objects;
33  import java.util.Set;
34  import java.util.stream.Collectors;
35  import java.util.stream.Stream;
36  
37  import org.apache.maven.api.xml.Dom;
38  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
39  import org.codehaus.plexus.util.xml.SerializerXMLWriter;
40  import org.codehaus.plexus.util.xml.XMLWriter;
41  import org.codehaus.plexus.util.xml.pull.XmlSerializer;
42  
43  /**
44   *  NOTE: remove all the util code in here when separated, this class should be pure data.
45   */
46  public class Xpp3Dom
47      implements Serializable, Dom
48  {
49      private static final long serialVersionUID = 2567894443061173996L;
50  
51      protected final String name;
52  
53      protected final String value;
54  
55      protected final Map<String, String> attributes;
56  
57      protected final List<Dom> children;
58  
59      protected final Object location;
60  
61  
62      public Xpp3Dom( String name )
63      {
64          this( name, null, null, null, null );
65      }
66  
67      public Xpp3Dom( String name, String value )
68      {
69          this( name, value, null, null, null );
70      }
71  
72      public Xpp3Dom( Dom from, String name )
73      {
74          this( name, from.getValue(), from.getAttributes(),
75                  from.getChildren(), from.getInputLocation() );
76      }
77  
78      public Xpp3Dom( String name, String value,
79                      Map<String, String> attributes,
80                      List<Dom> children,
81                      Object location )
82      {
83          this.name = Objects.requireNonNull( name );
84          this.value = value;
85          this.attributes = attributes != null
86                  ? Collections.unmodifiableMap( new HashMap<>( attributes ) )
87                  : Collections.emptyMap();
88          this.children = children != null
89                  ? Collections.unmodifiableList( new ArrayList<>( children ) )
90                  : Collections.emptyList();
91          this.location = location;
92      }
93  
94      @Override
95      public Dom merge( Dom source, Boolean childMergeOverride )
96      {
97          return merge( this, source, childMergeOverride );
98      }
99  
100     public Dom clone()
101     {
102         return this;
103     }
104 
105     // ----------------------------------------------------------------------
106     // Name handling
107     // ----------------------------------------------------------------------
108 
109     public String getName()
110     {
111         return name;
112     }
113 
114     // ----------------------------------------------------------------------
115     // Value handling
116     // ----------------------------------------------------------------------
117 
118     public String getValue()
119     {
120         return value;
121     }
122 
123     // ----------------------------------------------------------------------
124     // Attribute handling
125     // ----------------------------------------------------------------------
126 
127     @Override
128     public Map<String, String> getAttributes()
129     {
130         return attributes;
131     }
132 
133     public String getAttribute( String name )
134     {
135         return attributes.get( name );
136     }
137 
138     // ----------------------------------------------------------------------
139     // Child handling
140     // ----------------------------------------------------------------------
141 
142     public Dom getChild( String name )
143     {
144         if ( name != null )
145         {
146             ListIterator<Dom> it = children.listIterator( children.size() );
147             while ( it.hasPrevious() )
148             {
149                 Dom child = it.previous();
150                 if ( name.equals( child.getName() ) )
151                 {
152                     return child;
153                 }
154             }
155         }
156         return null;
157     }
158 
159     public List<Dom> getChildren()
160     {
161         return children;
162     }
163 
164     public int getChildCount()
165     {
166         return children.size();
167     }
168 
169     // ----------------------------------------------------------------------
170     // Input location handling
171     // ----------------------------------------------------------------------
172 
173     /**
174      * @since 3.2.0
175      * @return input location
176      */
177     public Object getInputLocation()
178     {
179         return location;
180     }
181 
182     // ----------------------------------------------------------------------
183     // Helpers
184     // ----------------------------------------------------------------------
185 
186     public void writeToSerializer( String namespace, XmlSerializer serializer )
187         throws IOException
188     {
189         // TODO: WARNING! Later versions of plexus-utils psit out an <?xml ?> header due to thinking this is a new
190         // document - not the desired behaviour!
191         SerializerXMLWriter xmlWriter = new SerializerXMLWriter( namespace, serializer );
192         Xpp3DomWriter.write( xmlWriter, this );
193         if ( xmlWriter.getExceptions().size() > 0 )
194         {
195             throw (IOException) xmlWriter.getExceptions().get( 0 );
196         }
197     }
198 
199     /**
200      * Merges one DOM into another, given a specific algorithm and possible override points for that algorithm.<p>
201      * The algorithm is as follows:
202      * <ol>
203      * <li> if the recessive DOM is null, there is nothing to do... return.</li>
204      * <li> Determine whether the dominant node will suppress the recessive one (flag=mergeSelf).
205      *   <ol type="A">
206      *   <li> retrieve the 'combine.self' attribute on the dominant node, and try to match against 'override'...
207      *        if it matches 'override', then set mergeSelf == false...the dominant node suppresses the recessive one
208      *        completely.</li>
209      *   <li> otherwise, use the default value for mergeSelf, which is true...this is the same as specifying
210      *        'combine.self' == 'merge' as an attribute of the dominant root node.</li>
211      *   </ol></li>
212      * <li> If mergeSelf == true
213      *   <ol type="A">
214      *   <li> if the dominant root node's value is empty, set it to the recessive root node's value</li>
215      *   <li> For each attribute in the recessive root node which is not set in the dominant root node, set it.</li>
216      *   <li> Determine whether children from the recessive DOM will be merged or appended to the dominant DOM as
217      *        siblings (flag=mergeChildren).
218      *     <ol type="i">
219      *     <li> if childMergeOverride is set (non-null), use that value (true/false)</li>
220      *     <li> retrieve the 'combine.children' attribute on the dominant node, and try to match against
221      *          'append'...</li>
222      *     <li> if it matches 'append', then set mergeChildren == false...the recessive children will be appended as
223      *          siblings of the dominant children.</li>
224      *     <li> otherwise, use the default value for mergeChildren, which is true...this is the same as specifying
225      *         'combine.children' == 'merge' as an attribute on the dominant root node.</li>
226      *     </ol></li>
227      *   <li> Iterate through the recessive children, and:
228      *     <ol type="i">
229      *     <li> if mergeChildren == true and there is a corresponding dominant child (matched by element name),
230      *          merge the two.</li>
231      *     <li> otherwise, add the recessive child as a new child on the dominant root node.</li>
232      *     </ol></li>
233      *   </ol></li>
234      * </ol>
235      */
236     @SuppressWarnings( "checkstyle:MethodLength" )
237     public static Dom merge( Dom dominant, Dom recessive, Boolean childMergeOverride )
238     {
239         // TODO: share this as some sort of assembler, implement a walk interface?
240         if ( recessive == null )
241         {
242             return dominant;
243         }
244         if ( dominant == null )
245         {
246             return recessive;
247         }
248 
249         boolean mergeSelf = true;
250 
251         String selfMergeMode = dominant.getAttribute( SELF_COMBINATION_MODE_ATTRIBUTE );
252 
253         if ( SELF_COMBINATION_OVERRIDE.equals( selfMergeMode ) )
254         {
255             mergeSelf = false;
256         }
257 
258         if ( mergeSelf )
259         {
260 
261             String value = null;
262             Object location = null;
263             Map<String, String> attrs = null;
264             List<Dom> children = null;
265 
266             if ( isEmpty( dominant.getValue() ) && !isEmpty( recessive.getValue() ) )
267             {
268                 value = recessive.getValue();
269                 location = recessive.getInputLocation();
270             }
271 
272             for ( Map.Entry<String, String> attr : recessive.getAttributes().entrySet() )
273             {
274                 String key = attr.getKey();
275                 if ( isEmpty( dominant.getAttribute( key ) ) && !SELF_COMBINATION_MODE_ATTRIBUTE.equals( key ) )
276                 {
277                     if ( attrs == null )
278                     {
279                         attrs = new HashMap<>();
280                     }
281                     attrs.put( key, attr.getValue() );
282                 }
283             }
284 
285             if ( recessive.getChildren().size() > 0 )
286             {
287                 boolean mergeChildren = true;
288                 if ( childMergeOverride != null )
289                 {
290                     mergeChildren = childMergeOverride;
291                 }
292                 else
293                 {
294                     String childMergeMode = dominant.getAttribute( CHILDREN_COMBINATION_MODE_ATTRIBUTE );
295                     if ( CHILDREN_COMBINATION_APPEND.equals( childMergeMode ) )
296                     {
297                         mergeChildren = false;
298                     }
299                 }
300 
301                 if ( !mergeChildren )
302                 {
303                     children = new ArrayList<>( recessive.getChildren().size() + dominant.getChildren().size() );
304                     children.addAll( recessive.getChildren() );
305                     children.addAll( dominant.getChildren() );
306                 }
307                 else
308                 {
309                     Map<String, Iterator<Dom>> commonChildren = new HashMap<>();
310                     Set<String> names = recessive.getChildren().stream()
311                             .map( Dom::getName ).collect( Collectors.toSet() );
312                     for ( String name : names )
313                     {
314                         List<Dom> dominantChildren = dominant.getChildren().stream()
315                                 .filter( n -> n.getName().equals( name ) )
316                                 .collect( Collectors.toList() );
317                         if ( dominantChildren.size() > 0 )
318                         {
319                             commonChildren.put( name, dominantChildren.iterator() );
320                         }
321                     }
322 
323                     for ( Dom recessiveChild : recessive.getChildren() )
324                     {
325                         String name = recessiveChild.getName();
326                         Iterator<Dom> it = commonChildren.computeIfAbsent( name,
327                                 n1 -> Stream.of( dominant.getChildren().stream()
328                                             .filter( n2 -> n2.getName().equals( n1 ) )
329                                             .collect( Collectors.toList() ) )
330                                         .filter( l -> !l.isEmpty() )
331                                         .map( List::iterator )
332                                         .findFirst()
333                                         .orElse( null ) );
334                         if ( it == null )
335                         {
336                             if ( children == null )
337                             {
338                                 children = new ArrayList<>( dominant.getChildren() );
339                             }
340                             children.add( recessiveChild );
341                         }
342                         else if ( it.hasNext() )
343                         {
344                             Dom dominantChild = it.next();
345 
346                             String dominantChildCombinationMode =
347                                     dominantChild.getAttribute( SELF_COMBINATION_MODE_ATTRIBUTE );
348                             if ( SELF_COMBINATION_REMOVE.equals( dominantChildCombinationMode ) )
349                             {
350                                 if ( children == null )
351                                 {
352                                     children = new ArrayList<>( dominant.getChildren() );
353                                 }
354                                 children.remove( dominantChild );
355                             }
356                             else
357                             {
358                                 int idx = ( children != null ? children : dominant.getChildren() )
359                                         .indexOf( dominantChild );
360                                 Dom merged = merge( dominantChild, recessiveChild, childMergeOverride );
361                                 if ( merged != dominantChild )
362                                 {
363                                     if ( children == null )
364                                     {
365                                         children = new ArrayList<>( dominant.getChildren() );
366                                     }
367                                     children.set( idx, merged );
368                                 }
369                             }
370                         }
371                     }
372                 }
373             }
374 
375             if ( value != null || attrs != null || children != null )
376             {
377                 if ( attrs != null )
378                 {
379                     Map<String, String> nattrs = attrs;
380                     attrs = new HashMap<>( dominant.getAttributes() );
381                     attrs.putAll( nattrs );
382                 }
383                 else
384                 {
385                     attrs = dominant.getAttributes();
386                 }
387                 if ( children == null )
388                 {
389                     children = dominant.getChildren();
390                 }
391                 return new Xpp3Dom( dominant.getName(), value != null ? value : dominant.getValue(),
392                                     attrs, children, location );
393             }
394         }
395         return dominant;
396     }
397 
398     /**
399      * Merge two DOMs, with one having dominance in the case of collision. Merge mechanisms (vs. override for nodes, or
400      * vs. append for children) is determined by attributes of the dominant root node.
401      *
402      * @see #CHILDREN_COMBINATION_MODE_ATTRIBUTE
403      * @see #SELF_COMBINATION_MODE_ATTRIBUTE
404      * @param dominant The dominant DOM into which the recessive value/attributes/children will be merged
405      * @param recessive The recessive DOM, which will be merged into the dominant DOM
406      * @return merged DOM
407      */
408     public static Dom merge( Dom dominant, Dom recessive )
409     {
410         return merge( dominant, recessive, null );
411     }
412 
413     // ----------------------------------------------------------------------
414     // Standard object handling
415     // ----------------------------------------------------------------------
416 
417 
418     @Override
419     public boolean equals( Object o )
420     {
421         if ( this == o )
422         {
423             return true;
424         }
425         if ( o == null || getClass() != o.getClass() )
426         {
427             return false;
428         }
429         Xpp3Dom xpp3Dom = (Xpp3Dom) o;
430         return name.equals( xpp3Dom.name ) && Objects.equals( value, xpp3Dom.value ) && attributes.equals(
431                 xpp3Dom.attributes ) && children.equals( xpp3Dom.children );
432     }
433 
434     @Override
435     public int hashCode()
436     {
437         return Objects.hash( name, value, attributes, children );
438     }
439 
440     @Override
441     public String toString()
442     {
443         StringWriter writer = new StringWriter();
444         Xpp3DomWriter.write( writer, this );
445         return writer.toString();
446     }
447 
448     public String toUnescapedString()
449     {
450         StringWriter writer = new StringWriter();
451         XMLWriter xmlWriter = new PrettyPrintXMLWriter( writer );
452         Xpp3DomWriter.write( xmlWriter, this, false );
453         return writer.toString();
454     }
455 
456     public static boolean isNotEmpty( String str )
457     {
458         return ( ( str != null ) && ( str.length() > 0 ) );
459     }
460 
461     public static boolean isEmpty( String str )
462     {
463         return ( ( str == null ) || ( str.trim().length() == 0 ) );
464     }
465 
466 }