/* * 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.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.annotations.concurrency.Immutable; import com.android.ide.common.xml.XmlPrettyPrinter; import com.android.utils.ILogger; import com.android.utils.PositionXmlParser; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.io.LineReader; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; /** * Contains all actions taken during a merging invocation. */ @Immutable public class Actions { // TODO: i18n @VisibleForTesting static final String HEADER = "-- Merging decision tree log ---\n"; // defines all the records for the merging tool activity, indexed by element name+key. // iterator should be ordered by the key insertion order. private final ImmutableMap<XmlNode.NodeKey, DecisionTreeRecord> mRecords; public Actions(ImmutableMap<XmlNode.NodeKey, DecisionTreeRecord> records) { mRecords = records; } /** * Returns a {@link com.google.common.collect.ImmutableList} of {@link NodeRecord}s for the * passed xml {@link Element} * @return the node records for that element or an empty list if none exist. */ @NonNull public ImmutableList<NodeRecord> getNodeRecords(Element element) { XmlNode.NodeKey nodeKey = XmlNode.NodeKey.fromXml(element); return mRecords.containsKey(nodeKey) ? mRecords.get(nodeKey).getNodeRecords() : ImmutableList.<NodeRecord>of(); } /** * Returns a {@link com.google.common.collect.ImmutableSet} of all the element's keys that have * at least one {@link NodeRecord}. */ @NonNull public ImmutableSet<XmlNode.NodeKey> getNodeKeys() { return mRecords.keySet(); } /** * Returns an {@link ImmutableList} of {@link NodeRecord} for the element identified with the * passed key. */ @NonNull public ImmutableList<NodeRecord> getNodeRecords(XmlNode.NodeKey key) { return mRecords.containsKey(key) ? mRecords.get(key).getNodeRecords() : ImmutableList.<NodeRecord>of(); } /** * Returns a {@link ImmutableList} of all attributes names that have at least one record for * the element identified with the passed key. */ @NonNull public ImmutableList<XmlNode.NodeName> getRecordedAttributeNames(XmlNode.NodeKey nodeKey) { DecisionTreeRecord decisionTreeRecord = mRecords.get(nodeKey); if (decisionTreeRecord == null) { return ImmutableList.of(); } return decisionTreeRecord.getAttributesRecords().keySet().asList(); } /** * Returns the {@link com.google.common.collect.ImmutableList} of {@link AttributeRecord} for * the attribute identified by attributeName of the element identified by elementKey. */ @NonNull public ImmutableList<AttributeRecord> getAttributeRecords(XmlNode.NodeKey elementKey, XmlNode.NodeName attributeName) { DecisionTreeRecord decisionTreeRecord = mRecords.get(elementKey); if (decisionTreeRecord == null) { return ImmutableList.of(); } return decisionTreeRecord.getAttributeRecords(attributeName); } /** * Initial dump of the merging tool actions, need to be refined and spec'ed out properly. * @param logger logger to log to at INFO level. */ void log(ILogger logger) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(HEADER); for (Map.Entry<XmlNode.NodeKey, Actions.DecisionTreeRecord> record : mRecords.entrySet()) { stringBuilder.append(record.getKey()).append("\n"); for (Actions.NodeRecord nodeRecord : record.getValue().getNodeRecords()) { nodeRecord.print(stringBuilder); stringBuilder.append('\n'); } for (Map.Entry<XmlNode.NodeName, List<Actions.AttributeRecord>> attributeRecords : record.getValue().mAttributeRecords.entrySet()) { stringBuilder.append('\t').append(attributeRecords.getKey()).append('\n'); for (Actions.AttributeRecord attributeRecord : attributeRecords.getValue()) { stringBuilder.append("\t\t"); attributeRecord.print(stringBuilder); stringBuilder.append('\n'); } } } logger.verbose(stringBuilder.toString()); } /** * Defines all possible actions taken from the merging tool for an xml element or attribute. */ enum ActionType { /** * The element was added into the resulting merged manifest. */ ADDED, /** * The element was injected from the merger invocation parameters. */ INJECTED, /** * The element was merged with another element into the resulting merged manifest. */ MERGED, /** * The element was rejected. */ REJECTED, /** * The implied element was added was added when importing a library that expected the * element to be present by default while targeted SDK requires its declaration. */ IMPLIED, } /** * Defines an abstract record contain common metadata for elements and attributes actions. */ public abstract static class Record { @NonNull protected final ActionType mActionType; @NonNull protected final ActionLocation mActionLocation; @NonNull protected final XmlNode.NodeKey mTargetId; @Nullable protected final String mReason; private Record(@NonNull ActionType actionType, @NonNull ActionLocation actionLocation, @NonNull XmlNode.NodeKey targetId, @Nullable String reason) { mActionType = Preconditions.checkNotNull(actionType); mActionLocation = Preconditions.checkNotNull(actionLocation); mTargetId = Preconditions.checkNotNull(targetId); mReason = reason; } private Record(@NonNull Element xml) { mActionType = ActionType.valueOf(xml.getAttribute("action-type")); mActionLocation = new ActionLocation(getFirstChildElement(xml)); mTargetId = new XmlNode.NodeKey(xml.getAttribute("target-id")); String reason = xml.getAttribute("reason"); mReason = Strings.isNullOrEmpty(reason) ? null : reason; } public ActionType getActionType() { return mActionType; } public ActionLocation getActionLocation() { return mActionLocation; } public XmlNode.NodeKey getTargetId() { return mTargetId; } public void print(StringBuilder stringBuilder) { stringBuilder.append(mActionType) .append(" from ") .append(mActionLocation); if (mReason != null) { stringBuilder.append(" reason: ") .append(mReason); } } public Element toXml(Document document) { Element record = document.createElement("record"); record.setAttribute("action-type", mActionType.toString()); record.setAttribute("target-id", mTargetId.toString()); if (mReason != null) { record.setAttribute("reason", mReason); } addAttributes(record); Element location = document.createElement("location"); record.appendChild(mActionLocation.toXml(location)); record.appendChild(location); return record; } protected abstract void addAttributes(Element element); } /** * Defines a merging tool action for an xml element. */ public static class NodeRecord extends Record { private final NodeOperationType mNodeOperationType; NodeRecord(@NonNull ActionType actionType, @NonNull ActionLocation actionLocation, @NonNull XmlNode.NodeKey targetId, @Nullable String reason, @NonNull NodeOperationType nodeOperationType) { super(actionType, actionLocation, targetId, reason); this.mNodeOperationType = Preconditions.checkNotNull(nodeOperationType); } NodeRecord(@NonNull Element xml) { super(xml); mNodeOperationType = NodeOperationType.valueOf(xml.getAttribute("opType")); } @Override protected void addAttributes(Element element) { element.setAttribute("opType", mNodeOperationType.toString()); } @Override public String toString() { return "Id=" + mTargetId.toString() + " actionType=" + getActionType() + " location=" + getActionLocation() + " opType=" + mNodeOperationType; } } /** * Defines a merging tool action for an xml attribute */ public static class AttributeRecord extends Record { // first in wins which should be fine, the first // operation type will be the highest priority one private final AttributeOperationType mOperationType; AttributeRecord( @NonNull ActionType actionType, @NonNull ActionLocation actionLocation, @NonNull XmlNode.NodeKey targetId, @Nullable String reason, @Nullable AttributeOperationType operationType) { super(actionType, actionLocation, targetId, reason); this.mOperationType = operationType; } AttributeRecord(@NonNull Element xml) { super(xml); mOperationType = AttributeOperationType.valueOf(xml.getAttribute("opType")); } @Nullable public AttributeOperationType getOperationType() { return mOperationType; } @Override protected void addAttributes(Element element) { if (mOperationType != null) { element.setAttribute("opType", mOperationType.toString()); } } @Override public String toString() { return "Id=" + mTargetId + " actionType=" + getActionType() + " location=" + getActionLocation() + " opType=" + getOperationType(); } } /** * Defines an action location which is composed of a pointer to the source location (e.g. a * file) and a position within that source location. */ public static final class ActionLocation { private final XmlLoader.SourceLocation mSourceLocation; private final PositionXmlParser.Position mPosition; public ActionLocation(@NonNull XmlLoader.SourceLocation sourceLocation, @NonNull PositionXmlParser.Position position) { mSourceLocation = Preconditions.checkNotNull(sourceLocation); mPosition = Preconditions.checkNotNull(position); } ActionLocation(Element xml) { final Element location = getFirstChildElement(xml); mSourceLocation = XmlLoader.locationFromXml(location); mPosition = PositionImpl.fromXml(getNextSiblingElement(location)); } public PositionXmlParser.Position getPosition() { return mPosition; } public XmlLoader.SourceLocation getSourceLocation() { return mSourceLocation; } @Override public String toString() { return mSourceLocation.print(true) + ":" + mPosition.getLine() + ":" + mPosition.getColumn(); } public Node toXml(Element location) { location.appendChild(mSourceLocation.toXml(location.getOwnerDocument())); location.appendChild(PositionImpl.toXml(mPosition, location.getOwnerDocument())); return location; } } public String persist() throws ParserConfigurationException, IOException, SAXException { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); Document document = documentBuilderFactory.newDocumentBuilder().newDocument(); Element rootElement = document.createElement("manifest-merger-mappings"); document.appendChild(rootElement); for (Map.Entry<XmlNode.NodeKey, DecisionTreeRecord> decisionTreeRecordEntry : mRecords.entrySet()) { Element elementActions = document.createElement("element-actions"); elementActions.setAttribute("id", decisionTreeRecordEntry.getKey().toString()); decisionTreeRecordEntry.getValue().toXml(elementActions); rootElement.appendChild(elementActions); } return XmlPrettyPrinter.prettyPrint(document, false); } @Nullable public static Actions load(InputStream inputStream) throws IOException, SAXException, ParserConfigurationException { return load(new PositionXmlParser().parse(inputStream)); } @Nullable public static Actions load(String xml) throws IOException, SAXException, ParserConfigurationException { return load(new PositionXmlParser().parse(xml)); } @Nullable private static Actions load(Document document) throws IOException { if (document == null) return null; Element rootElement = document.getDocumentElement(); if (!rootElement.getNodeName().equals("manifest-merger-mappings")) { throw new IOException("File is not a manifest-merger-mappings"); } ImmutableMap.Builder<XmlNode.NodeKey, DecisionTreeRecord> records = ImmutableMap.builder(); NodeList elementActions = rootElement.getChildNodes(); for (int i = 0; i < elementActions.getLength(); i++) { if (elementActions.item(i).getNodeType() != Node.ELEMENT_NODE) continue; Element elementAction = (Element) elementActions.item(i); XmlNode.NodeKey key = new XmlNode.NodeKey(elementAction.getAttribute("id")); DecisionTreeRecord decisionTreeRecord = new DecisionTreeRecord(elementAction); records.put(key, decisionTreeRecord); } return new Actions(records.build()); } private static Element getFirstChildElement(Element element) { Node child = element.getFirstChild(); while(child.getNodeType() != Node.ELEMENT_NODE) { child = child.getNextSibling(); } return (Element) child; } private static Element getNextSiblingElement(Element element) { Node sibling = element.getNextSibling(); while(sibling != null && sibling.getNodeType() != Node.ELEMENT_NODE) { sibling = sibling.getNextSibling(); } return (Element) sibling; } public ImmutableMultimap<Integer, Record> getResultingSourceMapping(XmlDocument xmlDocument) throws ParserConfigurationException, SAXException, IOException { XmlLoader.SourceLocation inMemory = XmlLoader.UNKNOWN; XmlDocument loadedWithLineNumbers = XmlLoader.load( xmlDocument.getSelectors(), xmlDocument.getSystemPropertyResolver(), inMemory, xmlDocument.prettyPrint(), XmlDocument.Type.MAIN, Optional.<String>absent() /* mainManifestPackageName */); ImmutableMultimap.Builder<Integer, Record> mappingBuilder = ImmutableMultimap.builder(); for (XmlElement xmlElement : loadedWithLineNumbers.getRootNode().getMergeableElements()) { parse(xmlElement, mappingBuilder); } return mappingBuilder.build(); } private void parse(XmlElement element, ImmutableMultimap.Builder<Integer, Record> mappings) { DecisionTreeRecord decisionTreeRecord = mRecords.get(element.getId()); if (decisionTreeRecord != null) { Actions.NodeRecord nodeRecord = findNodeRecord(decisionTreeRecord); if (nodeRecord != null) { mappings.put(element.getPosition().getLine(), nodeRecord); } for (XmlAttribute xmlAttribute : element.getAttributes()) { Actions.AttributeRecord attributeRecord = findAttributeRecord(decisionTreeRecord, xmlAttribute); if (attributeRecord != null) { mappings.put(xmlAttribute.getPosition().getLine(), attributeRecord); } } } for (XmlElement xmlElement : element.getMergeableElements()) { parse(xmlElement, mappings); } } public String blame(XmlDocument xmlDocument) throws IOException, SAXException, ParserConfigurationException { ImmutableMultimap<Integer, Record> resultingSourceMapping = getResultingSourceMapping(xmlDocument); LineReader lineReader = new LineReader( new StringReader(xmlDocument.prettyPrint())); StringBuilder actualMappings = new StringBuilder(); String line; int count = 1; while ((line = lineReader.readLine()) != null) { actualMappings.append(count).append(line).append("\n"); if (resultingSourceMapping.containsKey(count)) { for (Record record : resultingSourceMapping.get(count)) { actualMappings.append(count).append("-->") .append(record.getActionLocation().toString()) .append("\n"); } } count++; } return actualMappings.toString(); } @Nullable private static Actions.NodeRecord findNodeRecord(DecisionTreeRecord decisionTreeRecord) { for (Actions.NodeRecord nodeRecord : decisionTreeRecord.getNodeRecords()) { if (nodeRecord.getActionType() == Actions.ActionType.ADDED) { return nodeRecord; } } return null; } @Nullable private static Actions.AttributeRecord findAttributeRecord( DecisionTreeRecord decisionTreeRecord, XmlAttribute xmlAttribute) { for (Actions.AttributeRecord attributeRecord : decisionTreeRecord .getAttributeRecords(xmlAttribute.getName())) { if (attributeRecord.getActionType() == Actions.ActionType.ADDED) { return attributeRecord; } } return null; } /** * Internal structure on how {@link com.android.manifmerger.Actions.Record}s are kept for an * xml element. * * Each xml element should have an associated DecisionTreeRecord which keeps a list of * {@link com.android.manifmerger.Actions.NodeRecord} for all the node actions related * to this xml element. * * It will also contain a map indexed by attribute name on all the attribute actions related * to that particular attribute within the xml element. * */ static class DecisionTreeRecord { // all other occurrences of the nodes decisions, in order of decisions. private final List<NodeRecord> mNodeRecords = new ArrayList<NodeRecord>(); // all attributes decisions indexed by attribute name. final Map<XmlNode.NodeName, List<AttributeRecord>> mAttributeRecords = new HashMap<XmlNode.NodeName, List<AttributeRecord>>(); ImmutableList<NodeRecord> getNodeRecords() { return ImmutableList.copyOf(mNodeRecords); } ImmutableMap<XmlNode.NodeName, List<AttributeRecord>> getAttributesRecords() { return ImmutableMap.copyOf(mAttributeRecords); } DecisionTreeRecord() { } DecisionTreeRecord(Element elementAction) { Preconditions.checkArgument(elementAction.getNodeName().equals("element-actions")); NodeList childNodes = elementAction.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (child.getNodeName().equals("node-records")) { NodeList nodeRecords = child.getChildNodes(); for (int j = 0; j < nodeRecords.getLength(); j++) { if (nodeRecords.item(j).getNodeType() != Node.ELEMENT_NODE) continue; NodeRecord nodeRecord = new NodeRecord((Element) nodeRecords.item(j)); mNodeRecords.add(nodeRecord); } } else if (child.getNodeName().equals("attribute-records")) { // id, record* Element id = getFirstChildElement((Element) child); XmlNode.NodeName nodeName = Strings.isNullOrEmpty(id.getAttribute("name")) ? XmlNode.fromNSName( id.getAttribute("namespace-uri"), id.getAttribute("prefix"), id.getAttribute("local-name")) : XmlNode.fromXmlName(id.getAttribute("name")); Element record = id; ImmutableList.Builder<AttributeRecord> attributeRecords = ImmutableList.builder(); while ((record = getNextSiblingElement(record)) != null) { AttributeRecord attributeRecord = new AttributeRecord(record); attributeRecords.add(attributeRecord); } mAttributeRecords.put(nodeName, attributeRecords.build()); } } } void addNodeRecord(NodeRecord nodeRecord) { mNodeRecords.add(nodeRecord); } ImmutableList<AttributeRecord> getAttributeRecords(XmlNode.NodeName attributeName) { List<AttributeRecord> attributeRecords = mAttributeRecords.get(attributeName); return attributeRecords == null ? ImmutableList.<AttributeRecord>of() : ImmutableList.copyOf(attributeRecords); } public void toXml(Element elementAction) { Document document = elementAction.getOwnerDocument(); Element nodeRecords = document.createElement("node-records"); elementAction.appendChild(nodeRecords); for (NodeRecord nodeRecord : mNodeRecords) { Element xmlNode = nodeRecord.toXml(document); nodeRecords.appendChild(xmlNode); } for (Map.Entry<XmlNode.NodeName, List<AttributeRecord>> nodeNameListEntry : mAttributeRecords.entrySet()) { Element attributeRecords = document.createElement("attribute-records"); elementAction.appendChild(attributeRecords); Element id = document.createElement("id"); nodeNameListEntry.getKey().persistTo(id); attributeRecords.appendChild(id); for (AttributeRecord attributeRecord : nodeNameListEntry.getValue()) { Element xmlAttributeRecord = attributeRecord.toXml(document); attributeRecords.appendChild(xmlAttributeRecord); } } } } }