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