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 7c546567480e2f6ddcbdb3fef94daf611b859d3a..0c1cf6cf4936603f1088d5242fe90df9ec8b26db 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 5276c71b7f0e8cd0e4774580a63fda82d2c36c27..6060a8656534de2b800bb4243e9e99e1f1901086 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 1e0c70dd9498d3fb80afa0faab64e962cd58bce2..4b08c377620951178fbc869f6799ceff6a61f029 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 2df1625ff4eec412621463a375ab0000573fe7b8..6963537c94c1e58a0134396cebe41a19e6d8267c 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 1420cb0a40d5ba9f733e2ec2d874bda6d276f938..d7780e0a2a08421e7efcf24477c109658e2e36fa 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 a83c18066fe8422fd4a5e87c045b397efe8ecc96..69103220ed85490d12e08f9df9c3c06ba41b3f32 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 492739ae9ee53531b1cf0c09b4fa704b5e3970ff..273fe33cf679d3828ca5ec3aef3b8bf0197f7249 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 0000000000000000000000000000000000000000..4732a567f997db1ad7b77c8353a0911d72bb781a --- /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 0000000000000000000000000000000000000000..c680915103d3af66ee456142794deeac14b0fb66 --- /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 0000000000000000000000000000000000000000..da5d28e38501339c529f2306013038f95398def0 --- /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 0000000000000000000000000000000000000000..ae3a98af0441b4d138fd060e8ba353270056c9de --- /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 ad1156aabf66d0cbb46a115945ec006d4527aa8d..591381b329d1eaff2bbce3589545e4a4e0674952 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 73c77ab4db0ba633e0f34a44cb82d259a534b1a5..1b9f69da993cedb7c2aca1d71bc92b3a9ddb512b 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 0000000000000000000000000000000000000000..ee1f6b9f4f9580aa781a9db6dc01aa063ae3ceb3 --- /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 0000000000000000000000000000000000000000..7330d1c64efa529882617edf4c5fcd48b942824b --- /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 b6be20397d7a73a805dc6d951bed83a6a85fea35..aa855a8e43dd9cdef6537f5631d74272f8a05815 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 77926521fe75d1e3ddf8ca8e903611f431fa1643..f271b3c4a6faaaed69279d3295c5d737c127063c 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 1c075174ae6e833836a00f57b9fd5490d6c720fe..7b2b0926e02a5f5229e126c408fee27f32156c7d 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 cf76d9e57d292004ff2c40580ec61924fb6f4179..14317bb39456db932b310a1241006b25c92aa8af 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 30f2b5c901871da0d700e0b6e0507b6856e5f3b8..bbd3bcce6c9bc8ea71fd2797d963988e1e462f75 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 4be209d888583f1a1eb5862cc3b535543f6f9abe..4d5bb6788b6716719b37b1dd887926a2c5343643 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 dafb4ae15feddee0d7a9375396d00920c9fab422..3e439af934f301e88bda72b6de97dfdef0a8c24f 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 20a6514e787e04faf2fe53d79bee37532b895a02..a606808ad7bfe62bcf65d543b87b9f6da3dd5bf4 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 c65df13e805bd3f4fe73601e53e736cb81986b41..8f6eed6158616e852493a0f177ef1dda36c138f7 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 0000000000000000000000000000000000000000..da9e9c58eb9e7feda7ffd9dbf9b4cd9abf6f7e1b --- /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 c37d8004326613dd9cfe98a9ca0b8826fdf2fbba..b1f32e09058ea26562e8651078290f404d6240e1 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 d494f6bf64ad0b9aa782ea3d12a8eba452d89643..20afd4f2e5f2a99739964ef26b7be436fcf42230 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 0000000000000000000000000000000000000000..b4a6943f57c190e4363127abd56ddd15ca14235e --- /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"; + } + + } + +}