diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java index 4211452929b..ad0fdde117b 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -24,6 +24,8 @@ import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.security.Principal; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -36,6 +38,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TimeZone; + import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; @@ -120,6 +124,18 @@ public class MockHttpServletRequest implements HttpServletRequest { private static final ServletInputStream EMPTY_SERVLET_INPUT_STREAM = new DelegatingServletInputStream(new ByteArrayInputStream(new byte[0])); + /** + * Date formats as specified in the HTTP RFC + * @see Section 7.1.1.1 of RFC 7231 + */ + 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 final TimeZone GMT = TimeZone.getTimeZone("GMT"); + private boolean active = true; @@ -681,7 +697,7 @@ public class MockHttpServletRequest implements HttpServletRequest { } /** - * Returns the first preferred {@linkplain Locale locale} configured + * Return the first preferred {@linkplain Locale locale} configured * in this mock request. *

If no locales have been explicitly configured, the default, * preferred {@link Locale} for the server mocked by this @@ -699,7 +715,7 @@ public class MockHttpServletRequest implements HttpServletRequest { } /** - * Returns an {@linkplain Enumeration enumeration} of the preferred + * Return an {@linkplain Enumeration enumeration} of the preferred * {@linkplain Locale locales} configured in this mock request. *

If no locales have been explicitly configured, the default, * preferred {@link Locale} for the server mocked by this @@ -728,7 +744,7 @@ public class MockHttpServletRequest implements HttpServletRequest { } /** - * Returns {@code true} if the {@link #setSecure secure} flag has been set + * Return {@code true} if the {@link #setSecure secure} flag has been set * to {@code true} or if the {@link #getScheme scheme} is {@code https}. * @see javax.servlet.ServletRequest#isSecure() */ @@ -860,20 +876,16 @@ public class MockHttpServletRequest implements HttpServletRequest { /** * Add a header entry for the given name. - *

If there was no entry for that header name before, the value will be used - * as-is. In case of an existing entry, a String array will be created, - * adding the given value (more specifically, its toString representation) - * as further element. - *

Multiple values can only be stored as list of Strings, following the - * Servlet spec (see {@code getHeaders} accessor). As alternative to - * repeated {@code addHeader} calls for individual elements, you can - * use a single call with an entire array or Collection of values as - * parameter. + *

While this method can take any {@code Object} as a parameter, + * it is recommended to use the following types: + *

* @see #getHeaderNames - * @see #getHeader * @see #getHeaders - * @see #getDateHeader - * @see #getIntHeader */ public void addHeader(String name, Object value) { if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name)) { @@ -902,6 +914,18 @@ public class MockHttpServletRequest implements HttpServletRequest { } } + /** + * Return the long timestamp for the date header with the given {@code name}. + *

If the internal value representation is a String, this method will try + * to parse it as a date using the supported date formats: + *

+ * @param name the header name + * @see Section 7.1.1.1 of RFC 7231 + */ @Override public long getDateHeader(String name) { HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); @@ -912,15 +936,32 @@ public class MockHttpServletRequest implements HttpServletRequest { else if (value instanceof Number) { return ((Number) value).longValue(); } + else if (value instanceof String) { + return parseDateHeader(name, (String) value); + } else if (value != null) { throw new IllegalArgumentException( - "Value for header '" + name + "' is neither a Date nor a Number: " + value); + "Value for header '" + name + "' is not a Date, Number, or String: " + value); } else { return -1L; } } + private long parseDateHeader(String name, String value) { + for (String dateFormat : DATE_FORMATS) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); + simpleDateFormat.setTimeZone(GMT); + try { + return simpleDateFormat.parse(value).getTime(); + } + catch (ParseException ex) { + // ignore + } + } + throw new IllegalArgumentException("Cannot parse date value '" + value + "' for '" + name + "' header"); + } + @Override public String getHeader(String name) { HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); @@ -1171,7 +1212,7 @@ public class MockHttpServletRequest implements HttpServletRequest { @Override public Collection getParts() throws IOException, IllegalStateException, ServletException { List result = new LinkedList(); - for(List list : this.parts.values()) { + for (List list : this.parts.values()) { result.addAll(list); } return result; diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 5f2db3d3764..bccc8dbd3a6 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -23,12 +23,16 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; + import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; @@ -47,6 +51,7 @@ import org.springframework.web.util.WebUtils; * * @author Juergen Hoeller * @author Rod Johnson + * @author Brian Clozel * @since 1.0.2 */ public class MockHttpServletResponse implements HttpServletResponse { @@ -59,6 +64,10 @@ public class MockHttpServletResponse implements HttpServletResponse { private static final String LOCATION_HEADER = "Location"; + private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + + private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + //--------------------------------------------------------------------- // ServletResponse properties @@ -481,12 +490,18 @@ public class MockHttpServletResponse implements HttpServletResponse { @Override public void setDateHeader(String name, long value) { - setHeaderValue(name, value); + setHeaderValue(name, formatDate(value)); } @Override public void addDateHeader(String name, long value) { - addHeaderValue(name, value); + addHeaderValue(name, formatDate(value)); + } + + private String formatDate(long date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US); + dateFormat.setTimeZone(GMT); + return dateFormat.format(new Date(date)); } @Override diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java index f0cd2866e60..322634e205f 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-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. @@ -21,6 +21,7 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.List; @@ -40,11 +41,17 @@ import static org.junit.Assert.*; * @author Mark Fisher * @author Rossen Stoyanchev * @author Sam Brannen + * @author Brian Clozel + * @author Jakub Narloch */ public class MockHttpServletRequestTests { private static final String HOST = "Host"; + private static final String CONTENT_TYPE = "Content-Type"; + + private static final String IF_MODIFIED_SINCE = "If-Modified-Since"; + private MockHttpServletRequest request = new MockHttpServletRequest(); @@ -69,7 +76,7 @@ public class MockHttpServletRequestTests { String contentType = "test/plain"; request.setContentType(contentType); assertEquals(contentType, request.getContentType()); - assertEquals(contentType, request.getHeader("Content-Type")); + assertEquals(contentType, request.getHeader(CONTENT_TYPE)); assertNull(request.getCharacterEncoding()); } @@ -78,7 +85,7 @@ public class MockHttpServletRequestTests { String contentType = "test/plain;charset=UTF-8"; request.setContentType(contentType); assertEquals(contentType, request.getContentType()); - assertEquals(contentType, request.getHeader("Content-Type")); + assertEquals(contentType, request.getHeader(CONTENT_TYPE)); assertEquals("UTF-8", request.getCharacterEncoding()); } @@ -87,7 +94,7 @@ public class MockHttpServletRequestTests { String contentType = "test/plain"; request.addHeader("Content-Type", contentType); assertEquals(contentType, request.getContentType()); - assertEquals(contentType, request.getHeader("Content-Type")); + assertEquals(contentType, request.getHeader(CONTENT_TYPE)); assertNull(request.getCharacterEncoding()); } @@ -96,7 +103,7 @@ public class MockHttpServletRequestTests { String contentType = "test/plain;charset=UTF-8"; request.addHeader("Content-Type", contentType); assertEquals(contentType, request.getContentType()); - assertEquals(contentType, request.getHeader("Content-Type")); + assertEquals(contentType, request.getHeader(CONTENT_TYPE)); assertEquals("UTF-8", request.getCharacterEncoding()); } @@ -107,7 +114,7 @@ public class MockHttpServletRequestTests { String contentType = "test/plain;charset=\"utf-8\";foo=\"charset=bar\";foocharset=bar;foo=bar"; request.addHeader("Content-Type", contentType); assertEquals(contentType, request.getContentType()); - assertEquals(contentType, request.getHeader("Content-Type")); + assertEquals(contentType, request.getHeader(CONTENT_TYPE)); assertEquals("UTF-8", request.getCharacterEncoding()); } @@ -116,7 +123,7 @@ public class MockHttpServletRequestTests { request.setContentType("test/plain"); request.setCharacterEncoding("UTF-8"); assertEquals("test/plain", request.getContentType()); - assertEquals("test/plain;charset=UTF-8", request.getHeader("Content-Type")); + assertEquals("test/plain;charset=UTF-8", request.getHeader(CONTENT_TYPE)); assertEquals("UTF-8", request.getCharacterEncoding()); } @@ -125,7 +132,7 @@ public class MockHttpServletRequestTests { request.setCharacterEncoding("UTF-8"); request.setContentType("test/plain"); assertEquals("test/plain", request.getContentType()); - assertEquals("test/plain;charset=UTF-8", request.getHeader("Content-Type")); + assertEquals("test/plain;charset=UTF-8", request.getHeader(CONTENT_TYPE)); assertEquals("UTF-8", request.getCharacterEncoding()); } @@ -378,6 +385,44 @@ public class MockHttpServletRequestTests { assertTrue(request.isSecure()); } + @Test + public void httpHeaderDate() throws Exception { + Date date = new Date(); + request.addHeader(IF_MODIFIED_SINCE, date); + assertEquals(date.getTime(), request.getDateHeader(IF_MODIFIED_SINCE)); + } + + @Test + public void httpHeaderTimestamp() throws Exception { + long timestamp = new Date().getTime(); + request.addHeader(IF_MODIFIED_SINCE, timestamp); + assertEquals(timestamp, request.getDateHeader(IF_MODIFIED_SINCE)); + } + + @Test + public void httpHeaderRfcFormatedDate() throws Exception { + request.addHeader(IF_MODIFIED_SINCE, "Tue, 21 Jul 2015 10:00:00 GMT"); + assertEquals(1437472800000L, request.getDateHeader(IF_MODIFIED_SINCE)); + } + + @Test + public void httpHeaderFirstVariantFormatedDate() throws Exception { + request.addHeader(IF_MODIFIED_SINCE, "Tue, 21-Jul-15 10:00:00 GMT"); + assertEquals(1437472800000L, request.getDateHeader(IF_MODIFIED_SINCE)); + } + + @Test + public void httpHeaderSecondVariantFormatedDate() throws Exception { + request.addHeader(IF_MODIFIED_SINCE, "Tue Jul 21 10:00:00 2015"); + assertEquals(1437472800000L, request.getDateHeader(IF_MODIFIED_SINCE)); + } + + @Test(expected = IllegalArgumentException.class) + public void httpHeaderFormatedDateError() throws Exception { + request.addHeader(IF_MODIFIED_SINCE, "This is not a date"); + request.getDateHeader(IF_MODIFIED_SINCE); + } + private void assertEqualEnumerations(Enumeration enum1, Enumeration enum2) { assertNotNull(enum1); assertNotNull(enum2); diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 3ed21c536c5..05341c8a8b0 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-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. @@ -19,6 +19,7 @@ package org.springframework.mock.web; import java.io.IOException; import java.util.Arrays; import java.util.Collection; + import javax.servlet.http.HttpServletResponse; import org.junit.Test; @@ -238,6 +239,20 @@ public class MockHttpServletResponseTests { assertEquals(redirectUrl, response.getRedirectedUrl()); } + @Test + public void setDateHeader() { + response.setDateHeader("Last-Modified", 1437472800000L); + assertEquals("Tue, 21 Jul 2015 10:00:00 GMT", response.getHeader("Last-Modified")); + } + + @Test + public void addDateHeader() { + response.addDateHeader("Last-Modified", 1437472800000L); + response.addDateHeader("Last-Modified", 1437472801000L); + assertEquals("Tue, 21 Jul 2015 10:00:00 GMT", response.getHeaders("Last-Modified").get(0)); + assertEquals("Tue, 21 Jul 2015 10:00:01 GMT", response.getHeaders("Last-Modified").get(1)); + } + /** * SPR-10414 */ diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java index 40b9a8ce1fa..0071cf05b9a 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java @@ -24,6 +24,8 @@ import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.security.Principal; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -36,6 +38,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TimeZone; + import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; @@ -120,6 +124,17 @@ public class MockHttpServletRequest implements HttpServletRequest { private static final ServletInputStream EMPTY_SERVLET_INPUT_STREAM = new DelegatingServletInputStream(new ByteArrayInputStream(new byte[0])); + /** + * Date formats as specified in the HTTP RFC + * @see Section 7.1.1.1 of RFC 7231 + */ + 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 boolean active = true; @@ -860,20 +875,16 @@ public class MockHttpServletRequest implements HttpServletRequest { /** * Add a header entry for the given name. - *

If there was no entry for that header name before, the value will be used - * as-is. In case of an existing entry, a String array will be created, - * adding the given value (more specifically, its toString representation) - * as further element. - *

Multiple values can only be stored as list of Strings, following the - * Servlet spec (see {@code getHeaders} accessor). As alternative to - * repeated {@code addHeader} calls for individual elements, you can - * use a single call with an entire array or Collection of values as - * parameter. + *

While this method can take any {@code Object} as a parameter, + * it is recommended to use the following types: + *

    + *
  • String or any Object to be converted using {@code toString}, see {@link #getHeader}
  • + *
  • String, Number or Date for date headers, see {@link #getDateHeader}
  • + *
  • String or Number for integer headers, see {@link #getIntHeader}
  • + *
  • {@code String[]} and {@code Collection} for multiple values, see {@link #getHeaders}
  • + *
* @see #getHeaderNames - * @see #getHeader * @see #getHeaders - * @see #getDateHeader - * @see #getIntHeader */ public void addHeader(String name, Object value) { if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name)) { @@ -902,6 +913,18 @@ public class MockHttpServletRequest implements HttpServletRequest { } } + /** + * Return the long timestamp for the date header with the given {@code name}. + *

If the internal value representation is a String, this method will try + * to parse it as a date using the supported date formats: + *

    + *
  • "EEE, dd MMM yyyy HH:mm:ss zzz"
  • + *
  • "EEE, dd-MMM-yy HH:mm:ss zzz"
  • + *
  • "EEE MMM dd HH:mm:ss yyyy"
  • + *
+ * @param name the header name + * @see Section 7.1.1.1 of RFC 7231 + */ @Override public long getDateHeader(String name) { HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); @@ -912,15 +935,32 @@ public class MockHttpServletRequest implements HttpServletRequest { else if (value instanceof Number) { return ((Number) value).longValue(); } + else if (value instanceof String) { + return parseDateHeader(name, (String) value); + } else if (value != null) { throw new IllegalArgumentException( - "Value for header '" + name + "' is neither a Date nor a Number: " + value); + "Value for header '" + name + "' is not a Date, Number, or String: " + value); } else { return -1L; } } + private long parseDateHeader(String name, String value) { + for (String dateFormat : DATE_FORMATS) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); + simpleDateFormat.setTimeZone(GMT); + try { + return simpleDateFormat.parse(value).getTime(); + } + catch (ParseException ex) { + // ignore + } + } + throw new IllegalArgumentException("Cannot parse date value '" + value + "' for '" + name + "' header"); + } + @Override public String getHeader(String name) { HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); @@ -1171,7 +1211,7 @@ public class MockHttpServletRequest implements HttpServletRequest { @Override public Collection getParts() throws IOException, IllegalStateException, ServletException { List result = new LinkedList(); - for(List list : this.parts.values()) { + for (List list : this.parts.values()) { result.addAll(list); } return result;