Add ResolvableType to HttpEntity for multipart Publishers
This commit adds a ResolvableType field to HttpEntity, in order to support Publishers as multipart data. Without the type, the MultipartHttpMessageWriter does not know which delegate writer to use to write the part. Issue: SPR-16307
This commit is contained in:
@@ -16,7 +16,12 @@
|
||||
|
||||
package org.springframework.http;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@@ -67,6 +72,9 @@ public class HttpEntity<T> {
|
||||
@Nullable
|
||||
private final T body;
|
||||
|
||||
@Nullable
|
||||
private final ResolvableType bodyType;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new, empty {@code HttpEntity}.
|
||||
@@ -97,7 +105,18 @@ public class HttpEntity<T> {
|
||||
* @param headers the entity headers
|
||||
*/
|
||||
public HttpEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers) {
|
||||
this(body, null, headers);
|
||||
}
|
||||
|
||||
private HttpEntity(@Nullable T body, @Nullable ResolvableType bodyType,
|
||||
@Nullable MultiValueMap<String, String> headers) {
|
||||
this.body = body;
|
||||
|
||||
if (bodyType == null && body != null) {
|
||||
bodyType = ResolvableType.forClass(body.getClass());
|
||||
}
|
||||
this.bodyType = bodyType ;
|
||||
|
||||
HttpHeaders tempHeaders = new HttpHeaders();
|
||||
if (headers != null) {
|
||||
tempHeaders.putAll(headers);
|
||||
@@ -128,6 +147,13 @@ public class HttpEntity<T> {
|
||||
return (this.body != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the body.
|
||||
*/
|
||||
@Nullable
|
||||
public ResolvableType getBodyType() {
|
||||
return this.bodyType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
@@ -159,4 +185,44 @@ public class HttpEntity<T> {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
|
||||
// Static builder methods
|
||||
|
||||
/**
|
||||
* Create a new {@code HttpEntity} with the given {@link Publisher} as body, class contained in
|
||||
* {@code publisher}, and headers.
|
||||
* @param publisher the publisher to use as body
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @param headers the entity headers
|
||||
* @param <S> the type of the elements contained in the publisher
|
||||
* @param <P> the type of the {@code Publisher}
|
||||
* @return the created entity
|
||||
*/
|
||||
public static <S, P extends Publisher<S>> HttpEntity<P> fromPublisher(P publisher,
|
||||
Class<S> elementClass, @Nullable MultiValueMap<String, String> headers) {
|
||||
|
||||
Assert.notNull(publisher, "'publisher' must not be null");
|
||||
Assert.notNull(elementClass, "'elementClass' must not be null");
|
||||
return new HttpEntity<>(publisher, ResolvableType.forClass(elementClass), headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code HttpEntity} with the given {@link Publisher} as body, type contained in
|
||||
* {@code publisher}, and headers.
|
||||
* @param publisher the publisher to use as body
|
||||
* @param typeReference the type of elements contained in the publisher
|
||||
* @param headers the entity headers
|
||||
* @param <S> the type of the elements contained in the publisher
|
||||
* @param <P> the type of the {@code Publisher}
|
||||
* @return the created entity
|
||||
*/
|
||||
public static <S, P extends Publisher<S>> HttpEntity<P> fromPublisher(P publisher,
|
||||
ParameterizedTypeReference<S> typeReference,
|
||||
@Nullable MultiValueMap<String, String> headers) {
|
||||
|
||||
Assert.notNull(publisher, "'publisher' must not be null");
|
||||
Assert.notNull(typeReference, "'typeReference' must not be null");
|
||||
return new HttpEntity<>(publisher, ResolvableType.forType(typeReference), headers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -96,6 +100,11 @@ public final class MultipartBodyBuilder {
|
||||
Assert.hasLength(name, "'name' must not be empty");
|
||||
Assert.notNull(part, "'part' must not be null");
|
||||
|
||||
if (part instanceof Publisher) {
|
||||
throw new IllegalArgumentException("Use publisher(String, Publisher, Class) or " +
|
||||
"publisher(String, Publisher, ParameterizedTypeReference) for adding Publisher parts");
|
||||
}
|
||||
|
||||
Object partBody;
|
||||
HttpHeaders partHeaders = new HttpHeaders();
|
||||
|
||||
@@ -116,6 +125,54 @@ public final class MultipartBodyBuilder {
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link Publisher} part to this builder, allowing for further header customization with
|
||||
* the returned {@link PartBuilder}.
|
||||
* @param name the name of the part to add (may not be empty)
|
||||
* @param publisher the contents of the part to add
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @return a builder that allows for further header customization
|
||||
*/
|
||||
public <T, P extends Publisher<T>> PartBuilder asyncPart(String name, P publisher,
|
||||
Class<T> elementClass) {
|
||||
|
||||
Assert.notNull(elementClass, "'elementClass' must not be null");
|
||||
ResolvableType elementType = ResolvableType.forClass(elementClass);
|
||||
Assert.hasLength(name, "'name' must not be empty");
|
||||
Assert.notNull(publisher, "'publisher' must not be null");
|
||||
Assert.notNull(elementType, "'elementType' must not be null");
|
||||
|
||||
HttpHeaders partHeaders = new HttpHeaders();
|
||||
PublisherClassPartBuilder<T, P> builder =
|
||||
new PublisherClassPartBuilder<>(publisher, elementClass, partHeaders);
|
||||
this.parts.add(name, builder);
|
||||
return builder;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link Publisher} part to this builder, allowing for further header customization with
|
||||
* the returned {@link PartBuilder}.
|
||||
* @param name the name of the part to add (may not be empty)
|
||||
* @param publisher the contents of the part to add
|
||||
* @param elementType the type of elements contained in the publisher
|
||||
* @return a builder that allows for further header customization
|
||||
*/
|
||||
public <T, P extends Publisher<T>> PartBuilder asyncPart(String name, P publisher,
|
||||
ParameterizedTypeReference<T> elementType) {
|
||||
|
||||
Assert.notNull(elementType, "'elementType' must not be null");
|
||||
ResolvableType elementType1 = ResolvableType.forType(elementType);
|
||||
Assert.hasLength(name, "'name' must not be empty");
|
||||
Assert.notNull(publisher, "'publisher' must not be null");
|
||||
Assert.notNull(elementType1, "'elementType' must not be null");
|
||||
|
||||
HttpHeaders partHeaders = new HttpHeaders();
|
||||
PublisherTypReferencePartBuilder<T, P> builder =
|
||||
new PublisherTypReferencePartBuilder<>(publisher, elementType, partHeaders);
|
||||
this.parts.add(name, builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder interface that allows for customization of part headers.
|
||||
@@ -136,10 +193,9 @@ public final class MultipartBodyBuilder {
|
||||
private static class DefaultPartBuilder implements PartBuilder {
|
||||
|
||||
@Nullable
|
||||
private final Object body;
|
||||
|
||||
private final HttpHeaders headers;
|
||||
protected final Object body;
|
||||
|
||||
protected final HttpHeaders headers;
|
||||
|
||||
public DefaultPartBuilder(@Nullable Object body, HttpHeaders headers) {
|
||||
this.body = body;
|
||||
@@ -157,4 +213,44 @@ public final class MultipartBodyBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private static class PublisherClassPartBuilder<S, P extends Publisher<S>>
|
||||
extends DefaultPartBuilder {
|
||||
|
||||
private final Class<S> bodyType;
|
||||
|
||||
public PublisherClassPartBuilder(P body, Class<S> bodyType, HttpHeaders headers) {
|
||||
super(body, headers);
|
||||
this.bodyType = bodyType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public HttpEntity<?> build() {
|
||||
P body = (P) this.body;
|
||||
Assert.state(body != null, "'body' must not be null");
|
||||
return HttpEntity.fromPublisher(body, this.bodyType, this.headers);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PublisherTypReferencePartBuilder<S, P extends Publisher<S>>
|
||||
extends DefaultPartBuilder {
|
||||
|
||||
private final ParameterizedTypeReference<S> bodyType;
|
||||
|
||||
public PublisherTypReferencePartBuilder(P body, ParameterizedTypeReference<S> bodyType,
|
||||
HttpHeaders headers) {
|
||||
|
||||
super(body, headers);
|
||||
this.bodyType = bodyType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public HttpEntity<?> build() {
|
||||
P body = (P) this.body;
|
||||
Assert.state(body != null, "'body' must not be null");
|
||||
return HttpEntity.fromPublisher(body, this.bodyType, this.headers);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -230,31 +230,41 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
|
||||
MultipartHttpOutputMessage outputMessage = new MultipartHttpOutputMessage(this.bufferFactory, getCharset());
|
||||
|
||||
T body;
|
||||
ResolvableType bodyType = null;
|
||||
if (value instanceof HttpEntity) {
|
||||
outputMessage.getHeaders().putAll(((HttpEntity<T>) value).getHeaders());
|
||||
body = ((HttpEntity<T>) value).getBody();
|
||||
HttpEntity<T> httpEntity = (HttpEntity<T>) value;
|
||||
outputMessage.getHeaders().putAll(httpEntity.getHeaders());
|
||||
body = httpEntity.getBody();
|
||||
Assert.state(body != null, "MultipartHttpMessageWriter only supports HttpEntity with body");
|
||||
bodyType = httpEntity.getBodyType();
|
||||
}
|
||||
else {
|
||||
body = value;
|
||||
}
|
||||
|
||||
if (bodyType == null) {
|
||||
bodyType = ResolvableType.forClass(body.getClass());
|
||||
}
|
||||
|
||||
String filename = (body instanceof Resource ? ((Resource) body).getFilename() : null);
|
||||
outputMessage.getHeaders().setContentDispositionFormData(name, filename);
|
||||
|
||||
ResolvableType bodyType = ResolvableType.forClass(body.getClass());
|
||||
MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
|
||||
final ResolvableType finalBodyType = bodyType;
|
||||
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream()
|
||||
.filter(partWriter -> partWriter.canWrite(bodyType, contentType))
|
||||
.filter(partWriter -> partWriter.canWrite(finalBodyType, contentType))
|
||||
.findFirst();
|
||||
|
||||
if (!writer.isPresent()) {
|
||||
return Flux.error(new CodecException("No suitable writer found for part: " + name));
|
||||
}
|
||||
|
||||
Publisher<T> bodyPublisher =
|
||||
body instanceof Publisher ? (Publisher<T>) body : Mono.just(body);
|
||||
|
||||
Mono<Void> partWritten = ((HttpMessageWriter<T>) writer.get())
|
||||
.write(Mono.just(body), bodyType, contentType, outputMessage, Collections.emptyMap());
|
||||
.write(bodyPublisher, bodyType, contentType, outputMessage, Collections.emptyMap());
|
||||
|
||||
// partWritten.subscribe() is required in order to make sure MultipartHttpOutputMessage#getBody()
|
||||
// returns a non-null value (occurs with ResourceHttpMessageWriter that invokes
|
||||
|
||||
Reference in New Issue
Block a user