XmlNode.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.api.xml;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Immutable;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.annotations.ThreadSafe;
/**
* An immutable XML node representation that provides a clean API for working with XML data structures.
* This interface represents a single node in an XML document tree, containing information about
* the node's name, value, attributes, and child nodes.
*
* <p>Example usage:</p>
* <pre>
* XmlNode node = XmlNode.newBuilder()
* .name("configuration")
* .attribute("version", "1.0")
* .child(XmlNode.newInstance("property", "value"))
* .build();
* </pre>
*
* @since 4.0.0
*/
@Experimental
@ThreadSafe
@Immutable
public interface XmlNode {
@Deprecated(since = "4.0.0", forRemoval = true)
String CHILDREN_COMBINATION_MODE_ATTRIBUTE = XmlService.CHILDREN_COMBINATION_MODE_ATTRIBUTE;
@Deprecated(since = "4.0.0", forRemoval = true)
String CHILDREN_COMBINATION_MERGE = XmlService.CHILDREN_COMBINATION_MERGE;
@Deprecated(since = "4.0.0", forRemoval = true)
String CHILDREN_COMBINATION_APPEND = XmlService.CHILDREN_COMBINATION_APPEND;
/**
* This default mode for combining children DOMs during merge means that where element names match, the process will
* try to merge the element data, rather than putting the dominant and recessive elements (which share the same
* element name) as siblings in the resulting DOM.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
String DEFAULT_CHILDREN_COMBINATION_MODE = XmlService.DEFAULT_CHILDREN_COMBINATION_MODE;
@Deprecated(since = "4.0.0", forRemoval = true)
String SELF_COMBINATION_MODE_ATTRIBUTE = XmlService.SELF_COMBINATION_MODE_ATTRIBUTE;
@Deprecated(since = "4.0.0", forRemoval = true)
String SELF_COMBINATION_OVERRIDE = XmlService.SELF_COMBINATION_OVERRIDE;
@Deprecated(since = "4.0.0", forRemoval = true)
String SELF_COMBINATION_MERGE = XmlService.SELF_COMBINATION_MERGE;
@Deprecated(since = "4.0.0", forRemoval = true)
String SELF_COMBINATION_REMOVE = XmlService.SELF_COMBINATION_REMOVE;
/**
* In case of complex XML structures, combining can be done based on id.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
String ID_COMBINATION_MODE_ATTRIBUTE = XmlService.ID_COMBINATION_MODE_ATTRIBUTE;
/**
* In case of complex XML structures, combining can be done based on keys.
* This is a comma separated list of attribute names.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
String KEYS_COMBINATION_MODE_ATTRIBUTE = XmlService.KEYS_COMBINATION_MODE_ATTRIBUTE;
/**
* This default mode for combining a DOM node during merge means that where element names match, the process will
* try to merge the element attributes and values, rather than overriding the recessive element completely with the
* dominant one. This means that wherever the dominant element doesn't provide the value or a particular attribute,
* that value or attribute will be set from the recessive DOM node.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
String DEFAULT_SELF_COMBINATION_MODE = XmlService.DEFAULT_SELF_COMBINATION_MODE;
/**
* Returns the local name of this XML node.
*
* @return the node name, never {@code null}
*/
@Nonnull
String name();
/**
* Returns the namespace URI of this XML node.
*
* @return the namespace URI, never {@code null} (empty string if no namespace)
*/
@Nonnull
String namespaceUri();
/**
* Returns the namespace prefix of this XML node.
*
* @return the namespace prefix, never {@code null} (empty string if no prefix)
*/
@Nonnull
String prefix();
/**
* Returns the text content of this XML node.
*
* @return the node's text value, or {@code null} if none exists
*/
@Nullable
String value();
/**
* Returns an immutable map of all attributes defined on this XML node.
*
* @return map of attribute names to values, never {@code null}
*/
@Nonnull
Map<String, String> attributes();
/**
* Returns the value of a specific attribute.
*
* @param name the name of the attribute to retrieve
* @return the attribute value, or {@code null} if the attribute doesn't exist
* @throws NullPointerException if name is null
*/
@Nullable
String attribute(@Nonnull String name);
/**
* Returns an immutable list of all child nodes.
*
* @return list of child nodes, never {@code null}
*/
@Nonnull
List<XmlNode> children();
/**
* Returns the first child node with the specified name.
*
* @param name the name of the child node to find
* @return the first matching child node, or {@code null} if none found
*/
@Nullable
XmlNode child(String name);
/**
* Returns the input location information for this node, if available.
* This can be useful for error reporting and debugging.
*
* @return the input location object, or {@code null} if not available
*/
@Nullable
Object inputLocation();
// Deprecated methods that delegate to new ones
@Deprecated(since = "4.0.0", forRemoval = true)
@Nonnull
default String getName() {
return name();
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nonnull
default String getNamespaceUri() {
return namespaceUri();
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nonnull
default String getPrefix() {
return prefix();
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nullable
default String getValue() {
return value();
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nonnull
default Map<String, String> getAttributes() {
return attributes();
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nullable
default String getAttribute(@Nonnull String name) {
return attribute(name);
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nonnull
default List<XmlNode> getChildren() {
return children();
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nullable
default XmlNode getChild(String name) {
return child(name);
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Nullable
default Object getInputLocation() {
return inputLocation();
}
/**
* @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead
*/
@Deprecated(since = "4.0.0", forRemoval = true)
default XmlNode merge(@Nullable XmlNode source) {
return XmlService.merge(this, source);
}
/**
* @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead
*/
@Deprecated(since = "4.0.0", forRemoval = true)
default XmlNode merge(@Nullable XmlNode source, @Nullable Boolean childMergeOverride) {
return XmlService.merge(this, source, childMergeOverride);
}
/**
* Merge recessive into dominant and return either {@code dominant}
* with merged information or a clone of {@code recessive} if
* {@code dominant} is {@code null}.
*
* @param dominant the node
* @param recessive if {@code null}, nothing will happen
* @return the merged node
*
* @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@Nullable
static XmlNode merge(@Nullable XmlNode dominant, @Nullable XmlNode recessive) {
return XmlService.merge(dominant, recessive, null);
}
@Nullable
static XmlNode merge(
@Nullable XmlNode dominant, @Nullable XmlNode recessive, @Nullable Boolean childMergeOverride) {
return XmlService.merge(dominant, recessive, childMergeOverride);
}
/**
* Creates a new XmlNode instance with the specified name.
*
* @param name the name for the new node
* @return a new XmlNode instance
* @throws NullPointerException if name is null
*/
static XmlNode newInstance(String name) {
return newBuilder().name(name).build();
}
/**
* Creates a new XmlNode instance with the specified name and value.
*
* @param name the name for the new node
* @param value the value for the new node
* @return a new XmlNode instance
* @throws NullPointerException if name is null
*/
static XmlNode newInstance(String name, String value) {
return newBuilder().name(name).value(value).build();
}
/**
* Creates a new XmlNode instance with the specified name and children.
*
* @param name the name for the new node
* @param children the list of child nodes
* @return a new XmlNode instance
* @throws NullPointerException if name is null
*/
static XmlNode newInstance(String name, List<XmlNode> children) {
return newBuilder().name(name).children(children).build();
}
/**
* Creates a new XmlNode instance with all properties specified.
*
* @param name the name for the new node
* @param value the value for the new node
* @param attrs the attributes for the new node
* @param children the list of child nodes
* @param location the input location information
* @return a new XmlNode instance
* @throws NullPointerException if name is null
*/
static XmlNode newInstance(
String name, String value, Map<String, String> attrs, List<XmlNode> children, Object location) {
return newBuilder()
.name(name)
.value(value)
.attributes(attrs)
.children(children)
.inputLocation(location)
.build();
}
/**
* Returns a new builder for creating XmlNode instances.
*
* @return a new Builder instance
*/
static Builder newBuilder() {
return new Builder();
}
/**
* Builder class for creating XmlNode instances.
* <p>
* This builder provides a fluent API for setting the various properties of an XML node.
* All properties are optional except for the node name, which must be set before calling
* {@link #build()}.
*/
class Builder {
private String name;
private String value;
private String namespaceUri;
private String prefix;
private Map<String, String> attributes;
private List<XmlNode> children;
private Object inputLocation;
/**
* Sets the name of the XML node.
* <p>
* This is the only required property that must be set before calling {@link #build()}.
*
* @param name the name of the XML node
* @return this builder instance
* @throws NullPointerException if name is null
*/
public Builder name(String name) {
this.name = name;
return this;
}
/**
* Sets the text content of the XML node.
*
* @param value the text content of the XML node
* @return this builder instance
*/
public Builder value(String value) {
this.value = value;
return this;
}
/**
* Sets the namespace URI of the XML node.
*
* @param namespaceUri the namespace URI of the XML node
* @return this builder instance
*/
public Builder namespaceUri(String namespaceUri) {
this.namespaceUri = namespaceUri;
return this;
}
/**
* Sets the namespace prefix of the XML node.
*
* @param prefix the namespace prefix of the XML node
* @return this builder instance
*/
public Builder prefix(String prefix) {
this.prefix = prefix;
return this;
}
/**
* Sets the attributes of the XML node.
* <p>
* The provided map will be copied to ensure immutability.
*
* @param attributes the map of attribute names to values
* @return this builder instance
*/
public Builder attributes(Map<String, String> attributes) {
this.attributes = attributes;
return this;
}
/**
* Sets the child nodes of the XML node.
* <p>
* The provided list will be copied to ensure immutability.
*
* @param children the list of child nodes
* @return this builder instance
*/
public Builder children(List<XmlNode> children) {
this.children = children;
return this;
}
/**
* Sets the input location information for the XML node.
* <p>
* This is typically used for error reporting and debugging purposes.
*
* @param inputLocation the input location object
* @return this builder instance
*/
public Builder inputLocation(Object inputLocation) {
this.inputLocation = inputLocation;
return this;
}
/**
* Builds a new XmlNode instance with the current builder settings.
*
* @return a new immutable XmlNode instance
* @throws NullPointerException if name has not been set
*/
public XmlNode build() {
return new Impl(prefix, namespaceUri, name, value, attributes, children, inputLocation);
}
private record Impl(
String prefix,
String namespaceUri,
@Nonnull String name,
String value,
@Nonnull Map<String, String> attributes,
@Nonnull List<XmlNode> children,
Object inputLocation)
implements XmlNode, Serializable {
private Impl {
// Validation and normalization from the original constructor
prefix = prefix == null ? "" : prefix;
namespaceUri = namespaceUri == null ? "" : namespaceUri;
name = Objects.requireNonNull(name);
attributes = ImmutableCollections.copy(attributes);
children = ImmutableCollections.copy(children);
}
@Override
public String attribute(@Nonnull String name) {
return attributes.get(name);
}
@Override
public XmlNode child(String name) {
if (name != null) {
ListIterator<XmlNode> it = children.listIterator(children.size());
while (it.hasPrevious()) {
XmlNode child = it.previous();
if (name.equals(child.name())) {
return child;
}
}
}
return null;
}
@Override
public boolean equals(Object o) {
return this == o
|| o instanceof XmlNode that
&& Objects.equals(this.name, that.name())
&& Objects.equals(this.value, that.value())
&& Objects.equals(this.attributes, that.attributes())
&& Objects.equals(this.children, that.children());
}
@Override
public int hashCode() {
return Objects.hash(name, value, attributes, children);
}
@Override
public String toString() {
try {
StringWriter writer = new StringWriter();
XmlService.write(this, writer);
return writer.toString();
} catch (IOException e) {
return toStringObject();
}
}
private String toStringObject() {
StringBuilder sb = new StringBuilder();
sb.append("XmlNode[");
boolean w = false;
w = addToStringField(sb, prefix, o -> !o.isEmpty(), "prefix", w);
w = addToStringField(sb, namespaceUri, o -> !o.isEmpty(), "namespaceUri", w);
w = addToStringField(sb, name, o -> !o.isEmpty(), "name", w);
w = addToStringField(sb, value, o -> !o.isEmpty(), "value", w);
w = addToStringField(sb, attributes, o -> !o.isEmpty(), "attributes", w);
w = addToStringField(sb, children, o -> !o.isEmpty(), "children", w);
w = addToStringField(sb, inputLocation, Objects::nonNull, "inputLocation", w);
sb.append("]");
return sb.toString();
}
private static <T> boolean addToStringField(
StringBuilder sb, T o, Function<T, Boolean> p, String n, boolean w) {
if (!p.apply(o)) {
if (w) {
sb.append(", ");
} else {
w = true;
}
sb.append(n).append("='").append(o).append('\'');
}
return w;
}
}
}
}