Browse Source

Add checkNotModified support in ServerWebExchange

Issue: SPR-14522
pull/1153/head
Rossen Stoyanchev 10 years ago
parent
commit
6071e01168
  1. 39
      spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java
  2. 137
      spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java
  3. 332
      spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java

39
spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package org.springframework.web.server;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
@ -67,4 +68,42 @@ public interface ServerWebExchange { @@ -67,4 +68,42 @@ public interface ServerWebExchange {
*/
Mono<WebSession> getSession();
/**
* An overloaded variant of {@link #checkNotModified(String, Instant)} with
* a last-modified timestamp only.
* @param lastModified the last-modified time
* @return whether the request qualifies as not modified
*/
boolean checkNotModified(Instant lastModified);
/**
* An overloaded variant of {@link #checkNotModified(String, Instant)} with
* an {@code ETag} (entity tag) value only.
* @param etag the entity tag for the underlying resource.
* @return true if the request does not require further processing.
*/
boolean checkNotModified(String etag);
/**
* Check whether the requested resource has been modified given the supplied
* {@code ETag} (entity tag) and last-modified timestamp as determined by
* the application. Also transparently prepares the response, setting HTTP
* status, and adding "ETag" and "Last-Modified" headers when applicable.
* This method works with conditional GET/HEAD requests as well as with
* conditional POST/PUT/DELETE requests.
*
* <p><strong>Note:</strong> The HTTP specification recommends setting both
* ETag and Last-Modified values, but you can also use
* {@code #checkNotModified(String)} or
* {@link #checkNotModified(Instant)}.
*
* @param etag the entity tag that the application determined for the
* underlying resource. This parameter will be padded with quotes (")
* if necessary.
* @param lastModified the last-modified timestamp that the application
* determined for the underlying resource
* @return true if the request does not require further processing.
*/
boolean checkNotModified(String etag, Instant lastModified);
}

137
spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java

@ -16,15 +16,23 @@ @@ -16,15 +16,23 @@
package org.springframework.web.server.adapter;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionManager;
@ -37,6 +45,9 @@ import org.springframework.web.server.session.WebSessionManager; @@ -37,6 +45,9 @@ import org.springframework.web.server.session.WebSessionManager;
*/
public class DefaultServerWebExchange implements ServerWebExchange {
private static final List<HttpMethod> SAFE_METHODS = Arrays.asList(HttpMethod.GET, HttpMethod.HEAD);
private final ServerHttpRequest request;
private final ServerHttpResponse response;
@ -45,6 +56,8 @@ public class DefaultServerWebExchange implements ServerWebExchange { @@ -45,6 +56,8 @@ public class DefaultServerWebExchange implements ServerWebExchange {
private final Mono<WebSession> sessionMono;
private volatile boolean notModified;
public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
WebSessionManager sessionManager) {
@ -63,11 +76,19 @@ public class DefaultServerWebExchange implements ServerWebExchange { @@ -63,11 +76,19 @@ public class DefaultServerWebExchange implements ServerWebExchange {
return this.request;
}
private HttpHeaders getRequestHeaders() {
return getRequest().getHeaders();
}
@Override
public ServerHttpResponse getResponse() {
return this.response;
}
private HttpHeaders getResponseHeaders() {
return getResponse().getHeaders();
}
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
@ -83,4 +104,120 @@ public class DefaultServerWebExchange implements ServerWebExchange { @@ -83,4 +104,120 @@ public class DefaultServerWebExchange implements ServerWebExchange {
return this.sessionMono;
}
@Override
public boolean checkNotModified(Instant lastModified) {
return checkNotModified(null, lastModified);
}
@Override
public boolean checkNotModified(String etag) {
return checkNotModified(etag, Instant.MIN);
}
@Override
public boolean checkNotModified(String etag, Instant lastModified) {
HttpStatus status = getResponse().getStatusCode();
if (this.notModified || (status != null && !HttpStatus.OK.equals(status))) {
return this.notModified;
}
// Evaluate conditions in order of precedence.
// See https://tools.ietf.org/html/rfc7232#section-6
if (validateIfUnmodifiedSince(lastModified)) {
if (this.notModified) {
getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED);
}
return this.notModified;
}
boolean validated = validateIfNoneMatch(etag);
if (!validated) {
validateIfModifiedSince(lastModified);
}
// Update response
boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
if (this.notModified) {
getResponse().setStatusCode(isHttpGetOrHead ?
HttpStatus.NOT_MODIFIED : HttpStatus.PRECONDITION_FAILED);
}
if (isHttpGetOrHead) {
if (lastModified.isAfter(Instant.EPOCH) && getResponseHeaders().getLastModified() == -1) {
getResponseHeaders().setLastModified(lastModified.toEpochMilli());
}
if (StringUtils.hasLength(etag) && getResponseHeaders().getETag() == null) {
getResponseHeaders().setETag(padEtagIfNecessary(etag));
}
}
return this.notModified;
}
private boolean validateIfUnmodifiedSince(Instant lastModified) {
if (lastModified.isBefore(Instant.EPOCH)) {
return false;
}
long ifUnmodifiedSince = getRequestHeaders().getIfUnmodifiedSince();
if (ifUnmodifiedSince == -1) {
return false;
}
// We will perform this validation...
Instant sinceInstant = Instant.ofEpochMilli(ifUnmodifiedSince);
this.notModified = sinceInstant.isBefore(lastModified.truncatedTo(ChronoUnit.SECONDS));
return true;
}
private boolean validateIfNoneMatch(String etag) {
if (!StringUtils.hasLength(etag)) {
return false;
}
List<String> ifNoneMatch;
try {
ifNoneMatch = getRequestHeaders().getIfNoneMatch();
}
catch (IllegalArgumentException ex) {
return false;
}
if (ifNoneMatch.isEmpty()) {
return false;
}
// We will perform this validation...
etag = padEtagIfNecessary(etag);
for (String clientETag : ifNoneMatch) {
// 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/", ""))) {
this.notModified = true;
break;
}
}
return true;
}
private String padEtagIfNecessary(String etag) {
if (!StringUtils.hasLength(etag)) {
return etag;
}
if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) {
return etag;
}
return "\"" + etag + "\"";
}
private boolean validateIfModifiedSince(Instant lastModified) {
if (lastModified.isBefore(Instant.EPOCH)) {
return false;
}
long ifModifiedSince = getRequestHeaders().getIfModifiedSince();
if (ifModifiedSince == -1) {
return false;
}
// We will perform this validation...
this.notModified = ChronoUnit.SECONDS.between(lastModified, Instant.ofEpochMilli(ifModifiedSince)) >= 0;
return true;
}
}

332
spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java

@ -0,0 +1,332 @@ @@ -0,0 +1,332 @@
/*
* 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.
* 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.server.adapter;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Locale;
import java.util.TimeZone;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Parameterized tests for ServletWebRequest
* @author Rossen Stoyanchev
*/
@RunWith(Parameterized.class)
public class DefaultServerWebExchangeCheckNotModifiedTests {
private static final String CURRENT_TIME = "Wed, 09 Apr 2014 09:57:42 GMT";
private SimpleDateFormat dateFormat;
private MockServerHttpRequest request;
private MockServerHttpResponse response;
private DefaultServerWebExchange exchange;
private Instant currentDate;
@Parameter
public HttpMethod method;
@Parameters(name = "{0}")
static public Iterable<Object[]> safeMethods() {
return Arrays.asList(new Object[][] {
{HttpMethod.GET},
{HttpMethod.HEAD}
});
}
@Before
public void setUp() throws URISyntaxException {
currentDate = Instant.now().truncatedTo(ChronoUnit.SECONDS);
dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
request = new MockServerHttpRequest(method, new URI("http://example.org"));
response = new MockServerHttpResponse();
exchange = new DefaultServerWebExchange(request, response, new MockWebSessionManager());
}
@Test
public void checkNotModifiedNon2xxStatus() {
request.getHeaders().setIfModifiedSince(this.currentDate.toEpochMilli());
response.setStatusCode(HttpStatus.NOT_MODIFIED);
assertFalse(exchange.checkNotModified(this.currentDate));
assertEquals(304, response.getStatusCode().value());
assertEquals(-1, response.getHeaders().getLastModified());
}
@Test // SPR-14559
public void checkNotModifiedInvalidIfNoneMatchHeader() {
String eTag = "\"etagvalue\"";
request.getHeaders().setIfNoneMatch("missingquotes");
assertFalse(exchange.checkNotModified(eTag));
assertNull(response.getStatusCode());
assertEquals(eTag, response.getHeaders().getETag());
}
@Test
public void checkNotModifiedHeaderAlreadySet() {
request.getHeaders().setIfModifiedSince(currentDate.toEpochMilli());
response.getHeaders().add("Last-Modified", CURRENT_TIME);
assertTrue(exchange.checkNotModified(currentDate));
assertEquals(304, response.getStatusCode().value());
assertEquals(1, response.getHeaders().get("Last-Modified").size());
assertEquals(CURRENT_TIME, response.getHeaders().getFirst("Last-Modified"));
}
@Test
public void checkNotModifiedTimestamp() throws Exception {
request.getHeaders().setIfModifiedSince(currentDate.toEpochMilli());
assertTrue(exchange.checkNotModified(currentDate));
assertEquals(304, response.getStatusCode().value());
assertEquals(currentDate.toEpochMilli(), response.getHeaders().getLastModified());
}
@Test
public void checkModifiedTimestamp() {
Instant oneMinuteAgo = currentDate.minusSeconds(60);
request.getHeaders().setIfModifiedSince(oneMinuteAgo.toEpochMilli());
assertFalse(exchange.checkNotModified(currentDate));
assertNull(response.getStatusCode());
assertEquals(currentDate.toEpochMilli(), response.getHeaders().getLastModified());
}
@Test
public void checkNotModifiedETag() {
String eTag = "\"Foo\"";
request.getHeaders().setIfNoneMatch(eTag);
assertTrue(exchange.checkNotModified(eTag));
assertEquals(304, response.getStatusCode().value());
assertEquals(eTag, response.getHeaders().getETag());
}
@Test
public void checkNotModifiedETagWithSeparatorChars() {
String eTag = "\"Foo, Bar\"";
request.getHeaders().setIfNoneMatch(eTag);
assertTrue(exchange.checkNotModified(eTag));
assertEquals(304, response.getStatusCode().value());
assertEquals(eTag, response.getHeaders().getETag());
}
@Test
public void checkModifiedETag() {
String currentETag = "\"Foo\"";
String oldEtag = "Bar";
request.getHeaders().setIfNoneMatch(oldEtag);
assertFalse(exchange.checkNotModified(currentETag));
assertNull(response.getStatusCode());
assertEquals(currentETag, response.getHeaders().getETag());
}
@Test
public void checkNotModifiedUnpaddedETag() {
String eTag = "Foo";
String paddedEtag = String.format("\"%s\"", eTag);
request.getHeaders().setIfNoneMatch(paddedEtag);
assertTrue(exchange.checkNotModified(eTag));
assertEquals(304, response.getStatusCode().value());
assertEquals(paddedEtag, response.getHeaders().getETag());
}
@Test
public void checkModifiedUnpaddedETag() {
String currentETag = "Foo";
String oldEtag = "Bar";
request.getHeaders().setIfNoneMatch(oldEtag);
assertFalse(exchange.checkNotModified(currentETag));
assertNull(response.getStatusCode());
assertEquals(String.format("\"%s\"", currentETag), response.getHeaders().getETag());
}
@Test
public void checkNotModifiedWildcardIsIgnored() {
String eTag = "\"Foo\"";
request.getHeaders().setIfNoneMatch("*");
assertFalse(exchange.checkNotModified(eTag));
assertNull(response.getStatusCode());
assertEquals(eTag, response.getHeaders().getETag());
}
@Test
public void checkNotModifiedETagAndTimestamp() {
String eTag = "\"Foo\"";
request.getHeaders().setIfNoneMatch(eTag);
request.getHeaders().setIfModifiedSince(currentDate.toEpochMilli());
assertTrue(exchange.checkNotModified(eTag, currentDate));
assertEquals(304, response.getStatusCode().value());
assertEquals(eTag, response.getHeaders().getETag());
assertEquals(currentDate.toEpochMilli(), response.getHeaders().getLastModified());
}
// SPR-14224
@Test
public void checkNotModifiedETagAndModifiedTimestamp() {
String eTag = "\"Foo\"";
request.getHeaders().setIfNoneMatch(eTag);
Instant oneMinuteAgo = currentDate.minusSeconds(60);
request.getHeaders().setIfModifiedSince(oneMinuteAgo.toEpochMilli());
assertTrue(exchange.checkNotModified(eTag, currentDate));
assertEquals(304, response.getStatusCode().value());
assertEquals(eTag, response.getHeaders().getETag());
assertEquals(currentDate.toEpochMilli(), response.getHeaders().getLastModified());
}
@Test
public void checkModifiedETagAndNotModifiedTimestamp() throws Exception {
String currentETag = "\"Foo\"";
String oldEtag = "\"Bar\"";
request.getHeaders().setIfNoneMatch(oldEtag);
request.getHeaders().setIfModifiedSince(currentDate.toEpochMilli());
assertFalse(exchange.checkNotModified(currentETag, currentDate));
assertNull(response.getStatusCode());
assertEquals(currentETag, response.getHeaders().getETag());
assertEquals(currentDate.toEpochMilli(), response.getHeaders().getLastModified());
}
@Test
public void checkNotModifiedETagWeakStrong() {
String eTag = "\"Foo\"";
String weakEtag = String.format("W/%s", eTag);
request.getHeaders().setIfNoneMatch(eTag);
assertTrue(exchange.checkNotModified(weakEtag));
assertEquals(304, response.getStatusCode().value());
assertEquals(weakEtag, response.getHeaders().getETag());
}
@Test
public void checkNotModifiedETagStrongWeak() {
String eTag = "\"Foo\"";
request.getHeaders().setIfNoneMatch(String.format("W/%s", eTag));
assertTrue(exchange.checkNotModified(eTag));
assertEquals(304, response.getStatusCode().value());
assertEquals(eTag, response.getHeaders().getETag());
}
@Test
public void checkNotModifiedMultipleETags() {
String eTag = "\"Bar\"";
String multipleETags = String.format("\"Foo\", %s", eTag);
request.getHeaders().setIfNoneMatch(multipleETags);
assertTrue(exchange.checkNotModified(eTag));
assertEquals(304, response.getStatusCode().value());
assertEquals(eTag, response.getHeaders().getETag());
}
@Test
public void checkNotModifiedTimestampWithLengthPart() throws Exception {
long epochTime = dateFormat.parse(CURRENT_TIME).getTime();
request.setHttpMethod(HttpMethod.GET);
request.getHeaders().add("If-Modified-Since", "Wed, 09 Apr 2014 09:57:42 GMT; length=13774");
assertTrue(exchange.checkNotModified(Instant.ofEpochMilli(epochTime)));
assertEquals(304, response.getStatusCode().value());
assertEquals(epochTime, response.getHeaders().getLastModified());
}
@Test
public void checkModifiedTimestampWithLengthPart() throws Exception {
long epochTime = dateFormat.parse(CURRENT_TIME).getTime();
request.setHttpMethod(HttpMethod.GET);
request.getHeaders().add("If-Modified-Since", "Tue, 08 Apr 2014 09:57:42 GMT; length=13774");
assertFalse(exchange.checkNotModified(Instant.ofEpochMilli(epochTime)));
assertNull(response.getStatusCode());
assertEquals(epochTime, response.getHeaders().getLastModified());
}
@Test
public void checkNotModifiedTimestampConditionalPut() throws Exception {
Instant oneMinuteAgo = currentDate.minusSeconds(60);
request.setHttpMethod(HttpMethod.PUT);
request.getHeaders().setIfUnmodifiedSince(currentDate.toEpochMilli());
assertFalse(exchange.checkNotModified(oneMinuteAgo));
assertNull(response.getStatusCode());
assertEquals(-1, response.getHeaders().getLastModified());
}
@Test
public void checkNotModifiedTimestampConditionalPutConflict() throws Exception {
Instant oneMinuteAgo = currentDate.minusSeconds(60);
request.setHttpMethod(HttpMethod.PUT);
request.getHeaders().setIfUnmodifiedSince(oneMinuteAgo.toEpochMilli());
assertTrue(exchange.checkNotModified(currentDate));
assertEquals(412, response.getStatusCode().value());
assertEquals(-1, response.getHeaders().getLastModified());
}
}
Loading…
Cancel
Save