diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultRequest.java index 3e3ecc3c37..8333ac111d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultRequest.java @@ -24,11 +24,15 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; +import java.util.function.Supplier; +import java.util.stream.Stream; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyExtractor; +import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; @@ -68,8 +72,14 @@ class DefaultRequest implements Request { } @Override - public T body(BodyExtractor extractor) { - return extractor.extract(request(), this.strategies); + public T body(BodyExtractor extractor) { + return extractor.extract(request(), + new BodyExtractor.Context() { + @Override + public Supplier>> messageReaders() { + return DefaultRequest.this.strategies.messageReaders(); + } + }); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java index 5d4d8f1b37..2236be2277 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java @@ -41,6 +41,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyInserter; +import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -142,7 +144,7 @@ class DefaultResponseBuilder implements Response.BodyBuilder { @Override public Response build() { return body(BodyInserter.of( - (response, strategies) -> response.setComplete(), + (response, context) -> response.setComplete(), () -> null)); } @@ -150,18 +152,18 @@ class DefaultResponseBuilder implements Response.BodyBuilder { public > Response build(T voidPublisher) { Assert.notNull(voidPublisher, "'voidPublisher' must not be null"); return body(BodyInserter.of( - (response, strategies) -> Flux.from(voidPublisher).then(response.setComplete()), + (response, context) -> Flux.from(voidPublisher).then(response.setComplete()), () -> null)); } @Override - public Response body(BiFunction> writer, + public Response body(BiFunction> writer, Supplier supplier) { return body(BodyInserter.of(writer, supplier)); } @Override - public Response body(BodyInserter inserter) { + public Response body(BodyInserter inserter) { Assert.notNull(inserter, "'inserter' must not be null"); return new BodyInserterResponse(this.statusCode, this.headers, inserter); } @@ -235,11 +237,12 @@ class DefaultResponseBuilder implements Response.BodyBuilder { private static final class BodyInserterResponse extends AbstractResponse { - private final BodyInserter inserter; + private final BodyInserter inserter; - public BodyInserterResponse( - int statusCode, HttpHeaders headers, BodyInserter inserter) { + public BodyInserterResponse(int statusCode, HttpHeaders headers, + BodyInserter inserter) { + super(statusCode, headers); this.inserter = inserter; } @@ -253,7 +256,12 @@ class DefaultResponseBuilder implements Response.BodyBuilder { public Mono writeTo(ServerWebExchange exchange, StrategiesSupplier strategies) { ServerHttpResponse response = exchange.getResponse(); writeStatusAndHeaders(response); - return this.inserter.insert(response, strategies); + return this.inserter.insert(response, new BodyInserter.Context() { + @Override + public Supplier>> messageWriters() { + return strategies.messageWriters(); + } + }); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Request.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Request.java index f13c9c58a3..1ab0f09bb0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Request.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Request.java @@ -28,6 +28,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyExtractor; +import org.springframework.http.server.reactive.ServerHttpRequest; /** * Represents an HTTP request, as handled by a {@code HandlerFunction}. @@ -67,7 +69,7 @@ public interface Request { * @param the type of the body returned * @return the extracted body */ - T body(BodyExtractor extractor); + T body(BodyExtractor extractor); /** * Return the request attribute value if present. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RequestPredicates.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RequestPredicates.java index 09b6c0f054..c88c918ea0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RequestPredicates.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RequestPredicates.java @@ -27,6 +27,8 @@ import java.util.function.Predicate; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyExtractor; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; @@ -314,7 +316,7 @@ public abstract class RequestPredicates { } @Override - public T body(BodyExtractor extractor) { + public T body(BodyExtractor extractor) { return this.request.body(extractor); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java index 25b5e0206a..37122a1a43 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java @@ -32,6 +32,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyInserter; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; @@ -316,7 +317,7 @@ public interface Response { * @param the type contained in the body * @return the built response */ - Response body(BiFunction> writer, + Response body(BiFunction> writer, Supplier supplier); /** @@ -325,7 +326,7 @@ public interface Response { * @param the type contained in the body * @return the built response */ - Response body(BodyInserter inserter); + Response body(BodyInserter inserter); /** * Render the template with the given {@code name} using the given {@code modelAttributes}. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/support/RequestWrapper.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/support/RequestWrapper.java index 4d1d998e90..c11b091e95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/support/RequestWrapper.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/support/RequestWrapper.java @@ -28,8 +28,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyExtractor; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; -import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.HandlerFunction; import org.springframework.web.reactive.function.Request; @@ -83,7 +84,7 @@ public class RequestWrapper implements Request { } @Override - public T body(BodyExtractor extractor) { + public T body(BodyExtractor extractor) { return this.request.body(extractor); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultRequestTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultRequestTests.java index d48aefc067..aeff32af05 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultRequestTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultRequestTests.java @@ -52,7 +52,7 @@ import org.springframework.web.server.ServerWebExchange; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.web.reactive.function.BodyExtractors.toMono; +import static org.springframework.http.codec.BodyExtractors.toMono; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultResponseBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultResponseBuilderTests.java index 711f8698fb..110db3b3eb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultResponseBuilderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultResponseBuilderTests.java @@ -38,6 +38,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyInserter; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -217,7 +218,7 @@ public class DefaultResponseBuilderTests { public void bodyInserter() throws Exception { String body = "foo"; Supplier supplier = () -> body; - BiFunction> writer = + BiFunction> writer = (response, strategies) -> { byte[] bodyBytes = body.getBytes(UTF_8); ByteBuffer byteBuffer = ByteBuffer.wrap(bodyBytes); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java index 7f346eddfc..278d565a37 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java @@ -50,6 +50,7 @@ import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertEquals; +import static org.springframework.http.codec.BodyInserters.fromPublisher; import static org.springframework.web.reactive.function.RouterFunctions.route; /** @@ -155,14 +156,14 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr public Response> mono(Request request) { Person person = new Person("John"); - return Response.ok().body(BodyInserters.fromPublisher(Mono.just(person), Person.class)); + return Response.ok().body(fromPublisher(Mono.just(person), Person.class)); } public Response> flux(Request request) { Person person1 = new Person("John"); Person person2 = new Person("Jane"); return Response.ok().body( - BodyInserters.fromPublisher(Flux.just(person1, person2), Person.class)); + fromPublisher(Flux.just(person1, person2), Person.class)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/MockRequest.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/MockRequest.java index ebb3bb1d62..13f8a59903 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/MockRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/MockRequest.java @@ -33,6 +33,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; +import org.springframework.http.codec.BodyExtractor; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -88,9 +90,9 @@ public class MockRequest implements Request { return this.headers; } - @SuppressWarnings("unchecked") @Override - public S body(BodyExtractor extractor) { + @SuppressWarnings("unchecked") + public S body(BodyExtractor extractor){ return (S) this.body; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/PublisherHandlerFunctionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/PublisherHandlerFunctionIntegrationTests.java index 439edd50cf..633b74dab8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/PublisherHandlerFunctionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/PublisherHandlerFunctionIntegrationTests.java @@ -33,8 +33,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertEquals; -import static org.springframework.web.reactive.function.BodyExtractors.toMono; -import static org.springframework.web.reactive.function.BodyInserters.fromPublisher; +import static org.springframework.http.codec.BodyExtractors.toMono; +import static org.springframework.http.codec.BodyInserters.fromPublisher; import static org.springframework.web.reactive.function.RequestPredicates.GET; import static org.springframework.web.reactive.function.RequestPredicates.POST; import static org.springframework.web.reactive.function.RouterFunctions.route; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterFunctionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterFunctionTests.java index 0ffdb3c906..4cddcdd18b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterFunctionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterFunctionTests.java @@ -23,7 +23,7 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.springframework.web.reactive.function.BodyInserters.fromObject; +import static org.springframework.http.codec.BodyInserters.fromObject; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/SseHandlerFunctionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/SseHandlerFunctionIntegrationTests.java index b403025059..dc7f35b087 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/SseHandlerFunctionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/SseHandlerFunctionIntegrationTests.java @@ -30,6 +30,7 @@ import org.springframework.http.codec.ServerSentEvent; import org.springframework.tests.TestSubscriber; import org.springframework.web.client.reactive.WebClient; +import static org.springframework.http.codec.BodyInserters.fromServerSentEvents; import static org.springframework.web.client.reactive.ClientWebRequestBuilders.get; import static org.springframework.web.client.reactive.ResponseExtractors.bodyStream; import static org.springframework.web.reactive.function.RouterFunctions.route; @@ -111,13 +112,13 @@ public class SseHandlerFunctionIntegrationTests public Response> string(Request request) { Flux flux = Flux.interval(Duration.ofMillis(100)).map(l -> "foo " + l).take(2); - return Response.ok().body(BodyInserters.fromServerSentEvents(flux, String.class)); + return Response.ok().body(fromServerSentEvents(flux, String.class)); } public Response> person(Request request) { Flux flux = Flux.interval(Duration.ofMillis(100)) .map(l -> new Person("foo " + l)).take(2); - return Response.ok().body(BodyInserters.fromServerSentEvents(flux, Person.class)); + return Response.ok().body(fromServerSentEvents(flux, Person.class)); } public Response>> sse(Request request) { @@ -127,7 +128,7 @@ public class SseHandlerFunctionIntegrationTests .comment("bar") .build()).take(2); - return Response.ok().body(BodyInserters.fromServerSentEvents(flux)); + return Response.ok().body(fromServerSentEvents(flux)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractor.java b/spring-web/src/main/java/org/springframework/http/codec/BodyExtractor.java similarity index 52% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractor.java rename to spring-web/src/main/java/org/springframework/http/codec/BodyExtractor.java index c21cd30189..a2c469b540 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractor.java +++ b/spring-web/src/main/java/org/springframework/http/codec/BodyExtractor.java @@ -14,28 +14,43 @@ * limitations under the License. */ -package org.springframework.web.reactive.function; +package org.springframework.http.codec; -import org.springframework.http.server.reactive.ServerHttpRequest; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.http.ReactiveHttpInputMessage; /** - * A function that can extract data from a {@link Request} body. + * A function that can extract data from a {@link ReactiveHttpInputMessage} body. * * @param the type of data to extract * @author Arjen Poutsma * @since 5.0 - * @see Request#body(BodyExtractor) * @see BodyExtractors */ @FunctionalInterface -public interface BodyExtractor { +public interface BodyExtractor { /** * Extract from the given request. - * @param request the request to extract from - * @param strategies the strategies to use + * @param inputMessage request to extract from + * @param context the configuration to use * @return the extracted data */ - T extract(ServerHttpRequest request, StrategiesSupplier strategies); + T extract(M inputMessage, Context context); + + /** + * Defines the context used during the extraction. + */ + interface Context { + + /** + * Supply a {@linkplain Stream stream} of {@link HttpMessageReader}s to be used for body + * extraction. + * @return the stream of message readers + */ + Supplier>> messageReaders(); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java b/spring-web/src/main/java/org/springframework/http/codec/BodyExtractors.java similarity index 76% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java rename to spring-web/src/main/java/org/springframework/http/codec/BodyExtractors.java index 56fe29c191..d355de46fb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java +++ b/spring-web/src/main/java/org/springframework/http/codec/BodyExtractors.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.function; +package org.springframework.http.codec; import java.util.Collections; import java.util.List; @@ -28,11 +28,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.http.HttpMessage; import org.springframework.http.MediaType; -import org.springframework.http.codec.HttpMessageReader; -import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.util.Assert; -import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** * Implementations of {@link BodyExtractor} that read various bodies, such a reactive streams. @@ -48,7 +47,7 @@ public abstract class BodyExtractors { * @param the element type * @return a {@code BodyExtractor} that reads a mono */ - public static BodyExtractor> toMono(Class elementClass) { + public static BodyExtractor, ReactiveHttpInputMessage> toMono(Class elementClass) { Assert.notNull(elementClass, "'elementClass' must not be null"); return toMono(ResolvableType.forClass(elementClass)); } @@ -59,9 +58,9 @@ public abstract class BodyExtractors { * @param the element type * @return a {@code BodyExtractor} that reads a mono */ - public static BodyExtractor> toMono(ResolvableType elementType) { + public static BodyExtractor, ReactiveHttpInputMessage> toMono(ResolvableType elementType) { Assert.notNull(elementType, "'elementType' must not be null"); - return (request, strategies) -> readWithMessageReaders(request, strategies, + return (request, context) -> readWithMessageReaders(request, context, elementType, reader -> reader.readMono(elementType, request, Collections.emptyMap()), Mono::error); @@ -73,7 +72,7 @@ public abstract class BodyExtractors { * @param the element type * @return a {@code BodyExtractor} that reads a mono */ - public static BodyExtractor> toFlux(Class elementClass) { + public static BodyExtractor, ReactiveHttpInputMessage> toFlux(Class elementClass) { Assert.notNull(elementClass, "'elementClass' must not be null"); return toFlux(ResolvableType.forClass(elementClass)); } @@ -84,23 +83,23 @@ public abstract class BodyExtractors { * @param the element type * @return a {@code BodyExtractor} that reads a mono */ - public static BodyExtractor> toFlux(ResolvableType elementType) { + public static BodyExtractor, ReactiveHttpInputMessage> toFlux(ResolvableType elementType) { Assert.notNull(elementType, "'elementType' must not be null"); - return (request, strategies) -> readWithMessageReaders(request, strategies, + return (request, context) -> readWithMessageReaders(request, context, elementType, reader -> reader.read(elementType, request, Collections.emptyMap()), Flux::error); } private static > S readWithMessageReaders( - ServerHttpRequest request, - StrategiesSupplier strategies, + ReactiveHttpInputMessage inputMessage, + BodyExtractor.Context context, ResolvableType elementType, Function, S> readerFunction, Function unsupportedError) { - MediaType contentType = contentType(request); - Supplier>> messageReaders = strategies.messageReaders(); + MediaType contentType = contentType(inputMessage); + Supplier>> messageReaders = context.messageReaders(); return messageReaders.get() .filter(r -> r.canRead(elementType, contentType)) .findFirst() @@ -110,14 +109,14 @@ public abstract class BodyExtractors { List supportedMediaTypes = messageReaders.get() .flatMap(reader -> reader.getReadableMediaTypes().stream()) .collect(Collectors.toList()); - UnsupportedMediaTypeStatusException error = - new UnsupportedMediaTypeStatusException(contentType, supportedMediaTypes); + UnsupportedMediaTypeException error = + new UnsupportedMediaTypeException(contentType, supportedMediaTypes); return unsupportedError.apply(error); }); } - private static MediaType contentType(ServerHttpRequest request) { - MediaType result = request.getHeaders().getContentType(); + private static MediaType contentType(HttpMessage message) { + MediaType result = message.getHeaders().getContentType(); return result != null ? result : MediaType.APPLICATION_OCTET_STREAM; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserter.java b/spring-web/src/main/java/org/springframework/http/codec/BodyInserter.java similarity index 61% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserter.java rename to spring-web/src/main/java/org/springframework/http/codec/BodyInserter.java index 116166f8e4..e287e09506 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/BodyInserter.java @@ -14,35 +14,32 @@ * limitations under the License. */ -package org.springframework.web.reactive.function; +package org.springframework.http.codec; import java.util.function.BiFunction; import java.util.function.Supplier; +import java.util.stream.Stream; import reactor.core.publisher.Mono; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.util.Assert; /** - * A component that can insert data into a {@link Response} body. + * A combination of functions that can populate a {@link ReactiveHttpOutputMessage} body. * - * @param the type of data to insert * @author Arjen Poutsma * @since 5.0 - * @see Response#body() - * @see Response.BodyBuilder#body(BodyInserter) - * @see BodyInserters */ -public interface BodyInserter { +public interface BodyInserter { /** * Insert into the given response. - * @param response the response to insert into - * @param strategies the strategies to use + * @param outputMessage the response to insert into + * @param context the context to use * @return a {@code Mono} that indicates completion or error */ - Mono insert(ServerHttpResponse response, StrategiesSupplier strategies); + Mono insert(M outputMessage, Context context); /** * Return the type contained in the body. @@ -50,7 +47,6 @@ public interface BodyInserter { */ T t(); - /** * Return a new {@code BodyInserter} described by the given writer and supplier functions. * @param writer the writer function for the new inserter @@ -58,13 +54,29 @@ public interface BodyInserter { * @param the type supplied and written by the inserter * @return the new {@code BodyInserter} */ - static BodyInserter of(BiFunction> writer, + static BodyInserter of( + BiFunction> writer, Supplier supplier) { Assert.notNull(writer, "'writer' must not be null"); Assert.notNull(supplier, "'supplier' must not be null"); - return new BodyInserters.DefaultBodyInserter(writer, supplier); + return new BodyInserters.DefaultBodyInserter(writer, supplier); } + /** + * Defines the context used during the insertion. + */ + interface Context { + + /** + * Supply a {@linkplain Stream stream} of {@link HttpMessageWriter}s to be used for response + * body conversion. + * @return the stream of message writers + */ + Supplier>> messageWriters(); + + } + + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserters.java b/spring-web/src/main/java/org/springframework/http/codec/BodyInserters.java similarity index 77% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserters.java rename to spring-web/src/main/java/org/springframework/http/codec/BodyInserters.java index c698c07a67..df2d654c33 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserters.java +++ b/spring-web/src/main/java/org/springframework/http/codec/BodyInserters.java @@ -14,23 +14,22 @@ * limitations under the License. */ -package org.springframework.web.reactive.function; +package org.springframework.http.codec; import java.util.Collections; +import java.util.List; import java.util.function.BiFunction; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.codec.HttpMessageWriter; -import org.springframework.http.codec.ResourceHttpMessageWriter; -import org.springframework.http.codec.ServerSentEvent; -import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; @@ -61,10 +60,10 @@ public abstract class BodyInserters { * @param body the body of the response * @return a {@code BodyInserter} that writes a single object */ - public static BodyInserter fromObject(T body) { + public static BodyInserter fromObject(T body) { Assert.notNull(body, "'body' must not be null"); return BodyInserter.of( - (response, strategies) -> writeWithMessageWriters(response, strategies, + (response, context) -> writeWithMessageWriters(response, context, Mono.just(body), ResolvableType.forInstance(body)), () -> body); } @@ -74,10 +73,10 @@ public abstract class BodyInserters { * @param publisher the publisher to stream to the response body * @param elementClass the class of elements contained in the publisher * @param the type of the elements contained in the publisher - * @param the type of the {@code Publisher}. + * @param the type of the {@code Publisher} * @return a {@code BodyInserter} that writes a {@code Publisher} */ - public static , T> BodyInserter fromPublisher(S publisher, + public static , T> BodyInserter fromPublisher(S publisher, Class elementClass) { Assert.notNull(publisher, "'publisher' must not be null"); @@ -90,16 +89,16 @@ public abstract class BodyInserters { * @param publisher the publisher to stream to the response body * @param elementType the type of elements contained in the publisher * @param the type of the elements contained in the publisher - * @param the type of the {@code Publisher}. + * @param the type of the {@code Publisher} * @return a {@code BodyInserter} that writes a {@code Publisher} */ - public static , T> BodyInserter fromPublisher(S publisher, + public static , T> BodyInserter fromPublisher(S publisher, ResolvableType elementType) { Assert.notNull(publisher, "'publisher' must not be null"); Assert.notNull(elementType, "'elementType' must not be null"); return BodyInserter.of( - (response, strategies) -> writeWithMessageWriters(response, strategies, + (response, context) -> writeWithMessageWriters(response, context, publisher, elementType), () -> publisher ); @@ -114,10 +113,10 @@ public abstract class BodyInserters { * @param the type of the {@code Resource} * @return a {@code BodyInserter} that writes a {@code Publisher} */ - public static BodyInserter fromResource(T resource) { + public static BodyInserter fromResource(T resource) { Assert.notNull(resource, "'resource' must not be null"); return BodyInserter.of( - (response, strategies) -> { + (response, context) -> { ResourceHttpMessageWriter messageWriter = new ResourceHttpMessageWriter(); MediaType contentType = response.getHeaders().getContentType(); return messageWriter.write(Mono.just(resource), RESOURCE_TYPE, contentType, @@ -134,12 +133,12 @@ public abstract class BodyInserters { * @return a {@code BodyInserter} that writes a {@code ServerSentEvent} publisher * @see Server-Sent Events W3C recommendation */ - public static >> BodyInserter fromServerSentEvents( + public static >> BodyInserter fromServerSentEvents( S eventsPublisher) { Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null"); return BodyInserter.of( - (response, strategies) -> { + (response, context) -> { ServerSentEventHttpMessageWriter messageWriter = sseMessageWriter(); MediaType contentType = response.getHeaders().getContentType(); return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE, @@ -159,7 +158,7 @@ public abstract class BodyInserters { * Server-Sent Events * @see Server-Sent Events W3C recommendation */ - public static > BodyInserter fromServerSentEvents(S eventsPublisher, + public static > BodyInserter fromServerSentEvents(S eventsPublisher, Class eventClass) { Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null"); @@ -177,13 +176,13 @@ public abstract class BodyInserters { * Server-Sent Events * @see Server-Sent Events W3C recommendation */ - public static > BodyInserter fromServerSentEvents(S eventsPublisher, + public static > BodyInserter fromServerSentEvents(S eventsPublisher, ResolvableType eventType) { Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null"); Assert.notNull(eventType, "'eventType' must not be null"); return BodyInserter.of( - (response, strategies) -> { + (response, context) -> { ServerSentEventHttpMessageWriter messageWriter = sseMessageWriter(); MediaType contentType = response.getHeaders().getContentType(); return messageWriter.write(eventsPublisher, eventType, contentType, response, @@ -200,23 +199,27 @@ public abstract class BodyInserters { new ServerSentEventHttpMessageWriter(); } - private static Mono writeWithMessageWriters(ServerHttpResponse response, - StrategiesSupplier strategies, + private static Mono writeWithMessageWriters(ReactiveHttpOutputMessage outputMessage, + BodyInserter.Context context, Publisher body, ResolvableType bodyType) { - // TODO: use ContentNegotiatingResultHandlerSupport - MediaType contentType = response.getHeaders().getContentType(); - return strategies.messageWriters().get() + MediaType contentType = outputMessage.getHeaders().getContentType(); + Supplier>> messageWriters = context.messageWriters(); + return messageWriters.get() .filter(messageWriter -> messageWriter.canWrite(bodyType, contentType)) .findFirst() .map(BodyInserters::cast) .map(messageWriter -> messageWriter - .write(body, bodyType, contentType, response, Collections + .write(body, bodyType, contentType, outputMessage, Collections .emptyMap())) .orElseGet(() -> { - response.setStatusCode(HttpStatus.NOT_ACCEPTABLE); - return response.setComplete(); + List supportedMediaTypes = messageWriters.get() + .flatMap(reader -> reader.getWritableMediaTypes().stream()) + .collect(Collectors.toList()); + UnsupportedMediaTypeException error = + new UnsupportedMediaTypeException(contentType, supportedMediaTypes); + return Mono.error(error); }); } @@ -225,22 +228,23 @@ public abstract class BodyInserters { return (HttpMessageWriter) messageWriter; } - static class DefaultBodyInserter implements BodyInserter { + static class DefaultBodyInserter + implements BodyInserter { - private final BiFunction> writer; + private final BiFunction> writer; private final Supplier supplier; public DefaultBodyInserter( - BiFunction> writer, + BiFunction> writer, Supplier supplier) { this.writer = writer; this.supplier = supplier; } @Override - public Mono insert(ServerHttpResponse response, StrategiesSupplier strategies) { - return this.writer.apply(response, strategies); + public Mono insert(M outputMessage, Context context) { + return this.writer.apply(outputMessage, context); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/codec/UnsupportedMediaTypeException.java b/spring-web/src/main/java/org/springframework/http/codec/UnsupportedMediaTypeException.java new file mode 100644 index 0000000000..ca38457d32 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/UnsupportedMediaTypeException.java @@ -0,0 +1,74 @@ +/* + * 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.http.codec; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.http.MediaType; + +/** + * Exception thrown to indicate that a {@code Content-Type} is not supported. + * + * @author Arjen Poutsma + * @since 5.0 + */ +@SuppressWarnings("serial") +public class UnsupportedMediaTypeException extends NestedRuntimeException { + + private final MediaType contentType; + + private final List supportedMediaTypes; + + + /** + * Constructor for when the specified Content-Type is invalid. + */ + public UnsupportedMediaTypeException(String reason) { + super(reason); + this.contentType = null; + this.supportedMediaTypes = Collections.emptyList(); + } + + /** + * Constructor for when the Content-Type can be parsed but is not supported. + */ + public UnsupportedMediaTypeException(MediaType contentType, List supportedMediaTypes) { + super("Content type '" + contentType + "' not supported"); + this.contentType = contentType; + this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes); + } + + + /** + * Return the request Content-Type header if it was parsed successfully. + */ + public Optional getContentType() { + return Optional.ofNullable(this.contentType); + } + + /** + * Return the list of supported content types in cases when the Content-Type + * header is parsed but not supported, or an empty list otherwise. + */ + public List getSupportedMediaTypes() { + return this.supportedMediaTypes; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java b/spring-web/src/test/java/org/springframework/http/codec/BodyExtractorsTests.java similarity index 57% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java rename to spring-web/src/test/java/org/springframework/http/codec/BodyExtractorsTests.java index ba5f450bea..b7ae0b39f5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/BodyExtractorsTests.java @@ -14,31 +14,60 @@ * limitations under the License. */ -package org.springframework.web.reactive.function; +package org.springframework.http.codec; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.tests.TestSubscriber; -import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** * @author Arjen Poutsma */ public class BodyExtractorsTests { + private BodyExtractor.Context context; + + @Before + public void createContext() { + final List> messageReaders = new ArrayList<>(); + messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); + + this.context = new BodyExtractor.Context() { + @Override + public Supplier>> messageReaders() { + return messageReaders::stream; + } + }; + + } + @Test public void toMono() throws Exception { - BodyExtractor> extractor = BodyExtractors.toMono(String.class); + BodyExtractor, ReactiveHttpInputMessage> extractor = BodyExtractors.toMono(String.class); DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); DefaultDataBuffer dataBuffer = @@ -48,9 +77,7 @@ public class BodyExtractorsTests { MockServerHttpRequest request = new MockServerHttpRequest(); request.setBody(body); - StrategiesSupplier strategies = StrategiesSupplier.builder().build(); - - Mono result = extractor.extract(request, strategies); + Mono result = extractor.extract(request, this.context); TestSubscriber.subscribe(result) .assertComplete() @@ -59,7 +86,7 @@ public class BodyExtractorsTests { @Test public void toFlux() throws Exception { - BodyExtractor> extractor = BodyExtractors.toFlux(String.class); + BodyExtractor, ReactiveHttpInputMessage> extractor = BodyExtractors.toFlux(String.class); DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); DefaultDataBuffer dataBuffer = @@ -69,9 +96,7 @@ public class BodyExtractorsTests { MockServerHttpRequest request = new MockServerHttpRequest(); request.setBody(body); - StrategiesSupplier strategies = StrategiesSupplier.builder().build(); - - Flux result = extractor.extract(request, strategies); + Flux result = extractor.extract(request, this.context); TestSubscriber.subscribe(result) .assertComplete() .assertValues("foo"); @@ -79,7 +104,7 @@ public class BodyExtractorsTests { @Test public void toFluxUnacceptable() throws Exception { - BodyExtractor> extractor = BodyExtractors.toFlux(String.class); + BodyExtractor, ReactiveHttpInputMessage> extractor = BodyExtractors.toFlux(String.class); DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); DefaultDataBuffer dataBuffer = @@ -90,12 +115,16 @@ public class BodyExtractorsTests { request.getHeaders().setContentType(MediaType.APPLICATION_JSON); request.setBody(body); - StrategiesSupplier strategies = StrategiesSupplier.empty().build(); + BodyExtractor.Context emptyContext = new BodyExtractor.Context() { + @Override + public Supplier>> messageReaders() { + return () -> Collections.>emptySet().stream(); + } + }; - Flux result = extractor.extract(request, strategies); + Flux result = extractor.extract(request, emptyContext); TestSubscriber.subscribe(result) - .assertError(UnsupportedMediaTypeStatusException.class); - + .assertError(UnsupportedMediaTypeException.class); } } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java b/spring-web/src/test/java/org/springframework/http/codec/BodyInsertersTests.java similarity index 65% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java rename to spring-web/src/test/java/org/springframework/http/codec/BodyInsertersTests.java index 277a32f5db..622f26dd36 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/BodyInsertersTests.java @@ -14,20 +14,30 @@ * limitations under the License. */ -package org.springframework.web.reactive.function; +package org.springframework.http.codec; import java.nio.ByteBuffer; import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.http.codec.ServerSentEvent; +import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.tests.TestSubscriber; @@ -40,15 +50,35 @@ import static org.junit.Assert.assertEquals; */ public class BodyInsertersTests { + private BodyInserter.Context context; + + @Before + public void createContext() { + final List> messageWriters = new ArrayList<>(); + messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(new CharSequenceEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder())); + + this.context = new BodyInserter.Context() { + @Override + public Supplier>> messageWriters() { + return messageWriters::stream; + } + }; + + } + + @Test public void ofObject() throws Exception { String body = "foo"; - BodyInserter inserter = BodyInserters.fromObject(body); + BodyInserter inserter = BodyInserters.fromObject(body); assertEquals(body, inserter.t()); MockServerHttpResponse response = new MockServerHttpResponse(); - Mono result = inserter.insert(response, StrategiesSupplier.builder().build()); + Mono result = inserter.insert(response, this.context); TestSubscriber.subscribe(result) .assertComplete(); @@ -62,12 +92,12 @@ public class BodyInsertersTests { @Test public void ofPublisher() throws Exception { Flux body = Flux.just("foo"); - BodyInserter> inserter = BodyInserters.fromPublisher(body, String.class); + BodyInserter, ReactiveHttpOutputMessage> inserter = BodyInserters.fromPublisher(body, String.class); assertEquals(body, inserter.t()); MockServerHttpResponse response = new MockServerHttpResponse(); - Mono result = inserter.insert(response, StrategiesSupplier.builder().build()); + Mono result = inserter.insert(response, this.context); TestSubscriber.subscribe(result) .assertComplete(); @@ -81,12 +111,12 @@ public class BodyInsertersTests { @Test public void ofResource() throws Exception { Resource body = new ClassPathResource("response.txt", getClass()); - BodyInserter inserter = BodyInserters.fromResource(body); + BodyInserter inserter = BodyInserters.fromResource(body); assertEquals(body, inserter.t()); MockServerHttpResponse response = new MockServerHttpResponse(); - Mono result = inserter.insert(response, StrategiesSupplier.builder().build()); + Mono result = inserter.insert(response, this.context); TestSubscriber.subscribe(result) .assertComplete(); @@ -105,13 +135,13 @@ public class BodyInsertersTests { public void ofServerSentEventFlux() throws Exception { ServerSentEvent event = ServerSentEvent.builder("foo").build(); Flux> body = Flux.just(event); - BodyInserter>> inserter = + BodyInserter>, ServerHttpResponse> inserter = BodyInserters.fromServerSentEvents(body); assertEquals(body, inserter.t()); MockServerHttpResponse response = new MockServerHttpResponse(); - Mono result = inserter.insert(response, StrategiesSupplier.builder().build()); + Mono result = inserter.insert(response, this.context); TestSubscriber.subscribe(result) .assertComplete(); @@ -120,13 +150,13 @@ public class BodyInsertersTests { @Test public void ofServerSentEventClass() throws Exception { Flux body = Flux.just("foo"); - BodyInserter> inserter = + BodyInserter, ServerHttpResponse> inserter = BodyInserters.fromServerSentEvents(body, String.class); assertEquals(body, inserter.t()); MockServerHttpResponse response = new MockServerHttpResponse(); - Mono result = inserter.insert(response, StrategiesSupplier.builder().build()); + Mono result = inserter.insert(response, this.context); TestSubscriber.subscribe(result) .assertComplete(); diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/function/response.txt b/spring-web/src/test/resources/org/springframework/http/codec/response.txt similarity index 100% rename from spring-web-reactive/src/test/resources/org/springframework/web/reactive/function/response.txt rename to spring-web/src/test/resources/org/springframework/http/codec/response.txt