001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.servlet.support;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.LinkedHashSet;
024import java.util.Set;
025import java.util.concurrent.TimeUnit;
026
027import javax.servlet.ServletException;
028import javax.servlet.http.HttpServletRequest;
029import javax.servlet.http.HttpServletResponse;
030
031import org.springframework.http.CacheControl;
032import org.springframework.http.HttpHeaders;
033import org.springframework.http.HttpMethod;
034import org.springframework.lang.Nullable;
035import org.springframework.util.ObjectUtils;
036import org.springframework.util.StringUtils;
037import org.springframework.web.HttpRequestMethodNotSupportedException;
038import org.springframework.web.HttpSessionRequiredException;
039import org.springframework.web.context.support.WebApplicationObjectSupport;
040
041/**
042 * Convenient superclass for any kind of web content generator,
043 * like {@link org.springframework.web.servlet.mvc.AbstractController}
044 * and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}.
045 * Can also be used for custom handlers that have their own
046 * {@link org.springframework.web.servlet.HandlerAdapter}.
047 *
048 * <p>Supports HTTP cache control options. The usage of corresponding HTTP
049 * headers can be controlled via the {@link #setCacheSeconds "cacheSeconds"}
050 * and {@link #setCacheControl "cacheControl"} properties.
051 *
052 * <p><b>NOTE:</b> As of Spring 4.2, this generator's default behavior changed when
053 * using only {@link #setCacheSeconds}, sending HTTP response headers that are in line
054 * with current browsers and proxies implementations (i.e. no HTTP 1.0 headers anymore)
055 * Reverting to the previous behavior can be easily done by using one of the newly
056 * deprecated methods {@link #setUseExpiresHeader}, {@link #setUseCacheControlHeader},
057 * {@link #setUseCacheControlNoStore} or {@link #setAlwaysMustRevalidate}.
058 *
059 * @author Rod Johnson
060 * @author Juergen Hoeller
061 * @author Brian Clozel
062 * @author Rossen Stoyanchev
063 * @see #setCacheSeconds
064 * @see #setCacheControl
065 * @see #setRequireSession
066 */
067public abstract class WebContentGenerator extends WebApplicationObjectSupport {
068
069        /** HTTP method "GET". */
070        public static final String METHOD_GET = "GET";
071
072        /** HTTP method "HEAD". */
073        public static final String METHOD_HEAD = "HEAD";
074
075        /** HTTP method "POST". */
076        public static final String METHOD_POST = "POST";
077
078        private static final String HEADER_PRAGMA = "Pragma";
079
080        private static final String HEADER_EXPIRES = "Expires";
081
082        protected static final String HEADER_CACHE_CONTROL = "Cache-Control";
083
084
085        /** Set of supported HTTP methods. */
086        @Nullable
087        private Set<String> supportedMethods;
088
089        @Nullable
090        private String allowHeader;
091
092        private boolean requireSession = false;
093
094        @Nullable
095        private CacheControl cacheControl;
096
097        private int cacheSeconds = -1;
098
099        @Nullable
100        private String[] varyByRequestHeaders;
101
102
103        // deprecated fields
104
105        /** Use HTTP 1.0 expires header? */
106        private boolean useExpiresHeader = false;
107
108        /** Use HTTP 1.1 cache-control header? */
109        private boolean useCacheControlHeader = true;
110
111        /** Use HTTP 1.1 cache-control header value "no-store"? */
112        private boolean useCacheControlNoStore = true;
113
114        private boolean alwaysMustRevalidate = false;
115
116
117        /**
118         * Create a new WebContentGenerator which supports
119         * HTTP methods GET, HEAD and POST by default.
120         */
121        public WebContentGenerator() {
122                this(true);
123        }
124
125        /**
126         * Create a new WebContentGenerator.
127         * @param restrictDefaultSupportedMethods {@code true} if this
128         * generator should support HTTP methods GET, HEAD and POST by default,
129         * or {@code false} if it should be unrestricted
130         */
131        public WebContentGenerator(boolean restrictDefaultSupportedMethods) {
132                if (restrictDefaultSupportedMethods) {
133                        this.supportedMethods = new LinkedHashSet<>(4);
134                        this.supportedMethods.add(METHOD_GET);
135                        this.supportedMethods.add(METHOD_HEAD);
136                        this.supportedMethods.add(METHOD_POST);
137                }
138                initAllowHeader();
139        }
140
141        /**
142         * Create a new WebContentGenerator.
143         * @param supportedMethods the supported HTTP methods for this content generator
144         */
145        public WebContentGenerator(String... supportedMethods) {
146                setSupportedMethods(supportedMethods);
147        }
148
149
150        /**
151         * Set the HTTP methods that this content generator should support.
152         * <p>Default is GET, HEAD and POST for simple form controller types;
153         * unrestricted for general controllers and interceptors.
154         */
155        public final void setSupportedMethods(@Nullable String... methods) {
156                if (!ObjectUtils.isEmpty(methods)) {
157                        this.supportedMethods = new LinkedHashSet<>(Arrays.asList(methods));
158                }
159                else {
160                        this.supportedMethods = null;
161                }
162                initAllowHeader();
163        }
164
165        /**
166         * Return the HTTP methods that this content generator supports.
167         */
168        @Nullable
169        public final String[] getSupportedMethods() {
170                return (this.supportedMethods != null ? StringUtils.toStringArray(this.supportedMethods) : null);
171        }
172
173        private void initAllowHeader() {
174                Collection<String> allowedMethods;
175                if (this.supportedMethods == null) {
176                        allowedMethods = new ArrayList<>(HttpMethod.values().length - 1);
177                        for (HttpMethod method : HttpMethod.values()) {
178                                if (method != HttpMethod.TRACE) {
179                                        allowedMethods.add(method.name());
180                                }
181                        }
182                }
183                else if (this.supportedMethods.contains(HttpMethod.OPTIONS.name())) {
184                        allowedMethods = this.supportedMethods;
185                }
186                else {
187                        allowedMethods = new ArrayList<>(this.supportedMethods);
188                        allowedMethods.add(HttpMethod.OPTIONS.name());
189
190                }
191                this.allowHeader = StringUtils.collectionToCommaDelimitedString(allowedMethods);
192        }
193
194        /**
195         * Return the "Allow" header value to use in response to an HTTP OPTIONS request
196         * based on the configured {@link #setSupportedMethods supported methods} also
197         * automatically adding "OPTIONS" to the list even if not present as a supported
198         * method. This means subclasses don't have to explicitly list "OPTIONS" as a
199         * supported method as long as HTTP OPTIONS requests are handled before making a
200         * call to {@link #checkRequest(HttpServletRequest)}.
201         * @since 4.3
202         */
203        @Nullable
204        protected String getAllowHeader() {
205                return this.allowHeader;
206        }
207
208        /**
209         * Set whether a session should be required to handle requests.
210         */
211        public final void setRequireSession(boolean requireSession) {
212                this.requireSession = requireSession;
213        }
214
215        /**
216         * Return whether a session is required to handle requests.
217         */
218        public final boolean isRequireSession() {
219                return this.requireSession;
220        }
221
222        /**
223         * Set the {@link org.springframework.http.CacheControl} instance to build
224         * the Cache-Control HTTP response header.
225         * @since 4.2
226         */
227        public final void setCacheControl(@Nullable CacheControl cacheControl) {
228                this.cacheControl = cacheControl;
229        }
230
231        /**
232         * Get the {@link org.springframework.http.CacheControl} instance
233         * that builds the Cache-Control HTTP response header.
234         * @since 4.2
235         */
236        @Nullable
237        public final CacheControl getCacheControl() {
238                return this.cacheControl;
239        }
240
241        /**
242         * Cache content for the given number of seconds, by writing
243         * cache-related HTTP headers to the response:
244         * <ul>
245         * <li>seconds == -1 (default value): no generation cache-related headers</li>
246         * <li>seconds == 0: "Cache-Control: no-store" will prevent caching</li>
247         * <li>seconds > 0: "Cache-Control: max-age=seconds" will ask to cache content</li>
248         * </ul>
249         * <p>For more specific needs, a custom {@link org.springframework.http.CacheControl}
250         * should be used.
251         * @see #setCacheControl
252         */
253        public final void setCacheSeconds(int seconds) {
254                this.cacheSeconds = seconds;
255        }
256
257        /**
258         * Return the number of seconds that content is cached.
259         */
260        public final int getCacheSeconds() {
261                return this.cacheSeconds;
262        }
263
264        /**
265         * Configure one or more request header names (e.g. "Accept-Language") to
266         * add to the "Vary" response header to inform clients that the response is
267         * subject to content negotiation and variances based on the value of the
268         * given request headers. The configured request header names are added only
269         * if not already present in the response "Vary" header.
270         * @param varyByRequestHeaders one or more request header names
271         * @since 4.3
272         */
273        public final void setVaryByRequestHeaders(@Nullable String... varyByRequestHeaders) {
274                this.varyByRequestHeaders = varyByRequestHeaders;
275        }
276
277        /**
278         * Return the configured request header names for the "Vary" response header.
279         * @since 4.3
280         */
281        @Nullable
282        public final String[] getVaryByRequestHeaders() {
283                return this.varyByRequestHeaders;
284        }
285
286        /**
287         * Set whether to use the HTTP 1.0 expires header. Default is "false",
288         * as of 4.2.
289         * <p>Note: Cache headers will only get applied if caching is enabled
290         * (or explicitly prevented) for the current request.
291         * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control
292         * header will be required, with the HTTP 1.0 headers disappearing
293         */
294        @Deprecated
295        public final void setUseExpiresHeader(boolean useExpiresHeader) {
296                this.useExpiresHeader = useExpiresHeader;
297        }
298
299        /**
300         * Return whether the HTTP 1.0 expires header is used.
301         * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
302         */
303        @Deprecated
304        public final boolean isUseExpiresHeader() {
305                return this.useExpiresHeader;
306        }
307
308        /**
309         * Set whether to use the HTTP 1.1 cache-control header. Default is "true".
310         * <p>Note: Cache headers will only get applied if caching is enabled
311         * (or explicitly prevented) for the current request.
312         * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control
313         * header will be required, with the HTTP 1.0 headers disappearing
314         */
315        @Deprecated
316        public final void setUseCacheControlHeader(boolean useCacheControlHeader) {
317                this.useCacheControlHeader = useCacheControlHeader;
318        }
319
320        /**
321         * Return whether the HTTP 1.1 cache-control header is used.
322         * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
323         */
324        @Deprecated
325        public final boolean isUseCacheControlHeader() {
326                return this.useCacheControlHeader;
327        }
328
329        /**
330         * Set whether to use the HTTP 1.1 cache-control header value "no-store"
331         * when preventing caching. Default is "true".
332         * @deprecated as of 4.2, in favor of {@link #setCacheControl}
333         */
334        @Deprecated
335        public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) {
336                this.useCacheControlNoStore = useCacheControlNoStore;
337        }
338
339        /**
340         * Return whether the HTTP 1.1 cache-control header value "no-store" is used.
341         * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
342         */
343        @Deprecated
344        public final boolean isUseCacheControlNoStore() {
345                return this.useCacheControlNoStore;
346        }
347
348        /**
349         * An option to add 'must-revalidate' to every Cache-Control header.
350         * This may be useful with annotated controller methods, which can
351         * programmatically do a last-modified calculation as described in
352         * {@link org.springframework.web.context.request.WebRequest#checkNotModified(long)}.
353         * <p>Default is "false".
354         * @deprecated as of 4.2, in favor of {@link #setCacheControl}
355         */
356        @Deprecated
357        public final void setAlwaysMustRevalidate(boolean mustRevalidate) {
358                this.alwaysMustRevalidate = mustRevalidate;
359        }
360
361        /**
362         * Return whether 'must-revalidate' is added to every Cache-Control header.
363         * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
364         */
365        @Deprecated
366        public final boolean isAlwaysMustRevalidate() {
367                return this.alwaysMustRevalidate;
368        }
369
370
371        /**
372         * Check the given request for supported methods and a required session, if any.
373         * @param request current HTTP request
374         * @throws ServletException if the request cannot be handled because a check failed
375         * @since 4.2
376         */
377        protected final void checkRequest(HttpServletRequest request) throws ServletException {
378                // Check whether we should support the request method.
379                String method = request.getMethod();
380                if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
381                        throw new HttpRequestMethodNotSupportedException(method, this.supportedMethods);
382                }
383
384                // Check whether a session is required.
385                if (this.requireSession && request.getSession(false) == null) {
386                        throw new HttpSessionRequiredException("Pre-existing session required but none found");
387                }
388        }
389
390        /**
391         * Prepare the given response according to the settings of this generator.
392         * Applies the number of cache seconds specified for this generator.
393         * @param response current HTTP response
394         * @since 4.2
395         */
396        protected final void prepareResponse(HttpServletResponse response) {
397                if (this.cacheControl != null) {
398                        applyCacheControl(response, this.cacheControl);
399                }
400                else {
401                        applyCacheSeconds(response, this.cacheSeconds);
402                }
403                if (this.varyByRequestHeaders != null) {
404                        for (String value : getVaryRequestHeadersToAdd(response, this.varyByRequestHeaders)) {
405                                response.addHeader("Vary", value);
406                        }
407                }
408        }
409
410        /**
411         * Set the HTTP Cache-Control header according to the given settings.
412         * @param response current HTTP response
413         * @param cacheControl the pre-configured cache control settings
414         * @since 4.2
415         */
416        protected final void applyCacheControl(HttpServletResponse response, CacheControl cacheControl) {
417                String ccValue = cacheControl.getHeaderValue();
418                if (ccValue != null) {
419                        // Set computed HTTP 1.1 Cache-Control header
420                        response.setHeader(HEADER_CACHE_CONTROL, ccValue);
421
422                        if (response.containsHeader(HEADER_PRAGMA)) {
423                                // Reset HTTP 1.0 Pragma header if present
424                                response.setHeader(HEADER_PRAGMA, "");
425                        }
426                        if (response.containsHeader(HEADER_EXPIRES)) {
427                                // Reset HTTP 1.0 Expires header if present
428                                response.setHeader(HEADER_EXPIRES, "");
429                        }
430                }
431        }
432
433        /**
434         * Apply the given cache seconds and generate corresponding HTTP headers,
435         * i.e. allow caching for the given number of seconds in case of a positive
436         * value, prevent caching if given a 0 value, do nothing else.
437         * Does not tell the browser to revalidate the resource.
438         * @param response current HTTP response
439         * @param cacheSeconds positive number of seconds into the future that the
440         * response should be cacheable for, 0 to prevent caching
441         */
442        @SuppressWarnings("deprecation")
443        protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) {
444                if (this.useExpiresHeader || !this.useCacheControlHeader) {
445                        // Deprecated HTTP 1.0 cache behavior, as in previous Spring versions
446                        if (cacheSeconds > 0) {
447                                cacheForSeconds(response, cacheSeconds);
448                        }
449                        else if (cacheSeconds == 0) {
450                                preventCaching(response);
451                        }
452                }
453                else {
454                        CacheControl cControl;
455                        if (cacheSeconds > 0) {
456                                cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS);
457                                if (this.alwaysMustRevalidate) {
458                                        cControl = cControl.mustRevalidate();
459                                }
460                        }
461                        else if (cacheSeconds == 0) {
462                                cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache());
463                        }
464                        else {
465                                cControl = CacheControl.empty();
466                        }
467                        applyCacheControl(response, cControl);
468                }
469        }
470
471
472        /**
473         * Check and prepare the given request and response according to the settings
474         * of this generator.
475         * @see #checkRequest(HttpServletRequest)
476         * @see #prepareResponse(HttpServletResponse)
477         * @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored,
478         * with a must-revalidate header only generated if explicitly configured
479         */
480        @Deprecated
481        protected final void checkAndPrepare(
482                        HttpServletRequest request, HttpServletResponse response, boolean lastModified) throws ServletException {
483
484                checkRequest(request);
485                prepareResponse(response);
486        }
487
488        /**
489         * Check and prepare the given request and response according to the settings
490         * of this generator.
491         * @see #checkRequest(HttpServletRequest)
492         * @see #applyCacheSeconds(HttpServletResponse, int)
493         * @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored,
494         * with a must-revalidate header only generated if explicitly configured
495         */
496        @Deprecated
497        protected final void checkAndPrepare(
498                        HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
499                        throws ServletException {
500
501                checkRequest(request);
502                applyCacheSeconds(response, cacheSeconds);
503        }
504
505        /**
506         * Apply the given cache seconds and generate respective HTTP headers.
507         * <p>That is, allow caching for the given number of seconds in the
508         * case of a positive value, prevent caching if given a 0 value, else
509         * do nothing (i.e. leave caching to the client).
510         * @param response the current HTTP response
511         * @param cacheSeconds the (positive) number of seconds into the future
512         * that the response should be cacheable for; 0 to prevent caching; and
513         * a negative value to leave caching to the client.
514         * @param mustRevalidate whether the client should revalidate the resource
515         * (typically only necessary for controllers with last-modified support)
516         * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
517         */
518        @Deprecated
519        protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds, boolean mustRevalidate) {
520                if (cacheSeconds > 0) {
521                        cacheForSeconds(response, cacheSeconds, mustRevalidate);
522                }
523                else if (cacheSeconds == 0) {
524                        preventCaching(response);
525                }
526        }
527
528        /**
529         * Set HTTP headers to allow caching for the given number of seconds.
530         * Does not tell the browser to revalidate the resource.
531         * @param response current HTTP response
532         * @param seconds number of seconds into the future that the response
533         * should be cacheable for
534         * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
535         */
536        @Deprecated
537        protected final void cacheForSeconds(HttpServletResponse response, int seconds) {
538                cacheForSeconds(response, seconds, false);
539        }
540
541        /**
542         * Set HTTP headers to allow caching for the given number of seconds.
543         * Tells the browser to revalidate the resource if mustRevalidate is
544         * {@code true}.
545         * @param response the current HTTP response
546         * @param seconds number of seconds into the future that the response
547         * should be cacheable for
548         * @param mustRevalidate whether the client should revalidate the resource
549         * (typically only necessary for controllers with last-modified support)
550         * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
551         */
552        @Deprecated
553        protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
554                if (this.useExpiresHeader) {
555                        // HTTP 1.0 header
556                        response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L);
557                }
558                else if (response.containsHeader(HEADER_EXPIRES)) {
559                        // Reset HTTP 1.0 Expires header if present
560                        response.setHeader(HEADER_EXPIRES, "");
561                }
562
563                if (this.useCacheControlHeader) {
564                        // HTTP 1.1 header
565                        String headerValue = "max-age=" + seconds;
566                        if (mustRevalidate || this.alwaysMustRevalidate) {
567                                headerValue += ", must-revalidate";
568                        }
569                        response.setHeader(HEADER_CACHE_CONTROL, headerValue);
570                }
571
572                if (response.containsHeader(HEADER_PRAGMA)) {
573                        // Reset HTTP 1.0 Pragma header if present
574                        response.setHeader(HEADER_PRAGMA, "");
575                }
576        }
577
578        /**
579         * Prevent the response from being cached.
580         * Only called in HTTP 1.0 compatibility mode.
581         * <p>See {@code https://www.mnot.net/cache_docs}.
582         * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
583         */
584        @Deprecated
585        protected final void preventCaching(HttpServletResponse response) {
586                response.setHeader(HEADER_PRAGMA, "no-cache");
587
588                if (this.useExpiresHeader) {
589                        // HTTP 1.0 Expires header
590                        response.setDateHeader(HEADER_EXPIRES, 1L);
591                }
592
593                if (this.useCacheControlHeader) {
594                        // HTTP 1.1 Cache-Control header: "no-cache" is the standard value,
595                        // "no-store" is necessary to prevent caching on Firefox.
596                        response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
597                        if (this.useCacheControlNoStore) {
598                                response.addHeader(HEADER_CACHE_CONTROL, "no-store");
599                        }
600                }
601        }
602
603
604        private Collection<String> getVaryRequestHeadersToAdd(HttpServletResponse response, String[] varyByRequestHeaders) {
605                if (!response.containsHeader(HttpHeaders.VARY)) {
606                        return Arrays.asList(varyByRequestHeaders);
607                }
608                Collection<String> result = new ArrayList<>(varyByRequestHeaders.length);
609                Collections.addAll(result, varyByRequestHeaders);
610                for (String header : response.getHeaders(HttpHeaders.VARY)) {
611                        for (String existing : StringUtils.tokenizeToStringArray(header, ",")) {
612                                if ("*".equals(existing)) {
613                                        return Collections.emptyList();
614                                }
615                                for (String value : varyByRequestHeaders) {
616                                        if (value.equalsIgnoreCase(existing)) {
617                                                result.remove(value);
618                                        }
619                                }
620                        }
621                }
622                return result;
623        }
624
625}