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.apache.maven.api.xml;
20
21 import java.io.IOException;
22 import java.io.Serializable;
23 import java.io.StringWriter;
24 import java.util.List;
25 import java.util.ListIterator;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.function.Function;
29
30 import org.apache.maven.api.annotations.Experimental;
31 import org.apache.maven.api.annotations.Immutable;
32 import org.apache.maven.api.annotations.Nonnull;
33 import org.apache.maven.api.annotations.Nullable;
34 import org.apache.maven.api.annotations.ThreadSafe;
35
36 /**
37 * An immutable XML node representation that provides a clean API for working with XML data structures.
38 * This interface represents a single node in an XML document tree, containing information about
39 * the node's name, value, attributes, and child nodes.
40 *
41 * <p>Example usage:</p>
42 * <pre>
43 * XmlNode node = XmlNode.newBuilder()
44 * .name("configuration")
45 * .attribute("version", "1.0")
46 * .child(XmlNode.newInstance("property", "value"))
47 * .build();
48 * </pre>
49 *
50 * @since 4.0.0
51 */
52 @Experimental
53 @ThreadSafe
54 @Immutable
55 public interface XmlNode {
56
57 @Deprecated(since = "4.0.0", forRemoval = true)
58 String CHILDREN_COMBINATION_MODE_ATTRIBUTE = XmlService.CHILDREN_COMBINATION_MODE_ATTRIBUTE;
59
60 @Deprecated(since = "4.0.0", forRemoval = true)
61 String CHILDREN_COMBINATION_MERGE = XmlService.CHILDREN_COMBINATION_MERGE;
62
63 @Deprecated(since = "4.0.0", forRemoval = true)
64 String CHILDREN_COMBINATION_APPEND = XmlService.CHILDREN_COMBINATION_APPEND;
65
66 /**
67 * This default mode for combining children DOMs during merge means that where element names match, the process will
68 * try to merge the element data, rather than putting the dominant and recessive elements (which share the same
69 * element name) as siblings in the resulting DOM.
70 */
71 @Deprecated(since = "4.0.0", forRemoval = true)
72 String DEFAULT_CHILDREN_COMBINATION_MODE = XmlService.DEFAULT_CHILDREN_COMBINATION_MODE;
73
74 @Deprecated(since = "4.0.0", forRemoval = true)
75 String SELF_COMBINATION_MODE_ATTRIBUTE = XmlService.SELF_COMBINATION_MODE_ATTRIBUTE;
76
77 @Deprecated(since = "4.0.0", forRemoval = true)
78 String SELF_COMBINATION_OVERRIDE = XmlService.SELF_COMBINATION_OVERRIDE;
79
80 @Deprecated(since = "4.0.0", forRemoval = true)
81 String SELF_COMBINATION_MERGE = XmlService.SELF_COMBINATION_MERGE;
82
83 @Deprecated(since = "4.0.0", forRemoval = true)
84 String SELF_COMBINATION_REMOVE = XmlService.SELF_COMBINATION_REMOVE;
85
86 /**
87 * In case of complex XML structures, combining can be done based on id.
88 */
89 @Deprecated(since = "4.0.0", forRemoval = true)
90 String ID_COMBINATION_MODE_ATTRIBUTE = XmlService.ID_COMBINATION_MODE_ATTRIBUTE;
91
92 /**
93 * In case of complex XML structures, combining can be done based on keys.
94 * This is a comma separated list of attribute names.
95 */
96 @Deprecated(since = "4.0.0", forRemoval = true)
97 String KEYS_COMBINATION_MODE_ATTRIBUTE = XmlService.KEYS_COMBINATION_MODE_ATTRIBUTE;
98
99 /**
100 * This default mode for combining a DOM node during merge means that where element names match, the process will
101 * try to merge the element attributes and values, rather than overriding the recessive element completely with the
102 * dominant one. This means that wherever the dominant element doesn't provide the value or a particular attribute,
103 * that value or attribute will be set from the recessive DOM node.
104 */
105 @Deprecated(since = "4.0.0", forRemoval = true)
106 String DEFAULT_SELF_COMBINATION_MODE = XmlService.DEFAULT_SELF_COMBINATION_MODE;
107
108 /**
109 * Returns the local name of this XML node.
110 *
111 * @return the node name, never {@code null}
112 */
113 @Nonnull
114 String name();
115
116 /**
117 * Returns the namespace URI of this XML node.
118 *
119 * @return the namespace URI, never {@code null} (empty string if no namespace)
120 */
121 @Nonnull
122 String namespaceUri();
123
124 /**
125 * Returns the namespace prefix of this XML node.
126 *
127 * @return the namespace prefix, never {@code null} (empty string if no prefix)
128 */
129 @Nonnull
130 String prefix();
131
132 /**
133 * Returns the text content of this XML node.
134 *
135 * @return the node's text value, or {@code null} if none exists
136 */
137 @Nullable
138 String value();
139
140 /**
141 * Returns an immutable map of all attributes defined on this XML node.
142 *
143 * @return map of attribute names to values, never {@code null}
144 */
145 @Nonnull
146 Map<String, String> attributes();
147
148 /**
149 * Returns the value of a specific attribute.
150 *
151 * @param name the name of the attribute to retrieve
152 * @return the attribute value, or {@code null} if the attribute doesn't exist
153 * @throws NullPointerException if name is null
154 */
155 @Nullable
156 String attribute(@Nonnull String name);
157
158 /**
159 * Returns an immutable list of all child nodes.
160 *
161 * @return list of child nodes, never {@code null}
162 */
163 @Nonnull
164 List<XmlNode> children();
165
166 /**
167 * Returns the first child node with the specified name.
168 *
169 * @param name the name of the child node to find
170 * @return the first matching child node, or {@code null} if none found
171 */
172 @Nullable
173 XmlNode child(String name);
174
175 /**
176 * Returns the input location information for this node, if available.
177 * This can be useful for error reporting and debugging.
178 *
179 * @return the input location object, or {@code null} if not available
180 */
181 @Nullable
182 Object inputLocation();
183
184 // Deprecated methods that delegate to new ones
185 @Deprecated(since = "4.0.0", forRemoval = true)
186 @Nonnull
187 default String getName() {
188 return name();
189 }
190
191 @Deprecated(since = "4.0.0", forRemoval = true)
192 @Nonnull
193 default String getNamespaceUri() {
194 return namespaceUri();
195 }
196
197 @Deprecated(since = "4.0.0", forRemoval = true)
198 @Nonnull
199 default String getPrefix() {
200 return prefix();
201 }
202
203 @Deprecated(since = "4.0.0", forRemoval = true)
204 @Nullable
205 default String getValue() {
206 return value();
207 }
208
209 @Deprecated(since = "4.0.0", forRemoval = true)
210 @Nonnull
211 default Map<String, String> getAttributes() {
212 return attributes();
213 }
214
215 @Deprecated(since = "4.0.0", forRemoval = true)
216 @Nullable
217 default String getAttribute(@Nonnull String name) {
218 return attribute(name);
219 }
220
221 @Deprecated(since = "4.0.0", forRemoval = true)
222 @Nonnull
223 default List<XmlNode> getChildren() {
224 return children();
225 }
226
227 @Deprecated(since = "4.0.0", forRemoval = true)
228 @Nullable
229 default XmlNode getChild(String name) {
230 return child(name);
231 }
232
233 @Deprecated(since = "4.0.0", forRemoval = true)
234 @Nullable
235 default Object getInputLocation() {
236 return inputLocation();
237 }
238
239 /**
240 * @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead
241 */
242 @Deprecated(since = "4.0.0", forRemoval = true)
243 default XmlNode merge(@Nullable XmlNode source) {
244 return XmlService.merge(this, source);
245 }
246
247 /**
248 * @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead
249 */
250 @Deprecated(since = "4.0.0", forRemoval = true)
251 default XmlNode merge(@Nullable XmlNode source, @Nullable Boolean childMergeOverride) {
252 return XmlService.merge(this, source, childMergeOverride);
253 }
254
255 /**
256 * Merge recessive into dominant and return either {@code dominant}
257 * with merged information or a clone of {@code recessive} if
258 * {@code dominant} is {@code null}.
259 *
260 * @param dominant the node
261 * @param recessive if {@code null}, nothing will happen
262 * @return the merged node
263 *
264 * @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead
265 */
266 @Deprecated(since = "4.0.0", forRemoval = true)
267 @Nullable
268 static XmlNode merge(@Nullable XmlNode dominant, @Nullable XmlNode recessive) {
269 return XmlService.merge(dominant, recessive, null);
270 }
271
272 @Nullable
273 static XmlNode merge(
274 @Nullable XmlNode dominant, @Nullable XmlNode recessive, @Nullable Boolean childMergeOverride) {
275 return XmlService.merge(dominant, recessive, childMergeOverride);
276 }
277
278 /**
279 * Creates a new XmlNode instance with the specified name.
280 *
281 * @param name the name for the new node
282 * @return a new XmlNode instance
283 * @throws NullPointerException if name is null
284 */
285 static XmlNode newInstance(String name) {
286 return newBuilder().name(name).build();
287 }
288
289 /**
290 * Creates a new XmlNode instance with the specified name and value.
291 *
292 * @param name the name for the new node
293 * @param value the value for the new node
294 * @return a new XmlNode instance
295 * @throws NullPointerException if name is null
296 */
297 static XmlNode newInstance(String name, String value) {
298 return newBuilder().name(name).value(value).build();
299 }
300
301 /**
302 * Creates a new XmlNode instance with the specified name and children.
303 *
304 * @param name the name for the new node
305 * @param children the list of child nodes
306 * @return a new XmlNode instance
307 * @throws NullPointerException if name is null
308 */
309 static XmlNode newInstance(String name, List<XmlNode> children) {
310 return newBuilder().name(name).children(children).build();
311 }
312
313 /**
314 * Creates a new XmlNode instance with all properties specified.
315 *
316 * @param name the name for the new node
317 * @param value the value for the new node
318 * @param attrs the attributes for the new node
319 * @param children the list of child nodes
320 * @param location the input location information
321 * @return a new XmlNode instance
322 * @throws NullPointerException if name is null
323 */
324 static XmlNode newInstance(
325 String name, String value, Map<String, String> attrs, List<XmlNode> children, Object location) {
326 return newBuilder()
327 .name(name)
328 .value(value)
329 .attributes(attrs)
330 .children(children)
331 .inputLocation(location)
332 .build();
333 }
334
335 /**
336 * Returns a new builder for creating XmlNode instances.
337 *
338 * @return a new Builder instance
339 */
340 static Builder newBuilder() {
341 return new Builder();
342 }
343
344 /**
345 * Builder class for creating XmlNode instances.
346 * <p>
347 * This builder provides a fluent API for setting the various properties of an XML node.
348 * All properties are optional except for the node name, which must be set before calling
349 * {@link #build()}.
350 */
351 class Builder {
352 private String name;
353 private String value;
354 private String namespaceUri;
355 private String prefix;
356 private Map<String, String> attributes;
357 private List<XmlNode> children;
358 private Object inputLocation;
359
360 /**
361 * Sets the name of the XML node.
362 * <p>
363 * This is the only required property that must be set before calling {@link #build()}.
364 *
365 * @param name the name of the XML node
366 * @return this builder instance
367 * @throws NullPointerException if name is null
368 */
369 public Builder name(String name) {
370 this.name = name;
371 return this;
372 }
373
374 /**
375 * Sets the text content of the XML node.
376 *
377 * @param value the text content of the XML node
378 * @return this builder instance
379 */
380 public Builder value(String value) {
381 this.value = value;
382 return this;
383 }
384
385 /**
386 * Sets the namespace URI of the XML node.
387 *
388 * @param namespaceUri the namespace URI of the XML node
389 * @return this builder instance
390 */
391 public Builder namespaceUri(String namespaceUri) {
392 this.namespaceUri = namespaceUri;
393 return this;
394 }
395
396 /**
397 * Sets the namespace prefix of the XML node.
398 *
399 * @param prefix the namespace prefix of the XML node
400 * @return this builder instance
401 */
402 public Builder prefix(String prefix) {
403 this.prefix = prefix;
404 return this;
405 }
406
407 /**
408 * Sets the attributes of the XML node.
409 * <p>
410 * The provided map will be copied to ensure immutability.
411 *
412 * @param attributes the map of attribute names to values
413 * @return this builder instance
414 */
415 public Builder attributes(Map<String, String> attributes) {
416 this.attributes = attributes;
417 return this;
418 }
419
420 /**
421 * Sets the child nodes of the XML node.
422 * <p>
423 * The provided list will be copied to ensure immutability.
424 *
425 * @param children the list of child nodes
426 * @return this builder instance
427 */
428 public Builder children(List<XmlNode> children) {
429 this.children = children;
430 return this;
431 }
432
433 /**
434 * Sets the input location information for the XML node.
435 * <p>
436 * This is typically used for error reporting and debugging purposes.
437 *
438 * @param inputLocation the input location object
439 * @return this builder instance
440 */
441 public Builder inputLocation(Object inputLocation) {
442 this.inputLocation = inputLocation;
443 return this;
444 }
445
446 /**
447 * Builds a new XmlNode instance with the current builder settings.
448 *
449 * @return a new immutable XmlNode instance
450 * @throws NullPointerException if name has not been set
451 */
452 public XmlNode build() {
453 return new Impl(prefix, namespaceUri, name, value, attributes, children, inputLocation);
454 }
455
456 private record Impl(
457 String prefix,
458 String namespaceUri,
459 @Nonnull String name,
460 String value,
461 @Nonnull Map<String, String> attributes,
462 @Nonnull List<XmlNode> children,
463 Object inputLocation)
464 implements XmlNode, Serializable {
465
466 private Impl {
467 // Validation and normalization from the original constructor
468 prefix = prefix == null ? "" : prefix;
469 namespaceUri = namespaceUri == null ? "" : namespaceUri;
470 name = Objects.requireNonNull(name);
471 attributes = ImmutableCollections.copy(attributes);
472 children = ImmutableCollections.copy(children);
473 }
474
475 @Override
476 public String attribute(@Nonnull String name) {
477 return attributes.get(name);
478 }
479
480 @Override
481 public XmlNode child(String name) {
482 if (name != null) {
483 ListIterator<XmlNode> it = children.listIterator(children.size());
484 while (it.hasPrevious()) {
485 XmlNode child = it.previous();
486 if (name.equals(child.name())) {
487 return child;
488 }
489 }
490 }
491 return null;
492 }
493
494 @Override
495 public boolean equals(Object o) {
496 if (this == o) {
497 return true;
498 }
499 if (o == null || getClass() != o.getClass()) {
500 return false;
501 }
502 Impl that = (Impl) o;
503 return Objects.equals(this.name, that.name)
504 && Objects.equals(this.value, that.value)
505 && Objects.equals(this.attributes, that.attributes)
506 && Objects.equals(this.children, that.children);
507 }
508
509 @Override
510 public int hashCode() {
511 return Objects.hash(name, value, attributes, children);
512 }
513
514 @Override
515 public String toString() {
516 try {
517 StringWriter writer = new StringWriter();
518 XmlService.write(this, writer);
519 return writer.toString();
520 } catch (IOException e) {
521 return toStringObject();
522 }
523 }
524
525 private String toStringObject() {
526 StringBuilder sb = new StringBuilder();
527 sb.append("XmlNode[");
528 boolean w = false;
529 w = addToStringField(sb, prefix, o -> !o.isEmpty(), "prefix", w);
530 w = addToStringField(sb, namespaceUri, o -> !o.isEmpty(), "namespaceUri", w);
531 w = addToStringField(sb, name, o -> !o.isEmpty(), "name", w);
532 w = addToStringField(sb, value, o -> !o.isEmpty(), "value", w);
533 w = addToStringField(sb, attributes, o -> !o.isEmpty(), "attributes", w);
534 w = addToStringField(sb, children, o -> !o.isEmpty(), "children", w);
535 w = addToStringField(sb, inputLocation, Objects::nonNull, "inputLocation", w);
536 sb.append("]");
537 return sb.toString();
538 }
539
540 private static <T> boolean addToStringField(
541 StringBuilder sb, T o, Function<T, Boolean> p, String n, boolean w) {
542 if (!p.apply(o)) {
543 if (w) {
544 sb.append(", ");
545 } else {
546 w = true;
547 }
548 sb.append(n).append("='").append(o).append('\'');
549 }
550 return w;
551 }
552 }
553 }
554 }