1 /*
2 * Copyright 2002-2014 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springframework.web.servlet.support;
18
19 import java.util.Arrays;
20 import java.util.HashSet;
21 import java.util.Set;
22 import javax.servlet.ServletException;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
25
26 import org.springframework.util.StringUtils;
27 import org.springframework.web.HttpRequestMethodNotSupportedException;
28 import org.springframework.web.HttpSessionRequiredException;
29 import org.springframework.web.context.request.WebRequest;
30 import org.springframework.web.context.support.WebApplicationObjectSupport;
31
32 /**
33 * Convenient superclass for any kind of web content generator,
34 * like {@link org.springframework.web.servlet.mvc.AbstractController}
35 * and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}.
36 * Can also be used for custom handlers that have their own
37 * {@link org.springframework.web.servlet.HandlerAdapter}.
38 *
39 * <p>Supports HTTP cache control options. The usage of corresponding
40 * HTTP headers can be controlled via the "useExpiresHeader",
41 * "useCacheControlHeader" and "useCacheControlNoStore" properties.
42 *
43 * @author Rod Johnson
44 * @author Juergen Hoeller
45 * @see #setCacheSeconds
46 * @see #setRequireSession
47 */
48 public abstract class WebContentGenerator extends WebApplicationObjectSupport {
49
50 /** HTTP method "GET" */
51 public static final String METHOD_GET = "GET";
52
53 /** HTTP method "HEAD" */
54 public static final String METHOD_HEAD = "HEAD";
55
56 /** HTTP method "POST" */
57 public static final String METHOD_POST = "POST";
58
59
60 private static final String HEADER_PRAGMA = "Pragma";
61
62 private static final String HEADER_EXPIRES = "Expires";
63
64 private static final String HEADER_CACHE_CONTROL = "Cache-Control";
65
66
67 /** Set of supported HTTP methods */
68 private Set<String> supportedMethods;
69
70 private boolean requireSession = false;
71
72 /** Use HTTP 1.0 expires header? */
73 private boolean useExpiresHeader = true;
74
75 /** Use HTTP 1.1 cache-control header? */
76 private boolean useCacheControlHeader = true;
77
78 /** Use HTTP 1.1 cache-control header value "no-store"? */
79 private boolean useCacheControlNoStore = true;
80
81 private int cacheSeconds = -1;
82
83 private boolean alwaysMustRevalidate = false;
84
85
86 /**
87 * Create a new WebContentGenerator which supports
88 * HTTP methods GET, HEAD and POST by default.
89 */
90 public WebContentGenerator() {
91 this(true);
92 }
93
94 /**
95 * Create a new WebContentGenerator.
96 * @param restrictDefaultSupportedMethods {@code true} if this
97 * generator should support HTTP methods GET, HEAD and POST by default,
98 * or {@code false} if it should be unrestricted
99 */
100 public WebContentGenerator(boolean restrictDefaultSupportedMethods) {
101 if (restrictDefaultSupportedMethods) {
102 this.supportedMethods = new HashSet<String>(4);
103 this.supportedMethods.add(METHOD_GET);
104 this.supportedMethods.add(METHOD_HEAD);
105 this.supportedMethods.add(METHOD_POST);
106 }
107 }
108
109 /**
110 * Create a new WebContentGenerator.
111 * @param supportedMethods the supported HTTP methods for this content generator
112 */
113 public WebContentGenerator(String... supportedMethods) {
114 this.supportedMethods = new HashSet<String>(Arrays.asList(supportedMethods));
115 }
116
117
118 /**
119 * Set the HTTP methods that this content generator should support.
120 * <p>Default is GET, HEAD and POST for simple form controller types;
121 * unrestricted for general controllers and interceptors.
122 */
123 public final void setSupportedMethods(String... methods) {
124 if (methods != null) {
125 this.supportedMethods = new HashSet<String>(Arrays.asList(methods));
126 }
127 else {
128 this.supportedMethods = null;
129 }
130 }
131
132 /**
133 * Return the HTTP methods that this content generator supports.
134 */
135 public final String[] getSupportedMethods() {
136 return StringUtils.toStringArray(this.supportedMethods);
137 }
138
139 /**
140 * Set whether a session should be required to handle requests.
141 */
142 public final void setRequireSession(boolean requireSession) {
143 this.requireSession = requireSession;
144 }
145
146 /**
147 * Return whether a session is required to handle requests.
148 */
149 public final boolean isRequireSession() {
150 return this.requireSession;
151 }
152
153 /**
154 * Set whether to use the HTTP 1.0 expires header. Default is "true".
155 * <p>Note: Cache headers will only get applied if caching is enabled
156 * (or explicitly prevented) for the current request.
157 */
158 public final void setUseExpiresHeader(boolean useExpiresHeader) {
159 this.useExpiresHeader = useExpiresHeader;
160 }
161
162 /**
163 * Return whether the HTTP 1.0 expires header is used.
164 */
165 public final boolean isUseExpiresHeader() {
166 return this.useExpiresHeader;
167 }
168
169 /**
170 * Set whether to use the HTTP 1.1 cache-control header. Default is "true".
171 * <p>Note: Cache headers will only get applied if caching is enabled
172 * (or explicitly prevented) for the current request.
173 */
174 public final void setUseCacheControlHeader(boolean useCacheControlHeader) {
175 this.useCacheControlHeader = useCacheControlHeader;
176 }
177
178 /**
179 * Return whether the HTTP 1.1 cache-control header is used.
180 */
181 public final boolean isUseCacheControlHeader() {
182 return this.useCacheControlHeader;
183 }
184
185 /**
186 * Set whether to use the HTTP 1.1 cache-control header value "no-store"
187 * when preventing caching. Default is "true".
188 */
189 public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) {
190 this.useCacheControlNoStore = useCacheControlNoStore;
191 }
192
193 /**
194 * Return whether the HTTP 1.1 cache-control header value "no-store" is used.
195 */
196 public final boolean isUseCacheControlNoStore() {
197 return this.useCacheControlNoStore;
198 }
199
200 /**
201 * An option to add 'must-revalidate' to every Cache-Control header. This
202 * may be useful with annotated controller methods, which can
203 * programmatically do a lastModified calculation as described in
204 * {@link WebRequest#checkNotModified(long)}. Default is "false",
205 * effectively relying on whether the handler implements
206 * {@link org.springframework.web.servlet.mvc.LastModified} or not.
207 */
208 public void setAlwaysMustRevalidate(boolean mustRevalidate) {
209 this.alwaysMustRevalidate = mustRevalidate;
210 }
211
212 /**
213 * Return whether 'must-revalidate' is added to every Cache-Control header.
214 */
215 public boolean isAlwaysMustRevalidate() {
216 return alwaysMustRevalidate;
217 }
218
219 /**
220 * Cache content for the given number of seconds. Default is -1,
221 * indicating no generation of cache-related headers.
222 * <p>Only if this is set to 0 (no cache) or a positive value (cache for
223 * this many seconds) will this class generate cache headers.
224 * <p>The headers can be overwritten by subclasses, before content is generated.
225 */
226 public final void setCacheSeconds(int seconds) {
227 this.cacheSeconds = seconds;
228 }
229
230 /**
231 * Return the number of seconds that content is cached.
232 */
233 public final int getCacheSeconds() {
234 return this.cacheSeconds;
235 }
236
237
238 /**
239 * Check and prepare the given request and response according to the settings
240 * of this generator. Checks for supported methods and a required session,
241 * and applies the number of cache seconds specified for this generator.
242 * @param request current HTTP request
243 * @param response current HTTP response
244 * @param lastModified if the mapped handler provides Last-Modified support
245 * @throws ServletException if the request cannot be handled because a check failed
246 */
247 protected final void checkAndPrepare(
248 HttpServletRequest request, HttpServletResponse response, boolean lastModified)
249 throws ServletException {
250
251 checkAndPrepare(request, response, this.cacheSeconds, lastModified);
252 }
253
254 /**
255 * Check and prepare the given request and response according to the settings
256 * of this generator. Checks for supported methods and a required session,
257 * and applies the given number of cache seconds.
258 * @param request current HTTP request
259 * @param response current HTTP response
260 * @param cacheSeconds positive number of seconds into the future that the
261 * response should be cacheable for, 0 to prevent caching
262 * @param lastModified if the mapped handler provides Last-Modified support
263 * @throws ServletException if the request cannot be handled because a check failed
264 */
265 protected final void checkAndPrepare(
266 HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
267 throws ServletException {
268
269 // Check whether we should support the request method.
270 String method = request.getMethod();
271 if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
272 throw new HttpRequestMethodNotSupportedException(
273 method, StringUtils.toStringArray(this.supportedMethods));
274 }
275
276 // Check whether a session is required.
277 if (this.requireSession) {
278 if (request.getSession(false) == null) {
279 throw new HttpSessionRequiredException("Pre-existing session required but none found");
280 }
281 }
282
283 // Do declarative cache control.
284 // Revalidate if the controller supports last-modified.
285 applyCacheSeconds(response, cacheSeconds, lastModified);
286 }
287
288 /**
289 * Prevent the response from being cached.
290 * See {@code http://www.mnot.net/cache_docs}.
291 */
292 protected final void preventCaching(HttpServletResponse response) {
293 response.setHeader(HEADER_PRAGMA, "no-cache");
294 if (this.useExpiresHeader) {
295 // HTTP 1.0 header
296 response.setDateHeader(HEADER_EXPIRES, 1L);
297 }
298 if (this.useCacheControlHeader) {
299 // HTTP 1.1 header: "no-cache" is the standard value,
300 // "no-store" is necessary to prevent caching on FireFox.
301 response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
302 if (this.useCacheControlNoStore) {
303 response.addHeader(HEADER_CACHE_CONTROL, "no-store");
304 }
305 }
306 }
307
308 /**
309 * Set HTTP headers to allow caching for the given number of seconds.
310 * Does not tell the browser to revalidate the resource.
311 * @param response current HTTP response
312 * @param seconds number of seconds into the future that the response
313 * should be cacheable for
314 * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean)
315 */
316 protected final void cacheForSeconds(HttpServletResponse response, int seconds) {
317 cacheForSeconds(response, seconds, false);
318 }
319
320 /**
321 * Set HTTP headers to allow caching for the given number of seconds.
322 * Tells the browser to revalidate the resource if mustRevalidate is
323 * {@code true}.
324 * @param response the current HTTP response
325 * @param seconds number of seconds into the future that the response
326 * should be cacheable for
327 * @param mustRevalidate whether the client should revalidate the resource
328 * (typically only necessary for controllers with last-modified support)
329 */
330 protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
331 if (this.useExpiresHeader) {
332 // HTTP 1.0 header
333 response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L);
334 }
335 if (this.useCacheControlHeader) {
336 // HTTP 1.1 header
337 String headerValue = "max-age=" + seconds;
338 if (mustRevalidate || this.alwaysMustRevalidate) {
339 headerValue += ", must-revalidate";
340 }
341 response.setHeader(HEADER_CACHE_CONTROL, headerValue);
342 }
343 }
344
345 /**
346 * Apply the given cache seconds and generate corresponding HTTP headers,
347 * i.e. allow caching for the given number of seconds in case of a positive
348 * value, prevent caching if given a 0 value, do nothing else.
349 * Does not tell the browser to revalidate the resource.
350 * @param response current HTTP response
351 * @param seconds positive number of seconds into the future that the
352 * response should be cacheable for, 0 to prevent caching
353 * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean)
354 */
355 protected final void applyCacheSeconds(HttpServletResponse response, int seconds) {
356 applyCacheSeconds(response, seconds, false);
357 }
358
359 /**
360 * Apply the given cache seconds and generate respective HTTP headers.
361 * <p>That is, allow caching for the given number of seconds in the
362 * case of a positive value, prevent caching if given a 0 value, else
363 * do nothing (i.e. leave caching to the client).
364 * @param response the current HTTP response
365 * @param seconds the (positive) number of seconds into the future that
366 * the response should be cacheable for; 0 to prevent caching; and
367 * a negative value to leave caching to the client.
368 * @param mustRevalidate whether the client should revalidate the resource
369 * (typically only necessary for controllers with last-modified support)
370 */
371 protected final void applyCacheSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
372 if (seconds > 0) {
373 cacheForSeconds(response, seconds, mustRevalidate);
374 }
375 else if (seconds == 0) {
376 preventCaching(response);
377 }
378 // Leave caching to the client otherwise.
379 }
380
381 }