diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/context/PortletWebRequest.java b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/context/PortletWebRequest.java index 9be71736c4539170a47f6961f0bbbb796859a413..3eedfb651b60e8bae0cdcf854a86311edf634e7a 100644 --- a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/context/PortletWebRequest.java +++ b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/context/PortletWebRequest.java @@ -177,6 +177,14 @@ public class PortletWebRequest extends PortletRequestAttributes implements Nativ return false; } + /** + * Last-modified handling not supported for portlet requests: + * As a consequence, this method always returns false. + */ + public boolean checkNotModified(String eTag) { + return false; + } + public String getDescription(boolean includeClientInfo) { PortletRequest request = getRequest(); StringBuilder result = new StringBuilder(); diff --git a/org.springframework.web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java b/org.springframework.web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java index 458d67a29f722aab571cb992b1a89d081c7e0162..edd13a778a61034ad8ed25f1035242fa323676e7 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java +++ b/org.springframework.web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java @@ -130,6 +130,10 @@ public class FacesWebRequest extends FacesRequestAttributes implements NativeWeb return false; } + public boolean checkNotModified(String eTag) { + return false; + } + public String getDescription(boolean includeClientInfo) { ExternalContext externalContext = getExternalContext(); StringBuilder sb = new StringBuilder(); diff --git a/org.springframework.web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/org.springframework.web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java index b59909052b0d94ad71c63bab407b8256a59e95bf..17a23c08cedcf326c9593baca3b78e8cfb6ebe4c 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java +++ b/org.springframework.web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java @@ -40,10 +40,16 @@ import org.springframework.util.StringUtils; */ public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest { + private static final String HEADER_ETAG = "ETag"; + private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + private static final String HEADER_LAST_MODIFIED = "Last-Modified"; + private static final String METHOD_GET = "GET"; + private HttpServletResponse response; @@ -186,7 +192,7 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ long ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE); this.notModified = (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); if (this.response != null) { - if (this.notModified && "GET".equals(getRequest().getMethod())) { + if (this.notModified && METHOD_GET.equals(getRequest().getMethod())) { this.response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { @@ -197,6 +203,30 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return this.notModified; } + public boolean checkNotModified(String eTag) { + if (StringUtils.hasLength(eTag) && !this.notModified && + (this.response == null || !this.response.containsHeader(HEADER_ETAG))) { + if (!eTag.startsWith("\"")) { + eTag = "\"" + eTag; + } + if (!eTag.endsWith("\"")) { + eTag = eTag + "\""; + } + String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH); + this.notModified = eTag.equals(ifNoneMatch); + if (this.response != null) { + if (this.notModified && METHOD_GET.equals(getRequest().getMethod())) { + this.response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + else { + this.response.setHeader(HEADER_ETAG, eTag); + } + } + } + return this.notModified; + + } + public boolean isNotModified() { return this.notModified; } diff --git a/org.springframework.web/src/main/java/org/springframework/web/context/request/WebRequest.java b/org.springframework.web/src/main/java/org/springframework/web/context/request/WebRequest.java index 61219935944787b1bc718a2cdf271c66b60ff7e7..9db1794781c58fe6dfb144396b441d57412f7c83 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/context/request/WebRequest.java +++ b/org.springframework.web/src/main/java/org/springframework/web/context/request/WebRequest.java @@ -141,6 +141,9 @@ public interface WebRequest extends RequestAttributes { * model.addAttribute(...); * return "myViewName"; * } + *

Note: that you typically want to use either + * this {@link #checkNotModified(long)} method; or + * {@link #checkNotModified(String)}, but not both. * @param lastModifiedTimestamp the last-modified timestamp that * the application determined for the underlying resource * @return whether the request qualifies as not modified, @@ -149,6 +152,35 @@ public interface WebRequest extends RequestAttributes { */ boolean checkNotModified(long lastModifiedTimestamp); + /** + * Check whether the request qualifies as not modified given the + * supplied {@code ETag} (entity tag), as determined by the application. + *

This will also transparently set the appropriate response headers, + * for both the modified case and the not-modified case. + *

Typical usage: + *

+	 * public String myHandleMethod(WebRequest webRequest, Model model) {
+	 *   String eTag = // application-specific calculation
+	 *   if (request.checkNotModified(eTag)) {
+	 *     // shortcut exit - no further processing necessary
+	 *     return null;
+	 *   }
+	 *   // further request processing, actually building content
+	 *   model.addAttribute(...);
+	 *   return "myViewName";
+	 * }
+ *

Note: that you typically want to use either + * this {@link #checkNotModified(String)} method; or + * {@link #checkNotModified(long)}, but not both. + * @param eTag the entity tag that the application determined + * for the underlying resource. This parameter will be padded + * with quotes (") if necessary. + * @return whether the request qualifies as not modified, + * allowing to abort request processing and relying on the response + * telling the client that the content has not been modified + */ + boolean checkNotModified(String eTag); + /** * Get a short description of this request, * typically containing request URI and session id. diff --git a/org.springframework.web/src/test/java/org/springframework/web/context/request/ServletWebRequestTests.java b/org.springframework.web/src/test/java/org/springframework/web/context/request/ServletWebRequestTests.java index f8ae9ff4783e479ab0547efbe4de8544317d41a6..2e02082de587ffe9996e82b47511a51116ae344c 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/context/request/ServletWebRequestTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/context/request/ServletWebRequestTests.java @@ -16,6 +16,7 @@ package org.springframework.web.context.request; +import java.util.Date; import java.util.Locale; import java.util.Map; import javax.servlet.ServletRequest; @@ -25,27 +26,40 @@ import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; -import static org.junit.Assert.*; +import org.junit.Before; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.multipart.MultipartRequest; +import static org.junit.Assert.*; + /** * @author Juergen Hoeller * @since 26.07.2006 */ public class ServletWebRequestTests { + private MockHttpServletRequest servletRequest; + + private MockHttpServletResponse servletResponse; + + private ServletWebRequest request; + + @Before + public void setUp() { + servletRequest = new MockHttpServletRequest(); + servletResponse = new MockHttpServletResponse(); + request = new ServletWebRequest(servletRequest, servletResponse); + } + @Test - public void testParameters() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); + public void parameters() { servletRequest.addParameter("param1", "value1"); servletRequest.addParameter("param2", "value2"); servletRequest.addParameter("param2", "value2a"); - ServletWebRequest request = new ServletWebRequest(servletRequest); assertEquals("value1", request.getParameter("param1")); assertEquals(1, request.getParameterValues("param1").length); assertEquals("value1", request.getParameterValues("param1")[0]); @@ -64,19 +78,14 @@ public class ServletWebRequestTests { } @Test - public void testLocale() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); + public void locale() { servletRequest.addPreferredLocale(Locale.UK); - ServletWebRequest request = new ServletWebRequest(servletRequest); assertEquals(Locale.UK, request.getLocale()); } @Test - public void testNativeRequest() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - MockHttpServletResponse servletResponse = new MockHttpServletResponse(); - ServletWebRequest request = new ServletWebRequest(servletRequest, servletResponse); + public void nativeRequest() { assertSame(servletRequest, request.getNativeRequest()); assertSame(servletRequest, request.getNativeRequest(ServletRequest.class)); assertSame(servletRequest, request.getNativeRequest(HttpServletRequest.class)); @@ -90,9 +99,7 @@ public class ServletWebRequestTests { } @Test - public void testDecoratedNativeRequest() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + public void decoratedNativeRequest() { HttpServletRequest decoratedRequest = new HttpServletRequestWrapper(servletRequest); HttpServletResponse decoratedResponse = new HttpServletResponseWrapper(servletResponse); ServletWebRequest request = new ServletWebRequest(decoratedRequest, decoratedResponse); @@ -108,4 +115,65 @@ public class ServletWebRequestTests { assertNull(request.getNativeResponse(MultipartRequest.class)); } + @Test + public void checkNotModifiedTimeStamp() { + long currentTime = new Date().getTime(); + servletRequest.setMethod("GET"); + servletRequest.addHeader("If-Modified-Since", currentTime); + + request.checkNotModified(currentTime); + + assertEquals(304, servletResponse.getStatus()); + } + + @Test + public void checkModifiedTimeStamp() { + long currentTime = new Date().getTime(); + long oneMinuteAgo = currentTime - (1000 * 60); + servletRequest.setMethod("GET"); + servletRequest.addHeader("If-Modified-Since", oneMinuteAgo); + + request.checkNotModified(currentTime); + + assertEquals(200, servletResponse.getStatus()); + assertEquals(currentTime, servletResponse.getHeader("Last-Modified")); + } + + @Test + public void checkNotModifiedETag() { + String eTag = "Foo"; + servletRequest.setMethod("GET"); + servletRequest.addHeader("If-None-Match", "\"" + eTag + "\""); + + request.checkNotModified(eTag); + + assertEquals(304, servletResponse.getStatus()); + } + + @Test + public void checkModifiedETagNonQuoted() { + String currentETag = "Foo"; + String oldEtag = "Bar"; + servletRequest.setMethod("GET"); + servletRequest.addHeader("If-None-Match", "\"" + oldEtag + "\""); + + request.checkNotModified(currentETag); + + assertEquals(200, servletResponse.getStatus()); + assertEquals("\"" + currentETag + "\"", servletResponse.getHeader("ETag")); + } + + @Test + public void checkModifiedETagQuoted() { + String currentETag = "\"Foo\""; + String oldEtag = "Bar"; + servletRequest.setMethod("GET"); + servletRequest.addHeader("If-None-Match", oldEtag); + + request.checkNotModified(currentETag); + + assertEquals(200, servletResponse.getStatus()); + assertEquals(currentETag, servletResponse.getHeader("ETag")); + } + }