GH-2872: Parse all the multi-part files (#2878)

* GH-2872: Parse all the multi-part files

Fixes https://github.com/spring-projects/spring-integration/issues/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
This commit is contained in:
Artem Bilan
2019-04-02 13:08:57 -04:00
committed by Gary Russell
parent cb5d7f626a
commit 2e2b49a22f
2 changed files with 93 additions and 35 deletions

View File

@@ -20,11 +20,11 @@ import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.HttpMessageNotWritableException;
@@ -44,18 +44,19 @@ import org.springframework.web.multipart.MultipartFile;
* *
* @author Mark Fisher * @author Mark Fisher
* @author Gary Russell * @author Gary Russell
* @author Artem Bilan
*
* @since 2.0 * @since 2.0
*/ */
public class MultipartAwareFormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> { public class MultipartAwareFormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
private volatile MultipartFileReader<?> multipartFileReader = new DefaultMultipartFileReader(); private final FormHttpMessageConverter wrappedConverter = new AllEncompassingFormHttpMessageConverter();
private final AllEncompassingFormHttpMessageConverter wrappedConverter = new AllEncompassingFormHttpMessageConverter(); private MultipartFileReader<?> multipartFileReader = new DefaultMultipartFileReader();
/** /**
* Sets the character set used for writing form data. * Sets the character set used for writing form data.
*
* @param charset The charset. * @param charset The charset.
*/ */
public void setCharset(Charset charset) { public void setCharset(Charset charset) {
@@ -64,11 +65,10 @@ public class MultipartAwareFormHttpMessageConverter implements HttpMessageConver
/** /**
* Specify the {@link MultipartFileReader} to use when reading {@link MultipartFile} content. * Specify the {@link MultipartFileReader} to use when reading {@link MultipartFile} content.
*
* @param multipartFileReader The multipart file reader. * @param multipartFileReader The multipart file reader.
*/ */
public void setMultipartFileReader(MultipartFileReader<?> multipartFileReader) { public void setMultipartFileReader(MultipartFileReader<?> multipartFileReader) {
Assert.notNull(multipartFileReader, "multipartFileReader must not be null"); Assert.notNull(multipartFileReader, "'multipartFileReader' must not be null");
this.multipartFileReader = multipartFileReader; this.multipartFileReader = multipartFileReader;
} }
@@ -106,23 +106,22 @@ public class MultipartAwareFormHttpMessageConverter implements HttpMessageConver
} }
Assert.state(inputMessage instanceof MultipartHttpInputMessage, Assert.state(inputMessage instanceof MultipartHttpInputMessage,
"A request with 'multipart/form-data' Content-Type must be a MultipartHttpInputMessage. " "A request with 'multipart/form-data' Content-Type must be a MultipartHttpInputMessage. "
+ "Be sure to provide a 'multipartResolver' bean in the ApplicationContext."); + "Be sure to provide a 'multipartResolver' bean in the ApplicationContext.");
MultipartHttpInputMessage multipartInputMessage = (MultipartHttpInputMessage) inputMessage; return readMultipart((MultipartHttpInputMessage) inputMessage);
return this.readMultipart(multipartInputMessage);
} }
private MultiValueMap<String, ?> readMultipart(MultipartHttpInputMessage multipartRequest) throws IOException { private MultiValueMap<String, ?> readMultipart(MultipartHttpInputMessage multipartRequest) throws IOException {
MultiValueMap<String, Object> resultMap = new LinkedMultiValueMap<String, Object>(); MultiValueMap<String, Object> resultMap = new LinkedMultiValueMap<>();
Map<?, ?> parameterMap = multipartRequest.getParameterMap(); Map<?, ?> parameterMap = multipartRequest.getParameterMap();
for (Entry<?, ?> entry : parameterMap.entrySet()) { parameterMap.forEach((key, value) -> resultMap.add((String) key, value));
resultMap.add((String) entry.getKey(), entry.getValue());
} for (Map.Entry<String, List<MultipartFile>> entry : multipartRequest.getMultiFileMap().entrySet()) {
for (Map.Entry<String, MultipartFile> entry : multipartRequest.getFileMap().entrySet()) { List<MultipartFile> multipartFiles = entry.getValue();
MultipartFile multipartFile = entry.getValue(); for (MultipartFile multipartFile : multipartFiles) {
if (multipartFile.isEmpty()) { if (!multipartFile.isEmpty()) {
continue; resultMap.add(entry.getKey(), this.multipartFileReader.readMultipartFile(multipartFile));
}
} }
resultMap.add(entry.getKey(), this.multipartFileReader.readMultipartFile(multipartFile));
} }
return resultMap; return resultMap;
} }
@@ -130,6 +129,7 @@ public class MultipartAwareFormHttpMessageConverter implements HttpMessageConver
@Override @Override
public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage) public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException { throws IOException, HttpMessageNotWritableException {
this.wrappedConverter.write(map, contentType, outputMessage); this.wrappedConverter.write(map, contentType, outputMessage);
} }

View File

@@ -16,23 +16,29 @@
package org.springframework.integration.http.dsl; package org.springframework.integration.http.dsl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.ClientHttpResponse;
@@ -41,10 +47,14 @@ import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.context.IntegrationFlowContext; import org.springframework.integration.dsl.context.IntegrationFlowContext;
import org.springframework.integration.http.multipart.UploadedMultipartFile;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler; import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.security.channel.ChannelSecurityInterceptor; import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
import org.springframework.integration.security.channel.SecuredChannel; import org.springframework.integration.security.channel.SecuredChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.mock.web.MockPart;
import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased; import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter; import org.springframework.security.access.vote.RoleVoter;
@@ -64,6 +74,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
/** /**
* @author Artem Bilan * @author Artem Bilan
@@ -107,20 +120,14 @@ public class HttpDslTests {
get("/service") get("/service")
.with(httpBasic("admin", "admin")) .with(httpBasic("admin", "admin"))
.param("name", "foo")) .param("name", "foo"))
.andExpect( .andExpect(content().string("FOO"));
content()
.string("FOO"));
this.mockMvc.perform( this.mockMvc.perform(
get("/service") get("/service")
.with(httpBasic("user", "user")) .with(httpBasic("user", "user"))
.param("name", "name")) .param("name", "name"))
.andExpect( .andExpect(status().isForbidden())
status() .andExpect(content().string("Error"));
.isForbidden())
.andExpect(
content()
.string("Error"));
} }
@Test @Test
@@ -137,21 +144,59 @@ public class HttpDslTests {
this.mockMvc.perform( this.mockMvc.perform(
get("/dynamic") get("/dynamic")
.with(httpBasic("admin", "admin")) .with(httpBasic("user", "user"))
.param("name", "BAR")) .param("name", "BAR"))
.andExpect( .andExpect(content().string("bar"));
content()
.string("bar"));
flowRegistration.destroy(); flowRegistration.destroy();
this.mockMvc.perform( this.mockMvc.perform(
get("/dynamic") get("/dynamic")
.with(httpBasic("admin", "admin")) .with(httpBasic("user", "user"))
.param("name", "BAZ")) .param("name", "BAZ"))
.andExpect( .andExpect(status().isNotFound());
status() }
.isNotFound());
@Autowired
@Qualifier("multiPartFilesChannel")
private PollableChannel multiPartFilesChannel;
@Test
@SuppressWarnings("unchecked")
public void testMultiPartFiles() throws Exception {
MockPart mockPart1 = new MockPart("a1", "file1", "ABC".getBytes(StandardCharsets.UTF_8));
mockPart1.getHeaders().setContentType(MediaType.TEXT_PLAIN);
MockPart mockPart2 = new MockPart("a1", "file2", "DEF".getBytes(StandardCharsets.UTF_8));
mockPart2.getHeaders().setContentType(MediaType.TEXT_PLAIN);
this.mockMvc.perform(
multipart("/multiPartFiles")
.part(mockPart1, mockPart2)
.with(httpBasic("user", "user")))
.andExpect(status().isOk());
Message<?> result = this.multiPartFilesChannel.receive(10_000);
assertThat(result)
.isNotNull()
.extracting(Message::getPayload)
.satisfies((payload) ->
assertThat((Map<String, ?>) payload)
.hasSize(1)
.extracting((map) -> map.get("a1"))
.asList()
.hasSize(2)
.satisfies((list) -> {
assertThat(list)
.element(0)
.extracting((file) ->
((UploadedMultipartFile) file).getOriginalFilename())
.isEqualTo("file1");
assertThat(list)
.element(1)
.extracting((file) ->
((UploadedMultipartFile) file).getOriginalFilename())
.isEqualTo("file2");
}));
} }
@Configuration @Configuration
@@ -237,6 +282,19 @@ public class HttpDslTests {
new ResponseEntity<>(p.getResponseBodyAsString(), p.getStatusCode())); new ResponseEntity<>(p.getResponseBodyAsString(), p.getStatusCode()));
} }
@Bean
public IntegrationFlow multiPartFilesFlow() {
return IntegrationFlows
.from(Http.inboundChannelAdapter("/multiPartFiles"))
.channel((c) -> c.queue("multiPartFilesChannel"))
.get();
}
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@Bean @Bean
public AccessDecisionManager accessDecisionManager() { public AccessDecisionManager accessDecisionManager() {
return new AffirmativeBased(Collections.singletonList(new RoleVoter())); return new AffirmativeBased(Collections.singletonList(new RoleVoter()));