com.android.manifmerger.XmlElement Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of manifest-merger Show documentation
Show all versions of manifest-merger Show documentation
A Library to merge Android manifests.
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed 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 com.android.manifmerger;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourceFile;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.res2.MergingException;
import com.android.utils.ILogger;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Xml {@link org.w3c.dom.Element} which is mergeable.
*
* A mergeable element can contains 3 types of children :
*
* - a child element, which itself may or may not be mergeable.
* - xml attributes which are related to the element.
* - tools oriented attributes to trigger specific behaviors from the merging tool
*
*
* The two main responsibilities of this class is to be capable of comparing itself against
* another instance of the same type as well as providing XML element merging capabilities.
*/
public class XmlElement extends OrphanXmlElement {
@NonNull
private final XmlDocument mDocument;
@Nullable
private final NodeOperationType mNodeOperationType;
// list of non tools related attributes.
@NonNull
private final ImmutableList mAttributes;
// map of all tools related attributes keyed by target attribute name
@NonNull
private final Map mAttributesOperationTypes;
// list of mergeable children elements.
@NonNull
private final ImmutableList mMergeableChildren;
// optional selector declared on this xml element.
@Nullable
private final Selector mSelector;
// optional list of libraries that we should ignore the minSdk version
@NonNull
private final List mOverrideUsesSdkLibrarySelectors;
public XmlElement(@NonNull Element xml, @NonNull XmlDocument document) {
super(xml);
mDocument = Preconditions.checkNotNull(document);
Selector selector = null;
List overrideUsesSdkLibrarySelectors = ImmutableList.of();
ImmutableMap.Builder attributeOperationTypeBuilder =
ImmutableMap.builder();
ImmutableList.Builder attributesListBuilder = ImmutableList.builder();
NamedNodeMap namedNodeMap = getXml().getAttributes();
NodeOperationType lastNodeOperationType = null;
for (int i = 0; i < namedNodeMap.getLength(); i++) {
Node attribute = namedNodeMap.item(i);
if (SdkConstants.TOOLS_URI.equals(attribute.getNamespaceURI())) {
String instruction = attribute.getLocalName();
if (instruction.equals(NodeOperationType.NODE_LOCAL_NAME)) {
// should we flag an error when there are more than one operation type on a node ?
lastNodeOperationType = NodeOperationType.valueOf(
SdkUtils.camelCaseToConstantName(
attribute.getNodeValue()));
} else if (instruction.equals(Selector.SELECTOR_LOCAL_NAME)) {
selector = new Selector(attribute.getNodeValue());
} else if (instruction.equals(NodeOperationType.OVERRIDE_USES_SDK)) {
String nodeValue = attribute.getNodeValue();
ImmutableList.Builder builder = ImmutableList.builder();
for (String selectorValue : Splitter.on(',').split(nodeValue)) {
builder.add(new Selector(selectorValue.trim()));
}
overrideUsesSdkLibrarySelectors = builder.build();
} else {
AttributeOperationType attributeOperationType;
try {
attributeOperationType =
AttributeOperationType.valueOf(
SdkUtils.xmlNameToConstantName(instruction));
} catch (IllegalArgumentException e) {
try {
// is this another tool's operation type that we do not care about.
OtherOperationType.valueOf(instruction);
break;
} catch (IllegalArgumentException e1) {
String errorMessage =
String.format("Invalid instruction '%1$s', "
+ "valid instructions are : %2$s",
instruction,
Joiner.on(',').join(AttributeOperationType.values())
);
throw new RuntimeException(MergingException.wrapException(e)
.withMessage(errorMessage)
.withFile(mDocument.getSourceFile())
.withPosition(XmlDocument.getNodePosition(xml)).build());
}
}
for (String attributeName : Splitter.on(',').trimResults()
.split(attribute.getNodeValue())) {
if (attributeName.indexOf(XmlUtils.NS_SEPARATOR) == -1) {
String toolsPrefix = XmlUtils
.lookupNamespacePrefix(getXml(), SdkConstants.TOOLS_URI,
SdkConstants.ANDROID_NS_NAME, false);
// automatically provide the prefix.
attributeName = toolsPrefix + XmlUtils.NS_SEPARATOR + attributeName;
}
NodeName nodeName = XmlNode.fromXmlName(attributeName);
attributeOperationTypeBuilder.put(nodeName, attributeOperationType);
}
}
}
}
mAttributesOperationTypes = attributeOperationTypeBuilder.build();
for (int i = 0; i < namedNodeMap.getLength(); i++) {
Node attribute = namedNodeMap.item(i);
XmlAttribute xmlAttribute = new XmlAttribute(
this, (Attr) attribute, getType().getAttributeModel(XmlNode.fromXmlName(
((Attr) attribute).getName())));
attributesListBuilder.add(xmlAttribute);
}
mNodeOperationType = lastNodeOperationType;
mAttributes = attributesListBuilder.build();
mMergeableChildren = initMergeableChildren();
mSelector = selector;
mOverrideUsesSdkLibrarySelectors = overrideUsesSdkLibrarySelectors;
}
/**
* Returns the owning {@link com.android.manifmerger.XmlDocument}
*/
@NonNull
public XmlDocument getDocument() {
return mDocument;
}
/**
* Returns the list of attributes for this xml element.
*/
public List getAttributes() {
return mAttributes;
}
/**
* Returns the {@link com.android.manifmerger.XmlAttribute} for an attribute present on this
* xml element, or {@link com.google.common.base.Optional#absent} if not present.
* @param attributeName the attribute name.
*/
public Optional getAttribute(NodeName attributeName) {
for (XmlAttribute xmlAttribute : mAttributes) {
if (xmlAttribute.getName().equals(attributeName)) {
return Optional.of(xmlAttribute);
}
}
return Optional.absent();
}
/**
* Get the node operation type as optionally specified by the user. If the user did not
* explicitly specify how conflicting elements should be handled, a
* {@link com.android.manifmerger.NodeOperationType#MERGE} will be returned.
*/
@NonNull
public NodeOperationType getOperationType() {
return mNodeOperationType != null
? mNodeOperationType
: NodeOperationType.MERGE;
}
/**
* Get the attribute operation type as optionally specified by the user. If the user did not
* explicitly specify how conflicting attributes should be handled, a
* {@link AttributeOperationType#STRICT} will be returned.
*/
@NonNull
public AttributeOperationType getAttributeOperationType(NodeName attributeName) {
return mAttributesOperationTypes.containsKey(attributeName)
? mAttributesOperationTypes.get(attributeName)
: AttributeOperationType.STRICT;
}
@NonNull
public Collection> getAttributeOperations() {
return mAttributesOperationTypes.entrySet();
}
@NonNull
public List getOverrideUsesSdkLibrarySelectors() {
return mOverrideUsesSdkLibrarySelectors;
}
@NonNull
@Override
public SourcePosition getPosition() {
return XmlDocument.getNodePosition(this);
}
@NonNull
@Override
public SourceFile getSourceFile() {
return mDocument.getSourceFile();
}
/**
* Merge this xml element with a lower priority node.
*
* For now, attributes will be merged. If present on both xml elements, a warning will be
* issued and the attribute merge will be rejected.
*
* @param lowerPriorityNode lower priority Xml element to merge with.
* @param mergingReport the merging report to log errors and actions.
*/
public void mergeWithLowerPriorityNode(
@NonNull XmlElement lowerPriorityNode,
@NonNull MergingReport.Builder mergingReport) {
if (mSelector != null && !mSelector.isResolvable(getDocument().getSelectors())) {
mergingReport.addMessage(getSourceFilePosition(),
MergingReport.Record.Severity.ERROR,
String.format("'tools:selector=\"%1$s\"' is not a valid library identifier, "
+ "valid identifiers are : %2$s",
mSelector.toString(),
Joiner.on(',').join(mDocument.getSelectors().getKeys())));
return;
}
mergingReport.getLogger().info("Merging " + getId()
+ " with lower " + lowerPriorityNode.printPosition());
// workaround for 0.12 release and overlay treatment of manifest entries. This will
// need to be expressed in the model instead.
MergeType mergeType = getType().getMergeType();
// if element we are merging in is not a library (an overlay or an application), we should
// always merge the attributes otherwise, we do not merge the libraries
// attributes.
if (isA(ManifestModel.NodeTypes.MANIFEST)
&& lowerPriorityNode.getDocument().getFileType() != XmlDocument.Type.LIBRARY) {
mergeType = MergeType.MERGE;
}
if (mergeType != MergeType.MERGE_CHILDREN_ONLY) {
// make a copy of all the attributes metadata, it will eliminate elements from this
// list as it finds them explicitly defined in the lower priority node.
// At the end of the explicit attributes processing, the remaining elements of this
// list will need to be checked for default value that may clash with a locally
// defined attribute.
List attributeModels =
new ArrayList(lowerPriorityNode.getType().getAttributeModels());
// merge explicit attributes from lower priority node.
for (XmlAttribute lowerPriorityAttribute : lowerPriorityNode.getAttributes()) {
lowerPriorityAttribute.mergeInHigherPriorityElement(this, mergingReport);
if (lowerPriorityAttribute.getModel() != null) {
attributeModels.remove(lowerPriorityAttribute.getModel());
}
}
// merge implicit default values from lower priority node when we have an explicit
// attribute declared on this node.
for (AttributeModel attributeModel : attributeModels) {
if (attributeModel.getDefaultValue() != null) {
Optional myAttribute = getAttribute(attributeModel.getName());
if (myAttribute.isPresent()) {
myAttribute.get().mergeWithLowerPriorityDefaultValue(
mergingReport, lowerPriorityNode);
}
}
}
}
// are we supposed to merge children ?
if (mNodeOperationType != NodeOperationType.MERGE_ONLY_ATTRIBUTES) {
mergeChildren(lowerPriorityNode, mergingReport);
} else {
// record rejection of the lower priority node's children .
for (XmlElement lowerPriorityChild : lowerPriorityNode.getMergeableElements()) {
mergingReport.getActionRecorder().recordNodeAction(this,
Actions.ActionType.REJECTED,
lowerPriorityChild);
}
}
}
@NonNull
public ImmutableList getMergeableElements() {
return mMergeableChildren;
}
/**
* Returns a child of a particular type and a particular key.
* @param type the requested child type.
* @param keyValue the requested child key.
* @return the child of {@link com.google.common.base.Optional#absent()} if no child of this
* type and key exist.
*/
@NonNull
public Optional getNodeByTypeAndKey(
ManifestModel.NodeTypes type,
@Nullable String keyValue) {
for (XmlElement xmlElement : mMergeableChildren) {
if (xmlElement.isA(type) &&
(keyValue == null || keyValue.equals(xmlElement.getKey()))) {
return Optional.of(xmlElement);
}
}
return Optional.absent();
}
/**
* Returns all immediate children of this node for a particular type, irrespective of their
* key.
* @param type the type of children element requested.
* @return the list (potentially empty) of children.
*/
@NonNull
public ImmutableList getAllNodesByType(ManifestModel.NodeTypes type) {
ImmutableList.Builder listBuilder = ImmutableList.builder();
for (XmlElement mergeableChild : initMergeableChildren()) {
if (mergeableChild.isA(type)) {
listBuilder.add(mergeableChild);
}
}
return listBuilder.build();
}
// merge this higher priority node with a lower priority node.
public void mergeChildren(@NonNull XmlElement lowerPriorityNode,
@NonNull MergingReport.Builder mergingReport) {
// read all lower priority mergeable nodes.
// if the same node is not defined in this document merge it in.
// if the same is defined, so far, give an error message.
for (XmlElement lowerPriorityChild : lowerPriorityNode.getMergeableElements()) {
if (shouldIgnore(lowerPriorityChild, mergingReport)) {
continue;
}
mergeChild(lowerPriorityChild, mergingReport);
}
}
/**
* Returns true if this element supports having a tools:selector decoration, false otherwise.
*/
public boolean supportsSelector() {
return getOperationType().isSelectable();
}
// merge a child of a lower priority node into this higher priority node.
private void mergeChild(@NonNull XmlElement lowerPriorityChild, @NonNull MergingReport.Builder mergingReport) {
ILogger logger = mergingReport.getLogger();
// If this a custom element, we just blindly merge it in.
if (lowerPriorityChild.getType() == ManifestModel.NodeTypes.CUSTOM) {
handleCustomElement(lowerPriorityChild, mergingReport);
return;
}
Optional thisChildOptional =
getNodeByTypeAndKey(lowerPriorityChild.getType(),lowerPriorityChild.getKey());
// only in the lower priority document ?
if (!thisChildOptional.isPresent()) {
addElement(lowerPriorityChild, mergingReport);
return;
}
// it's defined in both files.
logger.verbose(lowerPriorityChild.getId() + " defined in both files...");
XmlElement thisChild = thisChildOptional.get();
switch (thisChild.getType().getMergeType()) {
case CONFLICT:
addMessage(mergingReport, MergingReport.Record.Severity.ERROR, String.format(
"Node %1$s cannot be present in more than one input file and it's "
+ "present at %2$s and %3$s",
thisChild.getType(),
thisChild.printPosition(),
lowerPriorityChild.printPosition()
));
break;
case ALWAYS:
// no merging, we consume the lower priority node unmodified.
// if the two elements are equal, just skip it.
// but check first that we are not supposed to replace or remove it.
@NonNull NodeOperationType operationType =
calculateNodeOperationType(thisChild, lowerPriorityChild);
if (operationType == NodeOperationType.REMOVE ||
operationType == NodeOperationType.REPLACE) {
mergingReport.getActionRecorder().recordNodeAction(thisChild,
Actions.ActionType.REJECTED, lowerPriorityChild);
break;
}
if (thisChild.getType().areMultipleDeclarationAllowed()) {
mergeChildrenWithMultipleDeclarations(lowerPriorityChild, mergingReport);
} else {
if (!thisChild.isEquals(lowerPriorityChild)) {
addElement(lowerPriorityChild, mergingReport);
}
}
break;
default:
// 2 nodes exist, some merging need to happen
handleTwoElementsExistence(thisChild, lowerPriorityChild, mergingReport);
break;
}
}
/**
* Handles presence of custom elements (elements not part of the android or tools
* namespaces). Such elements are merged unchanged into the resulting document, and
* optionally, the namespace definition is added to the merged document root element.
* @param customElement the custom element present in the lower priority document.
* @param mergingReport the merging report to log errors and actions.
*/
private void handleCustomElement(@NonNull XmlElement customElement,
@NonNull MergingReport.Builder mergingReport) {
addElement(customElement, mergingReport);
// add the custom namespace to the document generation.
String nodeName = customElement.getXml().getNodeName();
if (!nodeName.contains(":")) {
return;
}
@NonNull String prefix = nodeName.substring(0, nodeName.indexOf(':'));
String namespace = customElement.getDocument().getRootNode()
.getXml().getAttribute(SdkConstants.XMLNS_PREFIX + prefix);
if (namespace != null) {
getDocument().getRootNode().getXml().setAttributeNS(
SdkConstants.XMLNS_URI, SdkConstants.XMLNS_PREFIX + prefix, namespace);
}
}
/**
* Merges two children when this children's type allow multiple elements declaration with the
* same key value. In that case, we only merge the lower priority child if there is not already
* an element with the same key value that is equal to the lower priority child. Two children
* are equals if they have the same attributes and children declared irrespective of the
* declaration order.
*
* @param lowerPriorityChild the lower priority element's child.
* @param mergingReport the merging report to log errors and actions.
*/
private void mergeChildrenWithMultipleDeclarations(
@NonNull XmlElement lowerPriorityChild,
@NonNull MergingReport.Builder mergingReport) {
Preconditions.checkArgument(lowerPriorityChild.getType().areMultipleDeclarationAllowed());
if (lowerPriorityChild.getType().areMultipleDeclarationAllowed()) {
for (XmlElement sameTypeChild : getAllNodesByType(lowerPriorityChild.getType())) {
if (sameTypeChild.getId().equals(lowerPriorityChild.getId()) &&
sameTypeChild.isEquals(lowerPriorityChild)) {
return;
}
}
}
// if we end up here, we never found a child of this element with the same key and strictly
// equals to the lowerPriorityChild so we should merge it in.
addElement(lowerPriorityChild, mergingReport);
}
/**
* Determine if we should completely ignore a child from any merging activity.
* There are 2 situations where we should ignore a lower priority child :
*
*
* - The associate {@link com.android.manifmerger.ManifestModel.NodeTypes} is
* annotated with {@link com.android.manifmerger.MergeType#IGNORE}
* - This element has a child of the same type with no key that has a '
* tools:node="removeAll' attribute.
*
* @param lowerPriorityChild the lower priority child we should determine eligibility for
* merging.
* @return true if the element should be ignored, false otherwise.
*/
private boolean shouldIgnore(
@NonNull XmlElement lowerPriorityChild,
@NonNull MergingReport.Builder mergingReport) {
if (lowerPriorityChild.getType().getMergeType() == MergeType.IGNORE) {
return true;
}
// do we have an element of the same type of that child with no key ?
Optional thisChildElementOptional =
getNodeByTypeAndKey(lowerPriorityChild.getType(), null /* keyValue */);
if (!thisChildElementOptional.isPresent()) {
return false;
}
XmlElement thisChild = thisChildElementOptional.get();
// are we supposed to delete all occurrences and if yes, is there a selector defined to
// filter which elements should be deleted.
boolean shouldDelete = thisChild.mNodeOperationType == NodeOperationType.REMOVE_ALL
&& (thisChild.mSelector == null
|| thisChild.mSelector.appliesTo(lowerPriorityChild));
// if we should discard this child element, record the action.
if (shouldDelete) {
mergingReport.getActionRecorder().recordNodeAction(thisChildElementOptional.get(),
Actions.ActionType.REJECTED,
lowerPriorityChild);
}
return shouldDelete;
}
/**
* Handle 2 elements (of same identity) merging.
* higher priority one has a tools:node="remove", remove the low priority one
* higher priority one has a tools:node="replace", replace the low priority one
* higher priority one has a tools:node="strict", flag the error if not equals.
* default or tools:node="merge", merge the two elements.
* @param higherPriority the higher priority node.
* @param lowerPriority the lower priority element.
* @param mergingReport the merging report to log errors and actions.
*/
private void handleTwoElementsExistence(
@NonNull XmlElement higherPriority,
@NonNull XmlElement lowerPriority,
@NonNull MergingReport.Builder mergingReport) {
@NonNull NodeOperationType operationType = calculateNodeOperationType(higherPriority, lowerPriority);
// 2 nodes exist, 3 possibilities :
// higher priority one has a tools:node="remove", remove the low priority one
// higher priority one has a tools:node="replace", replace the low priority one
// higher priority one has a tools:node="strict", flag the error if not equals.
switch(operationType) {
case MERGE:
case MERGE_ONLY_ATTRIBUTES:
// record the action
mergingReport.getActionRecorder().recordNodeAction(higherPriority,
Actions.ActionType.MERGED, lowerPriority);
// and perform the merge
higherPriority.mergeWithLowerPriorityNode(lowerPriority, mergingReport);
break;
case REMOVE:
case REPLACE:
// so far remove and replace and similar, the post validation will take
// care of removing this node in the case of REMOVE.
// just don't import the lower priority node and record the action.
mergingReport.getActionRecorder().recordNodeAction(higherPriority,
Actions.ActionType.REJECTED, lowerPriority);
break;
case STRICT:
Optional compareMessage = higherPriority.compareTo(lowerPriority);
if (compareMessage.isPresent()) {
// flag error.
addMessage(mergingReport, MergingReport.Record.Severity.ERROR, String.format(
"Node %1$s at %2$s is tagged with tools:node=\"strict\", yet "
+ "%3$s at %4$s is different : %5$s",
higherPriority.getId(),
higherPriority.printPosition(),
lowerPriority.getId(),
lowerPriority.printPosition(),
compareMessage.get()
));
}
break;
default:
mergingReport.getLogger().error(null /* throwable */,
"Unhandled node operation type %s", higherPriority.getOperationType());
break;
}
}
/**
* Calculate the effective node operation type for a higher priority node when a lower priority
* node is queried for merge.
* @param higherPriority the higher priority node which may have a {@link NodeOperationType}
* declaration and may also have a {@link Selector} declaration.
* @param lowerPriority the lower priority node that is elected for merging with the higher
* priority node.
* @return the effective {@link NodeOperationType} that should be used to affect higher and
* lower priority nodes merging.
*/
@NonNull
private static NodeOperationType calculateNodeOperationType(
@NonNull XmlElement higherPriority,
@NonNull XmlElement lowerPriority) {
@NonNull NodeOperationType operationType = higherPriority.getOperationType();
// if the operation's selector exists and the lower priority node is not selected,
// we revert to default operation type which is merge.
if (higherPriority.supportsSelector()
&& higherPriority.mSelector != null
&& !higherPriority.mSelector.appliesTo(lowerPriority)) {
operationType = NodeOperationType.MERGE;
}
return operationType;
}
/**
* Add an element and its leading comments as the last sub-element of the current element.
* @param elementToBeAdded xml element to be added to the current element.
* @param mergingReport the merging report to log errors and actions.
*/
private void addElement(
@NonNull XmlElement elementToBeAdded, @NonNull MergingReport.Builder mergingReport) {
List comments = getLeadingComments(elementToBeAdded.getXml());
// record all the actions before the node is moved from the library document to the main
// merged document.
mergingReport.getActionRecorder().recordDefaultNodeAction(elementToBeAdded);
// only in the new file, just import it.
Node node = getXml().getOwnerDocument().adoptNode(elementToBeAdded.getXml());
getXml().appendChild(node);
// also adopt the child's comments if any.
for (Node comment : comments) {
Node newComment = getXml().getOwnerDocument().adoptNode(comment);
getXml().insertBefore(newComment, node);
}
mergingReport.getLogger().verbose("Adopted " + node);
}
public boolean isEquals(XmlElement otherNode) {
return !compareTo(otherNode).isPresent();
}
/**
* Returns a potentially null (if not present) selector decoration on this element.
*/
@Nullable
public Selector getSelector() {
return mSelector;
}
/**
* Compares this element with another {@link XmlElement} ignoring all attributes belonging to
* the {@link com.android.SdkConstants#TOOLS_URI} namespace.
*
* @param other the other element to compare against.
* @return a {@link String} describing the differences between the two XML elements or
* {@link Optional#absent()} if they are equals.
*/
@NonNull
public Optional compareTo(Object other) {
if (!(other instanceof XmlElement)) {
return Optional.of("Wrong type");
}
XmlElement otherNode = (XmlElement) other;
// compare element names
if (getXml().getNamespaceURI() != null) {
if (!getXml().getLocalName().equals(otherNode.getXml().getLocalName())) {
return Optional.of(
String.format("Element names do not match: %1$s versus %2$s",
getXml().getLocalName(),
otherNode.getXml().getLocalName()));
}
// compare element ns
String thisNS = getXml().getNamespaceURI();
String otherNS = otherNode.getXml().getNamespaceURI();
if ((thisNS == null && otherNS != null)
|| (thisNS != null && !thisNS.equals(otherNS))) {
return Optional.of(
String.format("Element namespaces names do not match: %1$s versus %2$s",
thisNS, otherNS));
}
} else {
if (!getXml().getNodeName().equals(otherNode.getXml().getNodeName())) {
return Optional.of(String.format("Element names do not match: %1$s versus %2$s",
getXml().getNodeName(),
otherNode.getXml().getNodeName()));
}
}
// compare attributes, we do it twice to identify added/missing elements in both lists.
Optional message = checkAttributes(this, otherNode);
if (message.isPresent()) {
return message;
}
message = checkAttributes(otherNode, this);
if (message.isPresent()) {
return message;
}
// compare children
@NonNull List expectedChildren = filterUninterestingNodes(getXml().getChildNodes());
@NonNull List actualChildren = filterUninterestingNodes(otherNode.getXml().getChildNodes());
if (expectedChildren.size() != actualChildren.size()) {
if (expectedChildren.size() > actualChildren.size()) {
// missing some.
@NonNull List missingChildrenNames =
Lists.transform(expectedChildren, NODE_TO_NAME);
missingChildrenNames.removeAll(Lists.transform(actualChildren, NODE_TO_NAME));
return Optional.of(String.format(
"%1$s: Number of children do not match up: "
+ "expected %2$d versus %3$d at %4$s, missing %5$s",
getId(),
expectedChildren.size(),
actualChildren.size(),
otherNode.printPosition(),
Joiner.on(",").join(missingChildrenNames)));
} else {
// extra ones.
@NonNull List extraChildrenNames = Lists.transform(actualChildren, NODE_TO_NAME);
extraChildrenNames.removeAll(Lists.transform(expectedChildren, NODE_TO_NAME));
return Optional.of(String.format(
"%1$s: Number of children do not match up: "
+ "expected %2$d versus %3$d at %4$s, extra elements found : %5$s",
getId(),
expectedChildren.size(),
actualChildren.size(),
otherNode.printPosition(),
Joiner.on(",").join(expectedChildren)));
}
}
for (Node expectedChild : expectedChildren) {
if (expectedChild.getNodeType() == Node.ELEMENT_NODE) {
@NonNull XmlElement expectedChildNode = new XmlElement((Element) expectedChild, mDocument);
message = findAndCompareNode(otherNode, actualChildren, expectedChildNode);
if (message.isPresent()) {
return message;
}
}
}
return Optional.absent();
}
private Optional findAndCompareNode(
@NonNull XmlElement otherElement,
@NonNull List otherElementChildren,
@NonNull XmlElement childNode) {
Optional message = Optional.absent();
for (Node potentialNode : otherElementChildren) {
if (potentialNode.getNodeType() == Node.ELEMENT_NODE) {
@NonNull XmlElement otherChildNode = new XmlElement((Element) potentialNode, mDocument);
if (childNode.getType() == otherChildNode.getType()) {
// check if this element uses a key.
if (childNode.getType().getNodeKeyResolver().getKeyAttributesNames()
.isEmpty()) {
// no key... try all the other elements, if we find one equal, we are done.
message = childNode.compareTo(otherChildNode);
if (!message.isPresent()) {
return Optional.absent();
}
} else {
// key...
if (childNode.getKey() == null) {
// other key MUST also be null.
if (otherChildNode.getKey() == null) {
return childNode.compareTo(otherChildNode);
}
} else {
if (childNode.getKey().equals(otherChildNode.getKey())) {
return childNode.compareTo(otherChildNode);
}
}
}
}
}
}
return message.isPresent()
? message
: Optional.of(String.format("Child %1$s not found in document %2$s",
childNode.getId(),
otherElement.printPosition()));
}
@NonNull
private static List filterUninterestingNodes(@NonNull NodeList nodeList) {
List interestingNodes = new ArrayList();
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.TEXT_NODE) {
Text t = (Text) node;
if (!t.getData().trim().isEmpty()) {
interestingNodes.add(node);
}
} else if (node.getNodeType() != Node.COMMENT_NODE) {
interestingNodes.add(node);
}
}
return interestingNodes;
}
private static Optional checkAttributes(
@NonNull XmlElement expected,
@NonNull XmlElement actual) {
for (XmlAttribute expectedAttr : expected.getAttributes()) {
XmlAttribute.NodeName attributeName = expectedAttr.getName();
if (attributeName.isInNamespace(SdkConstants.TOOLS_URI)) {
continue;
}
Optional actualAttr = actual.getAttribute(attributeName);
if (actualAttr.isPresent()) {
if (!expectedAttr.getValue().equals(actualAttr.get().getValue())) {
return Optional.of(
String.format("Attribute %1$s do not match: %2$s versus %3$s at %4$s",
expectedAttr.getId(),
expectedAttr.getValue(),
actualAttr.get().getValue(),
actual.printPosition()));
}
} else {
return Optional.of(String.format("Attribute %1$s not found at %2$s",
expectedAttr.getId(), actual.printPosition()));
}
}
return Optional.absent();
}
@SuppressWarnings("SpellCheckingInspection")
private ImmutableList initMergeableChildren() {
ImmutableList.Builder mergeableNodes = new ImmutableList.Builder();
NodeList nodeList = getXml().getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (node instanceof Element) {
XmlElement xmlElement = new XmlElement((Element) node, mDocument);
mergeableNodes.add(xmlElement);
}
}
return mergeableNodes.build();
}
/**
* Returns all leading comments in the source xml before the node to be adopted.
* @param nodeToBeAdopted node that will be added as a child to this node.
*/
static List getLeadingComments(@NonNull Node nodeToBeAdopted) {
@NonNull ImmutableList.Builder nodesToAdopt = new ImmutableList.Builder();
Node previousSibling = nodeToBeAdopted.getPreviousSibling();
while (previousSibling != null
&& (previousSibling.getNodeType() == Node.COMMENT_NODE
|| previousSibling.getNodeType() == Node.TEXT_NODE)) {
// we really only care about comments.
if (previousSibling.getNodeType() == Node.COMMENT_NODE) {
nodesToAdopt.add(previousSibling);
}
previousSibling = previousSibling.getPreviousSibling();
}
return nodesToAdopt.build().reverse();
}
void addMessage(@NonNull MergingReport.Builder mergingReport,
@NonNull MergingReport.Record.Severity severity,
@NonNull String message) {
mergingReport.addMessage(getSourceFilePosition(),
severity,
message);
}
}