/* * 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; /** * Convenience class for use in tests to resolve a {@link Method} and/or any of * its {@link MethodParameter}s based on some hints. * *

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. * *

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: * *

 * ResolvableMethod resolvableMethod = ResolvableMethod.on(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();
 *
 * // ...
 * 
* *

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 targetClass; private String methodName; private Class[] argumentTypes; private ResolvableType returnType; private final List> annotationTypes = new ArrayList<>(4); private ResolvableMethod(Class targetClass) { this.targetClass = targetClass; } public ResolvableMethod name(String methodName) { this.methodName = methodName; return this; } public ResolvableMethod argumentTypes(Class... argumentTypes) { this.argumentTypes = argumentTypes; return this; } public ResolvableMethod returning(ResolvableType resolvableType) { this.returnType = resolvableType; return this; } public ResolvableMethod annotated(Class annotationType) { this.annotationTypes.add(annotationType); return this; } // Resolve methods public Method resolve() { Set methods = MethodIntrospector.selectMethods(this.targetClass, (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; } } if (!ObjectUtils.isEmpty(this.argumentTypes)) { if (!Arrays.equals(this.argumentTypes, method.getParameterTypes())) { return false; } } for (Class annotationType : this.annotationTypes) { if (AnnotationUtils.findAnnotation(method, annotationType) == null) { 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 MethodParameter resolveReturnType() { Method method = resolve(); return new MethodParameter(method, -1); } @SafeVarargs public final MethodParameter resolveParam(Predicate... predicates) { return resolveParam(null, predicates); } @SafeVarargs public final MethodParameter resolveParam(ResolvableType type, Predicate... predicates) { List 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.targetClass + ", name= " + this.methodName + ", returnType=" + this.returnType + ", annotations=" + this.annotationTypes; } public static ResolvableMethod on(Class clazz) { return new ResolvableMethod(clazz); } }