aboutsummaryrefslogtreecommitdiff
path: root/builder
diff options
context:
space:
mode:
authorXavier Ducrohet <xav@android.com>2012-11-30 17:58:29 -0800
committerXavier Ducrohet <xav@android.com>2012-12-10 16:39:27 -0800
commit3b49b176fcbfe1bbae0f63f769a39a3cb41c79ef (patch)
tree12b5ab771ec4bcbcdf2810a64373abe4fb09a5e5 /builder
parentec43c79dad9cae08f059a00b155b334f614a1e1c (diff)
downloadbuild-3b49b176fcbfe1bbae0f63f769a39a3cb41c79ef.tar.gz
Adds a ResourceMerger to the AndroidBuilder library.
This will allow us to manually merge various resource folders. This supports having multiple non-overlay folders while detecting duplicates, as well as applying overlays. The merger then creates a single resource folder that can be fed to aapt. This merger should be incremental (not yet) and be as efficient as possible. Change-Id: I9e9b32954668160f97ff12fa70cd6078386b51c6
Diffstat (limited to 'builder')
-rw-r--r--builder/prebuilts/common.jarbin43848 -> 47074 bytes
-rw-r--r--builder/prebuilts/layoutlib_api.jarbin0 -> 55394 bytes
-rw-r--r--builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java39
-rw-r--r--builder/src/main/java/com/android/builder/resources/Resource.java84
-rw-r--r--builder/src/main/java/com/android/builder/resources/ResourceFile.java60
-rw-r--r--builder/src/main/java/com/android/builder/resources/ResourceMerger.java48
-rw-r--r--builder/src/main/java/com/android/builder/resources/ResourceSet.java340
-rw-r--r--builder/src/main/java/com/android/builder/resources/ValueResourceParser.java130
-rw-r--r--builder/src/test/java/com/android/builder/resources/BaseTestCase.java41
-rw-r--r--builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java136
-rw-r--r--builder/src/test/java/com/android/builder/resources/ResourceSetTest.java85
-rw-r--r--builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java84
-rw-r--r--builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.pngbin0 -> 2574 bytes
-rw-r--r--builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.pngbin0 -> 2574 bytes
-rw-r--r--builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml14
-rw-r--r--builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml14
-rw-r--r--builder/src/test/resources/testData/baseMerge/overlay/values/values.xml6
-rw-r--r--builder/src/test/resources/testData/baseResourceSet/drawable/icon.pngbin0 -> 2574 bytes
-rw-r--r--builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.pngbin0 -> 2574 bytes
-rw-r--r--builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml14
-rw-r--r--builder/src/test/resources/testData/baseResourceSet/layout/main.xml14
-rw-r--r--builder/src/test/resources/testData/baseResourceSet/raw/foo.dat1
-rw-r--r--builder/src/test/resources/testData/baseResourceSet/values/values.xml76
-rw-r--r--builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.pngbin0 -> 2574 bytes
-rw-r--r--builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.pngbin0 -> 2574 bytes
25 files changed, 1186 insertions, 0 deletions
diff --git a/builder/prebuilts/common.jar b/builder/prebuilts/common.jar
index f6897fa..0398dbc 100644
--- a/builder/prebuilts/common.jar
+++ b/builder/prebuilts/common.jar
Binary files differ
diff --git a/builder/prebuilts/layoutlib_api.jar b/builder/prebuilts/layoutlib_api.jar
new file mode 100644
index 0000000..cd6f17a
--- /dev/null
+++ b/builder/prebuilts/layoutlib_api.jar
Binary files differ
diff --git a/builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java b/builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java
new file mode 100644
index 0000000..94593af
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.builder.resources;
+
+/**
+ * Exception when a resource is declared more than once in a {@link ResourceSet}
+ */
+public class DuplicateResourceException extends Exception {
+
+ private Resource mOne;
+ private Resource mTwo;
+
+ DuplicateResourceException(Resource one, Resource two) {
+ mOne = one;
+ mTwo = two;
+ }
+
+ public Resource getOne() {
+ return mOne;
+ }
+
+ public Resource getTwo() {
+ return mTwo;
+ }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/Resource.java b/builder/src/main/java/com/android/builder/resources/Resource.java
new file mode 100644
index 0000000..b5dd69c
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/Resource.java
@@ -0,0 +1,84 @@
+/*
+ * 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.builder.resources;
+
+import com.android.resources.ResourceType;
+import org.w3c.dom.Node;
+
+/**
+ * A resource.
+ *
+ * This includes the name, type, source file as a {@link ResourceFile} and an optional {@link Node}
+ * in case of a resource coming from a value file.
+ *
+ */
+public class Resource {
+
+ private final String mName;
+ private final ResourceType mType;
+
+ private final Node mValue;
+ private ResourceFile mSource;
+
+ Resource(String name, ResourceType type, Node value) {
+ mName = name;
+ mType = type;
+ mValue = value;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public ResourceType getType() {
+ return mType;
+ }
+
+ public ResourceFile getSource() {
+ return mSource;
+ }
+
+ public Node getValue() {
+ return mValue;
+ }
+
+ void setSource(ResourceFile sourceFile) {
+ mSource = sourceFile;
+ }
+
+ /**
+ * Returns a key for this resource. They key uniquely identifies this resource by combining
+ * resource type, qualifiers, and name.
+ * @return the key for this resource.
+ */
+ String getKey() {
+ String qualifiers = mSource != null ? mSource.getQualifiers() : null;
+ if (qualifiers != null) {
+ return mType.getName() + "-" + qualifiers + "/" + mName;
+ }
+
+ return mType.getName() + "/" + mName;
+ }
+
+ @Override
+ public String toString() {
+ return "Resource{" +
+ "mName='" + mName + '\'' +
+ ", mType=" + mType +
+ '}';
+ }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceFile.java b/builder/src/main/java/com/android/builder/resources/ResourceFile.java
new file mode 100644
index 0000000..7c335ee
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/ResourceFile.java
@@ -0,0 +1,60 @@
+/*
+ * 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.builder.resources;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.google.common.collect.Lists;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ */
+class ResourceFile {
+
+ private final File mFile;
+ private final List<Resource> mItems;
+ private final String mQualifiers;
+
+ ResourceFile(File file, Resource item, String qualifiers) {
+ mFile = file;
+ mItems = Collections.singletonList(item);
+ mQualifiers = qualifiers;
+
+ item.setSource(this);
+ }
+
+ ResourceFile(@NonNull File file, @NonNull List<Resource> items, @Nullable String qualifiers) {
+ mFile = file;
+ mItems = Lists.newArrayList(items);
+ mQualifiers = qualifiers;
+
+ for (Resource item : items) {
+ item.setSource(this);
+ }
+ }
+
+ @NonNull File getFile() {
+ return mFile;
+ }
+
+ @Nullable String getQualifiers() {
+ return mQualifiers;
+ }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceMerger.java b/builder/src/main/java/com/android/builder/resources/ResourceMerger.java
new file mode 100644
index 0000000..a059ec2
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/ResourceMerger.java
@@ -0,0 +1,48 @@
+/*
+ * 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.builder.resources;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+/**
+ */
+public class ResourceMerger {
+
+ /**
+ * The merged resources
+ */
+ private final Map<String, Resource> mItems = Maps.newHashMap();
+
+ public ResourceMerger() {
+
+ }
+
+ /**
+ * adds a new sourceset and overlays it on top of the existing resource items
+ * @param resourceSet the ResourceSet to add.
+ */
+ public void addResourceSet(ResourceSet resourceSet) {
+ // TODO figure out if we allow partial overlay through a per-resource flag.
+ mItems.putAll(resourceSet.getResourceMap());
+ }
+
+ public ResourceSet getMergedSet() {
+ return new ResourceSet(mItems);
+ }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceSet.java b/builder/src/main/java/com/android/builder/resources/ResourceSet.java
new file mode 100644
index 0000000..7fd7cb5
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/ResourceSet.java
@@ -0,0 +1,340 @@
+/*
+ * 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.builder.resources;
+
+import com.android.SdkConstants;
+import com.android.resources.FolderTypeRelationship;
+import com.android.resources.ResourceConstants;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.utils.XmlUtils;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.io.Files;
+import com.sun.xml.internal.rngom.ast.builder.BuildException;
+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 javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a set of resources at the same level (not overlay) coming from different sources
+ * (folders or resource bundles)
+ */
+public class ResourceSet {
+
+ /**
+ * Resource files
+ */
+ //private final List<ResourceFile> mFiles = Lists.newArrayList();
+
+ /**
+ * The key is the {@link com.android.builder.resources.Resource#getKey()}.
+ */
+ private final Map<String, Resource> mItems = Maps.newHashMap();
+
+ public ResourceSet() {
+ // nothing done here
+ }
+
+ ResourceSet(Map<String, Resource> items) {
+ mItems.putAll(items);
+ }
+
+ public void addSource(File file) throws DuplicateResourceException {
+ if (file.isDirectory()) {
+ readFolder(file);
+
+ } else if (file.isFile()) {
+ // TODO
+ }
+ }
+
+ public int getSize() {
+ return mItems.size();
+ }
+
+ public Collection<Resource> getResources() {
+ return mItems.values();
+ }
+
+ public Map<String, Resource> getResourceMap() {
+ return mItems;
+ }
+
+ public void writeTo(File rootFolder) throws IOException {
+ // map of XML values files to write after parsing all the files.
+ // the key is the qualifier.
+ Multimap<String, Resource> map = ArrayListMultimap.create();
+
+ for (Resource item : mItems.values()) {
+ if (item.getValue() != null) {
+ String qualifier = item.getSource().getQualifiers();
+ if (qualifier == null) {
+ qualifier = "";
+ }
+
+ map.put(qualifier, item);
+ } else {
+ // we can write the file.
+ ResourceFile resourceFile = item.getSource();
+ File file = resourceFile.getFile();
+
+ String filename = file.getName();
+ String folderName = item.getType().getName();
+ String qualifiers = resourceFile.getQualifiers();
+ if (qualifiers != null && qualifiers.length() > 0) {
+ folderName = folderName + SdkConstants.RES_QUALIFIER_SEP + qualifiers;
+ }
+
+ File typeFolder = new File(rootFolder, folderName);
+ if (!typeFolder.isDirectory()) {
+ typeFolder.mkdirs();
+ }
+
+ File outFile = new File(typeFolder, filename);
+ Files.copy(file, outFile);
+ }
+ }
+
+ // now write the values files.
+ for (String key : map.keySet()) {
+ // the key is the qualifier.
+ String folderName = key.length() > 0 ?
+ ResourceFolderType.VALUES.getName() + SdkConstants.RES_QUALIFIER_SEP + key :
+ ResourceFolderType.VALUES.getName();
+
+ File valuesFolder = new File(rootFolder, folderName);
+ valuesFolder.mkdirs();
+ File outFile = new File(valuesFolder, "values.xml");
+
+ // get the list of items to write
+ Collection<Resource> items = map.get(key);
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ factory.setIgnoringComments(true);
+ DocumentBuilder builder;
+
+ try {
+ builder = factory.newDocumentBuilder();
+ Document document = builder.newDocument();
+
+ Node rootNode = document.createElement(SdkConstants.TAG_RESOURCES);
+ document.appendChild(rootNode);
+
+ for (Resource item : items) {
+ Node adoptedNode = adoptNode(document, item.getValue());
+ rootNode.appendChild(adoptedNode);
+ }
+
+ String content = XmlUtils.toXml(document, false /*preserveWhitespace*/);
+
+ Files.write(content, outFile, Charsets.UTF_8);
+ } catch (ParserConfigurationException e) {
+ throw new BuildException(e);
+ }
+ }
+ }
+
+ /**
+ * Makes a new document adopt a node from a different document, and correctly reassign namespace
+ * and prefix
+ * @param document the new document
+ * @param node the node to adopt.
+ * @return the adopted node.
+ */
+ private Node adoptNode(Document document, Node node) {
+ Node newNode = document.adoptNode(node);
+
+ updateNamespace(newNode, document);
+
+ return newNode;
+ }
+
+ /**
+ * Updates the namespace of a given node (and its children) to work in a given document
+ * @param node the node to update
+ * @param document the new document
+ */
+ private void updateNamespace(Node node, Document document) {
+
+ // first process this node
+ processSingleNodeNamespace(node, document);
+
+ // then its attributes
+ NamedNodeMap attributes = node.getAttributes();
+ if (attributes != null) {
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ processSingleNodeNamespace(attributes.item(i), document);
+ }
+ }
+
+ // then do it for the children nodes.
+ NodeList children = node.getChildNodes();
+ if (children != null) {
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child != null) {
+ updateNamespace(child, document);
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the namespace of a given node to work with a given document.
+ * @param node the node to update
+ * @param document the new document
+ */
+ private void processSingleNodeNamespace(Node node, Document document) {
+ String ns = node.getNamespaceURI();
+ if (ns != null) {
+ NamedNodeMap docAttributes = document.getAttributes();
+
+ String prefix = getPrefixForNs(docAttributes, ns);
+ if (prefix == null) {
+ prefix = getUniqueNsAttribute(docAttributes);
+ Attr nsAttr = document.createAttribute(prefix);
+ nsAttr.setValue(ns);
+ document.getChildNodes().item(0).getAttributes().setNamedItem(nsAttr);
+ }
+
+ // set the prefix on the node, by removing the xmlns: start
+ prefix = prefix.substring(6);
+ node.setPrefix(prefix);
+ }
+ }
+
+ /**
+ * Looks for an existing prefix for a a given namespace.
+ * The prefix must start with "xmlns:". The whole prefix is returned.
+ * @param attributes the list of attributes to look through
+ * @param ns the namespace to find.
+ * @return the found prefix or null if none is found.
+ */
+ private String getPrefixForNs(NamedNodeMap attributes, String ns) {
+ if (attributes != null) {
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ if (ns.equals(attribute.getValue()) && ns.startsWith(SdkConstants.XMLNS_PREFIX)) {
+ return attribute.getName();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private String getUniqueNsAttribute(NamedNodeMap attributes) {
+ if (attributes == null) {
+ return "xmlns:ns1";
+ }
+
+ int i = 2;
+ while (true) {
+ String name = String.format("xmlns:ns%d", i++);
+ if (attributes.getNamedItem(name) == null) {
+ return name;
+ }
+ }
+ }
+
+ private void readFolder(File rootFolder) throws DuplicateResourceException {
+ File[] folders = rootFolder.listFiles();
+ if (folders != null) {
+ for (File folder : folders) {
+ if (folder.isDirectory()) {
+ parseFolder(folder);
+ }
+ }
+ }
+ }
+
+ private void parseFolder(File folder) throws DuplicateResourceException {
+
+ // get the type.
+ String folderName = folder.getName();
+ int pos = folderName.indexOf(ResourceConstants.RES_QUALIFIER_SEP);
+ ResourceFolderType folderType;
+ String qualifiers = null;
+ if (pos != -1) {
+ folderType = ResourceFolderType.getTypeByName(folderName.substring(0, pos));
+ qualifiers = folderName.substring(pos + 1);
+ } else {
+ folderType = ResourceFolderType.getTypeByName(folderName);
+ }
+
+ boolean singleResourceFile = folderType != ResourceFolderType.VALUES;
+ List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType);
+
+ // get the files
+ File[] files = folder.listFiles();
+ if (files != null && files.length > 0) {
+ for (File file : files) {
+ if (singleResourceFile) {
+ // get the resource name based on the filename
+ String name = file.getName();
+ pos = name.indexOf('.');
+ name = name.substring(0, pos);
+
+ Resource item = new Resource(name, types.get(0), null);
+ ResourceFile resourceFile = new ResourceFile(file, item, qualifiers);
+
+ checkItem(item);
+
+ mItems.put(item.getKey(), item);
+ } else {
+ ValueResourceParser parser = new ValueResourceParser(file);
+ try {
+ List<Resource> items = parser.parseFile();
+
+ ResourceFile resourceFile = new ResourceFile(file, items, qualifiers);
+
+ for (Resource item : items) {
+ checkItem(item);
+ mItems.put(item.getKey(), item);
+ }
+ } catch (FileNotFoundException e) {
+ // wont happen as we know the file exists.
+ }
+ }
+ }
+ }
+ }
+
+ private void checkItem(Resource item) throws DuplicateResourceException {
+ Resource otherItem = mItems.get(item.getKey());
+ if (otherItem != null) {
+ throw new DuplicateResourceException(item, otherItem);
+ }
+ }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java b/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java
new file mode 100644
index 0000000..2ff61df
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java
@@ -0,0 +1,130 @@
+/*
+ * 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.builder.resources;
+
+import com.android.resources.ResourceType;
+import com.google.common.collect.Lists;
+import com.sun.xml.internal.rngom.ast.builder.BuildException;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_TYPE;
+import static com.android.SdkConstants.TAG_ITEM;
+
+/**
+ */
+class ValueResourceParser {
+
+ private final File mFile;
+
+ ValueResourceParser(File file) {
+ mFile = file;
+ }
+
+ List<Resource> parseFile() throws FileNotFoundException {
+ Document document = parseDocument();
+
+ NodeList nodes = document.getChildNodes();
+
+ // get the root node
+ Node rootNode = nodes.item(0);
+ nodes = rootNode.getChildNodes();
+
+ List<Resource> resources = Lists.newArrayListWithExpectedSize(nodes.getLength());
+
+ for (int i = 0, n = nodes.getLength(); i < n; i++) {
+ Node node = nodes.item(i);
+
+ if (node.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+
+ ResourceType type = getType(node);
+ String name = getName(node);
+
+ if (type != null && name != null) {
+ Resource r = new Resource(name, type, node);
+ resources.add(r);
+ }
+ }
+
+ return resources;
+ }
+
+ private ResourceType getType(Node node) {
+ 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 {
+ // the type is the name of the node.
+ typeString = nodeName;
+ }
+
+ if (typeString != null) {
+ return ResourceType.getEnum(typeString);
+ }
+
+ return null;
+ }
+
+ private String getName(Node node) {
+ Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_NAME);
+
+ if (attribute != null) {
+ return attribute.getValue();
+ }
+
+ return null;
+ }
+
+ private Document parseDocument() throws FileNotFoundException {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ BufferedInputStream stream = new BufferedInputStream(new FileInputStream(mFile));
+ InputSource is = new InputSource(stream);
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ try {
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ return builder.parse(is);
+ } catch (Exception e) {
+ throw new BuildException(e);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+}
diff --git a/builder/src/test/java/com/android/builder/resources/BaseTestCase.java b/builder/src/test/java/com/android/builder/resources/BaseTestCase.java
new file mode 100644
index 0000000..ac9f788
--- /dev/null
+++ b/builder/src/test/java/com/android/builder/resources/BaseTestCase.java
@@ -0,0 +1,41 @@
+/*
+ * 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.builder.resources;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.util.Map;
+
+public abstract class BaseTestCase extends TestCase {
+
+ protected static File getRoot(String name) {
+ File root = new File("src/test/resources/testData/" + name);
+ assertTrue("Test folder '" + name + "' does not exist!",
+ root.isDirectory());
+
+ return root;
+ }
+
+ protected void verifyResources(ResourceSet resourceSet, String... resourceKeys) {
+ Map<String, Resource> map = resourceSet.getResourceMap();
+
+ for (String res : resourceKeys) {
+ assertNotNull("resource '" + res + "' is missing!", map.get(res));
+ }
+ }
+}
diff --git a/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java b/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java
new file mode 100644
index 0000000..6b856e2
--- /dev/null
+++ b/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.builder.resources;
+
+import com.google.common.io.Files;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+public class ResourceMergerTest extends BaseTestCase {
+
+ private static ResourceSet sMergedSet = null;
+
+ public void testMergeByCount() throws Exception {
+ ResourceSet mergedSet = getMergedSet();
+
+ assertEquals(25, mergedSet.getSize());
+ }
+
+ public void testMergedResourcesByName() throws Exception {
+ ResourceSet mergedSet = getMergedSet();
+
+ verifyResources(mergedSet,
+ "drawable/icon",
+ "drawable-ldpi/icon",
+ "drawable/icon2",
+ "raw/foo",
+ "layout/main",
+ "layout/layout_ref",
+ "layout/alias_replaced_by_file",
+ "layout/file_replaced_by_alias",
+ "drawable/color_drawable",
+ "drawable/drawable_ref",
+ "color/color",
+ "string/basic_string",
+ "string/xliff_string",
+ "string/styled_string",
+ "style/style",
+ "array/string_array",
+ "attr/dimen_attr",
+ "attr/string_attr",
+ "attr/enum_attr",
+ "attr/flag_attr",
+ "declare-styleable/declare_styleable",
+ "dimen/dimen",
+ "id/item_id",
+ "integer/integer"
+ );
+ }
+
+ public void testReplacedLayout() throws Exception {
+ ResourceSet mergedSet = getMergedSet();
+
+ Resource mainLayout = mergedSet.getResourceMap().get("layout/main");
+ ResourceFile sourceFile = mainLayout.getSource();
+ assertTrue(sourceFile.getFile().getAbsolutePath().endsWith("overlay/layout/main.xml"));
+ }
+
+ public void testReplacedAlias() throws Exception {
+ ResourceSet mergedSet = getMergedSet();
+
+ Resource layout = mergedSet.getResourceMap().get("layout/alias_replaced_by_file");
+ // since it's replaced by a file, there's no node.
+ assertNull(layout.getValue());
+ }
+
+ public void testReplacedFile() throws Exception {
+ ResourceSet mergedSet = getMergedSet();
+
+ Resource layout = mergedSet.getResourceMap().get("layout/file_replaced_by_alias");
+ // since it's replaced by a file, there's no node.
+ assertNotNull(layout.getValue());
+ }
+
+ public void testMergeWrite() throws Exception {
+ ResourceSet mergedSet = getMergedSet();
+
+ File folder = getWrittenSet();
+
+ ResourceSet writtenSet = new ResourceSet();
+ writtenSet.addSource(folder);
+
+ // compare the two sets.
+ assertEquals(mergedSet.getSize(), writtenSet.getSize());
+
+ // compare the resources are all the same
+ Map<String, Resource> map = writtenSet.getResourceMap();
+ for (Resource item : mergedSet.getResources()) {
+ assertNotNull(map.get(item.getKey()));
+ }
+ }
+
+ private static ResourceSet getMergedSet() throws DuplicateResourceException {
+ if (sMergedSet == null) {
+ File root = getRoot("baseMerge");
+
+ ResourceSet res = ResourceSetTest.getBaseResourceSet();
+
+ ResourceSet overlay = new ResourceSet();
+ overlay.addSource(new File(root, "overlay"));
+
+ ResourceMerger merger = new ResourceMerger();
+ merger.addResourceSet(res);
+ merger.addResourceSet(overlay);
+
+ sMergedSet = merger.getMergedSet();
+ }
+
+ return sMergedSet;
+ }
+
+ private static File getWrittenSet() throws DuplicateResourceException, IOException {
+ ResourceSet mergedSet = getMergedSet();
+
+ File folder = Files.createTempDir();
+
+ mergedSet.writeTo(folder);
+
+ return folder;
+ }
+}
diff --git a/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java b/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java
new file mode 100644
index 0000000..bcc7b88
--- /dev/null
+++ b/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.builder.resources;
+
+import java.io.File;
+
+public class ResourceSetTest extends BaseTestCase {
+
+ private static ResourceSet sBaseResourceSet = null;
+
+ public void testBaseResourceSetByCount() throws Exception {
+ ResourceSet resourceSet = getBaseResourceSet();
+ assertEquals(23, resourceSet.getSize());
+ }
+
+ public void testBaseResourceSetByName() throws Exception {
+ ResourceSet resourceSet = getBaseResourceSet();
+
+ verifyResources(resourceSet,
+ "drawable/icon",
+ "drawable/patch",
+ "raw/foo",
+ "layout/main",
+ "layout/layout_ref",
+ "layout/alias_replaced_by_file",
+ "layout/file_replaced_by_alias",
+ "drawable/color_drawable",
+ "drawable/drawable_ref",
+ "color/color",
+ "string/basic_string",
+ "string/xliff_string",
+ "string/styled_string",
+ "style/style",
+ "array/string_array",
+ "attr/dimen_attr",
+ "attr/string_attr",
+ "attr/enum_attr",
+ "attr/flag_attr",
+ "declare-styleable/declare_styleable",
+ "dimen/dimen",
+ "id/item_id",
+ "integer/integer"
+ );
+ }
+
+ public void testDupResourceSet() throws Exception {
+ File root = getRoot("dupResourceSet");
+
+ ResourceSet set = new ResourceSet();
+ set.addSource(new File(root, "res1"));
+ boolean gotException = false;
+ try {
+ set.addSource(new File(root, "res2"));
+ } catch (DuplicateResourceException e) {
+ gotException = true;
+ }
+
+ assertTrue(gotException);
+ }
+
+ static ResourceSet getBaseResourceSet() throws DuplicateResourceException {
+ if (sBaseResourceSet == null) {
+ File root = getRoot("baseResourceSet");
+
+ sBaseResourceSet = new ResourceSet();
+ sBaseResourceSet.addSource(root);
+ }
+
+ return sBaseResourceSet;
+ }
+}
diff --git a/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java b/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java
new file mode 100644
index 0000000..dbd31a5
--- /dev/null
+++ b/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.builder.resources;
+
+import com.google.common.collect.Maps;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ */
+public class ValueResourceParserTest extends BaseTestCase {
+
+ private static List<Resource> sResources = null;
+
+ public void testParsedResourcesByCount() throws Exception {
+ List<Resource> resources = getParsedResources();
+
+ assertEquals(18, resources.size());
+ }
+
+ public void testParsedResourcesByName() throws Exception {
+ List<Resource> resources = getParsedResources();
+
+ // convert to a map
+ Map<String, Resource> map = Maps.newHashMapWithExpectedSize(resources.size());
+ for (Resource r : resources) {
+ map.put(r.getKey(), r);
+ }
+
+ String[] resourceNames = new String[] {
+ "drawable/color_drawable",
+ "drawable/drawable_ref",
+ "color/color",
+ "string/basic_string",
+ "string/xliff_string",
+ "string/styled_string",
+ "style/style",
+ "array/string_array",
+ "attr/dimen_attr",
+ "attr/string_attr",
+ "attr/enum_attr",
+ "attr/flag_attr",
+ "declare-styleable/declare_styleable",
+ "dimen/dimen",
+ "id/item_id",
+ "integer/integer",
+ "layout/layout_ref"
+ };
+
+ for (String name : resourceNames) {
+ assertNotNull(name, map.get(name));
+ }
+ }
+
+ private static List<Resource> getParsedResources() throws FileNotFoundException {
+ if (sResources == null) {
+ File root = getRoot("baseResourceSet");
+ File values = new File(root, "values");
+ File valuesXml = new File(values, "values.xml");
+
+ ValueResourceParser parser = new ValueResourceParser(valuesXml);
+ sResources = parser.parseFile();
+ }
+
+ return sResources;
+ }
+}
diff --git a/builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png b/builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png
new file mode 100644
index 0000000..a07c69f
--- /dev/null
+++ b/builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png
Binary files differ
diff --git a/builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png b/builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png
new file mode 100644
index 0000000..a07c69f
--- /dev/null
+++ b/builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png
Binary files differ
diff --git a/builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml b/builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+<TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Test App - Basic"
+ android:id="@+id/text"
+ />
+</LinearLayout>
+
diff --git a/builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml b/builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+<TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Test App - Basic"
+ android:id="@+id/text"
+ />
+</LinearLayout>
+
diff --git a/builder/src/test/resources/testData/baseMerge/overlay/values/values.xml b/builder/src/test/resources/testData/baseMerge/overlay/values/values.xml
new file mode 100644
index 0000000..a3c90cc
--- /dev/null
+++ b/builder/src/test/resources/testData/baseMerge/overlay/values/values.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="color">#FFFFFFFF</color>
+ <string name="basic_string">overlay_string</string>
+ <item type="layout" name="file_replaced_by_alias">@layout/ref</item>
+</resources>
diff --git a/builder/src/test/resources/testData/baseResourceSet/drawable/icon.png b/builder/src/test/resources/testData/baseResourceSet/drawable/icon.png
new file mode 100644
index 0000000..a07c69f
--- /dev/null
+++ b/builder/src/test/resources/testData/baseResourceSet/drawable/icon.png
Binary files differ
diff --git a/builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.png b/builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.png
new file mode 100644
index 0000000..a07c69f
--- /dev/null
+++ b/builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.png
Binary files differ
diff --git a/builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml b/builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+<TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Test App - Basic"
+ android:id="@+id/text"
+ />
+</LinearLayout>
+
diff --git a/builder/src/test/resources/testData/baseResourceSet/layout/main.xml b/builder/src/test/resources/testData/baseResourceSet/layout/main.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/baseResourceSet/layout/main.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+<TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Test App - Basic"
+ android:id="@+id/text"
+ />
+</LinearLayout>
+
diff --git a/builder/src/test/resources/testData/baseResourceSet/raw/foo.dat b/builder/src/test/resources/testData/baseResourceSet/raw/foo.dat
new file mode 100644
index 0000000..def08db
--- /dev/null
+++ b/builder/src/test/resources/testData/baseResourceSet/raw/foo.dat
@@ -0,0 +1 @@
+Foo! \ No newline at end of file
diff --git a/builder/src/test/resources/testData/baseResourceSet/values/values.xml b/builder/src/test/resources/testData/baseResourceSet/values/values.xml
new file mode 100644
index 0000000..ee38060
--- /dev/null
+++ b/builder/src/test/resources/testData/baseResourceSet/values/values.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <drawable name="color_drawable">#ffffffff</drawable>
+ <drawable name="drawable_ref">@drawable/stat_notify_sync_anim0</drawable>
+ <color name="color">#00000000</color>
+
+ <string name="basic_string">basic_string</string>
+ <string name="xliff_string"><xliff:g id="number" example="123">%1$s</xliff:g><xliff:g id="unit" example="KB">%2$s</xliff:g></string>
+ <string name="styled_string">Forgot your username or password\?\nVisit <b>google.com/accounts/recovery</b>.</string>
+
+ <style name="style">
+ <item name="android:singleLine">true</item>
+ <item name="android:textAppearance">@style/TextAppearance.WindowTitle</item>
+ <item name="android:shadowColor">#BB000000</item>
+ <item name="android:shadowRadius">2.75</item>
+ </style>
+
+ <!-- list of 3- or 4-letter mnemonics for a 10-key numeric keypad -->
+ <string-array translatable="false" name="string_array">
+ <item></item><!-- 0 -->
+ <item></item><!-- 1 -->
+ <item>ABC</item><!-- 2 -->
+ <item>DEF</item><!-- 3 -->
+ <item>GHI</item><!-- 4 -->
+ <item>JKL</item><!-- 5 -->
+ <item>MNO</item><!-- 6 -->
+ <item>PQRS</item><!-- 7 -->
+ <item>TUV</item><!-- 8 -->
+ <item>WXYZ</item><!-- 9 -->
+ </string-array>
+
+ <attr name="dimen_attr" format="dimension" />
+
+ <!-- Default font family. -->
+ <attr name="string_attr" format="string" />
+
+ <!-- Default text typeface. -->
+ <attr name="enum_attr">
+ <enum name="normal" value="0" />
+ <enum name="sans" value="1" />
+ <enum name="serif" value="2" />
+ <enum name="monospace" value="3" />
+ </attr>
+
+ <!-- Default text typeface style. -->
+ <attr name="flag_attr">
+ <flag name="normal" value="0" />
+ <flag name="bold" value="1" />
+ <flag name="italic" value="2" />
+ </attr>
+
+ <!-- These are the standard attributes that make up a complete theme. -->
+ <declare-styleable name="declare_styleable">
+ <!-- ============== -->
+ <!-- Generic styles -->
+ <!-- ============== -->
+ <eat-comment />
+
+ <!-- Default color of foreground imagery. -->
+ <attr name="blah" format="color" />
+ <!-- Default color of foreground imagery on an inverted background. -->
+ <attr name="android:colorForegroundInverse" />
+ </declare-styleable>
+
+ <!-- The width that is used when creating thumbnails of applications. -->
+ <dimen name="dimen">164dp</dimen>
+
+ <item type="id" name="item_id" />
+
+ <integer name="integer">75</integer>
+
+ <item type="layout" name="layout_ref">@layout/ref</item>
+ <item type="layout" name="alias_replaced_by_file">@layout/ref</item>
+
+</resources>
diff --git a/builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png b/builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png
new file mode 100644
index 0000000..a07c69f
--- /dev/null
+++ b/builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png
Binary files differ
diff --git a/builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png b/builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png
new file mode 100644
index 0000000..a07c69f
--- /dev/null
+++ b/builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png
Binary files differ