Skip to content

Commit 0dd320f

Browse files
committed
Support for BCP 47 language tags
Issue: SPR-13032
1 parent cad2ce0 commit 0dd320f

File tree

3 files changed

+153
-7
lines changed

3 files changed

+153
-7
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -81,6 +81,8 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
8181
public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";
8282

8383

84+
private boolean languageTagCompliant = false;
85+
8486
private Locale defaultLocale;
8587

8688
private TimeZone defaultTimeZone;
@@ -94,6 +96,30 @@ public CookieLocaleResolver() {
9496
setCookieName(DEFAULT_COOKIE_NAME);
9597
}
9698

99+
100+
/**
101+
* Specify whether this resolver's cookies should be compliant with BCP 47
102+
* language tags instead of Java's legacy locale specification format.
103+
* The default is {@code false}.
104+
* <p>Note: This mode requires JDK 7 or higher. Set this flag to {@code true}
105+
* for BCP 47 compliance on JDK 7+ only.
106+
* @since 4.3
107+
* @see Locale#forLanguageTag(String)
108+
* @see Locale#toLanguageTag()
109+
*/
110+
public void setLanguageTagCompliant(boolean languageTagCompliant) {
111+
this.languageTagCompliant = languageTagCompliant;
112+
}
113+
114+
/**
115+
* Return whether this resolver's cookies should be compliant with BCP 47
116+
* language tags instead of Java's legacy locale specification format.
117+
* @since 4.3
118+
*/
119+
public boolean isLanguageTagCompliant() {
120+
return this.languageTagCompliant;
121+
}
122+
97123
/**
98124
* Set a fixed Locale that this resolver will return if no cookie found.
99125
*/
@@ -111,6 +137,7 @@ protected Locale getDefaultLocale() {
111137

112138
/**
113139
* Set a fixed TimeZone that this resolver will return if no cookie found.
140+
* @since 4.0
114141
*/
115142
public void setDefaultTimeZone(TimeZone defaultTimeZone) {
116143
this.defaultTimeZone = defaultTimeZone;
@@ -119,6 +146,7 @@ public void setDefaultTimeZone(TimeZone defaultTimeZone) {
119146
/**
120147
* Return the fixed TimeZone that this resolver will return if no cookie found,
121148
* if any.
149+
* @since 4.0
122150
*/
123151
protected TimeZone getDefaultTimeZone() {
124152
return this.defaultTimeZone;
@@ -161,7 +189,7 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
161189
localePart = value.substring(0, spaceIndex);
162190
timeZonePart = value.substring(spaceIndex + 1);
163191
}
164-
locale = (!"-".equals(localePart) ? StringUtils.parseLocaleString(localePart) : null);
192+
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
165193
if (timeZonePart != null) {
166194
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
167195
}
@@ -171,7 +199,7 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
171199
}
172200
}
173201
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
174-
(locale != null ? locale: determineDefaultLocale(request)));
202+
(locale != null ? locale : determineDefaultLocale(request)));
175203
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
176204
(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
177205
}
@@ -191,18 +219,45 @@ public void setLocaleContext(HttpServletRequest request, HttpServletResponse res
191219
if (localeContext instanceof TimeZoneAwareLocaleContext) {
192220
timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
193221
}
194-
addCookie(response, (locale != null ? locale : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
222+
addCookie(response,
223+
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
195224
}
196225
else {
197226
removeCookie(response);
198227
}
199228
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
200-
(locale != null ? locale: determineDefaultLocale(request)));
229+
(locale != null ? locale : determineDefaultLocale(request)));
201230
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
202231
(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
203232
}
204233

205234

235+
/**
236+
* Parse the given locale value coming from an incoming cookie.
237+
* <p>The default implementation calls {@link StringUtils#parseLocaleString(String)}
238+
* or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the
239+
* {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property.
240+
* @param locale the locale value to parse
241+
* @return the corresponding {@code Locale} instance
242+
* @since 4.3
243+
*/
244+
protected Locale parseLocaleValue(String locale) {
245+
return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale));
246+
}
247+
248+
/**
249+
* Render the given locale as a text value for inclusion in a cookie.
250+
* <p>The default implementation calls {@link Locale#toString()}
251+
* or JDK 7's {@link Locale#toLanguageTag()}, depending on the
252+
* {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property.
253+
* @param locale the locale to stringify
254+
* @return a String representation for the given locale
255+
* @since 4.3
256+
*/
257+
protected String toLocaleValue(Locale locale) {
258+
return (isLanguageTagCompliant() ? locale.toLanguageTag() : locale.toString());
259+
}
260+
206261
/**
207262
* Determine the default locale for the given request,
208263
* Called if no locale cookie has been found.

spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.servlet.i18n;
1818

19+
import java.util.Locale;
1920
import javax.servlet.ServletException;
2021
import javax.servlet.http.HttpServletRequest;
2122
import javax.servlet.http.HttpServletResponse;
@@ -54,6 +55,8 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
5455

5556
private boolean ignoreInvalidLocale = false;
5657

58+
private boolean languageTagCompliant = false;
59+
5760

5861
/**
5962
* Set the name of the parameter that contains a locale specification
@@ -104,6 +107,29 @@ public boolean isIgnoreInvalidLocale() {
104107
return this.ignoreInvalidLocale;
105108
}
106109

110+
/**
111+
* Specify whether to parse request parameter values as BCP 47 language tags
112+
* instead of Java's legacy locale specification format.
113+
* The default is {@code false}.
114+
* <p>Note: This mode requires JDK 7 or higher. Set this flag to {@code true}
115+
* for BCP 47 compliance on JDK 7+ only.
116+
* @since 4.3
117+
* @see Locale#forLanguageTag(String)
118+
* @see Locale#toLanguageTag()
119+
*/
120+
public void setLanguageTagCompliant(boolean languageTagCompliant) {
121+
this.languageTagCompliant = languageTagCompliant;
122+
}
123+
124+
/**
125+
* Return whether to use BCP 47 language tags instead of Java's legacy
126+
* locale specification format.
127+
* @since 4.3
128+
*/
129+
public boolean isLanguageTagCompliant() {
130+
return this.languageTagCompliant;
131+
}
132+
107133

108134
@Override
109135
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
@@ -118,7 +144,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
118144
"No LocaleResolver found: not in a DispatcherServlet request?");
119145
}
120146
try {
121-
localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale));
147+
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
122148
}
123149
catch (IllegalArgumentException ex) {
124150
if (isIgnoreInvalidLocale()) {
@@ -147,4 +173,17 @@ private boolean checkHttpMethod(String currentMethod) {
147173
return false;
148174
}
149175

176+
/**
177+
* Parse the given locale value as coming from a request parameter.
178+
* <p>The default implementation calls {@link StringUtils#parseLocaleString(String)}
179+
* or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the
180+
* {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property.
181+
* @param locale the locale value to parse
182+
* @return the corresponding {@code Locale} instance
183+
* @since 4.3
184+
*/
185+
protected Locale parseLocaleValue(String locale) {
186+
return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale));
187+
}
188+
150189
}

spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -164,6 +164,58 @@ public void testSetAndResolveLocaleContextWithTimeZoneOnly() {
164164
assertEquals(TimeZone.getTimeZone("GMT+1"), ((TimeZoneAwareLocaleContext) loc).getTimeZone());
165165
}
166166

167+
@Test
168+
public void testSetAndResolveLocaleWithCountry() {
169+
MockHttpServletRequest request = new MockHttpServletRequest();
170+
MockHttpServletResponse response = new MockHttpServletResponse();
171+
172+
CookieLocaleResolver resolver = new CookieLocaleResolver();
173+
resolver.setLocale(request, response, new Locale("de", "AT"));
174+
175+
Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME);
176+
assertNotNull(cookie);
177+
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_NAME, cookie.getName());
178+
assertEquals(null, cookie.getDomain());
179+
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath());
180+
assertFalse(cookie.getSecure());
181+
assertEquals("de_AT", cookie.getValue());
182+
183+
request = new MockHttpServletRequest();
184+
request.setCookies(cookie);
185+
186+
resolver = new CookieLocaleResolver();
187+
Locale loc = resolver.resolveLocale(request);
188+
assertEquals("de", loc.getLanguage());
189+
assertEquals("AT", loc.getCountry());
190+
}
191+
192+
@Test
193+
public void testSetAndResolveLocaleWithCountryAsLanguageTag() {
194+
MockHttpServletRequest request = new MockHttpServletRequest();
195+
MockHttpServletResponse response = new MockHttpServletResponse();
196+
197+
CookieLocaleResolver resolver = new CookieLocaleResolver();
198+
resolver.setLanguageTagCompliant(true);
199+
resolver.setLocale(request, response, new Locale("de", "AT"));
200+
201+
Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME);
202+
assertNotNull(cookie);
203+
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_NAME, cookie.getName());
204+
assertEquals(null, cookie.getDomain());
205+
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath());
206+
assertFalse(cookie.getSecure());
207+
assertEquals("de-AT", cookie.getValue());
208+
209+
request = new MockHttpServletRequest();
210+
request.setCookies(cookie);
211+
212+
resolver = new CookieLocaleResolver();
213+
resolver.setLanguageTagCompliant(true);
214+
Locale loc = resolver.resolveLocale(request);
215+
assertEquals("de", loc.getLanguage());
216+
assertEquals("AT", loc.getCountry());
217+
}
218+
167219
@Test
168220
public void testCustomCookie() {
169221
MockHttpServletRequest request = new MockHttpServletRequest();

0 commit comments

Comments
 (0)