
io.micronaut.inject.qualifiers.MatchArgumentQualifier Maven / Gradle / Ivy
/*
* Copyright 2017-2024 original 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
*
* https://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 io.micronaut.inject.qualifiers;
import io.micronaut.context.Qualifier;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.BeanType;
import org.slf4j.Logger;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* A {@link Qualifier} that filters beans according to the type arguments.
*
* @param The type
* @author Denis Stepanov
* @since 4.6
*/
@Internal
public final class MatchArgumentQualifier implements Qualifier {
private static final Logger LOG = ClassUtils.getLogger(MatchArgumentQualifier.class);
private final Argument> argument;
private final Argument> covariantArgument;
private MatchArgumentQualifier(Argument> argument, Argument> covariantArgument) {
this.argument = argument;
this.covariantArgument = covariantArgument;
}
public static MatchArgumentQualifier ofArgument(Argument> argument) {
return new MatchArgumentQualifier<>(
argument,
null
);
}
/**
* Finds matches of a type with a covariant generic type (types that extend the type or are equal to it).
* The generic argument is assignable from the candidate generic type.
* Use-cases are generic deserializers, readers.
*
*
* Java example:
* {@code MyReader> candidate = ...; }
* {@code MyReader extends List> aMatch = candidate; }
*
*
* @param beanType The type of the beans
* @param genericArgument The generic argument of the bean type
* @param The bean type
* @return The qualifier
*/
@NonNull
public static MatchArgumentQualifier covariant(@NonNull Class beanType, @NonNull Argument> genericArgument) {
Argument> covariantArgument = Argument.ofTypeVariable(genericArgument.getType(), null, genericArgument.getAnnotationMetadata(), genericArgument.getTypeParameters());
return new MatchArgumentQualifier<>(
Argument.of(beanType, covariantArgument),
covariantArgument
);
}
/**
* Finds matches of a type with a contravariant generic type (types that is a super type or are equal to it).
* The candidate generic type is assignable from the generic argument.
* Use-cases are generic serializers, writers.
*
*
* Java example:
* {@code MyWriter candidate = ...; }
* {@code MyWriter super CharSequence> aMatch = candidate; }
*
*
* @param beanType The type of the beans
* @param genericArgument The generic argument of the bean type
* @param The bean type
* @return The qualifier
*/
@NonNull
public static MatchArgumentQualifier contravariant(@NonNull Class beanType, @NonNull Argument> genericArgument) {
return new MatchArgumentQualifier<>(
Argument.of(beanType, Argument.ofTypeVariable(genericArgument.getType(), null, genericArgument.getAnnotationMetadata(), genericArgument.getTypeParameters())),
null
);
}
@Override
public > Stream reduce(Class beanType, Stream candidates) {
return filter(beanType, candidates.toList()).stream();
}
@Override
public boolean doesQualify(Class beanType, Collection extends BeanType> candidates) {
return !filter(beanType, candidates).isEmpty();
}
@Override
public boolean doesQualify(Class beanType, BeanType candidate) {
throw new IllegalStateException("Not supported!");
}
@Override
public > Collection filter(Class beanType, Collection candidates) {
return filterArgumentTypeParameters(argument, candidates, null);
}
private boolean matchesArgumentTypeParameters(Argument> argument, Argument> candidateArgument) {
Argument>[] argumentTypeParameters = argument.getTypeParameters();
Argument>[] candidateTypeParameters = candidateArgument.getTypeParameters();
if (argumentTypeParameters.length != candidateTypeParameters.length) {
if (argumentTypeParameters.length == 0) {
for (Argument> candidateTypeParameter : candidateTypeParameters) {
if (!candidateTypeParameter.getType().equals(Object.class)) {
return false;
}
}
return true;
}
return false;
}
for (int i = 0; i < candidateTypeParameters.length; i++) {
Argument> typeParameter = argumentTypeParameters[i];
Argument> candidateTypeParameter = candidateTypeParameters[i];
if (!doesMatch(candidateTypeParameter, candidateTypeParameter.getType(), typeParameter, typeParameter.getType())) {
return false;
}
}
return true;
}
private > Collection filterArgumentTypeParameters(Argument> argument,
Collection result,
Function> typeArgumentExtractor) {
Argument>[] typeParameters = argument.getTypeParameters();
for (int i = 0; i < typeParameters.length; i++) {
int finalI = i;
Argument> typeParameter = typeParameters[i];
Function> getCandidateArgumentFunction = bd -> {
if (typeArgumentExtractor == null) {
if (bd instanceof BeanDefinition> beanDefinition) {
List> typeArguments = beanDefinition.getTypeArguments(argument.getType());
if (finalI < typeArguments.size()) {
return typeArguments.get(finalI);
}
}
return null;
}
Argument> apply = typeArgumentExtractor.apply(bd);
Argument>[] candidateTypeParameters = apply.getTypeParameters();
if (finalI < candidateTypeParameters.length) {
return candidateTypeParameters[finalI];
}
return Argument.OBJECT_ARGUMENT;
};
result = filterMatching(typeParameter, result, getCandidateArgumentFunction);
result = filterArgumentTypeParameters(typeParameter, result, getCandidateArgumentFunction);
}
return result;
}
private > List filterMatching(Argument> argument,
Collection candidates,
Function> typeArgumentExtractor) {
List selectedDirect = null;
boolean directMatch = false;
List, List>> closestMatches = null;
candidatesForLoop:
for (BT candidate : candidates) {
Class> argumentTypeClass = argument.getType();
if (argumentTypeClass.isPrimitive()) {
argumentTypeClass = ReflectionUtils.getWrapperType(argumentTypeClass);
}
Argument> candidateArgument = typeArgumentExtractor.apply(candidate);
if (candidateArgument == null) {
continue;
}
Class> candidateType = candidateArgument.getType();
if (argumentTypeClass.equals(candidateType)) {
if (!matchesArgumentTypeParameters(argument, candidateArgument)) {
// Eliminate a candidate that doesn't fully match
reject(argument, candidate);
continue;
}
if (!directMatch) {
selectedDirect = new ArrayList<>(3);
closestMatches = null;
directMatch = true;
}
selectedDirect.add(candidate);
continue;
}
if (directMatch) {
// After direct match found ignore all non-direct
reject(argument, candidate);
continue;
}
if (!doesMatch(candidateArgument, candidateType, argument, argumentTypeClass)) {
reject(argument, candidate);
continue;
}
if (closestMatches != null) {
// Compare the candidate with previous matches, possibly eliminating some of them
for (Iterator, List>> iterator = closestMatches.iterator(); iterator.hasNext(); ) {
Map.Entry, List> e = iterator.next();
Class> closestMatch = e.getKey();
List selected = e.getValue();
if (closestMatch.equals(candidateType)) {
// Same type as found before - also select this type
selected.add(candidate);
continue candidatesForLoop;
} else if (closestMatch.isAssignableFrom(candidateType)) {
// We found more close type - disregard previous selection
iterator.remove();
if (LOG.isTraceEnabled()) {
for (BT bt : selected) {
reject(argument, bt);
}
}
ArrayList newSelected = new ArrayList<>();
newSelected.add(candidate);
closestMatches.add(new AbstractMap.SimpleEntry<>(candidateType, newSelected));
continue candidatesForLoop;
} else if (candidateType.isAssignableFrom(closestMatch)) {
// Previous match is much closer
reject(argument, candidate);
continue candidatesForLoop;
}
}
} else {
closestMatches = new ArrayList<>();
}
ArrayList newSelected = new ArrayList<>();
newSelected.add(candidate);
closestMatches.add(new AbstractMap.SimpleEntry<>(candidateType, newSelected));
}
if (directMatch) {
return selectedDirect;
}
if (closestMatches != null) {
if (closestMatches.size() == 1) {
return closestMatches.iterator().next().getValue();
}
List result = new ArrayList<>();
for (Map.Entry, List> match : closestMatches) {
result.addAll(match.getValue());
}
return result;
}
return List.of();
}
private boolean doesMatch(Argument> candidateArgument,
Class> candidateType,
Argument> argument,
Class> argumentType) {
if (candidateType.equals(argumentType)) {
return true;
}
if (candidateType.equals(Enum.class)) {
// Avoid checking generic types for enums
return candidateType.isAssignableFrom(argumentType);
}
if (argument.isTypeVariable()) {
// Is compatible?
if (argument == covariantArgument) {
if (candidateArgument.isTypeVariable() && candidateType.isAssignableFrom(argumentType)) {
// Defined a type variable
return true;
}
return argumentType.isAssignableFrom(candidateType);
} else {
return candidateType.isAssignableFrom(argumentType);
}
}
if (candidateType.equals(Object.class)) {
return true;
}
if (!candidateArgument.isTypeVariable()) {
// Defined as a direct type
return false;
}
// Is compatible?
return candidateType.isAssignableFrom(argumentType);
}
private static > void reject(Argument> argument, BT candidate) {
if (LOG.isTraceEnabled()) {
LOG.trace("Bean candidate {} is not compatible with an argument {}", candidate, argument);
}
}
@Override
public String toString() {
return "Matches [" + argument.toString() + "]";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy