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