From 9aa6f5caacac316c74e374550a387f222e7e1921 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 21 Jun 2016 17:27:52 -0400 Subject: [PATCH] Add support for ResponseEntity result handling --- .../config/WebReactiveConfiguration.java | 16 +- ...AbstractMessageConverterResultHandler.java | 116 ++++++++++++++ .../annotation/ResponseBodyResultHandler.java | 84 ++-------- .../ResponseEntityResultHandler.java | 118 ++++++++++++++ .../config/WebReactiveConfigurationTests.java | 36 ++++- .../RequestMappingIntegrationTests.java | 12 ++ .../ResponseBodyResultHandlerTests.java | 8 +- .../ResponseEntityResultHandlerTests.java | 150 ++++++++++++++++++ 8 files changed, 455 insertions(+), 85 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index e30c1e4fc3f..740dbcc18aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -60,6 +60,7 @@ import org.springframework.web.reactive.result.method.HandlerMethodArgumentResol import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; @@ -335,13 +336,20 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } @Bean - public ResponseBodyResultHandler responseBodyResultHandler() { - return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService()); + public SimpleResultHandler simpleResultHandler() { + return new SimpleResultHandler(mvcConversionService()); } @Bean - public SimpleResultHandler simpleResultHandler() { - return new SimpleResultHandler(mvcConversionService()); + public ResponseEntityResultHandler responseEntityResultHandler() { + return new ResponseEntityResultHandler(getMessageConverters(), mvcConversionService(), + mvcContentTypeResolver()); + } + + @Bean + public ResponseBodyResultHandler responseBodyResultHandler() { + return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService(), + mvcContentTypeResolver()); } @Bean diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java new file mode 100644 index 00000000000..5201bf7f756 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java @@ -0,0 +1,116 @@ +/* + * 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.method.annotation; + +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; +import org.springframework.web.server.NotAcceptableStatusException; +import org.springframework.web.server.ServerWebExchange; + +/** + * Abstract base class for result handlers that handle return values by writing + * to the response with {@link HttpMessageConverter}. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractMessageConverterResultHandler extends ContentNegotiatingResultHandlerSupport { + + private final List> messageConverters; + + + /** + * Constructor with message converters, a {@code ConversionService}, and a + * {@code RequestedContentTypeResolver}. + * + * @param converters converters for writing the response body with + * @param conversionService for converting other reactive types (e.g. + * rx.Observable, rx.Single, etc.) to Flux or Mono + * @param contentTypeResolver for resolving the requested content type + */ + protected AbstractMessageConverterResultHandler(List> converters, + ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { + + super(conversionService, contentTypeResolver); + Assert.notEmpty(converters, "At least one message converter is required."); + this.messageConverters = converters; + } + + /** + * Return the configured message converters. + */ + public List> getMessageConverters() { + return this.messageConverters; + } + + + @SuppressWarnings("unchecked") + protected Mono writeBody(ServerWebExchange exchange, Object body, ResolvableType bodyType) { + + Publisher publisher; + ResolvableType elementType; + + if (getConversionService().canConvert(bodyType.getRawClass(), Publisher.class)) { + if (body != null) { + publisher = getConversionService().convert(body, Publisher.class); + } + else { + publisher = Mono.empty(); + } + elementType = bodyType.getGeneric(0); + if (Void.class.equals(elementType.getRawClass())) { + return Mono.from((Publisher) publisher); + } + } + else { + publisher = Mono.justOrEmpty(body); + elementType = bodyType; + } + + List producibleTypes = getProducibleMediaTypes(elementType); + MediaType bestMediaType = selectMediaType(exchange, producibleTypes); + + if (bestMediaType != null) { + for (HttpMessageConverter converter : getMessageConverters()) { + if (converter.canWrite(elementType, bestMediaType)) { + ServerHttpResponse response = exchange.getResponse(); + return converter.write((Publisher) publisher, elementType, bestMediaType, response); + } + } + } + + return Mono.error(new NotAcceptableStatusException(producibleTypes)); + } + + private List getProducibleMediaTypes(ResolvableType elementType) { + return getMessageConverters().stream() + .filter(converter -> converter.canWrite(elementType, null)) + .flatMap(converter -> converter.getWritableMediaTypes().stream()) + .collect(Collectors.toList()); + } + +} 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 6a04b69091d..1fbafc0b413 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 @@ -17,29 +17,20 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; -import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.util.Assert; import org.springframework.web.bind.annotation.ResponseBody; 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; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; -import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; -import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -48,21 +39,18 @@ import org.springframework.web.server.ServerWebExchange; * with {@code @ResponseBody} writing to the body of the request or response with * an {@link HttpMessageConverter}. * - *

By default the order for the result handler is set to 0. It is generally - * safe and expected it will be ordered ahead of other result handlers since it - * only gets involved based on the presence of an {@code @ResponseBody} - * annotation. + *

By default the order for the result handler is set to 100. It detects the + * presence of an {@code @ResponseBody} annotation and should be ordered after + * result handlers that look for a specific return type such as + * {@code ResponseEntity}. * * @author Rossen Stoyanchev * @author Stephane Maldini * @author Sebastien Deleuze * @author Arjen Poutsma */ -public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSupport - implements HandlerResultHandler, Ordered { - - private final List> messageConverters; - +public class ResponseBodyResultHandler extends AbstractMessageConverterResultHandler + implements HandlerResultHandler { /** * Constructor with message converters and a {@code ConversionService} only @@ -90,20 +78,11 @@ public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSu public ResponseBodyResultHandler(List> converters, ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { - super(conversionService, contentTypeResolver); - Assert.notEmpty(converters, "At least one message converter is required."); - this.messageConverters = converters; - setOrder(0); + super(converters, conversionService, contentTypeResolver); + setOrder(100); } - /** - * Return the configured message converters. - */ - public List> getMessageConverters() { - return this.messageConverters; - } - @Override public boolean supports(HandlerResult result) { Object handler = result.getHandler(); @@ -117,51 +96,10 @@ public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSu } @Override - @SuppressWarnings("unchecked") public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - - Publisher publisher; - ResolvableType elementType; - ResolvableType returnType = result.getReturnValueType(); - - if (getConversionService().canConvert(returnType.getRawClass(), Publisher.class)) { - Optional optionalValue = result.getReturnValue(); - if (optionalValue.isPresent()) { - publisher = getConversionService().convert(optionalValue.get(), Publisher.class); - } - else { - publisher = Mono.empty(); - } - elementType = returnType.getGeneric(0); - if (Void.class.equals(elementType.getRawClass())) { - return Mono.from((Publisher)publisher); - } - } - else { - publisher = Mono.justOrEmpty(result.getReturnValue()); - elementType = returnType; - } - - List producibleTypes = getProducibleMediaTypes(elementType); - MediaType bestMediaType = selectMediaType(exchange, producibleTypes); - - if (bestMediaType != null) { - for (HttpMessageConverter converter : this.messageConverters) { - if (converter.canWrite(elementType, bestMediaType)) { - ServerHttpResponse response = exchange.getResponse(); - return converter.write((Publisher) publisher, elementType, bestMediaType, response); - } - } - } - - return Mono.error(new NotAcceptableStatusException(producibleTypes)); - } - - private List getProducibleMediaTypes(ResolvableType type) { - return this.messageConverters.stream() - .filter(converter -> converter.canWrite(type, null)) - .flatMap(converter -> converter.getWritableMediaTypes().stream()) - .collect(Collectors.toList()); + Object body = result.getReturnValue().orElse(null); + ResolvableType bodyType = result.getReturnValueType(); + return writeBody(exchange, body, bodyType); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java new file mode 100644 index 00000000000..08a7114d905 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -0,0 +1,118 @@ +/* + * 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.method.annotation; + +import java.util.List; +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Handles {@link HttpEntity} and {@link ResponseEntity} return values. + * + *

By default the order for this result handler is set to 0. It is generally + * safe to place it early in the order as it looks for a concrete return type. + * + * @author Rossen Stoyanchev + */ +public class ResponseEntityResultHandler extends AbstractMessageConverterResultHandler + implements HandlerResultHandler { + + /** + * Constructor with message converters and a {@code ConversionService} only + * and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header + * to determine the requested content type. + * + * @param converters converters for writing the response body with + * @param conversionService for converting to Flux and Mono from other reactive types + */ + public ResponseEntityResultHandler(List> converters, + ConversionService conversionService) { + + this(converters, conversionService, new HeaderContentTypeResolver()); + } + + /** + * Constructor with message converters, a {@code ConversionService}, and a + * {@code RequestedContentTypeResolver}. + * + * @param converters converters for writing the response body with + * @param conversionService for converting other reactive types (e.g. + * rx.Observable, rx.Single, etc.) to Flux or Mono + * @param contentTypeResolver for resolving the requested content type + */ + public ResponseEntityResultHandler(List> converters, + ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { + + super(converters, conversionService, contentTypeResolver); + setOrder(0); + } + + + @Override + public boolean supports(HandlerResult result) { + ResolvableType returnType = result.getReturnValueType(); + return (HttpEntity.class.isAssignableFrom(returnType.getRawClass()) && + !RequestEntity.class.isAssignableFrom(returnType.getRawClass())); + } + + + @Override + public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { + + Object body = null; + + Optional optional = result.getReturnValue(); + if (optional.isPresent()) { + Assert.isInstanceOf(HttpEntity.class, optional.get()); + HttpEntity httpEntity = (HttpEntity) optional.get(); + + if (httpEntity instanceof ResponseEntity) { + ResponseEntity responseEntity = (ResponseEntity) httpEntity; + exchange.getResponse().setStatusCode(responseEntity.getStatusCode()); + } + + HttpHeaders entityHeaders = httpEntity.getHeaders(); + HttpHeaders responseHeaders = exchange.getResponse().getHeaders(); + + if (!entityHeaders.isEmpty()) { + entityHeaders.entrySet().stream() + .filter(entry -> responseHeaders.containsKey(entry.getKey())) + .forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue())); + } + + body = httpEntity.getBody(); + } + + ResolvableType bodyType = result.getReturnValueType().getGeneric(0); + return writeBody(exchange, body, bodyType); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 43578f26569..0f4e8e32c76 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -55,6 +55,7 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; import org.springframework.web.reactive.result.view.HttpMessageConverterView; import org.springframework.web.reactive.result.view.View; import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; @@ -183,13 +184,12 @@ public class WebReactiveConfigurationTests { service.canConvert(Observable.class, Flux.class); } - @Test - public void responseBodyResultHandler() throws Exception { + public void responseEntityResultHandler() throws Exception { ApplicationContext context = loadConfig(WebReactiveConfiguration.class); - String name = "responseBodyResultHandler"; - ResponseBodyResultHandler handler = context.getBean(name, ResponseBodyResultHandler.class); + String name = "responseEntityResultHandler"; + ResponseEntityResultHandler handler = context.getBean(name, ResponseEntityResultHandler.class); assertNotNull(handler); assertEquals(0, handler.getOrder()); @@ -202,6 +202,34 @@ public class WebReactiveConfigurationTests { assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); + + name = "mvcContentTypeResolver"; + RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); + assertSame(resolver, handler.getContentTypeResolver()); + } + + @Test + public void responseBodyResultHandler() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "responseBodyResultHandler"; + ResponseBodyResultHandler handler = context.getBean(name, ResponseBodyResultHandler.class); + assertNotNull(handler); + + assertEquals(100, handler.getOrder()); + + List> converters = handler.getMessageConverters(); + assertEquals(5, converters.size()); + + assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); + + name = "mvcContentTypeResolver"; + RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); + assertSame(resolver, handler.getContentTypeResolver()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 3ad58a92b0a..5e5338a33f2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -172,6 +172,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati serializeAsPojo("http://localhost:" + port + "/completable-future"); } + @Test + @Ignore // Issue #119 + public void serializeAsMonoResponseEntity() throws Exception { + serializeAsPojo("http://localhost:" + port + "/monoResponseEntity"); + } + @Test public void serializeAsMono() throws Exception { serializeAsPojo("http://localhost:" + port + "/mono"); @@ -450,6 +456,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); } + @RequestMapping("/monoResponseEntity") + public ResponseEntity> monoResponseEntity() { + Mono body = Mono.just(new Person("Robert")); + return ResponseEntity.ok(body); + } + @RequestMapping("/mono") public Mono monoResponseBody() { return Mono.just(new Person("Robert")); 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 de06a674884..0d0745ede97 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 @@ -56,6 +56,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; /** @@ -87,20 +88,19 @@ public class ResponseBodyResultHandlerTests { @Test public void defaultOrder() throws Exception { ResponseBodyResultHandler handler = createHandler(new StringEncoder()); - assertEquals(0, handler.getOrder()); + assertEquals(100, handler.getOrder()); } @Test public void usesContentTypeResolver() throws Exception { - MediaType contentType = MediaType.APPLICATION_JSON_UTF8; - RequestedContentTypeResolver resolver = new FixedContentTypeResolver(contentType); + RequestedContentTypeResolver resolver = new FixedContentTypeResolver(APPLICATION_JSON_UTF8); HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); ServerWebExchange exchange = createExchange("/foo"); HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); handler.handleResult(exchange, result).block(); - assertEquals(contentType, exchange.getResponse().getHeaders().getContentType()); + assertEquals(APPLICATION_JSON_UTF8, exchange.getResponse().getHeaders().getContentType()); } @Test 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 new file mode 100644 index 00000000000..3cdd9ad26c2 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -0,0 +1,150 @@ +/* + * 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.method.annotation; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Test; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +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.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.accept.FixedContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; + +/** + * Unit tests for {@link ResponseEntityResultHandler}. + * @author Rossen Stoyanchev + */ +public class ResponseEntityResultHandlerTests { + + private MockServerHttpResponse response = new MockServerHttpResponse(); + + + @Test + public void supports() throws NoSuchMethodException { + ResponseEntityResultHandler handler = createHandler(new StringEncoder()); + TestController controller = new TestController(); + + HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("responseString")); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("responseVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("string")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + } + + @Test + public void defaultOrder() throws Exception { + ResponseEntityResultHandler handler = createHandler(new StringEncoder()); + assertEquals(0, handler.getOrder()); + } + + @Test + public void jsonResponseBody() throws Exception { + RequestedContentTypeResolver resolver = new FixedContentTypeResolver(APPLICATION_JSON_UTF8); + HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); + + TestController controller = new TestController(); + HandlerMethod hm = new HandlerMethod(controller, controller.getClass().getMethod("responseString")); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + HandlerResult result = new HandlerResult(hm, ResponseEntity.ok("fooValue"), type); + + ServerWebExchange exchange = createExchange("/foo"); + handler.handleResult(exchange, result).block(); + + assertEquals(HttpStatus.OK, this.response.getStatus()); + assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("\"fooValue\"", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + } + + + private ResponseEntityResultHandler createHandler(Encoder... encoders) { + return createHandler(new HeaderContentTypeResolver(), encoders); + } + + private ResponseEntityResultHandler createHandler(RequestedContentTypeResolver resolver, + Encoder... encoders) { + + List> converters = Arrays.stream(encoders) + .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) + .collect(Collectors.toList()); + return new ResponseEntityResultHandler(converters, new DefaultConversionService(), resolver); + } + + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, this.response, sessionManager); + } + + + @SuppressWarnings("unused") + private static class TestController { + + public ResponseEntity responseString() { + return null; + } + + public ResponseEntity responseVoid() { + return null; + } + + public String string() { + return null; + } + } + +}