提交 e0e6736b 编写于 作者: S Sebastien Deleuze

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
上级 72a8868f
......@@ -52,6 +52,7 @@ public class SimpleTimeZoneAwareLocaleContext extends SimpleLocaleContext implem
}
@Override
public TimeZone getTimeZone() {
return this.timeZone;
}
......
......@@ -18,6 +18,7 @@ package org.springframework.mock.http.server.reactive;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.DefaultWebSessionManager;
/**
......@@ -37,7 +38,7 @@ public class MockServerWebExchange extends ServerWebExchangeDecorator {
public MockServerWebExchange(MockServerHttpRequest request) {
super(new DefaultServerWebExchange(
request, new MockServerHttpResponse(), new DefaultWebSessionManager(),
ServerCodecConfigurer.create()));
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver()));
}
......
......@@ -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.
......
......@@ -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();
......
......@@ -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;
......
......@@ -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) {
......
......@@ -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;
}
......
/*
* 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");
}
}
/*
* 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");
}
}
/*
* 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);
}
/**
* Locale related support classes.
* Provides standard LocaleContextResolver implementations.
*/
@NonNullApi
package org.springframework.web.server.i18n;
import org.springframework.lang.NonNullApi;
......@@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import org.reactivestreams.Publisher;
......@@ -260,6 +261,13 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest {
*/
B acceptCharset(Charset... acceptableCharsets);
/**
* Set the list of acceptable {@linkplain Locale locales}, as specified
* by the {@code Accept-Languages} header.
* @param acceptableLocales the acceptable locales
*/
B acceptLanguageAsLocales(Locale... acceptableLocales);
/**
* Set the value of the {@code If-Modified-Since} header.
* <p>The date should be specified as the number of milliseconds since
......@@ -420,6 +428,12 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest {
return this;
}
@Override
public BodyBuilder acceptLanguageAsLocales(Locale... acceptableLocales) {
this.headers.setAcceptLanguageAsLocales(Arrays.asList(acceptableLocales));
return this;
}
@Override
public BodyBuilder contentLength(long contentLength) {
this.headers.setContentLength(contentLength);
......
......@@ -18,6 +18,7 @@ package org.springframework.mock.http.server.reactive.test;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.DefaultWebSessionManager;
/**
......@@ -36,7 +37,8 @@ public class MockServerWebExchange extends ServerWebExchangeDecorator {
public MockServerWebExchange(MockServerHttpRequest request) {
super(new DefaultServerWebExchange(
request, new MockServerHttpResponse(), new DefaultWebSessionManager(), ServerCodecConfigurer.create()));
request, new MockServerHttpResponse(), new DefaultWebSessionManager(),
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver()));
}
......
/*
* 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.Arrays;
import java.util.Collections;
import java.util.Locale;
import org.junit.Test;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import static java.util.Locale.*;
import static org.junit.Assert.assertEquals;
/**
* Unit tests for {@link AcceptHeaderLocaleContextResolver}.
*
* @author Sebastien Deleuze
*/
public class AcceptHeaderLocaleContextResolverTests {
private AcceptHeaderLocaleContextResolver resolver = new AcceptHeaderLocaleContextResolver();
@Test
public void resolve() throws Exception {
assertEquals(CANADA, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
assertEquals(US, this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale());
}
@Test
public void resolvePreferredSupported() throws Exception {
this.resolver.setSupportedLocales(Collections.singletonList(CANADA));
assertEquals(CANADA, this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale());
}
@Test
public void resolvePreferredNotSupported() throws Exception {
this.resolver.setSupportedLocales(Collections.singletonList(CANADA));
assertEquals(US, this.resolver.resolveLocaleContext(exchange(US, UK)).getLocale());
}
@Test
public void resolvePreferredNotSupportedWithDefault() {
this.resolver.setSupportedLocales(Arrays.asList(US, JAPAN));
this.resolver.setDefaultLocale(JAPAN);
MockServerWebExchange exchange = new MockServerWebExchange(MockServerHttpRequest
.get("/")
.acceptLanguageAsLocales(KOREA)
.build());
assertEquals(JAPAN, this.resolver.resolveLocaleContext(exchange).getLocale());
}
@Test
public void defaultLocale() throws Exception {
this.resolver.setDefaultLocale(JAPANESE);
MockServerWebExchange exchange = new MockServerWebExchange(MockServerHttpRequest
.get("/")
.build());
assertEquals(JAPANESE, this.resolver.resolveLocaleContext(exchange).getLocale());
exchange = new MockServerWebExchange(MockServerHttpRequest
.get("/")
.acceptLanguageAsLocales(US)
.build());
assertEquals(US, this.resolver.resolveLocaleContext(exchange).getLocale());
}
private ServerWebExchange exchange(Locale... locales) {
return new MockServerWebExchange(MockServerHttpRequest
.get("")
.acceptLanguageAsLocales(locales)
.build());
}
}
package org.springframework.web.server.i18n;
import java.time.ZoneId;
import java.util.Locale;
import java.util.TimeZone;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import static java.util.Locale.CANADA;
import static java.util.Locale.FRANCE;
import static java.util.Locale.US;
import static org.junit.Assert.assertEquals;
/**
* Unit tests for {@link FixedLocaleContextResolver}.
*
* @author Sebastien Deleuze
*/
public class FixedLocaleContextResolverTests {
private FixedLocaleContextResolver resolver;
@Before
public void setup() {
Locale.setDefault(US);
}
@Test
public void resolveDefaultLocale() {
this.resolver = new FixedLocaleContextResolver();
assertEquals(US, this.resolver.resolveLocaleContext(exchange()).getLocale());
assertEquals(US, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
}
@Test
public void resolveCustomizedLocale() {
this.resolver = new FixedLocaleContextResolver(FRANCE);
assertEquals(FRANCE, this.resolver.resolveLocaleContext(exchange()).getLocale());
assertEquals(FRANCE, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
}
@Test
public void resolveCustomizedAndTimeZoneLocale() {
TimeZone timeZone = TimeZone.getTimeZone(ZoneId.of("UTC"));
this.resolver = new FixedLocaleContextResolver(FRANCE, timeZone);
TimeZoneAwareLocaleContext context = (TimeZoneAwareLocaleContext)this.resolver.resolveLocaleContext(exchange());
assertEquals(FRANCE, context.getLocale());
assertEquals(timeZone, context.getTimeZone());
}
private ServerWebExchange exchange(Locale... locales) {
return new MockServerWebExchange(MockServerHttpRequest
.get("")
.acceptLanguageAsLocales(locales)
.build());
}
}
......@@ -32,6 +32,7 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
......@@ -60,7 +61,8 @@ public class DefaultWebSessionManagerTests {
MockServerHttpRequest request = MockServerHttpRequest.get("/path").build();
MockServerHttpResponse response = new MockServerHttpResponse();
this.exchange = new DefaultServerWebExchange(request, response, this.manager, ServerCodecConfigurer.create());
this.exchange = new DefaultServerWebExchange(request, response, this.manager,
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
}
......
......@@ -58,9 +58,11 @@ import org.springframework.web.reactive.result.method.annotation.ResponseBodyRes
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
/**
* The main class for Spring WebFlux configuration.
......@@ -266,6 +268,18 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
return serverCodecConfigurer;
}
/**
* Override to plug a sub-class of {@link LocaleContextResolver}.
*/
protected LocaleContextResolver createLocaleContextResolver() {
return new AcceptHeaderLocaleContextResolver();
}
@Bean
public LocaleContextResolver localeContextResolver() {
return createLocaleContextResolver();
}
/**
* Override to configure the HTTP message readers and writers to use.
*/
......
......@@ -27,9 +27,11 @@ import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.Assert;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
/**
* Default implementation of {@link HandlerStrategies.Builder}.
......@@ -47,6 +49,8 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
private final List<WebExceptionHandler> exceptionHandlers = new ArrayList<>();
private LocaleContextResolver localeContextResolver;
public DefaultHandlerStrategiesBuilder() {
this.codecConfigurer.registerDefaults(false);
......@@ -55,6 +59,7 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
public void defaultConfiguration() {
this.codecConfigurer.registerDefaults(true);
exceptionHandler(new ResponseStatusExceptionHandler());
localeContextResolver(new AcceptHeaderLocaleContextResolver());
}
@Override
......@@ -94,11 +99,18 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
return this;
}
@Override
public HandlerStrategies.Builder localeContextResolver(LocaleContextResolver localeContextResolver) {
Assert.notNull(localeContextResolver, "'localeContextResolver' must not be null");
this.localeContextResolver = localeContextResolver;
return this;
}
@Override
public HandlerStrategies build() {
return new DefaultHandlerStrategies(this.codecConfigurer.getReaders(),
this.codecConfigurer.getWriters(), this.viewResolvers, this.webFilters,
this.exceptionHandlers);
this.exceptionHandlers, this.localeContextResolver);
}
......@@ -114,18 +126,23 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
private final List<WebExceptionHandler> exceptionHandlers;
private final LocaleContextResolver localeContextResolver;
public DefaultHandlerStrategies(
List<HttpMessageReader<?>> messageReaders,
List<HttpMessageWriter<?>> messageWriters,
List<ViewResolver> viewResolvers,
List<WebFilter> webFilters,
List<WebExceptionHandler> exceptionHandlers) {
List<WebExceptionHandler> exceptionHandlers,
LocaleContextResolver localeContextResolver) {
this.messageReaders = unmodifiableCopy(messageReaders);
this.messageWriters = unmodifiableCopy(messageWriters);
this.viewResolvers = unmodifiableCopy(viewResolvers);
this.webFilters = unmodifiableCopy(webFilters);
this.exceptionHandlers = unmodifiableCopy(exceptionHandlers);
this.localeContextResolver = localeContextResolver;
}
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
......@@ -156,6 +173,11 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
public List<WebExceptionHandler> exceptionHandlers() {
return this.exceptionHandlers;
}
@Override
public LocaleContextResolver localeContextResolver() {
return this.localeContextResolver;
}
}
}
......@@ -20,7 +20,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Stream;
......@@ -156,7 +155,7 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder {
ServerHttpResponse response = exchange.getResponse();
writeStatusAndHeaders(response);
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
Locale locale = resolveLocale(exchange);
Locale locale = exchange.getLocaleContext().getLocale();
Stream<ViewResolver> viewResolverStream = context.viewResolvers().stream();
return Flux.fromStream(viewResolverStream)
......@@ -167,11 +166,6 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder {
.flatMap(view -> view.render(model(), contentType, exchange));
}
private Locale resolveLocale(ServerWebExchange exchange) {
List<Locale> locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales();
return locales.isEmpty() ? Locale.getDefault() : locales.get(0);
}
}
}
......@@ -24,6 +24,7 @@ import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebFilter;
......@@ -35,6 +36,7 @@ import org.springframework.web.server.WebFilter;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 5.0
* @see RouterFunctions#toHttpHandler(RouterFunction, HandlerStrategies)
*/
......@@ -72,6 +74,12 @@ public interface HandlerStrategies {
*/
List<WebExceptionHandler> exceptionHandlers();
/**
* Return the {@link LocaleContextResolver} to be used for resolving locale context.
* @return the locale context resolver
*/
LocaleContextResolver localeContextResolver();
// Static methods
......@@ -146,6 +154,13 @@ public interface HandlerStrategies {
*/
Builder exceptionHandler(WebExceptionHandler exceptionHandler);
/**
* Add the given locale context resolver to this builder.
* @param localeContextResolver the locale context resolver to add
* @return this builder
*/
Builder localeContextResolver(LocaleContextResolver localeContextResolver);
/**
* Builds the {@link HandlerStrategies}.
* @return the built strategies
......
......@@ -199,6 +199,7 @@ public abstract class RouterFunctions {
return WebHttpHandlerBuilder.webHandler(webHandler)
.filters(strategies.webFilters())
.exceptionHandlers(strategies.exceptionHandlers())
.localeContextResolver(strategies.localeContextResolver())
.build();
}
......
......@@ -25,6 +25,8 @@ import java.util.TimeZone;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
......@@ -84,9 +86,10 @@ public class RequestContext {
this.model = model;
this.messageSource = messageSource;
List<Locale> locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales();
this.locale = locales.isEmpty() ? Locale.getDefault() : locales.get(0);
this.timeZone = TimeZone.getDefault(); // TODO
LocaleContext localeContext = exchange.getLocaleContext();
this.locale = localeContext.getLocale();
this.timeZone = (localeContext instanceof TimeZoneAwareLocaleContext ?
((TimeZoneAwareLocaleContext)localeContext).getTimeZone() : TimeZone.getDefault());
this.defaultHtmlEscape = null; // TODO
this.dataValueProcessor = dataValueProcessor;
......
......@@ -201,8 +201,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport
Model model = result.getModel();
MethodParameter parameter = result.getReturnTypeSource();
List<Locale> locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales();
Locale locale = locales.isEmpty() ? Locale.getDefault() : locales.get(0);
Locale locale = exchange.getLocaleContext().getLocale();
Class<?> clazz = valueType.getRawClass();
if (clazz == null) {
......
......@@ -21,7 +21,6 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
......@@ -189,8 +188,7 @@ public class FreeMarkerView extends AbstractUrlBasedView {
logger.debug("Rendering FreeMarker template [" + getUrl() + "].");
}
List<Locale> locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales();
Locale locale = locales.isEmpty() ? Locale.getDefault() : locales.get(0);
Locale locale = exchange.getLocaleContext().getLocale();
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
try {
......
/*
* 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.reactive.function.server;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.result.view.View;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.i18n.FixedLocaleContextResolver;
import static org.junit.Assert.assertEquals;
/**
* @author Sebastien Deleuze
*/
public class LocaleContextResolverIntegrationTests extends AbstractRouterFunctionIntegrationTests {
private final WebClient webClient = WebClient.create();
@Test
public void fixedLocale() {
Mono<ClientResponse> result = webClient
.get()
.uri("http://localhost:" + this.port + "/")
.exchange();
StepVerifier
.create(result)
.consumeNextWith(response -> {
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(Locale.GERMANY, response.headers().asHttpHeaders().getContentLanguage());
})
.verifyComplete();
}
@Override
protected RouterFunction<?> routerFunction() {
return RouterFunctions.route(RequestPredicates.path("/"), this::render);
}
public Mono<RenderingResponse> render(ServerRequest request) {
return RenderingResponse.create("foo").build();
}
@Override
protected HandlerStrategies handlerStrategies() {
return HandlerStrategies.builder()
.viewResolver(new DummyViewResolver())
.localeContextResolver(new FixedLocaleContextResolver(Locale.GERMANY))
.build();
}
private static class DummyViewResolver implements ViewResolver {
@Override
public Mono<View> resolveViewName(String viewName, Locale locale) {
return Mono.just(new DummyView(locale));
}
}
private static class DummyView implements View {
private final Locale locale;
public DummyView(Locale locale) {
this.locale = locale;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(MediaType.TEXT_HTML);
}
@Override
public Mono<Void> render(@Nullable Map<String, ?> model, @Nullable MediaType contentType,
ServerWebExchange exchange) {
exchange.getResponse().getHeaders().setContentLanguage(locale);
return Mono.empty();
}
}
}
......@@ -43,6 +43,7 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.MockWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
......@@ -82,7 +83,8 @@ public class SessionAttributeMethodArgumentResolverTests {
WebSessionManager sessionManager = new MockWebSessionManager(this.session);
ServerHttpRequest request = MockServerHttpRequest.get("/").build();
this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager, ServerCodecConfigurer.create());
this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithSessionAttribute", (Class<?>[]) null);
}
......
......@@ -31,6 +31,7 @@ import org.springframework.web.reactive.BindingContext;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.DefaultWebSession;
import org.springframework.web.server.session.WebSessionManager;
......@@ -65,7 +66,7 @@ public class WebSessionArgumentResolverTests {
WebSessionManager manager = exchange -> Mono.just(session);
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
ServerWebExchange exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(),
manager, ServerCodecConfigurer.create());
manager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
MethodParameter param = this.testMethod.arg(WebSession.class);
Object actual = this.resolver.resolveArgument(param, context, exchange).block();
......
/*
* 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.reactive.result.view;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurationSupport;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.result.method.annotation.AbstractRequestMappingIntegrationTests;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.i18n.FixedLocaleContextResolver;
import static org.junit.Assert.assertEquals;
/**
* @author Sebastien Deleuze
*/
public class LocaleContextResolverIntegrationTests extends AbstractRequestMappingIntegrationTests {
private final WebClient webClient = WebClient.create();
@Test
public void fixedLocale() {
Mono<ClientResponse> result = webClient
.get()
.uri("http://localhost:" + this.port + "/")
.exchange();
StepVerifier
.create(result)
.consumeNextWith(response -> {
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(Locale.GERMANY, response.headers().asHttpHeaders().getContentLanguage());
})
.verifyComplete();
}
@Override
protected ApplicationContext initApplicationContext() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(WebConfig.class);
context.refresh();
return context;
}
@Configuration
@ComponentScan(resourcePattern = "**/LocaleContextResolverIntegrationTests*.class")
@SuppressWarnings({"unused", "WeakerAccess"})
static class WebConfig extends WebFluxConfigurationSupport {
@Override
protected LocaleContextResolver createLocaleContextResolver() {
return new FixedLocaleContextResolver(Locale.GERMANY);
}
@Override
protected void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver((viewName, locale) -> Mono.just(new DummyView(locale)));
}
private static class DummyView implements View {
private final Locale locale;
public DummyView(Locale locale) {
this.locale = locale;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(MediaType.TEXT_HTML);
}
@Override
public Mono<Void> render(@Nullable Map<String, ?> model, @Nullable MediaType contentType,
ServerWebExchange exchange) {
exchange.getResponse().getHeaders().setContentLanguage(locale);
return Mono.empty();
}
}
}
@Controller
@SuppressWarnings("unused")
static class TestController {
@GetMapping("/")
public String foo() {
return "foo";
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册