Commit 1c4b4cb0 authored by Andy Wilkinson's avatar Andy Wilkinson

Provide config properties for configuring WebFlux's locale resolution

Previously, the locale context resolver used with WebFlux could only be
configured by provided a custom LocaleContextResolver bean. By
constrast, when using Spring MVC, the spring.mvc.locale and
spring.mvc.locale-resolver properties could be used to configure the
locale and the resolver (fixed or Accept header) respectively.

This commit introduces spring.web.locale and spring.web.locale-resolver
properties and deprecates their spring.mvc equivalents. The new
properties can be used to configure locale resolution with either
Spring MVC or WebFlux.

Closes gh-23449
parent ef89eb6d
......@@ -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")
......
/*
* 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
}
}
......@@ -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> 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;
}
}
......
......@@ -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<WebMvcProperties> mvcPropertiesProvider,
ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider, ListableBeanFactory beanFactory) {
public EnableWebMvcConfiguration(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
WebProperties webProperties, ObjectProvider<WebMvcRegistrations> 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;
}
......
......@@ -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 {
/**
......
......@@ -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)
......
......@@ -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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment