/* * 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.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 : * <ul> * <li>a child element, which itself may or may not be mergeable.</li> * <li>xml attributes which are related to the element.</li> * <li>tools oriented attributes to trigger specific behaviors from the merging tool</li> * </ul> * * 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; private final NodeOperationType mNodeOperationType; // list of non tools related attributes. private final ImmutableList<XmlAttribute> mAttributes; // map of all tools related attributes keyed by target attribute name private final Map<NodeName, AttributeOperationType> mAttributesOperationTypes; // list of mergeable children elements. private final ImmutableList<XmlElement> 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<Selector> mOverrideUsesSdkLibrarySelectors; public XmlElement(@NonNull Element xml, @NonNull XmlDocument document) { super(xml); mDocument = Preconditions.checkNotNull(document); Selector selector = null; List<Selector> overrideUsesSdkLibrarySelectors = ImmutableList.of(); ImmutableMap.Builder<NodeName, AttributeOperationType> attributeOperationTypeBuilder = ImmutableMap.builder(); ImmutableList.Builder<XmlAttribute> 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<Selector> 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(mDocument.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<XmlAttribute> 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<XmlAttribute> 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. */ 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. */ public AttributeOperationType getAttributeOperationType(NodeName attributeName) { return mAttributesOperationTypes.containsKey(attributeName) ? mAttributesOperationTypes.get(attributeName) : AttributeOperationType.STRICT; } public Collection<Map.Entry<NodeName, AttributeOperationType>> getAttributeOperations() { return mAttributesOperationTypes.entrySet(); } @NonNull public List<Selector> getOverrideUsesSdkLibrarySelectors() { return mOverrideUsesSdkLibrarySelectors; } @NonNull @Override public SourcePosition getPosition() { return mDocument.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( XmlElement lowerPriorityNode, 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 <manifest> attributes otherwise, we do not merge the libraries // <manifest> 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<AttributeModel> attributeModels = new ArrayList<AttributeModel>(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<XmlAttribute> 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); } } } public ImmutableList<XmlElement> 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. */ public Optional<XmlElement> 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. */ public ImmutableList<XmlElement> getAllNodesByType(ManifestModel.NodeTypes type) { ImmutableList.Builder<XmlElement> 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(XmlElement lowerPriorityNode, 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(XmlElement lowerPriorityChild, 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<XmlElement> 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. 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(XmlElement customElement, MergingReport.Builder mergingReport) { addElement(customElement, mergingReport); // add the custom namespace to the document generation. String nodeName = customElement.getXml().getNodeName(); if (!nodeName.contains(":")) { return; } 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( XmlElement lowerPriorityChild, 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 : * <p> * <ul> * <li>The associate {@link com.android.manifmerger.ManifestModel.NodeTypes} is * annotated with {@link com.android.manifmerger.MergeType#IGNORE}</li> * <li>This element has a child of the same type with no key that has a ' * tools:node="removeAll' attribute.</li> * </ul> * @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( XmlElement lowerPriorityChild, 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<XmlElement> 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( XmlElement higherPriority, XmlElement lowerPriority, MergingReport.Builder mergingReport) { 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<String> 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. */ private static NodeOperationType calculateNodeOperationType( @NonNull XmlElement higherPriority, @NonNull XmlElement lowerPriority) { 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(XmlElement elementToBeAdded, MergingReport.Builder mergingReport) { List<Node> 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. */ public Optional<String> 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<String> message = checkAttributes(this, otherNode); if (message.isPresent()) { return message; } message = checkAttributes(otherNode, this); if (message.isPresent()) { return message; } // compare children List<Node> expectedChildren = filterUninterestingNodes(getXml().getChildNodes()); List<Node> actualChildren = filterUninterestingNodes(otherNode.getXml().getChildNodes()); if (expectedChildren.size() != actualChildren.size()) { if (expectedChildren.size() > actualChildren.size()) { // missing some. List<String> 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. List<String> 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) { XmlElement expectedChildNode = new XmlElement((Element) expectedChild, mDocument); message = findAndCompareNode(otherNode, actualChildren, expectedChildNode); if (message.isPresent()) { return message; } } } return Optional.absent(); } private Optional<String> findAndCompareNode( XmlElement otherElement, List<Node> otherElementChildren, XmlElement childNode) { Optional<String> message = Optional.absent(); for (Node potentialNode : otherElementChildren) { if (potentialNode.getNodeType() == Node.ELEMENT_NODE) { 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())); } private static List<Node> filterUninterestingNodes(NodeList nodeList) { List<Node> interestingNodes = new ArrayList<Node>(); 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<String> checkAttributes( XmlElement expected, XmlElement actual) { for (XmlAttribute expectedAttr : expected.getAttributes()) { XmlAttribute.NodeName attributeName = expectedAttr.getName(); if (attributeName.isInNamespace(SdkConstants.TOOLS_URI)) { continue; } Optional<XmlAttribute> 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(); } private ImmutableList<XmlElement> initMergeableChildren() { ImmutableList.Builder<XmlElement> mergeableNodes = new ImmutableList.Builder<XmlElement>(); 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<Node> getLeadingComments(Node nodeToBeAdopted) { ImmutableList.Builder<Node> nodesToAdopt = new ImmutableList.Builder<Node>(); 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(MergingReport.Builder mergingReport, MergingReport.Record.Severity severity, String message) { mergingReport.addMessage(getSourceFilePosition(), severity, message); } }