diff --git a/docs/src/reference/docbook/http.xml b/docs/src/reference/docbook/http.xml index 580a2b1e71..7183c54b6c 100644 --- a/docs/src/reference/docbook/http.xml +++ b/docs/src/reference/docbook/http.xml @@ -100,6 +100,20 @@ By default the HTTP request will be generated using an instance of Si In the case of the Outbound Gateway, the reply message produced by the gateway will contain all Message Headers present in the request message. + Cookies + + Basic cookie support is provided by the transfer-cookies attribute on the outbound gateway. When + set to true (default is false), a Set-Cookie header received from the server in a response will be + converted to Cookie in the reply message. This header will then be used + on subsequent sends. This enables simple stateful interactions, such as... + + + ...->logonGateway->...->doWorkGateway->...->logoffGateway->... + + + If transfer-cookies is false, any Set-Cookie header received will + remain as Set-Cookie in the reply message, and will be dropped on subsequent sends. +
diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java index c20643b98f..ca3940f252 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java @@ -80,6 +80,7 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser { IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-timeout", "sendTimeout"); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "outputChannel"); HttpAdapterParsingUtils.configureUriVariableExpressions(builder, element); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "transfer-cookies"); return builder; } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java index b1d6a5fd0b..38aadf6be0 100755 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import javax.xml.transform.Source; @@ -88,6 +89,8 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe private volatile String charset = "UTF-8"; + private volatile boolean transferCookies = false; + private volatile HeaderMapper headerMapper = DefaultHttpHeaderMapper.outboundMapper(); private final Map uriVariableExpressions = new HashMap(); @@ -95,7 +98,6 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe private final RestTemplate restTemplate; private final StandardEvaluationContext evaluationContext; - /** * Create a handler that will send requests to the provided URI. @@ -215,6 +217,16 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe } } + /** + * Set to true if you wish 'Set-Cookie' headers in responses to be + * transferred as 'Cookie' headers in subsequent interactions for + * a message. + * @param transferCookies the transferCookies to set. + */ + public void setTransferCookies(boolean transferCookies) { + this.transferCookies = transferCookies; + } + @Override public void onInit() { super.onInit(); @@ -245,7 +257,11 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe HttpEntity httpRequest = this.generateHttpRequest(requestMessage); ResponseEntity httpResponse = this.restTemplate.exchange(this.uri, this.httpMethod, httpRequest, this.expectedResponseType, uriVariables); if (this.expectReply) { - Map headers = this.headerMapper.toHeaders(httpResponse.getHeaders()); + HttpHeaders httpHeaders = httpResponse.getHeaders(); + Map headers = this.headerMapper.toHeaders(httpHeaders); + if (this.transferCookies) { + this.doConvertSetCookie(headers); + } if (httpResponse.hasBody()) { Object responseBody = httpResponse.getBody(); MessageBuilder replyBuilder = (responseBody instanceof Message) ? @@ -269,6 +285,27 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe } } + /** + * Convert Set-Cookie to Cookie + */ + private void doConvertSetCookie(Map headers) { + String keyName = null; + for (String key : headers.keySet()) { + if (key.equalsIgnoreCase(DefaultHttpHeaderMapper.SET_COOKIE)) { + keyName = key; + break; + } + } + if (keyName != null) { + Object cookies = headers.remove(keyName); + headers.put(DefaultHttpHeaderMapper.COOKIE, cookies); + if (logger.isDebugEnabled()) { + logger.debug("Converted Set-Cookie header to Cookie for: " + + cookies); + } + } + } + private HttpEntity generateHttpRequest(Message message) throws Exception { Assert.notNull(message, "message must not be null"); return (this.extractPayload) ? this.createHttpEntityFromPayload(message) @@ -281,8 +318,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe // payload is already an HttpEntity, just return it as-is return (HttpEntity) payload; } - HttpHeaders httpHeaders = new HttpHeaders(); - this.headerMapper.fromHeaders(message.getHeaders(), httpHeaders); + HttpHeaders httpHeaders = this.mapHeaders(message); if (!shouldIncludeRequestBody()) { return new HttpEntity(httpHeaders); } @@ -302,8 +338,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe } private HttpEntity createHttpEntityFromMessage(Message message) { - HttpHeaders httpHeaders = new HttpHeaders(); - this.headerMapper.fromHeaders(message.getHeaders(), httpHeaders); + HttpHeaders httpHeaders = mapHeaders(message); if (shouldIncludeRequestBody()) { httpHeaders.setContentType(new MediaType("application", "x-java-serialized-object")); return new HttpEntity(message, httpHeaders); @@ -311,6 +346,12 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe return new HttpEntity(httpHeaders); } + protected HttpHeaders mapHeaders(Message message) { + HttpHeaders httpHeaders = new HttpHeaders(); + this.headerMapper.fromHeaders(message.getHeaders(), httpHeaders); + return httpHeaders; + } + @SuppressWarnings("unchecked") private MediaType resolveContentType(Object content) { MediaType contentType = null; diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java b/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java index 38a7fd7aa8..c8131986a4 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java @@ -100,7 +100,7 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF private static final String CONTENT_TYPE = "Content-Type"; - private static final String COOKIE = "Cookie"; + public static final String COOKIE = "Cookie"; private static final String DATE = "Date"; @@ -146,7 +146,7 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF private static final String SERVER = "Server"; - private static final String SET_COOKIE = "Set-Cookie"; + public static final String SET_COOKIE = "Set-Cookie"; private static final String TE = "TE"; diff --git a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.1.xsd b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.1.xsd index a39372fd6d..474bdd80b3 100644 --- a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.1.xsd +++ b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.1.xsd @@ -505,6 +505,15 @@ The String "HTTP_REQUEST_HEADERS" will match against any of the standard HTTP Re + + + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests-context.xml index 5069345cc3..a269148dbc 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests-context.xml +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests-context.xml @@ -35,7 +35,8 @@ reply-channel="replies" charset="UTF-8" order="77" - auto-startup="false"> + auto-startup="false" + transfer-cookies="true"> diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests.java index 22c6f3a648..15eef458bf 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpOutboundGatewayParserTests.java @@ -79,6 +79,7 @@ public class HttpOutboundGatewayParserTests { assertEquals(HttpMethod.POST, handlerAccessor.getPropertyValue("httpMethod")); assertEquals("UTF-8", handlerAccessor.getPropertyValue("charset")); assertEquals(true, handlerAccessor.getPropertyValue("extractPayload")); + assertEquals(false, handlerAccessor.getPropertyValue("transferCookies")); } @Test @@ -125,6 +126,7 @@ public class HttpOutboundGatewayParserTests { assertTrue(ObjectUtils.containsElement(mappedRequestHeaders, "requestHeader1")); assertTrue(ObjectUtils.containsElement(mappedRequestHeaders, "requestHeader2")); assertEquals("responseHeader", mappedResponseHeaders[0]); + assertEquals(true, handlerAccessor.getPropertyValue("transferCookies")); } diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/CookieTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/CookieTests-context.xml new file mode 100644 index 0000000000..cf885271ff --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/CookieTests-context.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/CookieTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/CookieTests.java new file mode 100644 index 0000000000..e79fda3da4 --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/CookieTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2011 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.outbound; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.message.GenericMessage; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Gary Russell + * @since 2.1 + * + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class CookieTests { + + @Autowired + private MessageChannel ch1; + + @Autowired + private QueueChannel ch6; + + private static ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + private static List allHeaders = new ArrayList(); + + @Test + public void testCookie() throws Exception { + ch1.send(new GenericMessage("Hello, world!")); + Assert.assertNotNull(ch6.receive()); + + + bos.close(); + BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bos.toByteArray()))); + String line = br.readLine(); + assertEquals("Hello, world!Hello, again!Hello, once more!", line); + assertNull(br.readLine()); + br.close(); + assertEquals(3, allHeaders.size()); + assertFalse(allHeaders.get(0).containsKey("Cookie")); + assertTrue(allHeaders.get(1).containsKey("Cookie")); + assertEquals("JSESSIONID=X123", allHeaders.get(1).get("Cookie").get(0)); + assertTrue(allHeaders.get(2).containsKey("Cookie")); + assertEquals("JSESSIONID=X124", allHeaders.get(2).get("Cookie").get(0)); + } + + public static class RequestFactory implements ClientHttpRequestFactory { + + private int count = 123; + + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) + throws IOException { + return new ClientHttpRequest() { + + private HttpHeaders headers = new HttpHeaders(); + + public HttpHeaders getHeaders() { + return headers; + } + + public OutputStream getBody() throws IOException { + return bos; + } + + public URI getURI() { + return null; + } + + public HttpMethod getMethod() { + return null; + } + + public ClientHttpResponse execute() throws IOException { + allHeaders.add(headers); + return new ClientHttpResponse() { + + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.set("Set-cookie", "JSESSIONID=X" + count++); // test case insensitivity + headers.set("Content-Length", "2"); + headers.set("Content-Type", "text/plain"); + return headers; + } + + public InputStream getBody() throws IOException { + return new ByteArrayInputStream("OK".getBytes()); + } + + public String getStatusText() throws IOException { + return "OK"; + } + + public HttpStatus getStatusCode() throws IOException { + return HttpStatus.OK; + } + + public void close() { + } + }; + } + }; + } + + } +}