Introduce LocaleContextResolver in WebFlux

This commit introduces LocaleContextResolver interface, which is used
at ServerWebExchange level to resolve Locale, TimeZone and other i18n
related informations.

It follows Spring MVC locale resolution patterns with a few differences:
 - Only LocaleContextResolver is supported since LocaleResolver is less
   flexible
 - Support is implemented in the org.springframework.web.server.i18n
   package of spring-web module rather than in spring-webflux in order
   to be able to leverage it at ServerWebExchange level

2 implementations are provided:
 - FixedLocaleContextResolver
 - AcceptHeaderLocaleContextResolver

It can be configured with both functional or annotation-based APIs.

Issue: SPR-15036
This commit is contained in:
Sebastien Deleuze
2017-06-06 09:31:58 +02:00
parent 72a8868f84
commit e0e6736bc5
28 changed files with 853 additions and 27 deletions

View File

@@ -24,11 +24,13 @@ import java.util.function.Consumer;
import reactor.core.publisher.Mono;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.i18n.LocaleContextResolver;
/**
* Contract for an HTTP request-response interaction. Provides access to the HTTP
@@ -98,6 +100,11 @@ public interface ServerWebExchange {
*/
Mono<MultiValueMap<String, Part>> getMultipartData();
/**
* Return the {@link LocaleContext} using the configured {@link LocaleContextResolver}.
*/
LocaleContext getLocaleContext();
/**
* Returns {@code true} if the one of the {@code checkNotModified} methods
* in this contract were used and they returned true.

View File

@@ -22,6 +22,7 @@ import java.util.Optional;
import reactor.core.publisher.Mono;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
@@ -90,6 +91,11 @@ public class ServerWebExchangeDecorator implements ServerWebExchange {
return getDelegate().getPrincipal();
}
@Override
public LocaleContext getLocaleContext() {
return getDelegate().getLocaleContext();
}
@Override
public Mono<MultiValueMap<String, String>> getFormData() {
return getDelegate().getFormData();

View File

@@ -28,6 +28,7 @@ import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -45,6 +46,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionManager;
@@ -82,6 +84,8 @@ public class DefaultServerWebExchange implements ServerWebExchange {
private final Mono<WebSession> sessionMono;
private final LocaleContextResolver localeContextResolver;
private final Mono<MultiValueMap<String, String>> formDataMono;
private final Mono<MultiValueMap<String, Part>> multipartDataMono;
@@ -89,20 +93,19 @@ public class DefaultServerWebExchange implements ServerWebExchange {
private volatile boolean notModified;
/**
* Alternate constructor with a WebSessionManager parameter.
*/
public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer) {
WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer, LocaleContextResolver localeContextResolver) {
Assert.notNull(request, "'request' is required");
Assert.notNull(response, "'response' is required");
Assert.notNull(sessionManager, "'sessionManager' is required");
Assert.notNull(codecConfigurer, "'codecConfigurer' is required");
Assert.notNull(localeContextResolver, "'localeContextResolver' is required");
this.request = request;
this.response = response;
this.sessionMono = sessionManager.getSession(this).cache();
this.localeContextResolver = localeContextResolver;
this.formDataMono = initFormData(request, codecConfigurer);
this.multipartDataMono = initMultipartData(request, codecConfigurer);
}
@@ -190,6 +193,11 @@ public class DefaultServerWebExchange implements ServerWebExchange {
return Mono.empty();
}
@Override
public LocaleContext getLocaleContext() {
return this.localeContextResolver.resolveLocaleContext(this);
}
@Override
public Mono<MultiValueMap<String, String>> getFormData() {
return this.formDataMono;

View File

@@ -31,9 +31,11 @@ import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.handler.WebHandlerDecorator;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
@@ -83,6 +85,8 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
private ServerCodecConfigurer codecConfigurer;
private LocaleContextResolver localeContextResolver;
public HttpWebHandlerAdapter(WebHandler delegate) {
super(delegate);
@@ -119,6 +123,16 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
this.codecConfigurer = codecConfigurer;
}
/**
* Configure a custom {@link LocaleContextResolver}. The provided instance is set on
* each created {@link DefaultServerWebExchange}.
* <p>By default this is set to {@link org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver}.
* @param localeContextResolver the locale context resolver to use
*/
public void setLocaleContextResolver(LocaleContextResolver localeContextResolver) {
this.localeContextResolver = localeContextResolver;
}
/**
* Return the configured {@link ServerCodecConfigurer}.
*/
@@ -126,6 +140,13 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
return (this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create());
}
/**
* Return the configured {@link LocaleContextResolver}.
*/
public LocaleContextResolver getLocaleContextResolver() {
return (this.localeContextResolver != null ? this.localeContextResolver : new AcceptHeaderLocaleContextResolver());
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
@@ -140,7 +161,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
}
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer());
return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer(), getLocaleContextResolver());
}
private void logHandleFailure(Throwable ex) {

View File

@@ -27,6 +27,7 @@ import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebFilter;
@@ -68,6 +69,9 @@ public class WebHttpHandlerBuilder {
/** Well-known name for the ServerCodecConfigurer in the bean factory. */
public static final String SERVER_CODEC_CONFIGURER_BEAN_NAME = "serverCodecConfigurer";
/** Well-known name for the LocaleContextResolver in the bean factory. */
public static final String LOCALE_CONTEXT_RESOLVER_BEAN_NAME = "localeContextResolver";
private final WebHandler webHandler;
@@ -79,6 +83,8 @@ public class WebHttpHandlerBuilder {
private ServerCodecConfigurer codecConfigurer;
private LocaleContextResolver localeContextResolver;
/**
* Private constructor.
@@ -112,6 +118,8 @@ public class WebHttpHandlerBuilder {
* {@link #WEB_SESSION_MANAGER_BEAN_NAME}.
* <li>{@link ServerCodecConfigurer} [0..1] -- looked up by the name
* {@link #SERVER_CODEC_CONFIGURER_BEAN_NAME}.
*<li>{@link LocaleContextResolver} [0..1] -- looked up by the name
* {@link #LOCALE_CONTEXT_RESOLVER_BEAN_NAME}.
* </ul>
* @param context the application context to use for the lookup
* @return the prepared builder
@@ -144,6 +152,14 @@ public class WebHttpHandlerBuilder {
// Fall back on default
}
try {
builder.localeContextResolver(
context.getBean(LOCALE_CONTEXT_RESOLVER_BEAN_NAME, LocaleContextResolver.class));
}
catch (NoSuchBeanDefinitionException ex) {
// Fall back on default
}
return builder;
}
@@ -234,6 +250,16 @@ that's */
return this;
}
/**
* Configure the {@link LocaleContextResolver} to set on the
* {@link ServerWebExchange WebServerExchange}.
* @param localeContextResolver the locale context resolver
*/
public WebHttpHandlerBuilder localeContextResolver(LocaleContextResolver localeContextResolver) {
this.localeContextResolver = localeContextResolver;
return this;
}
/**
* Build the {@link HttpHandler}.
@@ -252,6 +278,9 @@ that's */
if (this.codecConfigurer != null) {
adapted.setCodecConfigurer(this.codecConfigurer);
}
if (this.localeContextResolver != null) {
adapted.setLocaleContextResolver(this.localeContextResolver);
}
return adapted;
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2002-2017 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
*
* http://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.web.server.i18n;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;
/**
* {@link LocaleContextResolver} implementation that simply uses the primary locale
* specified in the "Accept-Language" header of the HTTP request (that is,
* the locale sent by the client browser, normally that of the client's OS).
*
* <p>Note: Does not support {@code setLocale}, since the accept header
* can only be changed through changing the client's locale settings.
*
* @author Sebastien Deleuze
* @since 5.0
*/
public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver {
private final List<Locale> supportedLocales = new ArrayList<>(4);
private Locale defaultLocale;
/**
* Configure supported locales to check against the requested locales
* determined via {@link HttpHeaders#getAcceptLanguageAsLocales()}.
* @param locales the supported locales
*/
public void setSupportedLocales(List<Locale> locales) {
this.supportedLocales.clear();
if (locales != null) {
this.supportedLocales.addAll(locales);
}
}
/**
* Return the configured list of supported locales.
*/
public List<Locale> getSupportedLocales() {
return this.supportedLocales;
}
/**
* Configure a fixed default locale to fall back on if the request does not
* have an "Accept-Language" header (not set by default).
* @param defaultLocale the default locale to use
*/
public void setDefaultLocale(Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
/**
* The configured default locale, if any.
*/
@Nullable
public Locale getDefaultLocale() {
return this.defaultLocale;
}
@Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
List<Locale> acceptableLocales = request.getHeaders().getAcceptLanguageAsLocales();
if (this.defaultLocale != null && acceptableLocales.isEmpty()) {
return new SimpleLocaleContext(this.defaultLocale);
}
Locale requestLocale = acceptableLocales.isEmpty() ? null : acceptableLocales.get(0);
if (isSupportedLocale(requestLocale)) {
return new SimpleLocaleContext(requestLocale);
}
Locale supportedLocale = findSupportedLocale(request);
if (supportedLocale != null) {
return new SimpleLocaleContext(supportedLocale);
}
return (defaultLocale != null ? new SimpleLocaleContext(defaultLocale) : new SimpleLocaleContext(requestLocale));
}
private boolean isSupportedLocale(@Nullable Locale locale) {
if (locale == null) {
return false;
}
List<Locale> supportedLocales = getSupportedLocales();
return (supportedLocales.isEmpty() || supportedLocales.contains(locale));
}
@Nullable
private Locale findSupportedLocale(ServerHttpRequest request) {
List<Locale> requestLocales = request.getHeaders().getAcceptLanguageAsLocales();
for (Locale locale : requestLocales) {
if (getSupportedLocales().contains(locale)) {
return locale;
}
}
return null;
}
@Override
public void setLocaleContext(ServerWebExchange exchange, @Nullable LocaleContext locale) {
throw new UnsupportedOperationException(
"Cannot change HTTP accept header - use a different locale context resolution strategy");
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2002-2013 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
*
* http://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.web.server.i18n;
import java.util.Locale;
import java.util.TimeZone;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* {@link LocaleContextResolver} implementation
* that always returns a fixed default locale and optionally time zone.
* Default is the current JVM's default locale.
*
* <p>Note: Does not support {@code setLocale(Context)}, as the fixed
* locale and time zone cannot be changed.
*
* @author Sebastien Deleuze
* @since 5.0
*/
public class FixedLocaleContextResolver implements LocaleContextResolver {
private final Locale locale;
private final TimeZone timeZone;
/**
* Create a default FixedLocaleResolver, exposing a configured default
* locale (or the JVM's default locale as fallback).
*/
public FixedLocaleContextResolver() {
this(Locale.getDefault());
}
/**
* Create a FixedLocaleResolver that exposes the given locale.
* @param locale the locale to expose
*/
public FixedLocaleContextResolver(Locale locale) {
this(locale, null);
}
/**
* Create a FixedLocaleResolver that exposes the given locale and time zone.
* @param locale the locale to expose
* @param timeZone the time zone to expose
*/
public FixedLocaleContextResolver(Locale locale, @Nullable TimeZone timeZone) {
Assert.notNull(locale, "Locale must not be null");
this.locale = locale;
this.timeZone = timeZone;
}
@Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
return new TimeZoneAwareLocaleContext() {
@Override
public Locale getLocale() {
return locale;
}
@Override
public TimeZone getTimeZone() {
return timeZone;
}
};
}
@Override
public void setLocaleContext(ServerWebExchange exchange, @Nullable LocaleContext localeContext) {
throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale context resolution strategy");
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2002-2013 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
*
* http://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.web.server.i18n;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;
/**
* Interface for web-based locale context resolution strategies that allows
* for both locale context resolution via the request and locale context modification
* via the HTTP exchange.
*
* <p>The {@link org.springframework.context.i18n.LocaleContext} object can potentially
* includes associated time zone and other locale related information.
*
* @author Sebastien Deleuze
* @since 5.0
* @see LocaleContext
*/
public interface LocaleContextResolver {
/**
* Resolve the current locale context via the given exchange.
*
* <p>The returned context may be a
* {@link org.springframework.context.i18n.TimeZoneAwareLocaleContext},
* containing a locale with associated time zone information.
* Simply apply an {@code instanceof} check and downcast accordingly.
* <p>Custom resolver implementations may also return extra settings in
* the returned context, which again can be accessed through downcasting.
* @param exchange current server exchange
* @return the current locale context (never {@code null}
*/
LocaleContext resolveLocaleContext(ServerWebExchange exchange);
/**
* Set the current locale context to the given one,
* potentially including a locale with associated time zone information.
* @param exchange current server exchange
* @param localeContext the new locale context, or {@code null} to clear the locale
* @throws UnsupportedOperationException if the LocaleResolver implementation
* does not support dynamic changing of the locale or time zone
* @see org.springframework.context.i18n.SimpleLocaleContext
* @see org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext
*/
void setLocaleContext(ServerWebExchange exchange, @Nullable LocaleContext localeContext);
}

View File

@@ -0,0 +1,8 @@
/**
* Locale related support classes.
* Provides standard LocaleContextResolver implementations.
*/
@NonNullApi
package org.springframework.web.server.i18n;
import org.springframework.lang.NonNullApi;