diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index e63cf80b6c3..0dd0b48e8dc 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -72,7 +72,7 @@ public class DocumentConfigurationProperties extends DefaultTask { .addSection("mail").withKeyPrefixes("spring.mail", "spring.sendgrid").addSection("cache") .withKeyPrefixes("spring.cache").addSection("server").withKeyPrefixes("server").addSection("web") .withKeyPrefixes("spring.hateoas", "spring.http", "spring.servlet", "spring.jersey", "spring.mvc", - "spring.resources", "spring.webflux") + "spring.resources", "spring.web", "spring.webflux") .addSection("json").withKeyPrefixes("spring.jackson", "spring.gson").addSection("rsocket") .withKeyPrefixes("spring.rsocket").addSection("templating") .withKeyPrefixes("spring.freemarker", "spring.groovy", "spring.mustache", "spring.thymeleaf") 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 new file mode 100644 index 00000000000..bf1b93ada22 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java @@ -0,0 +1,74 @@ +/* + * 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.Locale; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties Configuration properties} for general web concerns. + * + * @author Andy Wilkinson + * @since 2.4.0 + */ +@ConfigurationProperties("spring.web") +public class WebProperties { + + /** + * Locale to use. By default, this locale is overridden by the "Accept-Language" + * header. + */ + private Locale locale; + + /** + * Define how the locale should be resolved. + */ + private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER; + + public Locale getLocale() { + return this.locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public LocaleResolver getLocaleResolver() { + return this.localeResolver; + } + + public void setLocaleResolver(LocaleResolver localeResolver) { + this.localeResolver = localeResolver; + } + + public enum LocaleResolver { + + /** + * Always use the configured locale. + */ + FIXED, + + /** + * Use the "Accept-Language" header or the configured locale if the header is not + * set. + */ + ACCEPT_HEADER + + } + +} 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 afe5b6ebf07..dc03212fe72 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 @@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigura 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.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format; @@ -69,6 +70,8 @@ import org.springframework.web.reactive.result.method.annotation.ArgumentResolve import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; +import org.springframework.web.server.i18n.FixedLocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver; /** @@ -216,15 +219,19 @@ public class WebFluxAutoConfiguration { * Configuration equivalent to {@code @EnableWebFlux}. */ @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { private final WebFluxProperties webFluxProperties; + private final WebProperties webProperties; + private final WebFluxRegistrations webFluxRegistrations; - public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, + public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, WebProperties webProperties, ObjectProvider webFluxRegistrations) { this.webFluxProperties = webFluxProperties; + this.webProperties = webProperties; this.webFluxRegistrations = webFluxRegistrations.getIfUnique(); } @@ -269,7 +276,12 @@ public class WebFluxAutoConfiguration { @Override @ConditionalOnMissingBean public LocaleContextResolver localeContextResolver() { - return super.localeContextResolver(); + if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) { + return new FixedLocaleContextResolver(this.webProperties.getLocale()); + } + AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver(); + localeContextResolver.setDefaultLocale(this.webProperties.getLocale()); + return localeContextResolver; } } 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 134413dc5fe..80039127010 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 @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.ListIterator; +import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -51,6 +52,7 @@ 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.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format; @@ -358,23 +360,27 @@ public class WebMvcAutoConfiguration { * Configuration equivalent to {@code @EnableWebMvc}. */ @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { private final ResourceProperties resourceProperties; private final WebMvcProperties mvcProperties; + private final WebProperties webProperties; + private final ListableBeanFactory beanFactory; private final WebMvcRegistrations mvcRegistrations; private ResourceLoader resourceLoader; - public EnableWebMvcConfiguration(ResourceProperties resourceProperties, - ObjectProvider mvcPropertiesProvider, - ObjectProvider mvcRegistrationsProvider, ListableBeanFactory beanFactory) { + public EnableWebMvcConfiguration(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, + WebProperties webProperties, ObjectProvider mvcRegistrationsProvider, + ListableBeanFactory beanFactory) { this.resourceProperties = resourceProperties; - this.mvcProperties = mvcPropertiesProvider.getIfAvailable(); + this.mvcProperties = mvcProperties; + this.webProperties = webProperties; this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); this.beanFactory = beanFactory; } @@ -426,13 +432,18 @@ public class WebMvcAutoConfiguration { @Override @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = "spring.mvc", name = "locale", matchIfMissing = true) + @SuppressWarnings("deprecation") public LocaleResolver localeResolver() { + if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) { + return new FixedLocaleResolver(this.webProperties.getLocale()); + } if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); - localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); + Locale locale = (this.webProperties.getLocale() != null) ? this.webProperties.getLocale() + : this.mvcProperties.getLocale(); + localeResolver.setDefaultLocale(locale); return localeResolver; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java index f883fff7a05..b8f340543f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java @@ -121,6 +121,8 @@ public class WebMvcProperties { this.messageCodesResolverFormat = messageCodesResolverFormat; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.web.locale") public Locale getLocale() { return this.locale; } @@ -129,6 +131,8 @@ public class WebMvcProperties { this.locale = locale; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.web.locale-resolver") public LocaleResolver getLocaleResolver() { return this.localeResolver; } @@ -543,6 +547,12 @@ public class WebMvcProperties { } + /** + * Locale resolution options. + * @deprecated since 2.4.0 in favor of + * {@link org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver} + */ + @Deprecated public enum LocaleResolver { /** 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 17fb6d56ef8..9f950b4ca46 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 @@ -44,6 +44,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.i18n.LocaleContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; @@ -54,7 +55,10 @@ import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; @@ -74,6 +78,7 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; +import org.springframework.web.server.i18n.FixedLocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver; import org.springframework.web.util.pattern.PathPattern; @@ -454,6 +459,54 @@ class WebFluxAutoConfigurationTests { }); } + @Test + void defaultLocaleContextResolver() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(LocaleContextResolver.class); + LocaleContextResolver resolver = context.getBean(LocaleContextResolver.class); + assertThat(((AcceptHeaderLocaleContextResolver) resolver).getDefaultLocale()).isNull(); + }); + } + + @Test + void whenFixedLocalContextResolverIsUsedThenAcceptLanguagesHeaderIsIgnored() { + this.contextRunner.withPropertyValues("spring.web.locale:en_UK", "spring.web.locale-resolver=fixed") + .run((context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/") + .acceptLanguageAsLocales(StringUtils.parseLocaleString("nl_NL")).build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class); + assertThat(localeContextResolver).isInstanceOf(FixedLocaleContextResolver.class); + LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange); + assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("en_UK")); + }); + } + + @Test + void whenAcceptHeaderLocaleContextResolverIsUsedThenAcceptLanguagesHeaderIsHonoured() { + this.contextRunner.withPropertyValues("spring.web.locale:en_UK").run((context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/") + .acceptLanguageAsLocales(StringUtils.parseLocaleString("nl_NL")).build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class); + assertThat(localeContextResolver).isInstanceOf(AcceptHeaderLocaleContextResolver.class); + LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange); + assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("nl_NL")); + }); + } + + @Test + void whenAcceptHeaderLocaleContextResolverIsUsedAndHeaderIsAbsentThenConfiguredLocaleIsUsed() { + this.contextRunner.withPropertyValues("spring.web.locale:en_UK").run((context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class); + assertThat(localeContextResolver).isInstanceOf(AcceptHeaderLocaleContextResolver.class); + LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange); + assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("en_UK")); + }); + } + @Test void customLocaleContextResolver() { this.contextRunner.withUserConfiguration(LocaleContextResolverConfiguration.class) 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 53ec1b6153a..654ab8a5e30 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 @@ -38,6 +38,8 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.ValidatorFactory; 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.context.PropertyPlaceholderAutoConfiguration; @@ -289,10 +291,11 @@ class WebMvcAutoConfigurationTests { }); } - @Test - void overrideLocale() { - this.contextRunner.withPropertyValues("spring.mvc.locale:en_UK", "spring.mvc.locale-resolver=fixed") - .run((loader) -> { + @ParameterizedTest + @ValueSource(strings = { "mvc", "web" }) + void overrideLocale(String mvcOrWeb) { + this.contextRunner.withPropertyValues("spring." + mvcOrWeb + ".locale:en_UK", + "spring." + mvcOrWeb + ".locale-resolver=fixed").run((loader) -> { // mock request and set user preferred locale MockHttpServletRequest request = new MockHttpServletRequest(); request.addPreferredLocale(StringUtils.parseLocaleString("nl_NL")); @@ -306,9 +309,10 @@ class WebMvcAutoConfigurationTests { }); } - @Test - void useAcceptHeaderLocale() { - this.contextRunner.withPropertyValues("spring.mvc.locale:en_UK").run((loader) -> { + @ParameterizedTest + @ValueSource(strings = { "mvc", "web" }) + void useAcceptHeaderLocale(String mvcOrWeb) { + this.contextRunner.withPropertyValues("spring." + mvcOrWeb + ".locale:en_UK").run((loader) -> { // mock request and set user preferred locale MockHttpServletRequest request = new MockHttpServletRequest(); request.addPreferredLocale(StringUtils.parseLocaleString("nl_NL")); @@ -321,9 +325,10 @@ class WebMvcAutoConfigurationTests { }); } - @Test - void useDefaultLocaleIfAcceptHeaderNoSet() { - this.contextRunner.withPropertyValues("spring.mvc.locale:en_UK").run((context) -> { + @ParameterizedTest + @ValueSource(strings = { "mvc", "web" }) + void useDefaultLocaleIfAcceptHeaderNoSet(String mvcOrWeb) { + this.contextRunner.withPropertyValues("spring." + mvcOrWeb + ".locale:en_UK").run((context) -> { // mock request and set user preferred locale MockHttpServletRequest request = new MockHttpServletRequest(); LocaleResolver localeResolver = context.getBean(LocaleResolver.class);