| /* |
| * 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.SourceFilePosition; |
| import com.android.ide.common.blame.SourcePosition; |
| 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.ImmutableSet; |
| |
| import org.w3c.dom.Attr; |
| |
| /** |
| * Defines an XML attribute inside a {@link XmlElement}. |
| * |
| * Basically a facade object on {@link Attr} objects with some added features like automatic |
| * namespace handling, manifest merger friendly identifiers and smart replacement of shortened |
| * full qualified class names using manifest node's package setting from the the owning Android's |
| * document. |
| */ |
| public class XmlAttribute extends XmlNode { |
| |
| private final XmlElement mOwnerElement; |
| private final Attr mXml; |
| @Nullable |
| private final AttributeModel mAttributeModel; |
| |
| /** |
| * Creates a new facade object to a {@link Attr} xml attribute in a |
| * {@link XmlElement}. |
| * |
| * @param ownerElement the xml node object owning this attribute. |
| * @param xml the xml definition of the attribute. |
| */ |
| public XmlAttribute( |
| @NonNull XmlElement ownerElement, |
| @NonNull Attr xml, |
| @Nullable AttributeModel attributeModel) { |
| this.mOwnerElement = Preconditions.checkNotNull(ownerElement); |
| this.mXml = Preconditions.checkNotNull(xml); |
| this.mAttributeModel = attributeModel; |
| if (mAttributeModel != null && mAttributeModel.isPackageDependent()) { |
| String value = mXml.getValue(); |
| if (value == null || value.isEmpty()) return; |
| // placeholders are never expanded. |
| if (!PlaceholderHandler.isPlaceHolder(value)) { |
| String pkg = mOwnerElement.getDocument().getPackageNameForAttributeExpansion(); |
| // We know it's a shortened FQCN if it starts with a dot |
| // or does not contain any dot. |
| if (value.indexOf('.') == -1 || value.charAt(0) == '.') { |
| if (value.charAt(0) == '.') { |
| value = pkg + value; |
| } else { |
| value = pkg + '.' + value; |
| } |
| mXml.setValue(value); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the attribute's name, providing isolation from details like namespaces handling. |
| */ |
| @Override |
| public NodeName getName() { |
| return XmlNode.unwrapName(mXml); |
| } |
| |
| /** |
| * Returns the attribute's value |
| */ |
| public String getValue() { |
| return mXml.getValue(); |
| } |
| |
| /** |
| * Returns a display friendly identification string that can be used in machine and user |
| * readable messages. |
| */ |
| @Override |
| public NodeKey getId() { |
| // (Id of the parent element)@(my name) |
| String myName = mXml.getNamespaceURI() == null ? mXml.getName() : mXml.getLocalName(); |
| return new NodeKey(mOwnerElement.getId() + "@" + myName); |
| } |
| |
| @NonNull |
| @Override |
| public SourcePosition getPosition() { |
| try { |
| return mOwnerElement.getDocument().getNodePosition(this); |
| } catch(Exception e) { |
| return SourcePosition.UNKNOWN; |
| } |
| } |
| |
| @NonNull |
| @Override |
| public Attr getXml() { |
| return mXml; |
| } |
| |
| @Nullable |
| public AttributeModel getModel() { |
| return mAttributeModel; |
| } |
| |
| XmlElement getOwnerElement() { |
| return mOwnerElement; |
| } |
| |
| void mergeInHigherPriorityElement(XmlElement higherPriorityElement, |
| MergingReport.Builder mergingReport) { |
| |
| // does the higher priority has the same attribute as myself ? |
| Optional<XmlAttribute> higherPriorityAttributeOptional = |
| higherPriorityElement.getAttribute(getName()); |
| |
| AttributeOperationType attributeOperationType = |
| higherPriorityElement.getAttributeOperationType(getName()); |
| |
| if (higherPriorityAttributeOptional.isPresent()) { |
| |
| XmlAttribute higherPriorityAttribute = higherPriorityAttributeOptional.get(); |
| handleBothAttributePresent( |
| mergingReport, higherPriorityAttribute, attributeOperationType); |
| return; |
| } |
| |
| // it does not exist, verify if we are supposed to remove it. |
| if (attributeOperationType == AttributeOperationType.REMOVE) { |
| // record the fact the attribute was actively removed. |
| mergingReport.getActionRecorder().recordAttributeAction( |
| this, |
| Actions.ActionType.REJECTED, |
| AttributeOperationType.REMOVE); |
| return; |
| } |
| |
| // the node is not defined in the higher priority element, it's defined in this lower |
| // priority element, we need to merge this lower priority attribute value with a potential |
| // higher priority default value (implicitly set on the higher priority element). |
| String mergedValue = mergeThisAndDefaultValue(mergingReport, higherPriorityElement); |
| if (mergedValue == null) { |
| return; |
| } |
| |
| // ok merge it in the higher priority element. |
| getName().addToNode(higherPriorityElement.getXml(), mergedValue); |
| |
| // and record the action. |
| mergingReport.getActionRecorder().recordAttributeAction( |
| this, |
| Actions.ActionType.ADDED, |
| getOwnerElement().getAttributeOperationType(getName())); |
| } |
| |
| /** |
| * Handles merging of two attributes value explicitly declared in xml elements. |
| * |
| * @param report report to log errors and actions. |
| * @param higherPriority higher priority attribute we should merge this attribute with. |
| * @param operationType user operation type optionally requested by the user. |
| */ |
| private void handleBothAttributePresent( |
| MergingReport.Builder report, |
| XmlAttribute higherPriority, |
| AttributeOperationType operationType) { |
| |
| // handles tools: attribute separately. |
| |
| if (getXml().getNamespaceURI() != null |
| && getXml().getNamespaceURI().equals(SdkConstants.TOOLS_URI)) { |
| handleBothToolsAttributePresent(higherPriority); |
| return; |
| } |
| |
| // the attribute is present on both elements, there are 2 possibilities : |
| // 1. tools:replace was specified, replace the value. |
| // 2. nothing was specified, the values should be equal or this is an error. |
| if (operationType == AttributeOperationType.REPLACE) { |
| // record the fact the lower priority attribute was rejected. |
| report.getActionRecorder().recordAttributeAction( |
| this, |
| Actions.ActionType.REJECTED, |
| AttributeOperationType.REPLACE); |
| return; |
| } |
| // if the values are the same, then it's fine, otherwise flag the error. |
| if (mAttributeModel != null) { |
| String mergedValue = mAttributeModel.getMergingPolicy() |
| .merge(higherPriority.getValue(), getValue()); |
| if (mergedValue != null) { |
| higherPriority.mXml.setValue(mergedValue); |
| } else { |
| addConflictingValueMessage(report, higherPriority); |
| } |
| return; |
| } |
| // no merging policy, for now revert on checking manually for equality. |
| if (!getValue().equals(higherPriority.getValue())) { |
| addConflictingValueMessage(report, higherPriority); |
| } |
| } |
| |
| /** |
| * Handles tools: namespace attributes presence in both documents. |
| * @param higherPriority the higherPriority attribute |
| */ |
| private void handleBothToolsAttributePresent( |
| XmlAttribute higherPriority) { |
| |
| // do not merge tools:node attributes, the higher priority one wins. |
| if (getName().getLocalName().equals(NodeOperationType.NODE_LOCAL_NAME)) { |
| return; |
| } |
| |
| // everything else should be merged, duplicates should be eliminated. |
| Splitter splitter = Splitter.on(','); |
| ImmutableSet.Builder<String> targetValues = ImmutableSet.builder(); |
| targetValues.addAll(splitter.split(higherPriority.getValue())); |
| targetValues.addAll(splitter.split(getValue())); |
| higherPriority.getXml().setValue(Joiner.on(',').join(targetValues.build())); |
| } |
| |
| /** |
| * Merge this attribute value (on a lower priority element) with a implicit default value |
| * (implicitly declared on the implicitNode). |
| * @param mergingReport report to log errors and actions. |
| * @param implicitNode the lower priority node where the implicit attribute value resides. |
| * @return the merged value that should be stored in the attribute or null if nothing should |
| * be stored. |
| */ |
| private String mergeThisAndDefaultValue(MergingReport.Builder mergingReport, |
| XmlElement implicitNode) { |
| |
| String mergedValue = getValue(); |
| if (mAttributeModel == null || mAttributeModel.getDefaultValue() == null |
| || !mAttributeModel.getMergingPolicy().shouldMergeDefaultValues()) { |
| return mergedValue; |
| } |
| String defaultValue = mAttributeModel.getDefaultValue(); |
| if (defaultValue.equals(mergedValue)) { |
| // even though the lower priority attribute is only declared and its value is the same |
| // as the default value, ensure it gets added to the higher priority node. |
| return mergedValue; |
| } else { |
| // ok, the default value and actual declaration are different, delegate to the |
| // merging policy to figure out what value should be used if any. |
| mergedValue = mAttributeModel.getMergingPolicy().merge(defaultValue, mergedValue); |
| if (mergedValue == null) { |
| addIllegalImplicitOverrideMessage(mergingReport, mAttributeModel, implicitNode); |
| return null; |
| } |
| if (mergedValue.equals(defaultValue)) { |
| // no need to forcefully add an attribute to the parent with its default value |
| // since it was not declared to start with. |
| return null; |
| } |
| } |
| return mergedValue; |
| } |
| |
| /** |
| * Merge this attribute value with a lower priority node attribute default value. |
| * The attribute is not explicitly set on the implicitNode, yet it exist on this attribute |
| * {@link com.android.manifmerger.XmlElement} higher priority owner. |
| * |
| * @param mergingReport report to log errors and actions. |
| * @param implicitNode the lower priority node where the implicit attribute value resides. |
| */ |
| void mergeWithLowerPriorityDefaultValue( |
| MergingReport.Builder mergingReport, XmlElement implicitNode) { |
| |
| if (mAttributeModel == null || mAttributeModel.getDefaultValue() == null |
| || !mAttributeModel.getMergingPolicy().shouldMergeDefaultValues()) { |
| return; |
| } |
| // if this value has been explicitly set to replace the implicit default value, just |
| // log the action. |
| if (mOwnerElement.getAttributeOperationType(getName()) == AttributeOperationType.REPLACE) { |
| mergingReport.getActionRecorder().recordImplicitRejection(this, implicitNode); |
| return; |
| } |
| String mergedValue = mAttributeModel.getMergingPolicy().merge( |
| getValue(), mAttributeModel.getDefaultValue()); |
| if (mergedValue == null) { |
| addIllegalImplicitOverrideMessage(mergingReport, mAttributeModel, implicitNode); |
| } else { |
| getXml().setValue(mergedValue); |
| mergingReport.getActionRecorder().recordAttributeAction( |
| this, |
| Actions.ActionType.MERGED, |
| null /* attributeOperationType */); |
| } |
| } |
| |
| private void addIllegalImplicitOverrideMessage( |
| @NonNull MergingReport.Builder mergingReport, |
| @NonNull AttributeModel attributeModel, |
| @NonNull XmlElement implicitNode) { |
| String error = String.format("Attribute %1$s value=(%2$s) at %3$s" |
| + " cannot override implicit default value=(%4$s) at %5$s", |
| getId(), |
| getValue(), |
| printPosition(), |
| attributeModel.getDefaultValue(), |
| implicitNode.printPosition()); |
| addMessage(mergingReport, MergingReport.Record.Severity.ERROR, error); |
| } |
| |
| private void addConflictingValueMessage( |
| MergingReport.Builder report, |
| XmlAttribute higherPriority) { |
| |
| Actions.AttributeRecord attributeRecord = report.getActionRecorder() |
| .getAttributeCreationRecord(higherPriority); |
| |
| String error; |
| if (getOwnerElement().getType().getMergeType() == MergeType.MERGE_CHILDREN_ONLY) { |
| error = String.format( |
| "Attribute %1$s value=(%2$s) from %3$s\n" |
| + "\tis also present at %4$s value=(%5$s).\n" |
| + "\tAttributes of <%6$s> elements are not merged.", |
| higherPriority.getId(), |
| higherPriority.getValue(), |
| attributeRecord != null |
| ? attributeRecord.getActionLocation().print(true /*shortFormat*/) |
| : "(unknown)", |
| printPosition(), |
| getValue(), |
| getOwnerElement().getType().toXmlName()); |
| } else { |
| error = String.format( |
| "Attribute %1$s value=(%2$s) from %3$s\n" |
| + "\tis also present at %4$s value=(%5$s).\n" |
| + "\tSuggestion: add 'tools:replace=\"%6$s\"' to <%7$s> element " |
| + "at %8$s to override.", |
| higherPriority.getId(), |
| higherPriority.getValue(), |
| attributeRecord != null |
| ? attributeRecord.getActionLocation().print(true /*shortFormat*/) |
| : "(unknown)", |
| printPosition(), |
| getValue(), |
| mXml.getName(), |
| getOwnerElement().getType().toXmlName(), |
| higherPriority.getOwnerElement().printPosition()); |
| } |
| higherPriority.addMessage(report, |
| attributeRecord != null |
| ? attributeRecord.getActionLocation().getPosition() |
| : SourcePosition.UNKNOWN, |
| MergingReport.Record.Severity.ERROR, error); |
| } |
| |
| void addMessage(MergingReport.Builder report, |
| MergingReport.Record.Severity severity, |
| String message) { |
| addMessage(report, getPosition(), severity, message); |
| } |
| |
| void addMessage(MergingReport.Builder report, |
| SourcePosition position, |
| MergingReport.Record.Severity severity, |
| String message) { |
| report.addMessage( |
| new SourceFilePosition(getOwnerElement().getDocument().getSourceFile(), position), |
| severity, message); |
| } |
| |
| @NonNull |
| @Override |
| public SourceFile getSourceFile() { |
| return getOwnerElement().getSourceFile(); |
| } |
| } |