/*
* Copyright 2002-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.springframework.web.reactive.result;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
/**
* Convenience class for use in tests to resolve a {@link Method} and/or any of
* its {@link MethodParameter}s based on some hints.
*
* <p>In tests we often create a class (e.g. TestController) with diverse method
* signatures and annotations to test with. Use of descriptive method and argument
* names combined with using reflection, it becomes challenging to read and write
* tests and it becomes necessary to navigate to the actual method declaration
* which is cumbersome and involves several steps.
*
* <p>The idea here is to provide enough hints to resolving a method uniquely
* where the hints document exactly what is being tested and there is usually no
* need to navigate to the actual method declaration. For example if testing
* response handling, the return type may be used as a hint:
*
* <pre>
* ResolvableMethod resolvableMethod = ResolvableMethod.onClass(TestController.class);
* ResolvableType type = ResolvableType.forClassWithGenerics(Mono.class, View.class);
* Method method = resolvableMethod.returning(type).resolve();
*
* type = ResolvableType.forClassWithGenerics(Mono.class, String.class);
* method = resolvableMethod.returning(type).resolve();
*
* // ...
* </pre>
*
* <p>Additional {@code resolve} methods provide options to obtain one of the method
* arguments or return type as a {@link MethodParameter}.
*
* @author Rossen Stoyanchev
*/
public class ResolvableMethod {
private final Class<?> objectClass;
private final Object object;
private String methodName;
private Class<?>[] argumentTypes;
private ResolvableType returnType;
private final List<Class<? extends Annotation>> annotationTypes = new ArrayList<>(4);
private final List<Predicate<Method>> predicates = new ArrayList<>(4);
private ResolvableMethod(Class<?> objectClass) {
Assert.notNull(objectClass);
this.objectClass = objectClass;
this.object = null;
}
private ResolvableMethod(Object object) {
Assert.notNull(object);
this.object = object;
this.objectClass = object.getClass();
}
/**
* Methods that match the given name (regardless of arguments).
*/
public ResolvableMethod name(String methodName) {
this.methodName = methodName;
return this;
}
/**
* Methods that match the given argument types.
*/
public ResolvableMethod argumentTypes(Class<?>... argumentTypes) {
this.argumentTypes = argumentTypes;
return this;
}
/**
* Methods declared to return the given type.
*/
public ResolvableMethod returning(ResolvableType resolvableType) {
this.returnType = resolvableType;
return this;
}
/**
* Methods with the given annotation.
*/
public ResolvableMethod annotated(Class<? extends Annotation> annotationType) {
this.annotationTypes.add(annotationType);
return this;
}
/**
* Methods matching the given predicate.
*/
public final ResolvableMethod matching(Predicate<Method> methodPredicate) {
this.predicates.add(methodPredicate);
return this;
}
// Resolve methods
public Method resolve() {
Set<Method> methods = MethodIntrospector.selectMethods(this.objectClass,
(ReflectionUtils.MethodFilter) method -> {
if (this.methodName != null && !this.methodName.equals(method.getName())) {
return false;
}
if (getReturnType() != null) {
// String comparison (ResolvableType's with different providers)
String actual = ResolvableType.forMethodReturnType(method).toString();
if (!actual.equals(getReturnType()) && !Object.class.equals(method.getDeclaringClass())) {
return false;
}
}
else if (!ObjectUtils.isEmpty(this.argumentTypes)) {
if (!Arrays.equals(this.argumentTypes, method.getParameterTypes())) {
return false;
}
}
else if (this.annotationTypes.stream()
.filter(annotType -> AnnotationUtils.findAnnotation(method, annotType) == null)
.findFirst()
.isPresent()) {
return false;
}
else if (this.predicates.stream().filter(p -> !p.test(method)).findFirst().isPresent()) {
return false;
}
return true;
});
Assert.isTrue(!methods.isEmpty(), "No matching method: " + this);
Assert.isTrue(methods.size() == 1, "Multiple matching methods: " + this);
return methods.iterator().next();
}
private String getReturnType() {
return this.returnType != null ? this.returnType.toString() : null;
}
public InvocableHandlerMethod resolveHandlerMethod() {
Assert.notNull(this.object);
return new InvocableHandlerMethod(this.object, resolve());
}
public MethodParameter resolveReturnType() {
Method method = resolve();
return new MethodParameter(method, -1);
}
@SafeVarargs
public final MethodParameter resolveParam(Predicate<MethodParameter>... predicates) {
return resolveParam(null, predicates);
}
@SafeVarargs
public final MethodParameter resolveParam(ResolvableType type,
Predicate<MethodParameter>... predicates) {
List<MethodParameter> matches = new ArrayList<>();
Method method = resolve();
for (int i = 0; i < method.getParameterCount(); i++) {
MethodParameter param = new MethodParameter(method, i);
if (type != null) {
if (!ResolvableType.forMethodParameter(param).toString().equals(type.toString())) {
continue;
}
}
if (!ObjectUtils.isEmpty(predicates)) {
if (Arrays.stream(predicates).filter(p -> !p.test(param)).findFirst().isPresent()) {
continue;
}
}
matches.add(param);
}
Assert.isTrue(!matches.isEmpty(), "No matching arg on " + method.toString());
Assert.isTrue(matches.size() == 1, "Multiple matching args: " + matches + " on " + method.toString());
return matches.get(0);
}
@Override
public String toString() {
return "Class=" + this.objectClass +
", name=" + (this.methodName != null ? this.methodName : "<not specified>") +
", returnType=" + (this.returnType != null ? this.returnType : "<not specified>") +
", annotations=" + this.annotationTypes;
}
public static ResolvableMethod onClass(Class<?> clazz) {
return new ResolvableMethod(clazz);
}
public static ResolvableMethod on(Object object) {
return new ResolvableMethod(object);
}
}