diff --git a/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java index 18983a14cf..ee9f8bf0c6 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java @@ -155,6 +155,17 @@ public class FacesWebRequest extends FacesRequestAttributes implements NativeWeb return false; } + /** + * Last-modified handling not supported for portlet requests: + * As a consequence, this method always returns {@code false}. + * + * @since 4.2 + */ + @Override + public boolean checkNotModified(String etag, long lastModifiedTimestamp) { + return false; + } + @Override public String getDescription(boolean includeClientInfo) { ExternalContext externalContext = getExternalContext(); 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 079314f606..5b8c5314ee 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-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.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; @@ -35,6 +36,9 @@ import org.springframework.web.util.WebUtils; * {@link WebRequest} adapter for an {@link javax.servlet.http.HttpServletRequest}. * * @author Juergen Hoeller + * @author Brian Clozel + * @author Markus Malkusch + * * @since 2.0 */ public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest { @@ -72,7 +76,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ super(request, response); } - @Override public Object getNativeRequest() { return getRequest(); @@ -93,7 +96,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return WebUtils.getNativeResponse(getResponse(), requiredType); } - /** * Return the HTTP method of the request. * @since 4.0.2 @@ -169,35 +171,15 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ } @Override - @SuppressWarnings("deprecation") public boolean checkNotModified(long lastModifiedTimestamp) { HttpServletResponse response = getResponse(); - if (lastModifiedTimestamp >= 0 && !this.notModified && - (response == null || !response.containsHeader(HEADER_LAST_MODIFIED))) { - long ifModifiedSince = -1; - try { - ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE); - } - catch (IllegalArgumentException ex) { - String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE); - // 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); + if (lastModifiedTimestamp >= 0 && !this.notModified) { + if (response == null || !response.containsHeader(HEADER_LAST_MODIFIED)) { + this.notModified = isTimeStampNotModified(lastModifiedTimestamp); + if (response != null) { + if (this.notModified && supportsNotModifiedStatus()) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } - catch (IllegalArgumentException ex2) { - // Giving up - } - } - } - this.notModified = (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); - if (response != null) { - if (this.notModified && supportsNotModifiedStatus()) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } - else { response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); } } @@ -205,18 +187,40 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return this.notModified; } + @SuppressWarnings("deprecation") + private boolean isTimeStampNotModified(long lastModifiedTimestamp) { + long ifModifiedSince = -1; + try { + ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE); + } + catch (IllegalArgumentException ex) { + String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE); + // 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); + } + catch (IllegalArgumentException ex2) { + // Giving up + } + } + } + return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); + } + @Override public boolean checkNotModified(String etag) { HttpServletResponse response = getResponse(); - if (StringUtils.hasLength(etag) && !this.notModified && - (response == null || !response.containsHeader(HEADER_ETAG))) { - String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH); - this.notModified = etag.equals(ifNoneMatch); - if (response != null) { - if (this.notModified && supportsNotModifiedStatus()) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } - else { + if (StringUtils.hasLength(etag) && !this.notModified) { + if (response == null || !response.containsHeader(HEADER_ETAG)) { + etag = addEtagPadding(etag); + this.notModified = isETagNotModified(etag); + if (response != null) { + if (this.notModified && supportsNotModifiedStatus()) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } response.setHeader(HEADER_ETAG, etag); } } @@ -224,11 +228,56 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return this.notModified; } + private String addEtagPadding(String etag) { + if (!(etag.startsWith("\"") || etag.startsWith("W/\"")) || !etag.endsWith("\"")) { + etag = "\"" + etag + "\""; + } + return etag; + } + + private boolean isETagNotModified(String etag) { + if (StringUtils.hasLength(etag)) { + String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH); + if (StringUtils.hasLength(ifNoneMatch)) { + String[] clientETags = StringUtils.delimitedListToStringArray(ifNoneMatch, ",", " "); + for (String clientETag : clientETags) { + // compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 + if (StringUtils.hasLength(clientETag) && + (clientETag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", "")) + || clientETag.equals("*"))) { + return true; + } + } + } + } + return false; + } + private boolean supportsNotModifiedStatus() { String method = getRequest().getMethod(); return (METHOD_GET.equals(method) || METHOD_HEAD.equals(method)); } + @Override + public boolean checkNotModified(String etag, long lastModifiedTimestamp) { + HttpServletResponse response = getResponse(); + if (StringUtils.hasLength(etag) && !this.notModified) { + if (response == null || + (!response.containsHeader(HEADER_ETAG) && !response.containsHeader(HEADER_LAST_MODIFIED))) { + etag = addEtagPadding(etag); + this.notModified = isETagNotModified(etag) && isTimeStampNotModified(lastModifiedTimestamp); + if (response != null) { + if (this.notModified && supportsNotModifiedStatus()) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + response.setHeader(HEADER_ETAG, etag); + response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + } + } + } + return this.notModified; + } + public boolean isNotModified() { return this.notModified; } 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 401451494f..6a9d6bb547 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-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. @@ -141,9 +141,12 @@ public interface WebRequest extends RequestAttributes { * model.addAttribute(...); * return "myViewName"; * } - *
Note: that you typically want to use either + *
Note: you can use either * this {@code #checkNotModified(long)} method; or - * {@link #checkNotModified(String)}, but not both. + * {@link #checkNotModified(String)}. If you want enforce both + * a strong entity tag and a Last-Modified value, + * as recommended by the HTTP specification, + * then you should use {@link #checkNotModified(String, long)}. *
If the "If-Modified-Since" header is set but cannot be parsed * to a date value, this method will ignore the header and proceed * with setting the last-modified timestamp on the response. @@ -172,9 +175,12 @@ public interface WebRequest extends RequestAttributes { * model.addAttribute(...); * return "myViewName"; * } - *
Note: that you typically want to use either + *
Note: you can use either * this {@code #checkNotModified(String)} method; or - * {@link #checkNotModified(long)}, but not both. + * {@link #checkNotModified(long)}. If you want enforce both + * a strong entity tag and a Last-Modified value, + * as recommended by the HTTP specification, + * then you should use {@link #checkNotModified(String, long)}. * @param etag the entity tag that the application determined * for the underlying resource. This parameter will be padded * with quotes (") if necessary. @@ -184,6 +190,42 @@ public interface WebRequest extends RequestAttributes { */ boolean checkNotModified(String etag); + /** + * Check whether the request qualifies as not modified given the + * supplied {@code ETag} (entity tag) and 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. + *
Typical usage: + *
+ * public String myHandleMethod(WebRequest webRequest, Model model) { + * String eTag = // application-specific calculation + * long lastModified = // application-specific calculation + * if (request.checkNotModified(eTag, lastModified)) { + * // shortcut exit - no further processing necessary + * return null; + * } + * // further request processing, actually building content + * model.addAttribute(...); + * return "myViewName"; + * }+ *
Note: The HTTP specification recommends + * setting both ETag and Last-Modified values, but you can also + * use {@code #checkNotModified(String)} or + * {@link #checkNotModified(long)}. + * @param etag the entity tag that the application determined + * for the underlying resource. This parameter will be padded + * 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 + * + * @since 4.2 + */ + boolean checkNotModified(String etag, long lastModifiedTimestamp); + /** * Get a short description of this request, * typically containing request URI and session id. 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 new file mode 100644 index 0000000000..7121a8bfe8 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java @@ -0,0 +1,258 @@ +/* + * 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. + * 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.web.context.request; + +import static org.junit.Assert.*; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; + +/** + * Parameterized tests for ServletWebRequest + * @author Juergen Hoeller + * @author Brian Clozel + * @author Markus Malkusch + */ +@RunWith(Parameterized.class) +public class ServletWebRequestHttpMethodsTests { + + private SimpleDateFormat dateFormat; + + private MockHttpServletRequest servletRequest; + + private MockHttpServletResponse servletResponse; + + private ServletWebRequest request; + + private String method; + + @Parameters + static public Iterable