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 }