From c22e65584828a5c20c4a945e0635fdf36d982987 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 29 Oct 2020 12:46:24 +0000 Subject: [PATCH] Move spring.resources.* properties to spring.web.resources.* Closes gh-23917 --- .../web/OnEnabledResourceChainCondition.java | 28 +- .../autoconfigure/web/ResourceProperties.java | 420 ++++---------- .../boot/autoconfigure/web/WebProperties.java | 525 ++++++++++++++++++ ...ResourceHandlerRegistrationCustomizer.java | 22 +- .../reactive/WebFluxAutoConfiguration.java | 39 +- .../AbstractErrorWebExceptionHandler.java | 34 +- .../DefaultErrorWebExceptionHandler.java | 23 +- .../error/ErrorWebFluxAutoConfiguration.java | 12 +- .../web/servlet/WebMvcAutoConfiguration.java | 60 +- .../error/DefaultErrorViewResolver.java | 32 +- .../error/ErrorMvcAutoConfiguration.java | 18 +- ...onditionalOnEnabledResourceChainTests.java | 30 +- .../web/ResourcePropertiesBindingTests.java | 2 + .../web/ResourcePropertiesTests.java | 6 +- .../WebPropertiesResourcesBindingTests.java | 69 +++ .../web/WebPropertiesResourcesTests.java | 108 ++++ .../WebFluxAutoConfigurationTests.java | 48 +- .../DefaultErrorWebExceptionHandlerTests.java | 4 +- .../servlet/WebMvcAutoConfigurationTests.java | 104 ++-- .../error/DefaultErrorViewResolverTests.java | 17 +- ...DevToolsPropertyDefaultsPostProcessor.java | 28 +- .../LocalDevToolsAutoConfigurationTests.java | 20 +- .../docs/asciidoc/spring-boot-features.adoc | 38 +- 23 files changed, 1168 insertions(+), 519 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesBindingTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java index 5c82245e76..c28112d713 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java @@ -19,9 +19,13 @@ package org.springframework.boot.autoconfigure.web; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.ClassUtils; @@ -41,10 +45,11 @@ class OnEnabledResourceChainCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConfigurableEnvironment environment = (ConfigurableEnvironment) context.getEnvironment(); - boolean fixed = getEnabledProperty(environment, "strategy.fixed.", false); - boolean content = getEnabledProperty(environment, "strategy.content.", false); - Boolean chain = getEnabledProperty(environment, "", null); - Boolean match = ResourceProperties.Chain.getEnabled(fixed, content, chain); + String prefix = determineResourcePropertiesPrefix(environment); + boolean fixed = getEnabledProperty(environment, prefix, "strategy.fixed.", false); + boolean content = getEnabledProperty(environment, prefix, "strategy.content.", false); + Boolean chain = getEnabledProperty(environment, prefix, "", null); + Boolean match = Chain.getEnabled(fixed, content, chain); ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnEnabledResourceChain.class); if (match == null) { if (ClassUtils.isPresent(WEBJAR_ASSET_LOCATOR, getClass().getClassLoader())) { @@ -58,8 +63,19 @@ class OnEnabledResourceChainCondition extends SpringBootCondition { return ConditionOutcome.noMatch(message.because("disabled")); } - private Boolean getEnabledProperty(ConfigurableEnvironment environment, String key, Boolean defaultValue) { - String name = "spring.resources.chain." + key + "enabled"; + @SuppressWarnings("deprecation") + private String determineResourcePropertiesPrefix(Environment environment) { + BindResult result = Binder.get(environment) + .bind("spring.resources", org.springframework.boot.autoconfigure.web.ResourceProperties.class); + if (result.isBound() && result.get().hasBeenCustomized()) { + return "spring.resources.chain."; + } + return "spring.web.resources.chain."; + } + + private Boolean getEnabledProperty(ConfigurableEnvironment environment, String prefix, String key, + Boolean defaultValue) { + String name = prefix + key + "enabled"; return environment.getProperty(name, Boolean.class, defaultValue); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java index 6f4fb0992d..5ebe96d0b8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java @@ -17,14 +17,10 @@ package org.springframework.boot.autoconfigure.web; import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.concurrent.TimeUnit; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.convert.DurationUnit; -import org.springframework.http.CacheControl; /** * Properties used to configure resource handling. @@ -35,137 +31,79 @@ import org.springframework.http.CacheControl; * @author Venil Noronha * @author Kristine Jetzke * @since 1.1.0 + * @deprecated since 2.4.0 in favor of {@link WebProperties.Resources} */ +@Deprecated @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) -public class ResourceProperties { - - private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", - "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; - - /** - * Locations of static resources. Defaults to classpath:[/META-INF/resources/, - * /resources/, /static/, /public/]. - */ - private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; - - /** - * Whether to enable default resource handling. - */ - private boolean addMappings = true; +public class ResourceProperties extends Resources { private final Chain chain = new Chain(); private final Cache cache = new Cache(); + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.static-locations") public String[] getStaticLocations() { - return this.staticLocations; - } - - public void setStaticLocations(String[] staticLocations) { - this.staticLocations = appendSlashIfNecessary(staticLocations); - } - - private String[] appendSlashIfNecessary(String[] staticLocations) { - String[] normalized = new String[staticLocations.length]; - for (int i = 0; i < staticLocations.length; i++) { - String location = staticLocations[i]; - normalized[i] = location.endsWith("/") ? location : location + "/"; - } - return normalized; + return super.getStaticLocations(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.add-mappings") public boolean isAddMappings() { - return this.addMappings; - } - - public void setAddMappings(boolean addMappings) { - this.addMappings = addMappings; + return super.isAddMappings(); } + @Override public Chain getChain() { return this.chain; } + @Override public Cache getCache() { return this.cache; } - /** - * Configuration for the Spring Resource Handling chain. - */ - public static class Chain { - - /** - * Whether to enable the Spring Resource Handling chain. By default, disabled - * unless at least one strategy has been enabled. - */ - private Boolean enabled; + @Deprecated + public static class Chain extends Resources.Chain { - /** - * Whether to enable caching in the Resource chain. - */ - private boolean cache = true; + private final org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy strategy = new org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy(); /** * Whether to enable HTML5 application cache manifest rewriting. */ private boolean htmlApplicationCache = false; - /** - * Whether to enable resolution of already compressed resources (gzip, brotli). - * Checks for a resource name with the '.gz' or '.br' file extensions. - */ - private boolean compressed = false; - - private final Strategy strategy = new Strategy(); - - /** - * Return whether the resource chain is enabled. Return {@code null} if no - * specific settings are present. - * @return whether the resource chain is enabled or {@code null} if no specified - * settings are present. - */ + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.enabled") public Boolean getEnabled() { - return getEnabled(getStrategy().getFixed().isEnabled(), getStrategy().getContent().isEnabled(), - this.enabled); - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; + return super.getEnabled(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.cache") public boolean isCache() { - return this.cache; - } - - public void setCache(boolean cache) { - this.cache = cache; - } - - public Strategy getStrategy() { - return this.strategy; + return super.isCache(); } @DeprecatedConfigurationProperty(reason = "The appcache manifest feature is being removed from browsers.") - @Deprecated public boolean isHtmlApplicationCache() { return this.htmlApplicationCache; } public void setHtmlApplicationCache(boolean htmlApplicationCache) { this.htmlApplicationCache = htmlApplicationCache; + this.customized = true; } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.compressed") public boolean isCompressed() { - return this.compressed; - } - - public void setCompressed(boolean compressed) { - this.compressed = compressed; + return super.isCompressed(); } - static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled, Boolean chainEnabled) { - return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled; + @Override + public org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy getStrategy() { + return this.strategy; } } @@ -173,17 +111,20 @@ public class ResourceProperties { /** * Strategies for extracting and embedding a resource version in its URL path. */ - public static class Strategy { + @Deprecated + public static class Strategy extends Resources.Chain.Strategy { - private final Fixed fixed = new Fixed(); + private final org.springframework.boot.autoconfigure.web.ResourceProperties.Fixed fixed = new org.springframework.boot.autoconfigure.web.ResourceProperties.Fixed(); - private final Content content = new Content(); + private final org.springframework.boot.autoconfigure.web.ResourceProperties.Content content = new org.springframework.boot.autoconfigure.web.ResourceProperties.Content(); - public Fixed getFixed() { + @Override + public org.springframework.boot.autoconfigure.web.ResourceProperties.Fixed getFixed() { return this.fixed; } - public Content getContent() { + @Override + public org.springframework.boot.autoconfigure.web.ResourceProperties.Content getContent() { return this.content; } @@ -192,32 +133,19 @@ public class ResourceProperties { /** * Version Strategy based on content hashing. */ - public static class Content { - - /** - * Whether to enable the content Version Strategy. - */ - private boolean enabled; - - /** - * Comma-separated list of patterns to apply to the content Version Strategy. - */ - private String[] paths = new String[] { "/**" }; + @Deprecated + public static class Content extends Resources.Chain.Strategy.Content { + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.content.enabled") public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; + return super.isEnabled(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.content.paths") public String[] getPaths() { - return this.paths; - } - - public void setPaths(String[] paths) { - this.paths = paths; + return super.getPaths(); } } @@ -225,45 +153,25 @@ public class ResourceProperties { /** * Version Strategy based on a fixed version string. */ - public static class Fixed { - - /** - * Whether to enable the fixed Version Strategy. - */ - private boolean enabled; - - /** - * Comma-separated list of patterns to apply to the fixed Version Strategy. - */ - private String[] paths = new String[] { "/**" }; - - /** - * Version string to use for the fixed Version Strategy. - */ - private String version; + @Deprecated + public static class Fixed extends Resources.Chain.Strategy.Fixed { + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.fixed.enabled") public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; + return super.isEnabled(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.fixed.paths") public String[] getPaths() { - return this.paths; - } - - public void setPaths(String[] paths) { - this.paths = paths; + return super.getPaths(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.fixed.version") public String getVersion() { - return this.version; - } - - public void setVersion(String version) { - this.version = version; + return super.getVersion(); } } @@ -271,30 +179,18 @@ public class ResourceProperties { /** * Cache configuration. */ - public static class Cache { - - /** - * Cache period for the resources served by the resource handler. If a duration - * suffix is not specified, seconds will be used. Can be overridden by the - * 'spring.resources.cache.cachecontrol' properties. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration period; + @Deprecated + public static class Cache extends Resources.Cache { - /** - * Cache control HTTP headers, only allows valid directive combinations. Overrides - * the 'spring.resources.cache.period' property. - */ private final Cachecontrol cachecontrol = new Cachecontrol(); + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.period") public Duration getPeriod() { - return this.period; - } - - public void setPeriod(Duration period) { - this.period = period; + return super.getPeriod(); } + @Override public Cachecontrol getCachecontrol() { return this.cachecontrol; } @@ -302,196 +198,74 @@ public class ResourceProperties { /** * Cache Control HTTP header configuration. */ - public static class Cachecontrol { - - /** - * Maximum time the response should be cached, in seconds if no duration - * suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration maxAge; - - /** - * Indicate that the cached response can be reused only if re-validated with - * the server. - */ - private Boolean noCache; - - /** - * Indicate to not cache the response in any case. - */ - private Boolean noStore; - - /** - * Indicate that once it has become stale, a cache must not use the response - * without re-validating it with the server. - */ - private Boolean mustRevalidate; - - /** - * Indicate intermediaries (caches and others) that they should not transform - * the response content. - */ - private Boolean noTransform; - - /** - * Indicate that any cache may store the response. - */ - private Boolean cachePublic; - - /** - * Indicate that the response message is intended for a single user and must - * not be stored by a shared cache. - */ - private Boolean cachePrivate; - - /** - * Same meaning as the "must-revalidate" directive, except that it does not - * apply to private caches. - */ - private Boolean proxyRevalidate; - - /** - * Maximum time the response can be served after it becomes stale, in seconds - * if no duration suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration staleWhileRevalidate; - - /** - * Maximum time the response may be used when errors are encountered, in - * seconds if no duration suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration staleIfError; - - /** - * Maximum time the response should be cached by shared caches, in seconds if - * no duration suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration sMaxAge; + @Deprecated + public static class Cachecontrol extends Resources.Cache.Cachecontrol { + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.max-age") public Duration getMaxAge() { - return this.maxAge; - } - - public void setMaxAge(Duration maxAge) { - this.maxAge = maxAge; + return super.getMaxAge(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.no-cache") public Boolean getNoCache() { - return this.noCache; - } - - public void setNoCache(Boolean noCache) { - this.noCache = noCache; + return super.getNoCache(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.no-store") public Boolean getNoStore() { - return this.noStore; - } - - public void setNoStore(Boolean noStore) { - this.noStore = noStore; + return super.getNoStore(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.must-revalidate") public Boolean getMustRevalidate() { - return this.mustRevalidate; - } - - public void setMustRevalidate(Boolean mustRevalidate) { - this.mustRevalidate = mustRevalidate; + return super.getMustRevalidate(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.no-transform") public Boolean getNoTransform() { - return this.noTransform; - } - - public void setNoTransform(Boolean noTransform) { - this.noTransform = noTransform; + return super.getNoTransform(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.cache-public") public Boolean getCachePublic() { - return this.cachePublic; - } - - public void setCachePublic(Boolean cachePublic) { - this.cachePublic = cachePublic; + return super.getCachePublic(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.cache-private") public Boolean getCachePrivate() { - return this.cachePrivate; - } - - public void setCachePrivate(Boolean cachePrivate) { - this.cachePrivate = cachePrivate; + return super.getCachePrivate(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.proxy-revalidate") public Boolean getProxyRevalidate() { - return this.proxyRevalidate; - } - - public void setProxyRevalidate(Boolean proxyRevalidate) { - this.proxyRevalidate = proxyRevalidate; + return super.getProxyRevalidate(); } + @Override + @DeprecatedConfigurationProperty( + replacement = "spring.web.resources.cache.cachecontrol.stale-while-revaliate") public Duration getStaleWhileRevalidate() { - return this.staleWhileRevalidate; - } - - public void setStaleWhileRevalidate(Duration staleWhileRevalidate) { - this.staleWhileRevalidate = staleWhileRevalidate; + return super.getStaleWhileRevalidate(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.stale-if-error") public Duration getStaleIfError() { - return this.staleIfError; - } - - public void setStaleIfError(Duration staleIfError) { - this.staleIfError = staleIfError; + return super.getStaleIfError(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.s-max-age") public Duration getSMaxAge() { - return this.sMaxAge; - } - - public void setSMaxAge(Duration sMaxAge) { - this.sMaxAge = sMaxAge; - } - - public CacheControl toHttpCacheControl() { - PropertyMapper map = PropertyMapper.get(); - CacheControl control = createCacheControl(); - map.from(this::getMustRevalidate).whenTrue().toCall(control::mustRevalidate); - map.from(this::getNoTransform).whenTrue().toCall(control::noTransform); - map.from(this::getCachePublic).whenTrue().toCall(control::cachePublic); - map.from(this::getCachePrivate).whenTrue().toCall(control::cachePrivate); - map.from(this::getProxyRevalidate).whenTrue().toCall(control::proxyRevalidate); - map.from(this::getStaleWhileRevalidate).whenNonNull() - .to((duration) -> control.staleWhileRevalidate(duration.getSeconds(), TimeUnit.SECONDS)); - map.from(this::getStaleIfError).whenNonNull() - .to((duration) -> control.staleIfError(duration.getSeconds(), TimeUnit.SECONDS)); - map.from(this::getSMaxAge).whenNonNull() - .to((duration) -> control.sMaxAge(duration.getSeconds(), TimeUnit.SECONDS)); - // check if cacheControl remained untouched - if (control.getHeaderValue() == null) { - return null; - } - return control; - } - - private CacheControl createCacheControl() { - if (Boolean.TRUE.equals(this.noStore)) { - return CacheControl.noStore(); - } - if (Boolean.TRUE.equals(this.noCache)) { - return CacheControl.noCache(); - } - if (this.maxAge != null) { - return CacheControl.maxAge(this.maxAge.getSeconds(), TimeUnit.SECONDS); - } - return CacheControl.empty(); + return super.getSMaxAge(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java index bf1b93ada2..e3dc6f1976 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java @@ -16,9 +16,15 @@ package org.springframework.boot.autoconfigure.web; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Locale; +import java.util.concurrent.TimeUnit; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.http.CacheControl; /** * {@link ConfigurationProperties Configuration properties} for general web concerns. @@ -40,6 +46,8 @@ public class WebProperties { */ private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER; + private final Resources resources = new Resources(); + public Locale getLocale() { return this.locale; } @@ -56,6 +64,10 @@ public class WebProperties { this.localeResolver = localeResolver; } + public Resources getResources() { + return this.resources; + } + public enum LocaleResolver { /** @@ -71,4 +83,517 @@ public class WebProperties { } + public static class Resources { + + private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", + "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; + + /** + * Locations of static resources. Defaults to classpath:[/META-INF/resources/, + * /resources/, /static/, /public/]. + */ + private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; + + /** + * Whether to enable default resource handling. + */ + private boolean addMappings = true; + + private boolean customized = false; + + private final Chain chain = new Chain(); + + private final Cache cache = new Cache(); + + public String[] getStaticLocations() { + return this.staticLocations; + } + + public void setStaticLocations(String[] staticLocations) { + this.staticLocations = appendSlashIfNecessary(staticLocations); + this.customized = true; + } + + private String[] appendSlashIfNecessary(String[] staticLocations) { + String[] normalized = new String[staticLocations.length]; + for (int i = 0; i < staticLocations.length; i++) { + String location = staticLocations[i]; + normalized[i] = location.endsWith("/") ? location : location + "/"; + } + return normalized; + } + + public boolean isAddMappings() { + return this.addMappings; + } + + public void setAddMappings(boolean addMappings) { + this.customized = true; + this.addMappings = addMappings; + } + + public Chain getChain() { + return this.chain; + } + + public Cache getCache() { + return this.cache; + } + + public boolean hasBeenCustomized() { + return this.customized || getChain().hasBeenCustomized() || getCache().hasBeenCustomized(); + } + + /** + * Configuration for the Spring Resource Handling chain. + */ + public static class Chain { + + boolean customized = false; + + /** + * Whether to enable the Spring Resource Handling chain. By default, disabled + * unless at least one strategy has been enabled. + */ + private Boolean enabled; + + /** + * Whether to enable caching in the Resource chain. + */ + private boolean cache = true; + + /** + * Whether to enable resolution of already compressed resources (gzip, + * brotli). Checks for a resource name with the '.gz' or '.br' file + * extensions. + */ + private boolean compressed = false; + + private final Strategy strategy = new Strategy(); + + /** + * Return whether the resource chain is enabled. Return {@code null} if no + * specific settings are present. + * @return whether the resource chain is enabled or {@code null} if no + * specified settings are present. + */ + public Boolean getEnabled() { + return getEnabled(getStrategy().getFixed().isEnabled(), getStrategy().getContent().isEnabled(), + this.enabled); + } + + private boolean hasBeenCustomized() { + return this.customized || getStrategy().hasBeenCustomized(); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + this.customized = true; + } + + public boolean isCache() { + return this.cache; + } + + public void setCache(boolean cache) { + this.cache = cache; + this.customized = true; + } + + public Strategy getStrategy() { + return this.strategy; + } + + public boolean isCompressed() { + return this.compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + this.customized = true; + } + + static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled, Boolean chainEnabled) { + return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled; + } + + /** + * Strategies for extracting and embedding a resource version in its URL path. + */ + public static class Strategy { + + private final Fixed fixed = new Fixed(); + + private final Content content = new Content(); + + public Fixed getFixed() { + return this.fixed; + } + + public Content getContent() { + return this.content; + } + + private boolean hasBeenCustomized() { + return getFixed().hasBeenCustomized() || getContent().hasBeenCustomized(); + } + + /** + * Version Strategy based on content hashing. + */ + public static class Content { + + private boolean customized = false; + + /** + * Whether to enable the content Version Strategy. + */ + private boolean enabled; + + /** + * Comma-separated list of patterns to apply to the content Version + * Strategy. + */ + private String[] paths = new String[] { "/**" }; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.customized = true; + this.enabled = enabled; + } + + public String[] getPaths() { + return this.paths; + } + + public void setPaths(String[] paths) { + this.customized = true; + this.paths = paths; + } + + private boolean hasBeenCustomized() { + return this.customized; + } + + } + + /** + * Version Strategy based on a fixed version string. + */ + public static class Fixed { + + private boolean customized = false; + + /** + * Whether to enable the fixed Version Strategy. + */ + private boolean enabled; + + /** + * Comma-separated list of patterns to apply to the fixed Version + * Strategy. + */ + private String[] paths = new String[] { "/**" }; + + /** + * Version string to use for the fixed Version Strategy. + */ + private String version; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.customized = true; + this.enabled = enabled; + } + + public String[] getPaths() { + return this.paths; + } + + public void setPaths(String[] paths) { + this.customized = true; + this.paths = paths; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.customized = true; + this.version = version; + } + + private boolean hasBeenCustomized() { + return this.customized; + } + + } + + } + + } + + /** + * Cache configuration. + */ + public static class Cache { + + private boolean customized = false; + + /** + * Cache period for the resources served by the resource handler. If a + * duration suffix is not specified, seconds will be used. Can be overridden + * by the 'spring.web.resources.cache.cachecontrol' properties. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration period; + + /** + * Cache control HTTP headers, only allows valid directive combinations. + * Overrides the 'spring.web.resources.cache.period' property. + */ + private final Cachecontrol cachecontrol = new Cachecontrol(); + + public Duration getPeriod() { + return this.period; + } + + public void setPeriod(Duration period) { + this.customized = true; + this.period = period; + } + + public Cachecontrol getCachecontrol() { + return this.cachecontrol; + } + + private boolean hasBeenCustomized() { + return this.customized || getCachecontrol().hasBeenCustomized(); + } + + /** + * Cache Control HTTP header configuration. + */ + public static class Cachecontrol { + + private boolean customized = false; + + /** + * Maximum time the response should be cached, in seconds if no duration + * suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxAge; + + /** + * Indicate that the cached response can be reused only if re-validated + * with the server. + */ + private Boolean noCache; + + /** + * Indicate to not cache the response in any case. + */ + private Boolean noStore; + + /** + * Indicate that once it has become stale, a cache must not use the + * response without re-validating it with the server. + */ + private Boolean mustRevalidate; + + /** + * Indicate intermediaries (caches and others) that they should not + * transform the response content. + */ + private Boolean noTransform; + + /** + * Indicate that any cache may store the response. + */ + private Boolean cachePublic; + + /** + * Indicate that the response message is intended for a single user and + * must not be stored by a shared cache. + */ + private Boolean cachePrivate; + + /** + * Same meaning as the "must-revalidate" directive, except that it does + * not apply to private caches. + */ + private Boolean proxyRevalidate; + + /** + * Maximum time the response can be served after it becomes stale, in + * seconds if no duration suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration staleWhileRevalidate; + + /** + * Maximum time the response may be used when errors are encountered, in + * seconds if no duration suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration staleIfError; + + /** + * Maximum time the response should be cached by shared caches, in seconds + * if no duration suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration sMaxAge; + + public Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(Duration maxAge) { + this.customized = true; + this.maxAge = maxAge; + } + + public Boolean getNoCache() { + return this.noCache; + } + + public void setNoCache(Boolean noCache) { + this.customized = true; + this.noCache = noCache; + } + + public Boolean getNoStore() { + return this.noStore; + } + + public void setNoStore(Boolean noStore) { + this.customized = true; + this.noStore = noStore; + } + + public Boolean getMustRevalidate() { + return this.mustRevalidate; + } + + public void setMustRevalidate(Boolean mustRevalidate) { + this.customized = true; + this.mustRevalidate = mustRevalidate; + } + + public Boolean getNoTransform() { + return this.noTransform; + } + + public void setNoTransform(Boolean noTransform) { + this.customized = true; + this.noTransform = noTransform; + } + + public Boolean getCachePublic() { + return this.cachePublic; + } + + public void setCachePublic(Boolean cachePublic) { + this.customized = true; + this.cachePublic = cachePublic; + } + + public Boolean getCachePrivate() { + return this.cachePrivate; + } + + public void setCachePrivate(Boolean cachePrivate) { + this.customized = true; + this.cachePrivate = cachePrivate; + } + + public Boolean getProxyRevalidate() { + return this.proxyRevalidate; + } + + public void setProxyRevalidate(Boolean proxyRevalidate) { + this.customized = true; + this.proxyRevalidate = proxyRevalidate; + } + + public Duration getStaleWhileRevalidate() { + return this.staleWhileRevalidate; + } + + public void setStaleWhileRevalidate(Duration staleWhileRevalidate) { + this.customized = true; + this.staleWhileRevalidate = staleWhileRevalidate; + } + + public Duration getStaleIfError() { + return this.staleIfError; + } + + public void setStaleIfError(Duration staleIfError) { + this.customized = true; + this.staleIfError = staleIfError; + } + + public Duration getSMaxAge() { + return this.sMaxAge; + } + + public void setSMaxAge(Duration sMaxAge) { + this.customized = true; + this.sMaxAge = sMaxAge; + } + + public CacheControl toHttpCacheControl() { + PropertyMapper map = PropertyMapper.get(); + CacheControl control = createCacheControl(); + map.from(this::getMustRevalidate).whenTrue().toCall(control::mustRevalidate); + map.from(this::getNoTransform).whenTrue().toCall(control::noTransform); + map.from(this::getCachePublic).whenTrue().toCall(control::cachePublic); + map.from(this::getCachePrivate).whenTrue().toCall(control::cachePrivate); + map.from(this::getProxyRevalidate).whenTrue().toCall(control::proxyRevalidate); + map.from(this::getStaleWhileRevalidate).whenNonNull() + .to((duration) -> control.staleWhileRevalidate(duration.getSeconds(), TimeUnit.SECONDS)); + map.from(this::getStaleIfError).whenNonNull() + .to((duration) -> control.staleIfError(duration.getSeconds(), TimeUnit.SECONDS)); + map.from(this::getSMaxAge).whenNonNull() + .to((duration) -> control.sMaxAge(duration.getSeconds(), TimeUnit.SECONDS)); + // check if cacheControl remained untouched + if (control.getHeaderValue() == null) { + return null; + } + return control; + } + + private CacheControl createCacheControl() { + if (Boolean.TRUE.equals(this.noStore)) { + return CacheControl.noStore(); + } + if (Boolean.TRUE.equals(this.noCache)) { + return CacheControl.noCache(); + } + if (this.maxAge != null) { + return CacheControl.maxAge(this.maxAge.getSeconds(), TimeUnit.SECONDS); + } + return CacheControl.empty(); + } + + private boolean hasBeenCustomized() { + return this.customized; + } + + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java index 549c50f837..492dcd0690 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java @@ -16,8 +16,7 @@ package org.springframework.boot.autoconfigure.web.reactive; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.web.reactive.config.ResourceChainRegistration; import org.springframework.web.reactive.config.ResourceHandlerRegistration; import org.springframework.web.reactive.resource.EncodedResourceResolver; @@ -32,30 +31,35 @@ import org.springframework.web.reactive.resource.VersionResourceResolver; */ class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer { - @Autowired - private ResourceProperties resourceProperties = new ResourceProperties(); + private final Resources resourceProperties; + + ResourceChainResourceHandlerRegistrationCustomizer(Resources resources) { + this.resourceProperties = resources; + } @Override public void customize(ResourceHandlerRegistration registration) { - ResourceProperties.Chain properties = this.resourceProperties.getChain(); + Resources.Chain properties = this.resourceProperties.getChain(); configureResourceChain(properties, registration.resourceChain(properties.isCache())); } @SuppressWarnings("deprecation") - private void configureResourceChain(ResourceProperties.Chain properties, ResourceChainRegistration chain) { - ResourceProperties.Strategy strategy = properties.getStrategy(); + private void configureResourceChain(Resources.Chain properties, ResourceChainRegistration chain) { + Resources.Chain.Strategy strategy = properties.getStrategy(); if (properties.isCompressed()) { chain.addResolver(new EncodedResourceResolver()); } if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) { chain.addResolver(getVersionResourceResolver(strategy)); } - if (properties.isHtmlApplicationCache()) { + if ((properties instanceof org.springframework.boot.autoconfigure.web.ResourceProperties.Chain) + && ((org.springframework.boot.autoconfigure.web.ResourceProperties.Chain) properties) + .isHtmlApplicationCache()) { chain.addTransformer(new org.springframework.web.reactive.resource.AppCacheManifestTransformer()); } } - private ResourceResolver getVersionResourceResolver(ResourceProperties.Strategy properties) { + private ResourceResolver getVersionResourceResolver(Resources.Chain.Strategy properties) { VersionResourceResolver resolver = new VersionResourceResolver(); if (properties.getFixed().isEnabled()) { String version = properties.getFixed().getVersion(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index dc03212fe7..873d077277 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -35,8 +35,8 @@ import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvi import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; -import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format; @@ -106,11 +106,16 @@ public class WebFluxAutoConfiguration { public static class WelcomePageConfiguration { @Bean + @SuppressWarnings("deprecation") public RouterFunctionMapping welcomePageRouterFunctionMapping(ApplicationContext applicationContext, - WebFluxProperties webFluxProperties, ResourceProperties resourceProperties) { + WebFluxProperties webFluxProperties, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { + String[] staticLocations = resourceProperties.hasBeenCustomized() ? resourceProperties.getStaticLocations() + : webProperties.getResources().getStaticLocations(); WelcomePageRouterFunctionFactory factory = new WelcomePageRouterFunctionFactory( - new TemplateAvailabilityProviders(applicationContext), applicationContext, - resourceProperties.getStaticLocations(), webFluxProperties.getStaticPathPattern()); + new TemplateAvailabilityProviders(applicationContext), applicationContext, staticLocations, + webFluxProperties.getStaticPathPattern()); RouterFunction routerFunction = factory.createRouterFunction(); if (routerFunction != null) { RouterFunctionMapping routerFunctionMapping = new RouterFunctionMapping(routerFunction); @@ -122,14 +127,16 @@ public class WebFluxAutoConfiguration { } + @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class }) + @EnableConfigurationProperties({ org.springframework.boot.autoconfigure.web.ResourceProperties.class, + WebProperties.class, WebFluxProperties.class }) @Import({ EnableWebFluxConfiguration.class }) public static class WebFluxConfig implements WebFluxConfigurer { private static final Log logger = LogFactory.getLog(WebFluxConfig.class); - private final ResourceProperties resourceProperties; + private final Resources resourceProperties; private final WebFluxProperties webFluxProperties; @@ -143,12 +150,14 @@ public class WebFluxAutoConfiguration { private final ObjectProvider viewResolvers; - public WebFluxConfig(ResourceProperties resourceProperties, WebFluxProperties webFluxProperties, - ListableBeanFactory beanFactory, ObjectProvider resolvers, + public WebFluxConfig(org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties, WebFluxProperties webFluxProperties, ListableBeanFactory beanFactory, + ObjectProvider resolvers, ObjectProvider codecCustomizers, ObjectProvider resourceHandlerRegistrationCustomizer, ObjectProvider viewResolvers) { - this.resourceProperties = resourceProperties; + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); this.webFluxProperties = webFluxProperties; this.beanFactory = beanFactory; this.argumentResolvers = resolvers; @@ -190,7 +199,8 @@ public class WebFluxAutoConfiguration { private void configureResourceCaching(ResourceHandlerRegistration registration) { Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); - ResourceProperties.Cache.Cachecontrol cacheControl = this.resourceProperties.getCache().getCachecontrol(); + WebProperties.Resources.Cache.Cachecontrol cacheControl = this.resourceProperties.getCache() + .getCachecontrol(); if (cachePeriod != null && cacheControl.getMaxAge() == null) { cacheControl.setMaxAge(cachePeriod); } @@ -291,8 +301,13 @@ public class WebFluxAutoConfiguration { static class ResourceChainCustomizerConfiguration { @Bean - ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() { - return new ResourceChainResourceHandlerRegistrationCustomizer(); + @SuppressWarnings("deprecation") + ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { + Resources resources = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); + return new ResourceChainResourceHandlerRegistrationCustomizer(resources); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java index 86e87c38d0..53f05c1600 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java @@ -28,7 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.reactive.error.ErrorAttributes; @@ -82,7 +82,7 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept private final ErrorAttributes errorAttributes; - private final ResourceProperties resourceProperties; + private final Resources resources; private final TemplateAvailabilityProviders templateAvailabilityProviders; @@ -92,13 +92,35 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept private List viewResolvers = Collections.emptyList(); - public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, + /** + * Create a new {@code AbstractErrorWebExceptionHandler}. + * @param errorAttributes the error attributes + * @param resourceProperties the resource properties + * @param applicationContext the application context + * @deprecated since 2.4.0 in favor of + * {@link #AbstractErrorWebExceptionHandler(ErrorAttributes, Resources, ApplicationContext)} + */ + @Deprecated + public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + ApplicationContext applicationContext) { + this(errorAttributes, (Resources) resourceProperties, applicationContext); + } + + /** + * Create a new {@code AbstractErrorWebExceptionHandler}. + * @param errorAttributes the error attributes + * @param resources the resources configuration properties + * @param applicationContext the application context + * @since 2.4.0 + */ + public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, ApplicationContext applicationContext) { Assert.notNull(errorAttributes, "ErrorAttributes must not be null"); - Assert.notNull(resourceProperties, "ResourceProperties must not be null"); + Assert.notNull(resources, "Resources must not be null"); Assert.notNull(applicationContext, "ApplicationContext must not be null"); this.errorAttributes = errorAttributes; - this.resourceProperties = resourceProperties; + this.resources = resources; this.applicationContext = applicationContext; this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext); } @@ -224,7 +246,7 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept } private Resource resolveResource(String viewName) { - for (String location : this.resourceProperties.getStaticLocations()) { + for (String location : this.resources.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java index ea6cd7edf9..772ddc555b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java @@ -27,7 +27,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.reactive.error.ErrorAttributes; @@ -97,10 +97,27 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa * @param resourceProperties the resources configuration properties * @param errorProperties the error configuration properties * @param applicationContext the current application context + * @deprecated since 2.4.0 in favor of + * {@link #DefaultErrorWebExceptionHandler(ErrorAttributes, Resources, ErrorProperties, ApplicationContext)} */ - public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, + @Deprecated + public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { - super(errorAttributes, resourceProperties, applicationContext); + this(errorAttributes, (Resources) resourceProperties, errorProperties, applicationContext); + } + + /** + * Create a new {@code DefaultErrorWebExceptionHandler} instance. + * @param errorAttributes the error attributes + * @param resources the resources configuration properties + * @param errorProperties the error configuration properties + * @param applicationContext the current application context + * @since 2.4.0 + */ + public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, + ErrorProperties errorProperties, ApplicationContext applicationContext) { + super(errorAttributes, resources, applicationContext); this.errorProperties = errorProperties; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java index c7b3ca7a82..e522e7d738 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java @@ -25,8 +25,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.SearchStrategy; -import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; @@ -48,11 +48,13 @@ import org.springframework.web.reactive.result.view.ViewResolver; * @author Scott Frederick * @since 2.0.0 */ +@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @AutoConfigureBefore(WebFluxAutoConfiguration.class) -@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class }) +@EnableConfigurationProperties({ ServerProperties.class, + org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class }) public class ErrorWebFluxAutoConfiguration { private final ServerProperties serverProperties; @@ -65,10 +67,12 @@ public class ErrorWebFluxAutoConfiguration { @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, - ResourceProperties resourceProperties, ObjectProvider viewResolvers, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties, ObjectProvider viewResolvers, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes, - resourceProperties, this.serverProperties.getError(), applicationContext); + resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources(), + this.serverProperties.getError(), applicationContext); exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList())); exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index 8003912701..2929d1936b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -33,7 +33,6 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; @@ -50,9 +49,9 @@ import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvi import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; -import org.springframework.boot.autoconfigure.web.ResourceProperties; -import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain.Strategy; import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format; @@ -175,15 +174,17 @@ public class WebMvcAutoConfiguration { // Defined as a nested config to ensure WebMvcConfigurer is not read when not // on the classpath + @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) - @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) + @EnableConfigurationProperties({ WebMvcProperties.class, + org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); - private final ResourceProperties resourceProperties; + private final Resources resourceProperties; private final WebMvcProperties mvcProperties; @@ -197,12 +198,15 @@ public class WebMvcAutoConfiguration { final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; - public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, - ListableBeanFactory beanFactory, ObjectProvider messageConvertersProvider, + public WebMvcAutoConfigurationAdapter( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, + ObjectProvider messageConvertersProvider, ObjectProvider resourceHandlerRegistrationCustomizerProvider, ObjectProvider dispatcherServletPath, ObjectProvider> servletRegistrations) { - this.resourceProperties = resourceProperties; + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; @@ -234,7 +238,6 @@ public class WebMvcAutoConfiguration { } @Override - @SuppressWarnings("deprecation") public void configurePathMatch(PathMatchConfigurer configurer) { if (this.mvcProperties.getPathmatch() .getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) { @@ -259,7 +262,6 @@ public class WebMvcAutoConfiguration { } @Override - @SuppressWarnings("deprecation") public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties.getContentnegotiation(); configurer.favorPathExtension(contentnegotiation.isFavorPathExtension()); @@ -363,7 +365,7 @@ public class WebMvcAutoConfiguration { @EnableConfigurationProperties(WebProperties.class) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { - private final ResourceProperties resourceProperties; + private final Resources resourceProperties; private final WebMvcProperties mvcProperties; @@ -375,10 +377,13 @@ public class WebMvcAutoConfiguration { private ResourceLoader resourceLoader; - public EnableWebMvcConfiguration(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, - WebProperties webProperties, ObjectProvider mvcRegistrationsProvider, - ListableBeanFactory beanFactory) { - this.resourceProperties = resourceProperties; + @SuppressWarnings("deprecation") + public EnableWebMvcConfiguration( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebMvcProperties mvcProperties, WebProperties webProperties, + ObjectProvider mvcRegistrationsProvider, ListableBeanFactory beanFactory) { + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); this.mvcProperties = mvcProperties; this.webProperties = webProperties; this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); @@ -551,8 +556,12 @@ public class WebMvcAutoConfiguration { static class ResourceChainCustomizerConfiguration { @Bean - ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() { - return new ResourceChainResourceHandlerRegistrationCustomizer(); + @SuppressWarnings("deprecation") + ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { + return new ResourceChainResourceHandlerRegistrationCustomizer( + resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources()); } } @@ -565,17 +574,20 @@ public class WebMvcAutoConfiguration { static class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer { - @Autowired - private ResourceProperties resourceProperties = new ResourceProperties(); + private final Resources resourceProperties; + + ResourceChainResourceHandlerRegistrationCustomizer(Resources resourceProperties) { + this.resourceProperties = resourceProperties; + } @Override public void customize(ResourceHandlerRegistration registration) { - ResourceProperties.Chain properties = this.resourceProperties.getChain(); + Resources.Chain properties = this.resourceProperties.getChain(); configureResourceChain(properties, registration.resourceChain(properties.isCache())); } @SuppressWarnings("deprecation") - private void configureResourceChain(ResourceProperties.Chain properties, ResourceChainRegistration chain) { + private void configureResourceChain(Resources.Chain properties, ResourceChainRegistration chain) { Strategy strategy = properties.getStrategy(); if (properties.isCompressed()) { chain.addResolver(new EncodedResourceResolver()); @@ -583,12 +595,14 @@ public class WebMvcAutoConfiguration { if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) { chain.addResolver(getVersionResourceResolver(strategy)); } - if (properties.isHtmlApplicationCache()) { + if (properties instanceof org.springframework.boot.autoconfigure.web.ResourceProperties.Chain + && ((org.springframework.boot.autoconfigure.web.ResourceProperties.Chain) properties) + .isHtmlApplicationCache()) { chain.addTransformer(new org.springframework.web.servlet.resource.AppCacheManifestTransformer()); } } - private ResourceResolver getVersionResourceResolver(ResourceProperties.Strategy properties) { + private ResourceResolver getVersionResourceResolver(Strategy properties) { VersionResourceResolver resolver = new VersionResourceResolver(); if (properties.getFixed().isEnabled()) { String version = properties.getFixed().getVersion(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java index b1181106d8..fe581911d1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java @@ -25,7 +25,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.io.Resource; @@ -68,7 +68,7 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private ApplicationContext applicationContext; - private final ResourceProperties resourceProperties; + private final Resources resources; private final TemplateAvailabilityProviders templateAvailabilityProviders; @@ -78,21 +78,35 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { * Create a new {@link DefaultErrorViewResolver} instance. * @param applicationContext the source application context * @param resourceProperties resource properties + * @deprecated since 2.4.0 in favour of + * {@link #DefaultErrorViewResolver(ApplicationContext, Resources)} */ - public DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) { + @Deprecated + public DefaultErrorViewResolver(ApplicationContext applicationContext, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties) { + this(applicationContext, (Resources) resourceProperties); + } + + /** + * Create a new {@link DefaultErrorViewResolver} instance. + * @param applicationContext the source application context + * @param resources resource properties + * @since 2.4.0 + */ + public DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resources) { Assert.notNull(applicationContext, "ApplicationContext must not be null"); - Assert.notNull(resourceProperties, "ResourceProperties must not be null"); + Assert.notNull(resources, "Resources must not be null"); this.applicationContext = applicationContext; - this.resourceProperties = resourceProperties; + this.resources = resources; this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext); } - DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties, + DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resourceProperties, TemplateAvailabilityProviders templateAvailabilityProviders) { Assert.notNull(applicationContext, "ApplicationContext must not be null"); - Assert.notNull(resourceProperties, "ResourceProperties must not be null"); + Assert.notNull(resourceProperties, "Resources must not be null"); this.applicationContext = applicationContext; - this.resourceProperties = resourceProperties; + this.resources = resourceProperties; this.templateAvailabilityProviders = templateAvailabilityProviders; } @@ -116,7 +130,7 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { } private ModelAndView resolveResource(String viewName, Map model) { - for (String location : this.resourceProperties.getStaticLocations()) { + for (String location : this.resources.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java index b6cf2f1d95..2ad277d4e2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java @@ -46,8 +46,9 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; -import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties; @@ -88,7 +89,7 @@ import org.springframework.web.util.HtmlUtils; @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) // Load before the main WebMvcAutoConfiguration so that the error View is available @AutoConfigureBefore(WebMvcAutoConfiguration.class) -@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class }) +@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class }) public class ErrorMvcAutoConfiguration { private final ServerProperties serverProperties; @@ -121,24 +122,29 @@ public class ErrorMvcAutoConfiguration { return new PreserveErrorControllerTargetClassPostProcessor(); } + @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties({ org.springframework.boot.autoconfigure.web.ResourceProperties.class, + WebProperties.class, WebMvcProperties.class }) static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; - private final ResourceProperties resourceProperties; + private final Resources resources; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, - ResourceProperties resourceProperties) { + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { this.applicationContext = applicationContext; - this.resourceProperties = resourceProperties; + this.resources = webProperties.getResources().hasBeenCustomized() ? webProperties.getResources() + : resourceProperties; } @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean(ErrorViewResolver.class) DefaultErrorViewResolver conventionErrorViewResolver() { - return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); + return new DefaultErrorViewResolver(this.applicationContext, this.resources); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java index e056962f8a..72836e1ccd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java @@ -18,6 +18,8 @@ package org.springframework.boot.autoconfigure.web; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -46,27 +48,31 @@ class ConditionalOnEnabledResourceChainTests { assertThat(this.context.containsBean("foo")).isFalse(); } - @Test - void disabledExplicitly() { - load("spring.resources.chain.enabled:false"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void disabledExplicitly(String prefix) { + load(prefix + "chain.enabled:false"); assertThat(this.context.containsBean("foo")).isFalse(); } - @Test - void enabledViaMainEnabledFlag() { - load("spring.resources.chain.enabled:true"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void enabledViaMainEnabledFlag(String prefix) { + load(prefix + "chain.enabled:true"); assertThat(this.context.containsBean("foo")).isTrue(); } - @Test - void enabledViaFixedStrategyFlag() { - load("spring.resources.chain.strategy.fixed.enabled:true"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void enabledViaFixedStrategyFlag(String prefix) { + load(prefix + "chain.strategy.fixed.enabled:true"); assertThat(this.context.containsBean("foo")).isTrue(); } - @Test - void enabledViaContentStrategyFlag() { - load("spring.resources.chain.strategy.content.enabled:true"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void enabledViaContentStrategyFlag(String prefix) { + load(prefix + "chain.strategy.content.enabled:true"); assertThat(this.context.containsBean("foo")).isTrue(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java index c7922f63d3..0f269586c5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java @@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Stephane Nicoll */ +@Deprecated class ResourcePropertiesBindingTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -60,6 +61,7 @@ class ResourcePropertiesBindingTests { }; } + @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ResourceProperties.class) static class TestConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java index 4172490aef..82b92eed11 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java @@ -20,7 +20,6 @@ import java.time.Duration; import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.web.ResourceProperties.Cache; import org.springframework.http.CacheControl; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Stephane Nicoll * @author Kristine Jetzke */ +@Deprecated class ResourcePropertiesTests { private final ResourceProperties properties = new ResourceProperties(); @@ -78,7 +78,7 @@ class ResourcePropertiesTests { @Test void cacheControlAllPropertiesSet() { - Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + ResourceProperties.Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); properties.setMaxAge(Duration.ofSeconds(4)); properties.setCachePrivate(true); properties.setCachePublic(true); @@ -96,7 +96,7 @@ class ResourcePropertiesTests { @Test void invalidCacheControlCombination() { - Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + ResourceProperties.Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); properties.setMaxAge(Duration.ofSeconds(4)); properties.setNoStore(true); CacheControl cacheControl = properties.toHttpCacheControl(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesBindingTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesBindingTests.java new file mode 100644 index 0000000000..ce8d794285 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesBindingTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Binding tests for {@link WebProperties.Resources}. + * + * @author Stephane Nicoll + */ +class WebPropertiesResourcesBindingTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(TestConfiguration.class); + + @Test + void staticLocationsExpandArray() { + this.contextRunner + .withPropertyValues("spring.web.resources.static-locations[0]=classpath:/one/", + "spring.web.resources.static-locations[1]=classpath:/two", + "spring.web.resources.static-locations[2]=classpath:/three/", + "spring.web.resources.static-locations[3]=classpath:/four", + "spring.web.resources.static-locations[4]=classpath:/five/", + "spring.web.resources.static-locations[5]=classpath:/six") + .run(assertResourceProperties((properties) -> assertThat(properties.getStaticLocations()).contains( + "classpath:/one/", "classpath:/two/", "classpath:/three/", "classpath:/four/", + "classpath:/five/", "classpath:/six/"))); + } + + private ContextConsumer assertResourceProperties(Consumer consumer) { + return (context) -> { + assertThat(context).hasSingleBean(WebProperties.class); + consumer.accept(context.getBean(WebProperties.class).getResources()); + }; + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesTests.java new file mode 100644 index 0000000000..6606ca73e1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Cache; +import org.springframework.http.CacheControl; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebProperties.Resources}. + * + * @author Stephane Nicoll + * @author Kristine Jetzke + */ +@Deprecated +class WebPropertiesResourcesTests { + + private final Resources properties = new WebProperties().getResources(); + + @Test + void resourceChainNoCustomization() { + assertThat(this.properties.getChain().getEnabled()).isNull(); + } + + @Test + void resourceChainStrategyEnabled() { + this.properties.getChain().getStrategy().getFixed().setEnabled(true); + assertThat(this.properties.getChain().getEnabled()).isTrue(); + } + + @Test + void resourceChainEnabled() { + this.properties.getChain().setEnabled(true); + assertThat(this.properties.getChain().getEnabled()).isTrue(); + } + + @Test + void resourceChainDisabled() { + this.properties.getChain().setEnabled(false); + assertThat(this.properties.getChain().getEnabled()).isFalse(); + } + + @Test + void defaultStaticLocationsAllEndWithTrailingSlash() { + assertThat(this.properties.getStaticLocations()).allMatch((location) -> location.endsWith("/")); + } + + @Test + void customStaticLocationsAreNormalizedToEndWithTrailingSlash() { + this.properties.setStaticLocations(new String[] { "/foo", "/bar", "/baz/" }); + String[] actual = this.properties.getStaticLocations(); + assertThat(actual).containsExactly("/foo/", "/bar/", "/baz/"); + } + + @Test + void emptyCacheControl() { + CacheControl cacheControl = this.properties.getCache().getCachecontrol().toHttpCacheControl(); + assertThat(cacheControl).isNull(); + } + + @Test + void cacheControlAllPropertiesSet() { + Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + properties.setMaxAge(Duration.ofSeconds(4)); + properties.setCachePrivate(true); + properties.setCachePublic(true); + properties.setMustRevalidate(true); + properties.setNoTransform(true); + properties.setProxyRevalidate(true); + properties.setSMaxAge(Duration.ofSeconds(5)); + properties.setStaleIfError(Duration.ofSeconds(6)); + properties.setStaleWhileRevalidate(Duration.ofSeconds(7)); + CacheControl cacheControl = properties.toHttpCacheControl(); + assertThat(cacheControl.getHeaderValue()) + .isEqualTo("max-age=4, must-revalidate, no-transform, public, private, proxy-revalidate," + + " s-maxage=5, stale-if-error=6, stale-while-revalidate=7"); + } + + @Test + void invalidCacheControlCombination() { + Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + properties.setMaxAge(Duration.ofSeconds(4)); + properties.setNoStore(true); + CacheControl cacheControl = properties.toHttpCacheControl(); + assertThat(cacheControl.getHeaderValue()).isEqualTo("no-store"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 9f950b4ca4..6383339162 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -33,6 +33,8 @@ import javax.validation.ValidatorFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; @@ -167,16 +169,18 @@ class WebFluxAutoConfigurationTests { }); } - @Test - void shouldNotMapResourcesWhenDisabled() { - this.contextRunner.withPropertyValues("spring.resources.add-mappings:false") + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void shouldNotMapResourcesWhenDisabled(String prefix) { + this.contextRunner.withPropertyValues(prefix + ".add-mappings:false") .run((context) -> assertThat(context.getBean("resourceHandlerMapping")) .isNotInstanceOf(SimpleUrlHandlerMapping.class)); } - @Test - void resourceHandlerChainEnabled() { - this.contextRunner.withPropertyValues("spring.resources.chain.enabled:true").run((context) -> { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void resourceHandlerChainEnabled(String prefix) { + this.contextRunner.withPropertyValues(prefix + "chain.enabled:true").run((context) -> { SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); @@ -406,10 +410,11 @@ class WebFluxAutoConfigurationTests { }); } - @Test - void cachePeriod() { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void cachePeriod(String prefix) { Assertions.setExtractBareNamePropertyMethods(false); - this.contextRunner.withPropertyValues("spring.resources.cache.period:5").run((context) -> { + this.contextRunner.withPropertyValues(prefix + "cache.period:5").run((context) -> { Map handlerMap = getHandlerMap(context); assertThat(handlerMap).hasSize(2); for (Object handler : handlerMap.values()) { @@ -422,11 +427,12 @@ class WebFluxAutoConfigurationTests { Assertions.setExtractBareNamePropertyMethods(true); } - @Test - void cacheControl() { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void cacheControl(String prefix) { Assertions.setExtractBareNamePropertyMethods(false); - this.contextRunner.withPropertyValues("spring.resources.cache.cachecontrol.max-age:5", - "spring.resources.cache.cachecontrol.proxy-revalidate:true").run((context) -> { + this.contextRunner.withPropertyValues(prefix + "cache.cachecontrol.max-age:5", + prefix + "cache.cachecontrol.proxy-revalidate:true").run((context) -> { Map handlerMap = getHandlerMap(context); assertThat(handlerMap).hasSize(2); for (Object handler : handlerMap.values()) { @@ -449,14 +455,14 @@ class WebFluxAutoConfigurationTests { }); } - @Test - void welcomePageHandlerMapping() { - this.contextRunner.withPropertyValues("spring.resources.static-locations=classpath:/welcome-page/") - .run((context) -> { - assertThat(context).getBeans(RouterFunctionMapping.class).hasSize(2); - assertThat(context.getBean("welcomePageRouterFunctionMapping", HandlerMapping.class)).isNotNull() - .extracting("order").isEqualTo(1); - }); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void welcomePageHandlerMapping(String prefix) { + this.contextRunner.withPropertyValues(prefix + "static-locations=classpath:/welcome-page/").run((context) -> { + assertThat(context).getBeans(RouterFunctionMapping.class).hasSize(2); + assertThat(context.getBean("welcomePageRouterFunctionMapping", HandlerMapping.class)).isNotNull() + .extracting("order").isEqualTo(1); + }); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java index 3ad649c184..409296e865 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.ApplicationContext; @@ -62,7 +62,7 @@ class DefaultErrorWebExceptionHandlerTests { @Test void nonStandardErrorStatusCodeShouldNotFail() { ErrorAttributes errorAttributes = mock(ErrorAttributes.class); - ResourceProperties resourceProperties = new ResourceProperties(); + Resources resourceProperties = new Resources(); ErrorProperties errorProperties = new ErrorProperties(); ApplicationContext context = new AnnotationConfigReactiveWebApplicationContext(); given(errorAttributes.getErrorAttributes(any(), any())).willReturn(getErrorAttributes()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index 654ab8a5e3..c9f457e402 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -204,15 +204,17 @@ class WebMvcAutoConfigurationTests { }); } - @Test - void resourceHandlerMappingDisabled() { - this.contextRunner.withPropertyValues("spring.resources.add-mappings:false") + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void resourceHandlerMappingDisabled(String prefix) { + this.contextRunner.withPropertyValues(prefix + "add-mappings:false") .run((context) -> assertThat(getResourceMappingLocations(context)).hasSize(0)); } - @Test - void resourceHandlerChainEnabled() { - this.contextRunner.withPropertyValues("spring.resources.chain.enabled:true").run((context) -> { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void resourceHandlerChainEnabled(String prefix) { + this.contextRunner.withPropertyValues(prefix + "chain.enabled:true").run((context) -> { assertThat(getResourceResolvers(context, "/webjars/**")).hasSize(2); assertThat(getResourceTransformers(context, "/webjars/**")).hasSize(1); assertThat(getResourceResolvers(context, "/**")).extractingResultOf("getClass") @@ -222,11 +224,13 @@ class WebMvcAutoConfigurationTests { }); } - @Test - void resourceHandlerFixedStrategyEnabled() { - this.contextRunner.withPropertyValues("spring.resources.chain.strategy.fixed.enabled:true", - "spring.resources.chain.strategy.fixed.version:test", - "spring.resources.chain.strategy.fixed.paths:/**/*.js").run((context) -> { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void resourceHandlerFixedStrategyEnabled(String prefix) { + this.contextRunner + .withPropertyValues(prefix + "chain.strategy.fixed.enabled:true", + prefix + "chain.strategy.fixed.version:test", prefix + "chain.strategy.fixed.paths:/**/*.js") + .run((context) -> { assertThat(getResourceResolvers(context, "/webjars/**")).hasSize(3); assertThat(getResourceTransformers(context, "/webjars/**")).hasSize(2); assertThat(getResourceResolvers(context, "/**")).extractingResultOf("getClass").containsOnly( @@ -239,10 +243,11 @@ class WebMvcAutoConfigurationTests { }); } - @Test - void resourceHandlerContentStrategyEnabled() { - this.contextRunner.withPropertyValues("spring.resources.chain.strategy.content.enabled:true", - "spring.resources.chain.strategy.content.paths:/**,/*.png").run((context) -> { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void resourceHandlerContentStrategyEnabled(String prefix) { + this.contextRunner.withPropertyValues(prefix + "chain.strategy.content.enabled:true", + prefix + "chain.strategy.content.paths:/**,/*.png").run((context) -> { assertThat(getResourceResolvers(context, "/webjars/**")).hasSize(3); assertThat(getResourceTransformers(context, "/webjars/**")).hasSize(2); assertThat(getResourceResolvers(context, "/**")).extractingResultOf("getClass").containsOnly( @@ -255,25 +260,25 @@ class WebMvcAutoConfigurationTests { }); } - @Test + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) @SuppressWarnings("deprecation") - void resourceHandlerChainCustomized() { - this.contextRunner - .withPropertyValues("spring.resources.chain.enabled:true", "spring.resources.chain.cache:false", - "spring.resources.chain.strategy.content.enabled:true", - "spring.resources.chain.strategy.content.paths:/**,/*.png", - "spring.resources.chain.strategy.fixed.enabled:true", - "spring.resources.chain.strategy.fixed.version:test", - "spring.resources.chain.strategy.fixed.paths:/**/*.js", - "spring.resources.chain.html-application-cache:true", "spring.resources.chain.compressed:true") - .run((context) -> { + void resourceHandlerChainCustomized(String prefix) { + this.contextRunner.withPropertyValues(prefix + "chain.enabled:true", prefix + "chain.cache:false", + prefix + "chain.strategy.content.enabled:true", prefix + "chain.strategy.content.paths:/**,/*.png", + prefix + "chain.strategy.fixed.enabled:true", prefix + "chain.strategy.fixed.version:test", + prefix + "chain.strategy.fixed.paths:/**/*.js", prefix + "chain.html-application-cache:true", + prefix + "chain.compressed:true").run((context) -> { assertThat(getResourceResolvers(context, "/webjars/**")).hasSize(3); - assertThat(getResourceTransformers(context, "/webjars/**")).hasSize(2); + assertThat(getResourceTransformers(context, "/webjars/**")) + .hasSize(prefix.equals("spring.resources.") ? 2 : 1); assertThat(getResourceResolvers(context, "/**")).extractingResultOf("getClass").containsOnly( EncodedResourceResolver.class, VersionResourceResolver.class, PathResourceResolver.class); - assertThat(getResourceTransformers(context, "/**")).extractingResultOf("getClass").containsOnly( - CssLinkResourceTransformer.class, - org.springframework.web.servlet.resource.AppCacheManifestTransformer.class); + assertThat(getResourceTransformers(context, "/**")).extractingResultOf("getClass") + .containsOnly(prefix.equals("spring.resources.") + ? new Class[] { CssLinkResourceTransformer.class, + org.springframework.web.servlet.resource.AppCacheManifestTransformer.class } + : new Class[] { CssLinkResourceTransformer.class }); VersionResourceResolver resolver = (VersionResourceResolver) getResourceResolvers(context, "/**") .get(1); Map strategyMap = resolver.getStrategyMap(); @@ -597,19 +602,20 @@ class WebMvcAutoConfigurationTests { }; } - @Test - void welcomePageHandlerMappingIsAutoConfigured() { - this.contextRunner.withPropertyValues("spring.resources.static-locations:classpath:/welcome-page/") - .run((context) -> { - assertThat(context).hasSingleBean(WelcomePageHandlerMapping.class); - WelcomePageHandlerMapping bean = context.getBean(WelcomePageHandlerMapping.class); - assertThat(bean.getRootHandler()).isNotNull(); - }); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void welcomePageHandlerMappingIsAutoConfigured(String prefix) { + this.contextRunner.withPropertyValues(prefix + "static-locations:classpath:/welcome-page/").run((context) -> { + assertThat(context).hasSingleBean(WelcomePageHandlerMapping.class); + WelcomePageHandlerMapping bean = context.getBean(WelcomePageHandlerMapping.class); + assertThat(bean.getRootHandler()).isNotNull(); + }); } - @Test - void welcomePageHandlerIncludesCorsConfiguration() { - this.contextRunner.withPropertyValues("spring.resources.static-locations:classpath:/welcome-page/") + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void welcomePageHandlerIncludesCorsConfiguration(String prefix) { + this.contextRunner.withPropertyValues(prefix + "static-locations:classpath:/welcome-page/") .withUserConfiguration(CorsConfigurer.class).run((context) -> { WelcomePageHandlerMapping bean = context.getBean(WelcomePageHandlerMapping.class); UrlBasedCorsConfigurationSource source = (UrlBasedCorsConfigurationSource) ReflectionTestUtils @@ -732,9 +738,10 @@ class WebMvcAutoConfigurationTests { .run((context) -> assertThat(context).hasNotFailed()); } - @Test - void cachePeriod() { - this.contextRunner.withPropertyValues("spring.resources.cache.period:5").run(this::assertCachePeriod); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void cachePeriod(String prefix) { + this.contextRunner.withPropertyValues(prefix + "cache.period:5").run(this::assertCachePeriod); } private void assertCachePeriod(AssertableWebApplicationContext context) { @@ -749,10 +756,11 @@ class WebMvcAutoConfigurationTests { } } - @Test - void cacheControl() { - this.contextRunner.withPropertyValues("spring.resources.cache.cachecontrol.max-age:5", - "spring.resources.cache.cachecontrol.proxy-revalidate:true").run(this::assertCacheControl); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void cacheControl(String prefix) { + this.contextRunner.withPropertyValues(prefix + "cache.cachecontrol.max-age:5", + prefix + "cache.cachecontrol.proxy-revalidate:true").run(this::assertCacheControl); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolverTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolverTests.java index f0b6e4fbb2..98781dc002 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolverTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolverTests.java @@ -30,7 +30,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.Ordered; @@ -65,7 +65,7 @@ class DefaultErrorViewResolverTests { @Mock private TemplateAvailabilityProvider templateAvailabilityProvider; - private ResourceProperties resourceProperties; + private Resources resourcesProperties; private Map model = new HashMap<>(); @@ -75,25 +75,24 @@ class DefaultErrorViewResolverTests { void setup() { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.refresh(); - this.resourceProperties = new ResourceProperties(); + this.resourcesProperties = new Resources(); TemplateAvailabilityProviders templateAvailabilityProviders = new TestTemplateAvailabilityProviders( this.templateAvailabilityProvider); - this.resolver = new DefaultErrorViewResolver(applicationContext, this.resourceProperties, + this.resolver = new DefaultErrorViewResolver(applicationContext, this.resourcesProperties, templateAvailabilityProviders); } @Test void createWhenApplicationContextIsNullShouldThrowException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new DefaultErrorViewResolver(null, new ResourceProperties())) + assertThatIllegalArgumentException().isThrownBy(() -> new DefaultErrorViewResolver(null, new Resources())) .withMessageContaining("ApplicationContext must not be null"); } @Test void createWhenResourcePropertiesIsNullShouldThrowException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DefaultErrorViewResolver(mock(ApplicationContext.class), null)) - .withMessageContaining("ResourceProperties must not be null"); + .isThrownBy(() -> new DefaultErrorViewResolver(mock(ApplicationContext.class), (Resources) null)) + .withMessageContaining("Resources must not be null"); } @Test @@ -197,7 +196,7 @@ class DefaultErrorViewResolverTests { private void setResourceLocation(String path) { String packageName = getClass().getPackage().getName(); - this.resourceProperties + this.resourcesProperties .setStaticLocations(new String[] { "classpath:" + packageName.replace('.', '/') + path + "/" }); } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java index 70bdfc1926..19410eb690 100755 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/env/DevToolsPropertyDefaultsPostProcessor.java @@ -23,6 +23,8 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.devtools.logger.DevToolsLogFactory; import org.springframework.boot.devtools.restart.Restarter; import org.springframework.boot.devtools.system.DevToolsEnablementDeducer; @@ -85,7 +87,9 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro if (canAddProperties(environment)) { logger.info(LogMessage.format("Devtools property defaults active! Set '%s' to 'false' to disable", ENABLED)); - environment.getPropertySources().addLast(new MapPropertySource("devtools", PROPERTIES)); + Map properties = new HashMap<>(PROPERTIES); + properties.putAll(getResourceProperties(environment)); + environment.getPropertySources().addLast(new MapPropertySource("devtools", properties)); } if (isWebApplication(environment) && !environment.containsProperty(WEB_LOGGING)) { logger.info(LogMessage.format( @@ -95,6 +99,28 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro } } + private Map getResourceProperties(Environment environment) { + Map resourceProperties = new HashMap<>(); + String prefix = determineResourcePropertiesPrefix(environment); + resourceProperties.put(prefix + "cache.period", "0"); + resourceProperties.put(prefix + "chain.cache", "false"); + System.out.println(resourceProperties); + return resourceProperties; + } + + @SuppressWarnings("deprecation") + private String determineResourcePropertiesPrefix(Environment environment) { + if (ClassUtils.isPresent("org.springframework.boot.autoconfigure.web.ResourceProperties", + getClass().getClassLoader())) { + BindResult result = Binder.get(environment) + .bind("spring.resources", org.springframework.boot.autoconfigure.web.ResourceProperties.class); + if (result.isBound() && result.get().hasBeenCustomized()) { + return "spring.resources."; + } + } + return "spring.web.resources."; + } + private boolean isLocalApplication(ConfigurableEnvironment environment) { return environment.getPropertySources().get("remoteUrl") == null; } diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java index 312e74f7f6..99c3d9618f 100644 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java @@ -35,7 +35,8 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.devtools.classpath.ClassPathChangedEvent; import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher; @@ -113,7 +114,17 @@ class LocalDevToolsAutoConfigurationTests { @Test void resourceCachePeriodIsZero() throws Exception { this.context = getContext(() -> initializeAndRun(WebResourcesConfig.class)); - ResourceProperties properties = this.context.getBean(ResourceProperties.class); + Resources properties = this.context.getBean(WebProperties.class).getResources(); + assertThat(properties.getCache().getPeriod()).isZero(); + } + + @SuppressWarnings("deprecation") + @Test + void deprecatedResourceCachePeriodIsZeroWhenDeprecatedResourcePropertiesAreInUse() throws Exception { + this.context = getContext(() -> initializeAndRun(WebResourcesConfig.class, + Collections.singletonMap("spring.resources.add-mappings", false))); + Resources properties = this.context + .getBean(org.springframework.boot.autoconfigure.web.ResourceProperties.class); assertThat(properties.getCache().getPeriod()).isZero(); } @@ -282,9 +293,10 @@ class LocalDevToolsAutoConfigurationTests { } + @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) - @Import({ ServletWebServerFactoryAutoConfiguration.class, LocalDevToolsAutoConfiguration.class, - ResourceProperties.class }) + @Import({ ServletWebServerFactoryAutoConfiguration.class, LocalDevToolsAutoConfiguration.class, WebProperties.class, + org.springframework.boot.autoconfigure.web.ResourceProperties.class }) static class WebResourcesConfig { } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index c1a01de779..bdfd04ddbe 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -2608,7 +2608,7 @@ For instance, relocating all resources to `/resources/**` can be achieved as fol static-path-pattern: "/resources/**" ---- -You can also customize the static resource locations by using the configprop:spring.resources.static-locations[] property (replacing the default values with a list of directory locations). +You can also customize the static resource locations by using the configprop:spring.web.resources.static-locations[] property (replacing the default values with a list of directory locations). The root Servlet context path, `"/"`, is automatically added as a location as well. In addition to the "`standard`" static resource locations mentioned earlier, a special case is made for https://www.webjars.org/[Webjars content]. @@ -2631,12 +2631,13 @@ To use cache busting, the following configuration configures a cache busting sol [source,yaml,indent=0,subs="verbatim,quotes,attributes",configprops,configblocks] ---- spring: - resources: - chain: - strategy: - content: - enabled: true - paths: "/**" + web: + resources: + chain: + strategy: + content: + enabled: true + paths: "/**" ---- NOTE: Links to resources are rewritten in templates at runtime, thanks to a `ResourceUrlEncodingFilter` that is auto-configured for Thymeleaf and FreeMarker. @@ -2650,16 +2651,17 @@ A "fixed" strategy adds a static version string in the URL without changing the [source,yaml,indent=0,subs="verbatim,quotes,attributes",configprops,configblocks] ---- spring: - resources: - chain: - strategy: - content: - enabled: true - paths: "/**" - fixed: - enabled: true - paths: "/js/lib/" - version: "v12" + web: + resources: + chain: + strategy: + content: + enabled: true + paths: "/**" + fixed: + enabled: true + paths: "/js/lib/" + version: "v12" ---- With this configuration, JavaScript modules located under `"/js/lib/"` use a fixed versioning strategy (`"/v12/js/lib/mymodule.js"`), while other resources still use the content one (``). @@ -3134,7 +3136,7 @@ For instance, relocating all resources to `/resources/**` can be achieved as fol static-path-pattern: "/resources/**" ---- -You can also customize the static resource locations by using `spring.resources.static-locations`. +You can also customize the static resource locations by using `spring.web.resources.static-locations`. Doing so replaces the default values with a list of directory locations. If you do so, the default welcome page detection switches to your custom locations. So, if there is an `index.html` in any of your locations on startup, it is the home page of the application.