org.springframework.web.reactive.function.BodyInserters 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.stream.Collectors;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Implementations of {@link BodyInserter} that write various bodies, such a reactive streams,
* server-sent events, resources, etc.
*
* @author Arjen Poutsma
* @since 5.0
*/
public abstract class BodyInserters {
private static final ResolvableType RESOURCE_TYPE =
ResolvableType.forClass(Resource.class);
private static final ResolvableType SERVER_SIDE_EVENT_TYPE =
ResolvableType.forClass(ServerSentEvent.class);
private static final ResolvableType FORM_TYPE =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
private static final ResolvableType MULTIPART_VALUE_TYPE = ResolvableType.forClassWithGenerics(
MultiValueMap.class, String.class, Object.class);
private static final BodyInserter EMPTY =
(response, context) -> response.setComplete();
/**
* Return an empty {@code BodyInserter} that writes nothing.
* @return an empty {@code BodyInserter}
*/
@SuppressWarnings("unchecked")
public static BodyInserter empty() {
return (BodyInserter)EMPTY;
}
/**
* Return a {@code BodyInserter} that writes the given single object.
* Note also that
* {@link org.springframework.web.reactive.function.client.WebClient WebClient} and
* {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}
* each offer a {@code syncBody(Object)} shortcut for providing an Object
* as the body.
* @param body the body of the response
* @return a {@code BodyInserter} that writes a single object
*/
public static BodyInserter fromObject(T body) {
return bodyInserterFor(Mono.just(body), ResolvableType.forInstance(body));
}
/**
* Return a {@code BodyInserter} that writes the given {@link Publisher}.
* Note also that
* {@link org.springframework.web.reactive.function.client.WebClient WebClient} and
* {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}
* each offer {@code body} shortcut methods for providing a Publisher as the body.
* @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}
* @return a {@code BodyInserter} that writes a {@code Publisher}
*/
public static > BodyInserter fromPublisher(
P publisher, Class elementClass) {
return bodyInserterFor(publisher, ResolvableType.forClass(elementClass));
}
/**
* Return a {@code BodyInserter} that writes the given {@link Publisher}.
* Note also that
* {@link org.springframework.web.reactive.function.client.WebClient WebClient} and
* {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}
* each offer {@code body} shortcut methods for providing a Publisher as the body.
* @param publisher the publisher to stream to the response body
* @param typeReference 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}
* @return a {@code BodyInserter} that writes a {@code Publisher}
*/
public static > BodyInserter fromPublisher(
P publisher, ParameterizedTypeReference typeReference) {
return bodyInserterFor(publisher, ResolvableType.forType(typeReference.getType()));
}
/**
* Return a {@code BodyInserter} that writes the given {@code Resource}.
* If the resource can be resolved to a {@linkplain Resource#getFile() file}, it will
* be copied using zero-copy.
* @param resource the resource to write to the output message
* @param the type of the {@code Resource}
* @return a {@code BodyInserter} that writes a {@code Publisher}
*/
public static BodyInserter fromResource(T resource) {
return (outputMessage, context) -> {
Mono inputStream = Mono.just(resource);
HttpMessageWriter messageWriter = resourceHttpMessageWriter(context);
Optional serverRequest = context.serverRequest();
if (serverRequest.isPresent() && outputMessage instanceof ServerHttpResponse) {
return messageWriter.write(inputStream, RESOURCE_TYPE, RESOURCE_TYPE, null,
serverRequest.get(), (ServerHttpResponse) outputMessage, context.hints());
}
else {
return messageWriter.write(inputStream, RESOURCE_TYPE, null, outputMessage, context.hints());
}
};
}
private static HttpMessageWriter resourceHttpMessageWriter(BodyInserter.Context context) {
return context.messageWriters().stream()
.filter(messageWriter -> messageWriter.canWrite(RESOURCE_TYPE, null))
.findFirst()
.map(BodyInserters::cast)
.orElseThrow(() -> new IllegalStateException(
"Could not find HttpMessageWriter that supports Resource objects"));
}
/**
* Return a {@code BodyInserter} that writes the given {@code ServerSentEvent} publisher.
* Note that a SSE {@code BodyInserter} can also be obtained by passing a stream of strings
* or POJOs (to be encoded as JSON) to {@link #fromPublisher(Publisher, Class)}, and specifying a
* {@link MediaType#TEXT_EVENT_STREAM text/event-stream} Content-Type.
* @param eventsPublisher the {@code ServerSentEvent} publisher to write to the response body
* @param the type of the elements contained in the {@link ServerSentEvent}
* @return a {@code BodyInserter} that writes a {@code ServerSentEvent} publisher
* @see Server-Sent Events W3C recommendation
*/
// Note that the returned BodyInserter is parameterized to ServerHttpResponse, not
// ReactiveHttpOutputMessage like other methods, since sending SSEs only typically happens on
// the server-side
public static >> BodyInserter fromServerSentEvents(
S eventsPublisher) {
return (serverResponse, context) -> {
HttpMessageWriter> messageWriter =
findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
return context.serverRequest()
.map(serverRequest -> messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM, serverRequest,
serverResponse, context.hints()))
.orElseGet(() -> messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
MediaType.TEXT_EVENT_STREAM, serverResponse, context.hints()));
};
}
/**
* Return a {@link FormInserter} that writes the given {@code MultiValueMap}
* as URL-encoded form data. The returned inserter allows for additional
* entries to be added via {@link FormInserter#with(String, Object)}.
*
* Note that you can also use the {@code syncBody(Object)} method in the
* request builders of both the {@code WebClient} and {@code WebTestClient}.
* In that case the setting of the request content type is also not required,
* just be sure the map contains String values only or otherwise it would be
* interpreted as a multipart request.
*
* @param formData the form data to write to the output message
* @return the inserter that allows adding more form data
*/
public static FormInserter fromFormData(MultiValueMap formData) {
return new DefaultFormInserter().with(formData);
}
/**
* Return a {@link FormInserter} that writes the given key-value pair as
* URL-encoded form data. The returned inserter allows for additional
* entries to be added via {@link FormInserter#with(String, Object)}.
* @param name the key to add to the form
* @param value the value to add to the form
* @return the inserter that allows adding more form data
*/
public static FormInserter fromFormData(String name, String value) {
Assert.notNull(name, "'name' must not be null");
Assert.notNull(value, "'value' must not be null");
return new DefaultFormInserter().with(name, value);
}
/**
* Return a {@link MultipartInserter} that writes the given
* {@code MultiValueMap} as multipart data. Values in the map can be an
* Object or an {@link HttpEntity}.
* Note that you can also build the multipart data externally with
* {@link MultipartBodyBuilder}, and pass the resulting map directly to the
* {@code syncBody(Object)} shortcut method in {@code WebClient}.
* @param multipartData the form data to write to the output message
* @return the inserter that allows adding more parts
* @see MultipartBodyBuilder
*/
public static MultipartInserter fromMultipartData(MultiValueMap multipartData) {
Assert.notNull(multipartData, "'multipartData' must not be null");
return new DefaultMultipartInserter().withInternal(multipartData);
}
/**
* Return a {@link MultipartInserter} that writes the given parts,
* as multipart data. Values in the map can be an Object or an
* {@link HttpEntity}.
* Note that you can also build the multipart data externally with
* {@link MultipartBodyBuilder}, and pass the resulting map directly to the
* {@code syncBody(Object)} shortcut method in {@code WebClient}.
* @param name the part name
* @param value the part value, an Object or {@code HttpEntity}
* @return the inserter that allows adding more parts
*/
public static MultipartInserter fromMultipartData(String name, Object value) {
Assert.notNull(name, "'name' must not be null");
Assert.notNull(value, "'value' must not be null");
return new DefaultMultipartInserter().with(name, value);
}
/**
* Return a {@link MultipartInserter} that writes the given asynchronous parts,
* as multipart data.
*
Note that you can also build the multipart data externally with
* {@link MultipartBodyBuilder}, and pass the resulting map directly to the
* {@code syncBody(Object)} shortcut method in {@code WebClient}.
* @param name the part name
* @param publisher the publisher that forms the part value
* @param elementClass the class contained in the {@code publisher}
* @return the inserter that allows adding more parts
*/
public static > MultipartInserter fromMultipartAsyncData(
String name, P publisher, Class elementClass) {
return new DefaultMultipartInserter().withPublisher(name, publisher, elementClass);
}
/**
* Variant of {@link #fromMultipartAsyncData(String, Publisher, Class)} that
* accepts a {@link ParameterizedTypeReference} for the element type, which
* allows specifying generic type information.
* Note that you can also build the multipart data externally with
* {@link MultipartBodyBuilder}, and pass the resulting map directly to the
* {@code syncBody(Object)} shortcut method in {@code WebClient}.
* @param name the part name
* @param publisher the publisher that forms the part value
* @param typeReference the type contained in the {@code publisher}
* @return the inserter that allows adding more parts
*/
public static > MultipartInserter fromMultipartAsyncData(
String name, P publisher, ParameterizedTypeReference typeReference) {
return new DefaultMultipartInserter().withPublisher(name, publisher, typeReference);
}
/**
* Return a {@code BodyInserter} that writes the given
* {@code Publisher} to the body.
* @param publisher the data buffer publisher to write
* @param the type of the publisher
* @return a {@code BodyInserter} that writes directly to the body
* @see ReactiveHttpOutputMessage#writeWith(Publisher)
*/
public static > BodyInserter fromDataBuffers(
T publisher) {
Assert.notNull(publisher, "Publisher must not be null");
return (outputMessage, context) -> outputMessage.writeWith(publisher);
}
private static , M extends ReactiveHttpOutputMessage> BodyInserter bodyInserterFor(
P body, ResolvableType bodyType) {
return (outputMessage, context) -> {
MediaType contentType = outputMessage.getHeaders().getContentType();
List> messageWriters = context.messageWriters();
return messageWriters.stream()
.filter(messageWriter -> messageWriter.canWrite(bodyType, contentType))
.findFirst()
.map(BodyInserters::cast)
.map(messageWriter -> {
Optional serverRequest = context.serverRequest();
if (serverRequest.isPresent() && outputMessage instanceof ServerHttpResponse) {
return messageWriter.write(body, bodyType, bodyType, contentType,
serverRequest.get(), (ServerHttpResponse) outputMessage,
context.hints());
}
else {
return messageWriter.write(body, bodyType, contentType, outputMessage, context.hints());
}
})
.orElseGet(() -> {
List supportedMediaTypes = messageWriters.stream()
.flatMap(reader -> reader.getWritableMediaTypes().stream())
.collect(Collectors.toList());
UnsupportedMediaTypeException error =
new UnsupportedMediaTypeException(contentType, supportedMediaTypes);
return Mono.error(error);
});
};
}
private static HttpMessageWriter findMessageWriter(
BodyInserter.Context context, ResolvableType type, MediaType mediaType) {
return context.messageWriters().stream()
.filter(messageWriter -> messageWriter.canWrite(type, mediaType))
.findFirst()
.map(BodyInserters::cast)
.orElseThrow(() -> new IllegalStateException(
"Could not find HttpMessageWriter that supports " + mediaType));
}
@SuppressWarnings("unchecked")
private static HttpMessageWriter cast(HttpMessageWriter messageWriter) {
return (HttpMessageWriter) messageWriter;
}
/**
* Extension of {@link BodyInserter} that allows for adding form data or
* multipart form data.
*/
public interface FormInserter extends BodyInserter, ClientHttpRequest> {
// FormInserter is parameterized to ClientHttpRequest (for client-side use only)
/**
* Adds the specified key-value pair to the form.
* @param key the key to be added
* @param value the value to be added
* @return this inserter for adding more parts
*/
FormInserter with(String key, T value);
/**
* Adds the specified values to the form.
* @param values the values to be added
* @return this inserter for adding more parts
*/
FormInserter with(MultiValueMap values);
}
/**
* Extension of {@link FormInserter} that allows for adding asynchronous parts.
*/
public interface MultipartInserter extends FormInserter