Skip to content

Commit 2e2b49a

Browse files
artembilangaryrussell
authored andcommitted
GH-2872: Parse all the multi-part files (#2878)
* GH-2872: Parse all the multi-part files Fixes #2872 The same HTML form entry may have several files in the multi-part request. Parse all of them in the `MultipartAwareFormHttpMessageConverter.java` and re-map to the result `MultiValueMap` **Cherry-pick to 5.1.x** * * Add test for multi-part files
1 parent cb5d7f6 commit 2e2b49a

File tree

2 files changed

+93
-35
lines changed

2 files changed

+93
-35
lines changed

spring-integration-http/src/main/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverter.java

+18-18
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
import java.nio.charset.Charset;
2121
import java.util.List;
2222
import java.util.Map;
23-
import java.util.Map.Entry;
2423

2524
import org.springframework.http.HttpInputMessage;
2625
import org.springframework.http.HttpOutputMessage;
2726
import org.springframework.http.MediaType;
27+
import org.springframework.http.converter.FormHttpMessageConverter;
2828
import org.springframework.http.converter.HttpMessageConverter;
2929
import org.springframework.http.converter.HttpMessageNotReadableException;
3030
import org.springframework.http.converter.HttpMessageNotWritableException;
@@ -44,18 +44,19 @@
4444
*
4545
* @author Mark Fisher
4646
* @author Gary Russell
47+
* @author Artem Bilan
48+
*
4749
* @since 2.0
4850
*/
4951
public class MultipartAwareFormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
5052

51-
private volatile MultipartFileReader<?> multipartFileReader = new DefaultMultipartFileReader();
53+
private final FormHttpMessageConverter wrappedConverter = new AllEncompassingFormHttpMessageConverter();
5254

53-
private final AllEncompassingFormHttpMessageConverter wrappedConverter = new AllEncompassingFormHttpMessageConverter();
55+
private MultipartFileReader<?> multipartFileReader = new DefaultMultipartFileReader();
5456

5557

5658
/**
5759
* Sets the character set used for writing form data.
58-
*
5960
* @param charset The charset.
6061
*/
6162
public void setCharset(Charset charset) {
@@ -64,11 +65,10 @@ public void setCharset(Charset charset) {
6465

6566
/**
6667
* Specify the {@link MultipartFileReader} to use when reading {@link MultipartFile} content.
67-
*
6868
* @param multipartFileReader The multipart file reader.
6969
*/
7070
public void setMultipartFileReader(MultipartFileReader<?> multipartFileReader) {
71-
Assert.notNull(multipartFileReader, "multipartFileReader must not be null");
71+
Assert.notNull(multipartFileReader, "'multipartFileReader' must not be null");
7272
this.multipartFileReader = multipartFileReader;
7373
}
7474

@@ -106,30 +106,30 @@ public boolean canWrite(Class<?> clazz, MediaType mediaType) {
106106
}
107107
Assert.state(inputMessage instanceof MultipartHttpInputMessage,
108108
"A request with 'multipart/form-data' Content-Type must be a MultipartHttpInputMessage. "
109-
+ "Be sure to provide a 'multipartResolver' bean in the ApplicationContext.");
110-
MultipartHttpInputMessage multipartInputMessage = (MultipartHttpInputMessage) inputMessage;
111-
return this.readMultipart(multipartInputMessage);
109+
+ "Be sure to provide a 'multipartResolver' bean in the ApplicationContext.");
110+
return readMultipart((MultipartHttpInputMessage) inputMessage);
112111
}
113112

114113
private MultiValueMap<String, ?> readMultipart(MultipartHttpInputMessage multipartRequest) throws IOException {
115-
MultiValueMap<String, Object> resultMap = new LinkedMultiValueMap<String, Object>();
114+
MultiValueMap<String, Object> resultMap = new LinkedMultiValueMap<>();
116115
Map<?, ?> parameterMap = multipartRequest.getParameterMap();
117-
for (Entry<?, ?> entry : parameterMap.entrySet()) {
118-
resultMap.add((String) entry.getKey(), entry.getValue());
119-
}
120-
for (Map.Entry<String, MultipartFile> entry : multipartRequest.getFileMap().entrySet()) {
121-
MultipartFile multipartFile = entry.getValue();
122-
if (multipartFile.isEmpty()) {
123-
continue;
116+
parameterMap.forEach((key, value) -> resultMap.add((String) key, value));
117+
118+
for (Map.Entry<String, List<MultipartFile>> entry : multipartRequest.getMultiFileMap().entrySet()) {
119+
List<MultipartFile> multipartFiles = entry.getValue();
120+
for (MultipartFile multipartFile : multipartFiles) {
121+
if (!multipartFile.isEmpty()) {
122+
resultMap.add(entry.getKey(), this.multipartFileReader.readMultipartFile(multipartFile));
123+
}
124124
}
125-
resultMap.add(entry.getKey(), this.multipartFileReader.readMultipartFile(multipartFile));
126125
}
127126
return resultMap;
128127
}
129128

130129
@Override
131130
public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
132131
throws IOException, HttpMessageNotWritableException {
132+
133133
this.wrappedConverter.write(map, contentType, outputMessage);
134134
}
135135

spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java

+75-17
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,29 @@
1616

1717
package org.springframework.integration.http.dsl;
1818

19+
import static org.assertj.core.api.Assertions.assertThat;
1920
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
2021
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
2122
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
23+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
2224
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
2325
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
2426

2527
import java.nio.charset.Charset;
28+
import java.nio.charset.StandardCharsets;
2629
import java.util.Collections;
2730
import java.util.List;
31+
import java.util.Map;
2832

2933
import org.junit.Before;
3034
import org.junit.Test;
3135
import org.junit.runner.RunWith;
3236

3337
import org.springframework.beans.factory.annotation.Autowired;
38+
import org.springframework.beans.factory.annotation.Qualifier;
3439
import org.springframework.context.annotation.Bean;
3540
import org.springframework.context.annotation.Configuration;
41+
import org.springframework.http.MediaType;
3642
import org.springframework.http.ResponseEntity;
3743
import org.springframework.http.client.ClientHttpRequestFactory;
3844
import org.springframework.http.client.ClientHttpResponse;
@@ -41,10 +47,14 @@
4147
import org.springframework.integration.dsl.IntegrationFlow;
4248
import org.springframework.integration.dsl.IntegrationFlows;
4349
import org.springframework.integration.dsl.context.IntegrationFlowContext;
50+
import org.springframework.integration.http.multipart.UploadedMultipartFile;
4451
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
4552
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
4653
import org.springframework.integration.security.channel.SecuredChannel;
54+
import org.springframework.messaging.Message;
4755
import org.springframework.messaging.MessageChannel;
56+
import org.springframework.messaging.PollableChannel;
57+
import org.springframework.mock.web.MockPart;
4858
import org.springframework.security.access.AccessDecisionManager;
4959
import org.springframework.security.access.vote.AffirmativeBased;
5060
import org.springframework.security.access.vote.RoleVoter;
@@ -64,6 +74,9 @@
6474
import org.springframework.web.client.DefaultResponseErrorHandler;
6575
import org.springframework.web.client.HttpClientErrorException;
6676
import org.springframework.web.context.WebApplicationContext;
77+
import org.springframework.web.multipart.MultipartResolver;
78+
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
79+
import org.springframework.web.servlet.DispatcherServlet;
6780

6881
/**
6982
* @author Artem Bilan
@@ -107,20 +120,14 @@ public void testHttpProxyFlow() throws Exception {
107120
get("/service")
108121
.with(httpBasic("admin", "admin"))
109122
.param("name", "foo"))
110-
.andExpect(
111-
content()
112-
.string("FOO"));
123+
.andExpect(content().string("FOO"));
113124

114125
this.mockMvc.perform(
115126
get("/service")
116127
.with(httpBasic("user", "user"))
117128
.param("name", "name"))
118-
.andExpect(
119-
status()
120-
.isForbidden())
121-
.andExpect(
122-
content()
123-
.string("Error"));
129+
.andExpect(status().isForbidden())
130+
.andExpect(content().string("Error"));
124131
}
125132

126133
@Test
@@ -137,21 +144,59 @@ public void testDynamicHttpEndpoint() throws Exception {
137144

138145
this.mockMvc.perform(
139146
get("/dynamic")
140-
.with(httpBasic("admin", "admin"))
147+
.with(httpBasic("user", "user"))
141148
.param("name", "BAR"))
142-
.andExpect(
143-
content()
144-
.string("bar"));
149+
.andExpect(content().string("bar"));
145150

146151
flowRegistration.destroy();
147152

148153
this.mockMvc.perform(
149154
get("/dynamic")
150-
.with(httpBasic("admin", "admin"))
155+
.with(httpBasic("user", "user"))
151156
.param("name", "BAZ"))
152-
.andExpect(
153-
status()
154-
.isNotFound());
157+
.andExpect(status().isNotFound());
158+
}
159+
160+
@Autowired
161+
@Qualifier("multiPartFilesChannel")
162+
private PollableChannel multiPartFilesChannel;
163+
164+
@Test
165+
@SuppressWarnings("unchecked")
166+
public void testMultiPartFiles() throws Exception {
167+
MockPart mockPart1 = new MockPart("a1", "file1", "ABC".getBytes(StandardCharsets.UTF_8));
168+
mockPart1.getHeaders().setContentType(MediaType.TEXT_PLAIN);
169+
MockPart mockPart2 = new MockPart("a1", "file2", "DEF".getBytes(StandardCharsets.UTF_8));
170+
mockPart2.getHeaders().setContentType(MediaType.TEXT_PLAIN);
171+
this.mockMvc.perform(
172+
multipart("/multiPartFiles")
173+
.part(mockPart1, mockPart2)
174+
.with(httpBasic("user", "user")))
175+
.andExpect(status().isOk());
176+
177+
Message<?> result = this.multiPartFilesChannel.receive(10_000);
178+
179+
assertThat(result)
180+
.isNotNull()
181+
.extracting(Message::getPayload)
182+
.satisfies((payload) ->
183+
assertThat((Map<String, ?>) payload)
184+
.hasSize(1)
185+
.extracting((map) -> map.get("a1"))
186+
.asList()
187+
.hasSize(2)
188+
.satisfies((list) -> {
189+
assertThat(list)
190+
.element(0)
191+
.extracting((file) ->
192+
((UploadedMultipartFile) file).getOriginalFilename())
193+
.isEqualTo("file1");
194+
assertThat(list)
195+
.element(1)
196+
.extracting((file) ->
197+
((UploadedMultipartFile) file).getOriginalFilename())
198+
.isEqualTo("file2");
199+
}));
155200
}
156201

157202
@Configuration
@@ -237,6 +282,19 @@ public IntegrationFlow httpProxyErrorFlow() {
237282
new ResponseEntity<>(p.getResponseBodyAsString(), p.getStatusCode()));
238283
}
239284

285+
@Bean
286+
public IntegrationFlow multiPartFilesFlow() {
287+
return IntegrationFlows
288+
.from(Http.inboundChannelAdapter("/multiPartFiles"))
289+
.channel((c) -> c.queue("multiPartFilesChannel"))
290+
.get();
291+
}
292+
293+
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
294+
public MultipartResolver multipartResolver() {
295+
return new StandardServletMultipartResolver();
296+
}
297+
240298
@Bean
241299
public AccessDecisionManager accessDecisionManager() {
242300
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));

0 commit comments

Comments
 (0)