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
This commit is contained in:
Artem Bilan
2013-05-27 18:36:56 +03:00
committed by Gary Russell
parent 9285867465
commit 4609b861d1
6 changed files with 305 additions and 7 deletions

View File

@@ -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<HttpHeaders>, 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<HttpHeaders>, 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<HttpHeaders>, 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<HttpHeaders>, 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<HttpHeaders>, 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<HttpHeaders>, 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<HttpHeaders>, 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<HttpHeaders>, 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<HttpHeaders>, BeanF
mapper.setExcludedInboundStandardResponseHeaderNames(HTTP_RESPONSE_HEADER_NAMES_INBOUND_EXCLUSIONS);
return mapper;
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration/http"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<beans:bean class="org.springframework.integration.http.inbound.UriPathHandlerMapping"/>
<inbound-gateway path="/test" request-channel="testChannel"
payload-expression="T(org.springframework.web.context.request.RequestContextHolder).requestAttributes.request.queryString"/>
<int:publish-subscribe-channel id="testChannel"/>
<outbound-gateway id="proxyGateway" request-channel="testChannel"
url-expression="'http://testServer/test?' + payload"/>
<int:bridge input-channel="testChannel" output-channel="checkHeadersChannel"/>
<int:channel id="checkHeadersChannel">
<int:queue/>
</int:channel>
</beans:beans>

View File

@@ -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<ResponseEntity<?>>() {
@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<Object>(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"));
}
}

View File

@@ -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<HttpHeaders> 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<String, Object> 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<HttpHeaders> mapper = DefaultHttpHeaderMapper.inboundMapper();
Date ifModifiedSince = new Date();
long ifModifiedSinceTime = ifModifiedSince.getTime();
HttpHeaders headers = new HttpHeaders();
headers.setIfModifiedSince(ifModifiedSinceTime);
Map<String, ?> 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 {
}

View File

@@ -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<HttpHeaders> 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());
}
}

View File

@@ -213,6 +213,7 @@
For more information see <xref linkend="chain"/>.
</para>
</section>
<<<<<<< HEAD
<section id="3.0-jms-mdca-te">
<title>JMS Message Driven Channel Adapter</title>
<para>
@@ -326,5 +327,15 @@
For more information see <xref linkend="delayer"/>.
</para>
</section>
<section id="3.0-http-header-mapper">
<title>DefaultHttpHeaderMapper and 'If-(Un)Modified-Since' HTTP headers</title>
<para>
Previously, 'If-Modified-Since' and 'If-Unmodified-Since' HTTP headers were incorrectly processed
within from/to HTTP headers mapping in the <classname>DefaultHttpHeaderMapper</classname>.
Now, in addition to the fix of that issue, <classname>DefaultHttpHeaderMapper</classname> provides date parsing
from formatted strings for HTTP headers, which accept date-time values.
For more information see <xref linkend="http"/>.
</para>
</section>
</section>
</chapter>