Skip to content

Commit b5089ac

Browse files
committed
Support @RequestBody Flux<Part> in WebFlux
This commit turns the Synchronoss NIO Multipart HttpMessageReader into a reader of Flux<Part> and creates a separate reader that aggregates the parts into a MultiValueMap<String, Part>. Issue: SPR-14546
1 parent d43dfc7 commit b5089ac

File tree

9 files changed

+198
-57
lines changed

9 files changed

+198
-57
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
import org.springframework.core.codec.Encoder;
2222
import org.springframework.core.codec.StringDecoder;
2323
import org.springframework.http.codec.json.Jackson2JsonEncoder;
24-
import org.springframework.http.codec.multipart.SynchronossMultipartHttpMessageReader;
24+
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
25+
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
2526
import org.springframework.util.ClassUtils;
2627

2728
/**
@@ -65,7 +66,9 @@ public void addTypedReadersTo(List<HttpMessageReader<?>> result) {
6566
super.addTypedReadersTo(result);
6667
addReaderTo(result, FormHttpMessageReader::new);
6768
if (synchronossMultipartPresent) {
68-
addReaderTo(result, SynchronossMultipartHttpMessageReader::new);
69+
SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader();
70+
addReaderTo(result, () -> partReader);
71+
addReaderTo(result, () -> new MultipartHttpMessageReader(partReader));
6972
}
7073
}
7174

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.multipart;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.stream.Collectors;
25+
26+
import reactor.core.publisher.Flux;
27+
import reactor.core.publisher.Mono;
28+
29+
import org.springframework.core.ResolvableType;
30+
import org.springframework.http.MediaType;
31+
import org.springframework.http.ReactiveHttpInputMessage;
32+
import org.springframework.http.codec.HttpMessageReader;
33+
import org.springframework.util.Assert;
34+
import org.springframework.util.LinkedMultiValueMap;
35+
import org.springframework.util.MultiValueMap;
36+
37+
/**
38+
* {@code HttpMessageReader} for reading {@code "multipart/form-data"} requests
39+
* into a {@code MultiValueMap<String, Part>}.
40+
*
41+
* <p>Note that this reader depends on access to an
42+
* {@code HttpMessageReader<Part>} for the actual parsing of multipart content.
43+
* The purpose of this reader is to collect the parts into a map.
44+
*
45+
* @author Rossen Stoyanchev
46+
* @since 5.0
47+
*/
48+
public class MultipartHttpMessageReader implements HttpMessageReader<MultiValueMap<String, Part>> {
49+
50+
private static final ResolvableType MULTIPART_VALUE_TYPE = ResolvableType.forClassWithGenerics(
51+
MultiValueMap.class, String.class, Part.class);
52+
53+
54+
private final HttpMessageReader<Part> partReader;
55+
56+
57+
public MultipartHttpMessageReader(HttpMessageReader<Part> partReader) {
58+
Assert.notNull(partReader, "'partReader' is required");
59+
this.partReader = partReader;
60+
}
61+
62+
63+
@Override
64+
public List<MediaType> getReadableMediaTypes() {
65+
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);
66+
}
67+
68+
@Override
69+
public boolean canRead(ResolvableType elementType, MediaType mediaType) {
70+
return MULTIPART_VALUE_TYPE.isAssignableFrom(elementType) &&
71+
(mediaType == null || MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType));
72+
}
73+
74+
75+
@Override
76+
public Flux<MultiValueMap<String, Part>> read(ResolvableType elementType,
77+
ReactiveHttpInputMessage message, Map<String, Object> hints) {
78+
79+
return Flux.from(readMono(elementType, message, hints));
80+
}
81+
82+
83+
@Override
84+
public Mono<MultiValueMap<String, Part>> readMono(ResolvableType elementType,
85+
ReactiveHttpInputMessage inputMessage, Map<String, Object> hints) {
86+
87+
return this.partReader.read(elementType, inputMessage, hints)
88+
.collectMultimap(Part::getName).map(this::toMultiValueMap);
89+
}
90+
91+
private LinkedMultiValueMap<String, Part> toMultiValueMap(Map<String, Collection<Part>> map) {
92+
return new LinkedMultiValueMap<>(map.entrySet().stream()
93+
.collect(Collectors.toMap(Map.Entry::getKey, e -> toList(e.getValue()))));
94+
}
95+
96+
private List<Part> toList(Collection<Part> collection) {
97+
return collection instanceof List ? (List<Part>) collection : new ArrayList<>(collection);
98+
}
99+
100+
}
Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,12 @@
2525
import java.nio.channels.ReadableByteChannel;
2626
import java.nio.charset.Charset;
2727
import java.nio.charset.StandardCharsets;
28-
import java.util.ArrayList;
29-
import java.util.Collection;
3028
import java.util.Collections;
3129
import java.util.List;
3230
import java.util.Map;
3331
import java.util.Optional;
3432
import java.util.concurrent.atomic.AtomicInteger;
3533
import java.util.function.Consumer;
36-
import java.util.stream.Collectors;
3734

3835
import org.synchronoss.cloud.nio.multipart.Multipart;
3936
import org.synchronoss.cloud.nio.multipart.MultipartContext;
@@ -55,25 +52,24 @@
5552
import org.springframework.http.ReactiveHttpInputMessage;
5653
import org.springframework.http.codec.HttpMessageReader;
5754
import org.springframework.util.Assert;
58-
import org.springframework.util.LinkedMultiValueMap;
5955
import org.springframework.util.MimeType;
60-
import org.springframework.util.MultiValueMap;
6156
import org.springframework.util.StreamUtils;
6257

6358
/**
64-
* {@code HttpMessageReader} for {@code "multipart/form-data"} requests based
65-
* on the Synchronoss NIO Multipart library.
59+
* {@code HttpMessageReader} for parsing {@code "multipart/form-data"} requests
60+
* to a stream of {@link Part}'s using the Synchronoss NIO Multipart library.
61+
*
62+
* <p>This reader can be provided to {@link MultipartHttpMessageReader} in order
63+
* to aggregate all parts into a Map.
6664
*
6765
* @author Sebastien Deleuze
6866
* @author Rossen Stoyanchev
6967
* @author Arjen Poutsma
7068
* @since 5.0
7169
* @see <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
70+
* @see MultipartHttpMessageReader
7271
*/
73-
public class SynchronossMultipartHttpMessageReader implements HttpMessageReader<MultiValueMap<String, Part>> {
74-
75-
private static final ResolvableType MULTIPART_VALUE_TYPE = ResolvableType.forClassWithGenerics(
76-
MultiValueMap.class, String.class, Part.class);
72+
public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part> {
7773

7874

7975
@Override
@@ -83,34 +79,25 @@ public List<MediaType> getReadableMediaTypes() {
8379

8480
@Override
8581
public boolean canRead(ResolvableType elementType, MediaType mediaType) {
86-
return MULTIPART_VALUE_TYPE.isAssignableFrom(elementType) &&
82+
return Part.class.equals(elementType.resolve(Object.class)) &&
8783
(mediaType == null || MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType));
8884
}
8985

9086

9187
@Override
92-
public Flux<MultiValueMap<String, Part>> read(ResolvableType elementType,
93-
ReactiveHttpInputMessage message, Map<String, Object> hints) {
88+
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message,
89+
Map<String, Object> hints) {
9490

95-
return Flux.from(readMono(elementType, message, hints));
91+
return Flux.create(new SynchronossPartGenerator(message));
9692
}
9793

9894

9995
@Override
100-
public Mono<MultiValueMap<String, Part>> readMono(ResolvableType elementType,
101-
ReactiveHttpInputMessage inputMessage, Map<String, Object> hints) {
102-
103-
return Flux.create(new SynchronossPartGenerator(inputMessage))
104-
.collectMultimap(Part::getName).map(this::toMultiValueMap);
105-
}
106-
107-
private LinkedMultiValueMap<String, Part> toMultiValueMap(Map<String, Collection<Part>> map) {
108-
return new LinkedMultiValueMap<>(map.entrySet().stream()
109-
.collect(Collectors.toMap(Map.Entry::getKey, e -> toList(e.getValue()))));
110-
}
96+
public Mono<Part> readMono(ResolvableType elementType, ReactiveHttpInputMessage message,
97+
Map<String, Object> hints) {
11198

112-
private List<Part> toList(Collection<Part> collection) {
113-
return collection instanceof List ? (List<Part>) collection : new ArrayList<>(collection);
99+
return Mono.error(new UnsupportedOperationException(
100+
"This reader does not support reading a single element."));
114101
}
115102

116103

spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
import org.springframework.http.MediaType;
4242
import org.springframework.http.codec.json.Jackson2JsonDecoder;
4343
import org.springframework.http.codec.json.Jackson2JsonEncoder;
44-
import org.springframework.http.codec.multipart.SynchronossMultipartHttpMessageReader;
44+
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
45+
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
4546
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
4647
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
4748
import org.springframework.util.MimeTypeUtils;
@@ -63,14 +64,15 @@ public class ServerCodecConfigurerTests {
6364
@Test
6465
public void defaultReaders() throws Exception {
6566
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
66-
assertEquals(10, readers.size());
67+
assertEquals(11, readers.size());
6768
assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass());
6869
assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass());
6970
assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass());
7071
assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass());
7172
assertStringDecoder(getNextDecoder(readers), true);
7273
assertEquals(FormHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass());
73-
assertEquals(SynchronossMultipartHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass());
74+
assertEquals(SynchronossPartHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass());
75+
assertEquals(MultipartHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass());
7476
assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass());
7577
assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass());
7678
assertStringDecoder(getNextDecoder(readers), false);

spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ public String getFilename() {
102102
assertNotNull("No boundary found", contentType.getParameter("boundary"));
103103

104104
// see if Synchronoss NIO Multipart can read what we wrote
105-
SynchronossMultipartHttpMessageReader reader = new SynchronossMultipartHttpMessageReader();
105+
SynchronossPartHttpMessageReader synchronossReader = new SynchronossPartHttpMessageReader();
106+
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(synchronossReader);
107+
106108
MockServerHttpRequest request = MockServerHttpRequest.post("/foo")
107109
.header(HttpHeaders.CONTENT_TYPE, contentType.toString())
108110
.body(response.getBody());
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,53 +32,57 @@
3232
import org.springframework.http.HttpHeaders;
3333
import org.springframework.http.MediaType;
3434
import org.springframework.http.MockHttpOutputMessage;
35-
import org.springframework.http.codec.HttpMessageReader;
3635
import org.springframework.http.converter.FormHttpMessageConverter;
3736
import org.springframework.http.server.reactive.ServerHttpRequest;
3837
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
3938
import org.springframework.util.LinkedMultiValueMap;
4039
import org.springframework.util.MultiValueMap;
4140

42-
import static java.util.Collections.*;
43-
import static org.junit.Assert.*;
44-
import static org.springframework.http.HttpHeaders.*;
45-
import static org.springframework.http.MediaType.*;
41+
import static java.util.Collections.emptyMap;
42+
import static org.junit.Assert.assertEquals;
43+
import static org.junit.Assert.assertFalse;
44+
import static org.junit.Assert.assertTrue;
45+
import static org.springframework.core.ResolvableType.forClassWithGenerics;
46+
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
47+
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
48+
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
4649

4750
/**
4851
* @author Sebastien Deleuze
4952
*/
50-
public class SynchronossMultipartHttpMessageReaderTests {
53+
public class SynchronossPartHttpMessageReaderTests {
5154

52-
private final HttpMessageReader<MultiValueMap<String, Part>> reader = new SynchronossMultipartHttpMessageReader();
55+
private final MultipartHttpMessageReader reader =
56+
new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader());
5357

5458

5559
@Test
5660
public void canRead() {
5761
assertTrue(this.reader.canRead(
58-
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class),
62+
forClassWithGenerics(MultiValueMap.class, String.class, Part.class),
5963
MediaType.MULTIPART_FORM_DATA));
6064

6165
assertFalse(this.reader.canRead(
62-
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
66+
forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
6367
MediaType.MULTIPART_FORM_DATA));
6468

6569
assertFalse(this.reader.canRead(
66-
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
70+
forClassWithGenerics(MultiValueMap.class, String.class, String.class),
6771
MediaType.MULTIPART_FORM_DATA));
6872

6973
assertFalse(this.reader.canRead(
70-
ResolvableType.forClassWithGenerics(Map.class, String.class, String.class),
74+
forClassWithGenerics(Map.class, String.class, String.class),
7175
MediaType.MULTIPART_FORM_DATA));
7276

7377
assertFalse(this.reader.canRead(
74-
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class),
78+
forClassWithGenerics(MultiValueMap.class, String.class, Part.class),
7579
MediaType.APPLICATION_FORM_URLENCODED));
7680
}
7781

7882
@Test
7983
public void resolveParts() throws IOException {
8084
ServerHttpRequest request = generateMultipartRequest();
81-
ResolvableType elementType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
85+
ResolvableType elementType = forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
8286
MultiValueMap<String, Part> parts = this.reader.readMono(elementType, request, emptyMap()).block();
8387
assertEquals(2, parts.size());
8488

@@ -105,7 +109,7 @@ public void resolveParts() throws IOException {
105109
@Test
106110
public void bodyError() {
107111
ServerHttpRequest request = generateErrorMultipartRequest();
108-
ResolvableType elementType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
112+
ResolvableType elementType = forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
109113
StepVerifier.create(this.reader.readMono(elementType, request, emptyMap())).verifyError();
110114
}
111115

spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public void requestMappingHandlerAdapter() throws Exception {
103103
verify(webFluxConfigurer).configureArgumentResolvers(any());
104104

105105
assertSame(formatterRegistry.getValue(), initializerConversionService);
106-
assertEquals(10, codecsConfigurer.getValue().getReaders().size());
106+
assertEquals(11, codecsConfigurer.getValue().getReaders().size());
107107
}
108108

109109
@Test

spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void requestMappingHandlerAdapter() throws Exception {
127127
assertNotNull(adapter);
128128

129129
List<HttpMessageReader<?>> readers = adapter.getMessageCodecConfigurer().getReaders();
130-
assertEquals(10, readers.size());
130+
assertEquals(11, readers.size());
131131

132132
assertHasMessageReader(readers, forClass(byte[].class), APPLICATION_OCTET_STREAM);
133133
assertHasMessageReader(readers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM);

0 commit comments

Comments
 (0)