Browse Source

Support for decoding @RequestPart content

Issue: SPR-15515
pull/726/merge
Rossen Stoyanchev 9 years ago
parent
commit
2d37c966b2
  1. 28
      spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java
  2. 2
      spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java
  3. 39
      spring-web/src/main/java/org/springframework/http/codec/DefaultClientCodecConfigurer.java
  4. 2
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java
  5. 121
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestPartMethodArgumentResolver.java
  6. 7
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java
  7. 154
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java

28
spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java

@ -17,6 +17,7 @@ @@ -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 { @@ -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 { @@ -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);
}
}

2
spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java

@ -108,7 +108,7 @@ public interface CodecConfigurer { @@ -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

39
spring-web/src/main/java/org/springframework/http/codec/DefaultClientCodecConfigurer.java

@ -16,9 +16,11 @@ @@ -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 @@ -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 @@ -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 @@ -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;
}
}
}

2
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java

@ -134,10 +134,10 @@ class ControllerMethodResolver { @@ -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));

121
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestPartMethodArgumentResolver.java

@ -16,80 +16,131 @@ @@ -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 {
/**
* Class constructor with a default resolution mode flag.
* @param registry for checking reactive type wrappers
*/
public RequestPartMethodArgumentResolver(ReactiveAdapterRegistry registry) {
super(null, registry);
public class RequestPartMethodArgumentResolver extends AbstractMessageReaderArgumentResolver {
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) {
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
boolean isRequired = requestPart == null || requestPart.required();
String name = getPartName(parameter, requestPart);
Flux<Part> partFlux = getPartValues(name, exchange);
if (isRequired) {
partFlux = partFlux.switchIfEmpty(Flux.error(getMissingPartException(name, parameter)));
}
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);
});
}
@Override
protected Mono<Object> resolveName(String name, MethodParameter param, ServerWebExchange exchange) {
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;
}
Mono<Object> partsMono = exchange.getMultipartData()
private Flux<Part> getPartValues(String name, ServerWebExchange exchange) {
return 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);
.flatMapIterable(map -> map.getOrDefault(name, Collections.emptyList()));
}
@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 ServerWebInputException getMissingPartException(String name, MethodParameter param) {
String reason = "Required request part '" + name + "' is not present";
return new ServerWebInputException(reason, param);
}
private static class RequestPartNamedValueInfo extends NamedValueInfo {
private static class PartServerHttpRequest extends ServerHttpRequestDecorator {
private final Part part;
public PartServerHttpRequest(ServerHttpRequest delegate, Part part) {
super(delegate);
this.part = part;
}
RequestPartNamedValueInfo() {
super("", false, ValueConstants.DEFAULT_NONE);
@Override
public HttpHeaders getHeaders() {
return this.part.headers();
}
RequestPartNamedValueInfo(RequestPart annotation) {
super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
@Override
public Flux<DataBuffer> getBody() {
return this.part.content();
}
}

7
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java

@ -47,7 +47,8 @@ import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod @@ -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 { @@ -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 { @@ -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 { @@ -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());

154
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java

@ -16,10 +16,11 @@ @@ -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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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,89 +149,142 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes @@ -132,89 +149,142 @@ 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;
}
@Configuration
@EnableWebFlux
@SuppressWarnings("unused")
static class TestConfiguration {
@Bean
public MultipartController testController() {
return new MultipartController();
}
}
@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());
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>> parts) {
return parts.map(map -> map.toSingleValueMap().entrySet().stream()
.map(Map.Entry::getKey).sorted().collect(Collectors.joining(",", "Map[", "]")));
Mono<String> requestBodyMap(@RequestBody Mono<MultiValueMap<String, Part>> partsMono) {
return partsMono.map(MultipartIntegrationTests::partMapDescription);
}
@PostMapping("/requestBodyFlux")
Mono<String> requestBodyFlux(@RequestBody Flux<Part> parts) {
return parts.map(Part::name).collectList()
.map(names -> names.stream().sorted().collect(Collectors.joining(",", "Flux[", "]")));
return partFluxDescription(parts);
}
@PostMapping("/modelAttribute")
String modelAttribute(@ModelAttribute TestBean testBean) {
return testBean.toString();
String modelAttribute(@ModelAttribute FormBean formBean) {
return formBean.toString();
}
}
static class TestBean {
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 barPart;
private String fieldPart;
private FilePart fooPart;
private List<FilePart> fileParts;
public String getBarPart() {
return this.barPart;
public String getFieldPart() {
return this.fieldPart;
}
public void setBarPart(String barPart) {
this.barPart = barPart;
public void setFieldPart(String fieldPart) {
this.fieldPart = fieldPart;
}
public FilePart getFooPart() {
return this.fooPart;
public List<FilePart> getFileParts() {
return this.fileParts;
}
public void setFooPart(FilePart fooPart) {
this.fooPart = fooPart;
public void setFileParts(List<FilePart> fileParts) {
this.fileParts = fileParts;
}
@Override
public String toString() {
return "TestBean[barPart=" + getBarPart() + ",fooPart=" + getFooPart().filename() + "]";
return "FormBean[" + getFieldPart() + "," + partListDescription(getFileParts()) + "]";
}
}
private static class Person {
@Configuration
@EnableWebFlux
@SuppressWarnings("unused")
static class TestConfiguration {
private String name;
@Bean
public MultipartController multipartController() {
return new MultipartController();
@JsonCreator
public Person(@JsonProperty("name") String name) {
this.name = name;
}
public String getName() {
return name;
}
}

Loading…
Cancel
Save