From e0e6736bc58101af13a352d4aa2a6c2dc0994d72 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 6 Jun 2017 09:31:58 +0200 Subject: [PATCH] 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 --- .../SimpleTimeZoneAwareLocaleContext.java | 1 + .../reactive/MockServerWebExchange.java | 3 +- .../web/server/ServerWebExchange.java | 7 + .../server/ServerWebExchangeDecorator.java | 6 + .../adapter/DefaultServerWebExchange.java | 16 ++- .../server/adapter/HttpWebHandlerAdapter.java | 23 +++- .../server/adapter/WebHttpHandlerBuilder.java | 29 ++++ .../AcceptHeaderLocaleContextResolver.java | 127 +++++++++++++++++ .../i18n/FixedLocaleContextResolver.java | 92 +++++++++++++ .../server/i18n/LocaleContextResolver.java | 63 +++++++++ .../web/server/i18n/package-info.java | 8 ++ .../reactive/test/MockServerHttpRequest.java | 14 ++ .../reactive/test/MockServerWebExchange.java | 4 +- ...cceptHeaderLocaleContextResolverTests.java | 95 +++++++++++++ .../i18n/FixedLocaleContextResolverTests.java | 64 +++++++++ .../DefaultWebSessionManagerTests.java | 4 +- .../config/WebFluxConfigurationSupport.java | 14 ++ .../DefaultHandlerStrategiesBuilder.java | 26 +++- .../DefaultRenderingResponseBuilder.java | 8 +- .../function/server/HandlerStrategies.java | 15 ++ .../function/server/RouterFunctions.java | 1 + .../reactive/result/view/RequestContext.java | 9 +- .../view/ViewResolutionResultHandler.java | 3 +- .../view/freemarker/FreeMarkerView.java | 4 +- ...LocaleContextResolverIntegrationTests.java | 108 +++++++++++++++ ...nAttributeMethodArgumentResolverTests.java | 4 +- .../WebSessionArgumentResolverTests.java | 3 +- ...LocaleContextResolverIntegrationTests.java | 129 ++++++++++++++++++ 28 files changed, 853 insertions(+), 27 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/server/i18n/LocaleContextResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/server/i18n/package-info.java create mode 100644 spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/server/LocaleContextResolverIntegrationTests.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/result/view/LocaleContextResolverIntegrationTests.java diff --git a/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java index 7c54656748..0c1cf6cf49 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java @@ -52,6 +52,7 @@ public class SimpleTimeZoneAwareLocaleContext extends SimpleLocaleContext implem } + @Override public TimeZone getTimeZone() { return this.timeZone; } diff --git a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerWebExchange.java b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerWebExchange.java index 5276c71b7f..6060a86565 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerWebExchange.java +++ b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerWebExchange.java @@ -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())); } diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java index 1e0c70dd94..4b08c37762 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java @@ -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> 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. diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java index 2df1625ff4..6963537c94 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java +++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java @@ -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> getFormData() { return getDelegate().getFormData(); diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 1420cb0a40..d7780e0a2a 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -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 sessionMono; + private final LocaleContextResolver localeContextResolver; + private final Mono> formDataMono; private final Mono> 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> getFormData() { return this.formDataMono; diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index a83c18066f..69103220ed 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -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}. + *

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 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) { diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java b/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java index 492739ae9e..273fe33cf6 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java @@ -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}. *

  • {@link ServerCodecConfigurer} [0..1] -- looked up by the name * {@link #SERVER_CODEC_CONFIGURER_BEAN_NAME}. + *
  • {@link LocaleContextResolver} [0..1] -- looked up by the name + * {@link #LOCALE_CONTEXT_RESOLVER_BEAN_NAME}. * * @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; } diff --git a/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java b/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java new file mode 100644 index 0000000000..4732a567f9 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java @@ -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). + * + *

    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 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 locales) { + this.supportedLocales.clear(); + if (locales != null) { + this.supportedLocales.addAll(locales); + } + } + + /** + * Return the configured list of supported locales. + */ + public List 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 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 supportedLocales = getSupportedLocales(); + return (supportedLocales.isEmpty() || supportedLocales.contains(locale)); + } + + @Nullable + private Locale findSupportedLocale(ServerHttpRequest request) { + List 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"); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java b/spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java new file mode 100644 index 0000000000..c680915103 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java @@ -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. + * + *

    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"); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/server/i18n/LocaleContextResolver.java b/spring-web/src/main/java/org/springframework/web/server/i18n/LocaleContextResolver.java new file mode 100644 index 0000000000..da5d28e385 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/server/i18n/LocaleContextResolver.java @@ -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. + * + *

    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. + * + *

    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. + *

    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); + +} diff --git a/spring-web/src/main/java/org/springframework/web/server/i18n/package-info.java b/spring-web/src/main/java/org/springframework/web/server/i18n/package-info.java new file mode 100644 index 0000000000..ae3a98af04 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/server/i18n/package-info.java @@ -0,0 +1,8 @@ +/** + * Locale related support classes. + * Provides standard LocaleContextResolver implementations. + */ +@NonNullApi +package org.springframework.web.server.i18n; + +import org.springframework.lang.NonNullApi; diff --git a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java index ad1156aabf..591381b329 100644 --- a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java +++ b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java @@ -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. *

    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); diff --git a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerWebExchange.java b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerWebExchange.java index 73c77ab4db..1b9f69da99 100644 --- a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerWebExchange.java +++ b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerWebExchange.java @@ -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())); } diff --git a/spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java b/spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java new file mode 100644 index 0000000000..ee1f6b9f4f --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java @@ -0,0 +1,95 @@ +/* + * 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()); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java b/spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java new file mode 100644 index 0000000000..7330d1c64e --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java @@ -0,0 +1,64 @@ +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()); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java index b6be20397d..aa855a8e43 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -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()); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index 77926521fe..f271b3c4a6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -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. */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java index 1c075174ae..7b2b0926e0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java @@ -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 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 exceptionHandlers; + private final LocaleContextResolver localeContextResolver; + + public DefaultHandlerStrategies( List> messageReaders, List> messageWriters, List viewResolvers, List webFilters, - List exceptionHandlers) { + List 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 List unmodifiableCopy(List list) { @@ -156,6 +173,11 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { public List exceptionHandlers() { return this.exceptionHandlers; } + + @Override + public LocaleContextResolver localeContextResolver() { + return this.localeContextResolver; + } } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java index cf76d9e57d..14317bb394 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java @@ -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 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 locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales(); - return locales.isEmpty() ? Locale.getDefault() : locales.get(0); - - } } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java index 30f2b5c901..bbd3bcce6c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java @@ -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 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 diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index 4be209d888..4d5bb6788b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -199,6 +199,7 @@ public abstract class RouterFunctions { return WebHttpHandlerBuilder.webHandler(webHandler) .filters(strategies.webFilters()) .exceptionHandlers(strategies.exceptionHandlers()) + .localeContextResolver(strategies.localeContextResolver()) .build(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java index dafb4ae15f..3e439af934 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java @@ -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 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; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 20a6514e78..a606808ad7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -201,8 +201,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport Model model = result.getModel(); MethodParameter parameter = result.getReturnTypeSource(); - List 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) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index c65df13e80..8f6eed6158 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -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 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 { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/LocaleContextResolverIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/LocaleContextResolverIntegrationTests.java new file mode 100644 index 0000000000..da9e9c58eb --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/LocaleContextResolverIntegrationTests.java @@ -0,0 +1,108 @@ +/* + * 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 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 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 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 getSupportedMediaTypes() { + return Collections.singletonList(MediaType.TEXT_HTML); + } + + @Override + public Mono render(@Nullable Map model, @Nullable MediaType contentType, + ServerWebExchange exchange) { + exchange.getResponse().getHeaders().setContentLanguage(locale); + return Mono.empty(); + } + } +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java index c37d800432..b1f32e0905 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -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); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/WebSessionArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/WebSessionArgumentResolverTests.java index d494f6bf64..20afd4f2e5 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/WebSessionArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/WebSessionArgumentResolverTests.java @@ -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(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/LocaleContextResolverIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/LocaleContextResolverIntegrationTests.java new file mode 100644 index 0000000000..b4a6943f57 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/LocaleContextResolverIntegrationTests.java @@ -0,0 +1,129 @@ +/* + * 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 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 getSupportedMediaTypes() { + return Collections.singletonList(MediaType.TEXT_HTML); + } + + @Override + public Mono render(@Nullable Map 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"; + } + + } + +} -- GitLab