Add ServerRequest::checkNotModified

This commit adds the checkNotModified method to ServerRequest in both
WebFlux.fn and WebMvc.fn. Unlike other checkNotModified methods found
in the framework, this method does not return a boolean, but rather
a response wrapped in a Mono/Optional. If the resource has
not been changed, the not-modified response can be returned directly;
if the resource has changed, the user can create a corresponding
response using switchIfEmpty/orElse(Get).

Closes gh-24173
This commit is contained in:
Arjen Poutsma
2020-03-11 12:05:07 +01:00
parent 73e39726cd
commit 0dc1c7eb8b
7 changed files with 940 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -17,15 +17,18 @@
package org.springframework.web.servlet.function;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.Principal;
import java.time.Instant;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -36,8 +39,10 @@ import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.core.ParameterizedTypeReference;
@@ -47,11 +52,14 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriBuilder;
@@ -246,6 +254,26 @@ class DefaultServerRequest implements ServerRequest {
return Optional.ofNullable(this.serverHttpRequest.getPrincipal());
}
static Optional<ServerResponse> checkNotModified(HttpServletRequest servletRequest, @Nullable Instant lastModified,
@Nullable String etag) {
long lastModifiedTimestamp = -1;
if (lastModified != null && lastModified.isAfter(Instant.EPOCH)) {
lastModifiedTimestamp = lastModified.toEpochMilli();
}
CheckNotModifiedResponse response = new CheckNotModifiedResponse();
WebRequest webRequest = new ServletWebRequest(servletRequest, response);
if (webRequest.checkNotModified(etag, lastModifiedTimestamp)) {
return Optional.of(ServerResponse.status(response.status).
headers(headers -> headers.addAll(response.headers))
.build());
}
else {
return Optional.empty();
}
}
/**
* Default implementation of {@link Headers}.
*/
@@ -419,4 +447,207 @@ class DefaultServerRequest implements ServerRequest {
}
/**
* Simple implementation of {@link HttpServletResponse} used by
* {@link #checkNotModified(HttpServletRequest, Instant, String)} to record status and headers set by
* {@link ServletWebRequest#checkNotModified(String, long)}. Throws an {@code UnsupportedOperationException}
* for other methods.
*/
@SuppressWarnings("deprecation")
private static final class CheckNotModifiedResponse implements HttpServletResponse {
private final HttpHeaders headers = new HttpHeaders();
private int status = 200;
@Override
public boolean containsHeader(String name) {
return this.headers.containsKey(name);
}
@Override
public void setDateHeader(String name, long date) {
this.headers.setDate(name, date);
}
@Override
public void setHeader(String name, String value) {
this.headers.set(name, value);
}
@Override
public void addHeader(String name, String value) {
this.headers.add(name, value);
}
@Override
public void setStatus(int sc) {
this.status = sc;
}
@Override
public void setStatus(int sc, String sm) {
this.status = sc;
}
@Override
public int getStatus() {
return this.status;
}
@Override
public String getHeader(String name) {
return this.headers.getFirst(name);
}
@Override
public Collection<String> getHeaders(String name) {
List<String> result = this.headers.get(name);
return result != null ? result : Collections.emptyList();
}
@Override
public Collection<String> getHeaderNames() {
return this.headers.keySet();
}
// Unsupported
@Override
public void addCookie(Cookie cookie) {
throw new UnsupportedOperationException();
}
@Override
public String encodeURL(String url) {
throw new UnsupportedOperationException();
}
@Override
public String encodeRedirectURL(String url) {
throw new UnsupportedOperationException();
}
@Override
public String encodeUrl(String url) {
throw new UnsupportedOperationException();
}
@Override
public String encodeRedirectUrl(String url) {
throw new UnsupportedOperationException();
}
@Override
public void sendError(int sc, String msg) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void sendError(int sc) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void sendRedirect(String location) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void addDateHeader(String name, long date) {
throw new UnsupportedOperationException();
}
@Override
public void setIntHeader(String name, int value) {
throw new UnsupportedOperationException();
}
@Override
public void addIntHeader(String name, int value) {
throw new UnsupportedOperationException();
}
@Override
public String getCharacterEncoding() {
throw new UnsupportedOperationException();
}
@Override
public String getContentType() {
throw new UnsupportedOperationException();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public PrintWriter getWriter() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void setCharacterEncoding(String charset) {
throw new UnsupportedOperationException();
}
@Override
public void setContentLength(int len) {
throw new UnsupportedOperationException();
}
@Override
public void setContentLengthLong(long len) {
throw new UnsupportedOperationException();
}
@Override
public void setContentType(String type) {
throw new UnsupportedOperationException();
}
@Override
public void setBufferSize(int size) {
throw new UnsupportedOperationException();
}
@Override
public int getBufferSize() {
throw new UnsupportedOperationException();
}
@Override
public void flushBuffer() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void resetBuffer() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCommitted() {
throw new UnsupportedOperationException();
}
@Override
public void reset() {
throw new UnsupportedOperationException();
}
@Override
public void setLocale(Locale loc) {
throw new UnsupportedOperationException();
}
@Override
public Locale getLocale() {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -20,6 +20,7 @@ import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.security.Principal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -1017,6 +1018,21 @@ public abstract class RequestPredicates {
return this.request.servletRequest();
}
@Override
public Optional<ServerResponse> checkNotModified(Instant lastModified) {
return this.request.checkNotModified(lastModified);
}
@Override
public Optional<ServerResponse> checkNotModified(String etag) {
return this.request.checkNotModified(etag);
}
@Override
public Optional<ServerResponse> checkNotModified(Instant lastModified, String etag) {
return this.request.checkNotModified(lastModified, etag);
}
@Override
public String toString() {
return method() + " " + path();

View File

@@ -21,6 +21,7 @@ import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.Principal;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -42,6 +43,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.PathContainer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriBuilder;
@@ -224,6 +226,109 @@ public interface ServerRequest {
*/
HttpServletRequest servletRequest();
/**
* Check whether the requested resource has been modified given the
* supplied last-modified timestamp (as determined by the application).
* If not modified, this method returns a response with corresponding
* status code and headers, otherwise an empty result.
* <p>Typical usage:
* <pre class="code">
* public ServerResponse myHandleMethod(ServerRequest request) {
* Instant lastModified = // application-specific calculation
* return request.checkNotModified(lastModified)
* .orElseGet(() -> {
* // further request processing, actually building content
* return ServerResponse.ok().body(...);
* });
* }</pre>
* <p>This method works with conditional GET/HEAD requests, but
* also with conditional POST/PUT/DELETE requests.
* <p><strong>Note:</strong> you can use either
* this {@code #checkNotModified(Instant)} method; or
* {@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(Instant, String)}.
* @param lastModified the last-modified timestamp that the
* application determined for the underlying resource
* @return a corresponding response if the request qualifies as not
* modified, or an empty result otherwise.
* @since 5.2.5
*/
default Optional<ServerResponse> checkNotModified(Instant lastModified) {
Assert.notNull(lastModified, "LastModified must not be null");
return DefaultServerRequest.checkNotModified(servletRequest(), lastModified, null);
}
/**
* Check whether the requested resource has been modified given the
* supplied {@code ETag} (entity tag), as determined by the application.
* If not modified, this method returns a response with corresponding
* status code and headers, otherwise an empty result.
* <p>Typical usage:
* <pre class="code">
* public ServerResponse myHandleMethod(ServerRequest request) {
* String eTag = // application-specific calculation
* return request.checkNotModified(eTag)
* .orElseGet(() -> {
* // further request processing, actually building content
* return ServerResponse.ok().body(...);
* });
* }</pre>
* <p>This method works with conditional GET/HEAD requests, but
* also with conditional POST/PUT/DELETE requests.
* <p><strong>Note:</strong> you can use either
* this {@link #checkNotModified(Instant)} method; or
* {@code #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(Instant, String)}.
* @param etag the entity tag that the application determined
* for the underlying resource. This parameter will be padded
* with quotes (") if necessary.
* @return a corresponding response if the request qualifies as not
* modified, or an empty result otherwise.
* @since 5.2.5
*/
default Optional<ServerResponse> checkNotModified(String etag) {
Assert.notNull(etag, "Etag must not be null");
return DefaultServerRequest.checkNotModified(servletRequest(), null, 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.
* If not modified, this method returns a response with corresponding
* status code and headers, otherwise an empty result.
* <p>Typical usage:
* <pre class="code">
* public ServerResponse myHandleMethod(ServerRequest request) {
* Instant lastModified = // application-specific calculation
* String eTag = // application-specific calculation
* return request.checkNotModified(lastModified, eTag)
* .orElseGet(() -> {
* // further request processing, actually building content
* return ServerResponse.ok().body(...);
* });
* }</pre>
* <p>This method works with conditional GET/HEAD requests, but
* also with conditional POST/PUT/DELETE requests.
* @param lastModified the last-modified timestamp that the
* application determined for the underlying resource
* @param etag the entity tag that the application determined
* for the underlying resource. This parameter will be padded
* with quotes (") if necessary.
* @return a corresponding response if the request qualifies as not
* modified, or an empty result otherwise.
* @since 5.2.5
*/
default Optional<ServerResponse> checkNotModified(Instant lastModified, String etag) {
Assert.notNull(lastModified, "LastModified must not be null");
Assert.notNull(etag, "Etag must not be null");
return DefaultServerRequest.checkNotModified(servletRequest(), lastModified, etag);
}
// Static methods
@@ -248,6 +353,7 @@ public interface ServerRequest {
}
/**
* Represents the headers of the HTTP request.
* @see ServerRequest#headers()