org.springframework.web.servlet.support.WebContentGenerator Maven / Gradle / Ivy
/*
* Copyright 2002-2018 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
*
* https://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.servlet.support;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.HttpSessionRequiredException;
import org.springframework.web.context.support.WebApplicationObjectSupport;
/**
* Convenient superclass for any kind of web content generator,
* like {@link org.springframework.web.servlet.mvc.AbstractController}
* and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}.
* Can also be used for custom handlers that have their own
* {@link org.springframework.web.servlet.HandlerAdapter}.
*
* Supports HTTP cache control options. The usage of corresponding HTTP
* headers can be controlled via the {@link #setCacheSeconds "cacheSeconds"}
* and {@link #setCacheControl "cacheControl"} properties.
*
*
NOTE: As of Spring 4.2, this generator's default behavior changed when
* using only {@link #setCacheSeconds}, sending HTTP response headers that are in line
* with current browsers and proxies implementations (i.e. no HTTP 1.0 headers anymore)
* Reverting to the previous behavior can be easily done by using one of the newly
* deprecated methods {@link #setUseExpiresHeader}, {@link #setUseCacheControlHeader},
* {@link #setUseCacheControlNoStore} or {@link #setAlwaysMustRevalidate}.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @author Brian Clozel
* @author Rossen Stoyanchev
* @see #setCacheSeconds
* @see #setCacheControl
* @see #setRequireSession
*/
public abstract class WebContentGenerator extends WebApplicationObjectSupport {
/** HTTP method "GET". */
public static final String METHOD_GET = "GET";
/** HTTP method "HEAD". */
public static final String METHOD_HEAD = "HEAD";
/** HTTP method "POST". */
public static final String METHOD_POST = "POST";
private static final String HEADER_PRAGMA = "Pragma";
private static final String HEADER_EXPIRES = "Expires";
protected static final String HEADER_CACHE_CONTROL = "Cache-Control";
/** Set of supported HTTP methods. */
@Nullable
private Set supportedMethods;
@Nullable
private String allowHeader;
private boolean requireSession = false;
@Nullable
private CacheControl cacheControl;
private int cacheSeconds = -1;
@Nullable
private String[] varyByRequestHeaders;
// deprecated fields
/** Use HTTP 1.0 expires header? */
private boolean useExpiresHeader = false;
/** Use HTTP 1.1 cache-control header? */
private boolean useCacheControlHeader = true;
/** Use HTTP 1.1 cache-control header value "no-store"? */
private boolean useCacheControlNoStore = true;
private boolean alwaysMustRevalidate = false;
/**
* Create a new WebContentGenerator which supports
* HTTP methods GET, HEAD and POST by default.
*/
public WebContentGenerator() {
this(true);
}
/**
* Create a new WebContentGenerator.
* @param restrictDefaultSupportedMethods {@code true} if this
* generator should support HTTP methods GET, HEAD and POST by default,
* or {@code false} if it should be unrestricted
*/
public WebContentGenerator(boolean restrictDefaultSupportedMethods) {
if (restrictDefaultSupportedMethods) {
this.supportedMethods = new LinkedHashSet<>(4);
this.supportedMethods.add(METHOD_GET);
this.supportedMethods.add(METHOD_HEAD);
this.supportedMethods.add(METHOD_POST);
}
initAllowHeader();
}
/**
* Create a new WebContentGenerator.
* @param supportedMethods the supported HTTP methods for this content generator
*/
public WebContentGenerator(String... supportedMethods) {
setSupportedMethods(supportedMethods);
}
/**
* Set the HTTP methods that this content generator should support.
* Default is GET, HEAD and POST for simple form controller types;
* unrestricted for general controllers and interceptors.
*/
public final void setSupportedMethods(@Nullable String... methods) {
if (!ObjectUtils.isEmpty(methods)) {
this.supportedMethods = new LinkedHashSet<>(Arrays.asList(methods));
}
else {
this.supportedMethods = null;
}
initAllowHeader();
}
/**
* Return the HTTP methods that this content generator supports.
*/
@Nullable
public final String[] getSupportedMethods() {
return (this.supportedMethods != null ? StringUtils.toStringArray(this.supportedMethods) : null);
}
private void initAllowHeader() {
Collection allowedMethods;
if (this.supportedMethods == null) {
allowedMethods = new ArrayList<>(HttpMethod.values().length - 1);
for (HttpMethod method : HttpMethod.values()) {
if (method != HttpMethod.TRACE) {
allowedMethods.add(method.name());
}
}
}
else if (this.supportedMethods.contains(HttpMethod.OPTIONS.name())) {
allowedMethods = this.supportedMethods;
}
else {
allowedMethods = new ArrayList<>(this.supportedMethods);
allowedMethods.add(HttpMethod.OPTIONS.name());
}
this.allowHeader = StringUtils.collectionToCommaDelimitedString(allowedMethods);
}
/**
* Return the "Allow" header value to use in response to an HTTP OPTIONS request
* based on the configured {@link #setSupportedMethods supported methods} also
* automatically adding "OPTIONS" to the list even if not present as a supported
* method. This means subclasses don't have to explicitly list "OPTIONS" as a
* supported method as long as HTTP OPTIONS requests are handled before making a
* call to {@link #checkRequest(HttpServletRequest)}.
* @since 4.3
*/
@Nullable
protected String getAllowHeader() {
return this.allowHeader;
}
/**
* Set whether a session should be required to handle requests.
*/
public final void setRequireSession(boolean requireSession) {
this.requireSession = requireSession;
}
/**
* Return whether a session is required to handle requests.
*/
public final boolean isRequireSession() {
return this.requireSession;
}
/**
* Set the {@link org.springframework.http.CacheControl} instance to build
* the Cache-Control HTTP response header.
* @since 4.2
*/
public final void setCacheControl(@Nullable CacheControl cacheControl) {
this.cacheControl = cacheControl;
}
/**
* Get the {@link org.springframework.http.CacheControl} instance
* that builds the Cache-Control HTTP response header.
* @since 4.2
*/
@Nullable
public final CacheControl getCacheControl() {
return this.cacheControl;
}
/**
* Cache content for the given number of seconds, by writing
* cache-related HTTP headers to the response:
*
* - seconds == -1 (default value): no generation cache-related headers
* - seconds == 0: "Cache-Control: no-store" will prevent caching
* - seconds > 0: "Cache-Control: max-age=seconds" will ask to cache content
*
* For more specific needs, a custom {@link org.springframework.http.CacheControl}
* should be used.
* @see #setCacheControl
*/
public final void setCacheSeconds(int seconds) {
this.cacheSeconds = seconds;
}
/**
* Return the number of seconds that content is cached.
*/
public final int getCacheSeconds() {
return this.cacheSeconds;
}
/**
* Configure one or more request header names (e.g. "Accept-Language") to
* add to the "Vary" response header to inform clients that the response is
* subject to content negotiation and variances based on the value of the
* given request headers. The configured request header names are added only
* if not already present in the response "Vary" header.
* @param varyByRequestHeaders one or more request header names
* @since 4.3
*/
public final void setVaryByRequestHeaders(@Nullable String... varyByRequestHeaders) {
this.varyByRequestHeaders = varyByRequestHeaders;
}
/**
* Return the configured request header names for the "Vary" response header.
* @since 4.3
*/
@Nullable
public final String[] getVaryByRequestHeaders() {
return this.varyByRequestHeaders;
}
/**
* Set whether to use the HTTP 1.0 expires header. Default is "false",
* as of 4.2.
*
Note: Cache headers will only get applied if caching is enabled
* (or explicitly prevented) for the current request.
* @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control
* header will be required, with the HTTP 1.0 headers disappearing
*/
@Deprecated
public final void setUseExpiresHeader(boolean useExpiresHeader) {
this.useExpiresHeader = useExpiresHeader;
}
/**
* Return whether the HTTP 1.0 expires header is used.
* @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
@Deprecated
public final boolean isUseExpiresHeader() {
return this.useExpiresHeader;
}
/**
* Set whether to use the HTTP 1.1 cache-control header. Default is "true".
*
Note: Cache headers will only get applied if caching is enabled
* (or explicitly prevented) for the current request.
* @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control
* header will be required, with the HTTP 1.0 headers disappearing
*/
@Deprecated
public final void setUseCacheControlHeader(boolean useCacheControlHeader) {
this.useCacheControlHeader = useCacheControlHeader;
}
/**
* Return whether the HTTP 1.1 cache-control header is used.
* @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
@Deprecated
public final boolean isUseCacheControlHeader() {
return this.useCacheControlHeader;
}
/**
* Set whether to use the HTTP 1.1 cache-control header value "no-store"
* when preventing caching. Default is "true".
* @deprecated as of 4.2, in favor of {@link #setCacheControl}
*/
@Deprecated
public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) {
this.useCacheControlNoStore = useCacheControlNoStore;
}
/**
* Return whether the HTTP 1.1 cache-control header value "no-store" is used.
* @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
@Deprecated
public final boolean isUseCacheControlNoStore() {
return this.useCacheControlNoStore;
}
/**
* An option to add 'must-revalidate' to every Cache-Control header.
* This may be useful with annotated controller methods, which can
* programmatically do a last-modified calculation as described in
* {@link org.springframework.web.context.request.WebRequest#checkNotModified(long)}.
*
Default is "false".
* @deprecated as of 4.2, in favor of {@link #setCacheControl}
*/
@Deprecated
public final void setAlwaysMustRevalidate(boolean mustRevalidate) {
this.alwaysMustRevalidate = mustRevalidate;
}
/**
* Return whether 'must-revalidate' is added to every Cache-Control header.
* @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
@Deprecated
public final boolean isAlwaysMustRevalidate() {
return this.alwaysMustRevalidate;
}
/**
* Check the given request for supported methods and a required session, if any.
* @param request current HTTP request
* @throws ServletException if the request cannot be handled because a check failed
* @since 4.2
*/
protected final void checkRequest(HttpServletRequest request) throws ServletException {
// Check whether we should support the request method.
String method = request.getMethod();
if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
throw new HttpRequestMethodNotSupportedException(method, this.supportedMethods);
}
// Check whether a session is required.
if (this.requireSession && request.getSession(false) == null) {
throw new HttpSessionRequiredException("Pre-existing session required but none found");
}
}
/**
* Prepare the given response according to the settings of this generator.
* Applies the number of cache seconds specified for this generator.
* @param response current HTTP response
* @since 4.2
*/
protected final void prepareResponse(HttpServletResponse response) {
if (this.cacheControl != null) {
applyCacheControl(response, this.cacheControl);
}
else {
applyCacheSeconds(response, this.cacheSeconds);
}
if (this.varyByRequestHeaders != null) {
for (String value : getVaryRequestHeadersToAdd(response, this.varyByRequestHeaders)) {
response.addHeader("Vary", value);
}
}
}
/**
* Set the HTTP Cache-Control header according to the given settings.
* @param response current HTTP response
* @param cacheControl the pre-configured cache control settings
* @since 4.2
*/
protected final void applyCacheControl(HttpServletResponse response, CacheControl cacheControl) {
String ccValue = cacheControl.getHeaderValue();
if (ccValue != null) {
// Set computed HTTP 1.1 Cache-Control header
response.setHeader(HEADER_CACHE_CONTROL, ccValue);
if (response.containsHeader(HEADER_PRAGMA)) {
// Reset HTTP 1.0 Pragma header if present
response.setHeader(HEADER_PRAGMA, "");
}
if (response.containsHeader(HEADER_EXPIRES)) {
// Reset HTTP 1.0 Expires header if present
response.setHeader(HEADER_EXPIRES, "");
}
}
}
/**
* Apply the given cache seconds and generate corresponding HTTP headers,
* i.e. allow caching for the given number of seconds in case of a positive
* value, prevent caching if given a 0 value, do nothing else.
* Does not tell the browser to revalidate the resource.
* @param response current HTTP response
* @param cacheSeconds positive number of seconds into the future that the
* response should be cacheable for, 0 to prevent caching
*/
@SuppressWarnings("deprecation")
protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) {
if (this.useExpiresHeader || !this.useCacheControlHeader) {
// Deprecated HTTP 1.0 cache behavior, as in previous Spring versions
if (cacheSeconds > 0) {
cacheForSeconds(response, cacheSeconds);
}
else if (cacheSeconds == 0) {
preventCaching(response);
}
}
else {
CacheControl cControl;
if (cacheSeconds > 0) {
cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS);
if (this.alwaysMustRevalidate) {
cControl = cControl.mustRevalidate();
}
}
else if (cacheSeconds == 0) {
cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache());
}
else {
cControl = CacheControl.empty();
}
applyCacheControl(response, cControl);
}
}
/**
* Check and prepare the given request and response according to the settings
* of this generator.
* @see #checkRequest(HttpServletRequest)
* @see #prepareResponse(HttpServletResponse)
* @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored,
* with a must-revalidate header only generated if explicitly configured
*/
@Deprecated
protected final void checkAndPrepare(
HttpServletRequest request, HttpServletResponse response, boolean lastModified) throws ServletException {
checkRequest(request);
prepareResponse(response);
}
/**
* Check and prepare the given request and response according to the settings
* of this generator.
* @see #checkRequest(HttpServletRequest)
* @see #applyCacheSeconds(HttpServletResponse, int)
* @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored,
* with a must-revalidate header only generated if explicitly configured
*/
@Deprecated
protected final void checkAndPrepare(
HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
throws ServletException {
checkRequest(request);
applyCacheSeconds(response, cacheSeconds);
}
/**
* Apply the given cache seconds and generate respective HTTP headers.
*
That is, allow caching for the given number of seconds in the
* case of a positive value, prevent caching if given a 0 value, else
* do nothing (i.e. leave caching to the client).
* @param response the current HTTP response
* @param cacheSeconds the (positive) number of seconds into the future
* that the response should be cacheable for; 0 to prevent caching; and
* a negative value to leave caching to the client.
* @param mustRevalidate whether the client should revalidate the resource
* (typically only necessary for controllers with last-modified support)
* @deprecated as of 4.2, in favor of {@link #applyCacheControl}
*/
@Deprecated
protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds, boolean mustRevalidate) {
if (cacheSeconds > 0) {
cacheForSeconds(response, cacheSeconds, mustRevalidate);
}
else if (cacheSeconds == 0) {
preventCaching(response);
}
}
/**
* Set HTTP headers to allow caching for the given number of seconds.
* Does not tell the browser to revalidate the resource.
* @param response current HTTP response
* @param seconds number of seconds into the future that the response
* should be cacheable for
* @deprecated as of 4.2, in favor of {@link #applyCacheControl}
*/
@Deprecated
protected final void cacheForSeconds(HttpServletResponse response, int seconds) {
cacheForSeconds(response, seconds, false);
}
/**
* Set HTTP headers to allow caching for the given number of seconds.
* Tells the browser to revalidate the resource if mustRevalidate is
* {@code true}.
* @param response the current HTTP response
* @param seconds number of seconds into the future that the response
* should be cacheable for
* @param mustRevalidate whether the client should revalidate the resource
* (typically only necessary for controllers with last-modified support)
* @deprecated as of 4.2, in favor of {@link #applyCacheControl}
*/
@Deprecated
protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
if (this.useExpiresHeader) {
// HTTP 1.0 header
response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L);
}
else if (response.containsHeader(HEADER_EXPIRES)) {
// Reset HTTP 1.0 Expires header if present
response.setHeader(HEADER_EXPIRES, "");
}
if (this.useCacheControlHeader) {
// HTTP 1.1 header
String headerValue = "max-age=" + seconds;
if (mustRevalidate || this.alwaysMustRevalidate) {
headerValue += ", must-revalidate";
}
response.setHeader(HEADER_CACHE_CONTROL, headerValue);
}
if (response.containsHeader(HEADER_PRAGMA)) {
// Reset HTTP 1.0 Pragma header if present
response.setHeader(HEADER_PRAGMA, "");
}
}
/**
* Prevent the response from being cached.
* Only called in HTTP 1.0 compatibility mode.
*
See {@code https://www.mnot.net/cache_docs}.
* @deprecated as of 4.2, in favor of {@link #applyCacheControl}
*/
@Deprecated
protected final void preventCaching(HttpServletResponse response) {
response.setHeader(HEADER_PRAGMA, "no-cache");
if (this.useExpiresHeader) {
// HTTP 1.0 Expires header
response.setDateHeader(HEADER_EXPIRES, 1L);
}
if (this.useCacheControlHeader) {
// HTTP 1.1 Cache-Control header: "no-cache" is the standard value,
// "no-store" is necessary to prevent caching on Firefox.
response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
if (this.useCacheControlNoStore) {
response.addHeader(HEADER_CACHE_CONTROL, "no-store");
}
}
}
private Collection getVaryRequestHeadersToAdd(HttpServletResponse response, String[] varyByRequestHeaders) {
if (!response.containsHeader(HttpHeaders.VARY)) {
return Arrays.asList(varyByRequestHeaders);
}
Collection result = new ArrayList<>(varyByRequestHeaders.length);
Collections.addAll(result, varyByRequestHeaders);
for (String header : response.getHeaders(HttpHeaders.VARY)) {
for (String existing : StringUtils.tokenizeToStringArray(header, ",")) {
if ("*".equals(existing)) {
return Collections.emptyList();
}
for (String value : varyByRequestHeaders) {
if (value.equalsIgnoreCase(existing)) {
result.remove(value);
}
}
}
}
return result;
}
}