| /* |
| * 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.utils.PositionXmlParser; |
| 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. |
| * <p> |
| * Each action generates at least one {@link com.android.manifmerger.Actions.Record} |
| * containing enough information to generate a machine or human readable report. |
| * <p> |
| * |
| * 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. |
| * <p> |
| * |
| * This structure will permit displaying logs with co-located decisions records for each element, |
| * for instance : |
| * <pre> |
| * activity:com.foo.bar.MyApp |
| * Added from manifest.xml:31 |
| * Rejected from lib1_manifest.xml:65 |
| * </pre> |
| * |
| * <p> |
| * Each record for a node (element or attribute) will contain the following metadata : |
| * <p> |
| * |
| * <ul> |
| * <li>{@link com.android.manifmerger.Actions.ActionType} to identify whether the action |
| * applies to an attribute or an element.</li> |
| * <li>{@link com.android.manifmerger.Actions.ActionLocation} to identify the source xml |
| * location for the node.</li> |
| * </ul> |
| * |
| * <p> |
| * Elements will also contain: |
| * <ul> |
| * <li>Element name : a name composed of the element type and its key.</li> |
| * <li>{@link NodeOperationType} the highest priority tool annotation justifying the merging |
| * tool decision.</li> |
| * </ul> |
| * |
| * <p> |
| * While attributes will have: |
| * <ul> |
| * <li>element name</li> |
| * <li>attribute name : the namespace aware xml name</li> |
| * <li>{@link AttributeOperationType} the highest priority annotation justifying the merging |
| * tool decision.</li> |
| * </ul> |
| */ |
| 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") |
| private final Map<NodeKey, Actions.DecisionTreeRecord> mRecords = |
| new LinkedHashMap<NodeKey, Actions.DecisionTreeRecord>(); |
| |
| /** |
| * 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. |
| */ |
| void recordDefaultNodeAction(XmlElement xmlElement) { |
| if (!mRecords.containsKey(xmlElement.getId())) { |
| 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. |
| */ |
| void recordImpliedNodeAction(XmlElement xmlElement, String reason) { |
| NodeKey storageKey = xmlElement.getId(); |
| 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 Actions.ActionLocation( |
| xmlElement.getDocument().getSourceLocation(), |
| xmlElement.getDocument().getRootNode().getPosition()), |
| xmlElement.getId(), |
| 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( |
| XmlElement xmlElement, |
| 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( |
| XmlElement mergedElement, |
| Actions.ActionType actionType, |
| XmlElement targetElement) { |
| |
| Actions.NodeRecord record = new Actions.NodeRecord(actionType, |
| new Actions.ActionLocation( |
| targetElement.getDocument().getSourceLocation(), |
| targetElement.getPosition()), |
| targetElement.getId(), |
| 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( |
| XmlElement mergedElement, |
| Actions.NodeRecord nodeRecord) { |
| |
| NodeKey storageKey = mergedElement.getId(); |
| 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 PositionXmlParser.Position attributePosition, |
| @NonNull Actions.ActionType actionType, |
| @Nullable AttributeOperationType attributeOperationType) { |
| |
| XmlElement originElement = attribute.getOwnerElement(); |
| Actions.AttributeRecord attributeRecord = new Actions.AttributeRecord( |
| actionType, |
| new Actions.ActionLocation( |
| originElement.getDocument().getSourceLocation(), |
| attributePosition), |
| attribute.getId(), |
| 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( |
| XmlAttribute attribute, |
| Actions.AttributeRecord attributeRecord) { |
| |
| List<Actions.AttributeRecord> 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<Actions.AttributeRecord> attributeRecords = getAttributeRecords(attribute); |
| Actions.AttributeRecord attributeRecord = new Actions.AttributeRecord( |
| Actions.ActionType.REJECTED, |
| new Actions.ActionLocation( |
| implicitAttributeOwner.getDocument().getSourceLocation(), |
| implicitAttributeOwner.getPosition()), |
| attribute.getId(), |
| 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(XmlAttribute attribute) { |
| for (Actions.AttributeRecord attributeRecord : getAttributeRecords(attribute)) { |
| if (attributeRecord.getActionType() == Actions.ActionType.ADDED) { |
| return attributeRecord; |
| } |
| } |
| return null; |
| } |
| |
| private List<Actions.AttributeRecord> getAttributeRecords(XmlAttribute attribute) { |
| XmlElement originElement = attribute.getOwnerElement(); |
| NodeKey storageKey = originElement.getId(); |
| Actions.DecisionTreeRecord nodeDecisionTree = mRecords.get(storageKey); |
| // by now the node should have been added for this element. |
| assert (nodeDecisionTree != null); |
| List<Actions.AttributeRecord> attributeRecords = |
| nodeDecisionTree.mAttributeRecords.get(attribute.getName()); |
| if (attributeRecords == null) { |
| attributeRecords = new ArrayList<Actions.AttributeRecord>(); |
| nodeDecisionTree.mAttributeRecords.put(attribute.getName(), attributeRecords); |
| } |
| return attributeRecords; |
| } |
| |
| Actions build() { |
| return new Actions(new ImmutableMap.Builder<NodeKey, Actions.DecisionTreeRecord>() |
| .putAll(mRecords).build()); |
| } |
| } |