Skip to content

Commit e0e6736

Browse files
committed
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
1 parent 72a8868 commit e0e6736

28 files changed

+853
-27
lines changed

spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public SimpleTimeZoneAwareLocaleContext(@Nullable Locale locale, @Nullable TimeZ
5252
}
5353

5454

55+
@Override
5556
public TimeZone getTimeZone() {
5657
return this.timeZone;
5758
}

spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerWebExchange.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.springframework.http.codec.ServerCodecConfigurer;
1919
import org.springframework.web.server.ServerWebExchangeDecorator;
2020
import org.springframework.web.server.adapter.DefaultServerWebExchange;
21+
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
2122
import org.springframework.web.server.session.DefaultWebSessionManager;
2223

2324
/**
@@ -37,7 +38,7 @@ public class MockServerWebExchange extends ServerWebExchangeDecorator {
3738
public MockServerWebExchange(MockServerHttpRequest request) {
3839
super(new DefaultServerWebExchange(
3940
request, new MockServerHttpResponse(), new DefaultWebSessionManager(),
40-
ServerCodecConfigurer.create()));
41+
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver()));
4142
}
4243

4344

spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424

2525
import reactor.core.publisher.Mono;
2626

27+
import org.springframework.context.i18n.LocaleContext;
2728
import org.springframework.http.codec.multipart.Part;
2829
import org.springframework.http.server.reactive.ServerHttpRequest;
2930
import org.springframework.http.server.reactive.ServerHttpResponse;
3031
import org.springframework.lang.Nullable;
3132
import org.springframework.util.MultiValueMap;
33+
import org.springframework.web.server.i18n.LocaleContextResolver;
3234

3335
/**
3436
* Contract for an HTTP request-response interaction. Provides access to the HTTP
@@ -98,6 +100,11 @@ public interface ServerWebExchange {
98100
*/
99101
Mono<MultiValueMap<String, Part>> getMultipartData();
100102

103+
/**
104+
* Return the {@link LocaleContext} using the configured {@link LocaleContextResolver}.
105+
*/
106+
LocaleContext getLocaleContext();
107+
101108
/**
102109
* Returns {@code true} if the one of the {@code checkNotModified} methods
103110
* in this contract were used and they returned true.

spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import reactor.core.publisher.Mono;
2424

25+
import org.springframework.context.i18n.LocaleContext;
2526
import org.springframework.http.codec.multipart.Part;
2627
import org.springframework.http.server.reactive.ServerHttpRequest;
2728
import org.springframework.http.server.reactive.ServerHttpResponse;
@@ -90,6 +91,11 @@ public <T extends Principal> Mono<T> getPrincipal() {
9091
return getDelegate().getPrincipal();
9192
}
9293

94+
@Override
95+
public LocaleContext getLocaleContext() {
96+
return getDelegate().getLocaleContext();
97+
}
98+
9399
@Override
94100
public Mono<MultiValueMap<String, String>> getFormData() {
95101
return getDelegate().getFormData();

spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import reactor.core.publisher.Mono;
3030

31+
import org.springframework.context.i18n.LocaleContext;
3132
import org.springframework.core.ResolvableType;
3233
import org.springframework.http.HttpHeaders;
3334
import org.springframework.http.HttpMethod;
@@ -45,6 +46,7 @@
4546
import org.springframework.util.LinkedMultiValueMap;
4647
import org.springframework.util.MultiValueMap;
4748
import org.springframework.util.StringUtils;
49+
import org.springframework.web.server.i18n.LocaleContextResolver;
4850
import org.springframework.web.server.ServerWebExchange;
4951
import org.springframework.web.server.WebSession;
5052
import org.springframework.web.server.session.WebSessionManager;
@@ -82,27 +84,28 @@ public class DefaultServerWebExchange implements ServerWebExchange {
8284

8385
private final Mono<WebSession> sessionMono;
8486

87+
private final LocaleContextResolver localeContextResolver;
88+
8589
private final Mono<MultiValueMap<String, String>> formDataMono;
8690

8791
private final Mono<MultiValueMap<String, Part>> multipartDataMono;
8892

8993
private volatile boolean notModified;
9094

9195

92-
/**
93-
* Alternate constructor with a WebSessionManager parameter.
94-
*/
9596
public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
96-
WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer) {
97+
WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer, LocaleContextResolver localeContextResolver) {
9798

9899
Assert.notNull(request, "'request' is required");
99100
Assert.notNull(response, "'response' is required");
100101
Assert.notNull(sessionManager, "'sessionManager' is required");
101102
Assert.notNull(codecConfigurer, "'codecConfigurer' is required");
103+
Assert.notNull(localeContextResolver, "'localeContextResolver' is required");
102104

103105
this.request = request;
104106
this.response = response;
105107
this.sessionMono = sessionManager.getSession(this).cache();
108+
this.localeContextResolver = localeContextResolver;
106109
this.formDataMono = initFormData(request, codecConfigurer);
107110
this.multipartDataMono = initMultipartData(request, codecConfigurer);
108111
}
@@ -190,6 +193,11 @@ public <T extends Principal> Mono<T> getPrincipal() {
190193
return Mono.empty();
191194
}
192195

196+
@Override
197+
public LocaleContext getLocaleContext() {
198+
return this.localeContextResolver.resolveLocaleContext(this);
199+
}
200+
193201
@Override
194202
public Mono<MultiValueMap<String, String>> getFormData() {
195203
return this.formDataMono;

spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
import org.springframework.http.server.reactive.ServerHttpRequest;
3232
import org.springframework.http.server.reactive.ServerHttpResponse;
3333
import org.springframework.util.Assert;
34+
import org.springframework.web.server.i18n.LocaleContextResolver;
3435
import org.springframework.web.server.ServerWebExchange;
3536
import org.springframework.web.server.WebHandler;
3637
import org.springframework.web.server.handler.WebHandlerDecorator;
38+
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
3739
import org.springframework.web.server.session.DefaultWebSessionManager;
3840
import org.springframework.web.server.session.WebSessionManager;
3941

@@ -83,6 +85,8 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
8385

8486
private ServerCodecConfigurer codecConfigurer;
8587

88+
private LocaleContextResolver localeContextResolver;
89+
8690

8791
public HttpWebHandlerAdapter(WebHandler delegate) {
8892
super(delegate);
@@ -119,13 +123,30 @@ public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) {
119123
this.codecConfigurer = codecConfigurer;
120124
}
121125

126+
/**
127+
* Configure a custom {@link LocaleContextResolver}. The provided instance is set on
128+
* each created {@link DefaultServerWebExchange}.
129+
* <p>By default this is set to {@link org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver}.
130+
* @param localeContextResolver the locale context resolver to use
131+
*/
132+
public void setLocaleContextResolver(LocaleContextResolver localeContextResolver) {
133+
this.localeContextResolver = localeContextResolver;
134+
}
135+
122136
/**
123137
* Return the configured {@link ServerCodecConfigurer}.
124138
*/
125139
public ServerCodecConfigurer getCodecConfigurer() {
126140
return (this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create());
127141
}
128142

143+
/**
144+
* Return the configured {@link LocaleContextResolver}.
145+
*/
146+
public LocaleContextResolver getLocaleContextResolver() {
147+
return (this.localeContextResolver != null ? this.localeContextResolver : new AcceptHeaderLocaleContextResolver());
148+
}
149+
129150

130151
@Override
131152
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
@@ -140,7 +161,7 @@ public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response)
140161
}
141162

142163
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
143-
return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer());
164+
return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer(), getLocaleContextResolver());
144165
}
145166

146167
private void logHandleFailure(Throwable ex) {

spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.http.server.reactive.HttpHandler;
2828
import org.springframework.util.Assert;
2929
import org.springframework.util.ObjectUtils;
30+
import org.springframework.web.server.i18n.LocaleContextResolver;
3031
import org.springframework.web.server.ServerWebExchange;
3132
import org.springframework.web.server.WebExceptionHandler;
3233
import org.springframework.web.server.WebFilter;
@@ -68,6 +69,9 @@ public class WebHttpHandlerBuilder {
6869
/** Well-known name for the ServerCodecConfigurer in the bean factory. */
6970
public static final String SERVER_CODEC_CONFIGURER_BEAN_NAME = "serverCodecConfigurer";
7071

72+
/** Well-known name for the LocaleContextResolver in the bean factory. */
73+
public static final String LOCALE_CONTEXT_RESOLVER_BEAN_NAME = "localeContextResolver";
74+
7175

7276
private final WebHandler webHandler;
7377

@@ -79,6 +83,8 @@ public class WebHttpHandlerBuilder {
7983

8084
private ServerCodecConfigurer codecConfigurer;
8185

86+
private LocaleContextResolver localeContextResolver;
87+
8288

8389
/**
8490
* Private constructor.
@@ -112,6 +118,8 @@ public static WebHttpHandlerBuilder webHandler(WebHandler webHandler) {
112118
* {@link #WEB_SESSION_MANAGER_BEAN_NAME}.
113119
* <li>{@link ServerCodecConfigurer} [0..1] -- looked up by the name
114120
* {@link #SERVER_CODEC_CONFIGURER_BEAN_NAME}.
121+
*<li>{@link LocaleContextResolver} [0..1] -- looked up by the name
122+
* {@link #LOCALE_CONTEXT_RESOLVER_BEAN_NAME}.
115123
* </ul>
116124
* @param context the application context to use for the lookup
117125
* @return the prepared builder
@@ -144,6 +152,14 @@ public static WebHttpHandlerBuilder applicationContext(ApplicationContext contex
144152
// Fall back on default
145153
}
146154

155+
try {
156+
builder.localeContextResolver(
157+
context.getBean(LOCALE_CONTEXT_RESOLVER_BEAN_NAME, LocaleContextResolver.class));
158+
}
159+
catch (NoSuchBeanDefinitionException ex) {
160+
// Fall back on default
161+
}
162+
147163
return builder;
148164
}
149165

@@ -234,6 +250,16 @@ public WebHttpHandlerBuilder codecConfigurer(ServerCodecConfigurer codecConfigur
234250
return this;
235251
}
236252

253+
/**
254+
* Configure the {@link LocaleContextResolver} to set on the
255+
* {@link ServerWebExchange WebServerExchange}.
256+
* @param localeContextResolver the locale context resolver
257+
*/
258+
public WebHttpHandlerBuilder localeContextResolver(LocaleContextResolver localeContextResolver) {
259+
this.localeContextResolver = localeContextResolver;
260+
return this;
261+
}
262+
237263

238264
/**
239265
* Build the {@link HttpHandler}.
@@ -252,6 +278,9 @@ public HttpHandler build() {
252278
if (this.codecConfigurer != null) {
253279
adapted.setCodecConfigurer(this.codecConfigurer);
254280
}
281+
if (this.localeContextResolver != null) {
282+
adapted.setLocaleContextResolver(this.localeContextResolver);
283+
}
255284

256285
return adapted;
257286
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.server.i18n;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Locale;
22+
23+
import org.springframework.context.i18n.LocaleContext;
24+
import org.springframework.context.i18n.SimpleLocaleContext;
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.server.reactive.ServerHttpRequest;
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.web.server.ServerWebExchange;
29+
30+
/**
31+
* {@link LocaleContextResolver} implementation that simply uses the primary locale
32+
* specified in the "Accept-Language" header of the HTTP request (that is,
33+
* the locale sent by the client browser, normally that of the client's OS).
34+
*
35+
* <p>Note: Does not support {@code setLocale}, since the accept header
36+
* can only be changed through changing the client's locale settings.
37+
*
38+
* @author Sebastien Deleuze
39+
* @since 5.0
40+
*/
41+
public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver {
42+
43+
private final List<Locale> supportedLocales = new ArrayList<>(4);
44+
45+
private Locale defaultLocale;
46+
47+
48+
/**
49+
* Configure supported locales to check against the requested locales
50+
* determined via {@link HttpHeaders#getAcceptLanguageAsLocales()}.
51+
* @param locales the supported locales
52+
*/
53+
public void setSupportedLocales(List<Locale> locales) {
54+
this.supportedLocales.clear();
55+
if (locales != null) {
56+
this.supportedLocales.addAll(locales);
57+
}
58+
}
59+
60+
/**
61+
* Return the configured list of supported locales.
62+
*/
63+
public List<Locale> getSupportedLocales() {
64+
return this.supportedLocales;
65+
}
66+
67+
/**
68+
* Configure a fixed default locale to fall back on if the request does not
69+
* have an "Accept-Language" header (not set by default).
70+
* @param defaultLocale the default locale to use
71+
*/
72+
public void setDefaultLocale(Locale defaultLocale) {
73+
this.defaultLocale = defaultLocale;
74+
}
75+
76+
/**
77+
* The configured default locale, if any.
78+
*/
79+
@Nullable
80+
public Locale getDefaultLocale() {
81+
return this.defaultLocale;
82+
}
83+
84+
@Override
85+
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
86+
ServerHttpRequest request = exchange.getRequest();
87+
List<Locale> acceptableLocales = request.getHeaders().getAcceptLanguageAsLocales();
88+
if (this.defaultLocale != null && acceptableLocales.isEmpty()) {
89+
return new SimpleLocaleContext(this.defaultLocale);
90+
}
91+
Locale requestLocale = acceptableLocales.isEmpty() ? null : acceptableLocales.get(0);
92+
if (isSupportedLocale(requestLocale)) {
93+
return new SimpleLocaleContext(requestLocale);
94+
}
95+
Locale supportedLocale = findSupportedLocale(request);
96+
if (supportedLocale != null) {
97+
return new SimpleLocaleContext(supportedLocale);
98+
}
99+
return (defaultLocale != null ? new SimpleLocaleContext(defaultLocale) : new SimpleLocaleContext(requestLocale));
100+
}
101+
102+
private boolean isSupportedLocale(@Nullable Locale locale) {
103+
if (locale == null) {
104+
return false;
105+
}
106+
List<Locale> supportedLocales = getSupportedLocales();
107+
return (supportedLocales.isEmpty() || supportedLocales.contains(locale));
108+
}
109+
110+
@Nullable
111+
private Locale findSupportedLocale(ServerHttpRequest request) {
112+
List<Locale> requestLocales = request.getHeaders().getAcceptLanguageAsLocales();
113+
for (Locale locale : requestLocales) {
114+
if (getSupportedLocales().contains(locale)) {
115+
return locale;
116+
}
117+
}
118+
return null;
119+
}
120+
121+
@Override
122+
public void setLocaleContext(ServerWebExchange exchange, @Nullable LocaleContext locale) {
123+
throw new UnsupportedOperationException(
124+
"Cannot change HTTP accept header - use a different locale context resolution strategy");
125+
}
126+
127+
}

0 commit comments

Comments
 (0)