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:
committed by
Gary Russell
parent
cb5d7f626a
commit
2e2b49a22f
@@ -20,11 +20,11 @@ import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
@@ -44,18 +44,19 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param charset The 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.
|
||||
*
|
||||
* @param multipartFileReader The multipart file reader.
|
||||
*/
|
||||
public void setMultipartFileReader(MultipartFileReader<?> multipartFileReader) {
|
||||
Assert.notNull(multipartFileReader, "multipartFileReader must not be null");
|
||||
Assert.notNull(multipartFileReader, "'multipartFileReader' must not be null");
|
||||
this.multipartFileReader = multipartFileReader;
|
||||
}
|
||||
|
||||
@@ -106,23 +106,22 @@ public class MultipartAwareFormHttpMessageConverter implements HttpMessageConver
|
||||
}
|
||||
Assert.state(inputMessage instanceof MultipartHttpInputMessage,
|
||||
"A request with 'multipart/form-data' Content-Type must be a MultipartHttpInputMessage. "
|
||||
+ "Be sure to provide a 'multipartResolver' bean in the ApplicationContext.");
|
||||
MultipartHttpInputMessage multipartInputMessage = (MultipartHttpInputMessage) inputMessage;
|
||||
return this.readMultipart(multipartInputMessage);
|
||||
+ "Be sure to provide a 'multipartResolver' bean in the ApplicationContext.");
|
||||
return readMultipart((MultipartHttpInputMessage) inputMessage);
|
||||
}
|
||||
|
||||
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();
|
||||
for (Entry<?, ?> entry : parameterMap.entrySet()) {
|
||||
resultMap.add((String) entry.getKey(), entry.getValue());
|
||||
}
|
||||
for (Map.Entry<String, MultipartFile> entry : multipartRequest.getFileMap().entrySet()) {
|
||||
MultipartFile multipartFile = entry.getValue();
|
||||
if (multipartFile.isEmpty()) {
|
||||
continue;
|
||||
parameterMap.forEach((key, value) -> resultMap.add((String) key, value));
|
||||
|
||||
for (Map.Entry<String, List<MultipartFile>> entry : multipartRequest.getMultiFileMap().entrySet()) {
|
||||
List<MultipartFile> multipartFiles = entry.getValue();
|
||||
for (MultipartFile multipartFile : multipartFiles) {
|
||||
if (!multipartFile.isEmpty()) {
|
||||
resultMap.add(entry.getKey(), this.multipartFileReader.readMultipartFile(multipartFile));
|
||||
}
|
||||
}
|
||||
resultMap.add(entry.getKey(), this.multipartFileReader.readMultipartFile(multipartFile));
|
||||
}
|
||||
return resultMap;
|
||||
}
|
||||
@@ -130,6 +129,7 @@ public class MultipartAwareFormHttpMessageConverter implements HttpMessageConver
|
||||
@Override
|
||||
public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
|
||||
this.wrappedConverter.write(map, contentType, outputMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,23 +16,29 @@
|
||||
|
||||
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.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||
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.status;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
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.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
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.IntegrationFlows;
|
||||
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.security.channel.ChannelSecurityInterceptor;
|
||||
import org.springframework.integration.security.channel.SecuredChannel;
|
||||
import org.springframework.messaging.Message;
|
||||
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.vote.AffirmativeBased;
|
||||
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.HttpClientErrorException;
|
||||
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
|
||||
@@ -107,20 +120,14 @@ public class HttpDslTests {
|
||||
get("/service")
|
||||
.with(httpBasic("admin", "admin"))
|
||||
.param("name", "foo"))
|
||||
.andExpect(
|
||||
content()
|
||||
.string("FOO"));
|
||||
.andExpect(content().string("FOO"));
|
||||
|
||||
this.mockMvc.perform(
|
||||
get("/service")
|
||||
.with(httpBasic("user", "user"))
|
||||
.param("name", "name"))
|
||||
.andExpect(
|
||||
status()
|
||||
.isForbidden())
|
||||
.andExpect(
|
||||
content()
|
||||
.string("Error"));
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(content().string("Error"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -137,21 +144,59 @@ public class HttpDslTests {
|
||||
|
||||
this.mockMvc.perform(
|
||||
get("/dynamic")
|
||||
.with(httpBasic("admin", "admin"))
|
||||
.with(httpBasic("user", "user"))
|
||||
.param("name", "BAR"))
|
||||
.andExpect(
|
||||
content()
|
||||
.string("bar"));
|
||||
.andExpect(content().string("bar"));
|
||||
|
||||
flowRegistration.destroy();
|
||||
|
||||
this.mockMvc.perform(
|
||||
get("/dynamic")
|
||||
.with(httpBasic("admin", "admin"))
|
||||
.with(httpBasic("user", "user"))
|
||||
.param("name", "BAZ"))
|
||||
.andExpect(
|
||||
status()
|
||||
.isNotFound());
|
||||
.andExpect(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
|
||||
@@ -237,6 +282,19 @@ public class HttpDslTests {
|
||||
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
|
||||
public AccessDecisionManager accessDecisionManager() {
|
||||
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));
|
||||
|
||||
Reference in New Issue
Block a user