Moved BodyExtractor and BodyInserter to http.codec

This commit moves the web.reactive.function.[BodyInserter|BodyExtractor]
to http.codec, so that they can be used from the client as well.

Furthermore, it parameterized both inserter and extractor over
ReactiveHttpOutputMessage and ReactiveHttpInputMessage respectively, so
that they can be limited to only be used on the client or server.
This commit is contained in:
Arjen Poutsma
2016-10-05 15:01:23 +02:00
parent ed6c533079
commit dc1926a861
21 changed files with 320 additions and 128 deletions

View File

@@ -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> T body(BodyExtractor<T> extractor) {
return extractor.extract(request(), this.strategies);
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
return extractor.extract(request(),
new BodyExtractor.Context() {
@Override
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
return DefaultRequest.this.strategies.messageReaders();
}
});
}
@Override

View File

@@ -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<Void> build() {
return body(BodyInserter.of(
(response, strategies) -> response.setComplete(),
(response, context) -> response.setComplete(),
() -> null));
}
@@ -150,18 +152,18 @@ class DefaultResponseBuilder implements Response.BodyBuilder {
public <T extends Publisher<Void>> Response<T> 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 <T> Response<T> body(BiFunction<ServerHttpResponse, StrategiesSupplier, Mono<Void>> writer,
public <T> Response<T> body(BiFunction<ServerHttpResponse, BodyInserter.Context, Mono<Void>> writer,
Supplier<T> supplier) {
return body(BodyInserter.of(writer, supplier));
}
@Override
public <T> Response<T> body(BodyInserter<T> inserter) {
public <T> Response<T> body(BodyInserter<T, ? super ServerHttpResponse> inserter) {
Assert.notNull(inserter, "'inserter' must not be null");
return new BodyInserterResponse<T>(this.statusCode, this.headers, inserter);
}
@@ -235,11 +237,12 @@ class DefaultResponseBuilder implements Response.BodyBuilder {
private static final class BodyInserterResponse<T> extends AbstractResponse<T> {
private final BodyInserter<T> inserter;
private final BodyInserter<T, ? super ServerHttpResponse> inserter;
public BodyInserterResponse(
int statusCode, HttpHeaders headers, BodyInserter<T> inserter) {
public BodyInserterResponse(int statusCode, HttpHeaders headers,
BodyInserter<T, ? super ServerHttpResponse> inserter) {
super(statusCode, headers);
this.inserter = inserter;
}
@@ -253,7 +256,12 @@ class DefaultResponseBuilder implements Response.BodyBuilder {
public Mono<Void> 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<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters();
}
});
}
}

View File

@@ -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 <T> the type of the body returned
* @return the extracted body
*/
<T> T body(BodyExtractor<T> extractor);
<T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor);
/**
* Return the request attribute value if present.

View File

@@ -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> T body(BodyExtractor<T> extractor) {
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
return this.request.body(extractor);
}

View File

@@ -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<T> {
* @param <T> the type contained in the body
* @return the built response
*/
<T> Response<T> body(BiFunction<ServerHttpResponse, StrategiesSupplier, Mono<Void>> writer,
<T> Response<T> body(BiFunction<ServerHttpResponse, BodyInserter.Context, Mono<Void>> writer,
Supplier<T> supplier);
/**
@@ -325,7 +326,7 @@ public interface Response<T> {
* @param <T> the type contained in the body
* @return the built response
*/
<T> Response<T> body(BodyInserter<T> inserter);
<T> Response<T> body(BodyInserter<T, ? super ServerHttpResponse> inserter);
/**
* Render the template with the given {@code name} using the given {@code modelAttributes}.

View File

@@ -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> T body(BodyExtractor<T> extractor) {
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
return this.request.body(extractor);
}

View File

@@ -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

View File

@@ -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<String> supplier = () -> body;
BiFunction<ServerHttpResponse, StrategiesSupplier, Mono<Void>> writer =
BiFunction<ServerHttpResponse, BodyInserter.Context, Mono<Void>> writer =
(response, strategies) -> {
byte[] bodyBytes = body.getBytes(UTF_8);
ByteBuffer byteBuffer = ByteBuffer.wrap(bodyBytes);

View File

@@ -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<Publisher<Person>> 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<Publisher<Person>> 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));
}
}

View File

@@ -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<T> implements Request {
return this.headers;
}
@SuppressWarnings("unchecked")
@Override
public <S> S body(BodyExtractor<S> extractor) {
@SuppressWarnings("unchecked")
public <S> S body(BodyExtractor<S, ? super ServerHttpRequest> extractor){
return (S) this.body;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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<Publisher<String>> string(Request request) {
Flux<String> 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<Publisher<Person>> person(Request request) {
Flux<Person> 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<Publisher<ServerSentEvent<String>>> 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));
}
}

View File

@@ -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 <T> the type of data to extract
* @author Arjen Poutsma
* @since 5.0
* @see Request#body(BodyExtractor)
* @see BodyExtractors
*/
@FunctionalInterface
public interface BodyExtractor<T> {
public interface BodyExtractor<T, M extends ReactiveHttpInputMessage> {
/**
* 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<Stream<HttpMessageReader<?>>> messageReaders();
}
}

View File

@@ -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 <T> the element type
* @return a {@code BodyExtractor} that reads a mono
*/
public static <T> BodyExtractor<Mono<T>> toMono(Class<? extends T> elementClass) {
public static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(Class<? extends T> elementClass) {
Assert.notNull(elementClass, "'elementClass' must not be null");
return toMono(ResolvableType.forClass(elementClass));
}
@@ -59,9 +58,9 @@ public abstract class BodyExtractors {
* @param <T> the element type
* @return a {@code BodyExtractor} that reads a mono
*/
public static <T> BodyExtractor<Mono<T>> toMono(ResolvableType elementType) {
public static <T> BodyExtractor<Mono<T>, 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 <T> the element type
* @return a {@code BodyExtractor} that reads a mono
*/
public static <T> BodyExtractor<Flux<T>> toFlux(Class<? extends T> elementClass) {
public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(Class<? extends T> elementClass) {
Assert.notNull(elementClass, "'elementClass' must not be null");
return toFlux(ResolvableType.forClass(elementClass));
}
@@ -84,23 +83,23 @@ public abstract class BodyExtractors {
* @param <T> the element type
* @return a {@code BodyExtractor} that reads a mono
*/
public static <T> BodyExtractor<Flux<T>> toFlux(ResolvableType elementType) {
public static <T> BodyExtractor<Flux<T>, 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 <T, S extends Publisher<T>> S readWithMessageReaders(
ServerHttpRequest request,
StrategiesSupplier strategies,
ReactiveHttpInputMessage inputMessage,
BodyExtractor.Context context,
ResolvableType elementType,
Function<HttpMessageReader<T>, S> readerFunction,
Function<Throwable, S> unsupportedError) {
MediaType contentType = contentType(request);
Supplier<Stream<HttpMessageReader<?>>> messageReaders = strategies.messageReaders();
MediaType contentType = contentType(inputMessage);
Supplier<Stream<HttpMessageReader<?>>> messageReaders = context.messageReaders();
return messageReaders.get()
.filter(r -> r.canRead(elementType, contentType))
.findFirst()
@@ -110,14 +109,14 @@ public abstract class BodyExtractors {
List<MediaType> 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;
}

View File

@@ -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 <T> 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<T> {
public interface BodyInserter<T, M extends ReactiveHttpOutputMessage> {
/**
* 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<Void> insert(ServerHttpResponse response, StrategiesSupplier strategies);
Mono<Void> insert(M outputMessage, Context context);
/**
* Return the type contained in the body.
@@ -50,7 +47,6 @@ public interface BodyInserter<T> {
*/
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<T> {
* @param <T> the type supplied and written by the inserter
* @return the new {@code BodyInserter}
*/
static <T> BodyInserter<T> of(BiFunction<ServerHttpResponse, StrategiesSupplier, Mono<Void>> writer,
static <T, M extends ReactiveHttpOutputMessage> BodyInserter<T, M> of(
BiFunction<M, Context, Mono<Void>> writer,
Supplier<T> supplier) {
Assert.notNull(writer, "'writer' must not be null");
Assert.notNull(supplier, "'supplier' must not be null");
return new BodyInserters.DefaultBodyInserter<T>(writer, supplier);
return new BodyInserters.DefaultBodyInserter<T, M>(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<Stream<HttpMessageWriter<?>>> messageWriters();
}
}

View File

@@ -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 <T> BodyInserter<T> fromObject(T body) {
public static <T> BodyInserter<T, ReactiveHttpOutputMessage> 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 <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}.
* @param <S> the type of the {@code Publisher}
* @return a {@code BodyInserter} that writes a {@code Publisher}
*/
public static <S extends Publisher<T>, T> BodyInserter<S> fromPublisher(S publisher,
public static <S extends Publisher<T>, T> BodyInserter<S, ReactiveHttpOutputMessage> fromPublisher(S publisher,
Class<T> 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 <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}.
* @param <S> the type of the {@code Publisher}
* @return a {@code BodyInserter} that writes a {@code Publisher}
*/
public static <S extends Publisher<T>, T> BodyInserter<S> fromPublisher(S publisher,
public static <S extends Publisher<T>, T> BodyInserter<S, ReactiveHttpOutputMessage> 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 <T> the type of the {@code Resource}
* @return a {@code BodyInserter} that writes a {@code Publisher}
*/
public static <T extends Resource> BodyInserter<T> fromResource(T resource) {
public static <T extends Resource> BodyInserter<T, ReactiveHttpOutputMessage> 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 <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
*/
public static <T, S extends Publisher<ServerSentEvent<T>>> BodyInserter<S> fromServerSentEvents(
public static <T, S extends Publisher<ServerSentEvent<T>>> BodyInserter<S, ServerHttpResponse> 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 <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
*/
public static <T, S extends Publisher<T>> BodyInserter<S> fromServerSentEvents(S eventsPublisher,
public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fromServerSentEvents(S eventsPublisher,
Class<T> eventClass) {
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
@@ -177,13 +176,13 @@ public abstract class BodyInserters {
* Server-Sent Events
* @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
*/
public static <T, S extends Publisher<T>> BodyInserter<S> fromServerSentEvents(S eventsPublisher,
public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> 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 <T> Mono<Void> writeWithMessageWriters(ServerHttpResponse response,
StrategiesSupplier strategies,
private static <T> Mono<Void> writeWithMessageWriters(ReactiveHttpOutputMessage outputMessage,
BodyInserter.Context context,
Publisher<T> body,
ResolvableType bodyType) {
// TODO: use ContentNegotiatingResultHandlerSupport
MediaType contentType = response.getHeaders().getContentType();
return strategies.messageWriters().get()
MediaType contentType = outputMessage.getHeaders().getContentType();
Supplier<Stream<HttpMessageWriter<?>>> 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<MediaType> 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<T>) messageWriter;
}
static class DefaultBodyInserter<T> implements BodyInserter<T> {
static class DefaultBodyInserter<T, M extends ReactiveHttpOutputMessage>
implements BodyInserter<T, M> {
private final BiFunction<ServerHttpResponse, StrategiesSupplier, Mono<Void>> writer;
private final BiFunction<M, Context, Mono<Void>> writer;
private final Supplier<T> supplier;
public DefaultBodyInserter(
BiFunction<ServerHttpResponse, StrategiesSupplier, Mono<Void>> writer,
BiFunction<M, Context, Mono<Void>> writer,
Supplier<T> supplier) {
this.writer = writer;
this.supplier = supplier;
}
@Override
public Mono<Void> insert(ServerHttpResponse response, StrategiesSupplier strategies) {
return this.writer.apply(response, strategies);
public Mono<Void> insert(M outputMessage, Context context) {
return this.writer.apply(outputMessage, context);
}
@Override

View File

@@ -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<MediaType> 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<MediaType> 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<MediaType> 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<MediaType> getSupportedMediaTypes() {
return this.supportedMediaTypes;
}
}

View File

@@ -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<HttpMessageReader<?>> 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<Stream<HttpMessageReader<?>>> messageReaders() {
return messageReaders::stream;
}
};
}
@Test
public void toMono() throws Exception {
BodyExtractor<Mono<String>> extractor = BodyExtractors.toMono(String.class);
BodyExtractor<Mono<String>, 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<String> result = extractor.extract(request, strategies);
Mono<String> result = extractor.extract(request, this.context);
TestSubscriber.subscribe(result)
.assertComplete()
@@ -59,7 +86,7 @@ public class BodyExtractorsTests {
@Test
public void toFlux() throws Exception {
BodyExtractor<Flux<String>> extractor = BodyExtractors.toFlux(String.class);
BodyExtractor<Flux<String>, 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<String> result = extractor.extract(request, strategies);
Flux<String> 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<Flux<String>> extractor = BodyExtractors.toFlux(String.class);
BodyExtractor<Flux<String>, 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<Stream<HttpMessageReader<?>>> messageReaders() {
return () -> Collections.<HttpMessageReader<?>>emptySet().stream();
}
};
Flux<String> result = extractor.extract(request, strategies);
Flux<String> result = extractor.extract(request, emptyContext);
TestSubscriber.subscribe(result)
.assertError(UnsupportedMediaTypeStatusException.class);
.assertError(UnsupportedMediaTypeException.class);
}
}

View File

@@ -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<HttpMessageWriter<?>> 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<Stream<HttpMessageWriter<?>>> messageWriters() {
return messageWriters::stream;
}
};
}
@Test
public void ofObject() throws Exception {
String body = "foo";
BodyInserter<String> inserter = BodyInserters.fromObject(body);
BodyInserter<String, ReactiveHttpOutputMessage> inserter = BodyInserters.fromObject(body);
assertEquals(body, inserter.t());
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, StrategiesSupplier.builder().build());
Mono<Void> result = inserter.insert(response, this.context);
TestSubscriber.subscribe(result)
.assertComplete();
@@ -62,12 +92,12 @@ public class BodyInsertersTests {
@Test
public void ofPublisher() throws Exception {
Flux<String> body = Flux.just("foo");
BodyInserter<Flux<String>> inserter = BodyInserters.fromPublisher(body, String.class);
BodyInserter<Flux<String>, ReactiveHttpOutputMessage> inserter = BodyInserters.fromPublisher(body, String.class);
assertEquals(body, inserter.t());
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, StrategiesSupplier.builder().build());
Mono<Void> 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<Resource> inserter = BodyInserters.fromResource(body);
BodyInserter<Resource, ReactiveHttpOutputMessage> inserter = BodyInserters.fromResource(body);
assertEquals(body, inserter.t());
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, StrategiesSupplier.builder().build());
Mono<Void> result = inserter.insert(response, this.context);
TestSubscriber.subscribe(result)
.assertComplete();
@@ -105,13 +135,13 @@ public class BodyInsertersTests {
public void ofServerSentEventFlux() throws Exception {
ServerSentEvent<String> event = ServerSentEvent.builder("foo").build();
Flux<ServerSentEvent<String>> body = Flux.just(event);
BodyInserter<Flux<ServerSentEvent<String>>> inserter =
BodyInserter<Flux<ServerSentEvent<String>>, ServerHttpResponse> inserter =
BodyInserters.fromServerSentEvents(body);
assertEquals(body, inserter.t());
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, StrategiesSupplier.builder().build());
Mono<Void> result = inserter.insert(response, this.context);
TestSubscriber.subscribe(result)
.assertComplete();
@@ -120,13 +150,13 @@ public class BodyInsertersTests {
@Test
public void ofServerSentEventClass() throws Exception {
Flux<String> body = Flux.just("foo");
BodyInserter<Flux<String>> inserter =
BodyInserter<Flux<String>, ServerHttpResponse> inserter =
BodyInserters.fromServerSentEvents(body, String.class);
assertEquals(body, inserter.t());
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, StrategiesSupplier.builder().build());
Mono<Void> result = inserter.insert(response, this.context);
TestSubscriber.subscribe(result)
.assertComplete();