blob: 1210b813209759e84225004f35357bbd0bc4acb6 [file] [log] [blame]
/*
* 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.internal.xml;
import javax.xml.stream.XMLStreamException;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.api.xml.XmlNode;
/**
* NOTE: remove all the util code in here when separated, this class should be pure data.
*/
public class XmlNodeImpl implements Serializable, XmlNode {
private static final long serialVersionUID = 2567894443061173996L;
protected final String prefix;
protected final String namespaceUri;
protected final String name;
protected final String value;
protected final Map<String, String> attributes;
protected final List<XmlNode> children;
protected final Object location;
public XmlNodeImpl(String name) {
this(name, null, null, null, null);
}
public XmlNodeImpl(String name, String value) {
this(name, value, null, null, null);
}
public XmlNodeImpl(XmlNode from, String name) {
this(name, from.getValue(), from.getAttributes(), from.getChildren(), from.getInputLocation());
}
public XmlNodeImpl(
String name, String value, Map<String, String> attributes, List<XmlNode> children, Object location) {
this("", "", name, value, attributes, children, location);
}
public XmlNodeImpl(
String prefix,
String namespaceUri,
String name,
String value,
Map<String, String> attributes,
List<XmlNode> children,
Object location) {
this.prefix = prefix == null ? "" : prefix;
this.namespaceUri = namespaceUri == null ? "" : namespaceUri;
this.name = Objects.requireNonNull(name);
this.value = value;
this.attributes =
attributes != null ? Collections.unmodifiableMap(new HashMap<>(attributes)) : Collections.emptyMap();
this.children =
children != null ? Collections.unmodifiableList(new ArrayList<>(children)) : Collections.emptyList();
this.location = location;
}
@Override
public XmlNode merge(XmlNode source, Boolean childMergeOverride) {
return merge(this, source, childMergeOverride);
}
// ----------------------------------------------------------------------
// Name handling
// ----------------------------------------------------------------------
@Override
public String getPrefix() {
return prefix;
}
@Override
public String getNamespaceUri() {
return namespaceUri;
}
@Override
public String getName() {
return name;
}
// ----------------------------------------------------------------------
// Value handling
// ----------------------------------------------------------------------
public String getValue() {
return value;
}
// ----------------------------------------------------------------------
// Attribute handling
// ----------------------------------------------------------------------
@Override
public Map<String, String> getAttributes() {
return attributes;
}
public String getAttribute(String name) {
return attributes.get(name);
}
// ----------------------------------------------------------------------
// Child handling
// ----------------------------------------------------------------------
public XmlNode getChild(String name) {
if (name != null) {
ListIterator<XmlNode> it = children.listIterator(children.size());
while (it.hasPrevious()) {
XmlNode child = it.previous();
if (name.equals(child.getName())) {
return child;
}
}
}
return null;
}
public List<XmlNode> getChildren() {
return children;
}
public int getChildCount() {
return children.size();
}
// ----------------------------------------------------------------------
// Input location handling
// ----------------------------------------------------------------------
/**
* @since 3.2.0
* @return input location
*/
public Object getInputLocation() {
return location;
}
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* Merges one DOM into another, given a specific algorithm and possible override points for that algorithm.<p>
* The algorithm is as follows:
* <ol>
* <li> if the recessive DOM is null, there is nothing to do... return.</li>
* <li> Determine whether the dominant node will suppress the recessive one (flag=mergeSelf).
* <ol type="A">
* <li> retrieve the 'combine.self' attribute on the dominant node, and try to match against 'override'...
* if it matches 'override', then set mergeSelf == false...the dominant node suppresses the recessive one
* completely.</li>
* <li> otherwise, use the default value for mergeSelf, which is true...this is the same as specifying
* 'combine.self' == 'merge' as an attribute of the dominant root node.</li>
* </ol></li>
* <li> If mergeSelf == true
* <ol type="A">
* <li> Determine whether children from the recessive DOM will be merged or appended to the dominant DOM as
* siblings (flag=mergeChildren).
* <ol type="i">
* <li> if childMergeOverride is set (non-null), use that value (true/false)</li>
* <li> retrieve the 'combine.children' attribute on the dominant node, and try to match against
* 'append'...</li>
* <li> if it matches 'append', then set mergeChildren == false...the recessive children will be appended as
* siblings of the dominant children.</li>
* <li> otherwise, use the default value for mergeChildren, which is true...this is the same as specifying
* 'combine.children' == 'merge' as an attribute on the dominant root node.</li>
* </ol></li>
* <li> Iterate through the recessive children, and:
* <ol type="i">
* <li> if mergeChildren == true and there is a corresponding dominant child (matched by element name),
* merge the two.</li>
* <li> otherwise, add the recessive child as a new child on the dominant root node.</li>
* </ol></li>
* </ol></li>
* </ol>
*/
@SuppressWarnings("checkstyle:MethodLength")
public static XmlNode merge(XmlNode dominant, XmlNode recessive, Boolean childMergeOverride) {
// TODO: share this as some sort of assembler, implement a walk interface?
if (recessive == null) {
return dominant;
}
if (dominant == null) {
return recessive;
}
boolean mergeSelf = true;
String selfMergeMode = dominant.getAttribute(SELF_COMBINATION_MODE_ATTRIBUTE);
if (SELF_COMBINATION_OVERRIDE.equals(selfMergeMode)) {
mergeSelf = false;
}
if (mergeSelf) {
String value = dominant.getValue();
Object location = dominant.getInputLocation();
Map<String, String> attrs = dominant.getAttributes();
List<XmlNode> children = null;
for (Map.Entry<String, String> attr : recessive.getAttributes().entrySet()) {
String key = attr.getKey();
if (isEmpty(attrs.get(key))) {
if (attrs == dominant.getAttributes()) {
attrs = new HashMap<>(attrs);
}
attrs.put(key, attr.getValue());
}
}
if (!recessive.getChildren().isEmpty()) {
boolean mergeChildren = true;
if (childMergeOverride != null) {
mergeChildren = childMergeOverride;
} else {
String childMergeMode = attrs.get(CHILDREN_COMBINATION_MODE_ATTRIBUTE);
if (CHILDREN_COMBINATION_APPEND.equals(childMergeMode)) {
mergeChildren = false;
}
}
Map<String, Iterator<XmlNode>> commonChildren = new HashMap<>();
Set<String> names =
recessive.getChildren().stream().map(XmlNode::getName).collect(Collectors.toSet());
for (String name : names) {
List<XmlNode> dominantChildren = dominant.getChildren().stream()
.filter(n -> n.getName().equals(name))
.collect(Collectors.toList());
if (!dominantChildren.isEmpty()) {
commonChildren.put(name, dominantChildren.iterator());
}
}
String keysValue = recessive.getAttribute(KEYS_COMBINATION_MODE_ATTRIBUTE);
for (XmlNode recessiveChild : recessive.getChildren()) {
String idValue = recessiveChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE);
XmlNode childDom = null;
if (!isEmpty(idValue)) {
for (XmlNode dominantChild : dominant.getChildren()) {
if (idValue.equals(dominantChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE))) {
childDom = dominantChild;
// we have a match, so don't append but merge
mergeChildren = true;
}
}
} else if (!isEmpty(keysValue)) {
String[] keys = keysValue.split(",");
Map<String, Optional<String>> recessiveKeyValues = Stream.of(keys)
.collect(Collectors.toMap(
k -> k, k -> Optional.ofNullable(recessiveChild.getAttribute(k))));
for (XmlNode dominantChild : dominant.getChildren()) {
Map<String, Optional<String>> dominantKeyValues = Stream.of(keys)
.collect(Collectors.toMap(
k -> k, k -> Optional.ofNullable(dominantChild.getAttribute(k))));
if (recessiveKeyValues.equals(dominantKeyValues)) {
childDom = dominantChild;
// we have a match, so don't append but merge
mergeChildren = true;
}
}
} else {
childDom = dominant.getChild(recessiveChild.getName());
}
if (mergeChildren && childDom != null) {
String name = recessiveChild.getName();
Iterator<XmlNode> it =
commonChildren.computeIfAbsent(name, n1 -> Stream.of(dominant.getChildren().stream()
.filter(n2 -> n2.getName().equals(n1))
.collect(Collectors.toList()))
.filter(l -> !l.isEmpty())
.map(List::iterator)
.findFirst()
.orElse(null));
if (it == null) {
if (children == null) {
children = new ArrayList<>(dominant.getChildren());
}
children.add(recessiveChild);
} else if (it.hasNext()) {
XmlNode dominantChild = it.next();
String dominantChildCombinationMode =
dominantChild.getAttribute(SELF_COMBINATION_MODE_ATTRIBUTE);
if (SELF_COMBINATION_REMOVE.equals(dominantChildCombinationMode)) {
if (children == null) {
children = new ArrayList<>(dominant.getChildren());
}
children.remove(dominantChild);
} else {
int idx = dominant.getChildren().indexOf(dominantChild);
XmlNode merged = merge(dominantChild, recessiveChild, childMergeOverride);
if (merged != dominantChild) {
if (children == null) {
children = new ArrayList<>(dominant.getChildren());
}
children.set(idx, merged);
}
}
}
} else {
if (children == null) {
children = new ArrayList<>(dominant.getChildren());
}
int idx = mergeChildren
? children.size()
: recessive.getChildren().indexOf(recessiveChild);
children.add(idx, recessiveChild);
}
}
}
if (value != null || attrs != dominant.getAttributes() || children != null) {
if (children == null) {
children = dominant.getChildren();
}
return new XmlNodeImpl(
dominant.getName(), value != null ? value : dominant.getValue(), attrs, children, location);
}
}
return dominant;
}
/**
* Merge two DOMs, with one having dominance in the case of collision. Merge mechanisms (vs. override for nodes, or
* vs. append for children) is determined by attributes of the dominant root node.
*
* @see #CHILDREN_COMBINATION_MODE_ATTRIBUTE
* @see #SELF_COMBINATION_MODE_ATTRIBUTE
* @param dominant The dominant DOM into which the recessive value/attributes/children will be merged
* @param recessive The recessive DOM, which will be merged into the dominant DOM
* @return merged DOM
*/
public static XmlNode merge(XmlNode dominant, XmlNode recessive) {
return merge(dominant, recessive, null);
}
// ----------------------------------------------------------------------
// Standard object handling
// ----------------------------------------------------------------------
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
XmlNodeImpl that = (XmlNodeImpl) o;
return 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 {
return toStringXml();
} catch (XMLStreamException e) {
return toStringObject();
}
}
public String toStringXml() throws XMLStreamException {
StringWriter writer = new StringWriter();
XmlNodeWriter.write(writer, this);
return writer.toString();
}
public 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, location, Objects::nonNull, "location", 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;
}
private static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
}