blob: dd457e74302b86e3f5b6414528fbee22827019d1 [file] [log] [blame]
/*
* 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();
}
}