/* * Copyright 2016 the original author or authors. * * 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 org.gradle.api.internal.artifacts.ivyservice.resolveengine.excludes; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.gradle.api.artifacts.ModuleIdentifier; import org.gradle.api.internal.artifacts.ImmutableModuleIdentifierFactory; import org.gradle.internal.component.model.Exclude; import org.gradle.internal.component.model.IvyArtifactName; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static org.gradle.api.internal.artifacts.ivyservice.resolveengine.excludes.AbstractModuleExclusion.isWildcard; /** * Manages sets of exclude rules, allowing union and intersection operations on the rules. * * <p>This class attempts to reduce execution time, by flattening union and intersection specs, at the cost of more analysis at construction time. This is taken advantage of by {@link * org.gradle.api.internal.artifacts.ivyservice.resolveengine.graph.DependencyGraphBuilder}, on the assumption that there are many more edges in the dependency graph than there are exclude rules (ie * we evaluate the rules much more often that we construct them). </p> * * <p>Also, this class attempts to be quite accurate in determining if 2 specs will exclude exactly the same set of modules. {@link org.gradle.api.internal.artifacts.ivyservice.resolveengine.graph.DependencyGraphBuilder} * uses this to avoid traversing the dependency graph of a particular version that has already been traversed when a new incoming edge is added (eg a newly discovered dependency) and when an incoming * edge is removed (eg a conflict evicts a version that depends on the given version). </p> * * <ul> <li>When a module dependency has multiple exclusions, then the resulting exclusion is the _intersection_ of those exclusions (module is excluded if excluded by _any_).</li> <li>When a module * is depended on via a transitive path, then the resulting exclusion is the _intersection_ of the exclusions on each leg of the path (module is excluded if excluded by _any_).</li> <li>When a module * is depended on via multiple paths in the graph, then the resulting exclusion is the _union_ of the exclusions on each of those paths (module is excluded if excluded by _all_).</li> </ul> */ public class ModuleExclusions { private static final ExcludeNone EXCLUDE_NONE = new ExcludeNone(); private static final ExcludeAllModulesSpec EXCLUDE_ALL_MODULES_SPEC = new ExcludeAllModulesSpec(); private final ImmutableModuleIdentifierFactory moduleIdentifierFactory; private final Map<List<Exclude>, Map<Set<String>, ModuleExclusion>> cachedExcludes = Maps.newConcurrentMap(); private final Map<MergeOperation, AbstractModuleExclusion> mergeCache = Maps.newConcurrentMap(); private final Map<List<Exclude>, AbstractModuleExclusion> excludeAnyCache = Maps.newConcurrentMap(); private final Map<Set<AbstractModuleExclusion>, ImmutableModuleExclusionSet> exclusionSetCache = Maps.newConcurrentMap(); private final Map<AbstractModuleExclusion[], Map<AbstractModuleExclusion[], MergeOperation>> mergeOperationCache = Maps.newIdentityHashMap(); private final Object mergeOperationLock = new Object(); public ModuleExclusions(ImmutableModuleIdentifierFactory moduleIdentifierFactory) { this.moduleIdentifierFactory = moduleIdentifierFactory; } public ModuleExclusion excludeAny(List<Exclude> excludes, Set<String> hierarchy) { Map<Set<String>, ModuleExclusion> exclusionMap = cachedExcludes.get(excludes); if (exclusionMap == null) { exclusionMap = Maps.newConcurrentMap(); cachedExcludes.put(excludes, exclusionMap); } ModuleExclusion moduleExclusion = exclusionMap.get(hierarchy); if (moduleExclusion == null) { List<Exclude> filtered = Lists.newArrayList(); for (Exclude exclude : excludes) { for (String config : exclude.getConfigurations()) { if (hierarchy.contains(config)) { filtered.add(exclude); break; } } } moduleExclusion = excludeAny(filtered); exclusionMap.put(hierarchy, moduleExclusion); } return moduleExclusion; } private ImmutableModuleExclusionSet asImmutable(Set<AbstractModuleExclusion> excludes) { ImmutableModuleExclusionSet cached = exclusionSetCache.get(excludes); if (cached == null) { cached = new ImmutableModuleExclusionSet(excludes); exclusionSetCache.put(excludes, cached); } return cached; } /** * Returns a spec that excludes nothing. */ public static ModuleExclusion excludeNone() { return EXCLUDE_NONE; } /** * Returns a spec that excludes those modules and artifacts that are excluded by _any_ of the given exclude rules. */ public ModuleExclusion excludeAny(Exclude... excludes) { if (excludes.length == 0) { return EXCLUDE_NONE; } return excludeAny(Arrays.asList(excludes)); } /** * Returns a spec that excludes those modules and artifacts that are excluded by _any_ of the given exclude rules. */ public ModuleExclusion excludeAny(List<Exclude> excludes) { if (excludes.isEmpty()) { return EXCLUDE_NONE; } AbstractModuleExclusion exclusion = excludeAnyCache.get(excludes); if (exclusion != null) { return exclusion; } Set<AbstractModuleExclusion> exclusions = Sets.newHashSetWithExpectedSize(excludes.size()); for (Exclude exclude : excludes) { exclusions.add(forExclude(exclude)); } exclusion = new IntersectionExclusion(asImmutable(exclusions)); excludeAnyCache.put(excludes, exclusion); return exclusion; } private static AbstractModuleExclusion forExclude(Exclude rule) { // For custom ivy pattern matchers, don't inspect the rule any more deeply: this prevents us from doing smart merging later if (!PatternMatchers.isExactMatcher(rule.getMatcher())) { return new IvyPatternMatcherExcludeRuleSpec(rule); } ModuleIdentifier moduleId = rule.getModuleId(); IvyArtifactName artifact = rule.getArtifact(); boolean anyOrganisation = isWildcard(moduleId.getGroup()); boolean anyModule = isWildcard(moduleId.getName()); boolean anyArtifact = isWildcard(artifact.getName()) && isWildcard(artifact.getType()) && isWildcard(artifact.getExtension()); // Build a strongly typed (mergeable) exclude spec for each supplied rule if (anyArtifact) { if (!anyOrganisation && !anyModule) { return new ModuleIdExcludeSpec(moduleId); } else if (!anyModule) { return new ModuleNameExcludeSpec(moduleId.getName()); } else if (!anyOrganisation) { return new GroupNameExcludeSpec(moduleId.getGroup()); } else { return EXCLUDE_ALL_MODULES_SPEC; } } else { return new ArtifactExcludeSpec(moduleId, artifact); } } /** * Returns a spec that excludes those modules and artifacts that are excluded by _either_ of the given exclude rules. */ public ModuleExclusion intersect(ModuleExclusion one, ModuleExclusion two) { if (one == two) { return one; } if (one == EXCLUDE_NONE) { return two; } if (two == EXCLUDE_NONE) { return one; } if (one.equals(two)) { return one; } if (one instanceof IntersectionExclusion && ((IntersectionExclusion) one).getFilters().contains(two)) { return one; } else if (two instanceof IntersectionExclusion && ((IntersectionExclusion) two).getFilters().contains(one)) { return two; } Set<AbstractModuleExclusion> builder = Sets.newHashSet(); ((AbstractModuleExclusion) one).unpackIntersection(builder); ((AbstractModuleExclusion) two).unpackIntersection(builder); return new IntersectionExclusion(asImmutable(builder)); } /** * Returns a spec that excludes only those modules and artifacts that are excluded by _both_ of the supplied exclude rules. */ public ModuleExclusion union(ModuleExclusion one, ModuleExclusion two) { if (one == two) { return one; } if (one == EXCLUDE_NONE || two == EXCLUDE_NONE) { return EXCLUDE_NONE; } if (one.equals(two)) { return one; } List<AbstractModuleExclusion> specs = new ArrayList<AbstractModuleExclusion>(); ((AbstractModuleExclusion) one).unpackUnion(specs); ((AbstractModuleExclusion) two).unpackUnion(specs); for (int i = 0; i < specs.size();) { AbstractModuleExclusion spec = specs.get(i); AbstractModuleExclusion merged = null; // See if we can merge any of the following specs into one for (int j = i + 1; j < specs.size(); j++) { AbstractModuleExclusion other = specs.get(j); merged = maybeMergeIntoUnion(spec, other); if (merged != null) { specs.remove(j); break; } } if (merged != null) { specs.set(i, merged); } else { i++; } } if (specs.size() == 1) { return specs.get(0); } return new UnionExclusion(specs); } /** * Attempt to merge 2 exclusions into a single filter that is the union of both. * Currently this is only implemented when both exclusions are `IntersectionExclusion`s. */ private AbstractModuleExclusion maybeMergeIntoUnion(AbstractModuleExclusion one, AbstractModuleExclusion two) { if (one.equals(two)) { return one; } if (one instanceof IntersectionExclusion && two instanceof IntersectionExclusion) { return maybeMergeIntoUnion((IntersectionExclusion) one, (IntersectionExclusion) two); } return null; } private AbstractModuleExclusion maybeMergeIntoUnion(IntersectionExclusion one, IntersectionExclusion other) { if (one.equals(other)) { return one; } if (one.canMerge() && other.canMerge()) { AbstractModuleExclusion[] oneFilters = one.getFilters().elements; AbstractModuleExclusion[] otherFilters = other.getFilters().elements; if (Arrays.equals(oneFilters, otherFilters)) { return one; } MergeOperation merge = mergeOperation(oneFilters, otherFilters); AbstractModuleExclusion exclusion = mergeCache.get(merge); if (exclusion != null) { return exclusion; } return mergeAndCacheResult(merge, oneFilters, otherFilters); } return null; } private MergeOperation mergeOperation(AbstractModuleExclusion[] one, AbstractModuleExclusion[] two) { synchronized (mergeOperationLock) { Map<AbstractModuleExclusion[], MergeOperation> oneMap = mergeOperationCache.get(one); if (oneMap == null) { oneMap = Maps.newIdentityHashMap(); mergeOperationCache.put(one, oneMap); } MergeOperation mergeOperation = oneMap.get(two); if (mergeOperation != null) { return mergeOperation; } mergeOperation = new MergeOperation(one, two); oneMap.put(two, mergeOperation); return mergeOperation; } } private AbstractModuleExclusion mergeAndCacheResult(MergeOperation merge, AbstractModuleExclusion[] oneFilters, AbstractModuleExclusion[] otherFilters) { AbstractModuleExclusion exclusion; // Merge the exclude rules from both specs into a single union spec. final BitSet remaining = new BitSet(otherFilters.length); remaining.set(0, otherFilters.length, true); MergeSet merged = new MergeSet(remaining, oneFilters.length + otherFilters.length); for (AbstractModuleExclusion thisSpec : oneFilters) { if (!remaining.isEmpty()) { for (int i = remaining.nextSetBit(0); i >= 0; i = remaining.nextSetBit(i+1)) { AbstractModuleExclusion otherSpec = otherFilters[i]; merged.current = otherSpec; merged.idx = i; mergeExcludeRules(thisSpec, otherSpec, merged); } } } if (merged.isEmpty()) { exclusion = ModuleExclusions.EXCLUDE_NONE; } else { exclusion = new IntersectionExclusion(asImmutable(merged)); } mergeCache.put(merge, exclusion); return exclusion; } // Add exclusions to the list that will exclude modules/artifacts that are excluded by _both_ of the candidate rules. private void mergeExcludeRules(AbstractModuleExclusion spec1, AbstractModuleExclusion spec2, Set<AbstractModuleExclusion> merged) { if (spec1 == spec2) { merged.add(spec1); } else if (spec1 instanceof ExcludeAllModulesSpec) { // spec1 excludes everything: use spec2 excludes merged.add(spec2); } else if (spec2 instanceof ExcludeAllModulesSpec) { // spec2 excludes everything: use spec1 excludes merged.add(spec1); } else if (spec1 instanceof ArtifactExcludeSpec) { // Excludes _no_ modules, may exclude some artifacts. // This isn't right: We are losing the artifacts excluded by spec2 // (2 artifact excludes should cancel out unless equal) merged.add(spec1); } else if (spec2 instanceof ArtifactExcludeSpec) { // Excludes _no_ modules, may exclude some artifacts. // This isn't right: We are losing the artifacts excluded by spec2 merged.add(spec2); } else if (spec1 instanceof GroupNameExcludeSpec) { // Merge into a single exclusion for Group + Module mergeExcludeRules((GroupNameExcludeSpec) spec1, spec2, merged); } else if (spec2 instanceof GroupNameExcludeSpec) { // Merge into a single exclusion for Group + Module mergeExcludeRules((GroupNameExcludeSpec) spec2, spec1, merged); } else if (spec1 instanceof ModuleNameExcludeSpec) { // Merge into a single exclusion for Group + Module mergeExcludeRules((ModuleNameExcludeSpec) spec1, spec2, merged); } else if (spec2 instanceof ModuleNameExcludeSpec) { // Merge into a single exclusion for Group + Module mergeExcludeRules((ModuleNameExcludeSpec) spec2, spec1, merged); } else if ((spec1 instanceof ModuleIdExcludeSpec) && (spec2 instanceof ModuleIdExcludeSpec)) { // Excludes nothing if the excluded module ids don't match: in that case this rule contributes nothing to the union ModuleIdExcludeSpec moduleSpec1 = (ModuleIdExcludeSpec) spec1; ModuleIdExcludeSpec moduleSpec2 = (ModuleIdExcludeSpec) spec2; if (moduleSpec1.moduleId.equals(moduleSpec2.moduleId)) { merged.add(moduleSpec1); } } else { throw new UnsupportedOperationException(String.format("Cannot calculate intersection of exclude rules: %s, %s", spec1, spec2)); } } private void mergeExcludeRules(GroupNameExcludeSpec spec1, AbstractModuleExclusion spec2, Set<AbstractModuleExclusion> merged) { if (spec2 instanceof GroupNameExcludeSpec) { // Intersection of 2 group excludes does nothing unless excluded groups match GroupNameExcludeSpec groupNameExcludeSpec = (GroupNameExcludeSpec) spec2; if (spec1.group.equals(groupNameExcludeSpec.group)) { merged.add(spec1); } } else if (spec2 instanceof ModuleNameExcludeSpec) { // Intersection of group & module name exclude only excludes module with matching group + name ModuleNameExcludeSpec moduleNameExcludeSpec = (ModuleNameExcludeSpec) spec2; merged.add(new ModuleIdExcludeSpec(moduleIdentifierFactory.module(spec1.group, moduleNameExcludeSpec.module))); } else if (spec2 instanceof ModuleIdExcludeSpec) { // Intersection of group + module id exclude only excludes the module id if the excluded groups match ModuleIdExcludeSpec moduleIdExcludeSpec = (ModuleIdExcludeSpec) spec2; if (moduleIdExcludeSpec.moduleId.getGroup().equals(spec1.group)) { merged.add(spec2); } } else { throw new UnsupportedOperationException(String.format("Cannot calculate intersection of exclude rules: %s, %s", spec1, spec2)); } } private static void mergeExcludeRules(ModuleNameExcludeSpec spec1, AbstractModuleExclusion spec2, Set<AbstractModuleExclusion> merged) { if (spec2 instanceof ModuleNameExcludeSpec) { // Intersection of 2 module name excludes does nothing unless excluded module names match ModuleNameExcludeSpec moduleNameExcludeSpec = (ModuleNameExcludeSpec) spec2; if (spec1.module.equals(moduleNameExcludeSpec.module)) { merged.add(spec1); } } else if (spec2 instanceof ModuleIdExcludeSpec) { // Intersection of module name & module id exclude only excludes module if the excluded module names match ModuleIdExcludeSpec moduleIdExcludeSpec = (ModuleIdExcludeSpec) spec2; if (moduleIdExcludeSpec.moduleId.getName().equals(spec1.module)) { merged.add(spec2); } } else { throw new UnsupportedOperationException(String.format("Cannot calculate intersection of exclude rules: %s, %s", spec1, spec2)); } } private static final class MergeOperation { private final AbstractModuleExclusion[] one; private final AbstractModuleExclusion[] two; private final int hashCode; private MergeOperation(AbstractModuleExclusion[] one, AbstractModuleExclusion[] two) { this.one = one; this.two = two; this.hashCode = 31 * Arrays.hashCode(one) + Arrays.hashCode(two); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MergeOperation that = (MergeOperation) o; if (!Arrays.equals(one, that.one)) { return false; } return Arrays.equals(two, that.two); } @Override public int hashCode() { return hashCode; } } private static final class MergeSet extends HashSet<AbstractModuleExclusion> { private final BitSet remaining; private int idx; private AbstractModuleExclusion current; private MergeSet(BitSet remaining, int size) { super(size); this.remaining = remaining; } @Override public boolean add(AbstractModuleExclusion abstractModuleExclusion) { if (current == abstractModuleExclusion) { remaining.clear(idx); } return super.add(abstractModuleExclusion); } } }