From 37404d081e807c1f4926590a78f1d4da266fb5c3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 10:52:19 -0400 Subject: [PATCH] Use ContentTypeResolver for content negotiation --- .../annotation/ResponseBodyResultHandler.java | 44 +++++++++++++---- .../RequestMappingIntegrationTests.java | 13 ++--- .../ResponseBodyResultHandlerTests.java | 49 +++++++++++++++++-- 3 files changed, 84 insertions(+), 22 deletions(-) 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 fd4ce3c8e3..976b6f193d 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 @@ -42,6 +42,8 @@ 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.ContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -64,23 +66,44 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private final ConversionService conversionService; + private final ContentTypeResolver contentTypeResolver; + private final List supportedMediaTypes; private int order = 0; /** - * Constructor with message converters and conversion service. + * 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 ResponseBodyResultHandler(List> converters, + ConversionService conversionService) { + + this(converters, conversionService, new HeaderContentTypeResolver()); + } + + /** + * Constructor with message converters, a {@code ConversionService}, and a + * {@code ContentTypeResolver}. + * * @param messageConverters converters for writing the response body with * @param conversionService for converting to Flux and Mono from other reactive types */ public ResponseBodyResultHandler(List> messageConverters, - ConversionService conversionService) { + ConversionService conversionService, ContentTypeResolver contentTypeResolver) { Assert.notEmpty(messageConverters, "At least one message converter is required."); Assert.notNull(conversionService, "'conversionService' is required."); + Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required."); + this.messageConverters = messageConverters; this.conversionService = conversionService; + this.contentTypeResolver = contentTypeResolver; this.supportedMediaTypes = initSupportedMediaTypes(messageConverters); } @@ -149,10 +172,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } ServerHttpRequest request = exchange.getRequest(); - List compatibleMediaTypes = getCompatibleMediaTypes(request, elementType); + List compatibleMediaTypes = getCompatibleMediaTypes(exchange, elementType); if (compatibleMediaTypes.isEmpty()) { - List supported = getProducibleMediaTypes(elementType); - return Mono.error(new NotAcceptableStatusException(supported)); + if (result.getReturnValue().isPresent()) { + List mediaTypes = getProducibleMediaTypes(elementType); + return Mono.error(new NotAcceptableStatusException(mediaTypes)); + } + return Mono.empty(); } MediaType bestMediaType = selectBestMediaType(compatibleMediaTypes); @@ -167,10 +193,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Mono.error(new NotAcceptableStatusException(this.supportedMediaTypes)); } - private List getCompatibleMediaTypes(ServerHttpRequest request, + private List getCompatibleMediaTypes(ServerWebExchange exchange, ResolvableType elementType) { - List acceptableMediaTypes = getAcceptableMediaTypes(request); + List acceptableMediaTypes = getAcceptableMediaTypes(exchange); List producibleMediaTypes = getProducibleMediaTypes(elementType); Set compatibleMediaTypes = new LinkedHashSet<>(); @@ -187,8 +213,8 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return result; } - private List getAcceptableMediaTypes(ServerHttpRequest request) { - List mediaTypes = request.getHeaders().getAccept(); + private List getAcceptableMediaTypes(ServerWebExchange exchange) { + List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } 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 594f2ede8c..5075cdbed7 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 @@ -406,14 +406,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { - List> converters = - Arrays.asList(new ResourceHttpMessageConverter(), - new CodecHttpMessageConverter( - new ByteBufferEncoder(), new ByteBufferDecoder()), - new CodecHttpMessageConverter(new StringEncoder(), - new StringDecoder()), - new CodecHttpMessageConverter( - new JacksonJsonEncoder(), new JacksonJsonDecoder())); + List> converters = Arrays.asList( + new ResourceHttpMessageConverter(), + new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()), + new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()), + new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder())); ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(converters, conversionService()); resultHandler.setOrder(1); 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 c439417d1f..c30e36d075 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 @@ -16,6 +16,8 @@ package org.springframework.web.reactive.result.method.annotation; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -25,23 +27,36 @@ import org.reactivestreams.Publisher; 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.http.HttpMethod; +import org.springframework.http.MediaType; 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.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.ContentTypeResolver; +import org.springframework.web.reactive.accept.FixedContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +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; /** * Unit tests for {@link ResponseBodyResultHandler}. - * * @author Sebastien Deleuze * @author Rossen Stoyanchev */ @@ -50,7 +65,7 @@ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { - ResponseBodyResultHandler handler = createResultHandler(new StringEncoder()); + ResponseBodyResultHandler handler = createHandler(new StringEncoder()); TestController controller = new TestController(); HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated")); @@ -68,18 +83,42 @@ public class ResponseBodyResultHandlerTests { @Test public void defaultOrder() throws Exception { - ResponseBodyResultHandler handler = createResultHandler(new StringEncoder()); + ResponseBodyResultHandler handler = createHandler(new StringEncoder()); assertEquals(0, handler.getOrder()); } + @Test + public void contentTypeResolver() throws Exception { + MediaType contentType = MediaType.APPLICATION_JSON_UTF8; + ContentTypeResolver resolver = new FixedContentTypeResolver(contentType); + 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).get(); + + assertEquals(contentType, exchange.getResponse().getHeaders().getContentType()); + } + + + private ResponseBodyResultHandler createHandler(Encoder... encoders) { + return createHandler(new HeaderContentTypeResolver(), encoders); + } + + private ResponseBodyResultHandler createHandler(ContentTypeResolver resolver, + Encoder... encoders) { - private ResponseBodyResultHandler createResultHandler(Encoder... encoders) { List> converters = Arrays.stream(encoders) .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) .collect(Collectors.toList()); - return new ResponseBodyResultHandler(converters, new DefaultConversionService()); + return new ResponseBodyResultHandler(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, new MockServerHttpResponse(), sessionManager); + } @SuppressWarnings("unused")