001/* 002 * Copyright 2012-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 * http://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.boot.autoconfigure.web; 018 019import java.time.Duration; 020import java.time.temporal.ChronoUnit; 021import java.util.concurrent.TimeUnit; 022 023import org.springframework.boot.context.properties.ConfigurationProperties; 024import org.springframework.boot.context.properties.PropertyMapper; 025import org.springframework.boot.convert.DurationUnit; 026import org.springframework.http.CacheControl; 027 028/** 029 * Properties used to configure resource handling. 030 * 031 * @author Phillip Webb 032 * @author Brian Clozel 033 * @author Dave Syer 034 * @author Venil Noronha 035 * @author Kristine Jetzke 036 * @since 1.1.0 037 */ 038@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) 039public class ResourceProperties { 040 041 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 042 "classpath:/META-INF/resources/", "classpath:/resources/", 043 "classpath:/static/", "classpath:/public/" }; 044 045 /** 046 * Locations of static resources. Defaults to classpath:[/META-INF/resources/, 047 * /resources/, /static/, /public/]. 048 */ 049 private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; 050 051 /** 052 * Whether to enable default resource handling. 053 */ 054 private boolean addMappings = true; 055 056 private final Chain chain = new Chain(); 057 058 private final Cache cache = new Cache(); 059 060 public String[] getStaticLocations() { 061 return this.staticLocations; 062 } 063 064 public void setStaticLocations(String[] staticLocations) { 065 this.staticLocations = appendSlashIfNecessary(staticLocations); 066 } 067 068 private String[] appendSlashIfNecessary(String[] staticLocations) { 069 String[] normalized = new String[staticLocations.length]; 070 for (int i = 0; i < staticLocations.length; i++) { 071 String location = staticLocations[i]; 072 normalized[i] = location.endsWith("/") ? location : location + "/"; 073 } 074 return normalized; 075 } 076 077 public boolean isAddMappings() { 078 return this.addMappings; 079 } 080 081 public void setAddMappings(boolean addMappings) { 082 this.addMappings = addMappings; 083 } 084 085 public Chain getChain() { 086 return this.chain; 087 } 088 089 public Cache getCache() { 090 return this.cache; 091 } 092 093 /** 094 * Configuration for the Spring Resource Handling chain. 095 */ 096 public static class Chain { 097 098 /** 099 * Whether to enable the Spring Resource Handling chain. By default, disabled 100 * unless at least one strategy has been enabled. 101 */ 102 private Boolean enabled; 103 104 /** 105 * Whether to enable caching in the Resource chain. 106 */ 107 private boolean cache = true; 108 109 /** 110 * Whether to enable HTML5 application cache manifest rewriting. 111 */ 112 private boolean htmlApplicationCache = false; 113 114 /** 115 * Whether to enable resolution of already compressed resources (gzip, brotli). 116 * Checks for a resource name with the '.gz' or '.br' file extensions. 117 */ 118 private boolean compressed = false; 119 120 private final Strategy strategy = new Strategy(); 121 122 /** 123 * Return whether the resource chain is enabled. Return {@code null} if no 124 * specific settings are present. 125 * @return whether the resource chain is enabled or {@code null} if no specified 126 * settings are present. 127 */ 128 public Boolean getEnabled() { 129 return getEnabled(getStrategy().getFixed().isEnabled(), 130 getStrategy().getContent().isEnabled(), this.enabled); 131 } 132 133 public void setEnabled(boolean enabled) { 134 this.enabled = enabled; 135 } 136 137 public boolean isCache() { 138 return this.cache; 139 } 140 141 public void setCache(boolean cache) { 142 this.cache = cache; 143 } 144 145 public Strategy getStrategy() { 146 return this.strategy; 147 } 148 149 public boolean isHtmlApplicationCache() { 150 return this.htmlApplicationCache; 151 } 152 153 public void setHtmlApplicationCache(boolean htmlApplicationCache) { 154 this.htmlApplicationCache = htmlApplicationCache; 155 } 156 157 public boolean isCompressed() { 158 return this.compressed; 159 } 160 161 public void setCompressed(boolean compressed) { 162 this.compressed = compressed; 163 } 164 165 static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled, 166 Boolean chainEnabled) { 167 return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled; 168 } 169 170 } 171 172 /** 173 * Strategies for extracting and embedding a resource version in its URL path. 174 */ 175 public static class Strategy { 176 177 private final Fixed fixed = new Fixed(); 178 179 private final Content content = new Content(); 180 181 public Fixed getFixed() { 182 return this.fixed; 183 } 184 185 public Content getContent() { 186 return this.content; 187 } 188 189 } 190 191 /** 192 * Version Strategy based on content hashing. 193 */ 194 public static class Content { 195 196 /** 197 * Whether to enable the content Version Strategy. 198 */ 199 private boolean enabled; 200 201 /** 202 * Comma-separated list of patterns to apply to the content Version Strategy. 203 */ 204 private String[] paths = new String[] { "/**" }; 205 206 public boolean isEnabled() { 207 return this.enabled; 208 } 209 210 public void setEnabled(boolean enabled) { 211 this.enabled = enabled; 212 } 213 214 public String[] getPaths() { 215 return this.paths; 216 } 217 218 public void setPaths(String[] paths) { 219 this.paths = paths; 220 } 221 222 } 223 224 /** 225 * Version Strategy based on a fixed version string. 226 */ 227 public static class Fixed { 228 229 /** 230 * Whether to enable the fixed Version Strategy. 231 */ 232 private boolean enabled; 233 234 /** 235 * Comma-separated list of patterns to apply to the fixed Version Strategy. 236 */ 237 private String[] paths = new String[] { "/**" }; 238 239 /** 240 * Version string to use for the fixed Version Strategy. 241 */ 242 private String version; 243 244 public boolean isEnabled() { 245 return this.enabled; 246 } 247 248 public void setEnabled(boolean enabled) { 249 this.enabled = enabled; 250 } 251 252 public String[] getPaths() { 253 return this.paths; 254 } 255 256 public void setPaths(String[] paths) { 257 this.paths = paths; 258 } 259 260 public String getVersion() { 261 return this.version; 262 } 263 264 public void setVersion(String version) { 265 this.version = version; 266 } 267 268 } 269 270 /** 271 * Cache configuration. 272 */ 273 public static class Cache { 274 275 /** 276 * Cache period for the resources served by the resource handler. If a duration 277 * suffix is not specified, seconds will be used. Can be overridden by the 278 * 'spring.resources.cache.cachecontrol' properties. 279 */ 280 @DurationUnit(ChronoUnit.SECONDS) 281 private Duration period; 282 283 /** 284 * Cache control HTTP headers, only allows valid directive combinations. Overrides 285 * the 'spring.resources.cache.period' property. 286 */ 287 private final Cachecontrol cachecontrol = new Cachecontrol(); 288 289 public Duration getPeriod() { 290 return this.period; 291 } 292 293 public void setPeriod(Duration period) { 294 this.period = period; 295 } 296 297 public Cachecontrol getCachecontrol() { 298 return this.cachecontrol; 299 } 300 301 /** 302 * Cache Control HTTP header configuration. 303 */ 304 public static class Cachecontrol { 305 306 /** 307 * Maximum time the response should be cached, in seconds if no duration 308 * suffix is not specified. 309 */ 310 @DurationUnit(ChronoUnit.SECONDS) 311 private Duration maxAge; 312 313 /** 314 * Indicate that the cached response can be reused only if re-validated with 315 * the server. 316 */ 317 private Boolean noCache; 318 319 /** 320 * Indicate to not cache the response in any case. 321 */ 322 private Boolean noStore; 323 324 /** 325 * Indicate that once it has become stale, a cache must not use the response 326 * without re-validating it with the server. 327 */ 328 private Boolean mustRevalidate; 329 330 /** 331 * Indicate intermediaries (caches and others) that they should not transform 332 * the response content. 333 */ 334 private Boolean noTransform; 335 336 /** 337 * Indicate that any cache may store the response. 338 */ 339 private Boolean cachePublic; 340 341 /** 342 * Indicate that the response message is intended for a single user and must 343 * not be stored by a shared cache. 344 */ 345 private Boolean cachePrivate; 346 347 /** 348 * Same meaning as the "must-revalidate" directive, except that it does not 349 * apply to private caches. 350 */ 351 private Boolean proxyRevalidate; 352 353 /** 354 * Maximum time the response can be served after it becomes stale, in seconds 355 * if no duration suffix is not specified. 356 */ 357 @DurationUnit(ChronoUnit.SECONDS) 358 private Duration staleWhileRevalidate; 359 360 /** 361 * Maximum time the response may be used when errors are encountered, in 362 * seconds if no duration suffix is not specified. 363 */ 364 @DurationUnit(ChronoUnit.SECONDS) 365 private Duration staleIfError; 366 367 /** 368 * Maximum time the response should be cached by shared caches, in seconds if 369 * no duration suffix is not specified. 370 */ 371 @DurationUnit(ChronoUnit.SECONDS) 372 private Duration sMaxAge; 373 374 public Duration getMaxAge() { 375 return this.maxAge; 376 } 377 378 public void setMaxAge(Duration maxAge) { 379 this.maxAge = maxAge; 380 } 381 382 public Boolean getNoCache() { 383 return this.noCache; 384 } 385 386 public void setNoCache(Boolean noCache) { 387 this.noCache = noCache; 388 } 389 390 public Boolean getNoStore() { 391 return this.noStore; 392 } 393 394 public void setNoStore(Boolean noStore) { 395 this.noStore = noStore; 396 } 397 398 public Boolean getMustRevalidate() { 399 return this.mustRevalidate; 400 } 401 402 public void setMustRevalidate(Boolean mustRevalidate) { 403 this.mustRevalidate = mustRevalidate; 404 } 405 406 public Boolean getNoTransform() { 407 return this.noTransform; 408 } 409 410 public void setNoTransform(Boolean noTransform) { 411 this.noTransform = noTransform; 412 } 413 414 public Boolean getCachePublic() { 415 return this.cachePublic; 416 } 417 418 public void setCachePublic(Boolean cachePublic) { 419 this.cachePublic = cachePublic; 420 } 421 422 public Boolean getCachePrivate() { 423 return this.cachePrivate; 424 } 425 426 public void setCachePrivate(Boolean cachePrivate) { 427 this.cachePrivate = cachePrivate; 428 } 429 430 public Boolean getProxyRevalidate() { 431 return this.proxyRevalidate; 432 } 433 434 public void setProxyRevalidate(Boolean proxyRevalidate) { 435 this.proxyRevalidate = proxyRevalidate; 436 } 437 438 public Duration getStaleWhileRevalidate() { 439 return this.staleWhileRevalidate; 440 } 441 442 public void setStaleWhileRevalidate(Duration staleWhileRevalidate) { 443 this.staleWhileRevalidate = staleWhileRevalidate; 444 } 445 446 public Duration getStaleIfError() { 447 return this.staleIfError; 448 } 449 450 public void setStaleIfError(Duration staleIfError) { 451 this.staleIfError = staleIfError; 452 } 453 454 public Duration getSMaxAge() { 455 return this.sMaxAge; 456 } 457 458 public void setSMaxAge(Duration sMaxAge) { 459 this.sMaxAge = sMaxAge; 460 } 461 462 public CacheControl toHttpCacheControl() { 463 PropertyMapper map = PropertyMapper.get(); 464 CacheControl control = createCacheControl(); 465 map.from(this::getMustRevalidate).whenTrue() 466 .toCall(control::mustRevalidate); 467 map.from(this::getNoTransform).whenTrue().toCall(control::noTransform); 468 map.from(this::getCachePublic).whenTrue().toCall(control::cachePublic); 469 map.from(this::getCachePrivate).whenTrue().toCall(control::cachePrivate); 470 map.from(this::getProxyRevalidate).whenTrue() 471 .toCall(control::proxyRevalidate); 472 map.from(this::getStaleWhileRevalidate).whenNonNull().to( 473 (duration) -> control.staleWhileRevalidate(duration.getSeconds(), 474 TimeUnit.SECONDS)); 475 map.from(this::getStaleIfError).whenNonNull().to((duration) -> control 476 .staleIfError(duration.getSeconds(), TimeUnit.SECONDS)); 477 map.from(this::getSMaxAge).whenNonNull().to((duration) -> control 478 .sMaxAge(duration.getSeconds(), TimeUnit.SECONDS)); 479 return control; 480 } 481 482 private CacheControl createCacheControl() { 483 if (Boolean.TRUE.equals(this.noStore)) { 484 return CacheControl.noStore(); 485 } 486 if (Boolean.TRUE.equals(this.noCache)) { 487 return CacheControl.noCache(); 488 } 489 if (this.maxAge != null) { 490 return CacheControl.maxAge(this.maxAge.getSeconds(), 491 TimeUnit.SECONDS); 492 } 493 return CacheControl.empty(); 494 } 495 496 } 497 498 } 499 500}