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 437283c22e9..f4416eda0ee 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 a4836426e85..0eb5712649d 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 fc63515005f..8cdd19af189 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 4ac01a55b32..0eb04f7bdbe 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 2d94119c82d..f46c142128b 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 00000000000..2a83857cf19 --- /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 1256f96bd4e..0ebeddfa67d 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()); + this.resultHandler = new SimpleResultHandler(conversionService); + } - 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))); + @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 HandlerResult createHandlerResult(HandlerMethod hm, ResolvableType type) { - return new HandlerResult(hm, null, type); + 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 Flux flux() { return null; } - public Publisher publisherString() { - return null; - } + public Observable observable() { return null; } - public Publisher publisherVoid() { - return null; - } + public CompletableFuture completableFuture() { return null; } - public Flux streamVoid() { - return null; - } + public String string() { return null; } - public Observable observableVoid() { - 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 e99454f126c..38da2644c9f 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 e65e867a2c9..c90ddb83346 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 0f6cc3a3a00..4fb874abc5d 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 98d16dc17a7..11d61c1dcf0 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 f8f1276142e..4f8b7ed5112 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); - } - 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(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); + + 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); - - 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 = Collections.singletonMap("name", "Joe"); + testHandle("/account", returnType, returnValue, "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 5407b2822b5..fac9daa7ff8 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; + } + }