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 }