Move ResolvableType from HttpEntity to PublisherEntity
This commit moves the ResolvableType field from HttpEntity to PublisherEntity, a new entity type defined in MultipartBodyBuilder. With this change, the scope of the ResolvableType is limited to multipart-related code, instead of becoming part of the complete HttpEntity hierarchy. Issue: SPR-16307
This commit is contained in:
parent
6e587d5c57
commit
6c3a64578c
|
|
@ -16,12 +16,7 @@
|
||||||
|
|
||||||
package org.springframework.http;
|
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.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
|
@ -72,9 +67,6 @@ public class HttpEntity<T> {
|
||||||
@Nullable
|
@Nullable
|
||||||
private final T body;
|
private final T body;
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private final ResolvableType bodyType;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new, empty {@code HttpEntity}.
|
* Create a new, empty {@code HttpEntity}.
|
||||||
|
|
@ -105,18 +97,7 @@ public class HttpEntity<T> {
|
||||||
* @param headers the entity headers
|
* @param headers the entity headers
|
||||||
*/
|
*/
|
||||||
public HttpEntity(@Nullable T body, @Nullable MultiValueMap<String, String> 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;
|
this.body = body;
|
||||||
|
|
||||||
if (bodyType == null && body != null) {
|
|
||||||
bodyType = ResolvableType.forClass(body.getClass());
|
|
||||||
}
|
|
||||||
this.bodyType = bodyType ;
|
|
||||||
|
|
||||||
HttpHeaders tempHeaders = new HttpHeaders();
|
HttpHeaders tempHeaders = new HttpHeaders();
|
||||||
if (headers != null) {
|
if (headers != null) {
|
||||||
tempHeaders.putAll(headers);
|
tempHeaders.putAll(headers);
|
||||||
|
|
@ -147,13 +128,6 @@ public class HttpEntity<T> {
|
||||||
return (this.body != null);
|
return (this.body != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the type of the body.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public ResolvableType getBodyType() {
|
|
||||||
return this.bodyType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(@Nullable Object other) {
|
public boolean equals(@Nullable Object other) {
|
||||||
|
|
@ -185,44 +159,4 @@ public class HttpEntity<T> {
|
||||||
return builder.toString();
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,8 @@ public final class MultipartBodyBuilder {
|
||||||
Assert.notNull(elementType, "'elementType' must not be null");
|
Assert.notNull(elementType, "'elementType' must not be null");
|
||||||
|
|
||||||
HttpHeaders partHeaders = new HttpHeaders();
|
HttpHeaders partHeaders = new HttpHeaders();
|
||||||
PublisherClassPartBuilder<T, P> builder =
|
PublisherPartBuilder<T, P> builder =
|
||||||
new PublisherClassPartBuilder<>(publisher, elementClass, partHeaders);
|
new PublisherPartBuilder<>(publisher, elementClass, partHeaders);
|
||||||
this.parts.add(name, builder);
|
this.parts.add(name, builder);
|
||||||
return builder;
|
return builder;
|
||||||
|
|
||||||
|
|
@ -155,21 +155,21 @@ public final class MultipartBodyBuilder {
|
||||||
* the returned {@link PartBuilder}.
|
* the returned {@link PartBuilder}.
|
||||||
* @param name the name of the part to add (may not be empty)
|
* @param name the name of the part to add (may not be empty)
|
||||||
* @param publisher the contents of the part to add
|
* @param publisher the contents of the part to add
|
||||||
* @param elementType the type of elements contained in the publisher
|
* @param typeReference the type of elements contained in the publisher
|
||||||
* @return a builder that allows for further header customization
|
* @return a builder that allows for further header customization
|
||||||
*/
|
*/
|
||||||
public <T, P extends Publisher<T>> PartBuilder asyncPart(String name, P publisher,
|
public <T, P extends Publisher<T>> PartBuilder asyncPart(String name, P publisher,
|
||||||
ParameterizedTypeReference<T> elementType) {
|
ParameterizedTypeReference<T> typeReference) {
|
||||||
|
|
||||||
Assert.notNull(elementType, "'elementType' must not be null");
|
Assert.notNull(typeReference, "'typeReference' must not be null");
|
||||||
ResolvableType elementType1 = ResolvableType.forType(elementType);
|
ResolvableType elementType1 = ResolvableType.forType(typeReference);
|
||||||
Assert.hasLength(name, "'name' must not be empty");
|
Assert.hasLength(name, "'name' must not be empty");
|
||||||
Assert.notNull(publisher, "'publisher' must not be null");
|
Assert.notNull(publisher, "'publisher' must not be null");
|
||||||
Assert.notNull(elementType1, "'elementType' must not be null");
|
Assert.notNull(elementType1, "'typeReference' must not be null");
|
||||||
|
|
||||||
HttpHeaders partHeaders = new HttpHeaders();
|
HttpHeaders partHeaders = new HttpHeaders();
|
||||||
PublisherTypReferencePartBuilder<T, P> builder =
|
PublisherPartBuilder<T, P> builder =
|
||||||
new PublisherTypReferencePartBuilder<>(publisher, elementType, partHeaders);
|
new PublisherPartBuilder<>(publisher, typeReference, partHeaders);
|
||||||
this.parts.add(name, builder);
|
this.parts.add(name, builder);
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
@ -213,43 +213,57 @@ public final class MultipartBodyBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class PublisherClassPartBuilder<S, P extends Publisher<S>>
|
private static class PublisherPartBuilder<S, P extends Publisher<S>>
|
||||||
extends DefaultPartBuilder {
|
extends DefaultPartBuilder {
|
||||||
|
|
||||||
private final Class<S> bodyType;
|
private final ResolvableType resolvableType;
|
||||||
|
|
||||||
public PublisherClassPartBuilder(P body, Class<S> bodyType, HttpHeaders headers) {
|
public PublisherPartBuilder(P body, Class<S> elementClass, HttpHeaders headers) {
|
||||||
super(body, headers);
|
super(body, headers);
|
||||||
this.bodyType = bodyType;
|
this.resolvableType = ResolvableType.forClass(elementClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public PublisherPartBuilder(P body, ParameterizedTypeReference<S> typeReference,
|
||||||
@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) {
|
HttpHeaders headers) {
|
||||||
|
|
||||||
super(body, headers);
|
super(body, headers);
|
||||||
this.bodyType = bodyType;
|
this.resolvableType = ResolvableType.forType(typeReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public HttpEntity<?> build() {
|
public HttpEntity<?> build() {
|
||||||
P body = (P) this.body;
|
P publisher = (P) this.body;
|
||||||
Assert.state(body != null, "'body' must not be null");
|
Assert.state(publisher != null, "'publisher' must not be null");
|
||||||
return HttpEntity.fromPublisher(body, this.bodyType, this.headers);
|
return new PublisherEntity<>(publisher, this.resolvableType, this.headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific subtype of {@link HttpEntity} for containing {@link Publisher}s as body.
|
||||||
|
* Exposes the type contained in the publisher through {@link #getResolvableType()}.
|
||||||
|
* @param <T> The type contained in the publisher
|
||||||
|
* @param <P> The publisher
|
||||||
|
*/
|
||||||
|
public static final class PublisherEntity<T, P extends Publisher<T>> extends HttpEntity<P> {
|
||||||
|
|
||||||
|
private final ResolvableType resolvableType;
|
||||||
|
|
||||||
|
|
||||||
|
PublisherEntity(P publisher, ResolvableType resolvableType,
|
||||||
|
@Nullable MultiValueMap<String, String> headers) {
|
||||||
|
super(publisher, headers);
|
||||||
|
Assert.notNull(publisher, "'publisher' must not be null");
|
||||||
|
Assert.notNull(resolvableType, "'resolvableType' must not be null");
|
||||||
|
this.resolvableType = resolvableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the resolvable type for this entry.
|
||||||
|
*/
|
||||||
|
public ResolvableType getResolvableType() {
|
||||||
|
return this.resolvableType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||||
|
import org.springframework.http.client.MultipartBodyBuilder;
|
||||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||||
import org.springframework.http.codec.FormHttpMessageWriter;
|
import org.springframework.http.codec.FormHttpMessageWriter;
|
||||||
import org.springframework.http.codec.HttpMessageWriter;
|
import org.springframework.http.codec.HttpMessageWriter;
|
||||||
|
|
@ -230,20 +231,25 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
|
||||||
MultipartHttpOutputMessage outputMessage = new MultipartHttpOutputMessage(this.bufferFactory, getCharset());
|
MultipartHttpOutputMessage outputMessage = new MultipartHttpOutputMessage(this.bufferFactory, getCharset());
|
||||||
|
|
||||||
T body;
|
T body;
|
||||||
ResolvableType bodyType = null;
|
ResolvableType resolvableType = null;
|
||||||
if (value instanceof HttpEntity) {
|
if (value instanceof HttpEntity) {
|
||||||
HttpEntity<T> httpEntity = (HttpEntity<T>) value;
|
HttpEntity<T> httpEntity = (HttpEntity<T>) value;
|
||||||
outputMessage.getHeaders().putAll(httpEntity.getHeaders());
|
outputMessage.getHeaders().putAll(httpEntity.getHeaders());
|
||||||
body = httpEntity.getBody();
|
body = httpEntity.getBody();
|
||||||
Assert.state(body != null, "MultipartHttpMessageWriter only supports HttpEntity with body");
|
Assert.state(body != null, "MultipartHttpMessageWriter only supports HttpEntity with body");
|
||||||
bodyType = httpEntity.getBodyType();
|
|
||||||
|
if (httpEntity instanceof MultipartBodyBuilder.PublisherEntity<?, ?>) {
|
||||||
|
MultipartBodyBuilder.PublisherEntity<?, ?> publisherEntity =
|
||||||
|
(MultipartBodyBuilder.PublisherEntity<?, ?>) httpEntity;
|
||||||
|
resolvableType = publisherEntity.getResolvableType();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
body = value;
|
body = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodyType == null) {
|
if (resolvableType == null) {
|
||||||
bodyType = ResolvableType.forClass(body.getClass());
|
resolvableType = ResolvableType.forClass(body.getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
String filename = (body instanceof Resource ? ((Resource) body).getFilename() : null);
|
String filename = (body instanceof Resource ? ((Resource) body).getFilename() : null);
|
||||||
|
|
@ -251,7 +257,7 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
|
||||||
|
|
||||||
MediaType contentType = outputMessage.getHeaders().getContentType();
|
MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||||
|
|
||||||
final ResolvableType finalBodyType = bodyType;
|
final ResolvableType finalBodyType = resolvableType;
|
||||||
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream()
|
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream()
|
||||||
.filter(partWriter -> partWriter.canWrite(finalBodyType, contentType))
|
.filter(partWriter -> partWriter.canWrite(finalBodyType, contentType))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
|
|
@ -264,7 +270,7 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
|
||||||
body instanceof Publisher ? (Publisher<T>) body : Mono.just(body);
|
body instanceof Publisher ? (Publisher<T>) body : Mono.just(body);
|
||||||
|
|
||||||
Mono<Void> partWritten = ((HttpMessageWriter<T>) writer.get())
|
Mono<Void> partWritten = ((HttpMessageWriter<T>) writer.get())
|
||||||
.write(bodyPublisher, bodyType, contentType, outputMessage, Collections.emptyMap());
|
.write(bodyPublisher, resolvableType, contentType, outputMessage, Collections.emptyMap());
|
||||||
|
|
||||||
// partWritten.subscribe() is required in order to make sure MultipartHttpOutputMessage#getBody()
|
// partWritten.subscribe() is required in order to make sure MultipartHttpOutputMessage#getBody()
|
||||||
// returns a non-null value (occurs with ResourceHttpMessageWriter that invokes
|
// returns a non-null value (occurs with ResourceHttpMessageWriter that invokes
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import org.junit.Test;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
|
@ -49,11 +50,12 @@ public class MultipartBodyBuilderTests {
|
||||||
builder.part("key", multipartData).header("foo", "bar");
|
builder.part("key", multipartData).header("foo", "bar");
|
||||||
builder.part("logo", logo).header("baz", "qux");
|
builder.part("logo", logo).header("baz", "qux");
|
||||||
builder.part("entity", entity).header("baz", "qux");
|
builder.part("entity", entity).header("baz", "qux");
|
||||||
builder.asyncPart("publisher", publisher, String.class).header("baz", "qux");
|
builder.asyncPart("publisherClass", publisher, String.class).header("baz", "qux");
|
||||||
|
builder.asyncPart("publisherPtr", publisher, new ParameterizedTypeReference<String>() {}).header("baz", "qux");
|
||||||
|
|
||||||
MultiValueMap<String, HttpEntity<?>> result = builder.build();
|
MultiValueMap<String, HttpEntity<?>> result = builder.build();
|
||||||
|
|
||||||
assertEquals(4, result.size());
|
assertEquals(5, result.size());
|
||||||
assertNotNull(result.getFirst("key"));
|
assertNotNull(result.getFirst("key"));
|
||||||
assertEquals(multipartData, result.getFirst("key").getBody());
|
assertEquals(multipartData, result.getFirst("key").getBody());
|
||||||
assertEquals("bar", result.getFirst("key").getHeaders().getFirst("foo"));
|
assertEquals("bar", result.getFirst("key").getHeaders().getFirst("foo"));
|
||||||
|
|
@ -67,11 +69,15 @@ public class MultipartBodyBuilderTests {
|
||||||
assertEquals("bar", result.getFirst("entity").getHeaders().getFirst("foo"));
|
assertEquals("bar", result.getFirst("entity").getHeaders().getFirst("foo"));
|
||||||
assertEquals("qux", result.getFirst("entity").getHeaders().getFirst("baz"));
|
assertEquals("qux", result.getFirst("entity").getHeaders().getFirst("baz"));
|
||||||
|
|
||||||
assertNotNull(result.getFirst("publisher"));
|
assertNotNull(result.getFirst("publisherClass"));
|
||||||
assertEquals(publisher, result.getFirst("publisher").getBody());
|
assertEquals(publisher, result.getFirst("publisherClass").getBody());
|
||||||
assertEquals(ResolvableType.forClass(String.class), result.getFirst("publisher").getBodyType());
|
assertEquals(ResolvableType.forClass(String.class), ((MultipartBodyBuilder.PublisherEntity<?,?>) result.getFirst("publisherClass")).getResolvableType());
|
||||||
assertEquals("bar", result.getFirst("entity").getHeaders().getFirst("foo"));
|
assertEquals("qux", result.getFirst("publisherClass").getHeaders().getFirst("baz"));
|
||||||
assertEquals("qux", result.getFirst("entity").getHeaders().getFirst("baz"));
|
|
||||||
|
assertNotNull(result.getFirst("publisherPtr"));
|
||||||
|
assertEquals(publisher, result.getFirst("publisherPtr").getBody());
|
||||||
|
assertEquals(ResolvableType.forClass(String.class), ((MultipartBodyBuilder.PublisherEntity<?,?>) result.getFirst("publisherPtr")).getResolvableType());
|
||||||
|
assertEquals("qux", result.getFirst("publisherPtr").getHeaders().getFirst("baz"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue