diff options
author | Googler <noreply@google.com> | 2016-03-17 22:34:52 +0000 |
---|---|---|
committer | Dmitry Lomov <dslomov@google.com> | 2016-03-18 12:48:29 +0000 |
commit | 80665ec28f4fde484c35e2935e8d06aabe902841 (patch) | |
tree | bb05d02f898becb9a92e3bfe4af97a4649499884 /src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java | |
parent | 652bb6953d2f020322c08c806a1409aae7696c09 (diff) |
Part 3 of 5: Merging semantics.
Introduces the AndroidDataMerger, MergeConflict, and UnwrittenMergedAndroidData which is the entry point in the AndroidResourceProcessing *AndroidData lifecycle.
Also, refactors the AndroidDataSet parsing of resources, making it functionally immutable.
--
MOS_MIGRATED_REVID=117492690
Diffstat (limited to 'src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java')
-rw-r--r-- | src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java new file mode 100644 index 0000000000..50a471b656 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java @@ -0,0 +1,233 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// 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.google.devtools.build.android; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import com.android.ide.common.res2.MergingException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Handles the Merging of AndroidDataSet. + */ +public class AndroidDataMerger { + + /** An internal class that handles the homogenization of AndroidDataSets. */ + // TODO(corysmith): Move this functionality to AndroidDataSet? + private static class ResourceMap { + + private final Map<FullyQualifiedName, DataResource> overwritableResources; + private final Set<MergeConflict> conflicts; + private final Map<FullyQualifiedName, DataResource> nonOverwritingResourceMap; + + private ResourceMap( + Map<FullyQualifiedName, DataResource> overwritableResources, + Set<MergeConflict> conflicts, + Map<FullyQualifiedName, DataResource> nonOverwritingResourceMap) { + this.overwritableResources = overwritableResources; + this.conflicts = conflicts; + this.nonOverwritingResourceMap = nonOverwritingResourceMap; + } + + /** + * Creates ResourceMap from an AndroidDataSet. + */ + static ResourceMap from(AndroidDataSet data) { + Map<FullyQualifiedName, DataResource> overwritingResourceMap = new HashMap<>(); + Set<MergeConflict> conflicts = new HashSet<>(); + for (DataResource resource : data.getOverwritingResources()) { + if (overwritingResourceMap.containsKey(resource.fullyQualifiedName())) { + conflicts.add( + MergeConflict.between( + resource.fullyQualifiedName(), + overwritingResourceMap.get(resource.fullyQualifiedName()), + resource)); + } + overwritingResourceMap.put(resource.fullyQualifiedName(), resource); + } + + Map<FullyQualifiedName, DataResource> nonOverwritingResourceMap = new HashMap<>(); + for (DataResource resource : data.getNonOverwritingResources()) { + nonOverwritingResourceMap.put(resource.fullyQualifiedName(), resource); + } + return new ResourceMap(overwritingResourceMap, conflicts, nonOverwritingResourceMap); + } + + boolean containsOverwritable(FullyQualifiedName name) { + return overwritableResources.containsKey(name); + } + + Iterable<Map.Entry<FullyQualifiedName, DataResource>> iterateOverwritableEntries() { + return overwritableResources.entrySet(); + } + + public MergeConflict foundConflict(FullyQualifiedName key, DataResource value) { + return MergeConflict.between(key, overwritableResources.get(key), value); + } + + public Collection<DataResource> mergeNonOverwritable(ResourceMap other) { + Map<FullyQualifiedName, DataResource> merged = new HashMap<>(other.nonOverwritingResourceMap); + merged.putAll(nonOverwritingResourceMap); + return merged.values(); + } + } + + /** + * Merges DataResources into an UnwrittenMergedAndroidData. + * + * This method has two basic states, library and binary. These are distinguished by + * allowPrimaryOverrideAll, which allows the primary data to overwrite any value in the closure, + * a trait associated with binaries, as a binary is a leaf node. The other semantics are + * slightly more complicated: a given resource can be overwritten only if it resides in the + * direct dependencies of primary data. This forces an explicit simple priority for each resource, + * instead of the more subtle semantics of multiple layers of libraries with potential overwrites. + * + * The UnwrittenMergedAndroidData contains only one of each FullyQualifiedName in both the + * direct and transitive closure. + * + * The merge semantics are as follows: + * Key: + * A(): package A + * A(foo): package A with resource symbol foo + * A() -> B(): a dependency relationship of B.deps = [:A] + * A(),B() -> C(): a dependency relationship of C.deps = [:A,:B] + * + * For android library (allowPrimaryOverrideAll = False) + * + * A() -> B(foo) -> C(foo) == Valid + * A() -> B() -> C(foo) == Valid + * A() -> B() -> C(foo),D(foo) == Conflict + * A(foo) -> B(foo) -> C() == Conflict + * A(foo) -> B() -> C(foo) == Conflict + * A(foo),B(foo) -> C() -> D() == Conflict + * A() -> B(foo),C(foo) -> D() == Conflict + * A(foo),B(foo) -> C() -> D(foo) == Conflict + * A() -> B(foo),C(foo) -> D(foo) == Conflict + * + * For android binary (allowPrimaryOverrideAll = True) + * + * A() -> B(foo) -> C(foo) == Valid + * A() -> B() -> C(foo) == Valid + * A() -> B() -> C(foo),D(foo) == Conflict + * A(foo) -> B(foo) -> C() == Conflict + * A(foo) -> B() -> C(foo) == Valid + * A(foo),B(foo) -> C() -> D() == Conflict + * A() -> B(foo),C(foo) -> D() == Conflict + * A(foo),B(foo) -> C() -> D(foo) == Valid + * A() -> B(foo),C(foo) -> D(foo) == Valid + * + * @param transitive The transitive dependencies to merge. + * @param direct The direct dependencies to merge. + * @param primaryData The primary data to merge against. + * @param allowPrimaryOverrideAll Boolean that indicates if the primary data will be considered + * the ultimate source of truth, provided it doesn't conflict + * with itself. + * @return An UnwrittenMergedAndroidData, containing DataResource objects that can be written + * to disk for aapt processing or serialized for future merge passes. + * @throws MergingException if there are merge conflicts or issues with parsing resources from + * Primary. + * @throws IOException if there are issues with reading resources. + */ + UnwrittenMergedAndroidData merge( + AndroidDataSet transitive, + AndroidDataSet direct, + UnvalidatedAndroidData primaryData, + boolean allowPrimaryOverrideAll) + throws MergingException, IOException { + + // Extract the primary resources. + AndroidDataSet primary = AndroidDataSet.from(primaryData); + ResourceMap primaryMap = ResourceMap.from(primary); + + // Handle the overwriting resources first. + ResourceMap directMap = ResourceMap.from(direct); + ResourceMap transitiveMap = ResourceMap.from(transitive); + + List<DataResource> overwritableDeps = new ArrayList<>(); + + Set<MergeConflict> conflicts = new HashSet<>(); + conflicts.addAll(primaryMap.conflicts); + for (MergeConflict conflict : directMap.conflicts) { + if (allowPrimaryOverrideAll + && primaryMap.containsOverwritable(conflict.fullyQualifiedName())) { + continue; + } + conflicts.add(conflict); + } + + for (MergeConflict conflict : transitiveMap.conflicts) { + if (allowPrimaryOverrideAll + && primaryMap.containsOverwritable(conflict.fullyQualifiedName())) { + continue; + } + conflicts.add(conflict); + } + + for (Map.Entry<FullyQualifiedName, DataResource> entry : + directMap.iterateOverwritableEntries()) { + // Direct dependencies are simply overwritten, no conflict. + if (!primaryMap.containsOverwritable(entry.getKey())) { + overwritableDeps.add(entry.getValue()); + } + } + + for (Map.Entry<FullyQualifiedName, DataResource> entry : + transitiveMap.iterateOverwritableEntries()) { + // If the primary is considered to be intentional (usually at the binary level), + // skip. + if (primaryMap.containsOverwritable(entry.getKey()) && allowPrimaryOverrideAll) { + continue; + } + // If a transitive value is in the direct map report a conflict, as it is commonly + // unintentional. + if (directMap.containsOverwritable(entry.getKey())) { + conflicts.add(directMap.foundConflict(entry.getKey(), entry.getValue())); + } else if (primaryMap.containsOverwritable(entry.getKey())) { + // If overwriting a transitive value with a primary map, assume it's an unintentional + // override, unless allowPrimaryOverrideAll is set. At which point, this code path + // should not be reached. + conflicts.add(primaryMap.foundConflict(entry.getKey(), entry.getValue())); + } else { + // If it's in none of the of sources, add it. + overwritableDeps.add(entry.getValue()); + } + } + + if (!conflicts.isEmpty()) { + List<String> messages = new ArrayList<>(); + for (MergeConflict conflict : conflicts) { + messages.add(conflict.toConflictMessage()); + } + throw new MergingException(Joiner.on("\n").join(messages)); + } + + Collections.sort(overwritableDeps); + + return UnwrittenMergedAndroidData.of( + primaryData.getManifest(), + primary, + AndroidDataSet.of( + overwritableDeps, ImmutableList.copyOf(directMap.mergeNonOverwritable(transitiveMap)))); + } +} |