From 4609b861d10f71e12bc6806afc2d57302b676f6d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 27 May 2013 18:36:56 +0300 Subject: [PATCH] INT-2995: Fix HttpHeaders inconsistency Previously, 'If-Modified-Since' and 'If-Unmodified-Since' HTTP headers were incorrectly processed within from/to HTTP headers mapping, because Spring-Web `HttpHeaders` has a confusing method name, see: https://jira.springsource.org/browse/SPR-10600 * fix 'If-Modified-Since' and 'If-Unmodified-Since' processing independently from `HttpHeaders` * add fallback to formatted string for date ware HTTP headers * add Spring Integration HTTP proxy scenario test JIRA: https://jira.springsource.org/browse/INT-2995 --- .../http/support/DefaultHttpHeaderMapper.java | 96 +++++++++++- .../http/HttpProxyScenarioTests-context.xml | 26 ++++ .../http/HttpProxyScenarioTests.java | 141 ++++++++++++++++++ ...tpHeaderMapperFromMessageInboundTests.java | 18 ++- ...pHeaderMapperFromMessageOutboundTests.java | 20 +++ src/reference/docbook/whats-new.xml | 11 ++ 6 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests-context.xml create mode 100644 spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests.java 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 12c5b87dd0..57b9f926bf 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 @@ -19,7 +19,10 @@ package org.springframework.integration.http.support; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.text.DateFormat; import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -29,9 +32,12 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Locale; +import java.util.TimeZone; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -240,6 +246,14 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF public static final String HTTP_RESPONSE_HEADER_NAME_PATTERN = "HTTP_RESPONSE_HEADERS"; + // Copy of 'org.springframework.http.HttpHeaders#DATE_FORMATS' + private static final String[] DATE_FORMATS = new String[] { + "EEE, dd MMM yyyy HH:mm:ss zzz", + "EEE, dd-MMM-yy HH:mm:ss zzz", + "EEE MMM dd HH:mm:ss yyyy" + }; + + private static TimeZone GMT = TimeZone.getTimeZone("GMT"); private volatile String[] outboundHeaderNames = new String[0]; @@ -637,7 +651,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF target.setDate(((Number) value).longValue()); } else if (value instanceof String) { - target.setDate(Long.parseLong((String) value)); + try { + target.setDate(Long.parseLong((String) value)); + } + catch (NumberFormatException e) { + target.setDate(this.getFirstDate((String) value, DATE)); + } } else { Class clazz = (value != null) ? value.getClass() : null; @@ -663,7 +682,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF target.setExpires(((Number) value).longValue()); } else if (value instanceof String) { - target.setExpires(Long.parseLong((String) value)); + try { + target.setExpires(Long.parseLong((String) value)); + } + catch (NumberFormatException e) { + target.setExpires(this.getFirstDate((String) value, EXPIRES)); + } } else { Class clazz = (value != null) ? value.getClass() : null; @@ -679,7 +703,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF target.setIfModifiedSince(((Number) value).longValue()); } else if (value instanceof String) { - target.setIfModifiedSince(Long.parseLong((String) value)); + try { + target.setIfModifiedSince(Long.parseLong((String) value)); + } + catch (NumberFormatException e) { + target.setIfModifiedSince(this.getFirstDate((String) value, IF_MODIFIED_SINCE)); + } } else { Class clazz = (value != null) ? value.getClass() : null; @@ -687,6 +716,30 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF "Expected Date, Number, or String value for 'If-Modified-Since' header value, but received: " + clazz); } } + else if (IF_UNMODIFIED_SINCE.equalsIgnoreCase(name)) { + String ifUnmodifiedSinceValue = null; + if (value instanceof Date) { + ifUnmodifiedSinceValue = this.formatDate(((Date) value).getTime()); + } + else if (value instanceof Number) { + ifUnmodifiedSinceValue = this.formatDate(((Number) value).longValue()); + } + else if (value instanceof String) { + try { + ifUnmodifiedSinceValue = this.formatDate(Long.parseLong((String) value)); + } + catch (NumberFormatException e) { + long longValue = this.getFirstDate((String) value, IF_UNMODIFIED_SINCE); + ifUnmodifiedSinceValue = this.formatDate(longValue); + } + } + else { + Class clazz = (value != null) ? value.getClass() : null; + throw new IllegalArgumentException( + "Expected Date, Number, or String value for 'If-Unmodified-Since' header value, but received: " + clazz); + } + target.set(IF_UNMODIFIED_SINCE, ifUnmodifiedSinceValue); + } else if (IF_NONE_MATCH.equalsIgnoreCase(name)) { if (value instanceof String) { target.setIfNoneMatch((String) value); @@ -721,7 +774,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF target.setLastModified(((Number) value).longValue()); } else if (value instanceof String) { - target.setLastModified(Long.parseLong((String) value)); + try { + target.setLastModified(Long.parseLong((String) value)); + } + catch (NumberFormatException e) { + target.setLastModified(this.getFirstDate((String) value, LAST_MODIFIED)); + } } else { Class clazz = (value != null) ? value.getClass() : null; @@ -842,9 +900,13 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF else if (IF_NONE_MATCH.equalsIgnoreCase(name)) { return source.getIfNoneMatch(); } + else if (IF_MODIFIED_SINCE.equalsIgnoreCase(name)) { + long modifiedSince = source.getIfNotModifiedSince(); + return (modifiedSince > -1) ? modifiedSince : null; + } else if (IF_UNMODIFIED_SINCE.equalsIgnoreCase(name)) { - long unmodifiedSince = source.getIfNotModifiedSince(); - return (unmodifiedSince > -1) ? unmodifiedSince : null; + String unmodifiedSince = source.getFirst(IF_UNMODIFIED_SINCE); + return unmodifiedSince != null ? this.getFirstDate(unmodifiedSince, IF_UNMODIFIED_SINCE) : null; } else if (LAST_MODIFIED.equalsIgnoreCase(name)) { long lastModified = source.getLastModified(); @@ -896,6 +958,27 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF return null; } + // Utility methods + + private long getFirstDate(String headerValue, String headerName) { + for (String dateFormat : DATE_FORMATS) { + DateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); + simpleDateFormat.setTimeZone(GMT); + try { + return simpleDateFormat.parse(headerValue).getTime(); + } + catch (ParseException e) { + // ignore + } + } + throw new IllegalArgumentException("Cannot parse date value '" + headerValue +"' for '" + headerName + "' header"); + } + + private String formatDate(long date) { + DateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); + dateFormat.setTimeZone(GMT); + return dateFormat.format(new Date(date)); + } /** * Factory method for creating a basic outbound mapper instance. @@ -922,4 +1005,5 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF mapper.setExcludedInboundStandardResponseHeaderNames(HTTP_RESPONSE_HEADER_NAMES_INBOUND_EXCLUSIONS); return mapper; } + } 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 new file mode 100644 index 0000000000..a8e0bdc9a7 --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..6ad55a8638 --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2013 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.net.URI; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.PropertyAccessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.integration.Message; +import org.springframework.integration.MessageHeaders; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; + +/** + * @author Artem Bilan + * @since 3.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class HttpProxyScenarioTests { + + private final HandlerAdapter handlerAdapter = new HttpRequestHandlerAdapter(); + + @Autowired + private HandlerMapping handlerMapping; + + @Autowired + @Qualifier("proxyGateway.handler") + private HttpRequestExecutingMessageHandler handler; + + @Autowired + private PollableChannel checkHeadersChannel; + + @Test + public void testHttpProxyScenario() throws Exception { + DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + + Calendar c = Calendar.getInstance(); + c.set(Calendar.MILLISECOND, 0); + + final long ifModifiedSince = c.getTimeInMillis(); + String ifModifiedSinceValue = dateFormat.format(ifModifiedSince); + + c.add(Calendar.DATE, -1); + long ifUnmodifiedSince = c.getTimeInMillis(); + final String ifUnmodifiedSinceValue = dateFormat.format(ifUnmodifiedSince); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); + request.setQueryString("foo=bar&FOO=BAR"); + + request.addHeader("If-Modified-Since", ifModifiedSinceValue); + request.addHeader("If-Unmodified-Since", ifUnmodifiedSinceValue); + + 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/test?foo=bar&FOO=BAR"), uri); + HttpEntity httpEntity = (HttpEntity) invocation.getArguments()[2]; + HttpHeaders httpHeaders = httpEntity.getHeaders(); + assertEquals(ifModifiedSince, httpHeaders.getIfNotModifiedSince()); + assertEquals(ifUnmodifiedSinceValue, httpHeaders.getFirst("If-Unmodified-Since")); + return new ResponseEntity(httpEntity.getHeaders(), 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.handler); + dfa.setPropertyValue("restTemplate", template); + + RequestAttributes attributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attributes); + + this.handlerAdapter.handle(request, response, handler); + + assertNull(response.getHeaderValue("If-Modified-Since")); + assertNull(response.getHeaderValue("If-Unmodified-Since")); + + Message message = this.checkHeadersChannel.receive(2000); + MessageHeaders headers = message.getHeaders(); + + assertEquals(ifModifiedSince, headers.get("If-Modified-Since")); + assertEquals(ifUnmodifiedSince, headers.get("If-Unmodified-Since")); + + } + +} diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageInboundTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageInboundTests.java index 5ba79f58d8..bc86e05c82 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageInboundTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageInboundTests.java @@ -25,6 +25,7 @@ import java.net.URI; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -48,6 +49,7 @@ import org.springframework.util.CollectionUtils; * @author Oleg Zhurakousky * @author Mark Fisher * @author Gunnar Hillert + * @author Artem Bilan * @since 2.0.1 */ public class DefaultHttpHeaderMapperFromMessageInboundTests { @@ -549,13 +551,27 @@ public class DefaultHttpHeaderMapperFromMessageInboundTests { HeaderMapper mapper = DefaultHttpHeaderMapper.inboundMapper(); HttpHeaders headers = new HttpHeaders(); // suppressed in response on inbound, by default - headers.put("Content-Length", Arrays.asList(new String[] {"3"})); + headers.put("Content-Length", Arrays.asList("3")); Map messageHeaders = mapper.toHeaders(headers); headers = new HttpHeaders(); mapper.fromHeaders(new MessageHeaders(messageHeaders), headers); assertNull(headers.get("Content-Length")); } + @Test + public void testInt2995IfModifiedSince() throws Exception{ + HeaderMapper mapper = DefaultHttpHeaderMapper.inboundMapper(); + Date ifModifiedSince = new Date(); + long ifModifiedSinceTime = ifModifiedSince.getTime(); + HttpHeaders headers = new HttpHeaders(); + headers.setIfModifiedSince(ifModifiedSinceTime); + Map result = mapper.toHeaders(headers); + Calendar c = Calendar.getInstance(); + c.setTime(ifModifiedSince); + c.set(Calendar.MILLISECOND, 0); + assertEquals(c.getTimeInMillis(), result.get("If-Modified-Since")); + } + public static class TestClass { } diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageOutboundTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageOutboundTests.java index c75f16acb9..82911df7f6 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageOutboundTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/support/DefaultHttpHeaderMapperFromMessageOutboundTests.java @@ -25,17 +25,22 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; import org.junit.Test; + import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.integration.Message; import org.springframework.integration.MessageHeaders; import org.springframework.integration.mapping.HeaderMapper; +import org.springframework.integration.support.MessageBuilder; import org.springframework.util.CollectionUtils; /** @@ -668,4 +673,19 @@ public class DefaultHttpHeaderMapperFromMessageOutboundTests { assertEquals(0, messageHeaders.size()); } + public void testInt2995IfModifiedSince() throws Exception{ + Date ifModifiedSince = new Date(); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + String value = dateFormat.format(ifModifiedSince); + Message testMessage = MessageBuilder.withPayload("foo").setHeader("If-Modified-Since", value).build(); + HeaderMapper mapper = DefaultHttpHeaderMapper.outboundMapper(); + HttpHeaders headers = new HttpHeaders(); + mapper.fromHeaders(testMessage.getHeaders(), headers); + Calendar c = Calendar.getInstance(); + c.setTime(ifModifiedSince); + c.set(Calendar.MILLISECOND, 0); + assertEquals(c.getTimeInMillis(), headers.getIfNotModifiedSince()); + } + } diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 8013df27cd..ab047bd926 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -213,6 +213,7 @@ For more information see . +<<<<<<< HEAD
JMS Message Driven Channel Adapter @@ -326,5 +327,15 @@ For more information see .
+
+ DefaultHttpHeaderMapper and 'If-(Un)Modified-Since' HTTP headers + + Previously, 'If-Modified-Since' and 'If-Unmodified-Since' HTTP headers were incorrectly processed + within from/to HTTP headers mapping in the DefaultHttpHeaderMapper. + Now, in addition to the fix of that issue, DefaultHttpHeaderMapper provides date parsing + from formatted strings for HTTP headers, which accept date-time values. + For more information see . + +