com.android.manifmerger.ActionRecorder Maven / Gradle / Ivy
/*
* 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 static com.android.manifmerger.XmlNode.NodeKey;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.concurrency.GuardedBy;
import com.android.ide.common.blame.SourceFilePosition;
import com.android.ide.common.blame.SourcePosition;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Records all the actions taken by the merging tool.
*
* Each action generates at least one {@link com.android.manifmerger.Actions.Record}
* containing enough information to generate a machine or human readable report.
*
*
* The records are not organized in a temporal structure as the merging tool takes such decisions
* but are keyed by xml elements and attributes. For each node (elements or attributes), a linked
* list of actions that happened to the node is recorded to display all decisions that were made
* for that particular node.
*
*
* This structure will permit displaying logs with co-located decisions records for each element,
* for instance :
*
* activity:com.foo.bar.MyApp
* Added from manifest.xml:31
* Rejected from lib1_manifest.xml:65
*
*
*
* Each record for a node (element or attribute) will contain the following metadata :
*
*
*
* - {@link com.android.manifmerger.Actions.ActionType} to identify whether the action
* applies to an attribute or an element.
* - {@link com.android.ide.common.blame.SourceFilePosition} to identify the source xml
* location for the node.
*
*
*
* Elements will also contain:
*
* - Element name : a name composed of the element type and its key.
* - {@link NodeOperationType} the highest priority tool annotation justifying the merging
* tool decision.
*
*
*
* While attributes will have:
*
* - element name
* - attribute name : the namespace aware xml name
* - {@link AttributeOperationType} the highest priority annotation justifying the merging
* tool decision.
*
*/
public class ActionRecorder {
/**
* Defines all the records for the merging tool activity, indexed by element name+key. Iterator
* should be ordered by the key insertion order.
*
* This is not a concurrent map, so we will need to guard multi-threaded access when
* adding/removing elements.
*/
@GuardedBy("this")
@NonNull
private final Map mRecords =
new LinkedHashMap();
/**
* When the first xml file is loaded, there is nothing to merge with, however, each xml element
* and attribute added to the initial merged file need to be recorded.
*
* @param xmlElement xml element added to the initial merged document.
*/
synchronized void recordDefaultNodeAction(@NonNull XmlElement xmlElement) {
if (!mRecords.containsKey(xmlElement.getOriginalId())) {
recordNodeAction(xmlElement, Actions.ActionType.ADDED);
for (XmlAttribute xmlAttribute : xmlElement.getAttributes()) {
AttributeOperationType attributeOperation = xmlElement
.getAttributeOperationType(xmlAttribute.getName());
recordAttributeAction(
xmlAttribute, Actions.ActionType.ADDED,
attributeOperation);
}
for (XmlElement childNode : xmlElement.getMergeableElements()) {
recordDefaultNodeAction(childNode);
}
}
}
/**
* Record a node that was added due to an implicit presence in earlier SDK release but requires
* an explicit declaration in the application targeted SDK.
* @param xmlElement the implied element that was added to the resulting xml.
* @param reason optional contextual information whey the implied element was added.
*/
synchronized void recordImpliedNodeAction(@NonNull XmlElement xmlElement, @Nullable String reason) {
NodeKey storageKey = xmlElement.getOriginalId();
Actions.DecisionTreeRecord nodeDecisionTree = mRecords.get(storageKey);
if (nodeDecisionTree == null) {
nodeDecisionTree = new Actions.DecisionTreeRecord();
mRecords.put(storageKey, nodeDecisionTree);
}
Actions.NodeRecord record = new Actions.NodeRecord(Actions.ActionType.IMPLIED,
new SourceFilePosition(
xmlElement.getDocument().getSourceFile(),
xmlElement.getDocument().getRootNode().getPosition()),
xmlElement.getOriginalId(),
reason,
xmlElement.getOperationType()
);
nodeDecisionTree.addNodeRecord(record);
}
/**
* Record a node action taken by the merging tool.
*
* @param xmlElement the action's target xml element
* @param actionType the action's type
*/
synchronized void recordNodeAction(
@NonNull XmlElement xmlElement,
@NonNull Actions.ActionType actionType) {
recordNodeAction(xmlElement, actionType, xmlElement);
}
/**
* Record a node action taken by the merging tool.
*
* @param mergedElement the merged xml element
* @param actionType the action's type
* @param targetElement the action's target when the action is rejected or replaced, it
* indicates what is the element being rejected or replaced.
*/
synchronized void recordNodeAction(
@NonNull XmlElement mergedElement,
@NonNull Actions.ActionType actionType,
@NonNull XmlElement targetElement) {
Actions.NodeRecord record = new Actions.NodeRecord(actionType,
new SourceFilePosition(
targetElement.getDocument().getSourceFile(),
targetElement.getPosition()),
targetElement.getOriginalId(),
null, /* reason */
mergedElement.getOperationType()
);
recordNodeAction(mergedElement, record);
}
/**
* Records a {@link com.android.manifmerger.Actions.NodeRecord} action on a xml element.
* @param mergedElement the target element of the action.
* @param nodeRecord the record of the action.
*/
synchronized void recordNodeAction(
@NonNull XmlElement mergedElement,
@NonNull Actions.NodeRecord nodeRecord) {
NodeKey storageKey = mergedElement.getOriginalId();
Actions.DecisionTreeRecord nodeDecisionTree = mRecords.get(storageKey);
if (nodeDecisionTree == null) {
nodeDecisionTree = new Actions.DecisionTreeRecord();
mRecords.put(storageKey, nodeDecisionTree);
}
nodeDecisionTree.addNodeRecord(nodeRecord);
}
/**
* Records an attribute action taken by the merging tool
*
* @param attribute the attribute in question.
* @param actionType the action's type
* @param attributeOperationType the original tool annotation leading to the merging tool
* decision.
*/
synchronized void recordAttributeAction(
@NonNull XmlAttribute attribute,
@NonNull Actions.ActionType actionType,
@Nullable AttributeOperationType attributeOperationType) {
recordAttributeAction(
attribute, attribute.getPosition(), actionType, attributeOperationType);
}
/**
* Records an attribute action taken by the merging tool
*
* @param attribute the attribute in question.
* @param attributePosition the attribute's position.
* @param actionType the action's type
* @param attributeOperationType the original tool annotation leading to the merging tool
* decision.
*/
synchronized void recordAttributeAction(
@NonNull XmlAttribute attribute,
@NonNull SourcePosition attributePosition,
@NonNull Actions.ActionType actionType,
@Nullable AttributeOperationType attributeOperationType) {
XmlElement originElement = attribute.getOwnerElement();
Actions.AttributeRecord attributeRecord = new Actions.AttributeRecord(
actionType,
new SourceFilePosition(
originElement.getDocument().getSourceFile(),
attributePosition),
attribute.getOriginalId(),
null, /* reason */
attributeOperationType
);
recordAttributeAction(attribute, attributeRecord);
}
/**
* Record a {@link com.android.manifmerger.Actions.AttributeRecord} action for an attribute of
* an xml element.
* @param attribute the attribute in question.
* @param attributeRecord the record of the action.
*/
synchronized void recordAttributeAction(
@NonNull XmlAttribute attribute,
@NonNull Actions.AttributeRecord attributeRecord) {
List attributeRecords = getAttributeRecords(attribute);
attributeRecords.add(attributeRecord);
}
/**
* Records when a default value that should be merged was rejected due to a tools:replace
* annotation.
*
* @param attribute the attribute which default value was ignored.
* @param implicitAttributeOwner the element owning the implicit default value.
*/
synchronized void recordImplicitRejection(
@NonNull XmlAttribute attribute,
@NonNull XmlElement implicitAttributeOwner) {
List attributeRecords = getAttributeRecords(attribute);
Actions.AttributeRecord attributeRecord = new Actions.AttributeRecord(
Actions.ActionType.REJECTED,
new SourceFilePosition(
implicitAttributeOwner.getDocument().getSourceFile(),
implicitAttributeOwner.getPosition()),
attribute.getOriginalId(),
null, /* reason */
AttributeOperationType.REPLACE
);
attributeRecords.add(attributeRecord);
}
/**
* Returns the record for an attribute creation event. The attribute is "created" when it is
* added for the first time into the resulting merged xml document.
*/
@Nullable
synchronized Actions.AttributeRecord getAttributeCreationRecord(
@NonNull XmlAttribute attribute) {
for (Actions.AttributeRecord attributeRecord : getAttributeRecords(attribute)) {
if (attributeRecord.getActionType() == Actions.ActionType.ADDED) {
return attributeRecord;
}
}
return null;
}
@NonNull
private synchronized List getAttributeRecords(@NonNull XmlAttribute attribute) {
XmlElement originElement = attribute.getOwnerElement();
NodeKey storageKey = originElement.getOriginalId();
@Nullable Actions.DecisionTreeRecord nodeDecisionTree = mRecords.get(storageKey);
// by now the node should have been added for this element.
Preconditions.checkNotNull(nodeDecisionTree, "No record for key [%s]", storageKey);
List attributeRecords =
nodeDecisionTree.mAttributeRecords.get(attribute.getName());
if (attributeRecords == null) {
attributeRecords = new ArrayList();
nodeDecisionTree.mAttributeRecords.put(attribute.getName(), attributeRecords);
}
return attributeRecords;
}
@NonNull
synchronized Actions build() {
return new Actions(ImmutableMap.copyOf(mRecords));
}
}