From fb2e79604859a5b6fbc9b1c491caee8c3a9dbdcc Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 2 Jul 2016 12:21:35 -0400 Subject: [PATCH] HandlerResult now requires MethodParameter as input HandlerAdapter's should always be able to provide a MethodParameter which in turn ensures that HandlerResultHandler's have full type information from method declarations. This commit also introduces ResolvableMethod for use in tests to make it easy to obtain MethodParameter return types. Issue: #128 --- .../web/reactive/HandlerResult.java | 16 +- .../reactive/result/SimpleHandlerAdapter.java | 22 ++- .../result/method/InvocableHandlerMethod.java | 4 +- .../annotation/ResponseBodyResultHandler.java | 9 +- .../view/ViewResolutionResultHandler.java | 17 +- .../web/reactive/result/ResolvableMethod.java | 141 ++++++++++++++++ .../result/SimpleResultHandlerTests.java | 85 ++++------ .../ResponseBodyResultHandlerTests.java | 4 +- .../ResponseEntityResultHandlerTests.java | 78 ++++++--- .../view/HttpMessageConverterViewTests.java | 21 ++- .../view/UrlBasedViewResolverTests.java | 2 - .../ViewResolutionResultHandlerTests.java | 155 +++++++++++------- .../view/freemarker/FreeMarkerViewTests.java | 12 +- 13 files changed, 381 insertions(+), 185 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 437283c22e..f4416eda0e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -21,6 +21,7 @@ import java.util.function.Function; import reactor.core.publisher.Mono; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; @@ -35,6 +36,7 @@ public class HandlerResult { private final Object handler; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private final Optional returnValue; private final ResolvableType returnType; @@ -50,7 +52,7 @@ public class HandlerResult { * @param returnValue the return value from the handler possibly {@code null} * @param returnType the return value type */ - public HandlerResult(Object handler, Object returnValue, ResolvableType returnType) { + public HandlerResult(Object handler, Object returnValue, MethodParameter returnType) { this(handler, returnValue, returnType, null); } @@ -61,12 +63,12 @@ public class HandlerResult { * @param returnType the return value type * @param model the model used for request handling */ - public HandlerResult(Object handler, Object returnValue, ResolvableType returnType, ModelMap model) { + public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, ModelMap model) { Assert.notNull(handler, "'handler' is required"); Assert.notNull(returnType, "'returnType' is required"); this.handler = handler; this.returnValue = Optional.ofNullable(returnValue); - this.returnType = returnType; + this.returnType = ResolvableType.forMethodParameter(returnType); this.model = (model != null ? model : new ExtendedModelMap()); } @@ -92,6 +94,14 @@ public class HandlerResult { return this.returnType; } + /** + * Return the {@link MethodParameter} from which + * {@link #getReturnType() returnType} was created. + */ + public MethodParameter getReturnTypeSource() { + return (MethodParameter) this.returnType.getSource(); + } + /** * Return the model used during request handling with attributes that may be * used to render HTML templates with. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java index a4836426e8..0eb5712649 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java @@ -16,14 +16,16 @@ package org.springframework.web.reactive.result; +import java.lang.reflect.Method; + import reactor.core.publisher.Mono; -import org.springframework.core.ResolvableType; +import org.springframework.core.MethodParameter; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; /** * HandlerAdapter that allows using the plain {@link WebHandler} contract with @@ -34,8 +36,18 @@ import org.springframework.web.server.ServerWebExchange; */ public class SimpleHandlerAdapter implements HandlerAdapter { - private static final ResolvableType MONO_VOID = ResolvableType.forClassWithGenerics( - Mono.class, Void.class); + private static final MethodParameter RETURN_TYPE; + + static { + try { + Method method = WebHandler.class.getMethod("handle", ServerWebExchange.class); + RETURN_TYPE = new MethodParameter(method, -1); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException( + "Failed to initialize the return type for WebHandler: " + ex.getMessage()); + } + } @Override @@ -47,7 +59,7 @@ public class SimpleHandlerAdapter implements HandlerAdapter { public Mono handle(ServerWebExchange exchange, Object handler) { WebHandler webHandler = (WebHandler) handler; Mono mono = webHandler.handle(exchange); - return Mono.just(new HandlerResult(webHandler, mono, MONO_VOID)); + return Mono.just(new HandlerResult(webHandler, mono, RETURN_TYPE)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index fc63515005..8cdd19af18 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -31,7 +31,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.core.ResolvableType; import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -90,8 +89,7 @@ public class InvocableHandlerMethod extends HandlerMethod { return resolveArguments(exchange, model, providedArgs).then(args -> { try { Object value = doInvoke(args); - ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); - HandlerResult handlerResult = new HandlerResult(this, value, type, model); + HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model); return Mono.just(handlerResult); } catch (InvocationTargetException ex) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 4ac01a55b3..0eb04f7bdb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -86,13 +86,8 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan @Override public boolean supports(HandlerResult result) { ResolvableType returnType = result.getReturnType(); - if (returnType.getSource() instanceof MethodParameter) { - MethodParameter parameter = (MethodParameter) returnType.getSource(); - if (hasResponseBodyAnnotation(parameter) && !isHttpEntityType(returnType)) { - return true; - } - } - return false; + MethodParameter parameter = result.getReturnTypeSource(); + return hasResponseBodyAnnotation(parameter) && !isHttpEntityType(returnType); } private boolean hasResponseBodyAnnotation(MethodParameter parameter) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 2d94119c82..f46c142128 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -38,7 +38,6 @@ import org.springframework.http.MediaType; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; @@ -151,13 +150,8 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler } private boolean hasModelAttributeAnnotation(HandlerResult result) { - if (result.getHandler() instanceof HandlerMethod) { - MethodParameter returnType = ((HandlerMethod) result.getHandler()).getReturnType(); - if (returnType.hasMethodAnnotation(ModelAttribute.class)) { - return true; - } - } - return false; + MethodParameter returnType = result.getReturnTypeSource(); + return returnType.hasMethodAnnotation(ModelAttribute.class); } private boolean isSupportedType(Class clazz) { @@ -263,14 +257,11 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler //noinspection unchecked result.getModel().addAllAttributes((Map) value); } - else if (result.getHandler() instanceof HandlerMethod) { - MethodParameter returnType = ((HandlerMethod) result.getHandler()).getReturnType(); + else { + MethodParameter returnType = result.getReturnTypeSource(); String name = getNameForReturnValue(value, returnType); result.getModel().addAttribute(name, value); } - else { - result.getModel().addAttribute(value); - } return value; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java new file mode 100644 index 0000000000..2a83857cf1 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java @@ -0,0 +1,141 @@ +/* + * 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.List; +import java.util.Set; + +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.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 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 returning(ResolvableType resolvableType) { + this.returnType = resolvableType; + return this; + } + + public ResolvableMethod annotated(Class annotationType) { + this.annotationTypes.add(annotationType); + return this; + } + + + public Method resolve() { + // String comparison (ResolvableType's with different providers) + String expected = this.returnType != null ? this.returnType.toString() : null; + + Set methods = MethodIntrospector.selectMethods(this.targetClass, + (ReflectionUtils.MethodFilter) method -> { + String actual = ResolvableType.forMethodReturnType(method).toString(); + if (this.methodName != null && !this.methodName.equals(method.getName())) { + return false; + } + if (expected != null) { + if (!actual.equals(expected) && !Object.class.equals(method.getDeclaringClass())) { + 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(); + } + + public MethodParameter resolveReturnType() { + Method method = resolve(); + return new MethodParameter(method, -1); + } + + + @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); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java index 1256f96bd4..0ebeddfa67 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java @@ -18,21 +18,21 @@ package org.springframework.web.reactive.result; import java.util.concurrent.CompletableFuture; +import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import rx.Observable; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; /** * Unit tests for {@link SimpleResultHandler}. @@ -41,72 +41,55 @@ import static org.junit.Assert.assertTrue; */ public class SimpleResultHandlerTests { - @Test - public void supportsWithConversionService() throws NoSuchMethodException { + private SimpleResultHandler resultHandler; + + @Before + public void setUp() throws Exception { GenericConversionService conversionService = new GenericConversionService(); conversionService.addConverter(new MonoToCompletableFutureConverter()); conversionService.addConverter(new PublisherToFluxConverter()); conversionService.addConverter(new ReactorToRxJava1Converter()); - - SimpleResultHandler resultHandler = new SimpleResultHandler(conversionService); - TestController controller = new TestController(); - - HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); - ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); + this.resultHandler = new SimpleResultHandler(conversionService); } - private HandlerResult createHandlerResult(HandlerMethod hm, ResolvableType type) { - return new HandlerResult(hm, null, type); + + @Test + public void supportsWithConversionService() throws NoSuchMethodException { + testSupports(ResolvableType.forClass(void.class), true); + testSupports(ResolvableType.forClassWithGenerics(Publisher.class, Void.class), true); + testSupports(ResolvableType.forClassWithGenerics(Flux.class, Void.class), true); + testSupports(ResolvableType.forClassWithGenerics(Observable.class, Void.class), true); + testSupports(ResolvableType.forClassWithGenerics(CompletableFuture.class, Void.class), true); + + testSupports(ResolvableType.forClass(String.class), false); + testSupports(ResolvableType.forClassWithGenerics(Publisher.class, String.class), false); + } + + private void testSupports(ResolvableType type, boolean result) { + MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType(); + HandlerResult handlerResult = new HandlerResult(new TestController(), null, param); + assertEquals(result, this.resultHandler.supports(handlerResult)); } @SuppressWarnings("unused") private static class TestController { - public void voidReturnValue() { - } + public void voidReturn() { } - public Publisher publisherString() { - return null; - } + public Publisher publisherString() { return null; } - public Publisher publisherVoid() { - return null; - } + public Flux flux() { return null; } - public Flux streamVoid() { - return null; - } + public Observable observable() { return null; } - public Observable observableVoid() { - return null; - } + public CompletableFuture completableFuture() { return null; } + + public String string() { return null; } + + public Publisher publisher() { return null; } - public CompletableFuture completableFutureVoid() { - return null; - } } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index e99454f126..38da2644c9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -25,7 +25,6 @@ import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; -import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; @@ -123,8 +122,7 @@ public class ResponseBodyResultHandlerTests { private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException { HandlerMethod hm = handlerMethod(controller, method); - ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - HandlerResult handlerResult = new HandlerResult(hm, null, type, new ExtendedModelMap()); + HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); assertEquals(result, this.resultHandler.supports(handlerResult)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index e65e867a2c..c90ddb8334 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -29,6 +29,7 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import rx.Single; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; @@ -48,12 +49,11 @@ import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.ui.ExtendedModelMap; -import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; @@ -75,9 +75,6 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics; */ public class ResponseEntityResultHandlerTests { - private static final Object HANDLER = new Object(); - - private ResponseEntityResultHandler resultHandler; private MockServerHttpResponse response = new MockServerHttpResponse(); @@ -119,23 +116,22 @@ public class ResponseEntityResultHandlerTests { @Test @SuppressWarnings("ConstantConditions") public void supports() throws NoSuchMethodException { - ModelMap model = new ExtendedModelMap(); Object value = null; - ResolvableType type = responseEntityType(String.class); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + ResolvableType type = responseEntity(String.class); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = forClassWithGenerics(Mono.class, responseEntityType(String.class)); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + type = classWithGenerics(Mono.class, responseEntity(String.class)); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = forClassWithGenerics(Single.class, responseEntityType(String.class)); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + type = classWithGenerics(Single.class, responseEntity(String.class)); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = forClassWithGenerics(CompletableFuture.class, responseEntityType(String.class)); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + type = classWithGenerics(CompletableFuture.class, responseEntity(String.class)); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); type = ResolvableType.forClass(String.class); - assertFalse(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + assertFalse(this.resultHandler.supports(handlerResult(value, type))); } @Test @@ -145,8 +141,9 @@ public class ResponseEntityResultHandlerTests { @Test public void statusCode() throws Exception { - ResolvableType type = responseEntityType(Void.class); - HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.noContent().build(), type); + ResponseEntity value = ResponseEntity.noContent().build(); + ResolvableType type = responseEntity(Void.class); + HandlerResult result = handlerResult(value, type); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.NO_CONTENT, this.response.getStatus()); @@ -157,8 +154,9 @@ public class ResponseEntityResultHandlerTests { @Test public void headers() throws Exception { URI location = new URI("/path"); - ResolvableType type = responseEntityType(Void.class); - HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.created(location).build(), type); + ResolvableType type = responseEntity(Void.class); + ResponseEntity value = ResponseEntity.created(location).build(); + HandlerResult result = handlerResult(value, type); this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.CREATED, this.response.getStatus()); @@ -170,25 +168,25 @@ public class ResponseEntityResultHandlerTests { @Test public void handleReturnTypes() throws Exception { Object returnValue = ResponseEntity.ok("abc"); - ResolvableType returnType = responseEntityType(String.class); + ResolvableType returnType = responseEntity(String.class); testHandle(returnValue, returnType); returnValue = Mono.just(ResponseEntity.ok("abc")); - returnType = forClassWithGenerics(Mono.class, responseEntityType(String.class)); + returnType = forClassWithGenerics(Mono.class, responseEntity(String.class)); testHandle(returnValue, returnType); returnValue = Mono.just(ResponseEntity.ok("abc")); - returnType = forClassWithGenerics(Single.class, responseEntityType(String.class)); + returnType = forClassWithGenerics(Single.class, responseEntity(String.class)); testHandle(returnValue, returnType); returnValue = Mono.just(ResponseEntity.ok("abc")); - returnType = forClassWithGenerics(CompletableFuture.class, responseEntityType(String.class)); + returnType = forClassWithGenerics(CompletableFuture.class, responseEntity(String.class)); testHandle(returnValue, returnType); } - private void testHandle(Object returnValue, ResolvableType returnType) { - HandlerResult result = new HandlerResult(HANDLER, returnValue, returnType); + private void testHandle(Object returnValue, ResolvableType type) { + HandlerResult result = handlerResult(returnValue, type); this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.OK, this.response.getStatus()); @@ -197,8 +195,17 @@ public class ResponseEntityResultHandlerTests { } - private ResolvableType responseEntityType(Class bodyType) { - return forClassWithGenerics(ResponseEntity.class, bodyType); + private ResolvableType responseEntity(Class bodyType) { + return classWithGenerics(ResponseEntity.class, ResolvableType.forClass(bodyType)); + } + + private ResolvableType classWithGenerics(Class sourceType, ResolvableType genericType) { + return ResolvableType.forClassWithGenerics(sourceType, genericType); + } + + private HandlerResult handlerResult(Object returnValue, ResolvableType type) { + MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType(); + return new HandlerResult(new TestController(), returnValue, param); } private void assertResponseBody(String responseBody) { @@ -207,4 +214,21 @@ public class ResponseEntityResultHandlerTests { DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } + + @SuppressWarnings("unused") + private static class TestController { + + ResponseEntity responseEntityString() { return null; } + + ResponseEntity responseEntityVoid() { return null; } + + Mono> mono() { return null; } + + Single> single() { return null; } + + CompletableFuture> completableFuture() { return null; } + + String string() { return null; } + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java index 0f6cc3a3a0..4fb874abc5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -31,19 +31,20 @@ import org.junit.Before; import org.junit.Test; import reactor.core.test.TestSubscriber; -import org.springframework.core.ResolvableType; -import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.http.codec.xml.Jaxb2Encoder; +import org.springframework.core.MethodParameter; import org.springframework.core.codec.StringEncoder; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.MimeType; import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; @@ -62,18 +63,17 @@ import static org.junit.Assert.fail; */ public class HttpMessageConverterViewTests { - private HttpMessageConverterView view; + private HttpMessageConverterView view = new HttpMessageConverterView(new JacksonJsonEncoder()); private HandlerResult result; - private ModelMap model; + private ModelMap model = new ExtendedModelMap(); @Before public void setup() throws Exception { - this.view = new HttpMessageConverterView(new JacksonJsonEncoder()); - this.model = new ExtendedModelMap(); - this.result = new HandlerResult(new Object(), null, ResolvableType.NONE, model); + MethodParameter param = ResolvableMethod.on(this.getClass()).name("handle").resolveReturnType(); + this.result = new HandlerResult(this, null, param, this.model); } @@ -176,4 +176,9 @@ public class HttpMessageConverterViewTests { } + @SuppressWarnings("unused") + private String handle() { + return null; + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java index 98d16dc17a..11d61c1dcf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java @@ -19,11 +19,9 @@ import java.util.Locale; import java.util.Map; import org.junit.Test; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.context.support.StaticApplicationContext; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.web.server.ServerWebExchange; import static org.junit.Assert.assertNotNull; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index f8f1276142..4f8b7ed511 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import rx.Single; +import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.ConfigurableConversionService; @@ -49,13 +50,12 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; @@ -90,26 +90,19 @@ public class ViewResolutionResultHandlerTests { @Test public void supports() throws Exception { - Object handler = new Object(); - HandlerMethod hm = handlerMethod(new TestController(), "modelAttributeMethod"); - testSupports(handler, ResolvableType.forClass(String.class), true); - testSupports(handler, ResolvableType.forClass(View.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Mono.class, String.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Mono.class, View.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Single.class, String.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Single.class, View.class), true); - testSupports(handler, ResolvableType.forClass(Model.class), true); - testSupports(handler, ResolvableType.forClass(Map.class), true); - testSupports(handler, ResolvableType.forClass(TestBean.class), true); - testSupports(handler, ResolvableType.forClass(Integer.class), false); - testSupports(hm, ResolvableType.forMethodParameter(hm.getReturnType()), true); - } + testSupports(ResolvableType.forClass(String.class), true); + testSupports(ResolvableType.forClass(View.class), true); + testSupports(ResolvableType.forClassWithGenerics(Mono.class, String.class), true); + testSupports(ResolvableType.forClassWithGenerics(Mono.class, View.class), true); + testSupports(ResolvableType.forClassWithGenerics(Single.class, String.class), true); + testSupports(ResolvableType.forClassWithGenerics(Single.class, View.class), true); + testSupports(ResolvableType.forClass(Model.class), true); + testSupports(ResolvableType.forClass(Map.class), true); + testSupports(ResolvableType.forClass(TestBean.class), true); + testSupports(ResolvableType.forClass(Integer.class), false); - private void testSupports(Object handler, ResolvableType returnType, boolean result) { - ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class)); - HandlerResult handlerResult = new HandlerResult(handler, null, returnType, new ExtendedModelMap()); - assertEquals(result, resultHandler.supports(handlerResult)); + testSupports(resolvableMethod().annotated(ModelAttribute.class), true); } @Test @@ -125,53 +118,50 @@ public class ViewResolutionResultHandlerTests { @Test public void handleReturnValueTypes() throws Exception { - Object handler = new Object(); Object returnValue; ResolvableType returnType; ViewResolver resolver = new TestViewResolver("account"); - returnValue = new TestView("account"); returnType = ResolvableType.forClass(View.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}"); + returnValue = new TestView("account"); + testHandle("/path", returnType, returnValue, "account: {id=123}"); - returnValue = Mono.just(new TestView("account")); returnType = ResolvableType.forClassWithGenerics(Mono.class, View.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}"); + returnValue = Mono.just(new TestView("account")); + testHandle("/path", returnType, returnValue, "account: {id=123}"); - returnValue = "account"; returnType = ResolvableType.forClass(String.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}", resolver); + returnValue = "account"; + testHandle("/path", returnType, returnValue, "account: {id=123}", resolver); - returnValue = Mono.just("account"); returnType = ResolvableType.forClassWithGenerics(Mono.class, String.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}", resolver); + returnValue = Mono.just("account"); + testHandle("/path", returnType, returnValue, "account: {id=123}", resolver); - returnValue = new ExtendedModelMap().addAttribute("name", "Joe"); returnType = ResolvableType.forClass(Model.class); - testHandle("/account", handler, returnValue, returnType, "account: {id=123, name=Joe}", resolver); + returnValue = new ExtendedModelMap().addAttribute("name", "Joe"); + testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver); - returnValue = Collections.singletonMap("name", "Joe"); returnType = ResolvableType.forClass(Map.class); - testHandle("/account", handler, returnValue, returnType, "account: {id=123, name=Joe}", resolver); + returnValue = Collections.singletonMap("name", "Joe"); + testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver); - HandlerMethod hm = handlerMethod(new TestController(), "modelAttributeMethod"); - returnValue = "Joe"; - returnType = ResolvableType.forMethodParameter(hm.getReturnType()); - testHandle("/account", hm, returnValue, returnType, "account: {id=123, name=Joe}", resolver); - - returnValue = new TestBean("Joe"); returnType = ResolvableType.forClass(TestBean.class); - testHandle("/account", handler, returnValue, returnType, "account: {id=123, testBean=TestBean[name=Joe]}", resolver); + returnValue = new TestBean("Joe"); + String responseBody = "account: {id=123, testBean=TestBean[name=Joe]}"; + testHandle("/account", returnType, returnValue, responseBody, resolver); + + testHandle("/account", resolvableMethod().annotated(ModelAttribute.class), + 99L, "account: {id=123, num=99}", resolver); } @Test public void handleWithMultipleResolvers() throws Exception { - Object handler = new Object(); Object returnValue = "profile"; ResolvableType returnType = ResolvableType.forClass(String.class); ViewResolver[] resolvers = {new TestViewResolver("account"), new TestViewResolver("profile")}; - testHandle("/account", handler, returnValue, returnType, "profile: {id=123}", resolvers); + testHandle("/account", returnType, returnValue, "profile: {id=123}", resolvers); } @Test @@ -180,11 +170,11 @@ public class ViewResolutionResultHandlerTests { testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, String.class)); } - private void testDefaultViewName(Object returnValue, ResolvableType returnType) + private void testDefaultViewName(Object returnValue, ResolvableType type) throws URISyntaxException { ModelMap model = new ExtendedModelMap().addAttribute("id", "123"); - HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), model); ViewResolutionResultHandler handler = createResultHandler(new TestViewResolver("account")); this.request.setUri(new URI("/account")); @@ -203,9 +193,9 @@ public class ViewResolutionResultHandlerTests { @Test public void unresolvedViewName() throws Exception { String returnValue = "account"; - ResolvableType returnType = ResolvableType.forClass(String.class); + ResolvableType type = ResolvableType.forClass(String.class); ExtendedModelMap model = new ExtendedModelMap(); - HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType, model); + HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType(type), model); this.request.setUri(new URI("/path")); Mono mono = createResultHandler().handleResult(this.exchange, handlerResult); @@ -217,7 +207,8 @@ public class ViewResolutionResultHandlerTests { public void contentNegotiation() throws Exception { TestBean value = new TestBean("Joe"); ResolvableType type = ResolvableType.forClass(TestBean.class); - HandlerResult handlerResult = new HandlerResult(new Object(), value, type, new ExtendedModelMap()); + ExtendedModelMap model = new ExtendedModelMap(); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), model); this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); this.request.setUri(new URI("/account")); @@ -236,7 +227,8 @@ public class ViewResolutionResultHandlerTests { public void contentNegotiationWith406() throws Exception { TestBean value = new TestBean("Joe"); ResolvableType type = ResolvableType.forClass(TestBean.class); - HandlerResult handlerResult = new HandlerResult(new Object(), value, type, new ExtendedModelMap()); + ExtendedModelMap model = new ExtendedModelMap(); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), model); this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); this.request.setUri(new URI("/account")); @@ -247,6 +239,26 @@ public class ViewResolutionResultHandlerTests { } + private MethodParameter returnType(ResolvableType type) { + return resolvableMethod().returning(type).resolveReturnType(); + } + + private ResolvableMethod resolvableMethod() { + return ResolvableMethod.on(TestController.class); + } + + private void testSupports(ResolvableType type, boolean result) { + testSupports(resolvableMethod().returning(type), result); + } + + private void testSupports(ResolvableMethod resolvableMethod, boolean result) { + ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class)); + MethodParameter returnType = resolvableMethod.resolveReturnType(); + ExtendedModelMap model = new ExtendedModelMap(); + HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, model); + assertEquals(result, resultHandler.supports(handlerResult)); + } + private ViewResolutionResultHandler createResultHandler(ViewResolver... resolvers) { return createResultHandler(Collections.emptyList(), resolvers); } @@ -261,15 +273,18 @@ public class ViewResolutionResultHandlerTests { return handler; } - private HandlerMethod handlerMethod(Object controller, String method) throws NoSuchMethodException { - return new HandlerMethod(controller, controller.getClass().getMethod(method)); + private void testHandle(String path, ResolvableType returnType, Object returnValue, + String responseBody, ViewResolver... resolvers) throws URISyntaxException { + + testHandle(path, resolvableMethod().returning(returnType), returnValue, responseBody, resolvers); } - private void testHandle(String path, Object handler, Object returnValue, ResolvableType returnType, + private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue, String responseBody, ViewResolver... resolvers) throws URISyntaxException { ModelMap model = new ExtendedModelMap().addAttribute("id", "123"); - HandlerResult result = new HandlerResult(handler, returnValue, returnType, model); + MethodParameter returnType = resolvableMethod.resolveReturnType(); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model); this.request.setUri(new URI(path)); createResultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertResponseBody(responseBody); @@ -350,15 +365,6 @@ public class ViewResolutionResultHandlerTests { } } - @Controller @SuppressWarnings("unused") - private static class TestController { - - @ModelAttribute("name") - public String modelAttributeMethod() { - return null; - } - } - private static class TestBean { private final String name; @@ -377,4 +383,31 @@ public class ViewResolutionResultHandlerTests { } } + @SuppressWarnings("unused") + private static class TestController { + + String string() { return null; } + + View view() { return null; } + + Mono monoString() { return null; } + + Mono monoView() { return null; } + + Single singleString() { return null; } + + Single singleView() { return null; } + + Model model() { return null; } + + Map map() { return null; } + + TestBean testBean() { return null; } + + Integer integer() { return null; } + + @ModelAttribute("num") + Long longAttribute() { return null; } + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index 5407b2822b..fac9daa7ff 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -29,13 +29,14 @@ import reactor.core.test.TestSubscriber; import org.springframework.context.ApplicationContextException; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.ResolvableType; +import org.springframework.core.MethodParameter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; @@ -125,7 +126,8 @@ public class FreeMarkerViewTests { ModelMap model = new ExtendedModelMap(); model.addAttribute("hello", "hi FreeMarker"); - HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model); + MethodParameter returnType = new MethodParameter(getClass().getDeclaredMethod("handle"), -1); + HandlerResult result = new HandlerResult(new Object(), "", returnType, model); view.render(result, null, this.exchange); TestSubscriber @@ -142,4 +144,10 @@ public class FreeMarkerViewTests { return new String(bytes, UTF_8); } + + @SuppressWarnings("unused") + private String handle() { + return null; + } + }