diff --git a/spring-test/src/main/java/org/springframework/mock/web/FastHttpDateFormat.java b/spring-test/src/main/java/org/springframework/mock/web/FastHttpDateFormat.java new file mode 100644 index 000000000000..e40240848dfd --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/FastHttpDateFormat.java @@ -0,0 +1,219 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.mock.web; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.TimeZone; + + +/** + * Utility class to generate HTTP dates. + *

+ * This class is based on code in Apache Tomcat. + * + * @author Remy Maucherat + * @author Andrey Grebnev + */ +public class FastHttpDateFormat { + //~ Static fields/initializers ===================================================================================== + + /** HTTP date format. */ + protected static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + + /** The set of SimpleDateFormat formats to use in getDateHeader(). */ + protected static final SimpleDateFormat[] formats = { + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) + }; + + /** GMT time zone - all HTTP dates are on GMT */ + protected static final TimeZone gmtZone = TimeZone.getTimeZone("GMT"); + + static { + format.setTimeZone(gmtZone); + + formats[0].setTimeZone(gmtZone); + formats[1].setTimeZone(gmtZone); + formats[2].setTimeZone(gmtZone); + } + + /** Instant on which the currentDate object was generated. */ + protected static long currentDateGenerated = 0L; + + /** Current formatted date. */ + protected static String currentDate = null; + + /** Formatter cache. */ + protected static final HashMap formatCache = new HashMap(); + + /** Parser cache. */ + protected static final HashMap parseCache = new HashMap(); + + //~ Methods ======================================================================================================== + + /** + * Formats a specified date to HTTP format. If local format is not null, it's used instead. + * + * @param value Date value to format + * @param threadLocalformat The format to use (or null -- then HTTP format will be used) + * + * @return Formatted date + */ + public static String formatDate(long value, DateFormat threadLocalformat) { + String cachedDate = null; + Long longValue = Long.valueOf(value); + + try { + cachedDate = formatCache.get(longValue); + } catch (Exception ignored) {} + + if (cachedDate != null) { + return cachedDate; + } + + String newDate; + Date dateValue = new Date(value); + + if (threadLocalformat != null) { + newDate = threadLocalformat.format(dateValue); + + synchronized (formatCache) { + updateCache(formatCache, longValue, newDate); + } + } else { + synchronized (formatCache) { + newDate = format.format(dateValue); + updateCache(formatCache, longValue, newDate); + } + } + + return newDate; + } + + /** + * Gets the current date in HTTP format. + * + * @return Current date in HTTP format + */ + public static String getCurrentDate() { + long now = System.currentTimeMillis(); + + if ((now - currentDateGenerated) > 1000) { + synchronized (format) { + if ((now - currentDateGenerated) > 1000) { + currentDateGenerated = now; + currentDate = format.format(new Date(now)); + } + } + } + + return currentDate; + } + + /** + * Parses date with given formatters. + * + * @param value The string to parse + * @param formats Array of formats to use + * + * @return Parsed date (or null if no formatter mached) + */ + private static Long internalParseDate(String value, DateFormat[] formats) { + Date date = null; + + for (int i = 0; (date == null) && (i < formats.length); i++) { + try { + date = formats[i].parse(value); + } catch (ParseException ignored) { + } + } + + if (date == null) { + return null; + } + + return new Long(date.getTime()); + } + + /** + * Tries to parse the given date as an HTTP date. If local format list is not null, it's used + * instead. + * + * @param value The string to parse + * @param threadLocalformats Array of formats to use for parsing. If null, HTTP formats are used. + * + * @return Parsed date (or -1 if error occurred) + */ + public static long parseDate(String value, DateFormat[] threadLocalformats) { + Long cachedDate = null; + + try { + cachedDate = (Long) parseCache.get(value); + } catch (Exception ignored) {} + + if (cachedDate != null) { + return cachedDate.longValue(); + } + + Long date; + + if (threadLocalformats != null) { + date = internalParseDate(value, threadLocalformats); + + synchronized (parseCache) { + updateCache(parseCache, value, date); + } + } else { + synchronized (parseCache) { + date = internalParseDate(value, formats); + updateCache(parseCache, value, date); + } + } + + if (date == null) { + return (-1L); + } else { + return date.longValue(); + } + } + + /** + * Updates cache. + * + * @param cache Cache to be updated + * @param key Key to be updated + * @param value New value + */ + @SuppressWarnings("unchecked") + private static void updateCache(HashMap cache, Object key, Object value) { + if (value == null) { + return; + } + + if (cache.size() > 1000) { + cache.clear(); + } + + cache.put(key, value); + } +} 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 8e1384c09e3d..0f1ff57406b4 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,7 @@ import java.io.Reader; import java.io.UnsupportedEncodingException; import java.security.Principal; +import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -36,6 +37,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; @@ -197,7 +199,8 @@ public class MockHttpServletRequest implements HttpServletRequest { private boolean requestedSessionIdFromURL = false; private final Map parts = new LinkedHashMap(); - + + private SimpleDateFormat[] formats; // --------------------------------------------------------------------- // Constructors @@ -253,9 +256,16 @@ public MockHttpServletRequest(ServletContext servletContext, String method, Stri this.method = method; this.requestURI = requestURI; this.locales.add(Locale.ENGLISH); + + this.formats = new SimpleDateFormat[3]; + this.formats[0] = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", this.getLocale()); + this.formats[1] = new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", this.getLocale()); + this.formats[2] = new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", this.getLocale()); + this.formats[0].setTimeZone(TimeZone.getTimeZone("GMT")); + this.formats[1].setTimeZone(TimeZone.getTimeZone("GMT")); + this.formats[2].setTimeZone(TimeZone.getTimeZone("GMT")); } - // --------------------------------------------------------------------- // Lifecycle methods // --------------------------------------------------------------------- @@ -820,6 +830,9 @@ public long getDateHeader(String name) { else if (value instanceof Number) { return ((Number) value).longValue(); } + else if (value instanceof String) { + return FastHttpDateFormat.parseDate((String)value, this.formats); + } else if (value != null) { throw new IllegalArgumentException( "Value for header '" + name + "' is neither a Date nor a Number: " + value); 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 395442fee684..165186341311 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.PrintWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; 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; @@ -460,9 +464,16 @@ public String getRedirectedUrl() { return getHeader(LOCATION_HEADER); } + private DateFormat format = null; + @Override public void setDateHeader(String name, long value) { - setHeaderValue(name, value); + if (this.format == null) { + this.format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", this.locale); + this.format.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + setHeaderValue(name, FastHttpDateFormat.formatDate(value, this.format)); } @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 35900fa6be23..9da666d19a21 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 @@ -16,14 +16,18 @@ package org.springframework.mock.web; +import java.text.DateFormat; +import java.text.SimpleDateFormat; 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; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; import org.junit.Test; @@ -232,6 +236,20 @@ public void getRequestURLWithNegativePort() { StringBuffer requestURL = request.getRequestURL(); assertEquals("http://localhost", requestURL.toString()); } + + /** + * SPR-11912 + */ + @Test + public void getDateHeaderWithString() { + DateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", request.getLocale()); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + long currentTime = System.currentTimeMillis(); + String formatStr = format.format(new Date(currentTime)); + request.addHeader("testDate", formatStr); + assertEquals(Long.toString(currentTime).substring(0, 10), + Long.toString(request.getDateHeader("testDate")).subSequence(0, 10)); + } private void assertEqualEnumerations(Enumeration enum1, Enumeration enum2) { assertNotNull(enum1); 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 55bb66b36d58..54c021615ced 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 @@ -17,13 +17,16 @@ package org.springframework.mock.web; import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collection; +import java.util.Date; +import java.util.TimeZone; import javax.servlet.http.HttpServletResponse; import org.junit.Test; - import org.springframework.web.util.WebUtils; import static org.junit.Assert.*; @@ -243,4 +246,15 @@ public void modifyStatusMessageAfterSendError() throws IOException { assertEquals(response.getStatus(),HttpServletResponse.SC_NOT_FOUND); } + /** + * SPR-11912 + */ + @Test + public void getDateHeader() { + DateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", response.getLocale()); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + long currentTime = System.currentTimeMillis(); + response.setDateHeader("testTime", currentTime); + assertEquals(format.format(new Date(currentTime)), response.getHeader("testTime")); + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java index 503662876f63..6da2f7ffbbd0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java @@ -16,17 +16,19 @@ package org.springframework.web.servlet.support; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; @@ -35,6 +37,7 @@ import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.FlashMapManager; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.WebUtils; /** * A base class for {@link FlashMapManager} implementations. @@ -175,14 +178,30 @@ protected boolean isFlashMapForRequest(FlashMap flashMap, HttpServletRequest req MultiValueMap targetParams = flashMap.getTargetRequestParams(); for (String expectedName : targetParams.keySet()) { for (String expectedValue : targetParams.get(expectedName)) { - if (!ObjectUtils.containsElement(request.getParameterValues(expectedName), expectedValue)) { + if (!ObjectUtils.containsElement(request.getParameterValues(expectedName), decodeString(request, expectedValue))) { return false; } } } return true; } - + + @SuppressWarnings("deprecation") + private String decodeString(HttpServletRequest request, String targetValue) { + String enc = request.getCharacterEncoding(); + if (enc == null) { + enc = WebUtils.DEFAULT_CHARACTER_ENCODING; + } + + String result; + try { + result = URLDecoder.decode(targetValue, enc); + } catch (UnsupportedEncodingException e) { + result = URLDecoder.decode(targetValue); + } + return result; + } + @Override public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response) { if (CollectionUtils.isEmpty(flashMap)) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/support/FlashMapManagerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/support/FlashMapManagerTests.java index f0399ada7d1e..0b4e06cd6568 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/support/FlashMapManagerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/support/FlashMapManagerTests.java @@ -16,16 +16,18 @@ package org.springframework.web.servlet.support; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; - import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.util.MultiValueMap; @@ -307,5 +309,29 @@ protected void updateFlashMaps(List flashMaps, HttpServletRequest requ this.flashMaps = flashMaps; } } + + // SPR-11821 + @Test + public void retrieveAndUpdateMatchByParamsWithSpace() throws UnsupportedEncodingException { + this.request.setCharacterEncoding("UTF-8"); + String enc = this.request.getCharacterEncoding(); + if (enc == null) { + enc = WebUtils.DEFAULT_CHARACTER_ENCODING; + } + + FlashMap flashMap = new FlashMap(); + flashMap.put("key", "value"); + flashMap.addTargetRequestParam("ab", URLEncoder.encode("a b", enc)); + flashMap.addTargetRequestParam("abc", URLEncoder.encode("a b c", enc)); + + this.flashMapManager.setFlashMaps(flashMap); + this.request.setParameter("ab", "a b"); + this.request.setParameter("abc", "a b c"); + FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(this.request, this.response); + + assertNotNull(inputFlashMap); + assertEquals("value", inputFlashMap.get("key")); + } + }