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