blob: d0563adb33a65887a4302b3d43ed07f4e5933056 [file] [log] [blame]
/*
* Copyright (C) 2012 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.ide.common.res2;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.TAG_DECLARE_STYLEABLE;
import static com.android.ide.common.res2.DataFile.FileType;
import static com.android.ide.common.res2.ResourceFile.ATTR_QUALIFIER;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceType;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
/**
* Implementation of {@link DataMerger} for {@link ResourceSet}, {@link ResourceItem}, and
* {@link ResourceFile}.
*/
public class ResourceMerger extends DataMerger<ResourceItem, ResourceFile, ResourceSet> {
/**
* Override of the normal ResourceItem to handle merged item cases.
* This is mostly to deal with items that do not have a matching source file.
* This override the method returning the qualifier or the source type, to directly
* return a value instead of relying on a source file (since merged items don't have any).
*/
private static class MergedResourceItem extends ResourceItem {
@NonNull
private final String mQualifiers;
/**
* Constructs the object with a name, type and optional value.
*
* Note that the object is not fully usable as-is. It must be added to a ResourceFile first.
*
* @param name the name of the resource
* @param type the type of the resource
* @param qualifiers the qualifiers of the resource
* @param value an optional Node that represents the resource value.
*/
public MergedResourceItem(
@NonNull String name,
@NonNull ResourceType type,
@NonNull String qualifiers,
@Nullable Node value) {
super(name, type, value);
mQualifiers = qualifiers;
}
@NonNull
@Override
public String getQualifiers() {
return mQualifiers;
}
@Override
@NonNull
public FileType getSourceType() {
return FileType.MULTI;
}
}
/**
* Map of items that are purely results of merges (ie item that made up of several
* original items). The first map key is the associated qualifier for the items,
* the second map key is the item name.
*/
protected final Map<String, Map<String, ResourceItem>> mMergedItems = Maps.newHashMap();
@Override
protected ResourceSet createFromXml(Node node) throws MergingException {
ResourceSet set = new ResourceSet("");
return (ResourceSet) set.createFromXml(node);
}
@Override
protected boolean requiresMerge(@NonNull String dataItemKey) {
return dataItemKey.startsWith("declare-styleable/");
}
@Override
protected void mergeItems(
@NonNull String dataItemKey,
@NonNull List<ResourceItem> items,
@NonNull MergeConsumer<ResourceItem> consumer) throws MergingException {
boolean touched = false; // touched becomes true if one is touched.
boolean removed = true; // removed stays true if all items are removed.
for (ResourceItem item : items) {
touched |= item.isTouched();
removed &= item.isRemoved();
}
// get the name of the item (the key is the full key not just the same).
ResourceItem sourceItem = items.get(0);
String itemName = sourceItem.getName();
String qualifier = sourceItem.getQualifiers();
// get the matching mergedItem
ResourceItem previouslyWrittenItem = getMergedItem(qualifier, itemName);
try {
if (touched || (previouslyWrittenItem == null && !removed)) {
DocumentBuilder builder = mFactory.newDocumentBuilder();
Document document = builder.newDocument();
Node declareStyleableNode = document.createElementNS(null, TAG_DECLARE_STYLEABLE);
Attr nameAttr = document.createAttribute(ATTR_NAME);
nameAttr.setValue(itemName);
declareStyleableNode.getAttributes().setNamedItem(nameAttr);
// loop through all the items and gather a unique list of nodes.
// because we start with the lower priority items, this means that attr with
// format inside declare-styleable will be processed first, and added first
// while the redundant attr (with no format) will be ignored.
Set<String> attrs = Sets.newHashSet();
for (ResourceItem item : items) {
if (!item.isRemoved()) {
Node oldDeclareStyleable = item.getValue();
if (oldDeclareStyleable != null) {
NodeList children = oldDeclareStyleable.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node attrNode = children.item(i);
if (attrNode.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
if (SdkConstants.TAG_EAT_COMMENT.equals(attrNode.getLocalName())) {
continue;
}
// get the name
NamedNodeMap attributes = attrNode.getAttributes();
nameAttr = (Attr) attributes.getNamedItemNS(null, ATTR_NAME);
if (nameAttr == null) {
continue;
}
String name = nameAttr.getNodeValue();
if (attrs.contains(name)) {
continue;
}
// duplicate the node.
attrs.add(name);
Node newAttrNode = NodeUtils.duplicateNode(document, attrNode);
declareStyleableNode.appendChild(newAttrNode);
}
}
}
}
// always write it for now.
MergedResourceItem newItem = new MergedResourceItem(
itemName,
sourceItem.getType(),
qualifier,
declareStyleableNode);
// check whether the result of the merge is new or touched compared
// to the previous state.
//noinspection ConstantConditions
if (previouslyWrittenItem == null ||
!NodeUtils.compareElementNode(newItem.getValue(), previouslyWrittenItem.getValue(), false)) {
newItem.setTouched();
}
// then always add it both to the list of merged items in the merge
// and to the consumer.
addMergedItem(qualifier, newItem);
consumer.addItem(newItem);
} else if (previouslyWrittenItem != null) {
// since we are keeping the previous merge item, no need
// to add it internally, just send it to the consumer.
if (removed) {
consumer.removeItem(previouslyWrittenItem, null);
} else {
// don't need to compute but we need to write the item anyway since
// the item might be written due to the values file requiring (re)writing due
// to another res change
consumer.addItem(previouslyWrittenItem);
}
}
} catch (ParserConfigurationException e) {
throw new MergingException(e);
}
}
@Nullable
private ResourceItem getMergedItem(@NonNull String qualifiers, @NonNull String name) {
Map<String, ResourceItem> map = mMergedItems.get(qualifiers);
if (map != null) {
return map.get(name);
}
return null;
}
@Override
protected void loadMergedItems(@NonNull Node mergedItemsNode) throws MergingException {
// loop on the qualifiers.
NodeList configurationList = mergedItemsNode.getChildNodes();
for (int j = 0, n2 = configurationList.getLength(); j < n2; j++) {
Node configuration = configurationList.item(j);
if (configuration.getNodeType() != Node.ELEMENT_NODE ||
!NODE_CONFIGURATION.equals(configuration.getLocalName())) {
continue;
}
// get the qualifier value.
Attr qualifierAttr = (Attr) configuration.getAttributes().getNamedItem(
ATTR_QUALIFIER);
if (qualifierAttr == null) {
continue;
}
String qualifier = qualifierAttr.getValue();
// get the resource items
NodeList itemList = configuration.getChildNodes();
for (int k = 0, n3 = itemList.getLength(); k < n3; k++) {
Node itemNode = itemList.item(k);
if (itemNode.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
ResourceItem item = getMergedResourceItem(itemNode, qualifier);
if (item != null) {
addMergedItem(qualifier, item);
}
}
}
}
@Override
protected void writeMergedItems(Document document, Node rootNode) {
Node mergedItemsNode = document.createElement(NODE_MERGED_ITEMS);
rootNode.appendChild(mergedItemsNode);
for (String qualifier : mMergedItems.keySet()) {
Map<String, ResourceItem> itemMap = mMergedItems.get(qualifier);
Node qualifierNode = document.createElement(NODE_CONFIGURATION);
NodeUtils.addAttribute(document, qualifierNode, null, ATTR_QUALIFIER,
qualifier);
mergedItemsNode.appendChild(qualifierNode);
for (ResourceItem item : itemMap.values()) {
Node adoptedNode = item.getAdoptedNode(document);
if (adoptedNode != null) {
qualifierNode.appendChild(adoptedNode);
}
}
}
}
private void addMergedItem(@NonNull String qualifier, @NonNull ResourceItem item) {
Map<String, ResourceItem> map = mMergedItems.get(qualifier);
if (map == null) {
map = Maps.newHashMap();
mMergedItems.put(qualifier, map);
}
map.put(item.getName(), item);
}
/**
* Returns a new ResourceItem object for a given node.
* @param node the node representing the resource.
* @return a ResourceItem object or null.
*/
static MergedResourceItem getMergedResourceItem(@NonNull Node node, @NonNull String qualifiers)
throws MergingException {
ResourceType type = ValueResourceParser2.getType(node, null);
String name = ValueResourceParser2.getName(node);
if (name != null && type != null) {
return new MergedResourceItem(name, type, qualifiers, node);
}
return null;
}
}