Support for decoding @RequestPart content
Issue: SPR-15515
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
package org.springframework.http.codec;
|
||||
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
|
||||
/**
|
||||
* Helps to configure a list of client-side HTTP message readers and writers
|
||||
@@ -55,6 +56,13 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
|
||||
*/
|
||||
interface ClientDefaultCodecsConfigurer extends DefaultCodecsConfigurer {
|
||||
|
||||
/**
|
||||
* Configure encoders or writers for use with
|
||||
* {@link org.springframework.http.codec.multipart.MultipartHttpMessageWriter
|
||||
* MultipartHttpMessageWriter}.
|
||||
*/
|
||||
MultipartCodecsConfigurer multipartCodecs();
|
||||
|
||||
/**
|
||||
* Configure the {@code Decoder} to use for Server-Sent Events.
|
||||
* <p>By default the {@link #jackson2Decoder} override is used for SSE.
|
||||
@@ -63,5 +71,25 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
|
||||
void serverSentEventDecoder(Decoder<?> decoder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry and container for multipart HTTP message writers.
|
||||
*/
|
||||
interface MultipartCodecsConfigurer {
|
||||
|
||||
/**
|
||||
* Add a Part {@code Encoder}, internally wrapped with
|
||||
* {@link EncoderHttpMessageWriter}.
|
||||
* @param encoder the encoder to add
|
||||
*/
|
||||
MultipartCodecsConfigurer encoder(Encoder<?> encoder);
|
||||
|
||||
/**
|
||||
* Add a Part {@link HttpMessageWriter}. For writers of type
|
||||
* {@link EncoderHttpMessageWriter} consider using the shortcut
|
||||
* {@link #encoder(Encoder)} instead.
|
||||
* @param writer the writer to add
|
||||
*/
|
||||
MultipartCodecsConfigurer writer(HttpMessageWriter<?> writer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public interface CodecConfigurer {
|
||||
void reader(HttpMessageReader<?> reader);
|
||||
|
||||
/**
|
||||
* Add a custom {@link HttpMessageWriter}. For readers of type
|
||||
* Add a custom {@link HttpMessageWriter}. For writers of type
|
||||
* {@link EncoderHttpMessageWriter} consider using the shortcut
|
||||
* {@link #encoder(Encoder)} instead.
|
||||
* @param writer the writer to add
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package org.springframework.http.codec;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||
@@ -48,6 +50,17 @@ class DefaultClientCodecConfigurer extends DefaultCodecConfigurer implements Cli
|
||||
extends AbstractDefaultCodecsConfigurer
|
||||
implements ClientCodecConfigurer.ClientDefaultCodecsConfigurer {
|
||||
|
||||
private DefaultMultipartCodecsConfigurer multipartCodecs;
|
||||
|
||||
|
||||
@Override
|
||||
public MultipartCodecsConfigurer multipartCodecs() {
|
||||
if (this.multipartCodecs == null) {
|
||||
this.multipartCodecs = new DefaultMultipartCodecsConfigurer();
|
||||
}
|
||||
return this.multipartCodecs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serverSentEventDecoder(Decoder<?> decoder) {
|
||||
HttpMessageReader<?> reader = new ServerSentEventHttpMessageReader(decoder);
|
||||
@@ -58,7 +71,10 @@ class DefaultClientCodecConfigurer extends DefaultCodecConfigurer implements Cli
|
||||
protected void addTypedWritersTo(List<HttpMessageWriter<?>> result) {
|
||||
super.addTypedWritersTo(result);
|
||||
addWriterTo(result, FormHttpMessageWriter::new);
|
||||
addWriterTo(result, MultipartHttpMessageWriter::new);
|
||||
addWriterTo(result, () -> findWriter(MultipartHttpMessageWriter.class,
|
||||
() -> this.multipartCodecs != null ?
|
||||
new MultipartHttpMessageWriter(this.multipartCodecs.getWriters()) :
|
||||
new MultipartHttpMessageWriter()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,7 +105,28 @@ class DefaultClientCodecConfigurer extends DefaultCodecConfigurer implements Cli
|
||||
addReaderTo(result,
|
||||
() -> new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(false)));
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultMultipartCodecsConfigurer implements MultipartCodecsConfigurer {
|
||||
|
||||
private final List<HttpMessageWriter<?>> writers = new ArrayList<>();
|
||||
|
||||
|
||||
@Override
|
||||
public MultipartCodecsConfigurer encoder(Encoder<?> encoder) {
|
||||
writer(new EncoderHttpMessageWriter<>(encoder));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipartCodecsConfigurer writer(HttpMessageWriter<?> writer) {
|
||||
this.writers.add(writer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<HttpMessageWriter<?>> getWriters() {
|
||||
return this.writers;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -134,10 +134,10 @@ class ControllerMethodResolver {
|
||||
// Annotation-based...
|
||||
registrar.add(new RequestParamMethodArgumentResolver(beanFactory, reactiveRegistry, false));
|
||||
registrar.add(new RequestParamMapMethodArgumentResolver(reactiveRegistry));
|
||||
registrar.add(new RequestPartMethodArgumentResolver(reactiveRegistry));
|
||||
registrar.add(new PathVariableMethodArgumentResolver(beanFactory, reactiveRegistry));
|
||||
registrar.add(new PathVariableMapMethodArgumentResolver(reactiveRegistry));
|
||||
registrar.addIfRequestBody(readers -> new RequestBodyArgumentResolver(readers, reactiveRegistry));
|
||||
registrar.addIfRequestBody(readers -> new RequestPartMethodArgumentResolver(readers, reactiveRegistry));
|
||||
registrar.addIfModelAttribute(() -> new ModelAttributeMethodArgumentResolver(reactiveRegistry, false));
|
||||
registrar.add(new RequestHeaderMethodArgumentResolver(beanFactory, reactiveRegistry));
|
||||
registrar.add(new RequestHeaderMapMethodArgumentResolver(reactiveRegistry));
|
||||
|
||||
@@ -16,80 +16,131 @@
|
||||
|
||||
package org.springframework.web.reactive.result.method.annotation;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
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.ServerHttpRequestDecorator;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.ValueConstants;
|
||||
import org.springframework.web.reactive.BindingContext;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
|
||||
/**
|
||||
* Resolver for method arguments annotated with @{@link RequestPart}.
|
||||
* Resolver for {@code @RequestPart} arguments where the named part is decoded
|
||||
* much like an {@code @RequestBody} argument but based on the content of an
|
||||
* individual part instead. The arguments may be wrapped with a reactive type
|
||||
* for a single value (e.g. Reactor {@code Mono}, RxJava {@code Single}).
|
||||
*
|
||||
* <p>This resolver also supports arguments of type {@link Part} which may be
|
||||
* wrapped with are reactive type for a single or multiple values.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class RequestPartMethodArgumentResolver extends AbstractNamedValueArgumentResolver {
|
||||
public class RequestPartMethodArgumentResolver extends AbstractMessageReaderArgumentResolver {
|
||||
|
||||
/**
|
||||
* Class constructor with a default resolution mode flag.
|
||||
* @param registry for checking reactive type wrappers
|
||||
*/
|
||||
public RequestPartMethodArgumentResolver(ReactiveAdapterRegistry registry) {
|
||||
super(null, registry);
|
||||
|
||||
public RequestPartMethodArgumentResolver(List<HttpMessageReader<?>> readers,
|
||||
ReactiveAdapterRegistry registry) {
|
||||
|
||||
super(readers, registry);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(RequestPart.class);
|
||||
return parameter.hasParameterAnnotation(RequestPart.class) ||
|
||||
checkParameterType(parameter, Part.class::isAssignableFrom);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
|
||||
RequestPart ann = parameter.getParameterAnnotation(RequestPart.class);
|
||||
return (ann != null ? new RequestPartNamedValueInfo(ann) : new RequestPartNamedValueInfo());
|
||||
}
|
||||
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
@Override
|
||||
protected Mono<Object> resolveName(String name, MethodParameter param, ServerWebExchange exchange) {
|
||||
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
|
||||
boolean isRequired = requestPart == null || requestPart.required();
|
||||
String name = getPartName(parameter, requestPart);
|
||||
|
||||
Mono<Object> partsMono = exchange.getMultipartData()
|
||||
.filter(map -> !CollectionUtils.isEmpty(map.get(name)))
|
||||
.map(map -> {
|
||||
List<Part> parts = map.get(name);
|
||||
return parts.size() == 1 ? parts.get(0) : parts;
|
||||
});
|
||||
|
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(param.getParameterType());
|
||||
return (adapter != null ? Mono.just(adapter.fromPublisher(partsMono)) : partsMono);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMissingValue(String name, MethodParameter param, ServerWebExchange exchange) {
|
||||
String type = param.getNestedParameterType().getSimpleName();
|
||||
String reason = "Required " + type + " parameter '" + name + "' is not present";
|
||||
throw new ServerWebInputException(reason, param);
|
||||
}
|
||||
|
||||
|
||||
private static class RequestPartNamedValueInfo extends NamedValueInfo {
|
||||
|
||||
RequestPartNamedValueInfo() {
|
||||
super("", false, ValueConstants.DEFAULT_NONE);
|
||||
Flux<Part> partFlux = getPartValues(name, exchange);
|
||||
if (isRequired) {
|
||||
partFlux = partFlux.switchIfEmpty(Flux.error(getMissingPartException(name, parameter)));
|
||||
}
|
||||
|
||||
RequestPartNamedValueInfo(RequestPart annotation) {
|
||||
super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
|
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(parameter.getParameterType());
|
||||
MethodParameter elementType = adapter != null ? parameter.nested() : parameter;
|
||||
|
||||
if (Part.class.isAssignableFrom(elementType.getNestedParameterType())) {
|
||||
if (adapter != null) {
|
||||
partFlux = adapter.isMultiValue() ? partFlux : partFlux.take(1);
|
||||
return Mono.just(adapter.fromPublisher(partFlux));
|
||||
}
|
||||
else {
|
||||
return partFlux.next().cast(Object.class);
|
||||
}
|
||||
}
|
||||
|
||||
return partFlux.next().flatMap(part -> {
|
||||
ServerHttpRequest partRequest = new PartServerHttpRequest(exchange.getRequest(), part);
|
||||
ServerWebExchange partExchange = exchange.mutate().request(partRequest).build();
|
||||
return readBody(parameter, isRequired, bindingContext, partExchange);
|
||||
});
|
||||
}
|
||||
|
||||
private String getPartName(MethodParameter methodParam, RequestPart requestPart) {
|
||||
String partName = (requestPart != null ? requestPart.name() : "");
|
||||
if (partName.isEmpty()) {
|
||||
partName = methodParam.getParameterName();
|
||||
if (partName == null) {
|
||||
throw new IllegalArgumentException("Request part name for argument type [" +
|
||||
methodParam.getNestedParameterType().getName() +
|
||||
"] not specified, and parameter name information not found in class file either.");
|
||||
}
|
||||
}
|
||||
return partName;
|
||||
}
|
||||
|
||||
private Flux<Part> getPartValues(String name, ServerWebExchange exchange) {
|
||||
return exchange.getMultipartData()
|
||||
.filter(map -> !CollectionUtils.isEmpty(map.get(name)))
|
||||
.flatMapIterable(map -> map.getOrDefault(name, Collections.emptyList()));
|
||||
}
|
||||
|
||||
private ServerWebInputException getMissingPartException(String name, MethodParameter param) {
|
||||
String reason = "Required request part '" + name + "' is not present";
|
||||
return new ServerWebInputException(reason, param);
|
||||
}
|
||||
|
||||
|
||||
private static class PartServerHttpRequest extends ServerHttpRequestDecorator {
|
||||
|
||||
private final Part part;
|
||||
|
||||
public PartServerHttpRequest(ServerHttpRequest delegate, Part part) {
|
||||
super(delegate);
|
||||
this.part = part;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.part.headers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> getBody() {
|
||||
return this.part.content();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ControllerMethodResolver}.
|
||||
@@ -92,10 +93,10 @@ public class ControllerMethodResolverTests {
|
||||
AtomicInteger index = new AtomicInteger(-1);
|
||||
assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestBodyArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestHeaderMapMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
@@ -131,7 +132,6 @@ public class ControllerMethodResolverTests {
|
||||
AtomicInteger index = new AtomicInteger(-1);
|
||||
assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
@@ -198,7 +198,6 @@ public class ControllerMethodResolverTests {
|
||||
AtomicInteger index = new AtomicInteger(-1);
|
||||
assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass());
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
|
||||
package org.springframework.web.reactive.result.method.annotation;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -29,13 +30,17 @@ import reactor.test.StepVerifier;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
@@ -50,6 +55,7 @@ import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
@@ -64,7 +70,16 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
super.setup();
|
||||
this.webClient = WebClient.create("http://localhost:" + this.port);
|
||||
|
||||
ExchangeStrategies strategies = ExchangeStrategies.builder().defaultCodecs(configurer ->
|
||||
configurer.multipartCodecs()
|
||||
.encoder(CharSequenceEncoder.allMimeTypes())
|
||||
.writer(new ResourceHttpMessageWriter())
|
||||
.encoder(new Jackson2JsonEncoder())).build();
|
||||
|
||||
this.webClient = WebClient.builder().baseUrl("http://localhost:" + this.port)
|
||||
.exchangeStrategies(strategies)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +117,8 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
|
||||
.bodyToMono(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(body -> assertEquals("Map[barPart,fooPart]", body))
|
||||
.consumeNextWith(body -> assertEquals(
|
||||
"Map[[fieldPart],[fileParts:foo.txt,fileParts:logo.png],[jsonPart]]", body))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@@ -117,7 +133,8 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
|
||||
.bodyToMono(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(body -> assertEquals("Flux[barPart,fooPart]", body))
|
||||
.consumeNextWith(body -> assertEquals(
|
||||
"[fieldPart,fileParts:foo.txt,fileParts:logo.png,jsonPart]", body))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@@ -132,80 +149,34 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
|
||||
.bodyToMono(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(body -> assertEquals("TestBean[barPart=bar,fooPart=foo.txt]", body))
|
||||
.consumeNextWith(body -> assertEquals(
|
||||
"FormBean[fieldValue,[fileParts:foo.txt,fileParts:logo.png]]", body))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
||||
private MultiValueMap<String, Object> generateBody() {
|
||||
HttpHeaders fooHeaders = new HttpHeaders();
|
||||
fooHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
ClassPathResource fooResource = new ClassPathResource("org/springframework/http/codec/multipart/foo.txt");
|
||||
HttpEntity<ClassPathResource> fooPart = new HttpEntity<>(fooResource, fooHeaders);
|
||||
HttpEntity<String> barPart = new HttpEntity<>("bar");
|
||||
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("fooPart", fooPart);
|
||||
parts.add("barPart", barPart);
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.TEXT_PLAIN);
|
||||
ClassPathResource resource = new ClassPathResource("foo.txt", MultipartHttpMessageReader.class);
|
||||
parts.add("fileParts", new HttpEntity<>(resource, headers));
|
||||
|
||||
headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.IMAGE_PNG);
|
||||
resource = new ClassPathResource("logo.png", getClass());
|
||||
parts.add("fileParts", new HttpEntity<>(resource, headers));
|
||||
|
||||
headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
parts.add("jsonPart", new HttpEntity<>(new Person("Jason"), headers));
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
@RestController
|
||||
@SuppressWarnings("unused")
|
||||
static class MultipartController {
|
||||
|
||||
@PostMapping("/requestPart")
|
||||
void requestPart(@RequestPart FormFieldPart barPart, @RequestPart Mono<FilePart> fooPart) {
|
||||
assertEquals("bar", barPart.value());
|
||||
assertEquals("foo.txt", fooPart.block(Duration.ZERO).filename());
|
||||
}
|
||||
|
||||
@PostMapping("/requestBodyMap")
|
||||
Mono<String> requestBodyMap(@RequestBody Mono<MultiValueMap<String, Part>> parts) {
|
||||
return parts.map(map -> map.toSingleValueMap().entrySet().stream()
|
||||
.map(Map.Entry::getKey).sorted().collect(Collectors.joining(",", "Map[", "]")));
|
||||
}
|
||||
|
||||
@PostMapping("/requestBodyFlux")
|
||||
Mono<String> requestBodyFlux(@RequestBody Flux<Part> parts) {
|
||||
return parts.map(Part::name).collectList()
|
||||
.map(names -> names.stream().sorted().collect(Collectors.joining(",", "Flux[", "]")));
|
||||
}
|
||||
|
||||
@PostMapping("/modelAttribute")
|
||||
String modelAttribute(@ModelAttribute TestBean testBean) {
|
||||
return testBean.toString();
|
||||
}
|
||||
}
|
||||
|
||||
static class TestBean {
|
||||
|
||||
private String barPart;
|
||||
|
||||
private FilePart fooPart;
|
||||
|
||||
|
||||
public String getBarPart() {
|
||||
return this.barPart;
|
||||
}
|
||||
|
||||
public void setBarPart(String barPart) {
|
||||
this.barPart = barPart;
|
||||
}
|
||||
|
||||
public FilePart getFooPart() {
|
||||
return this.fooPart;
|
||||
}
|
||||
|
||||
public void setFooPart(FilePart fooPart) {
|
||||
this.fooPart = fooPart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TestBean[barPart=" + getBarPart() + ",fooPart=" + getFooPart().filename() + "]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@@ -213,9 +184,108 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
public MultipartController multipartController() {
|
||||
public MultipartController testController() {
|
||||
return new MultipartController();
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@SuppressWarnings("unused")
|
||||
static class MultipartController {
|
||||
|
||||
@PostMapping("/requestPart")
|
||||
void requestPart(
|
||||
@RequestPart FormFieldPart fieldPart,
|
||||
@RequestPart("fileParts") FilePart fileParts,
|
||||
@RequestPart("fileParts") Mono<FilePart> filePartsMono,
|
||||
@RequestPart("fileParts") Flux<FilePart> filePartsFlux,
|
||||
@RequestPart("jsonPart") Person person,
|
||||
@RequestPart("jsonPart") Mono<Person> personMono) {
|
||||
|
||||
assertEquals("fieldValue", fieldPart.value());
|
||||
assertEquals("fileParts:foo.txt", partDescription(fileParts));
|
||||
assertEquals("fileParts:foo.txt", partDescription(filePartsMono.block()));
|
||||
assertEquals("[fileParts:foo.txt,fileParts:logo.png]", partFluxDescription(filePartsFlux).block());
|
||||
assertEquals("Jason", person.getName());
|
||||
assertEquals("Jason", personMono.block().getName());
|
||||
}
|
||||
|
||||
@PostMapping("/requestBodyMap")
|
||||
Mono<String> requestBodyMap(@RequestBody Mono<MultiValueMap<String, Part>> partsMono) {
|
||||
return partsMono.map(MultipartIntegrationTests::partMapDescription);
|
||||
}
|
||||
|
||||
@PostMapping("/requestBodyFlux")
|
||||
Mono<String> requestBodyFlux(@RequestBody Flux<Part> parts) {
|
||||
return partFluxDescription(parts);
|
||||
}
|
||||
|
||||
@PostMapping("/modelAttribute")
|
||||
String modelAttribute(@ModelAttribute FormBean formBean) {
|
||||
return formBean.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static String partMapDescription(MultiValueMap<String, Part> partsMap) {
|
||||
return partsMap.keySet().stream().sorted()
|
||||
.map(key -> partListDescription(partsMap.get(key)))
|
||||
.collect(Collectors.joining(",", "Map[", "]"));
|
||||
}
|
||||
|
||||
private static Mono<String> partFluxDescription(Flux<? extends Part> partsFlux) {
|
||||
return partsFlux.log().collectList().map(MultipartIntegrationTests::partListDescription);
|
||||
}
|
||||
|
||||
private static String partListDescription(List<? extends Part> parts) {
|
||||
return parts.stream().map(MultipartIntegrationTests::partDescription)
|
||||
.collect(Collectors.joining(",", "[", "]"));
|
||||
}
|
||||
|
||||
private static String partDescription(Part part) {
|
||||
return part instanceof FilePart ? part.name() + ":" + ((FilePart) part).filename() : part.name();
|
||||
}
|
||||
|
||||
static class FormBean {
|
||||
|
||||
private String fieldPart;
|
||||
|
||||
private List<FilePart> fileParts;
|
||||
|
||||
|
||||
public String getFieldPart() {
|
||||
return this.fieldPart;
|
||||
}
|
||||
|
||||
public void setFieldPart(String fieldPart) {
|
||||
this.fieldPart = fieldPart;
|
||||
}
|
||||
|
||||
public List<FilePart> getFileParts() {
|
||||
return this.fileParts;
|
||||
}
|
||||
|
||||
public void setFileParts(List<FilePart> fileParts) {
|
||||
this.fileParts = fileParts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FormBean[" + getFieldPart() + "," + partListDescription(getFileParts()) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private static class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
@JsonCreator
|
||||
public Person(@JsonProperty("name") String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user