/*
* Copyright 2016-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.springframework.data.repository.core.support;
import static org.springframework.core.GenericTypeResolver.*;
import static org.springframework.util.ReflectionUtils.*;
import lombok.Value;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
/**
* This {@link RepositoryInformation} uses a {@link ConversionService} to check whether method arguments can be
* converted for invocation of implementation methods.
*
* @author Mark Paluch
* @author Oliver Gierke
* @since 2.0
*/
public class ReactiveRepositoryInformation extends DefaultRepositoryInformation {
/**
* Creates a new {@link ReactiveRepositoryInformation} for the given {@link RepositoryMetadata}, repository base
* class, custom implementation and {@link ConversionService}.
*
* @param metadata must not be {@literal null}.
* @param repositoryBaseClass must not be {@literal null}.
* @param customImplementationClass can be {@literal null}.
*/
public ReactiveRepositoryInformation(RepositoryMetadata metadata, Class<?> repositoryBaseClass,
Optional<Class<?>> customImplementationClass) {
super(metadata, repositoryBaseClass, customImplementationClass);
}
/**
* Returns the given target class' method if the given method (declared in the repository interface) was also declared
* at the target class. Returns the given method if the given base class does not declare the method given. Takes
* generics into account.
*
* @param method must not be {@literal null}.
* @param baseClass must not be {@literal null}.
* @return
*/
@Override
Method getTargetClassMethod(Method method, Optional<Class<?>> baseClass) {
Supplier<Optional<Method>> directMatch = () -> baseClass
.map(it -> findMethod(it, method.getName(), method.getParameterTypes()));
Supplier<Optional<Method>> detailedComparison = () -> baseClass.flatMap(it -> {
List<Supplier<Optional<Method>>> suppliers = new ArrayList<>();
if (usesParametersWithReactiveWrappers(method)) {
suppliers.add(() -> getMethodCandidate(method, it, assignableWrapperMatch())); //
suppliers.add(() -> getMethodCandidate(method, it, wrapperConversionMatch()));
}
suppliers.add(() -> getMethodCandidate(method, it, matchParameterOrComponentType(getRepositoryInterface())));
return Optionals.firstNonEmpty(Streamable.of(suppliers));
});
return Optionals.firstNonEmpty(directMatch, detailedComparison).orElse(method);
}
/**
* {@link Predicate} to check parameter assignability between a parameters in which the declared parameter may be
* wrapped but supports unwrapping. Usually types like {@link java.util.Optional} or {@link java.util.stream.Stream}.
*
* @param repositoryInterface
* @return
* @see QueryExecutionConverters
* @see #matchesGenericType
*/
private Predicate<ParameterOverrideCriteria> matchParameterOrComponentType(Class<?> repositoryInterface) {
return (parameterCriteria) -> {
Class<?> parameterType = resolveParameterType(parameterCriteria.getDeclared(), repositoryInterface);
Type genericType = parameterCriteria.getGenericBaseType();
if (genericType instanceof TypeVariable<?>) {
if (!matchesGenericType((TypeVariable<?>) genericType,
ResolvableType.forMethodParameter(parameterCriteria.getDeclared()))) {
return false;
}
}
return parameterCriteria.getBaseType().isAssignableFrom(parameterType)
&& parameterCriteria.isAssignableFromDeclared();
};
}
/**
* Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped.
*
* @param parameterType must not be {@literal null}.
* @return
*/
private static boolean isNonUnwrappingWrapper(Class<?> parameterType) {
Assert.notNull(parameterType, "Parameter type must not be null!");
return QueryExecutionConverters.supports(parameterType)
&& !QueryExecutionConverters.supportsUnwrapping(parameterType);
}
/**
* Returns whether the given {@link Method} uses a reactive wrapper type as parameter.
*
* @param method must not be {@literal null}.
* @return
*/
private static boolean usesParametersWithReactiveWrappers(Method method) {
Assert.notNull(method, "Method must not be null!");
return Arrays.stream(method.getParameterTypes())//
.anyMatch(ReactiveRepositoryInformation::isNonUnwrappingWrapper);
}
/**
* Returns a candidate method from the base class for the given one or the method given in the first place if none one
* the base class matches.
*
* @param method must not be {@literal null}.
* @param baseClass must not be {@literal null}.
* @param predicate must not be {@literal null}.
* @return
*/
private static Optional<Method> getMethodCandidate(Method method, Class<?> baseClass,
Predicate<ParameterOverrideCriteria> predicate) {
return Arrays.stream(baseClass.getMethods())//
.filter(it -> method.getName().equals(it.getName()))//
.filter(it -> method.getParameterCount() == it.getParameterCount())//
.filter(it -> parametersMatch(it, method, predicate))//
.findFirst();
}
/**
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
* against the ones bound in the given repository interface.
*
* @param baseClassMethod must not be {@literal null}.
* @param declaredMethod must not be {@literal null}.
* @param predicate must not be {@literal null}.
* @return
*/
private static boolean parametersMatch(Method baseClassMethod, Method declaredMethod,
Predicate<ParameterOverrideCriteria> predicate) {
return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate);
}
/**
* {@link Predicate} to check whether a method parameter is a {@link #isNonUnwrappingWrapper(Class)} and can be
* converted into a different wrapper. Usually {@link rx.Observable} to {@link org.reactivestreams.Publisher}
* conversion.
*
* @return
*/
private static Predicate<ParameterOverrideCriteria> wrapperConversionMatch() {
return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
&& isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
&& ReactiveWrapperConverters.canConvert(parameterCriteria.getDeclaredType(), parameterCriteria.getBaseType());
}
/**
* {@link Predicate} to check parameter assignability between a {@link #isNonUnwrappingWrapper(Class)} parameter and a
* declared parameter. Usually {@link reactor.core.publisher.Flux} vs. {@link org.reactivestreams.Publisher}
* conversion.
*
* @return
*/
private static Predicate<ParameterOverrideCriteria> assignableWrapperMatch() {
return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
&& isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
&& parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType());
}
private static Stream<ParameterOverrideCriteria> methodParameters(Method first, Method second) {
Assert.isTrue(first.getParameterCount() == second.getParameterCount(), "Method parameter count must be equal!");
return IntStream.range(0, first.getParameterCount()) //
.mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(first, index),
new MethodParameter(second, index)));
}
/**
* Criterion to represent {@link MethodParameter}s from a base method and its declared (overriden) method.
* <p>
* Method parameters indexes are correlated so {@link ParameterOverrideCriteria} applies only to methods with same
* parameter count.
*/
@Value(staticConstructor = "of")
private static class ParameterOverrideCriteria {
private final MethodParameter base;
private final MethodParameter declared;
/**
* @return base method parameter type.
*/
public Class<?> getBaseType() {
return base.getParameterType();
}
/**
* @return generic base method parameter type.
*/
public Type getGenericBaseType() {
return base.getGenericParameterType();
}
/**
* @return declared method parameter type.
*/
public Class<?> getDeclaredType() {
return declared.getParameterType();
}
public boolean isAssignableFromDeclared() {
return getBaseType().isAssignableFrom(getDeclaredType());
}
}
}