blob: fdf46fd068d97674a15f80183f8ed5443dd20ccd [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.ANDROID_NS_NAME_PREFIX;
import static com.android.SdkConstants.ATTR_FORMAT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.TAG_EAT_COMMENT;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_SKIP;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceType;
import com.android.utils.PositionXmlParser;
import com.android.utils.XmlUtils;
import com.google.common.collect.Lists;
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.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;
/**
* Parser for "values" files.
*
* This parses the file and returns a list of {@link ResourceItem} object.
*/
class ValueResourceParser2 {
@NonNull
private final File mFile;
/**
* Creates the parser for a given file.
* @param file the file to parse.
*/
ValueResourceParser2(@NonNull File file) {
mFile = file;
}
/**
* Parses the file and returns a list of {@link ResourceItem} objects.
* @return a list of resources.
*
* @throws MergingException if a merging exception happens
*/
@NonNull
List<ResourceItem> parseFile() throws MergingException {
Document document = parseDocument(mFile);
// get the root node
Node rootNode = document.getDocumentElement();
if (rootNode == null) {
return Collections.emptyList();
}
NodeList nodes = rootNode.getChildNodes();
final int count = nodes.getLength();
// list containing the result
List<ResourceItem> resources = Lists.newArrayListWithExpectedSize(count);
// Multimap to detect dups
Map<ResourceType, Set<String>> map = Maps.newEnumMap(ResourceType.class);
for (int i = 0, n = nodes.getLength(); i < n; i++) {
Node node = nodes.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
ResourceItem resource = getResource(node, mFile);
if (resource != null) {
// check this is not a dup
checkDuplicate(resource, map, mFile);
resources.add(resource);
if (resource.getType() == ResourceType.DECLARE_STYLEABLE) {
// Need to also create ATTR items for its children
addStyleableItems(node, resources, map, mFile);
}
}
}
return resources;
}
/**
* Returns a new ResourceItem object for a given node.
* @param node the node representing the resource.
* @return a ResourceItem object or null.
*/
static ResourceItem getResource(@NonNull Node node, @Nullable File from)
throws MergingException {
ResourceType type = getType(node, from);
String name = getName(node);
if (name != null) {
if (type != null) {
ValueResourceNameValidator.validate(name, type, from);
return new ResourceItem(name, type, node);
}
} else if (type == ResourceType.PUBLIC) {
// Allow a <public /> node with no name: this means all resources are private
return new ResourceItem("", type, node);
}
return null;
}
/**
* Returns the type of the ResourceItem based on a node's attributes.
* @param node the node
* @return the ResourceType or null if it could not be inferred.
*/
static ResourceType getType(@NonNull Node node, @Nullable File from) throws MergingException {
String nodeName = node.getLocalName();
String typeString = null;
if (TAG_ITEM.equals(nodeName)) {
Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_TYPE);
if (attribute != null) {
typeString = attribute.getValue();
}
} else if (TAG_EAT_COMMENT.equals(nodeName) || TAG_SKIP.equals(nodeName)) {
return null;
} else {
// the type is the name of the node.
typeString = nodeName;
}
if (typeString != null) {
ResourceType type = ResourceType.getEnum(typeString);
if (type != null) {
return type;
}
throw MergingException.withMessage("Unsupported type '%s'", typeString).withFile(from)
.build();
}
throw MergingException.withMessage("Unsupported node '%s'", nodeName).withFile(from).build();
}
/**
* Returns the name of the resource based a node's attributes.
* @param node the node.
* @return the name or null if it could not be inferred.
*/
static String getName(@NonNull Node node) {
Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_NAME);
if (attribute != null) {
return attribute.getValue();
}
return null;
}
/**
* Loads the DOM for a given file and returns a {@link Document} object.
* @param file the file to parse
* @return a Document object.
* @throws MergingException if a merging exception happens
*/
@NonNull
static Document parseDocument(@NonNull File file) throws MergingException {
try {
return PositionXmlParser.parse(new BufferedInputStream(new FileInputStream(file)));
} catch (SAXException e) {
throw MergingException.wrapException(e).withFile(file).build();
} catch (ParserConfigurationException e) {
throw MergingException.wrapException(e).withFile(file).build();
} catch (IOException e) {
throw MergingException.wrapException(e).withFile(file).build();
}
}
/**
* Adds any declare styleable attr items below the given declare styleable nodes
* into the given list
*
* @param styleableNode the declare styleable node
* @param list the list to add items into
* @param map map of existing items to detect dups.
*/
static void addStyleableItems(@NonNull Node styleableNode,
@NonNull List<ResourceItem> list,
@Nullable Map<ResourceType, Set<String>> map,
@Nullable File from)
throws MergingException {
assert styleableNode.getNodeName().equals(ResourceType.DECLARE_STYLEABLE.getName());
NodeList nodes = styleableNode.getChildNodes();
for (int i = 0, n = nodes.getLength(); i < n; i++) {
Node node = nodes.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
ResourceItem resource = getResource(node, from);
if (resource != null) {
assert resource.getType() == ResourceType.ATTR;
// is the attribute in the android namespace?
if (!resource.getName().startsWith(ANDROID_NS_NAME_PREFIX)) {
if (hasFormatAttribute(node) || XmlUtils.hasElementChildren(node)) {
checkDuplicate(resource, map, from);
resource.setIgnoredFromDiskMerge(true);
list.add(resource);
}
}
}
}
}
private static void checkDuplicate(@NonNull ResourceItem resource,
@Nullable Map<ResourceType, Set<String>> map,
@Nullable File from)
throws MergingException {
if (map == null) {
return;
}
String name = resource.getName();
Set<String> set = map.get(resource.getType());
if (set == null) {
set = Sets.newHashSet(name);
map.put(resource.getType(), set);
} else {
if (set.contains(name) && resource.getType() != ResourceType.PUBLIC) {
throw MergingException.withMessage(
"Found item %s/%s more than one time",
resource.getType().getDisplayName(), name).withFile(from).build();
}
set.add(name);
}
}
private static boolean hasFormatAttribute(Node node) {
return node.getAttributes().getNamedItemNS(null, ATTR_FORMAT) != null;
}
}