View Javadoc
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 }