org.springframework.web.reactive.function.BodyExtractors Maven / Gradle / Ivy
/*
* Copyright 2002-2018 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.function;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpMessage;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MultiValueMap;
/**
* Implementations of {@link BodyExtractor} that read various bodies, such a reactive streams.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @since 5.0
*/
public abstract class BodyExtractors {
private static final ResolvableType FORM_MAP_TYPE =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
private static final ResolvableType MULTIPART_MAP_TYPE = ResolvableType.forClassWithGenerics(
MultiValueMap.class, String.class, Part.class);
private static final ResolvableType PART_TYPE = ResolvableType.forClass(Part.class);
private static final ResolvableType VOID_TYPE = ResolvableType.forClass(Void.class);
/**
* Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}.
* @param elementClass the class of element in the {@code Mono}
* @param the element type
* @return a {@code BodyExtractor} that reads a mono
*/
public static BodyExtractor, ReactiveHttpInputMessage> toMono(Class elementClass) {
return toMono(ResolvableType.forClass(elementClass));
}
/**
* Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}.
* The given {@link ParameterizedTypeReference} is used to pass generic type information, for
* instance when using the {@link org.springframework.web.reactive.function.client.WebClient WebClient}
*
* Mono<Map<String, String>> body = this.webClient
* .get()
* .uri("http://example.com")
* .exchange()
* .flatMap(r -> r.body(toMono(new ParameterizedTypeReference<Map<String,String>>() {})));
*
* @param typeReference a reference to the type of element in the {@code Mono}
* @param the element type
* @return a {@code BodyExtractor} that reads a mono
*/
public static BodyExtractor, ReactiveHttpInputMessage> toMono(
ParameterizedTypeReference typeReference) {
return toMono(ResolvableType.forType(typeReference.getType()));
}
static BodyExtractor, ReactiveHttpInputMessage> toMono(ResolvableType elementType) {
return (inputMessage, context) -> readWithMessageReaders(inputMessage, context,
elementType,
(HttpMessageReader reader) -> {
Optional serverResponse = context.serverResponse();
if (serverResponse.isPresent() && inputMessage instanceof ServerHttpRequest) {
return reader.readMono(elementType, elementType, (ServerHttpRequest) inputMessage,
serverResponse.get(), context.hints());
}
else {
return reader.readMono(elementType, inputMessage, context.hints());
}
},
ex -> (inputMessage.getHeaders().getContentType() == null) ?
Mono.from(permitEmptyOrFail(inputMessage, ex)) : Mono.error(ex),
Mono::empty);
}
/**
* Return a {@code BodyExtractor} that reads into a Reactor {@link Flux}.
* @param elementClass the class of element in the {@code Flux}
* @param the element type
* @return a {@code BodyExtractor} that reads a flux
*/
public static BodyExtractor, ReactiveHttpInputMessage> toFlux(Class elementClass) {
return toFlux(ResolvableType.forClass(elementClass));
}
/**
* Return a {@code BodyExtractor} that reads into a Reactor {@link Flux}.
* The given {@link ParameterizedTypeReference} is used to pass generic type information, for
* instance when using the {@link org.springframework.web.reactive.function.client.WebClient WebClient}
*
* Flux<ServerSentEvent<String>> body = this.webClient
* .get()
* .uri("http://example.com")
* .exchange()
* .flatMap(r -> r.body(toFlux(new ParameterizedTypeReference<ServerSentEvent<String>>() {})));
*
* @param typeReference a reference to the type of element in the {@code Flux}
* @param the element type
* @return a {@code BodyExtractor} that reads a flux
*/
public static BodyExtractor, ReactiveHttpInputMessage> toFlux(
ParameterizedTypeReference typeReference) {
return toFlux(ResolvableType.forType(typeReference.getType()));
}
@SuppressWarnings("unchecked")
static BodyExtractor, ReactiveHttpInputMessage> toFlux(ResolvableType elementType) {
return (inputMessage, context) -> readWithMessageReaders(inputMessage, context,
elementType,
(HttpMessageReader reader) -> {
Optional serverResponse = context.serverResponse();
if (serverResponse.isPresent() && inputMessage instanceof ServerHttpRequest) {
return reader.read(elementType, elementType, (ServerHttpRequest) inputMessage,
serverResponse.get(), context.hints());
}
else {
return reader.read(elementType, inputMessage, context.hints());
}
},
ex -> (inputMessage.getHeaders().getContentType() == null) ?
permitEmptyOrFail(inputMessage, ex) : Flux.error(ex),
Flux::empty);
}
@SuppressWarnings("unchecked")
private static Flux permitEmptyOrFail(ReactiveHttpInputMessage message, UnsupportedMediaTypeException ex) {
return message.getBody().doOnNext(buffer -> {
throw ex;
}).map(o -> (T) o);
}
/**
* Return a {@code BodyExtractor} that reads form data into a {@link MultiValueMap}.
* @return a {@code BodyExtractor} that reads form data
*/
// Note that the returned BodyExtractor is parameterized to ServerHttpRequest, not
// ReactiveHttpInputMessage like other methods, since reading form data only typically happens on
// the server-side
public static BodyExtractor>, ServerHttpRequest> toFormData() {
return (request, context) -> {
ResolvableType type = FORM_MAP_TYPE;
HttpMessageReader> reader =
messageReader(type, MediaType.APPLICATION_FORM_URLENCODED, context);
return context.serverResponse()
.map(response -> reader.readMono(type, type, request, response, context.hints()))
.orElseGet(() -> reader.readMono(type, request, context.hints()));
};
}
/**
* Return a {@code BodyExtractor} that reads multipart (i.e. file upload) form data into a
* {@link MultiValueMap}.
* @return a {@code BodyExtractor} that reads multipart data
*/
// Note that the returned BodyExtractor is parameterized to ServerHttpRequest, not
// ReactiveHttpInputMessage like other methods, since reading form data only typically happens on
// the server-side
public static BodyExtractor>, ServerHttpRequest> toMultipartData() {
return (serverRequest, context) -> {
ResolvableType type = MULTIPART_MAP_TYPE;
HttpMessageReader> reader =
messageReader(type, MediaType.MULTIPART_FORM_DATA, context);
return context.serverResponse()
.map(response -> reader.readMono(type, type, serverRequest, response, context.hints()))
.orElseGet(() -> reader.readMono(type, serverRequest, context.hints()));
};
}
/**
* Return a {@code BodyExtractor} that reads multipart (i.e. file upload) form data into a
* {@link MultiValueMap}.
* @return a {@code BodyExtractor} that reads multipart data
*/
// Note that the returned BodyExtractor is parameterized to ServerHttpRequest, not
// ReactiveHttpInputMessage like other methods, since reading form data only typically happens on
// the server-side
public static BodyExtractor, ServerHttpRequest> toParts() {
return (serverRequest, context) -> {
ResolvableType type = PART_TYPE;
HttpMessageReader reader = messageReader(type, MediaType.MULTIPART_FORM_DATA, context);
return context.serverResponse()
.map(response -> reader.read(type, type, serverRequest, response, context.hints()))
.orElseGet(() -> reader.read(type, serverRequest, context.hints()));
};
}
/**
* Return a {@code BodyExtractor} that returns the body of the message as a {@link Flux} of
* {@link DataBuffer}s.
* Note that the returned buffers should be released after usage by calling
* {@link org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer)}
* @return a {@code BodyExtractor} that returns the body
* @see ReactiveHttpInputMessage#getBody()
*/
public static BodyExtractor, ReactiveHttpInputMessage> toDataBuffers() {
return (inputMessage, context) -> inputMessage.getBody();
}
private static > S readWithMessageReaders(
ReactiveHttpInputMessage inputMessage, BodyExtractor.Context context, ResolvableType elementType,
Function, S> readerFunction,
Function unsupportedError,
Supplier empty) {
if (VOID_TYPE.equals(elementType)) {
return empty.get();
}
MediaType contentType = contentType(inputMessage);
List> messageReaders = context.messageReaders();
return messageReaders.stream()
.filter(r -> r.canRead(elementType, contentType))
.findFirst()
.map(BodyExtractors::cast)
.map(readerFunction)
.orElseGet(() -> {
List supportedMediaTypes = messageReaders.stream()
.flatMap(reader -> reader.getReadableMediaTypes().stream())
.collect(Collectors.toList());
UnsupportedMediaTypeException error =
new UnsupportedMediaTypeException(contentType, supportedMediaTypes);
return unsupportedError.apply(error);
});
}
private static HttpMessageReader messageReader(ResolvableType elementType,
MediaType mediaType, BodyExtractor.Context context) {
return context.messageReaders().stream()
.filter(messageReader -> messageReader.canRead(elementType, mediaType))
.findFirst()
.map(BodyExtractors::cast)
.orElseThrow(() -> new IllegalStateException(
"Could not find HttpMessageReader that supports \"" + mediaType +
"\" and \"" + elementType + "\""));
}
private static MediaType contentType(HttpMessage message) {
MediaType result = message.getHeaders().getContentType();
return result != null ? result : MediaType.APPLICATION_OCTET_STREAM;
}
@SuppressWarnings("unchecked")
private static HttpMessageReader cast(HttpMessageReader messageReader) {
return (HttpMessageReader) messageReader;
}
}