| /* |
| * 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; |
| } |
| } |