/* * 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.internal.component.model; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.gradle.api.attributes.Attribute; import org.gradle.api.attributes.AttributeContainer; import org.gradle.api.attributes.HasAttributes; import org.gradle.api.internal.attributes.AttributeValue; import org.gradle.api.internal.attributes.MultipleCandidatesResult; import org.gradle.internal.Cast; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; public class ComponentAttributeMatcher { /** * Determines whether the given candidate is compatible with the requested criteria, according to the given schema. */ public boolean isMatching(AttributeSelectionSchema schema, AttributeContainer candidate, AttributeContainer requested) { if (requested.isEmpty() || candidate.isEmpty()) { return true; } MatchDetails details = new MatchDetails<AttributeContainer>(candidate); doMatchCandidate(schema, candidate, requested, details); return details.compatible; } /** * Selects the candidates from the given set that are compatible with the requested criteria, according to the given schema. */ public <T extends HasAttributes> List<T> match(AttributeSelectionSchema schema, Collection<T> candidates, AttributeContainer requested) { if (candidates.size() == 1) { T candidate = candidates.iterator().next(); if (isMatching(schema, candidate.getAttributes(), requested)) { return Collections.singletonList(candidate); } } return new Matcher<T>(schema, candidates, requested).getMatches(); } private void doMatchCandidate(AttributeSelectionSchema schema, HasAttributes candidate, AttributeContainer requested, MatchDetails details) { Set<Attribute<Object>> requestedAttributes = Cast.uncheckedCast(requested.keySet()); AttributeContainer candidateAttributesContainer = candidate.getAttributes(); Set<Attribute<Object>> candidateAttributes = Cast.uncheckedCast(candidateAttributesContainer.keySet()); for (Iterator<Attribute<Object>> requestedIterator = requestedAttributes.iterator(); details.compatible && requestedIterator.hasNext();) { Attribute<Object> attribute = requestedIterator.next(); AttributeValue<Object> requestedValue = attributeValue(attribute, schema, requested); AttributeValue<Object> actualValue = attributeValue(attribute, schema, candidateAttributesContainer); if (actualValue.isPresent()) { details.update(attribute, schema, requestedValue, actualValue); } } if (!details.compatible) { return; } for (Iterator<Attribute<Object>> candidateIterator = candidateAttributes.iterator(); details.compatible && candidateIterator.hasNext();) { Attribute<Object> attribute = candidateIterator.next(); if (requestedAttributes.contains(attribute)) { continue; } AttributeValue<Object> actualValue = attributeValue(attribute, schema, candidateAttributesContainer); details.updateForMissingConsumerValue(attribute, actualValue); } } private AttributeValue<Object> attributeValue(Attribute<Object> attribute, AttributeSelectionSchema schema, AttributeContainer container) { if (container.contains(attribute)) { return AttributeValue.of(container.getAttribute(attribute)); } if (schema.hasAttribute(attribute)) { return AttributeValue.missing(); } else { return AttributeValue.unknown(); } } private class Matcher<T extends HasAttributes> { private final AttributeSelectionSchema schema; private final List<MatchDetails<T>> matchDetails; private final AttributeContainer requested; public Matcher(AttributeSelectionSchema schema, Collection<T> candidates, AttributeContainer requested) { this.schema = schema; this.matchDetails = Lists.newArrayListWithCapacity(candidates.size()); for (T cand : candidates) { matchDetails.add(new MatchDetails<T>(cand)); } this.requested = requested; doMatch(); } private void doMatch() { for (MatchDetails<T> matchDetail : matchDetails) { doMatchCandidate(schema, matchDetail.candidate, requested, matchDetail); } } public List<T> getMatches() { List<MatchDetails<T>> compatible = new ArrayList<MatchDetails<T>>(1); for (MatchDetails<T> details : matchDetails) { if (details.compatible) { compatible.add(details); } } if (compatible.size() > 1) { compatible = selectClosestMatches(compatible); } if (compatible.isEmpty()) { return Collections.emptyList(); } if (compatible.size() == 1) { return Collections.singletonList(compatible.get(0).candidate); } List<T> selected = new ArrayList<T>(compatible.size()); for (MatchDetails<T> details : compatible) { selected.add(details.candidate); } return selected; } private List<MatchDetails<T>> selectClosestMatches(List<MatchDetails<T>> compatible) { // check whether any single match is a superset of the others for (MatchDetails<T> details : compatible) { boolean superSetToAll = true; for (MatchDetails candidate : compatible) { if (details != candidate && (!details.matched.containsAll(candidate.matched) || details.matched.equals(candidate.matched))) { superSetToAll = false; break; } } if (superSetToAll) { return Collections.singletonList(details); } } // if there's more than one compatible match, prefer the closest. However there's a catch. // We need to look at all candidates globally, and select the closest match for each attribute // then see if there's a non-empty intersection. List<MatchDetails<T>> remainingMatches = Lists.newArrayList(compatible); List<MatchDetails<T>> best = Lists.newArrayListWithCapacity(compatible.size()); Multimap<Object, MatchDetails<T>> candidatesByValue = LinkedHashMultimap.create(); Set<Attribute<?>> allAttributes = Sets.newHashSet(); for (MatchDetails<T> details : compatible) { allAttributes.addAll(details.matchesByAttribute.keySet()); } for (Attribute<?> attribute : allAttributes) { for (MatchDetails<T> details : compatible) { Map<Attribute<Object>, Object> matchedAttributes = details.matchesByAttribute; Object val = matchedAttributes.get(attribute); candidatesByValue.put(val, details); } disambiguate(attribute, requested.getAttribute(attribute), remainingMatches, candidatesByValue, schema, best); if (remainingMatches.isEmpty()) { // the intersection is empty, so we cannot choose return compatible; } candidatesByValue.clear(); best.clear(); } // there's a subset (or not) of best matches return remainingMatches; } private void disambiguate(Attribute<?> attribute, Object requested, List<MatchDetails<T>> remainingMatches, Multimap<Object, MatchDetails<T>> candidatesByValue, AttributeSelectionSchema schema, List<MatchDetails<T>> best) { if (candidatesByValue.isEmpty()) { // missing or unknown return; } MultipleCandidatesResult<Object> details = new DefaultCandidateResult<MatchDetails<T>>(candidatesByValue, best); schema.disambiguate(attribute, requested, details); remainingMatches.retainAll(best); } } private static class MatchDetails<T extends HasAttributes> { private final Set<Attribute<Object>> matched = Sets.newHashSet(); private final Map<Attribute<Object>, Object> matchesByAttribute = Maps.newHashMap(); private final T candidate; private boolean compatible = true; MatchDetails(T candidate) { this.candidate = candidate; } void update(final Attribute<Object> attribute, AttributeSelectionSchema schema, AttributeValue<Object> consumerValue, AttributeValue<Object> producerValue) { DefaultCompatibilityCheckResult<Object> details = new DefaultCompatibilityCheckResult<Object>(consumerValue.get(), producerValue.get()); schema.matchValue(attribute, details); if (details.isCompatible()) { matched.add(attribute); matchesByAttribute.put(attribute, producerValue.get()); } else { compatible = false; } } void updateForMissingConsumerValue(Attribute<Object> attribute, AttributeValue<Object> producerValue) { matchesByAttribute.put(attribute, producerValue.get()); } } }