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 }