Skip to content

Commit 2d37c96

Browse files
committed
Support for decoding @RequestPart content
Issue: SPR-15515
1 parent be0b671 commit 2d37c96

File tree

7 files changed

+269
-84
lines changed

7 files changed

+269
-84
lines changed

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.http.codec;
1818

1919
import org.springframework.core.codec.Decoder;
20+
import org.springframework.core.codec.Encoder;
2021

2122
/**
2223
* Helps to configure a list of client-side HTTP message readers and writers
@@ -55,6 +56,13 @@ static ClientCodecConfigurer create() {
5556
*/
5657
interface ClientDefaultCodecsConfigurer extends DefaultCodecsConfigurer {
5758

59+
/**
60+
* Configure encoders or writers for use with
61+
* {@link org.springframework.http.codec.multipart.MultipartHttpMessageWriter
62+
* MultipartHttpMessageWriter}.
63+
*/
64+
MultipartCodecsConfigurer multipartCodecs();
65+
5866
/**
5967
* Configure the {@code Decoder} to use for Server-Sent Events.
6068
* <p>By default the {@link #jackson2Decoder} override is used for SSE.
@@ -63,5 +71,25 @@ interface ClientDefaultCodecsConfigurer extends DefaultCodecsConfigurer {
6371
void serverSentEventDecoder(Decoder<?> decoder);
6472
}
6573

74+
/**
75+
* Registry and container for multipart HTTP message writers.
76+
*/
77+
interface MultipartCodecsConfigurer {
78+
79+
/**
80+
* Add a Part {@code Encoder}, internally wrapped with
81+
* {@link EncoderHttpMessageWriter}.
82+
* @param encoder the encoder to add
83+
*/
84+
MultipartCodecsConfigurer encoder(Encoder<?> encoder);
85+
86+
/**
87+
* Add a Part {@link HttpMessageWriter}. For writers of type
88+
* {@link EncoderHttpMessageWriter} consider using the shortcut
89+
* {@link #encoder(Encoder)} instead.
90+
* @param writer the writer to add
91+
*/
92+
MultipartCodecsConfigurer writer(HttpMessageWriter<?> writer);
93+
}
6694

6795
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ interface CustomCodecsConfigurer {
108108
void reader(HttpMessageReader<?> reader);
109109

110110
/**
111-
* Add a custom {@link HttpMessageWriter}. For readers of type
111+
* Add a custom {@link HttpMessageWriter}. For writers of type
112112
* {@link EncoderHttpMessageWriter} consider using the shortcut
113113
* {@link #encoder(Encoder)} instead.
114114
* @param writer the writer to add

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package org.springframework.http.codec;
1818

19+
import java.util.ArrayList;
1920
import java.util.List;
2021

2122
import org.springframework.core.codec.Decoder;
23+
import org.springframework.core.codec.Encoder;
2224
import org.springframework.core.codec.StringDecoder;
2325
import org.springframework.http.codec.json.Jackson2JsonDecoder;
2426
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
@@ -48,6 +50,17 @@ private static class DefaultClientDefaultCodecsConfigurer
4850
extends AbstractDefaultCodecsConfigurer
4951
implements ClientCodecConfigurer.ClientDefaultCodecsConfigurer {
5052

53+
private DefaultMultipartCodecsConfigurer multipartCodecs;
54+
55+
56+
@Override
57+
public MultipartCodecsConfigurer multipartCodecs() {
58+
if (this.multipartCodecs == null) {
59+
this.multipartCodecs = new DefaultMultipartCodecsConfigurer();
60+
}
61+
return this.multipartCodecs;
62+
}
63+
5164
@Override
5265
public void serverSentEventDecoder(Decoder<?> decoder) {
5366
HttpMessageReader<?> reader = new ServerSentEventHttpMessageReader(decoder);
@@ -58,7 +71,10 @@ public void serverSentEventDecoder(Decoder<?> decoder) {
5871
protected void addTypedWritersTo(List<HttpMessageWriter<?>> result) {
5972
super.addTypedWritersTo(result);
6073
addWriterTo(result, FormHttpMessageWriter::new);
61-
addWriterTo(result, MultipartHttpMessageWriter::new);
74+
addWriterTo(result, () -> findWriter(MultipartHttpMessageWriter.class,
75+
() -> this.multipartCodecs != null ?
76+
new MultipartHttpMessageWriter(this.multipartCodecs.getWriters()) :
77+
new MultipartHttpMessageWriter()));
6278
}
6379

6480
@Override
@@ -89,7 +105,28 @@ protected void addStringReaderTo(List<HttpMessageReader<?>> result) {
89105
addReaderTo(result,
90106
() -> new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(false)));
91107
}
108+
}
109+
110+
private static class DefaultMultipartCodecsConfigurer implements MultipartCodecsConfigurer {
111+
112+
private final List<HttpMessageWriter<?>> writers = new ArrayList<>();
113+
114+
115+
@Override
116+
public MultipartCodecsConfigurer encoder(Encoder<?> encoder) {
117+
writer(new EncoderHttpMessageWriter<>(encoder));
118+
return this;
119+
}
92120

121+
@Override
122+
public MultipartCodecsConfigurer writer(HttpMessageWriter<?> writer) {
123+
this.writers.add(writer);
124+
return this;
125+
}
126+
127+
public List<HttpMessageWriter<?>> getWriters() {
128+
return this.writers;
129+
}
93130
}
94131

95132
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ private void addResolversTo(ArgumentResolverRegistrar registrar,
134134
// Annotation-based...
135135
registrar.add(new RequestParamMethodArgumentResolver(beanFactory, reactiveRegistry, false));
136136
registrar.add(new RequestParamMapMethodArgumentResolver(reactiveRegistry));
137-
registrar.add(new RequestPartMethodArgumentResolver(reactiveRegistry));
138137
registrar.add(new PathVariableMethodArgumentResolver(beanFactory, reactiveRegistry));
139138
registrar.add(new PathVariableMapMethodArgumentResolver(reactiveRegistry));
140139
registrar.addIfRequestBody(readers -> new RequestBodyArgumentResolver(readers, reactiveRegistry));
140+
registrar.addIfRequestBody(readers -> new RequestPartMethodArgumentResolver(readers, reactiveRegistry));
141141
registrar.addIfModelAttribute(() -> new ModelAttributeMethodArgumentResolver(reactiveRegistry, false));
142142
registrar.add(new RequestHeaderMethodArgumentResolver(beanFactory, reactiveRegistry));
143143
registrar.add(new RequestHeaderMapMethodArgumentResolver(reactiveRegistry));

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

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,80 +16,131 @@
1616

1717
package org.springframework.web.reactive.result.method.annotation;
1818

19+
import java.util.Collections;
1920
import java.util.List;
2021

22+
import reactor.core.publisher.Flux;
2123
import reactor.core.publisher.Mono;
2224

2325
import org.springframework.core.MethodParameter;
2426
import org.springframework.core.ReactiveAdapter;
2527
import org.springframework.core.ReactiveAdapterRegistry;
28+
import org.springframework.core.io.buffer.DataBuffer;
29+
import org.springframework.http.HttpHeaders;
30+
import org.springframework.http.codec.HttpMessageReader;
2631
import org.springframework.http.codec.multipart.Part;
32+
import org.springframework.http.server.reactive.ServerHttpRequest;
33+
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
2734
import org.springframework.util.CollectionUtils;
2835
import org.springframework.web.bind.annotation.RequestPart;
29-
import org.springframework.web.bind.annotation.ValueConstants;
36+
import org.springframework.web.reactive.BindingContext;
3037
import org.springframework.web.server.ServerWebExchange;
3138
import org.springframework.web.server.ServerWebInputException;
3239

3340
/**
34-
* Resolver for method arguments annotated with @{@link RequestPart}.
41+
* Resolver for {@code @RequestPart} arguments where the named part is decoded
42+
* much like an {@code @RequestBody} argument but based on the content of an
43+
* individual part instead. The arguments may be wrapped with a reactive type
44+
* for a single value (e.g. Reactor {@code Mono}, RxJava {@code Single}).
45+
*
46+
* <p>This resolver also supports arguments of type {@link Part} which may be
47+
* wrapped with are reactive type for a single or multiple values.
3548
*
36-
* @author Sebastien Deleuze
3749
* @author Rossen Stoyanchev
3850
* @since 5.0
3951
*/
40-
public class RequestPartMethodArgumentResolver extends AbstractNamedValueArgumentResolver {
41-
42-
/**
43-
* Class constructor with a default resolution mode flag.
44-
* @param registry for checking reactive type wrappers
45-
*/
46-
public RequestPartMethodArgumentResolver(ReactiveAdapterRegistry registry) {
47-
super(null, registry);
52+
public class RequestPartMethodArgumentResolver extends AbstractMessageReaderArgumentResolver {
53+
54+
55+
public RequestPartMethodArgumentResolver(List<HttpMessageReader<?>> readers,
56+
ReactiveAdapterRegistry registry) {
57+
58+
super(readers, registry);
4859
}
4960

5061

5162
@Override
5263
public boolean supportsParameter(MethodParameter parameter) {
53-
return parameter.hasParameterAnnotation(RequestPart.class);
64+
return parameter.hasParameterAnnotation(RequestPart.class) ||
65+
checkParameterType(parameter, Part.class::isAssignableFrom);
5466
}
5567

5668

5769
@Override
58-
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
59-
RequestPart ann = parameter.getParameterAnnotation(RequestPart.class);
60-
return (ann != null ? new RequestPartNamedValueInfo(ann) : new RequestPartNamedValueInfo());
70+
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
71+
ServerWebExchange exchange) {
72+
73+
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
74+
boolean isRequired = requestPart == null || requestPart.required();
75+
String name = getPartName(parameter, requestPart);
76+
77+
Flux<Part> partFlux = getPartValues(name, exchange);
78+
if (isRequired) {
79+
partFlux = partFlux.switchIfEmpty(Flux.error(getMissingPartException(name, parameter)));
80+
}
81+
82+
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(parameter.getParameterType());
83+
MethodParameter elementType = adapter != null ? parameter.nested() : parameter;
84+
85+
if (Part.class.isAssignableFrom(elementType.getNestedParameterType())) {
86+
if (adapter != null) {
87+
partFlux = adapter.isMultiValue() ? partFlux : partFlux.take(1);
88+
return Mono.just(adapter.fromPublisher(partFlux));
89+
}
90+
else {
91+
return partFlux.next().cast(Object.class);
92+
}
93+
}
94+
95+
return partFlux.next().flatMap(part -> {
96+
ServerHttpRequest partRequest = new PartServerHttpRequest(exchange.getRequest(), part);
97+
ServerWebExchange partExchange = exchange.mutate().request(partRequest).build();
98+
return readBody(parameter, isRequired, bindingContext, partExchange);
99+
});
61100
}
62101

63-
@Override
64-
protected Mono<Object> resolveName(String name, MethodParameter param, ServerWebExchange exchange) {
102+
private String getPartName(MethodParameter methodParam, RequestPart requestPart) {
103+
String partName = (requestPart != null ? requestPart.name() : "");
104+
if (partName.isEmpty()) {
105+
partName = methodParam.getParameterName();
106+
if (partName == null) {
107+
throw new IllegalArgumentException("Request part name for argument type [" +
108+
methodParam.getNestedParameterType().getName() +
109+
"] not specified, and parameter name information not found in class file either.");
110+
}
111+
}
112+
return partName;
113+
}
65114

66-
Mono<Object> partsMono = exchange.getMultipartData()
115+
private Flux<Part> getPartValues(String name, ServerWebExchange exchange) {
116+
return exchange.getMultipartData()
67117
.filter(map -> !CollectionUtils.isEmpty(map.get(name)))
68-
.map(map -> {
69-
List<Part> parts = map.get(name);
70-
return parts.size() == 1 ? parts.get(0) : parts;
71-
});
72-
73-
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(param.getParameterType());
74-
return (adapter != null ? Mono.just(adapter.fromPublisher(partsMono)) : partsMono);
118+
.flatMapIterable(map -> map.getOrDefault(name, Collections.emptyList()));
75119
}
76120

77-
@Override
78-
protected void handleMissingValue(String name, MethodParameter param, ServerWebExchange exchange) {
79-
String type = param.getNestedParameterType().getSimpleName();
80-
String reason = "Required " + type + " parameter '" + name + "' is not present";
81-
throw new ServerWebInputException(reason, param);
121+
private ServerWebInputException getMissingPartException(String name, MethodParameter param) {
122+
String reason = "Required request part '" + name + "' is not present";
123+
return new ServerWebInputException(reason, param);
82124
}
83125

84126

85-
private static class RequestPartNamedValueInfo extends NamedValueInfo {
127+
private static class PartServerHttpRequest extends ServerHttpRequestDecorator {
128+
129+
private final Part part;
130+
131+
public PartServerHttpRequest(ServerHttpRequest delegate, Part part) {
132+
super(delegate);
133+
this.part = part;
134+
}
86135

87-
RequestPartNamedValueInfo() {
88-
super("", false, ValueConstants.DEFAULT_NONE);
136+
@Override
137+
public HttpHeaders getHeaders() {
138+
return this.part.headers();
89139
}
90140

91-
RequestPartNamedValueInfo(RequestPart annotation) {
92-
super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
141+
@Override
142+
public Flux<DataBuffer> getBody() {
143+
return this.part.content();
93144
}
94145
}
95146

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
import org.springframework.web.server.ResponseStatusException;
4848
import org.springframework.web.server.ServerWebExchange;
4949

50-
import static org.junit.Assert.*;
50+
import static org.junit.Assert.assertEquals;
51+
import static org.junit.Assert.assertNotNull;
5152

5253
/**
5354
* Unit tests for {@link ControllerMethodResolver}.
@@ -92,10 +93,10 @@ public void requestMappingArgumentResolvers() throws Exception {
9293
AtomicInteger index = new AtomicInteger(-1);
9394
assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass());
9495
assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass());
95-
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
9696
assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass());
9797
assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass());
9898
assertEquals(RequestBodyArgumentResolver.class, next(resolvers, index).getClass());
99+
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
99100
assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
100101
assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass());
101102
assertEquals(RequestHeaderMapMethodArgumentResolver.class, next(resolvers, index).getClass());
@@ -131,7 +132,6 @@ public void modelAttributeArgumentResolvers() throws Exception {
131132
AtomicInteger index = new AtomicInteger(-1);
132133
assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass());
133134
assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass());
134-
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
135135
assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass());
136136
assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass());
137137
assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
@@ -198,7 +198,6 @@ public void exceptionHandlerArgumentResolvers() throws Exception {
198198
AtomicInteger index = new AtomicInteger(-1);
199199
assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass());
200200
assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass());
201-
assertEquals(RequestPartMethodArgumentResolver.class, next(resolvers, index).getClass());
202201
assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass());
203202
assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass());
204203
assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass());

0 commit comments

Comments
 (0)