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 }