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