diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 628ac443b50..2b5c482eb99 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -23,22 +23,35 @@ import reactor.Publishers; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; /** - * Supports {@link HandlerResult} with a {@code Publisher} value. + * Supports {@link HandlerResult} with a {@code void} or {@code Publisher} value. + * An optional {link ConversionService} can be used to support types that can be converted to + * {@code Publisher}, like {@code Observable} or {@code CompletableFuture}. * * @author Sebastien Deleuze */ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler { - private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics(Publisher.class, Void.class); - private int order = Ordered.LOWEST_PRECEDENCE; + private ConversionService conversionService; + + + public SimpleHandlerResultHandler() { + } + + public SimpleHandlerResultHandler(ConversionService conversionService) { + Assert.notNull(conversionService, "'conversionService' is required."); + this.conversionService = conversionService; + } + public void setOrder(int order) { this.order = order; @@ -52,14 +65,24 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public boolean supports(HandlerResult result) { ResolvableType type = result.getValueType(); - return type != null && PUBLISHER_VOID.isAssignableFrom(type); + return type != null && Void.TYPE.equals(type.getRawClass()) || + (Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()) && isConvertibleToPublisher(type)); + } + + private boolean isConvertibleToPublisher(ResolvableType type) { + return Publisher.class.isAssignableFrom(type.getRawClass()) || + ((this.conversionService != null) && this.conversionService.canConvert(type.getRawClass(), Publisher.class)); } @Override public Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, HandlerResult result) { - Publisher completion = Publishers.completable((Publisher)result.getValue()); + Object value = result.getValue(); + if (Void.TYPE.equals(result.getValueType().getRawClass())) { + return response.writeHeaders(); + } + Publisher completion = (value instanceof Publisher ? (Publisher)value : this.conversionService.convert(value, Publisher.class)); return Publishers.concat(Publishers.from(Arrays.asList(completion, response.writeHeaders()))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index 481291b7b2d..0a04a12ebaa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -16,12 +16,20 @@ package org.springframework.web.reactive.handler; +import java.util.concurrent.CompletableFuture; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.rx.Stream; +import rx.Observable; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -38,7 +46,7 @@ public class SimpleHandlerResultHandlerTests { HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); type = ResolvableType.forMethodParameter(hm.getReturnType()); @@ -47,14 +55,61 @@ public class SimpleHandlerResultHandlerTests { hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + // Reactor Stream is a Publisher + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + } + + @Test + public void supportsWithConversionService() throws NoSuchMethodException { + + GenericConversionService conversionService = new GenericConversionService(); + conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + conversionService.addConverter(new ReactiveStreamsToReactorConverter()); + conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); + SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService); + TestController controller = new TestController(); + + HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); } @SuppressWarnings("unused") private static class TestController { - public Publisher voidReturnValue() { - return null; + public void voidReturnValue() { } public Publisher publisherString() { @@ -64,6 +119,18 @@ public class SimpleHandlerResultHandlerTests { public Publisher publisherVoid() { return null; } + + public Stream streamVoid() { + return null; + } + + public Observable observableVoid() { + return null; + } + + public CompletableFuture completableFutureVoid() { + return null; + } } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 98210fbeb93..c5c4bd5eede 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -197,16 +197,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - public void create() throws Exception { - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + this.port + "/create"); - RequestEntity> request = RequestEntity.post(url) - .contentType(MediaType.APPLICATION_JSON) - .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); - ResponseEntity response = restTemplate.exchange(request, Void.class); + public void publisherCreate() throws Exception { + create("http://localhost:" + this.port + "/publisher-create"); + } - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestController.class).persons.size()); + @Test + public void streamCreate() throws Exception { + create("http://localhost:" + this.port + "/stream-create"); + } + + @Test + public void observableCreate() throws Exception { + create("http://localhost:" + this.port + "/observable-create"); } @@ -259,6 +261,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("MARIE", results.get(1).getName()); } + private void create(String requestUrl) throws Exception { + RestTemplate restTemplate = new RestTemplate(); + URI url = new URI(requestUrl); + RequestEntity> request = RequestEntity.post(url) + .contentType(MediaType.APPLICATION_JSON) + .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); + ResponseEntity response = restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, this.wac.getBean(TestController.class).persons.size()); + } + @Configuration @SuppressWarnings("unused") @@ -295,7 +309,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public SimpleHandlerResultHandler simpleHandlerResultHandler() { - return new SimpleHandlerResultHandler(); + return new SimpleHandlerResultHandler(conversionService()); } } @@ -448,11 +462,21 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati }); } - @RequestMapping("/create") - public Publisher create(@RequestBody Stream personStream) { + @RequestMapping("/publisher-create") + public Publisher publisherCreate(@RequestBody Publisher personStream) { + return Streams.wrap(personStream).toList().onSuccess(persons::addAll).after(); + } + + @RequestMapping("/stream-create") + public Promise streamCreate(@RequestBody Stream personStream) { return personStream.toList().onSuccess(persons::addAll).after(); } + @RequestMapping("/observable-create") + public Observable observableCreate(@RequestBody Observable personStream) { + return personStream.toList().doOnNext(p -> persons.addAll(p)).flatMap(document -> Observable.empty()); + } + //TODO add mixed and T request mappings tests }