diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java index 6cc8e052721..b9ff874f9bb 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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.util.Date; import java.util.Iterator; import java.util.Locale; import java.util.Map; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -47,6 +48,8 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; private static final String HEADER_LAST_MODIFIED = "Last-Modified"; @@ -55,6 +58,12 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final String METHOD_HEAD = "HEAD"; + private static final String METHOD_POST = "POST"; + + private static final String METHOD_PUT = "PUT"; + + private static final String METHOD_DELETE = "DELETE"; + /** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */ private static final boolean servlet3Present = @@ -183,11 +192,18 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ if (isCompatibleWithConditionalRequests(response)) { this.notModified = isTimestampNotModified(lastModifiedTimestamp); if (response != null) { - if (this.notModified && supportsNotModifiedStatus()) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + if (supportsNotModifiedStatus()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { + response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + } } - if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { - response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + else if (supportsConditionalUpdate()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + } } } } @@ -223,14 +239,21 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ etag = addEtagPadding(etag); this.notModified = isEtagNotModified(etag) && isTimestampNotModified(lastModifiedTimestamp); if (response != null) { - if (this.notModified && supportsNotModifiedStatus()) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } - if (isHeaderAbsent(response, HEADER_ETAG)) { - response.setHeader(HEADER_ETAG, etag); + if (supportsNotModifiedStatus()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + if (isHeaderAbsent(response, HEADER_ETAG)) { + response.setHeader(HEADER_ETAG, etag); + } + if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { + response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + } } - if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { - response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + else if (supportsConditionalUpdate()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + } } } } @@ -250,7 +273,8 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return true; } return HttpStatus.valueOf(response.getStatus()).is2xxSuccessful(); - } catch (IllegalArgumentException e) { + } + catch (IllegalArgumentException e) { return true; } } @@ -268,27 +292,46 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return (METHOD_GET.equals(method) || METHOD_HEAD.equals(method)); } - @SuppressWarnings("deprecation") + private boolean supportsConditionalUpdate() { + String method = getRequest().getMethod(); + String ifUnmodifiedHeader = getRequest().getHeader(HEADER_IF_UNMODIFIED_SINCE); + return (METHOD_POST.equals(method) || METHOD_PUT.equals(method) || METHOD_DELETE.equals(method)) + && StringUtils.hasLength(ifUnmodifiedHeader); + } + private boolean isTimestampNotModified(long lastModifiedTimestamp) { - long ifModifiedSince = -1; + long ifModifiedSince = parseDateHeader(HEADER_IF_MODIFIED_SINCE); + if (ifModifiedSince != -1) { + return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); + } + long ifUnmodifiedSince = parseDateHeader(HEADER_IF_UNMODIFIED_SINCE); + if (ifUnmodifiedSince != -1) { + return (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000)); + } + return false; + } + + @SuppressWarnings("deprecation") + private long parseDateHeader(String headerName) { + long dateValue = -1; try { - ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE); + dateValue = getRequest().getDateHeader(headerName); } catch (IllegalArgumentException ex) { - String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE); + String headerValue = getRequest().getHeader(headerName); // Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774" int separatorIndex = headerValue.indexOf(';'); if (separatorIndex != -1) { String datePart = headerValue.substring(0, separatorIndex); try { - ifModifiedSince = Date.parse(datePart); + dateValue = Date.parse(datePart); } catch (IllegalArgumentException ex2) { // Giving up } } } - return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); + return dateValue; } private boolean isEtagNotModified(String etag) { diff --git a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java index cea9fa70af3..8c190f74c24 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -126,10 +126,10 @@ public interface WebRequest extends RequestAttributes { boolean isSecure(); /** - * Check whether the request qualifies as not modified given the + * Check whether the requested resource has been modified given the * supplied last-modified timestamp (as determined by the application). - *

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

This will also transparently set the "Last-Modified" response header + * and HTTP status when applicable. *

Typical usage: *

 	 * public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -142,6 +142,8 @@ public interface WebRequest extends RequestAttributes {
 	 *   model.addAttribute(...);
 	 *   return "myViewName";
 	 * }
+ *

This method works with conditional GET/HEAD requests, but + * also with conditional POST/PUT/DELETE requests. *

Note: you can use either * this {@code #checkNotModified(long)} method; or * {@link #checkNotModified(String)}. If you want enforce both @@ -160,10 +162,10 @@ public interface WebRequest extends RequestAttributes { boolean checkNotModified(long lastModifiedTimestamp); /** - * Check whether the request qualifies as not modified given the + * Check whether the requested resource has been 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. + *

This will also transparently set the "ETag" response header + * and HTTP status when applicable. *

Typical usage: *

 	 * public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -185,18 +187,16 @@ public interface WebRequest extends RequestAttributes {
 	 * @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
+	 * @return true if the request does not require further processing.
 	 */
 	boolean checkNotModified(String etag);
 
 	/**
-	 * Check whether the request qualifies as not modified given the
+	 * Check whether the requested resource has been modified given the
 	 * supplied {@code ETag} (entity tag) and last-modified timestamp,
 	 * as determined by the application.
 	 * 

This will also transparently set the "ETag" and "Last-Modified" - * response headers, for both the modified case and the not-modified case. + * response headers, and HTTP status when applicable. *

Typical usage: *

 	 * public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -210,6 +210,8 @@ public interface WebRequest extends RequestAttributes {
 	 *   model.addAttribute(...);
 	 *   return "myViewName";
 	 * }
+ *

This method works with conditional GET/HEAD requests, but + * also with conditional POST/PUT/DELETE requests. *

Note: The HTTP specification recommends * setting both ETag and Last-Modified values, but you can also * use {@code #checkNotModified(String)} or @@ -219,9 +221,7 @@ public interface WebRequest extends RequestAttributes { * with quotes (") if necessary. * @param lastModifiedTimestamp the last-modified timestamp that * the application determined for the underlying resource - * @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 + * @return true if the request does not require further processing. * @since 4.2 */ boolean checkNotModified(String etag, long lastModifiedTimestamp); diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java index 3832ae3a0b4..6dd2adf9edf 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -16,6 +16,8 @@ package org.springframework.web.context.request; +import static org.junit.Assert.*; + import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; @@ -32,8 +34,6 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; -import static org.junit.Assert.*; - /** * Parameterized tests for ServletWebRequest * @author Juergen Hoeller @@ -293,4 +293,28 @@ public class ServletWebRequestHttpMethodsTests { assertEquals(dateFormat.format(epochTime), servletResponse.getHeader("Last-Modified")); } + @Test + public void checkNotModifiedTimestampConditionalPut() throws Exception { + long currentEpoch = currentDate.getTime(); + long oneMinuteAgo = currentEpoch - (1000 * 60); + servletRequest.setMethod("PUT"); + servletRequest.addHeader("If-UnModified-Since", currentEpoch); + + assertFalse(request.checkNotModified(oneMinuteAgo)); + assertEquals(200, servletResponse.getStatus()); + assertEquals(null, servletResponse.getHeader("Last-Modified")); + } + + @Test + public void checkNotModifiedTimestampConditionalPutConflict() throws Exception { + long currentEpoch = currentDate.getTime(); + long oneMinuteAgo = currentEpoch - (1000 * 60); + servletRequest.setMethod("PUT"); + servletRequest.addHeader("If-UnModified-Since", oneMinuteAgo); + + assertTrue(request.checkNotModified(currentEpoch)); + assertEquals(412, servletResponse.getStatus()); + assertEquals(null, servletResponse.getHeader("Last-Modified")); + } + } diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index 2f82b89af83..2db3fc036b4 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -4449,17 +4449,24 @@ This can be achieved as follows: ---- There are two key elements here: calling `request.checkNotModified(lastModified)` and -returning `null`. The former sets the response status to 304 before it returns `true`. +returning `null`. The former sets the appropriate response status and headers +before it returns `true`. The latter, in combination with the former, causes Spring MVC to do no further processing of the request. Note that there are 3 variants for this: * `request.checkNotModified(lastModified)` compares lastModified with the -`'If-Modified-Since'` request header -* `request.checkNotModified(eTag)` compares eTag with the `'ETag'` request header +`'If-Modified-Since'` or `'If-Unmodified-Since'` request header +* `request.checkNotModified(eTag)` compares eTag with the `'If-None-Match'` request header * `request.checkNotModified(eTag, lastModified)` does both, meaning that both -conditions should be valid for the server to issue an `HTTP 304 Not Modified` response +conditions should be valid + +When receiving conditional `'GET'`/`'HEAD'` requests, `checkNotModified` will check +that the resource has not been modified and if so, it will result in a `HTTP 304 Not Modified` +response. In case of conditional `'POST'`/`'PUT'`/`'DELETE'` requests, `checkNotModified` +will check that the resource has not been modified and if it has been, it will result in a +`HTTP 409 Precondition Failed` response to prevent concurrent modifications. [[mvc-httpcaching-shallowetag]]