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}