diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java index fd0b0fb4b7..ecf9866a26 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java @@ -126,7 +126,7 @@ public abstract class ContentNegotiatingResultHandlerSupport implements Ordered } private List getAcceptableTypes(ServerWebExchange exchange) { - List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); + List mediaTypes = getContentTypeResolver().resolveMediaTypes(exchange); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } 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 index 5201bf7f75..4394b698a9 100644 --- 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 @@ -19,6 +19,7 @@ import java.util.List; import java.util.stream.Collectors; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; @@ -82,16 +83,22 @@ public abstract class AbstractMessageConverterResultHandler extends ContentNegot 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; } + if (Void.class.equals(elementType.getRawClass())) { + return Mono.from((Publisher) publisher); + } + List producibleTypes = getProducibleMediaTypes(elementType); + if (producibleTypes.isEmpty()) { + return Mono.error(new IllegalStateException( + "No converter for return value type: " + elementType)); + } + MediaType bestMediaType = selectMediaType(exchange, producibleTypes); if (bestMediaType != null) { 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 index 08a7114d90..42e50afe65 100644 --- 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 @@ -104,7 +104,7 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH if (!entityHeaders.isEmpty()) { entityHeaders.entrySet().stream() - .filter(entry -> responseHeaders.containsKey(entry.getKey())) + .filter(entry -> !responseHeaders.containsKey(entry.getKey())) .forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue())); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java new file mode 100644 index 0000000000..8059a9bdf6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java @@ -0,0 +1,137 @@ +/* + * 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.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +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.mockito.Mockito.mock; +import static org.springframework.http.MediaType.ALL; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; +import static org.springframework.http.MediaType.IMAGE_GIF; +import static org.springframework.http.MediaType.IMAGE_JPEG; +import static org.springframework.http.MediaType.IMAGE_PNG; +import static org.springframework.http.MediaType.TEXT_PLAIN; +import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; + +/** + * Unit tests for {@link ContentNegotiatingResultHandlerSupport}. + * @author Rossen Stoyanchev + */ +public class ContentNegotiatingResultHandlerSupportTests { + + private TestHandlerSupport handlerSupport; + + private MockServerHttpRequest request; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.handlerSupport = new TestHandlerSupport(); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange( + this.request, new MockServerHttpResponse(), mock(WebSessionManager.class)); + } + + + @Test + public void usesContentTypeResolver() throws Exception { + RequestedContentTypeResolver resolver = new FixedContentTypeResolver(IMAGE_GIF); + TestHandlerSupport handlerSupport = new TestHandlerSupport(resolver); + + List mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG); + MediaType actual = handlerSupport.selectMediaType(this.exchange, mediaTypes); + + assertEquals(IMAGE_GIF, actual); + } + + @Test + public void producibleMediaTypesRequestAttribute() throws Exception { + Set producible = Collections.singleton(IMAGE_GIF); + this.exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producible); + + List mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG); + MediaType actual = handlerSupport.selectMediaType(this.exchange, mediaTypes); + + assertEquals(IMAGE_GIF, actual); + } + + @Test // SPR-9160 + public void sortsByQuality() throws Exception { + this.request.getHeaders().add("Accept", "text/plain; q=0.5, application/json"); + + List mediaTypes = Arrays.asList(TEXT_PLAIN, APPLICATION_JSON_UTF8); + MediaType actual = this.handlerSupport.selectMediaType(this.exchange, mediaTypes); + + assertEquals(APPLICATION_JSON_UTF8, actual); + } + + @Test + public void charsetFromAcceptHeader() throws Exception { + MediaType text8859 = MediaType.parseMediaType("text/plain;charset=ISO-8859-1"); + MediaType textUtf8 = MediaType.parseMediaType("text/plain;charset=UTF-8"); + + this.request.getHeaders().setAccept(Collections.singletonList(text8859)); + MediaType actual = this.handlerSupport.selectMediaType(this.exchange, Collections.singletonList(textUtf8)); + + assertEquals(text8859, actual); + } + + @Test // SPR-12894 + public void noConcreteMediaType() throws Exception { + List producible = Collections.singletonList(ALL); + MediaType actual = this.handlerSupport.selectMediaType(this.exchange, producible); + + assertEquals(APPLICATION_OCTET_STREAM, actual); + } + + + + + private static class TestHandlerSupport extends ContentNegotiatingResultHandlerSupport { + + protected TestHandlerSupport() { + this(new HeaderContentTypeResolver()); + } + + public TestHandlerSupport(RequestedContentTypeResolver contentTypeResolver) { + super(new GenericConversionService(), contentTypeResolver); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java new file mode 100644 index 0000000000..3f66528315 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -0,0 +1,155 @@ +/* + * 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.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Observable; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +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.util.ObjectUtils; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +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.assertNull; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link AbstractMessageConverterResultHandler}. + * @author Rossen Stoyanchev + */ +public class MessageConverterResultHandlerTests { + + private AbstractMessageConverterResultHandler resultHandler; + + private MockServerHttpResponse response = new MockServerHttpResponse(); + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.resultHandler = createResultHandler(); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + } + + + @Test // SPR-12894 + @Ignore // GH # 121 + public void useDefaultContentType() throws Exception { + Object body = new ByteArrayResource("body".getBytes("UTF-8")); + ResolvableType bodyType = ResolvableType.forType(Resource.class); + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertEquals("image/jpeg", this.response.getHeaders().getFirst("Content-Type")); + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("body", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + } + + @Test + public void voidReturnType() throws Exception { + testVoidReturnType(null, ResolvableType.forType(Void.class)); + testVoidReturnType(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class)); + testVoidReturnType(Flux.empty(), ResolvableType.forClassWithGenerics(Flux.class, Void.class)); + testVoidReturnType(Observable.empty(), ResolvableType.forClassWithGenerics(Observable.class, Void.class)); + } + + private void testVoidReturnType(Object body, ResolvableType bodyType) { + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertNull(this.response.getHeaders().get("Content-Type")); + assertNull(this.response.getBody()); + } + + @Test // SPR-13135 + public void unsupportedReturnType() throws Exception { + ByteArrayOutputStream body = new ByteArrayOutputStream(); + ResolvableType bodyType = ResolvableType.forType(OutputStream.class); + + HttpMessageConverter converter = new CodecHttpMessageConverter<>(new ByteBufferEncoder()); + Mono mono = createResultHandler(converter).writeBody(this.exchange, body, bodyType); + + TestSubscriber.subscribe(mono).assertError(IllegalStateException.class); + } + + + private AbstractMessageConverterResultHandler createResultHandler(HttpMessageConverter... converters) { + List> converterList; + if (ObjectUtils.isEmpty(converters)) { + converterList = new ArrayList<>(); + converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder())); + converterList.add(new CodecHttpMessageConverter<>(new StringEncoder())); + converterList.add(new ResourceHttpMessageConverter()); + converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder())); + converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder())); + } + else { + converterList = Arrays.asList(converters); + } + + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); + + return new AbstractMessageConverterResultHandler(converterList, service, resolver) {}; + } + +} 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 0d0745ede9..f2cd44b397 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 @@ -17,143 +17,154 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; -import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import org.junit.Before; import org.junit.Test; -import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; 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.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.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; +import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.HandlerMapping; 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.reactive.accept.RequestedContentTypeResolverBuilder; 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 ResponseBodyResultHandler}. + * + * consider whether the logic under test is in a parent class, then see: + *
    + *
  • {@code MessageConverterResultHandlerTests}, + *
  • {@code ContentNegotiatingResultHandlerSupportTests} + *
* @author Sebastien Deleuze * @author Rossen Stoyanchev */ public class ResponseBodyResultHandlerTests { + private ResponseBodyResultHandler resultHandler; + + private MockServerHttpResponse response = new MockServerHttpResponse(); + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.resultHandler = createHandler(); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + } + + + private ResponseBodyResultHandler createHandler(HttpMessageConverter... converters) { + List> converterList; + if (ObjectUtils.isEmpty(converters)) { + converterList = new ArrayList<>(); + converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder())); + converterList.add(new CodecHttpMessageConverter<>(new StringEncoder())); + converterList.add(new ResourceHttpMessageConverter()); + converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder())); + converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder())); + } + else { + converterList = Arrays.asList(converters); + } + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); + + return new ResponseBodyResultHandler(converterList, new DefaultConversionService(), resolver); + } @Test public void supports() throws NoSuchMethodException { - ResponseBodyResultHandler handler = createHandler(new StringEncoder()); TestController controller = new TestController(); + testSupports(controller, "handleReturningString", true); + testSupports(controller, "handleReturningVoid", true); + testSupports(controller, "doWork", false); - HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated")); + TestRestController restController = new TestRestController(); + testSupports(restController, "handleReturningString", true); + testSupports(restController, "handleReturningVoid", true); + } + + private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException { + HandlerMethod hm = handlerMethod(controller, method); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + HandlerResult handlerResult = new HandlerResult(hm, null, type, new ExtendedModelMap()); + assertEquals(result, this.resultHandler.supports(handlerResult)); } @Test public void defaultOrder() throws Exception { - ResponseBodyResultHandler handler = createHandler(new StringEncoder()); - assertEquals(100, handler.getOrder()); - } - - @Test - public void usesContentTypeResolver() throws Exception { - 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(APPLICATION_JSON_UTF8, exchange.getResponse().getHeaders().getContentType()); - } - - @Test - public void detectsProducibleMediaTypesAttribute() throws Exception { - ServerWebExchange exchange = createExchange("/foo"); - Set mediaTypes = Collections.singleton(MediaType.APPLICATION_JSON); - exchange.getAttributes().put(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); - - HandlerResultHandler handler = createHandler(new StringEncoder(), new JacksonJsonEncoder()); - - HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); - handler.handleResult(exchange, result).block(); - - assertEquals(MediaType.APPLICATION_JSON, exchange.getResponse().getHeaders().getContentType()); + assertEquals(100, this.resultHandler.getOrder()); } - private ResponseBodyResultHandler createHandler(Encoder... encoders) { - return createHandler(new HeaderContentTypeResolver(), encoders); - } - - private ResponseBodyResultHandler createHandler(RequestedContentTypeResolver resolver, - Encoder... encoders) { - - List> converters = Arrays.stream(encoders) - .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) - .collect(Collectors.toList()); - 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); + private HandlerMethod handlerMethod(Object controller, String method) throws NoSuchMethodException { + return new HandlerMethod(controller, controller.getClass().getMethod(method)); } - @SuppressWarnings("unused") + @RestController @SuppressWarnings("unused") + private static class TestRestController { + + public String handleReturningString() { + return null; + } + + public Void handleReturningVoid() { + return null; + } + } + + @Controller @SuppressWarnings("unused") private static class TestController { - public Publisher notAnnotated() { + @ResponseBody + public String handleReturningString() { return null; } @ResponseBody - public Publisher publisherString() { + public Void handleReturningVoid() { return null; } - @ResponseBody - public Publisher publisherVoid() { + public String doWork() { return null; } + } } 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 ba131de3f2..1b4a1b8ebc 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 @@ -16,134 +16,139 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +import org.junit.Before; 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.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Encoder; 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.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; +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.web.method.HandlerMethod; +import org.springframework.ui.ModelMap; +import org.springframework.util.ObjectUtils; 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.reactive.accept.RequestedContentTypeResolverBuilder; 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.assertNull; 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}. + * Unit tests for {@link ResponseEntityResultHandler}. When adding a test also + * consider whether the logic under test is in a parent class, then see: + *
    + *
  • {@code MessageConverterResultHandlerTests}, + *
  • {@code ContentNegotiatingResultHandlerSupportTests} + *
* @author Rossen Stoyanchev */ public class ResponseEntityResultHandlerTests { + private static final Object HANDLER = new Object(); + + + private ResponseEntityResultHandler resultHandler; + private MockServerHttpResponse response = new MockServerHttpResponse(); + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.resultHandler = createHandler(); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + } + + private ResponseEntityResultHandler createHandler(HttpMessageConverter... converters) { + List> converterList; + if (ObjectUtils.isEmpty(converters)) { + converterList = new ArrayList<>(); + converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder())); + converterList.add(new CodecHttpMessageConverter<>(new StringEncoder())); + converterList.add(new ResourceHttpMessageConverter()); + converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder())); + converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder())); + } + else { + converterList = Arrays.asList(converters); + } + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); + + return new ResponseEntityResultHandler(converterList, new DefaultConversionService(), resolver); + } + @Test public void supports() throws NoSuchMethodException { - ResponseEntityResultHandler handler = createHandler(new StringEncoder()); - TestController controller = new TestController(); + ModelMap model = new ExtendedModelMap(); - 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()))); + ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, String.class); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); - hm = new HandlerMethod(controller, TestController.class.getMethod("responseVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); - hm = new HandlerMethod(controller, TestController.class.getMethod("string")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + type = ResolvableType.forClass(Void.class); + assertFalse(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); } @Test public void defaultOrder() throws Exception { - ResponseEntityResultHandler handler = createHandler(new StringEncoder()); - assertEquals(0, handler.getOrder()); + assertEquals(0, this.resultHandler.getOrder()); } @Test - public void jsonResponseBody() throws Exception { - RequestedContentTypeResolver resolver = new FixedContentTypeResolver(APPLICATION_JSON_UTF8); - HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); + public void statusCode() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.noContent().build(), type); + this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); - 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")))); + assertEquals(HttpStatus.NO_CONTENT, this.response.getStatus()); + assertEquals(0, this.response.getHeaders().size()); + assertNull(this.response.getBody()); } + @Test + public void headers() throws Exception { + URI location = new URI("/path"); + ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.created(location).build(), type); + this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); - 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; - } + assertEquals(HttpStatus.CREATED, this.response.getStatus()); + assertEquals(1, this.response.getHeaders().size()); + assertEquals(location, this.response.getHeaders().getLocation()); + assertNull(this.response.getBody()); } }