From 4f648a7e4f1589fc8836336c0b94cffb0f5126ac Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 2 Sep 2015 17:30:35 -0400 Subject: [PATCH] INT-3818: Multipart Pass Through Converter JIRA: https://jira.spring.io/browse/INT-3818 INT-3818: Fix Pre JDK8 Syntax Polishing - PR Comments Add author name --- .../http/HttpProxyScenarioTests-context.xml | 32 +++++-- .../http/HttpProxyScenarioTests.java | 67 ++++++++++++- .../inbound/MultipartAsRawByteArrayTests.java | 95 +++++++++++++++++++ src/reference/asciidoc/http.adoc | 26 +++++ src/reference/asciidoc/whats-new.adoc | 5 + 5 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 spring-integration-http/src/test/java/org/springframework/integration/http/inbound/MultipartAsRawByteArrayTests.java diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests-context.xml index dcb766da82..3ace0c6de6 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests-context.xml +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests-context.xml @@ -1,11 +1,14 @@ - + http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> @@ -21,4 +24,21 @@ + + + + + + + + + + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests.java index 41b1959669..86d6333e8d 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package org.springframework.integration.http; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import java.net.URI; import java.text.DateFormat; @@ -41,7 +43,6 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler; import org.springframework.messaging.Message; @@ -49,6 +50,7 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.PollableChannel; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.util.LinkedMultiValueMap; @@ -63,10 +65,12 @@ import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; /** * @author Artem Bilan + * @author Gary Russell * @since 3.0 */ @ContextConfiguration @RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext public class HttpProxyScenarioTests { private final HandlerAdapter handlerAdapter = new HttpRequestHandlerAdapter(); @@ -78,6 +82,10 @@ public class HttpProxyScenarioTests { @Qualifier("proxyGateway.handler") private HttpRequestExecutingMessageHandler handler; + @Autowired + @Qualifier("proxyGatewaymp.handler") + private HttpRequestExecutingMessageHandler handlermp; + @Autowired private PollableChannel checkHeadersChannel; @@ -114,6 +122,7 @@ public class HttpProxyScenarioTests { final String contentDispositionValue = "attachment; filename=\"test.txt\""; Mockito.doAnswer(new Answer>() { + @Override public ResponseEntity answer(InvocationOnMock invocation) throws Throwable { URI uri = (URI) invocation.getArguments()[0]; @@ -129,6 +138,7 @@ public class HttpProxyScenarioTests { responseHeaders.set("Content-Disposition", contentDispositionValue); return new ResponseEntity(responseHeaders, HttpStatus.OK); } + }).when(template).exchange(Mockito.any(URI.class), Mockito.any(HttpMethod.class), Mockito.any(HttpEntity.class), (Class) Mockito.any(Class.class)); @@ -155,4 +165,57 @@ public class HttpProxyScenarioTests { RequestContextHolder.resetRequestAttributes(); } + @Test + public void testHttpMultipartProxyScenario() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/testmp"); + + request.addHeader("Connection", "Keep-Alive"); + request.setContentType("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ"); + request.setContent("foo".getBytes()); + + Object handler = this.handlerMapping.getHandler(request).getHandler(); + assertNotNull(handler); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + RestTemplate template = Mockito.spy(new RestTemplate()); + Mockito.doAnswer(new Answer>() { + + @Override + public ResponseEntity answer(InvocationOnMock invocation) throws Throwable { + URI uri = (URI) invocation.getArguments()[0]; + assertEquals(new URI("http://testServer/testmp"), uri); + HttpEntity httpEntity = (HttpEntity) invocation.getArguments()[2]; + HttpHeaders httpHeaders = httpEntity.getHeaders(); + assertEquals("Keep-Alive", httpHeaders.getFirst("Connection")); + assertEquals("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ", + httpHeaders.getContentType().toString()); + + HttpEntity entity = (HttpEntity) invocation.getArguments()[2]; + assertThat(entity.getBody(), instanceOf(byte[].class)); + assertEquals("foo", new String((byte[])entity.getBody())); + + MultiValueMap responseHeaders = new LinkedMultiValueMap(httpHeaders); + responseHeaders.set("Connection", "close"); + responseHeaders.set("Content-Type", "text/plain"); + return new ResponseEntity(responseHeaders, HttpStatus.OK); + } + + }).when(template).exchange(Mockito.any(URI.class), Mockito.any(HttpMethod.class), + Mockito.any(HttpEntity.class), (Class) Mockito.any(Class.class)); + + PropertyAccessor dfa = new DirectFieldAccessor(this.handlermp); + dfa.setPropertyValue("restTemplate", template); + + RequestAttributes attributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attributes); + + this.handlerAdapter.handle(request, response, handler); + + assertEquals("close", response.getHeaderValue("Connection")); + assertEquals("text/plain", response.getContentType()); + + RequestContextHolder.resetRequestAttributes(); + } + } diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/MultipartAsRawByteArrayTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/MultipartAsRawByteArrayTests.java new file mode 100644 index 0000000000..a0cf07fe69 --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/MultipartAsRawByteArrayTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.http.inbound; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Enumeration; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.messaging.Message; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +/** + * @author Gary Russell + * @since 4.2 + * + */ +public class MultipartAsRawByteArrayTests { + + @SuppressWarnings("unchecked") + @Test + public void testMultiPass() throws Exception { + HttpRequestHandlingMessagingGateway gw = new HttpRequestHandlingMessagingGateway(false); + gw.setMessageConverters( + Collections.> singletonList(new ByteArrayHttpMessageConverter())); + gw.setMergeWithDefaultConverters(false); + QueueChannel requestChannel = new QueueChannel(); + gw.setRequestChannel(requestChannel); + gw.setBeanFactory(mock(BeanFactory.class)); + gw.setRequestPayloadType(byte[].class); + gw.afterPropertiesSet(); + + HttpServletRequest request = mock(HttpServletRequest.class); + ServletInputStream sis = mock(ServletInputStream.class); + doAnswer(new Answer() { + + int done; + + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + byte[] buff = (byte[]) invocation.getArguments()[0]; + buff[0] = 'f'; + buff[1] = 'o'; + buff[2] = 'o'; + return done++ > 0 ? -1 : 3; + } + + }).when(sis).read(Matchers.any(byte[].class)); + when(request.getInputStream()).thenReturn(sis); + when(request.getMethod()).thenReturn("POST"); + when(request.getHeaderNames()).thenReturn(mock(Enumeration.class)); + when(request.getContentType()).thenReturn("multipart/form-data"); + RequestContextHolder.setRequestAttributes(mock(RequestAttributes.class)); + gw.handleRequest(request, mock(HttpServletResponse.class)); + Message received = requestChannel.receive(10000); + assertNotNull(received); + assertThat(received.getPayload(), instanceOf(byte[].class)); + assertEquals("foo", new String((byte[])received.getPayload())); + } + +} diff --git a/src/reference/asciidoc/http.adoc b/src/reference/asciidoc/http.adoc index 8d3033fbe3..30df9bc418 100644 --- a/src/reference/asciidoc/http.adoc +++ b/src/reference/asciidoc/http.adoc @@ -58,6 +58,32 @@ If it does in fact locate that bean, then the support for MultipartFiles will be Otherwise, it will fail when trying to map a multipart-file request to a Spring Integration Message. For more on Spring's support for MultipartResolvers, refer to the http://static.springsource.org/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-multipart[Spring Reference Manual]. +[NOTE] +==== +If you wish to proxy a `multipart/form-data` to another server, it may be better to keep it in raw form. +To handle this situation, do not add the `multipartResolver` bean to the context; configure the endpoint to expect +a `byte[]` request; customize the message converters to include a `ByteArrayHttpMessageConverter`, and +disable the default multipart converter. +You may need some other converter(s) for the replies: + +[source, xml] +---- + + + + + + + +---- +==== + In sending a response to the client there are a number of ways to customize the behavior of the gateway. By default the gateway will simply acknowledge that the request was received by sending a 200 status code back. It is possible to customize this response by providing a 'viewName' to be resolved by the Spring MVC `ViewResolver`. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index f9aa3f2561..3f6590c731 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -292,6 +292,11 @@ The default is now `500 Internal Server Error` instead of `200 OK`. See <> for more information. +===== Form Data + +Documentation is provided for when proxying `multipart/form-data` requests. +See <> for more information. + [[x4.2-gw]] ==== Gateway Changes