Add multi assets folder support + assets in libraries.
Xavier Ducrohet [Tue, 29 Jan 2013 22:46:16 +0000 (14:46 -0800)]
Because managing those is very similar to resources,
this changeset refactors Resource(Item*), ResourceFile,
ResoureSet and ResourceMerger into base classes DataItem,
DataFile, DataSet and DataMerger providing most of the
mechanism to incrementally merge any type of data.

(* for consistency, Resource is renamed ResourceItem)

New classes to support the assets: AssetItem, AssetFile,
AssetSet, AssetMerger.

Also refactors the test data and add new test and test data
specifically for the asset.

Change-Id: I75d0a95aa330ab9ce25132800fe9ba2e57710b87

100 files changed:
builder/src/main/java/com/android/builder/VariantConfiguration.java
builder/src/main/java/com/android/builder/resources/AssetFile.java [copied from builder/src/main/java/com/android/builder/resources/ResourceMap.java with 52% similarity]
builder/src/main/java/com/android/builder/resources/AssetItem.java [copied from builder/src/main/java/com/android/builder/resources/ResourceMap.java with 52% similarity]
builder/src/main/java/com/android/builder/resources/AssetMerger.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/resources/AssetSet.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/resources/DataFile.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/resources/DataItem.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/resources/DataMap.java [moved from builder/src/main/java/com/android/builder/resources/ResourceMap.java with 66% similarity]
builder/src/main/java/com/android/builder/resources/DataMerger.java [new file with mode: 0755]
builder/src/main/java/com/android/builder/resources/DataSet.java [new file with mode: 0755]
builder/src/main/java/com/android/builder/resources/DuplicateDataException.java [moved from builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java with 75% similarity]
builder/src/main/java/com/android/builder/resources/Resource.java [deleted file]
builder/src/main/java/com/android/builder/resources/ResourceFile.java
builder/src/main/java/com/android/builder/resources/ResourceItem.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/resources/ResourceMerger.java [changed mode: 0755->0644]
builder/src/main/java/com/android/builder/resources/ResourceSet.java [changed mode: 0755->0644]
builder/src/main/java/com/android/builder/resources/ValueResourceParser.java
builder/src/test/java/com/android/builder/TestUtils.java
builder/src/test/java/com/android/builder/resources/AssetMergerTest.java [new file with mode: 0755]
builder/src/test/java/com/android/builder/resources/AssetSetTest.java [new file with mode: 0644]
builder/src/test/java/com/android/builder/resources/BaseTestCase.java
builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java
builder/src/test/java/com/android/builder/resources/ResourceSetTest.java
builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java
builder/src/test/resources/testData/assets/baseMerge/merger.xml [new file with mode: 0644]
builder/src/test/resources/testData/assets/baseMerge/overlay/icon.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/removed_overlay.png with 100% similarity]
builder/src/test/resources/testData/assets/baseMerge/overlay/icon2.png [copied from builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png with 100% similarity]
builder/src/test/resources/testData/assets/baseSet/foo.dat [copied from builder/src/test/resources/testData/baseResourceSet/raw/foo.dat with 100% similarity]
builder/src/test/resources/testData/assets/baseSet/icon.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/assets/baseSet/main.xml [copied from builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml with 100% similarity]
builder/src/test/resources/testData/assets/baseSet/values.xml [copied from builder/src/test/resources/testData/baseResourceSet/values/values.xml with 100% similarity]
builder/src/test/resources/testData/assets/dupSet/assets1/icon.png [copied from builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png with 100% similarity]
builder/src/test/resources/testData/assets/dupSet/assets2/icon.png [moved from builder/src/test/resources/testData/baseResourceSet/drawable/icon.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/assetOut/overlay_added.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/assetOut/overlay_removed.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable-ldpi/removed.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/assetOut/removed.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable-ldpi/removed.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/assetOut/touched.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/touched.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/assetOut/untouched.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/untouched.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/main/added.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/touched.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/main/overlay_added.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/main/overlay_removed.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable-hdpi/new_alternate.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/main/touched.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/touched.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/main/untouched.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/untouched.png with 100% similarity]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/merger.xml [new file with mode: 0644]
builder/src/test/resources/testData/assets/incMergeData/basicFiles/overlay/overlay_added.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/resources/baseMerge/merger.xml [moved from builder/src/test/resources/testData/baseMerge/merger.xml with 71% similarity]
builder/src/test/resources/testData/resources/baseMerge/overlay/drawable-ldpi/icon.png [moved from builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png with 100% similarity]
builder/src/test/resources/testData/resources/baseMerge/overlay/drawable/icon2.png [moved from builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png with 100% similarity]
builder/src/test/resources/testData/resources/baseMerge/overlay/layout/alias_replaced_by_file.xml [moved from builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml with 100% similarity]
builder/src/test/resources/testData/resources/baseMerge/overlay/layout/main.xml [moved from builder/src/test/resources/testData/baseResourceSet/layout/main.xml with 100% similarity]
builder/src/test/resources/testData/resources/baseMerge/overlay/values/values.xml [moved from builder/src/test/resources/testData/baseMerge/overlay/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/baseSet/drawable/icon.png [moved from builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png with 100% similarity]
builder/src/test/resources/testData/resources/baseSet/drawable/patch.9.png [moved from builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.png with 100% similarity]
builder/src/test/resources/testData/resources/baseSet/layout/file_replaced_by_alias.xml [moved from builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml with 100% similarity]
builder/src/test/resources/testData/resources/baseSet/layout/main.xml [moved from builder/src/test/resources/testData/incMergeData/filesVsValues/main/layout/main.xml with 100% similarity]
builder/src/test/resources/testData/resources/baseSet/raw/foo.dat [moved from builder/src/test/resources/testData/baseResourceSet/raw/foo.dat with 100% similarity]
builder/src/test/resources/testData/resources/baseSet/values/values.xml [moved from builder/src/test/resources/testData/baseResourceSet/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/baseSet/values/values.xml~ [moved from builder/src/test/resources/testData/baseResourceSet/values/values.xml~ with 100% similarity]
builder/src/test/resources/testData/resources/dupSet/res1/drawable/icon.png [copied from builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png with 100% similarity]
builder/src/test/resources/testData/resources/dupSet/res2/drawable/icon.png [moved from builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/main/drawable/new_overlay.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/main/drawable/removed_overlay.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/removed_overlay.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/main/drawable/touched.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/touched.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/main/drawable/untouched.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/untouched.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/merger.xml [moved from builder/src/test/resources/testData/incMergeData/basicFiles/merger.xml with 90% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/new_overlay.png [copied from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/overlay/drawable-hdpi/new_alternate.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable-hdpi/new_alternate.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/overlay/drawable/new_overlay.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/resOut/drawable-ldpi/removed.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable-ldpi/removed.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/resOut/drawable/new_overlay.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/new_overlay.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/resOut/drawable/removed_overlay.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/removed_overlay.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/resOut/drawable/touched.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/touched.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicFiles/resOut/drawable/untouched.png [moved from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/untouched.png with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues/main/values/values.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues/main/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues/merger.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues/merger.xml with 86% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues/overlay/values-fr/values.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues/overlay/values-fr/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues/overlay/values/values.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues/overlay/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues/resOut/values-en/values.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues/resOut/values-en/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues/resOut/values/values.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues/resOut/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues2/main/values/values.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues2/main/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues2/merger.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues2/merger.xml with 84% similarity]
builder/src/test/resources/testData/resources/incMergeData/basicValues2/resOut/values/values.xml [moved from builder/src/test/resources/testData/incMergeData/basicValues2/resOut/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/filesVsValues/main/layout/alias_replaced_by_file.xml [moved from builder/src/test/resources/testData/incMergeData/filesVsValues/main/layout/alias_replaced_by_file.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/filesVsValues/main/layout/main.xml [moved from builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/layout/main.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/filesVsValues/main/values/values.xml [moved from builder/src/test/resources/testData/incMergeData/filesVsValues/main/values/values.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/filesVsValues/merger.xml [moved from builder/src/test/resources/testData/incMergeData/filesVsValues/merger.xml with 91% similarity]
builder/src/test/resources/testData/resources/incMergeData/filesVsValues/resOut/layout/file_replaced_by_alias.xml [moved from builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/layout/file_replaced_by_alias.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/filesVsValues/resOut/layout/main.xml [moved from builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml with 100% similarity]
builder/src/test/resources/testData/resources/incMergeData/filesVsValues/resOut/values/values.xml [moved from builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/values/values.xml with 100% similarity]
gradle/src/main/groovy/com/android/build/gradle/AppPlugin.groovy
gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
gradle/src/main/groovy/com/android/build/gradle/BuildVariant.groovy
gradle/src/main/groovy/com/android/build/gradle/LibraryPlugin.groovy
gradle/src/main/groovy/com/android/build/gradle/internal/ApplicationVariant.groovy
gradle/src/main/groovy/com/android/build/gradle/internal/DefaultBuildVariant.groovy
gradle/src/main/groovy/com/android/build/gradle/tasks/MergeAssets.groovy [new file with mode: 0644]
gradle/src/main/groovy/com/android/build/gradle/tasks/MergeResources.groovy
gradle/src/main/groovy/com/android/build/gradle/tasks/ProcessAndroidResources.groovy
gradle/src/test/groovy/com/android/build/gradle/AppPluginDslTest.groovy
gradle/src/test/groovy/com/android/build/gradle/LibraryPluginDslTest.groovy

index 24105f0..4cfebca 100644 (file)
@@ -19,6 +19,7 @@ package com.android.builder;
 import com.android.annotations.NonNull;
 import com.android.annotations.Nullable;
 import com.android.annotations.VisibleForTesting;
+import com.android.builder.resources.AssetSet;
 import com.android.builder.resources.ResourceSet;
 import com.android.builder.signing.SigningConfig;
 import com.google.common.collect.Lists;
@@ -573,6 +574,58 @@ public class VariantConfiguration {
     }
 
     /**
+     * Returns the dynamic list of {@link AssetSet} based on the configuration, its dependencies,
+     * as well as tested config if applicable (test of a library).
+     *
+     * The list is ordered in ascending order of importance, meaning the first set is meant to be
+     * overridden by the 2nd one and so on. This is meant to facilitate usage of the list in a
+     * {@link com.android.builder.resources.ResourceMerger}.
+     *
+     * @return a list ResourceSet.
+     */
+    @NonNull public List<AssetSet> getAssetSets() {
+        List<AssetSet> assetSets = Lists.newArrayList();
+
+        // the list of dependency must be reversed to use the right overlay order.
+        for (int n = mFlatLibraries.size() - 1 ; n >= 0 ; n--) {
+            AndroidDependency dependency = mFlatLibraries.get(n);
+            File assetFolder = dependency.getAssetsFolder();
+            if (assetFolder != null) {
+                AssetSet assetSet = new AssetSet(dependency.getFolder().getName());
+                assetSet.addSource(assetFolder);
+                assetSets.add(assetSet);
+            }
+        }
+
+        Set<File> mainResDirs = mDefaultSourceProvider.getAssetsDirectories();
+
+        AssetSet assetSet = new AssetSet(ProductFlavor.MAIN);
+        assetSet.addSources(mainResDirs);
+        assetSets.add(assetSet);
+
+        // the list of flavor must be reversed to use the right overlay order.
+        for (int n = mFlavorSourceProviders.size() - 1; n >= 0 ; n--) {
+            SourceProvider sourceProvider = mFlavorSourceProviders.get(n);
+
+            Set<File> flavorResDirs = sourceProvider.getAssetsDirectories();
+            // we need the same of the flavor config, but it's in a different list.
+            // This is fine as both list are parallel collections with the same number of items.
+            assetSet = new AssetSet(mFlavorConfigs.get(n).getName());
+            assetSet.addSources(flavorResDirs);
+            assetSets.add(assetSet);
+        }
+
+        if (mBuildTypeSourceProvider != null) {
+            Set<File> typeResDirs = mBuildTypeSourceProvider.getAssetsDirectories();
+            assetSet = new AssetSet(mBuildType.getName());
+            assetSet.addSources(typeResDirs);
+            assetSets.add(assetSet);
+        }
+
+        return assetSets;
+    }
+
+    /**
      * Returns all the renderscript import folder that are outside of the current project.
      */
     @NonNull
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2013 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.
 package com.android.builder.resources;
 
 import com.android.annotations.NonNull;
-import com.google.common.collect.ListMultimap;
+
+import java.io.File;
 
 /**
- * A Resource Map able to provide a {@link ListMultimap} of Resources where the keys are
- * the value returned by {@link Resource#getKey()}
+ * Represents a file in an asset folder.
  */
-interface ResourceMap {
-
-    /**
-     * Returns the number of resources.
-     * @return the number of resources.
-     */
-    int size();
+class AssetFile extends DataFile<AssetItem> {
 
     /**
-     * a Multi map of (key, resource) where key is the result of
-     * {@link com.android.builder.resources.Resource#getKey()}
-     * @return a non null map
+     * Creates a resource file with a single resource item.
+     *
+     * The source file is set on the item with {@link AssetItem#setSource(DataFile)}
+     *
+     * @param file the File
+     * @param item the resource item
      */
-    @NonNull
-    ListMultimap<String, Resource> getResourceMap();
+    AssetFile(@NonNull File file, @NonNull AssetItem item) {
+        super(file, item);
+    }
 }
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2013 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.
 package com.android.builder.resources;
 
 import com.android.annotations.NonNull;
-import com.google.common.collect.ListMultimap;
 
 /**
- * A Resource Map able to provide a {@link ListMultimap} of Resources where the keys are
- * the value returned by {@link Resource#getKey()}
+ * An asset.
+ *
+ * This includes the name and source file as a {@link AssetFile}.
+ *
  */
-interface ResourceMap {
-
-    /**
-     * Returns the number of resources.
-     * @return the number of resources.
-     */
-    int size();
+class AssetItem extends DataItem<AssetFile> {
 
     /**
-     * a Multi map of (key, resource) where key is the result of
-     * {@link com.android.builder.resources.Resource#getKey()}
-     * @return a non null map
+     * 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
      */
-    @NonNull
-    ListMultimap<String, Resource> getResourceMap();
+    AssetItem(@NonNull String name) {
+        super(name);
+    }
 }
diff --git a/builder/src/main/java/com/android/builder/resources/AssetMerger.java b/builder/src/main/java/com/android/builder/resources/AssetMerger.java
new file mode 100644 (file)
index 0000000..3a0bb46
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 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.builder.internal.util.concurrent.WaitableExecutor;
+import com.google.common.io.Files;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+/**
+ * Implementation of {@link DataMerger} for {@link AssetSet}, {@link AssetItem}, and
+ * {@link AssetFile}.
+ */
+public class AssetMerger extends DataMerger<AssetItem, AssetFile, AssetSet> {
+
+    @Override
+    protected void removeItem(File rootFolder, AssetItem item, AssetItem replacedBy) {
+        // only remove the file if there is no replacement.
+        if (replacedBy == null) {
+            File removedFile = new File(rootFolder, item.getName());
+            removedFile.delete();
+        }
+    }
+
+    @Override
+    protected void writeItem(@NonNull final File rootFolder, @NonNull final AssetItem item,
+                             @NonNull WaitableExecutor executor) throws IOException {
+        // Only write it if the state is TOUCHED.
+        if (item.isTouched()) {
+            executor.execute(new Callable() {
+                @Override
+                public Object call() throws Exception {
+                    AssetFile assetFile = item.getSource();
+
+                    File file = assetFile.getFile();
+                    String filename = file.getName();
+
+                    File outFile = new File(rootFolder, filename);
+                    Files.copy(file, outFile);
+
+                    return null;
+                }
+            });
+        }
+    }
+
+    @Override
+    protected AssetSet createFromXml(Node node) {
+        AssetSet set = new AssetSet("");
+        return (AssetSet) set.createFromXml(node);
+    }
+
+    @Override
+    protected void postWriteDataFolder(File rootFolder) throws IOException {
+        // nothing to do.
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/AssetSet.java b/builder/src/main/java/com/android/builder/resources/AssetSet.java
new file mode 100644 (file)
index 0000000..716e643
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013 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 org.w3c.dom.Attr;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Represents a set of Assets.
+ */
+public class AssetSet extends DataSet<AssetItem, AssetFile> {
+
+    /**
+     * Creates an asset set with a given configName. The name is used to identify the set
+     * across sessions.
+     *
+     * @param configName the name of the config this set is associated with.
+     */
+    public AssetSet(String configName) {
+        super(configName);
+    }
+
+    @Override
+    protected DataSet<AssetItem, AssetFile> createSet(String name) {
+        return new AssetSet(name);
+    }
+
+    @Override
+    protected AssetFile createFileAndItems(File file) {
+        // key is going to be the full filename, since you can have both
+        //     icon.png
+        // and
+        //     icon.txt
+        // in the asset folder.
+
+        return new AssetFile(file, new AssetItem(file.getName()));
+    }
+
+    @Override
+    protected AssetFile createFileAndItems(File file, Node fileNode) {
+        Attr nameAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_NAME);
+        if (nameAttr == null) {
+            return null;
+        }
+
+        AssetItem item = new AssetItem(nameAttr.getValue());
+        return new AssetFile(file, item);
+    }
+
+    @Override
+    protected boolean isValidSourceFile(File sourceFolder, File file) {
+        // valid files are right under the source folder
+        return file.getParentFile().equals(sourceFolder);
+    }
+
+    @Override
+    protected void readSourceFolder(File sourceFolder) throws DuplicateDataException, IOException {
+        // get the files
+        File[] files = sourceFolder.listFiles();
+        if (files != null && files.length > 0) {
+            for (File file : files) {
+                if (!file.isFile() || !checkFileForAndroidRes(file)) {
+                    continue;
+                }
+
+                handleNewFile(sourceFolder, file);
+            }
+        }
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/DataFile.java b/builder/src/main/java/com/android/builder/resources/DataFile.java
new file mode 100644 (file)
index 0000000..6cfc9cc
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * 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.google.common.collect.Maps;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a data file.
+ *
+ * It contains a link to its {@link java.io.File}, and the {@DataItem}s it generates.
+ *
+ */
+abstract class DataFile<I extends DataItem> {
+
+    static enum FileType {
+        SINGLE, MULTI
+    }
+
+    private final FileType mType;
+    private final File mFile;
+    private final Map<String, I> mItems;
+
+    /**
+     * Creates a data file with a single data item.
+     *
+     * The source file is set on the item with {@link DataItem#setSource(DataFile)}
+     *
+     * The type of the DataFile will by {@link FileType#SINGLE}.
+     *
+     * @param file the File
+     * @param item the data item
+     */
+    DataFile(@NonNull File file, @NonNull I item) {
+        mType = FileType.SINGLE;
+        mFile = file;
+
+        item.setSource(this);
+        mItems = Collections.singletonMap(item.getKey(), item);
+    }
+
+    /**
+     * Creates a data file with a list of data items.
+     *
+     * The source file is set on the items with {@link DataItem#setSource(DataFile)}
+     *
+     * The type of the DataFile will by {@link FileType#MULTI}.
+     *
+     * @param file the File
+     * @param items the data items
+     */
+    DataFile(@NonNull File file, @NonNull List<I> items) {
+        mType = FileType.MULTI;
+        mFile = file;
+
+        mItems = Maps.newHashMapWithExpectedSize(items.size());
+        for (I item : items) {
+            item.setSource(this);
+            mItems.put(item.getKey(), item);
+        }
+    }
+
+    @NonNull
+    FileType getType() {
+        return mType;
+    }
+
+    @NonNull
+    File getFile() {
+        return mFile;
+    }
+
+    I getItem() {
+        assert mItems.size() == 1;
+        return mItems.values().iterator().next();
+    }
+
+    @NonNull
+    Collection<I> getItems() {
+        return mItems.values();
+    }
+
+    @NonNull
+    Map<String, I> getItemMap() {
+        return mItems;
+    }
+
+    void addItems(Collection<I> items) {
+        for (I item : items) {
+            mItems.put(item.getKey(), item);
+            item.setSource(this);
+        }
+    }
+
+    void addExtraAttributes(Document document, Node node, String namespaceUri) {
+        // nothing
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/DataItem.java b/builder/src/main/java/com/android/builder/resources/DataItem.java
new file mode 100644 (file)
index 0000000..31fba82
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * 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 org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+/**
+ * Base item.
+ *
+ * This includes its name and source file as a {@link com.android.builder.resources.DataFile}.
+ *
+ */
+abstract class DataItem<F extends DataFile> {
+
+    private static final int MASK_TOUCHED = 0x01;
+    private static final int MASK_REMOVED = 0x02;
+    private static final int MASK_WRITTEN = 0x10;
+
+    private final String mName;
+    private F mSource;
+
+    /**
+     * The status of the Item. It's a bit mask as opposed to an enum
+     * to differentiate removed and removed+written
+     */
+    private int mStatus = 0;
+
+    /**
+     * 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 DataFile first.
+     *
+     * @param name the name of the item
+     */
+    DataItem(@NonNull String name) {
+        mName = name;
+    }
+
+    /**
+     * Returns the name of the item.
+     * @return the name.
+     */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Returns the DataFile the item is coming from. Can be null.
+     * @return the data file.
+     */
+    public F getSource() {
+        return mSource;
+    }
+
+    /**
+     * Sets the DataFile
+     * @param sourceFile the DataFile
+     */
+    void setSource(F sourceFile) {
+        mSource = sourceFile;
+    }
+
+    /**
+     * Resets the state of the item be WRITTEN. All other states are removed.
+     * @return this
+     *
+     * @see #isWritten()
+     */
+    DataItem resetStatusToWritten() {
+        mStatus = MASK_WRITTEN;
+        return this;
+    }
+
+    /**
+     * Sets the item status to be WRITTEN. Other states are kept.
+     * @return this
+     *
+     * @see #isWritten()
+     */
+    DataItem setWritten() {
+        mStatus |= MASK_WRITTEN;
+        return this;
+    }
+
+    /**
+     * Sets the item status to be REMOVED. Other states are kept.
+     * @return this
+     *
+     * @see #isRemoved()
+     */
+    DataItem setRemoved() {
+        mStatus |= MASK_REMOVED;
+        return this;
+    }
+
+    /**
+     * Sets the item status to be TOUCHED. Other states are kept.
+     * @return this
+     *
+     * @see #isTouched()
+     */
+    DataItem setTouched() {
+        mStatus |= MASK_TOUCHED;
+        return this;
+    }
+
+    /**
+     * Returns whether the item status is REMOVED
+     * @return true if removed
+     */
+    boolean isRemoved() {
+        return (mStatus & MASK_REMOVED) != 0;
+    }
+
+    /**
+     * Returns whether the item status is TOUCHED
+     * @return true if touched
+     */
+    boolean isTouched() {
+        return (mStatus & MASK_TOUCHED) != 0;
+    }
+
+    /**
+     * Returns whether the item status is WRITTEN
+     * @return true if written
+     */
+    boolean isWritten() {
+        return (mStatus & MASK_WRITTEN) != 0;
+    }
+
+    protected int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Returns a key for this item. They key uniquely identifies this item. This is the name.
+     *
+     * @return the key for this item.
+     *
+     */
+    String getKey() {
+        return mName;
+    }
+
+    void addExtraAttributes(Document document, Node node, String namespaceUri) {
+        // nothing
+    }
+
+    Node getAdoptedNode(Document document) {
+        return null;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        DataItem resource = (DataItem) o;
+
+        return mName.equals(resource.mName);
+    }
+
+    @Override
+    public int hashCode() {
+        return mName.hashCode();
+    }
+}
@@ -20,22 +20,22 @@ import com.android.annotations.NonNull;
 import com.google.common.collect.ListMultimap;
 
 /**
- * A Resource Map able to provide a {@link ListMultimap} of Resources where the keys are
- * the value returned by {@link Resource#getKey()}
+ * A DataItem Map able to provide a {@link com.google.common.collect.ListMultimap} of data items
+ * where the keys are the value returned by {@link DataItem#getKey()}
  */
-interface ResourceMap {
+interface DataMap<T extends DataItem> {
 
     /**
-     * Returns the number of resources.
-     * @return the number of resources.
+     * Returns the number of items.
+     * @return the number of items
      */
     int size();
 
     /**
-     * a Multi map of (key, resource) where key is the result of
-     * {@link com.android.builder.resources.Resource#getKey()}
+     * a Multi map of (key, dataItem) where key is the result of
+     * {@link DataItem#getKey()}
      * @return a non null map
      */
     @NonNull
-    ListMultimap<String, Resource> getResourceMap();
+    ListMultimap<String, T> getDataMap();
 }
diff --git a/builder/src/main/java/com/android/builder/resources/DataMerger.java b/builder/src/main/java/com/android/builder/resources/DataMerger.java
new file mode 100755 (executable)
index 0000000..e7baa86
--- /dev/null
@@ -0,0 +1,481 @@
+/*
+ * 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.VisibleForTesting;
+import com.android.builder.internal.util.concurrent.WaitableExecutor;
+import com.android.ide.common.xml.XmlPrettyPrinter;
+import com.android.utils.Pair;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Merges {@link DataSet}s and writes a resulting data folder.
+ *
+ * This is able to save its post work state and reload this for incremental update.
+ */
+abstract class DataMerger<I extends DataItem<F>, F extends DataFile<I>, S extends DataSet<I,F>> implements DataMap<I> {
+
+    static final String FN_MERGER_XML = "merger.xml";
+    private static final String NODE_MERGER = "merger";
+    private static final String NODE_DATA_SET = "dataSet";
+
+    /**
+     * All the DataSets.
+     */
+    private final List<S> mDataSets = Lists.newArrayList();
+
+    public DataMerger() { }
+
+    /**
+     * Post write processing.
+     *
+     * This can be used for delayed file writing.
+     *
+     * @param rootFolder the root of the output.
+     *
+     * @throws IOException
+     */
+    protected abstract void postWriteDataFolder(File rootFolder) throws IOException;
+
+    /**
+     * Removes an data Item.
+     *
+     * This method can optionally receive the item that replaces the removed item in case
+     * processing can be optimized. the replacement item has already been written.
+     *
+     * @param rootFolder
+     * @param item
+     * @param replacedBy
+     */
+    protected abstract void removeItem(File rootFolder, I item, I replacedBy);
+
+    /**
+     * Writes a given DataItem to a given root folder.
+     *
+     * @param rootFolder the root res folder
+     * @param item the resource to add.
+     * @param executor an executor
+     * @throws java.io.IOException
+     */
+    protected abstract void writeItem(@NonNull final File rootFolder,
+                                      @NonNull final I item,
+                                      @NonNull WaitableExecutor executor) throws IOException;
+
+
+    protected abstract S createFromXml(Node node);
+
+    /**
+     * adds a new {@link DataSet} and overlays it on top of the existing DataSet.
+     *
+     * @param resourceSet the ResourceSet to add.
+     */
+    public void addDataSet(S resourceSet) {
+        // TODO figure out if we allow partial overlay through a per-resource flag.
+        mDataSets.add(resourceSet);
+    }
+
+    /**
+     * Returns the list of ResourceSet objects.
+     * @return the resource sets.
+     */
+    @VisibleForTesting
+    List<S> getDataSets() {
+        return mDataSets;
+    }
+
+    @VisibleForTesting
+    void validateDataSets() throws DuplicateDataException {
+        for (S resourceSet : mDataSets) {
+            resourceSet.checkItems();
+        }
+    }
+
+    /**
+     * Returns the number of items.
+     * @return the number of items.
+     *
+     * @see DataMap
+     */
+    @Override
+    public int size() {
+        // put all the resource keys in a set.
+        Set<String> keys = Sets.newHashSet();
+
+        for (S resourceSet : mDataSets) {
+            ListMultimap<String, I> map = resourceSet.getDataMap();
+            keys.addAll(map.keySet());
+        }
+
+        return keys.size();
+    }
+
+    /**
+     * Returns a map of the data items.
+     * @return a map of items.
+     *
+     * @see DataMap
+     */
+    @NonNull
+    @Override
+    public ListMultimap<String, I> getDataMap() {
+        // put all the sets in a multimap. The result is that for each key,
+        // there is a sorted list of items from all the layers, including removed ones.
+        ListMultimap<String, I> fullItemMultimap = ArrayListMultimap.create();
+
+        for (S resourceSet : mDataSets) {
+            ListMultimap<String, I> map = resourceSet.getDataMap();
+            for (Map.Entry<String, Collection<I>> entry : map.asMap().entrySet()) {
+                fullItemMultimap.putAll(entry.getKey(), entry.getValue());
+            }
+        }
+
+        return fullItemMultimap;
+    }
+
+    /**
+     * Writes the result of the merge to a destination data folder.
+     *
+     * @param rootFolder the folder to write the resources in.
+     * @throws java.io.IOException
+     * @throws DuplicateDataException
+     * @throws ExecutionException
+     * @throws InterruptedException
+     */
+    public void writeDataFolder(@NonNull File rootFolder)
+            throws IOException, DuplicateDataException, ExecutionException, InterruptedException {
+
+        WaitableExecutor executor = new WaitableExecutor();
+
+        // get all the items keys.
+        Set<String> dataItemKeys = Sets.newHashSet();
+
+        for (S dataSet : mDataSets) {
+            // quick check on duplicates in the resource set.
+            dataSet.checkItems();
+            ListMultimap<String, I> map = dataSet.getDataMap();
+            dataItemKeys.addAll(map.keySet());
+        }
+
+        // loop on all the data items.
+        for (String dataItemKey : dataItemKeys) {
+            // for each items, look in the data sets, starting from the end of the list.
+
+            I previouslyWritten = null;
+            I toWrite = null;
+
+            /*
+             * We are looking for what to write/delete: the last non deleted item, and the
+             * previously written one.
+             */
+
+            setLoop: for (int i = mDataSets.size() - 1 ; i >= 0 ; i--) {
+                S dataSet = mDataSets.get(i);
+
+                // look for the resource key in the set
+                ListMultimap<String, I> itemMap = dataSet.getDataMap();
+
+                List<I> items = itemMap.get(dataItemKey);
+                if (items.isEmpty()) {
+                    continue;
+                }
+
+                // The list can contain at max 2 items. One touched and one deleted.
+                // More than one deleted means there was more than one which isn't possible
+                // More than one touched means there is more than one and this isn't possible.
+                for (int ii = items.size() - 1 ; ii >= 0 ; ii--) {
+                    I item = items.get(ii);
+
+                    if (item.isWritten()) {
+                        assert previouslyWritten == null;
+                        previouslyWritten = item;
+                    }
+
+                    if (toWrite == null && !item.isRemoved()) {
+                        toWrite = item;
+                    }
+
+                    if (toWrite != null && previouslyWritten != null) {
+                        break setLoop;
+                    }
+                }
+            }
+
+            // done searching, we should at least have something.
+            assert previouslyWritten != null || toWrite != null;
+
+            // now need to handle, the type of each (single res file, multi res file), whether
+            // they are the same object or not, whether the previously written object was deleted.
+
+            if (toWrite == null) {
+                // nothing to write? delete only then.
+                assert previouslyWritten.isRemoved();
+
+                removeItem(rootFolder, previouslyWritten, null /*replacedBy*/);
+
+            } else if (previouslyWritten == null || previouslyWritten == toWrite) {
+                // easy one: new or updated res
+
+                writeItem(rootFolder, toWrite, executor);
+            } else {
+                // replacement of a resource by another.
+
+                // first force the writing of the new one.
+                toWrite.setTouched();
+
+                // write the new value
+                writeItem(rootFolder, toWrite, executor);
+
+                removeItem(rootFolder, previouslyWritten, toWrite);
+            }
+        }
+
+        postWriteDataFolder(rootFolder);
+
+        executor.waitForTasks();
+    }
+
+    /**
+     * Writes a single blog file to store all that the DataMerger knows about.
+     *
+     * @param blobRootFolder the root folder where blobs are store.
+     * @throws IOException
+     *
+     * @see #loadFromBlob(File)
+     */
+    public void writeBlobTo(File blobRootFolder) throws IOException {
+        // write "compact" blob
+        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(NODE_MERGER);
+            document.appendChild(rootNode);
+
+            for (S dataSet : mDataSets) {
+                Node dataSetNode = document.createElement(NODE_DATA_SET);
+                rootNode.appendChild(dataSetNode);
+
+                dataSet.appendToXml(dataSetNode, document);
+            }
+
+            String content = XmlPrettyPrinter.prettyPrint(document);
+
+            createDir(blobRootFolder);
+            Files.write(content, new File(blobRootFolder, FN_MERGER_XML), Charsets.UTF_8);
+        } catch (ParserConfigurationException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Loads the merger state from a blob file.
+     *
+     * @param blobRootFolder the folder containing the blob.
+     * @return true if the blob was loaded.
+     * @throws IOException
+     *
+     * @see #writeBlobTo(File)
+     */
+    public boolean loadFromBlob(File blobRootFolder) throws IOException {
+        File file = new File(blobRootFolder, FN_MERGER_XML);
+        if (!file.isFile()) {
+            return false;
+        }
+
+        BufferedInputStream stream = null;
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            stream = new BufferedInputStream(new FileInputStream(file));
+            InputSource is = new InputSource(stream);
+            factory.setNamespaceAware(true);
+            factory.setValidating(false);
+
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(is);
+
+            // get the root node
+            Node rootNode = document.getDocumentElement();
+            if (rootNode == null || !NODE_MERGER.equals(rootNode.getLocalName())) {
+                return false;
+            }
+
+            NodeList nodes = rootNode.getChildNodes();
+
+            for (int i = 0, n = nodes.getLength(); i < n; i++) {
+                Node node = nodes.item(i);
+
+                if (node.getNodeType() != Node.ELEMENT_NODE ||
+                        !NODE_DATA_SET.equals(node.getLocalName())) {
+                    continue;
+                }
+
+                S dataSet = createFromXml(node);
+                if (dataSet != null) {
+                    mDataSets.add(dataSet);
+                }
+            }
+
+            setItemsToWritten();
+
+            return true;
+        } catch (FileNotFoundException e) {
+            throw new IOException(e);
+        } catch (ParserConfigurationException e) {
+            throw new IOException(e);
+        } catch (SAXException e) {
+            throw new IOException(e);
+        } finally {
+            try {
+                if (stream != null) {
+                    stream.close();
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+
+    /**
+     * Sets all existing items to have their state be WRITTEN.
+     *
+     * This only sets the last item to be written.
+     *
+     * @see DataItem#isWritten()
+     */
+    private void setItemsToWritten() {
+        ListMultimap<String, I> itemMap = ArrayListMultimap.create();
+
+        for (S dataSet : mDataSets) {
+            ListMultimap<String, I> map = dataSet.getDataMap();
+            for (Map.Entry<String, Collection<I>> entry : map.asMap().entrySet()) {
+                itemMap.putAll(entry.getKey(), entry.getValue());
+            }
+        }
+
+        for (String key : itemMap.keySet()) {
+            List<I> itemList = itemMap.get(key);
+            itemList.get(itemList.size() - 1).resetStatusToWritten();
+        }
+    }
+
+    /**
+     * Checks that a loaded merger can be updated with a given list of DataSet.
+     *
+     * For now this means the sets haven't changed.
+     *
+     * @param dataSets the resource sets.
+     * @return true if the update can be performed. false if a full merge should be done.
+     */
+    public boolean checkValidUpdate(List<S> dataSets) {
+        if (dataSets.size() != mDataSets.size()) {
+            return false;
+        }
+
+        for (int i = 0, n = dataSets.size(); i < n; i++) {
+            S localSet = mDataSets.get(i);
+            S newSet = dataSets.get(i);
+
+            List<File> localSourceFiles = localSet.getSourceFiles();
+            List<File> newSourceFiles = newSet.getSourceFiles();
+
+            // compare the config name and source files sizes.
+            if (!newSet.getConfigName().equals(localSet.getConfigName()) ||
+                    localSourceFiles.size() != newSourceFiles.size()) {
+                return false;
+            }
+
+            // compare the source files. The order is not important so it should be normalized
+            // before it's compared.
+            // make copies to sort.
+            localSourceFiles = Lists.newArrayList(localSourceFiles);
+            Collections.sort(localSourceFiles);
+            newSourceFiles = Lists.newArrayList(newSourceFiles);
+            Collections.sort(newSourceFiles);
+
+            if (!localSourceFiles.equals(newSourceFiles)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns a DataSet that contains a given file.
+     *
+     * "contains" means that the DataSet has a source file/folder that is the root folder
+     * of this file. The folder and/or file doesn't have to exist.
+     *
+     * @param file the file to check
+     * @return a pair containing the ResourceSet and its source file that contains the file.
+     */
+    public Pair<S, File> getDataSetContaining(File file) {
+        for (S dataSet : mDataSets) {
+            File sourceFile = dataSet.findMatchingSourceFile(file);
+            if (file != null) {
+                return Pair.of(dataSet, sourceFile);
+            }
+        }
+
+        return null;
+    }
+
+    protected synchronized void createDir(File folder) throws IOException {
+        if (!folder.isDirectory() && !folder.mkdirs()) {
+            throw new IOException("Failed to create directory: " + folder);
+        }
+    }
+
+
+    @Override
+    public String toString() {
+        return Arrays.toString(mDataSets.toArray());
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/DataSet.java b/builder/src/main/java/com/android/builder/resources/DataSet.java
new file mode 100755 (executable)
index 0000000..ba4ef99
--- /dev/null
@@ -0,0 +1,475 @@
+/*
+ * 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.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a set of {@link DataItem}s.
+ *
+ * The items can be coming from multiple source folders, and duplicates are detected.
+ *
+ * Each source folders is considered to be at the same level. To use overlays, a
+ * {@link DataMerger} must be used.
+ *
+ * Creating the set and adding folders does not load the data.
+ * The data can be loaded from the files, or from a blob which is generated by the set itself.
+ *
+ * Upon loading the data from the blob, the data can be updated with fresher files. Each item
+ * that is updated is flagged as such, in order to manage incremental update.
+ *
+ * Writing/Loading the blob is not done through this class directly, but instead through the
+ * {@link DataMerger} which contains DataSet objects.
+ */
+abstract class DataSet<I extends DataItem<F>, F extends DataFile<I>> implements SourceSet, DataMap<I> {
+
+    static final String NODE_SOURCE = "source";
+    static final String NODE_FILE = "file";
+    static final String ATTR_CONFIG = "config";
+    static final String ATTR_PATH = "path";
+    static final String ATTR_NAME = "name";
+
+    private final String mConfigName;
+
+    /**
+     * List of source files. The may not have been loaded yet.
+     */
+    private final List<File> mSourceFiles = Lists.newArrayList();
+
+    /**
+     * The key is the {@link DataItem#getKey()}.
+     * This is a multimap to support moving a data item from one file to another (values file)
+     * during incremental update.
+     */
+    private final ListMultimap<String, I> mItems = ArrayListMultimap.create();
+
+    /**
+     * Map of source files to DataFiles. This is a multimap because the key is the source
+     * file/folder, not the File for the DataFile itself.
+     */
+    private final ListMultimap<File, F> mSourceFileToDataFilesMap = ArrayListMultimap.create();
+
+    /**
+     * Map from a File to its DataFile.
+     */
+    private final Map<File, F> mDataFileMap = Maps.newHashMap();
+
+    /**
+     * Creates a DataSet with a given configName. The name is used to identify the set
+     * across sessions.
+     *
+     * @param configName the name of the config this set is associated with.
+     */
+    public DataSet(String configName) {
+        mConfigName = configName;
+    }
+
+    protected abstract DataSet<I, F> createSet(String name);
+
+    protected abstract F createFileAndItems(File file, Node fileNode);
+
+    /**
+     * Reads the content of a data folders and loads the DataItem.
+     *
+     * This should generate DataFiles, and process them with
+     * {@link #processNewDataFile(java.io.File, DataFile, boolean)}.
+     *
+     * @param sourceFolder the source folder to load the resources from.
+     *
+     * @throws com.android.builder.resources.DuplicateDataException
+     * @throws java.io.IOException
+     */
+    protected abstract void readSourceFolder(File sourceFolder)
+            throws DuplicateDataException, IOException;
+
+    protected abstract F createFileAndItems(File file);
+
+
+    /**
+     * Adds a collection of source files.
+     * @param files the source files to add.
+     */
+    public void addSources(Collection<File> files) {
+        mSourceFiles.addAll(files);
+    }
+
+    /**
+     * Adds a new source file
+     * @param file the source file.
+     */
+    public void addSource(File file) {
+        mSourceFiles.add(file);
+    }
+
+    /**
+     * Get the list of source files.
+     * @return the source files.
+     */
+    @NonNull
+    @Override
+    public List<File> getSourceFiles() {
+        return mSourceFiles;
+    }
+
+    /**
+     * Returns the config name.
+     * @return the config name.
+     */
+    public String getConfigName() {
+        return mConfigName;
+    }
+
+    /**
+     * Returns a matching Source file that contains a given file.
+     *
+     * "contains" means that the source file/folder is the root folder
+     * of this file. The folder and/or file doesn't have to exist.
+     *
+     * @param file the file to search for
+     * @return the Source file or null if no match is found.
+     */
+    @Override
+    public File findMatchingSourceFile(File file) {
+        for (File sourceFile : mSourceFiles) {
+            if (sourceFile.equals(file)) {
+                return sourceFile;
+            } else if (sourceFile.isDirectory()) {
+                String sourcePath = sourceFile.getAbsolutePath() + File.separator;
+                if (file.getAbsolutePath().startsWith(sourcePath)) {
+                    return sourceFile;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the number of items.
+     * @return the number of items.
+     *
+     * @see DataMap
+     */
+    @Override
+    public int size() {
+        // returns the number of keys, not the size of the multimap which would include duplicate
+        // ResourceItem objects.
+        return mItems.keySet().size();
+    }
+
+    /**
+     * Returns whether the set is empty of items.
+     * @return true if the set contains no items.
+     */
+    public boolean isEmpty() {
+        return mItems.isEmpty();
+    }
+
+    /**
+     * Returns a map of the items.
+     * @return a map of items.
+     *
+     * @see DataMap
+     */
+    @NonNull
+    @Override
+    public ListMultimap<String, I> getDataMap() {
+        return mItems;
+    }
+
+    /**
+     * Loads the DataSet from the files its source folders contain.
+     *
+     * All loaded items are set to TOUCHED. This is so that after loading the resources from
+     * the files, they can be written directly (since touched force them to be written).
+     *
+     * This also checks for duplicates items.
+     *
+     * @throws DuplicateDataException
+     * @throws IOException
+     */
+    public void loadFromFiles() throws DuplicateDataException, IOException {
+        for (File file : mSourceFiles) {
+            if (file.isDirectory()) {
+                readSourceFolder(file);
+
+            } else if (file.isFile()) {
+                // TODO support resource bundle
+            }
+        }
+        checkItems();
+    }
+
+    /**
+     * Appends the DataSet to a given DOM object.
+     *
+     * @param setNode the root node for this set.
+     * @param document The root XML document
+     */
+    void appendToXml(Node setNode, Document document) {
+        // add the config name attribute
+        NodeUtils.addAttribute(document, setNode, null, ATTR_CONFIG, mConfigName);
+
+        // add the source files.
+        // we need to loop on the source files themselves and not the map to ensure we
+        // write empty resourceSets
+        for (File sourceFile : mSourceFiles) {
+
+            // the node for the source and its path attribute
+            Node sourceNode = document.createElement(NODE_SOURCE);
+            setNode.appendChild(sourceNode);
+            NodeUtils.addAttribute(document, sourceNode, null, ATTR_PATH,
+                    sourceFile.getAbsolutePath());
+
+            Collection<F> dataFiles = mSourceFileToDataFilesMap.get(sourceFile);
+
+            for (F dataFile : dataFiles) {
+                // the node for the file and its path and qualifiers attribute
+                Node fileNode = document.createElement(NODE_FILE);
+                sourceNode.appendChild(fileNode);
+                NodeUtils.addAttribute(document, fileNode, null, ATTR_PATH,
+                        dataFile.getFile().getAbsolutePath());
+                dataFile.addExtraAttributes(document, fileNode, null);
+
+                if (dataFile.getType() == DataFile.FileType.MULTI) {
+                    for (I item : dataFile.getItems()) {
+                        Node adoptedNode = item.getAdoptedNode(document);
+                        if (adoptedNode != null) {
+                            fileNode.appendChild(adoptedNode);
+                        }
+                    }
+                } else {
+                    I dataItem = dataFile.getItem();
+                    NodeUtils.addAttribute(document, fileNode, null, ATTR_NAME, dataItem.getName());
+                    dataItem.addExtraAttributes(document, fileNode, null);
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates and returns a new DataSet from an XML node that was created with
+     * {@link #appendToXml(org.w3c.dom.Node, org.w3c.dom.Document)}
+     *
+     * The object this method is called on is not modified. This should be static but can't be
+     * due to children classes.
+     *
+     * @param dataSetNode the node to read from.
+     * @return a new DataSet object or null.
+     */
+    DataSet<I,F> createFromXml(Node dataSetNode) {
+        // get the config name
+        Attr configNameAttr = (Attr) dataSetNode.getAttributes().getNamedItem(ATTR_CONFIG);
+        if (configNameAttr == null) {
+            return null;
+        }
+
+        // create the DataSet that will be filled with the content of the XML.
+        DataSet<I, F> dataSet = createSet(configNameAttr.getValue());
+
+        // loop on the source nodes
+        NodeList sourceNodes = dataSetNode.getChildNodes();
+        for (int i = 0, n = sourceNodes.getLength(); i < n; i++) {
+            Node sourceNode = sourceNodes.item(i);
+
+            if (sourceNode.getNodeType() != Node.ELEMENT_NODE ||
+                    !NODE_SOURCE.equals(sourceNode.getLocalName())) {
+                continue;
+            }
+
+            Attr pathAttr = (Attr) sourceNode.getAttributes().getNamedItem(ATTR_PATH);
+            if (pathAttr == null) {
+                continue;
+            }
+
+            File sourceFolder = new File(pathAttr.getValue());
+            dataSet.mSourceFiles.add(sourceFolder);
+
+            // now loop on the files inside the source folder.
+            NodeList fileNodes = sourceNode.getChildNodes();
+            for (int j = 0, m = fileNodes.getLength(); j < m; j++) {
+                Node fileNode = fileNodes.item(j);
+
+                if (fileNode.getNodeType() != Node.ELEMENT_NODE ||
+                        !NODE_FILE.equals(fileNode.getLocalName())) {
+                    continue;
+                }
+
+                pathAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_PATH);
+                if (pathAttr == null) {
+                    continue;
+                }
+                
+                F dataFile = createFileAndItems(new File(pathAttr.getValue()), fileNode);
+
+                if (dataFile != null) {
+                    dataSet.processNewDataFile(sourceFolder, dataFile, false /*setTouched*/);
+                }
+            }
+        }
+
+        return dataSet;
+    }
+
+    /**
+     * Checks for duplicate items across all source files.
+     *
+     * @throws DuplicateDataException if a duplicated item is found.
+     */
+    void checkItems() throws DuplicateDataException {
+        // check a list for duplicate, ignoring removed items.
+        for (Map.Entry<String, Collection<I>> entry : mItems.asMap().entrySet()) {
+            Collection<I> items = entry.getValue();
+
+            // there can be several version of the same key if some are "removed"
+            I lastItem = null;
+            for (I item : items) {
+                if (!item.isRemoved()) {
+                    if (lastItem == null) {
+                        lastItem = item;
+                    } else {
+                        throw new DuplicateDataException(item, lastItem);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Update the DataSet with a given file.
+     *
+     * @param sourceFolder the sourceFile containing the changedFile
+     * @param changedFile The changed file
+     * @param fileStatus the change state
+     * @return true if the set was properly updated, false otherwise
+     */
+    public boolean updateWith(File sourceFolder, File changedFile, FileStatus fileStatus)
+            throws IOException {
+        switch (fileStatus) {
+            case NEW:
+                if (isValidSourceFile(sourceFolder, changedFile)) {
+                    return handleNewFile(sourceFolder, changedFile);
+                }
+                return true;
+            case CHANGED:
+                return handleChangedFile(sourceFolder, changedFile);
+            case REMOVED:
+                F dataFile = mDataFileMap.get(changedFile);
+
+                // flag all resource items are removed
+                for (I dataItem : dataFile.getItems()) {
+                    dataItem.setRemoved();
+                }
+                return true;
+        }
+
+        return false;
+    }
+
+    protected abstract boolean isValidSourceFile(File sourceFolder, File changedFile);
+
+    protected boolean handleNewFile(File sourceFolder, File file) {
+        F dataFile = createFileAndItems(file);
+        processNewDataFile(sourceFolder, dataFile, true /*setTouched*/);
+        return true;
+    }
+
+    protected void processNewDataFile(File sourceFolder, F dataFile, boolean setTouched) {
+        Collection<I> dataItems = dataFile.getItems();
+
+        addDataFile(sourceFolder, dataFile);
+
+        for (I dataItem : dataItems) {
+            mItems.put(dataItem.getKey(), dataItem);
+            if (setTouched) {
+                dataItem.setTouched();
+            }
+        }
+    }
+
+    protected boolean handleChangedFile(File sourceFolder, File changedFile) throws IOException {
+        F dataFile = mDataFileMap.get(changedFile);
+        dataFile.getItem().setTouched();
+        return true;
+    }
+
+    protected void addItem(I item, String key) {
+        if (key == null) {
+            key = item.getKey();
+        }
+
+        mItems.put(key, item);
+    }
+
+    protected F getDataFile(File file) {
+        return mDataFileMap.get(file);
+    }
+
+    /**
+     * Adds a new DataFile to this.
+     *
+     * @param sourceFile the parent source file.
+     * @param dataFile the DataFile
+     */
+    private void addDataFile(File sourceFile, F dataFile) {
+        mSourceFileToDataFilesMap.put(sourceFile, dataFile);
+        mDataFileMap.put(dataFile.getFile(), dataFile);
+    }
+
+    @Override
+    public String toString() {
+        return Arrays.toString(mSourceFiles.toArray());
+    }
+
+    /**
+     * Checks a file to make sure it is a valid file in the android res/asset folders.
+     * @param file the file to check
+     * @return true if it is a valid file, false if it should be ignored.
+     */
+    protected boolean checkFileForAndroidRes(File file) {
+        // TODO: use the aapt ignore pattern value.
+
+        String name = file.getName();
+        int pos = name.lastIndexOf('.');
+        String extension = "";
+        if (pos != -1) {
+            extension = name.substring(pos + 1);
+        }
+
+        // ignore hidden files and backup files
+        return !(name.charAt(0) == '.' || name.charAt(name.length() - 1) == '~') &&
+                !"scc".equalsIgnoreCase(extension) &&     // VisualSourceSafe
+                !"swp".equalsIgnoreCase(extension) &&     // vi swap file
+                !"thumbs.db".equalsIgnoreCase(name) &&    // image index file
+                !"picasa.ini".equalsIgnoreCase(name);     // image index file
+    }
+}
 package com.android.builder.resources;
 
 /**
- * Exception when a resource is declared more than once in a {@link ResourceSet}
+ * Exception when a {@link DataItem} is declared more than once in a {@link DataSet}
  */
-public class DuplicateResourceException extends Exception {
+public class DuplicateDataException extends Exception {
 
-    private Resource mOne;
-    private Resource mTwo;
+    private DataItem mOne;
+    private DataItem mTwo;
 
-    DuplicateResourceException(Resource one, Resource two) {
+    DuplicateDataException(DataItem one, DataItem two) {
         super(String.format("Duplicate resources: %1s:%2s, %3s:%4s",
                 one.getSource().getFile().getAbsolutePath(), one.getKey(),
                 two.getSource().getFile().getAbsolutePath(), two.getKey()));
@@ -32,11 +32,11 @@ public class DuplicateResourceException extends Exception {
         mTwo = two;
     }
 
-    public Resource getOne() {
+    public DataItem getOne() {
         return mOne;
     }
 
-    public Resource getTwo() {
+    public DataItem 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
deleted file mode 100644 (file)
index 65f476c..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * 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.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.
- *
- */
-class Resource {
-
-    private static final int MASK_TOUCHED = 0x01;
-    private static final int MASK_REMOVED = 0x02;
-    private static final int MASK_WRITTEN = 0x10;
-
-    private final String mName;
-    private final ResourceType mType;
-
-    private Node mValue;
-    private ResourceFile mSource;
-
-    /**
-     * The status of the Resource. It's a bit mask as opposed to an enum
-     * to differentiate removed and removed+written
-     */
-    private int mStatus = 0;
-
-    /**
-     * 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 value an optional Node that represents the resource value.
-     */
-    Resource(@NonNull String name, @NonNull ResourceType type, Node value) {
-        mName = name;
-        mType = type;
-        mValue = value;
-    }
-
-    /**
-     * Returns the name of the resource.
-     * @return the name.
-     */
-    @NonNull
-    public String getName() {
-        return mName;
-    }
-
-    /**
-     * Returns the type of the resource.
-     * @return the type.
-     */
-    @NonNull
-    public ResourceType getType() {
-        return mType;
-    }
-
-    /**
-     * Returns the ResourceFile the resource is coming from. Can be null.
-     * @return the resource file.
-     */
-    public ResourceFile getSource() {
-        return mSource;
-    }
-
-    /**
-     * Returns the optional value of the resource. Can be null
-     * @return the value or null.
-     */
-    public Node getValue() {
-        return mValue;
-    }
-
-    /**
-     * Sets the value of the resource and set its state to TOUCHED.
-     * @param from the resource to copy the value from.
-     */
-    void setValue(Resource from) {
-        mValue = from.mValue;
-        setTouched();
-    }
-
-
-    /**
-     * Sets the ResourceFile
-     * @param sourceFile the ResourceFile
-     */
-    void setSource(ResourceFile sourceFile) {
-        mSource = sourceFile;
-    }
-
-    /**
-     * Resets the state of the resource be WRITTEN. All other states are removed.
-     * @return this
-     *
-     * @see #isWritten()
-     */
-    Resource resetStatusToWritten() {
-        mStatus = MASK_WRITTEN;
-        return this;
-    }
-
-    /**
-     * Sets the resource be WRITTEN. Other states are kept.
-     * @return this
-     *
-     * @see #isWritten()
-     */
-    Resource setWritten() {
-        mStatus |= MASK_WRITTEN;
-        return this;
-    }
-
-    /**
-     * Sets the resource be REMOVED. Other states are kept.
-     * @return this
-     *
-     * @see #isRemoved()
-     */
-    Resource setRemoved() {
-        mStatus |= MASK_REMOVED;
-        return this;
-    }
-
-    /**
-     * Sets the resource be TOUCHED. Other states are kept.
-     * @return this
-     *
-     * @see #isTouched()
-     */
-    Resource setTouched() {
-        mStatus |= MASK_TOUCHED;
-        return this;
-    }
-
-    /**
-     * Returns whether the resource is REMOVED
-     * @return true if removed
-     */
-    boolean isRemoved() {
-        return (mStatus & MASK_REMOVED) != 0;
-    }
-
-    /**
-     * Returns whether the resource is TOUCHED
-     * @return true if touched
-     */
-    boolean isTouched() {
-        return (mStatus & MASK_TOUCHED) != 0;
-    }
-
-    /**
-     * Returns whether the resource is WRITTEN
-     * @return true if written
-     */
-    boolean isWritten() {
-        return (mStatus & MASK_WRITTEN) != 0;
-    }
-
-    /**
-     * Returns a key for this resource. They key uniquely identifies this resource by combining
-     * resource type, qualifiers, and name.
-     *
-     * If the resource has not been added to a {@link ResourceFile}, this will throw an
-     * {@link IllegalStateException}.
-     *
-     * @return the key for this resource.
-     *
-     * @throws IllegalStateException if the resource is not added to a ResourceFile
-     */
-    String getKey() {
-        if (mSource == null) {
-            throw new IllegalStateException(
-                    "Resource.getKey called on object with no ResourceFile: " + this);
-        }
-        String qualifiers = mSource.getQualifiers();
-        if (qualifiers != null && qualifiers.length() > 0) {
-            return mType.getName() + "-" + qualifiers + "/" + mName;
-        }
-
-        return mType.getName() + "/" + mName;
-    }
-
-    /**
-     * Compares the Resource {@link #getValue()} together and returns true if they are the same.
-     * @param resource The Resource object to compare to.
-     * @return true if equal
-     */
-    public boolean compareValueWith(Resource resource) {
-        if (mValue != null && resource.mValue != null) {
-            return NodeUtils.compareElementNode(mValue, resource.mValue);
-        }
-
-        return mValue == resource.mValue;
-    }
-
-    @Override
-    public String toString() {
-        return "Resource{" +
-                "mName='" + mName + '\'' +
-                ", mType=" + mType +
-                ", mStatus=" + mStatus +
-                '}';
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        Resource resource = (Resource) o;
-
-        return mName.equals(resource.mName) && mType == resource.mType;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = mName.hashCode();
-        result = 31 * result + mType.hashCode();
-        return result;
-    }
-}
index 42ba9bc..7d7f270 100644 (file)
@@ -18,38 +18,33 @@ package com.android.builder.resources;
 
 import com.android.annotations.NonNull;
 import com.android.annotations.Nullable;
-import com.google.common.collect.Maps;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
 
 import java.io.File;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Represents a file in a resource folders.
  *
  * It contains a link to the {@link File}, the qualifier string (which is the name of the folder
- * after the first '-' character), a list of {@link Resource} and a type.
+ * after the first '-' character), a list of {@link ResourceItem} and a type.
  *
  * The type of the file is based on whether the file is located in a values folder (FileType.MULTI)
  * or in another folder (FileType.SINGLE).
  */
-class ResourceFile {
+class ResourceFile extends DataFile<ResourceItem> {
 
-    static enum FileType {
-        SINGLE, MULTI
-    }
+    static final String ATTR_QUALIFIER = "qualifiers";
+    static final String ATTR_TYPE = "type";
 
-    private final FileType mType;
-    private final File mFile;
-    private final Map<String, Resource> mItems;
     private final String mQualifiers;
 
     /**
      * Creates a resource file with a single resource item.
      *
-     * The source file is set on the item with {@link Resource#setSource(ResourceFile)}
+     * The source file is set on the item with {@link ResourceItem#setSource(ResourceFile)}
      *
      * The type of the ResourceFile will by {@link FileType#SINGLE}.
      *
@@ -57,19 +52,15 @@ class ResourceFile {
      * @param item the resource item
      * @param qualifiers the qualifiers.
      */
-    ResourceFile(@NonNull File file, @NonNull Resource item, @Nullable String qualifiers) {
-        mType = FileType.SINGLE;
-        mFile = file;
+    ResourceFile(@NonNull File file, @NonNull ResourceItem item, @Nullable String qualifiers) {
+        super(file, item);
         mQualifiers = qualifiers;
-
-        item.setSource(this);
-        mItems = Collections.singletonMap(item.getKey(), item);
     }
 
     /**
      * Creates a resource file with a list of resource items.
      *
-     * The source file is set on the items with {@link Resource#setSource(ResourceFile)}
+     * The source file is set on the items with {@link ResourceItem#setSource(ResourceFile)}
      *
      * The type of the ResourceFile will by {@link FileType#MULTI}.
      *
@@ -77,52 +68,19 @@ class ResourceFile {
      * @param items the resource items
      * @param qualifiers the qualifiers.
      */
-    ResourceFile(@NonNull File file, @NonNull List<Resource> items, @Nullable String qualifiers) {
-        mType = FileType.MULTI;
-        mFile = file;
+    ResourceFile(@NonNull File file, @NonNull List<ResourceItem> items,
+                 @Nullable String qualifiers) {
+        super(file, items);
         mQualifiers = qualifiers;
-
-        mItems = Maps.newHashMapWithExpectedSize(items.size());
-        for (Resource item : items) {
-            item.setSource(this);
-            mItems.put(item.getKey(), item);
-        }
-    }
-
-    @NonNull
-    FileType getType() {
-        return mType;
-    }
-
-    @NonNull
-    File getFile() {
-        return mFile;
     }
-
     @Nullable
     String getQualifiers() {
         return mQualifiers;
     }
 
-    Resource getItem() {
-        assert mItems.size() == 1;
-        return mItems.values().iterator().next();
-    }
-
-    @NonNull
-    Collection<Resource> getItems() {
-        return mItems.values();
-    }
-
-    @NonNull
-    Map<String, Resource> getItemMap() {
-        return mItems;
-    }
-
-    void addItems(Collection<Resource> items) {
-        for (Resource item : items) {
-            mItems.put(item.getKey(), item);
-            item.setSource(this);
-        }
+    @Override
+    void addExtraAttributes(Document document, Node node, String namespaceUri) {
+        NodeUtils.addAttribute(document, node, namespaceUri, ATTR_QUALIFIER,
+                getQualifiers());
     }
 }
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceItem.java b/builder/src/main/java/com/android/builder/resources/ResourceItem.java
new file mode 100644 (file)
index 0000000..47c898c
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * 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.resources.ResourceType;
+import org.w3c.dom.Document;
+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.
+ *
+ */
+class ResourceItem extends DataItem<ResourceFile> {
+
+    private static final String ATTR_TYPE = "type";
+
+
+    private final ResourceType mType;
+    private Node mValue;
+
+    /**
+     * 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 value an optional Node that represents the resource value.
+     */
+    ResourceItem(@NonNull String name, @NonNull ResourceType type, Node value) {
+        super(name);
+        mType = type;
+        mValue = value;
+    }
+
+    /**
+     * Returns the type of the resource.
+     * @return the type.
+     */
+    @NonNull
+    public ResourceType getType() {
+        return mType;
+    }
+
+    /**
+     * Returns the optional value of the resource. Can be null
+     * @return the value or null.
+     */
+    public Node getValue() {
+        return mValue;
+    }
+
+    /**
+     * Sets the value of the resource and set its state to TOUCHED.
+     * @param from the resource to copy the value from.
+     */
+    void setValue(ResourceItem from) {
+        mValue = from.mValue;
+        setTouched();
+    }
+
+    /**
+     * Returns a key for this resource. They key uniquely identifies this resource by combining
+     * resource type, qualifiers, and name.
+     *
+     * If the resource has not been added to a {@link ResourceFile}, this will throw an
+     * {@link IllegalStateException}.
+     *
+     * @return the key for this resource.
+     *
+     * @throws IllegalStateException if the resource is not added to a ResourceFile
+     */
+    @Override
+    String getKey() {
+        if (getSource() == null) {
+            throw new IllegalStateException(
+                    "ResourceItem.getKey called on object with no ResourceFile: " + this);
+        }
+        String qualifiers = getSource().getQualifiers();
+        if (qualifiers != null && qualifiers.length() > 0) {
+            return mType.getName() + "-" + qualifiers + "/" + getName();
+        }
+
+        return mType.getName() + "/" + getName();
+    }
+
+    @Override
+    void addExtraAttributes(Document document, Node node, String namespaceUri) {
+        NodeUtils.addAttribute(document, node, null, ATTR_TYPE, mType.getName());
+    }
+
+    @Override
+    Node getAdoptedNode(Document document) {
+        return NodeUtils.adoptNode(document, mValue);
+    }
+
+    /**
+     * Compares the ResourceItem {@link #getValue()} together and returns true if they are the same.
+     * @param resource The ResourceItem object to compare to.
+     * @return true if equal
+     */
+    public boolean compareValueWith(ResourceItem resource) {
+        if (mValue != null && resource.mValue != null) {
+            return NodeUtils.compareElementNode(mValue, resource.mValue);
+        }
+
+        return mValue == resource.mValue;
+    }
+
+    @Override
+    public String toString() {
+        return "ResourceItem{" +
+                "mName='" + getName() + '\'' +
+                ", mType=" + mType +
+                ", mStatus=" + getStatus() +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+
+        ResourceItem that = (ResourceItem) o;
+
+        if (mType != that.mType) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + mType.hashCode();
+        return result;
+    }
+}
old mode 100755 (executable)
new mode 100644 (file)
index 647beb2..ffc25f7
@@ -18,37 +18,24 @@ package com.android.builder.resources;
 
 import com.android.annotations.NonNull;
 import com.android.annotations.Nullable;
-import com.android.annotations.VisibleForTesting;
 import com.android.builder.AaptRunner;
 import com.android.builder.internal.util.concurrent.WaitableExecutor;
 import com.android.ide.common.xml.XmlPrettyPrinter;
 import com.android.resources.ResourceFolderType;
-import com.android.utils.Pair;
 import com.google.common.base.Charsets;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.io.Files;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
-import java.io.BufferedInputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -58,91 +45,25 @@ import static com.android.SdkConstants.RES_QUALIFIER_SEP;
 import static com.android.SdkConstants.TAG_RESOURCES;
 
 /**
- * Merges {@link ResourceSet}s and writes a resource folder that can be fed to aapt.
- *
- * This is able to save its post work state and reload this for incremental update.
+ * Implementation of {@link DataMerger} for {@link ResourceSet}, {@link ResourceItem}, and
+ * {@link ResourceFile}.
  */
-public class ResourceMerger implements ResourceMap {
+public class ResourceMerger extends DataMerger<ResourceItem, ResourceFile, ResourceSet> {
 
-    static final String FN_MERGER_XML = "merger.xml";
     private static final String FN_VALUES_XML = "values.xml";
-    private static final String NODE_MERGER = "merger";
-    private static final String NODE_RESOURCE_SET = "resourceSet";
-
-    /**
-     * All the resources. The merged version will be the last item in the list.
-     */
-    private final List<ResourceSet> mResourceSets = Lists.newArrayList();
-
-    public ResourceMerger() { }
 
-    /**
-     * adds a new {@link ResourceSet} and overlays it on top of the existing ResourceSets.
-     *
-     * @param resourceSet the ResourceSet to add.
-     */
-    public void addResourceSet(ResourceSet resourceSet) {
-        // TODO figure out if we allow partial overlay through a per-resource flag.
-        mResourceSets.add(resourceSet);
-    }
 
+    private AaptRunner mAaptRunner;
     /**
-     * Returns the list of ResourceSet objects.
-     * @return the resource sets.
-     */
-    @VisibleForTesting
-    List<ResourceSet> getResourceSets() {
-        return mResourceSets;
-    }
-
-    @VisibleForTesting
-    void validateResourceSets() throws DuplicateResourceException {
-        for (ResourceSet resourceSet : mResourceSets) {
-            resourceSet.checkItems();
-        }
-    }
-
-    /**
-     * Returns the number of resources.
-     * @return the number of resources.
-     *
-     * @see ResourceMap
+     * map of XML values files to write after parsing all the files. the key is the qualifier.
      */
-    @Override
-    public int size() {
-        // put all the resource keys in a set.
-        Set<String> keys = Sets.newHashSet();
-
-        for (ResourceSet resourceSet : mResourceSets) {
-            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
-            keys.addAll(map.keySet());
-        }
-
-        return keys.size();
-    }
-
+    private ListMultimap<String, ResourceItem> mValuesResMap;
     /**
-     * Returns a map of the resources.
-     * @return a map of items.
-     *
-     * @see ResourceMap
+     * Set of qualifier that had a previously written resource now gone.
+     * This is to keep a list of values files that must be written out even with no
+     * touched or updated resources, in case one or more resources were removed.
      */
-    @NonNull
-    @Override
-    public ListMultimap<String, Resource> getResourceMap() {
-        // put all the sets in a multimap. The result is that for each key,
-        // there is a sorted list of items from all the layers, including removed ones.
-        ListMultimap<String, Resource> fullItemMultimap = ArrayListMultimap.create();
-
-        for (ResourceSet resourceSet : mResourceSets) {
-            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
-            for (Map.Entry<String, Collection<Resource>> entry : map.asMap().entrySet()) {
-                fullItemMultimap.putAll(entry.getKey(), entry.getValue());
-            }
-        }
-
-        return fullItemMultimap;
-    }
+    private Set<String> mQualifierWithDeletedValues;
 
     /**
      * Writes the result of the merge to a destination resource folder.
@@ -151,250 +72,43 @@ public class ResourceMerger implements ResourceMap {
      *
      * @param rootFolder the folder to write the resources in.
      * @param aaptRunner an aapt runner.
-     * @throws IOException
-     * @throws DuplicateResourceException
-     * @throws ExecutionException
+     * @throws java.io.IOException
+     * @throws DuplicateDataException
+     * @throws java.util.concurrent.ExecutionException
      * @throws InterruptedException
      */
-    public void writeResourceFolder(@NonNull File rootFolder, @Nullable AaptRunner aaptRunner)
-            throws IOException, DuplicateResourceException, ExecutionException,
-            InterruptedException {
-        WaitableExecutor executor = new WaitableExecutor();
-
-        // get all the resource keys.
-        Set<String> resourceKeys = Sets.newHashSet();
-
-        for (ResourceSet resourceSet : mResourceSets) {
-            // quick check on duplicates in the resource set.
-            resourceSet.checkItems();
-            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
-            resourceKeys.addAll(map.keySet());
-        }
-
-        // map of XML values files to write after parsing all the files.
-        // the key is the qualifier.
-        ListMultimap<String, Resource> valuesResMap = ArrayListMultimap.create();
-        // set of qualifier that was a previously written resource disappear. This is to keep track
-        // of which file to write if no other resources are touched.
-        Set<String> qualifierWithDeletedValues = Sets.newHashSet();
-
-        // loop on all the resources.
-        for (String resourceKey : resourceKeys) {
-            // for each resource, look in the resource sets, starting from the end of the list.
-
-            Resource previouslyWritten = null;
-            Resource toWrite = null;
+    public void writeDataFolder(@NonNull File rootFolder, @Nullable AaptRunner aaptRunner)
+            throws IOException, DuplicateDataException, ExecutionException, InterruptedException {
 
-            /*
-             * We are looking for what to write/delete: the last non deleted item, and the
-             * previously written one.
-             */
+        // init some field used during the write.
+        mAaptRunner = aaptRunner;
+        mValuesResMap = ArrayListMultimap.create();
+        mQualifierWithDeletedValues = Sets.newHashSet();
 
-            setLoop: for (int i = mResourceSets.size() - 1 ; i >= 0 ; i--) {
-                ResourceSet resourceSet = mResourceSets.get(i);
-
-                // look for the resource key in the set
-                ListMultimap<String, Resource> resourceMap = resourceSet.getResourceMap();
-
-                List<Resource> resources = resourceMap.get(resourceKey);
-                if (resources.isEmpty()) {
-                    continue;
-                }
-
-                // The list can contain at max 2 items. One touched and one deleted.
-                // More than one deleted means there was more than one which isn't possible
-                // More than one touched means there is more than one and this isn't possible.
-                for (int ii = resources.size() - 1 ; ii >= 0 ; ii--) {
-                    Resource resource = resources.get(ii);
-
-                    if (resource.isWritten()) {
-                        assert previouslyWritten == null;
-                        previouslyWritten = resource;
-                    }
-
-                    if (toWrite == null && !resource.isRemoved()) {
-                        toWrite = resource;
-                    }
-
-                    if (toWrite != null && previouslyWritten != null) {
-                        break setLoop;
-                    }
-                }
-            }
-
-            // done searching, we should at least have something.
-            assert previouslyWritten != null || toWrite != null;
-
-            // now need to handle, the type of each (single res file, multi res file), whether
-            // they are the same object or not, whether the previously written object was deleted.
-
-            if (toWrite == null) {
-                // nothing to write? delete only then.
-                assert previouslyWritten.isRemoved();
-
-                ResourceFile.FileType type = previouslyWritten.getSource().getType();
-
-                if (type == ResourceFile.FileType.SINGLE) {
-                    removeOutFile(rootFolder, previouslyWritten.getSource());
-                } else {
-                    qualifierWithDeletedValues.add(previouslyWritten.getSource().getQualifiers());
-                }
-
-            } else if (previouslyWritten == null || previouslyWritten == toWrite) {
-                // easy one: new or updated res
-
-                writeResource(rootFolder, valuesResMap, toWrite, executor, aaptRunner);
-            } else {
-                // replacement of a resource by another.
-
-                // first force the writing of the new one.
-                toWrite.setTouched();
-
-                // write the new value
-                writeResource(rootFolder, valuesResMap, toWrite, executor, aaptRunner);
-
-                ResourceFile.FileType previousType = previouslyWritten.getSource().getType();
-                ResourceFile.FileType newType = toWrite.getSource().getType();
-
-                if (previousType == newType) {
-                    // if the type is multi, then we make sure to flag the
-                    // qualifier as deleted.
-                    if (previousType == ResourceFile.FileType.MULTI) {
-                        qualifierWithDeletedValues.add(
-                                previouslyWritten.getSource().getQualifiers());
-                    }
-                } else if (newType == ResourceFile.FileType.SINGLE) {
-                    // new type is single, so old type is multi.
-                    // ensure the previous one is deleted by forcing rewrite of its associated
-                    // qualifiers.
-                    qualifierWithDeletedValues.add(previouslyWritten.getSource().getQualifiers());
-                } else {
-                    // new type is values, and old is single res file.
-                    // delete the old single res file
-                    removeOutFile(rootFolder, previouslyWritten.getSource());
-                }
-            }
-        }
-
-        // now write the values files.
-        for (String key : valuesResMap.keySet()) {
-            // the key is the qualifier.
-
-            // check if we have to write the file due to deleted values.
-            // also remove it from that list anyway (to detect empty qualifiers later).
-            boolean mustWriteFile = qualifierWithDeletedValues.remove(key);
-
-            // get the list of items to write
-            Collection<Resource> items = valuesResMap.get(key);
-
-            // now check if we really have to write it
-            if (!mustWriteFile) {
-                for (Resource item : items) {
-                    if (item.isTouched()) {
-                        mustWriteFile = true;
-                        break;
-                    }
-                }
-            }
-
-            if (mustWriteFile) {
-                String folderName = key.length() > 0 ?
-                        ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key :
-                        ResourceFolderType.VALUES.getName();
-
-                File valuesFolder = new File(rootFolder, folderName);
-                createDir(valuesFolder);
-                File outFile = new File(valuesFolder, FN_VALUES_XML);
-
-                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(TAG_RESOURCES);
-                    document.appendChild(rootNode);
-
-                    for (Resource item : items) {
-                        Node adoptedNode = NodeUtils.adoptNode(document, item.getValue());
-                        rootNode.appendChild(adoptedNode);
-                    }
-
-                    String content = XmlPrettyPrinter.prettyPrint(document);
-
-                    Files.write(content, outFile, Charsets.UTF_8);
-                } catch (ParserConfigurationException e) {
-                    throw new IOException(e);
-                }
-            }
-        }
-
-        // now remove empty values files.
-        for (String key : qualifierWithDeletedValues) {
-            String folderName = key != null && key.length() > 0 ?
-                    ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key :
-                    ResourceFolderType.VALUES.getName();
-
-            removeOutFile(rootFolder, folderName, FN_VALUES_XML);
-        }
-
-        executor.waitForTasks();
-    }
-
-    /**
-     * Removes a file that already exists in the out res folder.
-     * @param outFolder the out res folder
-     * @param sourceFile the source file that created the file to remove.
-     * @return true if success.
-     */
-    private static boolean removeOutFile(File outFolder, ResourceFile sourceFile) {
-        if (sourceFile.getType() == ResourceFile.FileType.MULTI) {
-            throw new IllegalArgumentException("SourceFile cannot be a FileType.MULTI");
+        try {
+            super.writeDataFolder(rootFolder);
+        } finally {
+            mAaptRunner = null;
+            mValuesResMap = null;
+            mQualifierWithDeletedValues = null;
         }
-
-        File file = sourceFile.getFile();
-        String fileName = file.getName();
-        String folderName = file.getParentFile().getName();
-
-        return removeOutFile(outFolder, folderName, fileName);
     }
 
     /**
-     * Removes a file from a folder based on a sub folder name and a filename
+     * Writes a given ResourceItem to a given root res folder.
      *
-     * @param outFolder the root folder to remove the file from
-     * @param folderName the sub folder name
-     * @param fileName the file name.
-     * @return true if success.
-     */
-    private static boolean removeOutFile(File outFolder, String folderName, String fileName) {
-        File valuesFolder = new File(outFolder, folderName);
-        File outFile = new File(valuesFolder, fileName);
-        return outFile.delete();
-    }
-
-    /**
-     * Writes a given Resource to a given root res folder.
-     * If the Resource is to be written in a "Values" folder, then it is added to a map instead.
+     * If the ResourceItem is to be written in a "Values" folder, then it is added to a map instead.
      *
      * @param rootFolder the root res folder
-     * @param valuesResMap a map of existing values-type resources where the key is the qualifiers
-     *                     of the values folder.
-     * @param resource the resource to add.
+     * @param item the resource to add.
      * @param executor an executor
-     * @param aaptRunner an aapt runner.
-     * @throws IOException
+     * @throws java.io.IOException
      */
-    private void writeResource(@NonNull final File rootFolder,
-                               @NonNull ListMultimap<String, Resource> valuesResMap,
-                               @NonNull final Resource resource,
-                               @NonNull WaitableExecutor executor,
-                               @Nullable final AaptRunner aaptRunner) throws IOException {
-        ResourceFile.FileType type = resource.getSource().getType();
+    @Override
+    protected void writeItem(@NonNull final File rootFolder,
+                             @NonNull final ResourceItem item,
+                             @NonNull WaitableExecutor executor) throws IOException {
+        ResourceFile.FileType type = item.getSource().getType();
 
         if (type == ResourceFile.FileType.MULTI) {
             // this is a resource for the values files
@@ -402,24 +116,24 @@ public class ResourceMerger implements ResourceMap {
             // just add the node to write to the map based on the qualifier.
             // We'll figure out later if the files needs to be written or (not)
 
-            String qualifier = resource.getSource().getQualifiers();
+            String qualifier = item.getSource().getQualifiers();
             if (qualifier == null) {
                 qualifier = "";
             }
 
-            valuesResMap.put(qualifier, resource);
+            mValuesResMap.put(qualifier, item);
         } else {
             // This is a single value file.
             // Only write it if the state is TOUCHED.
-            if (resource.isTouched()) {
+            if (item.isTouched()) {
                 executor.execute(new Callable() {
                     @Override
                     public Object call() throws Exception {
-                        ResourceFile resourceFile = resource.getSource();
+                        ResourceFile resourceFile = item.getSource();
                         File file = resourceFile.getFile();
 
                         String filename = file.getName();
-                        String folderName = resource.getType().getName();
+                        String folderName = item.getType().getName();
                         String qualifiers = resourceFile.getQualifiers();
                         if (qualifiers != null && qualifiers.length() > 0) {
                             folderName = folderName + RES_QUALIFIER_SEP + qualifiers;
@@ -430,10 +144,10 @@ public class ResourceMerger implements ResourceMap {
 
                         File outFile = new File(typeFolder, filename);
 
-                        if (aaptRunner != null && filename.endsWith(DOT_9PNG)) {
+                        if (mAaptRunner != null && filename.endsWith(DOT_9PNG)) {
                             // run aapt in single crunch mode on the original file to write the
                             // destination file.
-                            aaptRunner.crunchPng(file, outFile);
+                            mAaptRunner.crunchPng(file, outFile);
                         } else {
                             Files.copy(file, outFile);
                         }
@@ -444,205 +158,160 @@ public class ResourceMerger implements ResourceMap {
         }
     }
 
+    @Override
+    protected void removeItem(File rootFolder, ResourceItem removedItem, ResourceItem replacedBy) {
+        ResourceFile.FileType removedType = removedItem.getSource().getType();
+        ResourceFile.FileType replacedType = replacedBy != null ?
+                replacedBy.getSource().getType() : null;
+
+        if (removedType == replacedType) {
+            // if the type is multi, then we make sure to flag the qualifier as deleted.
+            if (removedType == ResourceFile.FileType.MULTI) {
+                mQualifierWithDeletedValues.add(
+                        removedItem.getSource().getQualifiers());
+            } else {
+                // both are single type resources, so we actually don't delete the previous
+                // file as the new one will replace it instead.
+            }
+        } else if (removedType == ResourceFile.FileType.SINGLE) {
+            // removed type is single.
+            // The case of both single type is above, so here either, there is no replacement
+            // or the replacement is multi. We always need to remove the old file.
+            // if replacedType is non-null, then it was values, if not,
+            removeOutFile(rootFolder, removedItem.getSource());
+        } else {
+            // removed type is multi.
+            // whether the new type is single or doesn't exist, we always need to mark the qualifier
+            // for rewrite.
+            mQualifierWithDeletedValues.add(removedItem.getSource().getQualifiers());
+        }
+    }
+
     /**
-     * Writes a single blog file to store all that the ResourceMerger knows about.
+     * Removes a file that already exists in the out res folder. This has to be a non value file.
      *
-     * @param blobRootFolder the root folder where blobs are store.
-     * @throws java.io.IOException
-     *
-     * @see #loadFromBlob(java.io.File)
+     * @param outFolder the out res folder
+     * @param resourceFile the source file that created the file to remove.
+     * @return true if success.
      */
-    public void writeBlobTo(File blobRootFolder) throws IOException {
-        // write "compact" blob
-        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(NODE_MERGER);
-            document.appendChild(rootNode);
-
-            for (ResourceSet resourceSet : mResourceSets) {
-                Node resourceSetNode = document.createElement(NODE_RESOURCE_SET);
-                rootNode.appendChild(resourceSetNode);
-
-                resourceSet.appendToXml(resourceSetNode, document);
-            }
+    private static boolean removeOutFile(File outFolder, ResourceFile resourceFile) {
+        if (resourceFile.getType() == ResourceFile.FileType.MULTI) {
+            throw new IllegalArgumentException("SourceFile cannot be a FileType.MULTI");
+        }
 
-            String content = XmlPrettyPrinter.prettyPrint(document);
+        File file = resourceFile.getFile();
+        String fileName = file.getName();
+        String folderName = file.getParentFile().getName();
 
-            createDir(blobRootFolder);
-            Files.write(content, new File(blobRootFolder, FN_MERGER_XML), Charsets.UTF_8);
-        } catch (ParserConfigurationException e) {
-            throw new IOException(e);
-        }
+        return removeOutFile(outFolder, folderName, fileName);
     }
 
     /**
-     * Loads the merger state from a blob file.
-     *
-     * @param blobRootFolder the folder containing the blob.
-     * @return true if the blob was loaded.
-     * @throws IOException
+     * Removes a file from a folder based on a sub folder name and a filename
      *
-     * @see #writeBlobTo(java.io.File)
+     * @param outFolder the root folder to remove the file from
+     * @param folderName the sub folder name
+     * @param fileName the file name.
+     * @return true if success.
      */
-    public boolean loadFromBlob(File blobRootFolder) throws IOException {
-        File file = new File(blobRootFolder, FN_MERGER_XML);
-        if (!file.isFile()) {
-            return false;
-        }
-
-        BufferedInputStream stream = null;
-        try {
-            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-            stream = new BufferedInputStream(new FileInputStream(file));
-            InputSource is = new InputSource(stream);
-            factory.setNamespaceAware(true);
-            factory.setValidating(false);
-
-            DocumentBuilder builder = factory.newDocumentBuilder();
-            Document document = builder.parse(is);
-
-            // get the root node
-            Node rootNode = document.getDocumentElement();
-            if (rootNode == null || !NODE_MERGER.equals(rootNode.getLocalName())) {
-                return false;
-            }
+    private static boolean removeOutFile(File outFolder, String folderName, String fileName) {
+        File valuesFolder = new File(outFolder, folderName);
+        File outFile = new File(valuesFolder, fileName);
+        return outFile.delete();
+    }
 
-            NodeList nodes = rootNode.getChildNodes();
 
-            for (int i = 0, n = nodes.getLength(); i < n; i++) {
-                Node node = nodes.item(i);
+    @Override
+    protected void postWriteDataFolder(File rootFolder) throws IOException {
 
-                if (node.getNodeType() != Node.ELEMENT_NODE ||
-                        !NODE_RESOURCE_SET.equals(node.getLocalName())) {
-                    continue;
-                }
+        // now write the values files.
+        for (String key : mValuesResMap.keySet()) {
+            // the key is the qualifier.
 
-                ResourceSet resourceSet = ResourceSet.createFromXml(node);
-                if (resourceSet != null) {
-                    mResourceSets.add(resourceSet);
-                }
-            }
+            // check if we have to write the file due to deleted values.
+            // also remove it from that list anyway (to detect empty qualifiers later).
+            boolean mustWriteFile = mQualifierWithDeletedValues.remove(key);
 
-            setResourcesToWritten();
+            // get the list of items to write
+            Collection<ResourceItem> items = mValuesResMap.get(key);
 
-            return true;
-        } catch (FileNotFoundException e) {
-            throw new IOException(e);
-        } catch (ParserConfigurationException e) {
-            throw new IOException(e);
-        } catch (SAXException e) {
-            throw new IOException(e);
-        } finally {
-            try {
-                if (stream != null) {
-                    stream.close();
+            // now check if we really have to write it
+            if (!mustWriteFile) {
+                for (ResourceItem item : items) {
+                    if (item.isTouched()) {
+                        mustWriteFile = true;
+                        break;
+                    }
                 }
-            } catch (IOException e) {
-                // ignore
             }
-        }
-    }
 
-    /**
-     * Sets all existing resources to have their state be WRITTEN.
-     *
-     * @see com.android.builder.resources.Resource#isWritten()
-     */
-    private void setResourcesToWritten() {
-        ListMultimap<String, Resource> resources = ArrayListMultimap.create();
-
-        for (ResourceSet resourceSet : mResourceSets) {
-            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
-            for (Map.Entry<String, Collection<Resource>> entry : map.asMap().entrySet()) {
-                resources.putAll(entry.getKey(), entry.getValue());
-            }
-        }
+            if (mustWriteFile) {
+                String folderName = key.length() > 0 ?
+                        ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key :
+                        ResourceFolderType.VALUES.getName();
 
-        for (String key : resources.keySet()) {
-            List<Resource> resourceList = resources.get(key);
-            resourceList.get(resourceList.size() - 1).resetStatusToWritten();
-        }
-    }
+                File valuesFolder = new File(rootFolder, folderName);
+                createDir(valuesFolder);
+                File outFile = new File(valuesFolder, FN_VALUES_XML);
 
-    /**
-     * Checks that a loaded merger can be updated with a given list of ResourceSet.
-     *
-     * For now this means the sets haven't changed.
-     *
-     * @param resourceSets the resource sets.
-     * @return true if the update can be performed. false if a full merge should be done.
-     */
-    public boolean checkValidUpdate(List<ResourceSet> resourceSets) {
-        if (resourceSets.size() != mResourceSets.size()) {
-            return false;
-        }
+                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+                factory.setNamespaceAware(true);
+                factory.setValidating(false);
+                factory.setIgnoringComments(true);
+                DocumentBuilder builder;
 
-        for (int i = 0, n = resourceSets.size(); i < n; i++) {
-            ResourceSet localSet = mResourceSets.get(i);
-            ResourceSet newSet = resourceSets.get(i);
+                try {
+                    builder = factory.newDocumentBuilder();
+                    Document document = builder.newDocument();
 
-            List<File> localSourceFiles = localSet.getSourceFiles();
-            List<File> newSourceFiles = newSet.getSourceFiles();
+                    Node rootNode = document.createElement(TAG_RESOURCES);
+                    document.appendChild(rootNode);
 
-            // compare the config name and source files sizes.
-            if (!newSet.getConfigName().equals(localSet.getConfigName()) ||
-                    localSourceFiles.size() != newSourceFiles.size()) {
-                return false;
-            }
+                    for (ResourceItem item : items) {
+                        Node adoptedNode = NodeUtils.adoptNode(document, item.getValue());
+                        rootNode.appendChild(adoptedNode);
+                    }
 
-            // compare the source files. The order is not important so it should be normalized
-            // before it's compared.
-            // make copies to sort.
-            localSourceFiles = Lists.newArrayList(localSourceFiles);
-            Collections.sort(localSourceFiles);
-            newSourceFiles = Lists.newArrayList(newSourceFiles);
-            Collections.sort(newSourceFiles);
+                    String content = XmlPrettyPrinter.prettyPrint(document);
 
-            if (!localSourceFiles.equals(newSourceFiles)) {
-                return false;
+                    Files.write(content, outFile, Charsets.UTF_8);
+                } catch (ParserConfigurationException e) {
+                    throw new IOException(e);
+                }
             }
         }
 
-        return true;
-    }
+        // now remove empty values files.
+        for (String key : mQualifierWithDeletedValues) {
+            String folderName = key != null && key.length() > 0 ?
+                    ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key :
+                    ResourceFolderType.VALUES.getName();
 
-    /**
-     * Returns a ResourceSet that contains a given file.
-     *
-     * "contains" means that the ResourceSet has a source file/folder that is the root folder
-     * of this file. The folder and/or file doesn't have to exist.
-     *
-     * @param file the file to check
-     * @return a pair containing the ResourceSet and its source file that contains the file.
-     */
-    public Pair<ResourceSet, File> getResourceSetContaining(File file) {
-        for (ResourceSet resourceSet : mResourceSets) {
-            File sourceFile = resourceSet.findMatchingSourceFile(file);
-            if (file != null) {
-                return Pair.of(resourceSet, sourceFile);
-            }
+            removeOutFile(rootFolder, folderName, FN_VALUES_XML);
         }
-
-        return null;
     }
 
-    private synchronized void createDir(File folder) throws IOException {
-        if (!folder.isDirectory() && !folder.mkdirs()) {
-            throw new IOException("Failed to create directory: " + folder);
-        }
+    @Override
+    protected ResourceSet createFromXml(Node node) {
+        ResourceSet set = new ResourceSet("");
+        return (ResourceSet) set.createFromXml(node);
     }
 
 
+    /**
+     * Call {@link #writeDataFolder(java.io.File, com.android.builder.AaptRunner)} instead.
+     *
+     * @param rootFolder the folder to write the resources in.
+     * @throws java.io.IOException
+     * @throws DuplicateDataException
+     * @throws java.util.concurrent.ExecutionException
+     * @throws InterruptedException
+     */
     @Override
-    public String toString() {
-        return Arrays.toString(mResourceSets.toArray());
+    public void writeDataFolder(@NonNull File rootFolder)
+            throws IOException, DuplicateDataException, ExecutionException, InterruptedException {
+
+        throw new UnsupportedOperationException(
+                "Call writeDataFolder(File, AaptRunner) instead");
     }
 }
old mode 100755 (executable)
new mode 100644 (file)
index d537042..db76e80
@@ -22,655 +22,258 @@ import com.android.resources.FolderTypeRelationship;
 import com.android.resources.ResourceConstants;
 import com.android.resources.ResourceFolderType;
 import com.android.resources.ResourceType;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import org.w3c.dom.Attr;
-import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+import static com.android.builder.resources.ResourceFile.ATTR_QUALIFIER;
+import static com.android.builder.resources.ResourceFile.ATTR_TYPE;
+
 /**
- * Represents a set of resources.
- *
- * The resources can be coming from multiple source folders. Duplicates are detected (either
- * from the same source folder -- same resource in values files -- or across the source folders.
- *
- * Each source folders is considered to be at the same level. To use overlays, a
- * {@link ResourceMerger} must be used.
- *
- * Creating the set and adding folders does not load the data.
- * The data can be loaded from the files, or from a blob which is generated by the set itself.
+ * Implementation of {@link DataSet} for {@link ResourceItem} and {@link ResourceFile}.
  *
- * Upon loading the data from the blob, the data can be updated with fresher files. Each resource
- * that is updated is flagged as such, in order to manage incremental update.
- *
- * Writing/Loading the blob is not done through this class directly, but instead through the
- * {@link ResourceMerger} which contains ResourceSet objects.
+ * This is able to detect duplicates from the same source folders (same resource coming from
+ * the values folder in same or different files).
  */
-public class ResourceSet implements SourceSet, ResourceMap {
-
-    private static final String NODE_SOURCE = "source";
-    private static final String ATTR_CONFIG = "config";
-    private static final String ATTR_PATH = "path";
-    private static final String NODE_FILE = "file";
-    private static final String ATTR_QUALIFIER = "qualifiers";
-    private static final String ATTR_TYPE = "type";
-    private static final String ATTR_NAME = "name";
+public class ResourceSet extends DataSet<ResourceItem, ResourceFile> {
 
-    private final String mConfigName;
-
-    /**
-     * List of source files. The may not have been loaded yet.
-     */
-    private final List<File> mSourceFiles = Lists.newArrayList();
-
-    /**
-     * The key is the {@link com.android.builder.resources.Resource#getKey()}.
-     * This is a multimap to support moving a resource from one file to another (values file)
-     * during incremental update.
-     */
-    private final ListMultimap<String, Resource> mItems = ArrayListMultimap.create();
-
-    /**
-     * Map of source files to ResourceFiles. This is a multimap because the key is the source
-     * file/folder, not the
-     * File for the resource file itself.
-     */
-    private final ListMultimap<File, ResourceFile> mSourceFileToResourceFilesMap = ArrayListMultimap.create();
-    /**
-     * Map from a File to its ResourceFile.
-     */
-    private final Map<File, ResourceFile> mResourceFileMap = Maps.newHashMap();
-
-    /**
-     * Creates a resource set with a given configName. The name is used to identify the set
-     * across sessions.
-     *
-     * @param configName the name of the config this set is associated with.
-     */
-    public ResourceSet(String configName) {
-        mConfigName = configName;
-    }
-
-    /**
-     * Adds a collection of source files.
-     * @param files the source files to add.
-     */
-    public void addSources(Collection<File> files) {
-        mSourceFiles.addAll(files);
-    }
-
-    /**
-     * Adds a new source file
-     * @param file the source file.
-     */
-    public void addSource(File file) {
-        mSourceFiles.add(file);
+    public ResourceSet(String name) {
+        super(name);
     }
 
-    /**
-     * Get the list of source files.
-     * @return the source files.
-     */
-    @NonNull
     @Override
-    public List<File> getSourceFiles() {
-        return mSourceFiles;
-    }
-
-    /**
-     * Returns the config name.
-     * @return the config name.
-     */
-    public String getConfigName() {
-        return mConfigName;
+    protected DataSet<ResourceItem, ResourceFile> createSet(String name) {
+        return new ResourceSet(name);
     }
 
-    /**
-     * Returns a matching Source file that contains a given file.
-     *
-     * "contains" means that the source file/folder is the root folder
-     * of this file. The folder and/or file doesn't have to exist.
-     *
-     * @param file the file to search for
-     * @return the Source file or null if no match is found.
-     */
     @Override
-    public File findMatchingSourceFile(File file) {
-        for (File sourceFile : mSourceFiles) {
-            if (sourceFile.equals(file)) {
-                return sourceFile;
-            } else if (sourceFile.isDirectory()) {
-                String sourcePath = sourceFile.getAbsolutePath() + File.separator;
-                if (file.getAbsolutePath().startsWith(sourcePath)) {
-                    return sourceFile;
-                }
-            }
-        }
+    protected ResourceFile createFileAndItems(File file) {
+        // get the type.
+        FolderData folderData = getFolderData(file.getParentFile());
 
-        return null;
+        return createResourceFile(file, folderData);
     }
 
-    /**
-     * Returns the number of resources.
-     * @return the number of resources.
-     *
-     * @see ResourceMap
-     */
     @Override
-    public int size() {
-        // returns the number of keys, not the size of the multimap which would include duplicate
-        // Resource objects.
-        return mItems.keySet().size();
-    }
+    protected ResourceFile createFileAndItems(File file, Node fileNode) {
+        Attr qualifierAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_QUALIFIER);
+        String qualifier = qualifierAttr != null ? qualifierAttr.getValue() : null;
 
-    /**
-     * Returns whether the set is empty of resources.
-     * @return true if the set contains no resources.
-     */
-    public boolean isEmpty() {
-        return mItems.isEmpty();
-    }
+        Attr typeAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_TYPE);
+        if (typeAttr == null) {
+            // multi res file
+            List<ResourceItem> resourceList = Lists.newArrayList();
 
-    /**
-     * Returns a map of the resources.
-     * @return a map of items.
-     *
-     * @see ResourceMap
-     */
-    @NonNull
-    @Override
-    public ListMultimap<String, Resource> getResourceMap() {
-        return mItems;
-    }
+            // loop on each node that represent a resource
+            NodeList resNodes = fileNode.getChildNodes();
+            for (int iii = 0, nnn = resNodes.getLength(); iii < nnn; iii++) {
+                Node resNode = resNodes.item(iii);
 
-    /**
-     * Loads the resource set from the file its source folder contains.
-     *
-     * All loaded resources are set to TOUCHED. This is so that after loading the resources from
-     * the files, they can be written directly (since touched force them to be written).
-     *
-     * This also checks for duplicates resources.
-     *
-     * @throws DuplicateResourceException
-     * @throws IOException
-     */
-    public void loadFromFiles() throws DuplicateResourceException, IOException {
-        for (File file : mSourceFiles) {
-            if (file.isDirectory()) {
-                readSourceFolder(file);
-
-            } else if (file.isFile()) {
-                // TODO support resource bundle
-            }
-        }
-        checkItems();
-    }
+                if (resNode.getNodeType() != Node.ELEMENT_NODE) {
+                    continue;
+                }
 
-    /**
-     * Appends the resourceSet to a given DOM object.
-     *
-     * @param resourceSetNode the root node for this resource set.
-     * @param document The root XML document
-     */
-    void appendToXml(Node resourceSetNode, Document document) {
-        // add the config name attribute
-        NodeUtils.addAttribute(document, resourceSetNode, null, ATTR_CONFIG, mConfigName);
-
-        // add the source files.
-        // we need to loop on the source files themselves and not the map to ensure we
-        // write empty resourceSets
-        for (File sourceFile : mSourceFiles) {
-
-            // the node for the source and its path attribute
-            Node sourceNode = document.createElement(NODE_SOURCE);
-            resourceSetNode.appendChild(sourceNode);
-            NodeUtils.addAttribute(document, sourceNode, null, ATTR_PATH,
-                    sourceFile.getAbsolutePath());
-
-            Collection<ResourceFile> resourceFiles = mSourceFileToResourceFilesMap.get(sourceFile);
-
-            for (ResourceFile resourceFile : resourceFiles) {
-                // the node for the file and its path and qualifiers attribute
-                Node fileNode = document.createElement(NODE_FILE);
-                sourceNode.appendChild(fileNode);
-                NodeUtils.addAttribute(document, fileNode, null, ATTR_PATH,
-                        resourceFile.getFile().getAbsolutePath());
-                NodeUtils.addAttribute(document, fileNode, null, ATTR_QUALIFIER,
-                        resourceFile.getQualifiers());
-
-                if (resourceFile.getType() == ResourceFile.FileType.MULTI) {
-                    for (Resource item : resourceFile.getItems()) {
-                        Node adoptedNode = NodeUtils.adoptNode(document, item.getValue());
-                        fileNode.appendChild(adoptedNode);
-                    }
-                } else {
-                    Resource item = resourceFile.getItem();
-                    NodeUtils.addAttribute(document, fileNode, null, ATTR_TYPE,
-                            item.getType().getName());
-                    NodeUtils.addAttribute(document, fileNode, null, ATTR_NAME, item.getName());
+                ResourceItem r = ValueResourceParser.getResource(resNode);
+                if (r != null) {
+                    resourceList.add(r);
                 }
             }
-        }
-    }
-
-    /**
-     * Creates a new ResourceSet from an XML node that was created with
-     * {@link #appendToXml(org.w3c.dom.Node, org.w3c.dom.Document)}
-     *
-     * @param resourceSetNode the node to read from.
-     * @return a new ResourceSet object or null.
-     */
-    static ResourceSet createFromXml(Node resourceSetNode) {
-        // get the config name
-        Attr configNameAttr = (Attr) resourceSetNode.getAttributes().getNamedItem(ATTR_CONFIG);
-        if (configNameAttr == null) {
-            return null;
-        }
-
-        // create the ResourceSet that will be filled with the content of the XML.
-        ResourceSet resourceSet = new ResourceSet(configNameAttr.getValue());
 
-        // loop on the source nodes
-        NodeList sourceNodes = resourceSetNode.getChildNodes();
-        for (int i = 0, n = sourceNodes.getLength(); i < n; i++) {
-            Node sourceNode = sourceNodes.item(i);
+            return new ResourceFile(file, resourceList, qualifier);
 
-            if (sourceNode.getNodeType() != Node.ELEMENT_NODE ||
-                    !NODE_SOURCE.equals(sourceNode.getLocalName())) {
-                continue;
+        } else {
+            // single res file
+            ResourceType type = ResourceType.getEnum(typeAttr.getValue());
+            if (type == null) {
+                return null;
             }
 
-            Attr pathAttr = (Attr) sourceNode.getAttributes().getNamedItem(ATTR_PATH);
-            if (pathAttr == null) {
-                continue;
+            Attr nameAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_NAME);
+            if (nameAttr == null) {
+                return null;
             }
 
-            File sourceFolder = new File(pathAttr.getValue());
-            resourceSet.mSourceFiles.add(sourceFolder);
-
-            // now loop on the files inside the source folder.
-            NodeList fileNodes = sourceNode.getChildNodes();
-            for (int j = 0, m = fileNodes.getLength(); j < m; j++) {
-                Node fileNode = fileNodes.item(j);
-
-                if (fileNode.getNodeType() != Node.ELEMENT_NODE ||
-                        !NODE_FILE.equals(fileNode.getLocalName())) {
-                    continue;
-                }
-
-                pathAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_PATH);
-                if (pathAttr == null) {
-                    continue;
-                }
-
-                Attr qualifierAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_QUALIFIER);
-                String qualifier = qualifierAttr != null ? qualifierAttr.getValue() : null;
-
-                Attr typeAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_TYPE);
-                if (typeAttr == null) {
-                    // multi res file
-                    List<Resource> resourceList = Lists.newArrayList();
-
-                    // loop on each node that represent a resource
-                    NodeList resNodes = fileNode.getChildNodes();
-                    for (int iii = 0, nnn = resNodes.getLength(); iii < nnn; iii++) {
-                        Node resNode = resNodes.item(iii);
-
-                        if (resNode.getNodeType() != Node.ELEMENT_NODE) {
-                            continue;
-                        }
-
-                        Resource r = ValueResourceParser.getResource(resNode);
-                        if (r != null) {
-                            resourceList.add(r);
-                        }
-                    }
-
-                    ResourceFile resourceFile = new ResourceFile(new File(pathAttr.getValue()),
-                            resourceList, qualifier);
-                    resourceSet.addResourceFile(sourceFolder, resourceFile);
-
-                    for (Resource item : resourceList) {
-                        resourceSet.mItems.put(item.getKey(), item);
-                    }
-
-                } else {
-                    // single res file
-                    ResourceType type = ResourceType.getEnum(typeAttr.getValue());
-                    if (type == null) {
-                        continue;
-                    }
-
-                    Attr nameAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_NAME);
-                    if (nameAttr == null) {
-                        continue;
-                    }
-
-                    Resource item = new Resource(nameAttr.getValue(), type, null);
-                    ResourceFile resourceFile = new ResourceFile(new File(pathAttr.getValue()),
-                            item, qualifier);
-
-                    resourceSet.addResourceFile(sourceFolder, resourceFile);
-                    resourceSet.mItems.put(item.getKey(), item);
-                }
-            }
+            ResourceItem item = new ResourceItem(nameAttr.getValue(), type, null);
+            return new ResourceFile(file, item, qualifier);
         }
-
-        return resourceSet;
     }
 
-    /**
-     * Reads the content of a resource folders and loads the resources.
-     * @param sourceFolder the source folder to load the resources from.
-     *
-     * @throws DuplicateResourceException
-     * @throws IOException
-     */
-    private void readSourceFolder(File sourceFolder)
-            throws DuplicateResourceException, IOException {
+    @Override
+    protected void readSourceFolder(File sourceFolder) throws DuplicateDataException, IOException {
         File[] folders = sourceFolder.listFiles();
         if (folders != null) {
             for (File folder : folders) {
                 // TODO: use the aapt ignore pattern value.
                 if (folder.isDirectory() &&
                         PackagingUtils.checkFolderForPackaging(folder.getName())) {
-                    parseFolder(sourceFolder, folder);
+                    FolderData folderData = getFolderData(folder);
+                    if (folderData.folderType != null) {
+                        parseFolder(sourceFolder, folder, folderData);
+                    }
                 }
             }
         }
     }
 
-    /**
-     * temp structure containing a qualifier string and a {@link ResourceType}.
-     */
-    private static class FolderData {
-        String qualifiers = null;
-        ResourceType type = null;
+    @Override
+    protected boolean isValidSourceFile(File sourceFolder, File file) {
+        File resFolder = file.getParentFile();
+        // valid files are right under a resource folder under the source folder
+        return resFolder.getParentFile().equals(sourceFolder) &&
+                ResourceFolderType.getFolderType(resFolder.getName()) != null;
     }
 
-    /**
-     * Returns a FolderData for the given folder
-     * @param folder the folder.
-     * @return the FolderData object.
-     */
-    @NonNull
-    private static FolderData getFolderData(File folder) {
-        FolderData fd = new FolderData();
+    @Override
+    protected boolean handleChangedFile(File sourceFolder, File changedFile) throws IOException {
 
-        String folderName = folder.getName();
-        int pos = folderName.indexOf(ResourceConstants.RES_QUALIFIER_SEP);
-        ResourceFolderType folderType;
-        if (pos != -1) {
-            folderType = ResourceFolderType.getTypeByName(folderName.substring(0, pos));
-            fd.qualifiers = folderName.substring(pos + 1);
+        FolderData folderData = getFolderData(changedFile.getParentFile());
+        ResourceFile resourceFile = getDataFile(changedFile);
+
+        if (folderData.type != null) {
+                    // single res file
+            resourceFile.getItem().setTouched();
         } else {
-            folderType = ResourceFolderType.getTypeByName(folderName);
-        }
+            // multi res. Need to parse the file and compare the items one by one.
+            ValueResourceParser parser = new ValueResourceParser(changedFile);
+
+            List<ResourceItem> parsedItems = parser.parseFile();
+            Map<String, ResourceItem> oldItems = Maps.newHashMap(resourceFile.getItemMap());
+            Map<String, ResourceItem> newItems  = Maps.newHashMap();
+
+            // create a fake ResourceFile to be able to call resource.getKey();
+            // It's ok because we never use this instance anyway.
+            ResourceFile fakeResourceFile = new ResourceFile(changedFile, parsedItems,
+                    resourceFile.getQualifiers());
+
+            for (ResourceItem newItem : parsedItems) {
+                String newKey = newItem.getKey();
+                ResourceItem oldItem = oldItems.get(newKey);
+
+                if (oldItem == null) {
+                    // this is a new item
+                    newItem.setTouched();
+                    newItems.put(newKey, newItem);
+                } else {
+                    // remove it from the list of oldItems (this is to detect deletion)
+                    //noinspection SuspiciousMethodCalls
+                    oldItems.remove(oldItem.getKey());
+
+                    // now compare the items
+                    if (!oldItem.compareValueWith(newItem)) {
+                        // if the values are different, take the values from the newItems
+                        // and update the old item status.
+
+                        oldItem.setValue(newItem);
+                    }
+                }
+            }
+
+            // at this point oldItems is left with the deleted items.
+            // just update their status to removed.
+            for (ResourceItem deletedItem : oldItems.values()) {
+                deletedItem.setRemoved();
+            }
 
-        if (folderType != ResourceFolderType.VALUES) {
-            fd.type = FolderTypeRelationship.getRelatedResourceTypes(folderType).get(0);
+            // Now we need to add the new items to the resource file and the main map
+            resourceFile.addItems(newItems.values());
+            for (Map.Entry<String, ResourceItem> entry : newItems.entrySet()) {
+                addItem(entry.getValue(), entry.getKey());
+            }
         }
 
-        return fd;
+       return true;
     }
 
+
     /**
      * Reads the content of a typed resource folder (sub folder to the root of res folder), and
      * loads the resources from it.
      *
      * @param sourceFolder the main res folder
      * @param folder the folder to read.
+     * @param folderData the folder Data
      *
      * @throws IOException
      */
-    private void parseFolder(File sourceFolder, File folder)
+    private void parseFolder(File sourceFolder, File folder, FolderData folderData)
             throws IOException {
-        // get the type.
-        FolderData folderData = getFolderData(folder);
-
-        // get the files
         File[] files = folder.listFiles();
         if (files != null && files.length > 0) {
             for (File file : files) {
-                if (!checkFileForAndroidRes(file)) {
+                if (!file.isFile() || !checkFileForAndroidRes(file)) {
                     continue;
                 }
-                if (folderData.type != null) {
-                    Resource item = handleSingleResFile(sourceFolder,
-                            folderData.qualifiers, folderData.type, file);
-                    item.setTouched();
-                } else {
-                    Collection<Resource> items = handleMultiResFile(sourceFolder,
-                            folderData.qualifiers, file);
-                    for (Resource item : items) {
-                        item.setTouched();
-                    }
-                }
-            }
-        }
-    }
 
-    /**
-     * Handles a single resource file (ie not located in "values") and create a Resource from it.
-     *
-     * @param sourceFolder the top res folder for the file
-     * @param qualifiers the qualifiers associated with the file
-     * @param type the ResourceType read from the parent folder name
-     * @param file the single resource file
-     * @return a Resource object
-     */
-    @NonNull
-    private Resource handleSingleResFile(File sourceFolder, String qualifiers,
-                                         ResourceType type, File file) {
-        int pos;// get the resource name based on the filename
-        String name = file.getName();
-        pos = name.indexOf('.');
-        if (pos >= 0) {
-            name = name.substring(0, pos);
+                ResourceFile resourceFile = createResourceFile(file, folderData);
+                processNewDataFile(sourceFolder, resourceFile, true /*setTouched*/);
+            }
         }
-
-        Resource item = new Resource(name, type, null);
-        ResourceFile resourceFile = new ResourceFile(file, item, qualifiers);
-        addResourceFile(sourceFolder, resourceFile);
-
-        mItems.put(item.getKey(), item);
-
-        return item;
     }
 
-    /**
-     * Handles a multi res file (in a "values" folder) and create Resource object from it.
-     *
-     * @param sourceFolder the top res folder for the file
-     * @param qualifiers the qualifiers associated with the file
-     * @param file the single resource file
-     * @return a list of created Resource objects.
-     *
-     * @throws IOException
-     */
-    @NonNull
-    private Collection<Resource> handleMultiResFile(File sourceFolder, String qualifiers, File file)
-            throws IOException {
-        ValueResourceParser parser = new ValueResourceParser(file);
-        List<Resource> items = parser.parseFile();
-
-        ResourceFile resourceFile = new ResourceFile(file, items, qualifiers);
-        addResourceFile(sourceFolder, resourceFile);
-
-        for (Resource item : items) {
-            mItems.put(item.getKey(), item);
-        }
-
-        return items;
-    }
+    private ResourceFile createResourceFile(File file, FolderData folderData) {
+        if (folderData.type != null) {
+            int pos;// get the resource name based on the filename
+            String name = file.getName();
+            pos = name.indexOf('.');
+            if (pos >= 0) {
+                name = name.substring(0, pos);
+            }
 
-    /**
-     * Adds a new ResourceFile to this.
-     *
-     * @param sourceFile the parent source file.
-     * @param resourceFile the ResourceFile
-     */
-    private void addResourceFile(File sourceFile, ResourceFile resourceFile) {
-        mSourceFileToResourceFilesMap.put(sourceFile, resourceFile);
-        mResourceFileMap.put(resourceFile.getFile(), resourceFile);
-    }
+            return new ResourceFile(
+                    file,
+                    new ResourceItem(name, folderData.type, null),
+                    folderData.qualifiers);
+        } else {
+            try {
+                ValueResourceParser parser = new ValueResourceParser(file);
+                List<ResourceItem> items = parser.parseFile();
 
-    /**
-     * Checks for duplicate resources across all source files.
-     *
-     * @throws DuplicateResourceException if a duplicated item is found.
-     */
-    void checkItems() throws DuplicateResourceException {
-        // check a list for duplicate, ignoring removed items.
-        for (Map.Entry<String, Collection<Resource>> entry : mItems.asMap().entrySet()) {
-            Collection<Resource> items = entry.getValue();
-
-            // there can be several version of the same key if some are "removed"
-            Resource lastItem = null;
-            for (Resource item : items) {
-                if (!item.isRemoved()) {
-                    if (lastItem == null) {
-                        lastItem = item;
-                    } else {
-                        throw new DuplicateResourceException(item, lastItem);
-                    }
-                }
+                return new ResourceFile(file, items, folderData.qualifiers);
+            } catch (IOException e) {
+                return null;
             }
         }
     }
 
     /**
-     * Update the ResourceSet with a given file.
-     *
-     * @param sourceFolder the sourceFile containing the changedFile
-     * @param changedFile The changed file
-     * @param fileStatus the change state
-     * @return true if the set was properly updated, false otherwise
+     * temp structure containing a qualifier string and a {@link com.android.resources.ResourceType}.
      */
-    public boolean updateWith(File sourceFolder, File changedFile, FileStatus fileStatus)
-            throws IOException {
-        switch (fileStatus) {
-            case CHANGED:
-                FolderData folderData = getFolderData(changedFile.getParentFile());
-                ResourceFile resourceFile = mResourceFileMap.get(changedFile);
-
-                if (folderData.type != null) {
-                    // single res file
-                    resourceFile.getItem().setTouched();
-                } else {
-                    // multi res. Need to parse the file and compare the items one by one.
-                    ValueResourceParser parser = new ValueResourceParser(changedFile);
-                    List<Resource> parsedItems = parser.parseFile();
-
-                    Map<String, Resource> oldItems = Maps.newHashMap(resourceFile.getItemMap());
-
-                    Map<String, Resource> newItems  = Maps.newHashMap();
-
-                    // create a fake ResourceFile to be able to call resource.getKey();
-                    // It's ok because we never use this instance anyway.
-                    ResourceFile fakeResourceFile = new ResourceFile(changedFile, parsedItems,
-                            resourceFile.getQualifiers());
-
-                    for (Resource newItem : parsedItems) {
-                        String newKey = newItem.getKey();
-                        Resource oldItem = oldItems.get(newKey);
-
-                        if (oldItem == null) {
-                            // this is a new item
-                            newItems.put(newKey, newItem.setTouched());
-                        } else {
-                            // remove it from the list of oldItems (this is to detect deletion)
-                            //noinspection SuspiciousMethodCalls
-                            oldItems.remove(oldItem.getKey());
-
-                            // now compare the items
-                            if (!oldItem.compareValueWith(newItem)) {
-                                // if the values are different, take the values from the newItems
-                                // and update the old item status.
-
-                                oldItem.setValue(newItem);
-                            }
-                        }
-                    }
-
-                    // at this point oldItems is left with the deleted items.
-                    // just update their status to removed.
-                    for (Resource deletedItem : oldItems.values()) {
-                        deletedItem.setRemoved();
-                    }
-
-                    // Now we need to add the new items to the resource file and the main map
-                    resourceFile.addItems(newItems.values());
-                    for (Map.Entry<String, Resource> entry : newItems.entrySet()) {
-                        mItems.put(entry.getKey(), entry.getValue());
-                    }
-                }
-
-                return true;
-            case NEW:
-                folderData = getFolderData(changedFile.getParentFile());
-
-                if (folderData.type != null) {
-                    Resource item = handleSingleResFile(sourceFolder, folderData.qualifiers,
-                            folderData.type, changedFile);
-                    item.setTouched();
-                } else {
-                    Collection<Resource> items = handleMultiResFile(sourceFolder,
-                            folderData.qualifiers, changedFile);
-                    for (Resource item : items) {
-                        item.setTouched();
-                    }
-                }
-
-                return true;
-            case REMOVED:
-                resourceFile = mResourceFileMap.get(changedFile);
-
-                // flag all resource items are removed
-                for (Resource item : resourceFile.getItems()) {
-                    item.setRemoved();
-                }
-                return true;
-        }
-
-        return false;
-    }
-
-    @Override
-    public String toString() {
-        return Arrays.toString(mSourceFiles.toArray());
+    private static class FolderData {
+        String qualifiers = null;
+        ResourceType type = null;
+        ResourceFolderType folderType = null;
     }
 
     /**
-     * Checks a file to make sure it is a valid file in the android res folder.
-     * @param file the file to check
-     * @return true if it is a valid file, false if it should be ignored.
+     * Returns a FolderData for the given folder
+     * @param folder the folder.
+     * @return the FolderData object.
      */
-    private boolean checkFileForAndroidRes(File file) {
-        // TODO: use the aapt ignore pattern value.
+    @NonNull
+    private static FolderData getFolderData(File folder) {
+        FolderData fd = new FolderData();
 
-        String name = file.getName();
-        int pos = name.lastIndexOf('.');
-        String extension = "";
+        String folderName = folder.getName();
+        int pos = folderName.indexOf(ResourceConstants.RES_QUALIFIER_SEP);
+        ResourceFolderType folderType;
         if (pos != -1) {
-            extension = name.substring(pos + 1);
+            fd.folderType = ResourceFolderType.getTypeByName(folderName.substring(0, pos));
+            fd.qualifiers = folderName.substring(pos + 1);
+        } else {
+            fd.folderType = ResourceFolderType.getTypeByName(folderName);
         }
 
-        // ignore hidden files and backup files
-        return !(name.charAt(0) == '.' || name.charAt(name.length() - 1) == '~') &&
-                !"scc".equalsIgnoreCase(extension) &&     // VisualSourceSafe
-                !"swp".equalsIgnoreCase(extension) &&     // vi swap file
-                !"thumbs.db".equalsIgnoreCase(name) &&    // image index file
-                !"picasa.ini".equalsIgnoreCase(name);     // image index file
+        if (fd.folderType != ResourceFolderType.VALUES) {
+            fd.type = FolderTypeRelationship.getRelatedResourceTypes(fd.folderType).get(0);
+        }
+
+        return fd;
     }
 }
index 9adda0a..f7d5f53 100644 (file)
@@ -44,7 +44,7 @@ import static com.android.SdkConstants.TAG_ITEM;
 /**
  * Parser for "values" files.
  *
- * This parses the file and returns a list of {@link Resource} object.
+ * This parses the file and returns a list of {@link ResourceItem} object.
  */
 class ValueResourceParser {
 
@@ -59,13 +59,13 @@ class ValueResourceParser {
     }
 
     /**
-     * Parses the file and returns a list of {@link Resource} objects.
+     * Parses the file and returns a list of {@link ResourceItem} objects.
      * @return a list of resources.
      *
      * @throws IOException
      */
     @NonNull
-    List<Resource> parseFile() throws IOException {
+    List<ResourceItem> parseFile() throws IOException {
         Document document = parseDocument(mFile);
 
         // get the root node
@@ -75,7 +75,7 @@ class ValueResourceParser {
         }
         NodeList nodes = rootNode.getChildNodes();
 
-        List<Resource> resources = Lists.newArrayListWithExpectedSize(nodes.getLength());
+        List<ResourceItem> resources = Lists.newArrayListWithExpectedSize(nodes.getLength());
 
         for (int i = 0, n = nodes.getLength(); i < n; i++) {
             Node node = nodes.item(i);
@@ -84,7 +84,7 @@ class ValueResourceParser {
                 continue;
             }
 
-            Resource resource = getResource(node);
+            ResourceItem resource = getResource(node);
             if (resource != null) {
                 resources.add(resource);
             }
@@ -94,23 +94,23 @@ class ValueResourceParser {
     }
 
     /**
-     * Returns a new Resource object for a given node.
+     * Returns a new ResourceItem object for a given node.
      * @param node the node representing the resource.
-     * @return a Resource object or null.
+     * @return a ResourceItem object or null.
      */
-    static Resource getResource(Node node) {
+    static ResourceItem getResource(Node node) {
         ResourceType type = getType(node);
         String name = getName(node);
 
         if (type != null && name != null) {
-            return new Resource(name, type, node);
+            return new ResourceItem(name, type, node);
         }
 
         return null;
     }
 
     /**
-     * Returns the type of the Resource based on a node's attributes.
+     * 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.
      */
index ad45ce3..07e356b 100644 (file)
@@ -34,15 +34,25 @@ public class TestUtils {
      * Note that this folder is relative to the root project which is where gradle
      * sets the current working dir when running the tests.
      *
-     * If you need a full folder path, use {@link #getCanonicalRoot(String)}.
+     * If you need a full folder path, use {@link #getCanonicalRoot(String...)}.
+     *
+     * @param names the names of the subfolders.
      *
-     * @param name the name of the subfolder.
      * @return a File
      */
-    public static File getRoot(String name) {
-        File root = new File("src/test/resources/testData/" + name);
-        TestCase.assertTrue("Test folder '" + name + "' does not exist!",
-                root.isDirectory());
+    public static File getRoot(String... names) {
+        File root = null;
+        for (String name : names) {
+            if (root == null) {
+                root = new File("src/test/resources/testData/" + name);
+            } else {
+                root = new File(root, name);
+            }
+
+            TestCase.assertTrue("Test folder '" + name + "' does not exist!",
+                    root.isDirectory());
+
+        }
 
         return root;
     }
@@ -53,11 +63,12 @@ public class TestUtils {
      * The full path is canonized.
      * This is basically ".../src/test/resources/testData/$name".
      *
-     * @param name the name of the subfolder.
+     * @param names the names of the subfolders.
+     *
      * @return a File
      */
-    public static File getCanonicalRoot(String name) throws IOException {
-        File root = getRoot(name);
+    public static File getCanonicalRoot(String... names) throws IOException {
+        File root = getRoot(names);
         return root.getCanonicalFile();
     }
 }
diff --git a/builder/src/test/java/com/android/builder/resources/AssetMergerTest.java b/builder/src/test/java/com/android/builder/resources/AssetMergerTest.java
new file mode 100755 (executable)
index 0000000..12b7bfc
--- /dev/null
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2013 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.builder.TestUtils;
+import com.google.common.collect.ListMultimap;
+import com.google.common.io.Files;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.regex.Pattern;
+
+public class AssetMergerTest extends BaseTestCase {
+
+    private static AssetMerger sAssetMerger = null;
+
+    public void testMergeByCount() throws Exception {
+        AssetMerger merger = getAssetMerger();
+
+        assertEquals(5, merger.size());
+    }
+
+    public void testMergedResourcesByName() throws Exception {
+        AssetMerger merger = getAssetMerger();
+
+        verifyResourceExists(merger,
+                "icon.png",
+                "icon2.png",
+                "main.xml",
+                "values.xml",
+                "foo.dat"
+        );
+    }
+
+    public void testMergeWrite() throws Exception {
+        AssetMerger merger = getAssetMerger();
+
+        File folder = getWrittenResources();
+
+        AssetSet writtenSet = new AssetSet("unused");
+        writtenSet.addSource(folder);
+        writtenSet.loadFromFiles();
+
+        // compare the two maps, but not using the full map as the set loaded from the output
+        // won't contains all versions of each AssetItem item.
+        compareResourceMaps(merger, writtenSet, false /*full compare*/);
+    }
+
+    public void testMergeBlob() throws Exception {
+        AssetMerger merger = getAssetMerger();
+
+        File folder = Files.createTempDir();
+        merger.writeBlobTo(folder);
+
+        AssetMerger loadedMerger = new AssetMerger();
+        loadedMerger.loadFromBlob(folder);
+
+        compareResourceMaps(merger, loadedMerger, true /*full compare*/);
+    }
+
+    /**
+     * Tests the path replacement in the merger.xml file loaded from testData/
+     * @throws Exception
+     */
+    public void testLoadingTestPathReplacement() throws Exception {
+        File root = TestUtils.getRoot("assets", "baseMerge");
+        File fakeRoot = getMergedBlobFolder(root);
+
+        AssetMerger assetMerger = new AssetMerger();
+        assetMerger.loadFromBlob(fakeRoot);
+        checkSourceFolders(assetMerger);
+
+        List<AssetSet> sets = assetMerger.getDataSets();
+        for (AssetSet set : sets) {
+            List<File> sourceFiles = set.getSourceFiles();
+
+            // there should only be one
+            assertEquals(1, sourceFiles.size());
+
+            File sourceFile = sourceFiles.get(0);
+            assertTrue(String.format("File %s is located in %s", sourceFile, root),
+                    sourceFile.getAbsolutePath().startsWith(root.getAbsolutePath()));
+        }
+    }
+
+    public void testUpdate() throws Exception {
+        File root = getIncMergeRoot("basicFiles");
+        File fakeRoot = getMergedBlobFolder(root);
+        AssetMerger assetMerger = new AssetMerger();
+        assetMerger.loadFromBlob(fakeRoot);
+        checkSourceFolders(assetMerger);
+
+        List<AssetSet> sets = assetMerger.getDataSets();
+        assertEquals(2, sets.size());
+
+        // ----------------
+        // first set is the main one, no change here
+        AssetSet mainSet = sets.get(0);
+        File mainFolder = new File(root, "main");
+
+        // touched/removed files:
+        File mainTouched = new File(mainFolder, "touched.png");
+        mainSet.updateWith(mainFolder, mainTouched, FileStatus.CHANGED);
+
+        File mainRemoved = new File(mainFolder, "removed.png");
+        mainSet.updateWith(mainFolder, mainRemoved, FileStatus.REMOVED);
+
+        File mainAdded = new File(mainFolder, "added.png");
+        mainSet.updateWith(mainFolder, mainAdded, FileStatus.NEW);
+
+        // ----------------
+        // second set is the overlay one
+        AssetSet overlaySet = sets.get(1);
+        File overlayFolder = new File(root, "overlay");
+
+        // new/removed files:
+        File overlayAdded = new File(overlayFolder, "overlay_added.png");
+        overlaySet.updateWith(overlayFolder, overlayAdded, FileStatus.NEW);
+
+        File overlayRemoved = new File(overlayFolder, "overlay_removed.png");
+        overlaySet.updateWith(overlayFolder, overlayRemoved, FileStatus.REMOVED);
+
+        // validate for duplicates
+        assetMerger.validateDataSets();
+
+        // check the content.
+        ListMultimap<String, AssetItem> mergedMap = assetMerger.getDataMap();
+
+        // check untouched.png file is WRITTEN
+        List<AssetItem> untouchedItem = mergedMap.get("untouched.png");
+        assertEquals(1, untouchedItem.size());
+        assertTrue(untouchedItem.get(0).isWritten());
+        assertFalse(untouchedItem.get(0).isTouched());
+        assertFalse(untouchedItem.get(0).isRemoved());
+
+        // check touched.png file is TOUCHED
+        List<AssetItem> touchedItem = mergedMap.get("touched.png");
+        assertEquals(1, touchedItem.size());
+        assertTrue(touchedItem.get(0).isWritten());
+        assertTrue(touchedItem.get(0).isTouched());
+        assertFalse(touchedItem.get(0).isRemoved());
+
+        // check removed file is REMOVED
+        List<AssetItem> removedItem = mergedMap.get("removed.png");
+        assertEquals(1, removedItem.size());
+        assertTrue(removedItem.get(0).isWritten());
+        assertTrue(removedItem.get(0).isRemoved());
+
+        // check new overlay: two objects, last one is TOUCHED
+        List<AssetItem> overlayAddedItem = mergedMap.get("overlay_added.png");
+        assertEquals(2, overlayAddedItem.size());
+        AssetItem newOverlay0 = overlayAddedItem.get(0);
+        assertTrue(newOverlay0.isWritten());
+        assertFalse(newOverlay0.isTouched());
+        AssetItem newOverlay1 = overlayAddedItem.get(1);
+        assertEquals(overlayAdded, newOverlay1.getSource().getFile());
+        assertFalse(newOverlay1.isWritten());
+        assertTrue(newOverlay1.isTouched());
+
+        // check removed overlay: two objects, last one is removed
+        List<AssetItem> overlayRemovedItem = mergedMap.get("overlay_removed.png");
+        assertEquals(2, overlayRemovedItem.size());
+        AssetItem overlayRemovedItem0 = overlayRemovedItem.get(0);
+        assertFalse(overlayRemovedItem0.isWritten());
+        assertFalse(overlayRemovedItem0.isTouched());
+        AssetItem overlayRemovedItem1 = overlayRemovedItem.get(1);
+        assertEquals(overlayRemoved, overlayRemovedItem1.getSource().getFile());
+        assertTrue(overlayRemovedItem1.isWritten());
+        assertTrue(overlayRemovedItem1.isRemoved());
+
+        // write and check the result of writeResourceFolder
+        // copy the current resOut which serves as pre incremental update state.
+        File resFolder = getFolderCopy(new File(root, "assetOut"));
+
+        // write the content of the resource merger.
+        assetMerger.writeDataFolder(resFolder);
+
+        // Check the content by checking the colors. All files should be green
+        checkImageColor(new File(resFolder, "untouched.png"), (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "touched.png"), (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "added.png"), (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "overlay_removed.png"), (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "overlay_added.png"), (int) 0xFF00FF00);
+
+        // also check the removed file is not there.
+        assertFalse(new File(resFolder, "removed.png").isFile());
+    }
+
+    public void testCheckValidUpdate() throws Exception {
+        // first merger
+        AssetMerger merger1 = createMerger(new String[][] {
+                new String[] { "main",    ("/main/res1"), ("/main/res2") },
+                new String[] { "overlay", ("/overlay/res1"), ("/overlay/res2") },
+        });
+
+        // 2nd merger with different order source files in sets.
+        AssetMerger merger2 = createMerger(new String[][] {
+                new String[] { "main",    ("/main/res2"), ("/main/res1") },
+                new String[] { "overlay", ("/overlay/res1"), ("/overlay/res2") },
+        });
+
+        assertTrue(merger1.checkValidUpdate(merger2.getDataSets()));
+
+        // write merger1 on disk to test writing empty AssetSets.
+        File folder = Files.createTempDir();
+        merger1.writeBlobTo(folder);
+
+        // reload it
+        AssetMerger loadedMerger = new AssetMerger();
+        loadedMerger.loadFromBlob(folder);
+
+        String expected = merger1.toString();
+        String actual = loadedMerger.toString();
+        if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
+            expected = expected.replaceAll(Pattern.quote(File.separator), "/").
+                                replaceAll("[A-Z]:/", "/");
+            actual = actual.replaceAll(Pattern.quote(File.separator), "/").
+                            replaceAll("[A-Z]:/", "/");
+            assertEquals("Actual: " + actual + "\nExpected: " + expected, expected, actual);
+        } else {
+            assertTrue("Actual: " + actual + "\nExpected: " + expected,
+                       loadedMerger.checkValidUpdate(merger1.getDataSets()));
+        }
+    }
+
+
+    public void testUpdateWithRemovedOverlay() throws Exception {
+        // Test with removed overlay
+        AssetMerger merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+                new String[] { "overlay", "/overlay/res1", "/overlay/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        AssetMerger merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res2", "/main/res1" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
+    }
+
+    public void testUpdateWithReplacedOverlays() throws Exception {
+        // Test with different overlays
+        AssetMerger merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+                new String[] { "overlay", "/overlay/res1", "/overlay/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        AssetMerger merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res2", "/main/res1" },
+                new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
+    }
+
+    public void testUpdateWithReorderedOverlays() throws Exception {
+        // Test with different overlays
+        AssetMerger merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+                new String[] { "overlay1", "/overlay1/res1", "/overlay1/res2" },
+                new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        AssetMerger merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res2", "/main/res1" },
+                new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
+                new String[] { "overlay1", "/overlay1/res1", "/overlay1/res2" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
+    }
+
+    public void testUpdateWithRemovedSourceFile() throws Exception {
+        // Test with different source files
+        AssetMerger merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        AssetMerger merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
+    }
+
+    /**
+     * Creates a fake merge with given sets.
+     *
+     * the data is an array of sets.
+     *
+     * Each set is [ setName, folder1, folder2, ...]
+     *
+     * @param data
+     * @return
+     */
+    private static AssetMerger createMerger(String[][] data) {
+        AssetMerger merger = new AssetMerger();
+        for (String[] setData : data) {
+            AssetSet set = new AssetSet(setData[0]);
+            merger.addDataSet(set);
+            for (int i = 1, n = setData.length; i < n; i++) {
+                set.addSource(new File(setData[i]));
+            }
+        }
+
+        return merger;
+    }
+
+    private static AssetMerger getAssetMerger()
+            throws DuplicateDataException, IOException {
+        if (sAssetMerger == null) {
+            File root = TestUtils.getRoot("assets", "baseMerge");
+
+            AssetSet res = AssetSetTest.getBaseAssetSet();
+
+            AssetSet overlay = new AssetSet("overlay");
+            overlay.addSource(new File(root, "overlay"));
+            overlay.loadFromFiles();
+
+            sAssetMerger = new AssetMerger();
+            sAssetMerger.addDataSet(res);
+            sAssetMerger.addDataSet(overlay);
+        }
+
+        return sAssetMerger;
+    }
+
+    private static File getWrittenResources() throws DuplicateDataException, IOException,
+            ExecutionException, InterruptedException {
+        AssetMerger assetMerger = getAssetMerger();
+
+        File folder = Files.createTempDir();
+
+        assetMerger.writeDataFolder(folder);
+
+        return folder;
+    }
+
+    private File getIncMergeRoot(String name) throws IOException {
+        File root = TestUtils.getCanonicalRoot("assets", "incMergeData");
+        return new File(root, name);
+    }
+
+    private static File getFolderCopy(File folder) throws IOException {
+        File dest = Files.createTempDir();
+        copyFolder(folder, dest);
+        return dest;
+    }
+
+    private static void copyFolder(File from, File to) throws IOException {
+        if (from.isFile()) {
+            Files.copy(from, to);
+        } else if (from.isDirectory()) {
+            if (!to.exists()) {
+                to.mkdirs();
+            }
+
+            File[] children = from.listFiles();
+            if (children != null) {
+                for (File f : children) {
+                    copyFolder(f, new File(to, f.getName()));
+                }
+            }
+        }
+    }
+}
diff --git a/builder/src/test/java/com/android/builder/resources/AssetSetTest.java b/builder/src/test/java/com/android/builder/resources/AssetSetTest.java
new file mode 100644 (file)
index 0000000..a0b0033
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 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.builder.TestUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class AssetSetTest extends BaseTestCase {
+
+    private static AssetSet sBaseResourceSet = null;
+
+    public void testBaseAssetSetByCount() throws Exception {
+        AssetSet assetSet = getBaseAssetSet();
+        assertEquals(4, assetSet.size());
+    }
+
+    public void testBaseAssetSetByName() throws Exception {
+        AssetSet assetSet = getBaseAssetSet();
+
+        verifyResourceExists(assetSet,
+                "foo.dat",
+                "icon.png",
+                "main.xml",
+                "values.xml"
+        );
+    }
+
+    public void testDupAssetSet() throws Exception {
+        File root = TestUtils.getRoot("assets", "dupSet");
+
+        AssetSet set = new AssetSet("main");
+        set.addSource(new File(root, "assets1"));
+        set.addSource(new File(root, "assets2"));
+        boolean gotException = false;
+        try {
+            set.loadFromFiles();
+        } catch (DuplicateDataException e) {
+            gotException = true;
+        }
+
+        assertTrue(gotException);
+    }
+
+    static AssetSet getBaseAssetSet() throws DuplicateDataException, IOException {
+        if (sBaseResourceSet == null) {
+            File root = TestUtils.getRoot("assets", "baseSet");
+
+            sBaseResourceSet = new AssetSet("main");
+            sBaseResourceSet.addSource(root);
+            sBaseResourceSet.loadFromFiles();
+        }
+
+        return sBaseResourceSet;
+    }
+}
index a18d69c..c82c587 100644 (file)
 
 package com.android.builder.resources;
 
+import com.google.common.base.Charsets;
 import com.google.common.collect.ListMultimap;
+import com.google.common.io.Files;
 import junit.framework.TestCase;
 
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
 import java.util.List;
+import java.util.regex.Matcher;
 
 public abstract class BaseTestCase extends TestCase {
 
-    protected void verifyResourceExists(ResourceMap resourceMap, String... resourceKeys) {
-        ListMultimap<String, Resource> map = resourceMap.getResourceMap();
+    protected void verifyResourceExists(DataMap<? extends DataItem> dataMap,
+                                        String... dataItemKeys) {
+        ListMultimap<String, ? extends DataItem> map = dataMap.getDataMap();
 
-        for (String resKey : resourceKeys) {
-            List<Resource> resources = map.get(resKey);
-            assertTrue("resource '" + resKey + "' is missing!", resources.size() > 0);
+        for (String resKey : dataItemKeys) {
+            List<? extends DataItem> items = map.get(resKey);
+            assertTrue("resource '" + resKey + "' is missing!", items.size() > 0);
         }
     }
 
@@ -39,20 +47,21 @@ public abstract class BaseTestCase extends TestCase {
      * the same number of items, otherwise it'll only checks that each resource key is present
      * in both maps.
      *
-     * @param resourceMap1 the first resource Map
-     * @param resourceMap2 the second resource Map
+     * @param dataMap1 the first resource Map
+     * @param dataMap2 the second resource Map
      * @param fullCompare whether a full compare is requested.
      */
-    protected void compareResourceMaps(ResourceMap resourceMap1, ResourceMap resourceMap2,
+    protected void compareResourceMaps(DataMap<? extends DataItem> dataMap1,
+                                       DataMap<? extends DataItem> dataMap2,
                                        boolean fullCompare) {
-        assertEquals(resourceMap1.size(), resourceMap2.size());
+        assertEquals(dataMap1.size(), dataMap2.size());
 
         // compare the resources are all the same
-        ListMultimap<String, Resource> map1 = resourceMap1.getResourceMap();
-        ListMultimap<String, Resource> map2 = resourceMap2.getResourceMap();
+        ListMultimap<String, ? extends DataItem> map1 = dataMap1.getDataMap();
+        ListMultimap<String, ? extends DataItem> map2 = dataMap2.getDataMap();
         for (String key : map1.keySet()) {
-            List<Resource> items1 = map1.get(key);
-            List<Resource> items2 = map2.get(key);
+            List<? extends DataItem> items1 = map1.get(key);
+            List<? extends DataItem> items2 = map2.get(key);
             if (fullCompare) {
                 assertEquals("Wrong size for " + key, items1.size(), items2.size());
             } else {
@@ -62,4 +71,75 @@ public abstract class BaseTestCase extends TestCase {
             }
         }
     }
+
+    protected static void checkImageColor(File file, int expectedColor) throws IOException {
+        assertTrue("File '" + file.getAbsolutePath() + "' does not exist.", file.isFile());
+
+        BufferedImage image = ImageIO.read(file);
+        int rgb = image.getRGB(0, 0);
+        assertEquals(String.format("Expected: 0x%08X, actual: 0x%08X for file %s",
+                expectedColor, rgb, file),
+                expectedColor, rgb);
+    }
+
+    /**
+     * Returns a folder containing a merger blob data for the given test data folder.
+     *
+     * This is to work around the fact that the merger blob data contains full path, but we don't
+     * know where this project is located on the drive. This rewrites the blob to contain the
+     * actual folder.
+     * (The blobs written in the test data contains placeholders for the path root and path
+     * separators)
+     *
+     * @param folder
+     * @return
+     * @throws java.io.IOException
+     */
+    protected static File getMergedBlobFolder(File folder) throws IOException {
+        File originalMerger = new File(folder, AssetMerger.FN_MERGER_XML);
+
+        String content = Files.toString(originalMerger, Charsets.UTF_8);
+
+        // search and replace $TOP$ with the root and $SEP$ with the platform separator.
+        content = content.replaceAll(
+                "\\$TOP\\$", Matcher.quoteReplacement(folder.getAbsolutePath())).
+                replaceAll("\\$SEP\\$", Matcher.quoteReplacement(File.separator));
+
+        File tempFolder = Files.createTempDir();
+        Files.write(content, new File(tempFolder, AssetMerger.FN_MERGER_XML), Charsets.UTF_8);
+
+        return tempFolder;
+    }
+
+    /**
+     * Post {@link #getMergedBlobFolder(java.io.File)} check. After the DataMerger is created
+     * from the file generated, this checks that the file replacement works and all the files are
+     * where they are supposed to be.
+     *
+     * @param dataMerger
+     */
+    protected void checkSourceFolders(
+            DataMerger<? extends DataItem, ? extends DataFile, ? extends DataSet> dataMerger) {
+
+        // Loop on all the data sets.
+        for (DataSet set : dataMerger.getDataSets()) {
+            // get the source files and verify they exists.
+            List<File> files = set.getSourceFiles();
+            for (File file : files) {
+                assertTrue(file.isDirectory());
+            }
+
+            // for each source file, also check that the files inside are in fact inside
+            // them. We don't check if those files are there though because the tests could
+            // be testing with missing files to simulate updates.
+            ListMultimap<String, ? extends DataItem> itemMap = set.getDataMap();
+
+            for (DataItem item : itemMap.values()) {
+                DataFile dataFile = item.getSource();
+                File file = dataFile.getFile();
+
+                assertNotNull(set.findMatchingSourceFile(file));
+            }
+        }
+    }
 }
index f056413..844506b 100755 (executable)
@@ -28,8 +28,6 @@ import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
 import java.util.Collections;
@@ -86,13 +84,13 @@ public class ResourceMergerTest extends BaseTestCase {
 
     public void testReplacedLayout() throws Exception {
         ResourceMerger merger = getResourceMerger();
-        ListMultimap<String, Resource> mergedMap = merger.getResourceMap();
+        ListMultimap<String, ResourceItem> mergedMap = merger.getDataMap();
 
-        List<Resource> values = mergedMap.get("layout/main");
+        List<ResourceItem> values = mergedMap.get("layout/main");
 
         // the overlay means there's 2 versions of this resource.
         assertEquals(2, values.size());
-        Resource mainLayout = values.get(1);
+        ResourceItem mainLayout = values.get(1);
 
         ResourceFile sourceFile = mainLayout.getSource();
         assertTrue(sourceFile.getFile().getAbsolutePath()
@@ -101,14 +99,14 @@ public class ResourceMergerTest extends BaseTestCase {
 
     public void testReplacedAlias() throws Exception {
         ResourceMerger merger = getResourceMerger();
-        ListMultimap<String, Resource> mergedMap = merger.getResourceMap();
+        ListMultimap<String, ResourceItem> mergedMap = merger.getDataMap();
 
 
-        List<Resource> values = mergedMap.get("layout/alias_replaced_by_file");
+        List<ResourceItem> values = mergedMap.get("layout/alias_replaced_by_file");
 
         // the overlay means there's 2 versions of this resource.
         assertEquals(2, values.size());
-        Resource layout = values.get(1);
+        ResourceItem layout = values.get(1);
 
         // since it's replaced by a file, there's no node.
         assertNull(layout.getValue());
@@ -116,15 +114,15 @@ public class ResourceMergerTest extends BaseTestCase {
 
     public void testReplacedFile() throws Exception {
         ResourceMerger merger = getResourceMerger();
-        ListMultimap<String, Resource> mergedMap = merger.getResourceMap();
+        ListMultimap<String, ResourceItem> mergedMap = merger.getDataMap();
 
-        List<Resource> values = mergedMap.get("layout/file_replaced_by_alias");
+        List<ResourceItem> values = mergedMap.get("layout/file_replaced_by_alias");
 
         // the overlay means there's 2 versions of this resource.
         assertEquals(2, values.size());
-        Resource layout = values.get(1);
+        ResourceItem layout = values.get(1);
 
-        // since it's replaced by a file, there's no node.
+        // since it's replaced by an alias, there's a node
         assertNotNull(layout.getValue());
     }
 
@@ -138,7 +136,7 @@ public class ResourceMergerTest extends BaseTestCase {
         writtenSet.loadFromFiles();
 
         // compare the two maps, but not using the full map as the set loaded from the output
-        // won't contains all versions of each Resource item.
+        // won't contains all versions of each ResourceItem item.
         compareResourceMaps(merger, writtenSet, false /*full compare*/);
     }
 
@@ -159,13 +157,14 @@ public class ResourceMergerTest extends BaseTestCase {
      * @throws Exception
      */
     public void testLoadingTestPathReplacement() throws Exception {
-        File root = TestUtils.getRoot("baseMerge");
+        File root = TestUtils.getRoot("resources", "baseMerge");
         File fakeRoot = getMergedBlobFolder(root);
 
         ResourceMerger resourceMerger = new ResourceMerger();
         resourceMerger.loadFromBlob(fakeRoot);
+        checkSourceFolders(resourceMerger);
 
-        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        List<ResourceSet> sets = resourceMerger.getDataSets();
         for (ResourceSet set : sets) {
             List<File> sourceFiles = set.getSourceFiles();
 
@@ -183,8 +182,9 @@ public class ResourceMergerTest extends BaseTestCase {
         File fakeRoot = getMergedBlobFolder(root);
         ResourceMerger resourceMerger = new ResourceMerger();
         resourceMerger.loadFromBlob(fakeRoot);
+        checkSourceFolders(resourceMerger);
 
-        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        List<ResourceSet> sets = resourceMerger.getDataSets();
         assertEquals(2, sets.size());
 
         // ----------------
@@ -222,43 +222,43 @@ public class ResourceMergerTest extends BaseTestCase {
         overlaySet.updateWith(overlayBase, overlayDrawableHdpiNewAlternate, FileStatus.NEW);
 
         // validate for duplicates
-        resourceMerger.validateResourceSets();
+        resourceMerger.validateDataSets();
 
         // check the content.
-        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+        ListMultimap<String, ResourceItem> mergedMap = resourceMerger.getDataMap();
 
         // check unchanged file is WRITTEN
-        List<Resource> drawableUntouched = mergedMap.get("drawable/untouched");
+        List<ResourceItem> drawableUntouched = mergedMap.get("drawable/untouched");
         assertEquals(1, drawableUntouched.size());
         assertTrue(drawableUntouched.get(0).isWritten());
         assertFalse(drawableUntouched.get(0).isTouched());
         assertFalse(drawableUntouched.get(0).isRemoved());
 
         // check replaced file is TOUCHED
-        List<Resource> drawableTouched = mergedMap.get("drawable/touched");
+        List<ResourceItem> drawableTouched = mergedMap.get("drawable/touched");
         assertEquals(1, drawableTouched.size());
         assertTrue(drawableTouched.get(0).isWritten());
         assertTrue(drawableTouched.get(0).isTouched());
         assertFalse(drawableTouched.get(0).isRemoved());
 
         // check removed file is REMOVED
-        List<Resource> drawableRemoved = mergedMap.get("drawable/removed");
+        List<ResourceItem> drawableRemoved = mergedMap.get("drawable/removed");
         assertEquals(1, drawableRemoved.size());
         assertTrue(drawableRemoved.get(0).isWritten());
         assertTrue(drawableRemoved.get(0).isRemoved());
 
         // check new overlay: two objects, last one is TOUCHED
-        List<Resource> drawableNewOverlay = mergedMap.get("drawable/new_overlay");
+        List<ResourceItem> drawableNewOverlay = mergedMap.get("drawable/new_overlay");
         assertEquals(2, drawableNewOverlay.size());
-        Resource newOverlay = drawableNewOverlay.get(1);
+        ResourceItem newOverlay = drawableNewOverlay.get(1);
         assertEquals(overlayDrawableNewOverlay, newOverlay.getSource().getFile());
         assertFalse(newOverlay.isWritten());
         assertTrue(newOverlay.isTouched());
 
         // check new alternate: one objects, last one is TOUCHED
-        List<Resource> drawableHdpiNewAlternate = mergedMap.get("drawable-hdpi/new_alternate");
+        List<ResourceItem> drawableHdpiNewAlternate = mergedMap.get("drawable-hdpi/new_alternate");
         assertEquals(1, drawableHdpiNewAlternate.size());
-        Resource newAlternate = drawableHdpiNewAlternate.get(0);
+        ResourceItem newAlternate = drawableHdpiNewAlternate.get(0);
         assertEquals(overlayDrawableHdpiNewAlternate, newAlternate.getSource().getFile());
         assertFalse(newAlternate.isWritten());
         assertTrue(newAlternate.isTouched());
@@ -268,7 +268,7 @@ public class ResourceMergerTest extends BaseTestCase {
         File resFolder = getFolderCopy(new File(root, "resOut"));
 
         // write the content of the resource merger.
-        resourceMerger.writeResourceFolder(resFolder, null /*aaptRunner*/);
+        resourceMerger.writeDataFolder(resFolder, null /*aaptRunner*/);
 
         // Check the content.
         checkImageColor(new File(resFolder, "drawable" + File.separator + "touched.png"),
@@ -289,8 +289,9 @@ public class ResourceMergerTest extends BaseTestCase {
         File fakeRoot = getMergedBlobFolder(root);
         ResourceMerger resourceMerger = new ResourceMerger();
         resourceMerger.loadFromBlob(fakeRoot);
+        checkSourceFolders(resourceMerger);
 
-        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        List<ResourceSet> sets = resourceMerger.getDataSets();
         assertEquals(2, sets.size());
 
         // ----------------
@@ -322,27 +323,27 @@ public class ResourceMergerTest extends BaseTestCase {
         overlaySet.updateWith(overlayBase, overlayValuesFrNew, FileStatus.NEW);
 
         // validate for duplicates
-        resourceMerger.validateResourceSets();
+        resourceMerger.validateDataSets();
 
         // check the content.
-        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+        ListMultimap<String, ResourceItem> mergedMap = resourceMerger.getDataMap();
 
         // check unchanged string is WRITTEN
-        List<Resource> valuesUntouched = mergedMap.get("string/untouched");
+        List<ResourceItem> valuesUntouched = mergedMap.get("string/untouched");
         assertEquals(1, valuesUntouched.size());
         assertTrue(valuesUntouched.get(0).isWritten());
         assertFalse(valuesUntouched.get(0).isTouched());
         assertFalse(valuesUntouched.get(0).isRemoved());
 
         // check replaced file is TOUCHED
-        List<Resource> valuesTouched = mergedMap.get("string/touched");
+        List<ResourceItem> valuesTouched = mergedMap.get("string/touched");
         assertEquals(1, valuesTouched.size());
         assertTrue(valuesTouched.get(0).isWritten());
         assertTrue(valuesTouched.get(0).isTouched());
         assertFalse(valuesTouched.get(0).isRemoved());
 
         // check removed file is REMOVED
-        List<Resource> valuesRemoved = mergedMap.get("string/removed");
+        List<ResourceItem> valuesRemoved = mergedMap.get("string/removed");
         assertEquals(1, valuesRemoved.size());
         assertTrue(valuesRemoved.get(0).isWritten());
         assertTrue(valuesRemoved.get(0).isRemoved());
@@ -353,16 +354,16 @@ public class ResourceMergerTest extends BaseTestCase {
         assertTrue(valuesRemoved.get(0).isRemoved());
 
         // check new overlay: two objects, last one is TOUCHED
-        List<Resource> valuesNewOverlay = mergedMap.get("string/new_overlay");
+        List<ResourceItem> valuesNewOverlay = mergedMap.get("string/new_overlay");
         assertEquals(2, valuesNewOverlay.size());
-        Resource newOverlay = valuesNewOverlay.get(1);
+        ResourceItem newOverlay = valuesNewOverlay.get(1);
         assertFalse(newOverlay.isWritten());
         assertTrue(newOverlay.isTouched());
 
         // check new alternate: one objects, last one is TOUCHED
-        List<Resource> valuesFrNewAlternate = mergedMap.get("string-fr/new_alternate");
+        List<ResourceItem> valuesFrNewAlternate = mergedMap.get("string-fr/new_alternate");
         assertEquals(1, valuesFrNewAlternate.size());
-        Resource newAlternate = valuesFrNewAlternate.get(0);
+        ResourceItem newAlternate = valuesFrNewAlternate.get(0);
         assertFalse(newAlternate.isWritten());
         assertTrue(newAlternate.isTouched());
 
@@ -371,7 +372,7 @@ public class ResourceMergerTest extends BaseTestCase {
         File resFolder = getFolderCopy(new File(root, "resOut"));
 
         // write the content of the resource merger.
-        resourceMerger.writeResourceFolder(resFolder, null /*aaptRunner*/);
+        resourceMerger.writeDataFolder(resFolder, null /*aaptRunner*/);
 
         // Check the content.
         // values/values.xml
@@ -395,8 +396,9 @@ public class ResourceMergerTest extends BaseTestCase {
         File fakeRoot = getMergedBlobFolder(root);
         ResourceMerger resourceMerger = new ResourceMerger();
         resourceMerger.loadFromBlob(fakeRoot);
+        checkSourceFolders(resourceMerger);
 
-        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        List<ResourceSet> sets = resourceMerger.getDataSets();
         assertEquals(2, sets.size());
 
         // ----------------
@@ -416,20 +418,20 @@ public class ResourceMergerTest extends BaseTestCase {
         overlaySet.updateWith(overlayBase, overlayValuesNew, FileStatus.REMOVED);
 
         // validate for duplicates
-        resourceMerger.validateResourceSets();
+        resourceMerger.validateDataSets();
 
         // check the content.
-        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+        ListMultimap<String, ResourceItem> mergedMap = resourceMerger.getDataMap();
 
         // check unchanged string is WRITTEN
-        List<Resource> valuesUntouched = mergedMap.get("string/untouched");
+        List<ResourceItem> valuesUntouched = mergedMap.get("string/untouched");
         assertEquals(1, valuesUntouched.size());
         assertTrue(valuesUntouched.get(0).isWritten());
         assertFalse(valuesUntouched.get(0).isTouched());
         assertFalse(valuesUntouched.get(0).isRemoved());
 
         // check removed_overlay is present twice.
-        List<Resource> valuesRemovedOverlay = mergedMap.get("string/removed_overlay");
+        List<ResourceItem> valuesRemovedOverlay = mergedMap.get("string/removed_overlay");
         assertEquals(2, valuesRemovedOverlay.size());
         // first is untouched
         assertFalse(valuesRemovedOverlay.get(0).isWritten());
@@ -445,7 +447,7 @@ public class ResourceMergerTest extends BaseTestCase {
         File resFolder = getFolderCopy(new File(root, "resOut"));
 
         // write the content of the resource merger.
-        resourceMerger.writeResourceFolder(resFolder, null /*aaptRunner*/);
+        resourceMerger.writeDataFolder(resFolder, null /*aaptRunner*/);
 
         // Check the content.
         // values/values.xml
@@ -460,8 +462,9 @@ public class ResourceMergerTest extends BaseTestCase {
         File fakeRoot = getMergedBlobFolder(root);
         ResourceMerger resourceMerger = new ResourceMerger();
         resourceMerger.loadFromBlob(fakeRoot);
+        checkSourceFolders(resourceMerger);
 
-        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        List<ResourceSet> sets = resourceMerger.getDataSets();
         assertEquals(1, sets.size());
 
         // ----------------
@@ -484,23 +487,23 @@ public class ResourceMergerTest extends BaseTestCase {
         mainSet.updateWith(mainBase, mainLayoutRemoved, FileStatus.REMOVED);
 
         // validate for duplicates
-        resourceMerger.validateResourceSets();
+        resourceMerger.validateDataSets();
 
         // check the content.
-        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+        ListMultimap<String, ResourceItem> mergedMap = resourceMerger.getDataMap();
 
         // check layout/main is unchanged
-        List<Resource> layoutMain = mergedMap.get("layout/main");
+        List<ResourceItem> layoutMain = mergedMap.get("layout/main");
         assertEquals(1, layoutMain.size());
         assertTrue(layoutMain.get(0).isWritten());
         assertFalse(layoutMain.get(0).isTouched());
         assertFalse(layoutMain.get(0).isRemoved());
 
         // check file_replaced_by_alias has 2 version, 2nd is TOUCHED, and contains a Node
-        List<Resource> layoutReplacedByAlias = mergedMap.get("layout/file_replaced_by_alias");
+        List<ResourceItem> layoutReplacedByAlias = mergedMap.get("layout/file_replaced_by_alias");
         assertEquals(2, layoutReplacedByAlias.size());
         // 1st one is removed version, as it already existed in the item multimap
-        Resource replacedByAlias = layoutReplacedByAlias.get(0);
+        ResourceItem replacedByAlias = layoutReplacedByAlias.get(0);
         assertTrue(replacedByAlias.isWritten());
         assertFalse(replacedByAlias.isTouched());
         assertTrue(replacedByAlias.isRemoved());
@@ -515,10 +518,10 @@ public class ResourceMergerTest extends BaseTestCase {
         assertEquals("values.xml", replacedByAlias.getSource().getFile().getName());
 
         // check alias_replaced_by_file has 2 version, 2nd is TOUCHED, and contains a Node
-        List<Resource> layoutReplacedByFile = mergedMap.get("layout/alias_replaced_by_file");
+        List<ResourceItem> layoutReplacedByFile = mergedMap.get("layout/alias_replaced_by_file");
         // 1st one is removed version, as it already existed in the item multimap
         assertEquals(2, layoutReplacedByFile.size());
-        Resource replacedByFile = layoutReplacedByFile.get(0);
+        ResourceItem replacedByFile = layoutReplacedByFile.get(0);
         assertTrue(replacedByFile.isWritten());
         assertFalse(replacedByFile.isTouched());
         assertTrue(replacedByFile.isRemoved());
@@ -537,7 +540,7 @@ public class ResourceMergerTest extends BaseTestCase {
         File resFolder = getFolderCopy(new File(root, "resOut"));
 
         // write the content of the resource merger.
-        resourceMerger.writeResourceFolder(resFolder, null /*aaptRunner*/);
+        resourceMerger.writeDataFolder(resFolder, null /*aaptRunner*/);
 
         // deleted layout/file_replaced_by_alias.xml
         assertFalse(new File(resFolder, "layout" + File.separator + "file_replaced_by_alias.xml")
@@ -567,7 +570,7 @@ public class ResourceMergerTest extends BaseTestCase {
                 new String[] { "overlay", ("/overlay/res1"), ("/overlay/res2") },
         });
 
-        assertTrue(merger1.checkValidUpdate(merger2.getResourceSets()));
+        assertTrue(merger1.checkValidUpdate(merger2.getDataSets()));
 
         // write merger1 on disk to test writing empty ResourceSets.
         File folder = Files.createTempDir();
@@ -587,11 +590,11 @@ public class ResourceMergerTest extends BaseTestCase {
             assertEquals("Actual: " + actual + "\nExpected: " + expected, expected, actual);
         } else {
             assertTrue("Actual: " + actual + "\nExpected: " + expected,
-                       loadedMerger.checkValidUpdate(merger1.getResourceSets()));
+                       loadedMerger.checkValidUpdate(merger1.getDataSets()));
         }
     }
 
-    public void testCheckValidUpdateFail() throws Exception {
+    public void testUpdateWithRemovedOverlay() throws Exception {
         // Test with removed overlay
         ResourceMerger merger1 = createMerger(new String[][] {
                 new String[] { "main",    "/main/res1", "/main/res2" },
@@ -603,56 +606,72 @@ public class ResourceMergerTest extends BaseTestCase {
                 new String[] { "main",    "/main/res2", "/main/res1" },
         });
 
-        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
+    }
 
+    public void testUpdateWithReplacedOverlays() throws Exception {
         // Test with different overlays
-        merger1 = createMerger(new String[][] {
+        ResourceMerger merger1 = createMerger(new String[][] {
                 new String[] { "main",    "/main/res1", "/main/res2" },
                 new String[] { "overlay", "/overlay/res1", "/overlay/res2" },
         });
 
         // 2nd merger with different order source files in sets.
-        merger2 = createMerger(new String[][] {
+        ResourceMerger merger2 = createMerger(new String[][] {
                 new String[] { "main",    "/main/res2", "/main/res1" },
                 new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
         });
 
-        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
+    }
 
+    public void testUpdateWithReorderedOverlays() throws Exception {
         // Test with different overlays
-        merger1 = createMerger(new String[][] {
+        ResourceMerger merger1 = createMerger(new String[][] {
                 new String[] { "main",    "/main/res1", "/main/res2" },
                 new String[] { "overlay1", "/overlay1/res1", "/overlay1/res2" },
                 new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
         });
 
         // 2nd merger with different order source files in sets.
-        merger2 = createMerger(new String[][] {
+        ResourceMerger merger2 = createMerger(new String[][] {
                 new String[] { "main",    "/main/res2", "/main/res1" },
                 new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
                 new String[] { "overlay1", "/overlay1/res1", "/overlay1/res2" },
         });
 
-        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
+    }
 
+    public void testUpdateWithRemovedSourceFile() throws Exception {
         // Test with different source files
-        merger1 = createMerger(new String[][] {
+        ResourceMerger merger1 = createMerger(new String[][] {
                 new String[] { "main",    "/main/res1", "/main/res2" },
         });
 
         // 2nd merger with different order source files in sets.
-        merger2 = createMerger(new String[][] {
+        ResourceMerger merger2 = createMerger(new String[][] {
                 new String[] { "main",    "/main/res1" },
         });
 
-        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+        assertFalse(merger1.checkValidUpdate(merger2.getDataSets()));
     }
 
+    /**
+     * Creates a fake merge with given sets.
+     *
+     * the data is an array of sets.
+     *
+     * Each set is [ setName, folder1, folder2, ...]
+     *
+     * @param data
+     * @return
+     */
     private static ResourceMerger createMerger(String[][] data) {
         ResourceMerger merger = new ResourceMerger();
         for (String[] setData : data) {
             ResourceSet set = new ResourceSet(setData[0]);
-            merger.addResourceSet(set);
+            merger.addDataSet(set);
             for (int i = 1, n = setData.length; i < n; i++) {
                 set.addSource(new File(setData[i]));
             }
@@ -662,9 +681,9 @@ public class ResourceMergerTest extends BaseTestCase {
     }
 
     private static ResourceMerger getResourceMerger()
-            throws DuplicateResourceException, IOException {
+            throws DuplicateDataException, IOException {
         if (sResourceMerger == null) {
-            File root = TestUtils.getRoot("baseMerge");
+            File root = TestUtils.getRoot("resources", "baseMerge");
 
             ResourceSet res = ResourceSetTest.getBaseResourceSet();
 
@@ -673,55 +692,26 @@ public class ResourceMergerTest extends BaseTestCase {
             overlay.loadFromFiles();
 
             sResourceMerger = new ResourceMerger();
-            sResourceMerger.addResourceSet(res);
-            sResourceMerger.addResourceSet(overlay);
+            sResourceMerger.addDataSet(res);
+            sResourceMerger.addDataSet(overlay);
         }
 
         return sResourceMerger;
     }
 
-    private static File getWrittenResources() throws DuplicateResourceException, IOException,
+    private static File getWrittenResources() throws DuplicateDataException, IOException,
             ExecutionException, InterruptedException {
         ResourceMerger resourceMerger = getResourceMerger();
 
         File folder = Files.createTempDir();
 
-        resourceMerger.writeResourceFolder(folder, null /*aaptRunner*/);
+        resourceMerger.writeDataFolder(folder, null /*aaptRunner*/);
 
         return folder;
     }
 
-    /**
-     * Returns a folder containing a merger blob data for the given test data folder.
-     *
-     * This is to work around the fact that the merger blob data contains full path, but we don't
-     * know where this project is located on the drive. This rewrites the blob to contain the
-     * actual folder.
-     * (The blobs written in the test data contains placeholders for the path root and path
-     * separators)
-     *
-     * @param folder
-     * @return
-     * @throws IOException
-     */
-    private static File getMergedBlobFolder(File folder) throws IOException {
-        File originalMerger = new File(folder, ResourceMerger.FN_MERGER_XML);
-
-        String content = Files.toString(originalMerger, Charsets.UTF_8);
-
-        // search and replace $TOP$ with the root and $SEP$ with the platform separator.
-        content = content.replaceAll(
-                "\\$TOP\\$", Matcher.quoteReplacement(folder.getAbsolutePath())).
-                replaceAll("\\$SEP\\$", Matcher.quoteReplacement(File.separator));
-
-        File tempFolder = Files.createTempDir();
-        Files.write(content, new File(tempFolder, ResourceMerger.FN_MERGER_XML), Charsets.UTF_8);
-
-        return tempFolder;
-    }
-
     private File getIncMergeRoot(String name) throws IOException {
-        File root = TestUtils.getCanonicalRoot("incMergeData");
+        File root = TestUtils.getCanonicalRoot("resources", "incMergeData");
         return new File(root, name);
     }
 
@@ -748,16 +738,6 @@ public class ResourceMergerTest extends BaseTestCase {
         }
     }
 
-    private static void checkImageColor(File file, int expectedColor) throws IOException {
-        assertTrue("File '" + file.getAbsolutePath() + "' does not exist.", file.isFile());
-
-        BufferedImage image = ImageIO.read(file);
-        int rgb = image.getRGB(0, 0);
-        assertEquals(String.format("Expected: 0x%08X, actual: 0x%08X for file %s",
-                expectedColor, rgb, file),
-                expectedColor, rgb);
-    }
-
     private static Map<String, String> quickStringOnlyValueFileParser(File file)
             throws IOException {
         Map<String, String> result = Maps.newHashMap();
index fa39ebf..815115f 100644 (file)
@@ -61,7 +61,7 @@ public class ResourceSetTest extends BaseTestCase {
     }
 
     public void testDupResourceSet() throws Exception {
-        File root = TestUtils.getRoot("dupResourceSet");
+        File root = TestUtils.getRoot("resources", "dupSet");
 
         ResourceSet set = new ResourceSet("main");
         set.addSource(new File(root, "res1"));
@@ -69,16 +69,16 @@ public class ResourceSetTest extends BaseTestCase {
         boolean gotException = false;
         try {
             set.loadFromFiles();
-        } catch (DuplicateResourceException e) {
+        } catch (DuplicateDataException e) {
             gotException = true;
         }
 
         assertTrue(gotException);
     }
 
-    static ResourceSet getBaseResourceSet() throws DuplicateResourceException, IOException {
+    static ResourceSet getBaseResourceSet() throws DuplicateDataException, IOException {
         if (sBaseResourceSet == null) {
-            File root = TestUtils.getRoot("baseResourceSet");
+            File root = TestUtils.getRoot("resources", "baseSet");
 
             sBaseResourceSet = new ResourceSet("main");
             sBaseResourceSet.addSource(root);
index 96dabfd..ed87857 100644 (file)
@@ -28,18 +28,18 @@ import java.util.Map;
  */
 public class ValueResourceParserTest extends BaseTestCase {
 
-    private static List<Resource> sResources = null;
+    private static List<ResourceItem> sResources = null;
 
     public void testParsedResourcesByCount() throws Exception {
-        List<Resource> resources = getParsedResources();
+        List<ResourceItem> resources = getParsedResources();
 
         assertEquals(18, resources.size());
     }
 
     public void testParsedResourcesByName() throws Exception {
-        List<Resource> resources = getParsedResources();
-        Map<String, Resource> resourceMap = Maps.newHashMapWithExpectedSize(resources.size());
-        for (Resource item : resources) {
+        List<ResourceItem> resources = getParsedResources();
+        Map<String, ResourceItem> resourceMap = Maps.newHashMapWithExpectedSize(resources.size());
+        for (ResourceItem item : resources) {
             resourceMap.put(item.getKey(), item);
         }
 
@@ -68,16 +68,16 @@ public class ValueResourceParserTest extends BaseTestCase {
         }
     }
 
-    private static List<Resource> getParsedResources() throws IOException {
+    private static List<ResourceItem> getParsedResources() throws IOException {
         if (sResources == null) {
-            File root = TestUtils.getRoot("baseResourceSet");
+            File root = TestUtils.getRoot("resources", "baseSet");
             File values = new File(root, "values");
             File valuesXml = new File(values, "values.xml");
 
             ValueResourceParser parser = new ValueResourceParser(valuesXml);
             sResources = parser.parseFile();
 
-            // create a fake resource file to allow calling Resource.getKey()
+            // create a fake resource file to allow calling ResourceItem.getKey()
             new ResourceFile(valuesXml, sResources, "");
         }
 
diff --git a/builder/src/test/resources/testData/assets/baseMerge/merger.xml b/builder/src/test/resources/testData/assets/baseMerge/merger.xml
new file mode 100644 (file)
index 0000000..c82364a
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger xmlns:ns1="urn:oasis:names:tc:xliff:document:1.2">
+    <dataSet config="main">
+        <source path="$TOP$$SEP$..$SEP$baseSet">
+            <file name="icon.png" path="$TOP$$SEP$..$SEP$baseSet$SEP$icon.png" />
+            <file name="main.xml" path="$TOP$$SEP$..$SEP$baseSet$SEP$main.xml" />
+            <file name="foo.dat" path="$TOP$$SEP$..$SEP$baseSet$SEP$foo.dat" />
+            <file name="value.xml" path="$TOP$$SEP$..$SEP$baseSet$SEP$value.xml" />
+        </source>
+    </dataSet>
+    <dataSet config="overlay">
+        <source path="$TOP$$SEP$overlay">
+            <file name="icon2.png" path="$TOP$$SEP$overlay$SEP$icon2.png" />
+            <file name="icon.png" path="$TOP$$SEP$overlay$SEP$icon.png" />
+        </source>
+    </dataSet>
+</merger>
new file mode 100644 (file)
index 0000000000000000000000000000000000000000..029155e2c1a0455e15334b4565e53e59eaeb4366
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger>
+    <dataSet config="main">
+        <source path="$TOP$$SEP$main">
+            <file name="untouched.png" path="$TOP$$SEP$main$SEP$untouched.png" />
+            <file name="touched.png" path="$TOP$$SEP$main$SEP$touched.png" />
+            <file name="removed.png" path="$TOP$$SEP$main$SEP$removed.png" />
+            <file name="overlay_removed.png" path="$TOP$$SEP$main$SEP$overlay_removed.png" />
+            <file name="overlay_added.png" path="$TOP$$SEP$main$SEP$overlay_added.png" />
+        </source>
+    </dataSet>
+    <dataSet config="overlay">
+        <source path="$TOP$$SEP$overlay">
+            <file name="overlay_removed.png" path="$TOP$$SEP$overlay$SEP$overlay_removed.png" />
+        </source>
+    </dataSet>
+</merger>
similarity index 71%
rename from builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml
rename to builder/src/test/resources/testData/assets/baseSet/main.xml
index 18613c421d3279c2c92adafdb19fdbcc6209d481..ca547b839e8a79a3fbe491baeaba7e0611782d45 100644 (file)
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merger xmlns:ns1="urn:oasis:names:tc:xliff:document:1.2">
-    <resourceSet config="main">
-        <source path="$TOP$$SEP$baseResourceSet">
-            <file name="icon" path="$TOP$$SEP$baseResourceSet$SEP$drawable$SEP$icon.png" qualifiers="" type="drawable"/>
-            <file name="patch" path="$TOP$$SEP$baseResourceSet$SEP$drawable$SEP$patch.9.png" qualifiers="" type="drawable"/>
-            <file name="file_replaced_by_alias" path="$TOP$$SEP$baseResourceSet$SEP$layout$SEP$file_replaced_by_alias.xml" qualifiers="" type="layout"/>
-            <file name="main" path="$TOP$$SEP$baseResourceSet$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
-            <file name="foo" path="$TOP$$SEP$baseResourceSet$SEP$raw$SEP$foo.dat" qualifiers="" type="raw"/>
-            <file path="$TOP$$SEP$baseResourceSet$SEP$values$SEP$values.xml" qualifiers="">
+    <dataSet config="main">
+        <source path="$TOP$$SEP$..$SEP$baseSet">
+            <file name="icon" path="$TOP$$SEP$..$SEP$baseSet$SEP$drawable$SEP$icon.png" qualifiers="" type="drawable"/>
+            <file name="patch" path="$TOP$$SEP$..$SEP$baseSet$SEP$drawable$SEP$patch.9.png" qualifiers="" type="drawable"/>
+            <file name="file_replaced_by_alias" path="$TOP$$SEP$..$SEP$baseSet$SEP$layout$SEP$file_replaced_by_alias.xml" qualifiers="" type="layout"/>
+            <file name="main" path="$TOP$$SEP$..$SEP$baseSet$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
+            <file name="foo" path="$TOP$$SEP$..$SEP$baseSet$SEP$raw$SEP$foo.dat" qualifiers="" type="raw"/>
+            <file path="$TOP$$SEP$..$SEP$baseSet$SEP$values$SEP$values.xml" qualifiers="">
                 <dimen name="dimen">164dp</dimen>
                 <integer name="integer">75</integer>
                 <color name="color">#00000000</color>
                 </attr>
             </file>
         </source>
-    </resourceSet>
-    <resourceSet config="overlay">
-        <source path="$TOP$$SEP$baseMerge$SEP$overlay">
-            <file name="icon2" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$drawable$SEP$icon2.png" qualifiers="" type="drawable"/>
-            <file name="icon" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$drawable-ldpi$SEP$icon.png" qualifiers="ldpi" type="drawable"/>
-            <file name="alias_replaced_by_file" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$layout$SEP$alias_replaced_by_file.xml" qualifiers="" type="layout"/>
-            <file name="main" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
-            <file path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$values$SEP$values.xml" qualifiers="">
+    </dataSet>
+    <dataSet config="overlay">
+        <source path="$TOP$$SEP$overlay">
+            <file name="icon2" path="$TOP$$SEP$overlay$SEP$drawable$SEP$icon2.png" qualifiers="" type="drawable"/>
+            <file name="icon" path="$TOP$$SEP$overlay$SEP$drawable-ldpi$SEP$icon.png" qualifiers="ldpi" type="drawable"/>
+            <file name="alias_replaced_by_file" path="$TOP$$SEP$overlay$SEP$layout$SEP$alias_replaced_by_file.xml" qualifiers="" type="layout"/>
+            <file name="main" path="$TOP$$SEP$overlay$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
+            <file path="$TOP$$SEP$overlay$SEP$values$SEP$values.xml" qualifiers="">
                 <color name="color">#FFFFFFFF</color>
                 <item name="file_replaced_by_alias" type="layout">@layout$SEP$ref</item>
                 <string name="basic_string">overlay_string</string>
             </file>
         </source>
-    </resourceSet>
+    </dataSet>
 </merger>
similarity index 90%
rename from builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/touched.png
rename to builder/src/test/resources/testData/assets/incMergeData/basicFiles/main/added.png
index 6f4a3cb778f1f7119b6d6090aadf7dc71a8d08a9..e2e88ccfc6f80653f727638d8d18b2c9b57d064a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merger>
-    <resourceSet config="main">
+    <dataSet config="main">
         <source path="$TOP$$SEP$main">
             <file name="untouched" path="$TOP$$SEP$main$SEP$drawable$SEP$untouched.png" qualifiers="" type="drawable"/>
             <file name="touched" path="$TOP$$SEP$main$SEP$drawable$SEP$touched.png" qualifiers="" type="drawable"/>
@@ -9,10 +9,10 @@
             <file name="removed" path="$TOP$$SEP$main$SEP$drawable-ldpi$SEP$removed.png" qualifiers="ldpi" type="drawable"/>
             <file name="removed_overlay" path="$TOP$$SEP$main$SEP$drawable$SEP$removed_overlay.png" qualifiers="" type="drawable"/>
         </source>
-    </resourceSet>
-    <resourceSet config="overlay">
+    </dataSet>
+    <dataSet config="overlay">
         <source path="$TOP$$SEP$overlay">
             <file name="removed_overlay" path="$TOP$$SEP$overlay$SEP$drawable$SEP$removed_overlay.png" qualifiers="" type="drawable"/>
         </source>
-    </resourceSet>
+    </dataSet>
 </merger>
similarity index 86%
rename from builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable-hdpi/new_alternate.png
rename to builder/src/test/resources/testData/assets/incMergeData/basicFiles/main/overlay_removed.png
index 79980b23ae1e2693897d3af9a611bb7491756cfe..646c8a5c50df2a63748820ba87911a48b2a689df 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merger>
-    <resourceSet config="main">
+    <dataSet config="main">
         <source path="$TOP$$SEP$main">
             <file path="$TOP$$SEP$main$SEP$values$SEP$values.xml" qualifiers="">
                 <string name="untouched">untouched</string>
@@ -12,8 +12,8 @@
                 <string name="removed">removed</string>
             </file>
         </source>
-    </resourceSet>
-    <resourceSet config="overlay">
+    </dataSet>
+    <dataSet config="overlay">
         <source path="$TOP$$SEP$overlay" />
-    </resourceSet>
+    </dataSet>
 </merger>
diff --git a/builder/src/test/resources/testData/assets/incMergeData/basicFiles/merger.xml b/builder/src/test/resources/testData/assets/incMergeData/basicFiles/merger.xml
similarity index 84%
rename from builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png
rename to builder/src/test/resources/testData/resources/baseMerge/overlay/drawable-ldpi/icon.png
index 33f43564de217a4ab9906ea37a6c9ef8ce5d0ff6..aa771aa0cb857683b5c3746cf21487fee257e90d 100644 (file)
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merger>
-    <resourceSet config="main">
+    <dataSet config="main">
         <source path="$TOP$$SEP$main">
             <file path="$TOP$$SEP$main$SEP$values$SEP$values.xml" qualifiers="">
                 <string name="untouched">untouched</string>
                 <string name="removed_overlay">untouched</string>
             </file>
         </source>
-    </resourceSet>
-    <resourceSet config="overlay">
+    </dataSet>
+    <dataSet config="overlay">
         <source path="$TOP$$SEP$overlay">
             <file path="$TOP$$SEP$overlay$SEP$values$SEP$values.xml" qualifiers="">
                 <string name="removed_overlay">overlay</string>
             </file>
         </source>
-    </resourceSet>
+    </dataSet>
 </merger>
similarity index 91%
rename from builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png
rename to builder/src/test/resources/testData/resources/baseSet/drawable/icon.png
index c2cd24063b6c6a8a3999b700556f1e61f50251fc..e42e67ae61f07757c4c073428091eb3bca48a3c4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merger>
-    <resourceSet config="main">
+    <dataSet config="main">
         <source path="$TOP$$SEP$main">
             <file name="main" path="$TOP$$SEP$main$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
             <file name="file_replaced_by_alias" path="$TOP$$SEP$main$SEP$layout$SEP$file_replaced_by_alias.xml" qualifiers="" type="layout"/>
@@ -8,5 +8,5 @@
                 <item type="layout" name="alias_replaced_by_file">@layout/main</item>
             </file>
         </source>
-    </resourceSet>
+    </dataSet>
 </merger>
index c5e7ad7cffe76d0d7d4e8d516b89befe1dbe8aa8..2bfb77523d4de515cb1b967f50e119c8f98c1321 100644 (file)
@@ -490,6 +490,9 @@ class AppPlugin extends com.android.build.gradle.BasePlugin implements org.gradl
         // Add a task to merge the resource folders
         createMergeResourcesTask(variant, true /*process9Patch*/)
 
+        // Add a task to merge the asset folders
+        createMergeAssetsTask(variant, null /*default location*/)
+
         // Add a task to create the BuildConfig class
         createBuildConfigTask(variant)
 
index 6bbd639bae4fc200991eb0b14486ed749455be8f..eb205485dc8b6fa2c65330083a523177ed5de7be 100644 (file)
@@ -39,6 +39,7 @@ import com.android.build.gradle.internal.tasks.ValidateSigningTask
 import com.android.build.gradle.tasks.AidlCompile
 import com.android.build.gradle.tasks.Dex
 import com.android.build.gradle.tasks.GenerateBuildConfig
+import com.android.build.gradle.tasks.MergeAssets
 import com.android.build.gradle.tasks.MergeResources
 import com.android.build.gradle.tasks.PackageApplication
 import com.android.build.gradle.tasks.ProcessAndroidResources
@@ -368,7 +369,7 @@ public abstract class BasePlugin {
         def mergeResourcesTask = project.tasks.add("merge${variant.name}Resources", MergeResources)
         variant.mergeResourcesTask = mergeResourcesTask
 
-        mergeResourcesTask.dependsOn variant.renderscriptCompileTask
+        mergeResourcesTask.dependsOn variant.prepareDependenciesTask, variant.renderscriptCompileTask
         mergeResourcesTask.plugin = this
         mergeResourcesTask.variant = variant
         mergeResourcesTask.incrementalFolder =
@@ -380,9 +381,25 @@ public abstract class BasePlugin {
             variant.config.getResourceSets(variant.renderscriptCompileTask.getResOutputDir())
         }
 
-        mergeResourcesTask.conventionMapping.outputDir = {
-            project.file(location)
+        mergeResourcesTask.conventionMapping.outputDir = { project.file(location) }
+    }
+
+    protected void createMergeAssetsTask(ApplicationVariant variant, String location) {
+        if (location == null) {
+            location = "$project.buildDir/assets/$variant.dirName"
         }
+
+        def mergeAssetsTask = project.tasks.add("merge${variant.name}Assets", MergeAssets)
+        variant.mergeAssetsTask = mergeAssetsTask
+
+        mergeAssetsTask.dependsOn variant.prepareDependenciesTask
+        mergeAssetsTask.plugin = this
+        mergeAssetsTask.variant = variant
+        mergeAssetsTask.incrementalFolder =
+            project.file("$project.buildDir/incremental/mergeAssets/$variant.dirName")
+
+        mergeAssetsTask.conventionMapping.inputAssetSets = { variant.config.assetSets }
+        mergeAssetsTask.conventionMapping.outputDir = { project.file(location) }
     }
 
     protected void createBuildConfigTask(ApplicationVariant variant) {
@@ -419,7 +436,7 @@ public abstract class BasePlugin {
         def processResources = project.tasks.add("process${variant.name}Resources",
                 ProcessAndroidResources)
         variant.processResourcesTask = processResources
-        processResources.dependsOn variant.processManifestTask, variant.mergeResourcesTask
+        processResources.dependsOn variant.processManifestTask, variant.mergeResourcesTask, variant.mergeAssetsTask
 
         processResources.plugin = this
         processResources.variant = variant
@@ -430,12 +447,12 @@ public abstract class BasePlugin {
             variant.processManifestTask.outManifest
         }
 
-        processResources.conventionMapping.mergedResFolder = {
+        processResources.conventionMapping.resFolder = {
             variant.mergeResourcesTask.outputDir
         }
 
         processResources.conventionMapping.assetsDir =  {
-            getFirstOptionalDir(config.defaultSourceSet.assetsDirectories)
+            variant.mergeAssetsTask.outputDir
         }
 
         processResources.conventionMapping.libraries = {
@@ -606,6 +623,9 @@ public abstract class BasePlugin {
         // Add a task to merge the resource folders
         createMergeResourcesTask(variant, true /*process9Patch*/)
 
+        // Add a task to merge the assets folders
+        createMergeAssetsTask(variant, null /*default location*/)
+
         if (testedVariant.config.type == VariantConfiguration.Type.LIBRARY) {
             // in this case the tested library must be fully built before test can be built!
             if (testedVariant.assembleTask != null) {
index 05663d0b3c8874715004a474a040435c2bce29e8..5b420223d730c190d71752680e2e3b163bb00534 100644 (file)
@@ -21,6 +21,7 @@ import com.android.annotations.Nullable
 import com.android.build.gradle.tasks.AidlCompile
 import com.android.build.gradle.tasks.Dex
 import com.android.build.gradle.tasks.GenerateBuildConfig
+import com.android.build.gradle.tasks.MergeAssets
 import com.android.build.gradle.tasks.MergeResources
 import com.android.build.gradle.tasks.PackageApplication
 import com.android.build.gradle.tasks.ProcessManifest
@@ -131,12 +132,18 @@ public interface BuildVariant {
     AidlCompile getAidlCompile()
 
     /**
-     * Returns the image processing task.
+     * Returns the resource merging task.
      */
     @Nullable
     MergeResources getMergeResources()
 
     /**
+     * Returns the asset merging task.
+     */
+    @Nullable
+    MergeAssets getMergeAssets()
+
+    /**
      * Returns the Android Resources processing task.
      */
     @NonNull
index 544d1c7da11529d775cb78a2af0431e48b582a83..e49309e330c7f94d1069786e42d4cf60c9953f4c 100644 (file)
@@ -175,6 +175,10 @@ public class LibraryPlugin extends BasePlugin implements Plugin<Project> {
         createMergeResourcesTask(variant, "$project.buildDir/$DIR_BUNDLES/${variant.dirName}/res",
                 false /*process9Patch*/)
 
+        // Add a task to merge the assets folders
+        createMergeAssetsTask(variant,
+                "$project.buildDir/$DIR_BUNDLES/${variant.dirName}/assets")
+
         // Add a task to create the BuildConfig class
         createBuildConfigTask(variant)
 
index b9841760f87d3c4e676b1d4d57cf2e597c6a1469..a54f917fd999bbaaeae7fd63deb7ec893fa1141e 100644 (file)
@@ -21,6 +21,7 @@ import com.android.build.gradle.internal.tasks.TestFlavorTask
 import com.android.build.gradle.tasks.AidlCompile
 import com.android.build.gradle.tasks.Dex
 import com.android.build.gradle.tasks.GenerateBuildConfig
+import com.android.build.gradle.tasks.MergeAssets
 import com.android.build.gradle.tasks.MergeResources
 import com.android.build.gradle.tasks.PackageApplication
 import com.android.build.gradle.tasks.ProcessAndroidResources
@@ -48,6 +49,7 @@ public abstract class ApplicationVariant {
     RenderscriptCompile renderscriptCompileTask
     AidlCompile aidlCompileTask
     MergeResources mergeResourcesTask
+    MergeAssets mergeAssetsTask
     ProcessAndroidResources processResourcesTask
     GenerateBuildConfig generateBuildConfigTask
 
index 22316b5456ccb6ec6388a0c704ba1c51ee11a9cb..177e4f2b39f88cd71a5a1dfd9d1c3c873f863990 100644 (file)
@@ -19,6 +19,7 @@ import com.android.build.gradle.BuildVariant
 import com.android.build.gradle.tasks.AidlCompile
 import com.android.build.gradle.tasks.Dex
 import com.android.build.gradle.tasks.GenerateBuildConfig
+import com.android.build.gradle.tasks.MergeAssets
 import com.android.build.gradle.tasks.MergeResources
 import com.android.build.gradle.tasks.PackageApplication
 import com.android.build.gradle.tasks.ProcessAndroidResources
@@ -108,6 +109,11 @@ public class DefaultBuildVariant implements BuildVariant {
     }
 
     @Override
+    MergeAssets getMergeAssets() {
+        return variant.mergeAssetsTask
+    }
+
+    @Override
     ProcessAndroidResources getProcessResources() {
         return variant.processResourcesTask
     }
new file mode 100644 (file)
index 0000000000000000000000000000000000000000..6b893da2c0259acad363099902b212414e52c5fb
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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.build.gradle.tasks
+import com.android.build.gradle.internal.tasks.IncrementalTask
+import com.android.builder.resources.AssetMerger
+import com.android.builder.resources.AssetSet
+import com.android.builder.resources.FileStatus
+import com.android.utils.Pair
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputDirectory
+
+public class MergeAssets extends IncrementalTask {
+
+    // ----- PUBLIC TASK API -----
+
+    @OutputDirectory
+    File outputDir
+
+    // ----- PRIVATE TASK API -----
+
+    // fake input to detect changes. Not actually used by the task
+    @InputFiles
+    Iterable<File> getRawInputFolders() {
+        return IncrementalTask.flattenSourceSets(getInputAssetSets())
+    }
+
+    // actual inputs
+    List<AssetSet> inputAssetSets
+
+    @Override
+    protected boolean isIncremental() {
+        return true
+    }
+
+    @Override
+    protected Collection<File> getOutputForIncrementalBuild() {
+        return Collections.singletonList(getOutputDir())
+    }
+
+    @Override
+    protected void doFullTaskAction() {
+        // this is full run, clean the previous output
+        File destinationDir = getOutputDir()
+        emptyFolder(destinationDir)
+
+        List<AssetSet> assetSets = getInputAssetSets()
+
+        // create a new merger and populate it with the sets.
+        AssetMerger merger = new AssetMerger()
+
+        for (AssetSet assetSet : assetSets) {
+            // set needs to be loaded.
+            assetSet.loadFromFiles()
+            merger.addDataSet(assetSet)
+        }
+
+        // get the merged set and write it down.
+        merger.writeDataFolder(destinationDir)
+
+        // No exception? Write the known state.
+        merger.writeBlobTo(getIncrementalFolder())
+    }
+
+    @Override
+    protected void doIncrementalTaskAction(Map<File, FileStatus> changedInputs) {
+        // create a merger and load the known state.
+        AssetMerger merger = new AssetMerger()
+        if (!merger.loadFromBlob(getIncrementalFolder())) {
+            doFullTaskAction()
+            return
+        }
+
+        // compare the known state to the current sets to detect incompatibility.
+        // This is in case there's a change that's too hard to do incrementally. In this case
+        // we'll simply revert to full build.
+        List<AssetSet> assetSets = getInputAssetSets()
+
+        if (!merger.checkValidUpdate(assetSets)) {
+            project.logger.info("Changed Asset sets: full task run!")
+            doFullTaskAction()
+            return
+        }
+
+        // The incremental process is the following:
+        // Loop on all the changed files, find which ResourceSet it belongs to, then ask
+        // the resource set to update itself with the new file.
+        for (Map.Entry<File, FileStatus> entry : changedInputs.entrySet()) {
+            File changedFile = entry.getKey()
+
+            Pair<AssetSet, File> matchSet = merger.getDataSetContaining(changedFile)
+            assert matchSet != null
+            if (matchSet == null) {
+                doFullTaskAction()
+                return
+            }
+
+            // do something?
+            if (!matchSet.getFirst().updateWith(
+                    matchSet.getSecond(), changedFile, entry.getValue())) {
+                project.logger.info(
+                        String.format("Failed to process %s event! Full task run",
+                                entry.getValue()))
+                doFullTaskAction()
+                return
+            }
+        }
+
+        merger.writeDataFolder(getOutputDir())
+
+        // No exception? Write the known state.
+        merger.writeBlobTo(getIncrementalFolder())
+    }
+}
index 13735ebc8636dd82096115fce29a154b85738406..8d567884dcd1654846970047ac896534f10b60db 100644 (file)
@@ -69,11 +69,11 @@ public class MergeResources extends IncrementalTask {
         for (ResourceSet resourceSet : resourceSets) {
             // set needs to be loaded.
             resourceSet.loadFromFiles()
-            merger.addResourceSet(resourceSet)
+            merger.addDataSet(resourceSet)
         }
 
         // get the merged set and write it down.
-        merger.writeResourceFolder(destinationDir, getProcess9Patch() ? builder.aaptRunner : null)
+        merger.writeDataFolder(destinationDir, getProcess9Patch() ? builder.aaptRunner : null)
 
         // No exception? Write the known state.
         merger.writeBlobTo(getIncrementalFolder())
@@ -105,7 +105,7 @@ public class MergeResources extends IncrementalTask {
         for (Map.Entry<File, FileStatus> entry : changedInputs.entrySet()) {
             File changedFile = entry.getKey()
 
-            Pair<ResourceSet, File> matchSet = merger.getResourceSetContaining(changedFile)
+            Pair<ResourceSet, File> matchSet = merger.getDataSetContaining(changedFile)
             assert matchSet != null
             if (matchSet == null) {
                 doFullTaskAction()
@@ -123,7 +123,7 @@ public class MergeResources extends IncrementalTask {
             }
         }
 
-        merger.writeResourceFolder(getOutputDir(), getProcess9Patch() ? builder.aaptRunner : null)
+        merger.writeDataFolder(getOutputDir(), getProcess9Patch() ? builder.aaptRunner : null)
 
         // No exception? Write the known state.
         merger.writeBlobTo(getIncrementalFolder())
index 7b7e35e200c08128da28e8fb8ec1e5c18c5154fc..bb37f393b04aaf44a3446aecfcb358282f9593e7 100644 (file)
@@ -35,7 +35,10 @@ public class ProcessAndroidResources extends IncrementalTask {
     File manifestFile
 
     @InputDirectory
-    File mergedResFolder
+    File resFolder
+
+    @InputDirectory @Optional
+    File assetsDir
 
     @OutputDirectory @Optional
     File sourceOutputDir
@@ -51,9 +54,6 @@ public class ProcessAndroidResources extends IncrementalTask {
 
     // ----- PRIVATE TASK API -----
 
-    @InputDirectory @Optional
-    File assetsDir
-
     @Nested
     List<SymbolFileProvider> libraries
 
@@ -73,7 +73,7 @@ public class ProcessAndroidResources extends IncrementalTask {
     protected void doFullTaskAction() {
         getBuilder().processResources(
                 getManifestFile(),
-                getMergedResFolder(),
+                getResFolder(),
                 getAssetsDir(),
                 getLibraries(),
                 getPackageOverride(),
index db5e2210e23e431389845ee5ce09e05882e9e677..b532581513fc4b7a51140ca7609b670d2f910743 100644 (file)
@@ -208,6 +208,7 @@ public class AppPluginDslTest extends BaseTest {
         assertNotNull(variant.processManifest)
         assertNotNull(variant.aidlCompile)
         assertNotNull(variant.mergeResources)
+        assertNotNull(variant.mergeAssets)
         assertNotNull(variant.processResources)
         assertNotNull(variant.generateBuildConfig)
         assertNotNull(variant.javaCompile)
index ffa6cbc200726a7ffb8e12d965b453328184654a..5f342ba292bad29797f7cf69f3ae53cf006d5481 100644 (file)
@@ -95,6 +95,7 @@ public class LibraryPluginDslTest extends BaseTest {
         assertNotNull(variant.processManifest)
         assertNotNull(variant.aidlCompile)
         assertNotNull(variant.mergeResources)
+        assertNotNull(variant.mergeAssets)
         assertNotNull(variant.processResources)
         assertNotNull(variant.generateBuildConfig)
         assertNotNull(variant.javaCompile)