org.gradle.internal.component.model.MultipleCandidateMatcher Maven / Gradle / Ivy
Show all versions of gradle-api Show documentation
/*
* Copyright 2017 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.ImmutableList;
import com.google.common.collect.Sets;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.HasAttributes;
import org.gradle.api.internal.attributes.AttributeContainerInternal;
import org.gradle.api.internal.attributes.AttributeValue;
import org.gradle.api.internal.attributes.ImmutableAttributes;
import javax.annotation.Nullable;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* This is the heart of the attribute matching algorithm and is used whenever there are multiple candidates to choose from.
*
*
* -
* For each candidate, check whether its attribute values are compatible (according to the {@link AttributeSelectionSchema)} with the values that were requested.
* Any missing or extra attributes on the candidate are ignored at this point. If there are 0 or 1 compatible candidates after this, return that as the result.
*
* -
* If there are multiple candidates matching the requested values, check whether one of the candidates is a strict superset of all the others, i.e. it matched more
* of the requested attributes than any other one. Missing or extra attributes don't count. If such a strict superset candidate exists, it is returned as the single match.
*
* -
* Otherwise continue with disambiguation. Disambiguation iterates through the attributes and presents the different values to the {@link AttributeSelectionSchema}.
* The schema can declare a subset of these values as preferred. Candidates whose value is not in that subset are rejected. If a single candidate remains after
* disambiguating the requested attributes, this candidate is returned. Otherwise disambiguation continues with extra attributes.
*
* -
* If we run out of candidates during this process, the intersection of preferred values is not satisfied by any of them, so there could be multiple valid choices.
* In that case return all compatible candidates, as none of them is preferable over any other.
*
* -
* If there are one or more candidates left after disambiguation, return those.
*
*
*
*
*
* Implementation notes:
*
* For matching and disambiguating the requested values, we keep a table of values to avoid recomputing them. The table has one row for each candidate and one column for each attribute.
* The cells contain the values of the candidate for the given attribute. The first row contains the requested values. This table is packed into a single flat array in order to reduce
* memory usage and increase data locality.
*
* The information which candidates are compatible and which candidates are still valid during disambiguation is kept in two {@link BitSet}s. The nth bit is set if the nth candidate
* is compatible. The longest match is kept using two integers, one containing the length of the match, the other containing the index of the candidate that was the longest.
*
*
*/
class MultipleCandidateMatcher {
private final AttributeSelectionSchema schema;
private final ImmutableAttributes requested;
private final List extends T> candidates;
private final ImmutableAttributes[] candidateAttributeSets;
private final AttributeMatchingExplanationBuilder explanationBuilder;
private final List> requestedAttributes;
private final BitSet compatible;
private final Object[] requestedAttributeValues;
private int candidateWithLongestMatch;
private int lengthOfLongestMatch;
private BitSet remaining;
private Attribute>[] extraAttributes;
MultipleCandidateMatcher(AttributeSelectionSchema schema, Collection extends T> candidates, ImmutableAttributes requested, AttributeMatchingExplanationBuilder explanationBuilder) {
this.schema = schema;
this.requested = requested;
this.candidates = (candidates instanceof List) ? (List extends T>) candidates : ImmutableList.copyOf(candidates);
candidateAttributeSets = new ImmutableAttributes[candidates.size()];
this.explanationBuilder = explanationBuilder;
for (int i = 0; i < candidates.size(); i++) {
candidateAttributeSets[i] = ((AttributeContainerInternal) this.candidates.get(i).getAttributes()).asImmutable();
}
this.requestedAttributes = requested.keySet().asList();
requestedAttributeValues = new Object[(1 + candidates.size()) * requestedAttributes.size()];
compatible = new BitSet(candidates.size());
compatible.set(0, candidates.size());
}
public List getMatches() {
fillRequestedValues();
findCompatibleCandidates();
if (compatible.cardinality() <= 1) {
return getCandidates(compatible);
}
if (longestMatchIsSuperSetOfAllOthers()) {
T o = candidates.get(candidateWithLongestMatch);
explanationBuilder.candidateIsSuperSetOfAllOthers(o);
return Collections.singletonList(o);
}
return disambiguateCompatibleCandidates();
}
private void fillRequestedValues() {
for (int a = 0; a < requestedAttributes.size(); a++) {
Attribute> attribute = requestedAttributes.get(a);
AttributeValue> attributeValue = requested.findEntry(attribute);
setRequestedValue(a, attributeValue.isPresent() ? attributeValue.get() : null);
}
}
private void findCompatibleCandidates() {
if (requested.isEmpty()) {
// Avoid iterating on candidates if there's no requested attribute
return;
}
for (int c = 0; c < candidates.size(); c++) {
matchCandidate(c);
}
}
private void matchCandidate(int c) {
int matchLength = 0;
for (int a = 0; a < requestedAttributes.size(); a++) {
MatchResult result = recordAndMatchCandidateValue(c, a);
if (result == MatchResult.NO_MATCH) {
compatible.clear(c);
return;
}
if (result == MatchResult.MATCH) {
matchLength++;
}
}
if (matchLength > lengthOfLongestMatch) {
lengthOfLongestMatch = matchLength;
candidateWithLongestMatch = c;
}
}
private MatchResult recordAndMatchCandidateValue(int c, int a) {
Object requestedValue = getRequestedValue(a);
Attribute> attribute = requestedAttributes.get(a);
AttributeValue> candidateValue = candidateAttributeSets[c].findEntry(attribute.getName());
if (!candidateValue.isPresent()) {
setCandidateValue(c, a, null);
explanationBuilder.candidateAttributeMissing(candidates.get(c), attribute, requestedValue);
return MatchResult.MISSING;
}
Object coercedValue = candidateValue.coerce(attribute);
setCandidateValue(c, a, coercedValue);
if (schema.matchValue(attribute, requestedValue, coercedValue)) {
return MatchResult.MATCH;
}
explanationBuilder.candidateAttributeDoesNotMatch(candidates.get(c), attribute, requestedValue, candidateValue);
return MatchResult.NO_MATCH;
}
private boolean longestMatchIsSuperSetOfAllOthers() {
for (int c = compatible.nextSetBit(0); c >= 0; c = compatible.nextSetBit(c + 1)) {
if (c == candidateWithLongestMatch) {
continue;
}
int lengthOfOtherMatch = 0;
for (int a = 0; a < requestedAttributes.size(); a++) {
if (getCandidateValue(c, a) == null) {
continue;
}
lengthOfOtherMatch++;
if (getCandidateValue(candidateWithLongestMatch, a) == null) {
return false;
}
}
if (lengthOfOtherMatch == lengthOfLongestMatch) {
return false;
}
}
return true;
}
private List disambiguateCompatibleCandidates() {
remaining = new BitSet(candidates.size());
remaining.or(compatible);
disambiguateWithRequestedAttributeValues();
if (remaining.cardinality() > 1) {
disambiguateWithExtraAttributes();
}
if (remaining.cardinality() > 1) {
disambiguateWithRequestedAttributeKeys();
}
return remaining.cardinality() == 0 ? getCandidates(compatible) : getCandidates(remaining);
}
private void disambiguateWithRequestedAttributeKeys() {
if (requestedAttributes.isEmpty()) {
return;
}
for (Attribute> extraAttribute : extraAttributes) {
// We consider only extra attributes which are NOT on every candidate:
// Because they are EXTRA attributes, we consider that a
// candidate which does NOT provide this value is a better match
int candidateCount = candidateAttributeSets.length;
BitSet any = new BitSet(candidateCount);
for (int c = 0; c < candidateCount; c++) {
ImmutableAttributes candidateAttributeSet = candidateAttributeSets[c];
if (candidateAttributeSet.findEntry(extraAttribute.getName()).isPresent()) {
any.set(c);
}
}
if (any.cardinality() > 0 && any.cardinality() != candidateCount) {
// there is at least one candidate which does NOT provide this attribute
remaining.andNot(any);
if (remaining.cardinality() == 0) {
// there are no left candidate, do not bother checking other attributes
break;
}
}
}
}
private void disambiguateWithRequestedAttributeValues() {
for (int a = 0; a < requestedAttributes.size(); a++) {
disambiguateWithAttribute(a);
if (remaining.cardinality() == 0) {
return;
}
}
}
private void disambiguateWithAttribute(int a) {
Set