View Javadoc
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 }