Skip to content

Commit 88e4006

Browse files
committed
CookieLocaleResolver is RFC6265 and language tag compliant by default
Like CookieLocaleResolver, LocaleChangeInterceptor parses both locale formats by default now. Since it does not need to render the locale, its languageTagCompliant property is not relevant anymore at all. The parseLocale method in StringUtils validates the locale value now and turns an empty locale into null, compatible with parseLocaleString behavior and in particular aligned with web locale parsing needs. Issue: SPR-16700 Issue: SPR-16651
1 parent 955665b commit 88e4006

File tree

5 files changed

+68
-51
lines changed

5 files changed

+68
-51
lines changed

spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Locale;
2020

2121
import org.springframework.core.convert.converter.Converter;
22+
import org.springframework.lang.Nullable;
2223
import org.springframework.util.StringUtils;
2324

2425
/**
@@ -35,6 +36,7 @@
3536
final class StringToLocaleConverter implements Converter<String, Locale> {
3637

3738
@Override
39+
@Nullable
3840
public Locale convert(String source) {
3941
return StringUtils.parseLocale(source);
4042
}

spring-core/src/main/java/org/springframework/util/StringUtils.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,9 @@ public static String uriDecode(String source, Charset charset) {
772772
public static Locale parseLocale(String localeValue) {
773773
String[] tokens = tokenizeLocaleSource(localeValue);
774774
if (tokens.length == 1) {
775-
return Locale.forLanguageTag(localeValue);
775+
validateLocalePart(localeValue);
776+
Locale resolved = Locale.forLanguageTag(localeValue);
777+
return (resolved.getLanguage().length() > 0 ? resolved : null);
776778
}
777779
return parseLocaleTokens(localeValue, tokens);
778780
}
@@ -821,7 +823,7 @@ private static Locale parseLocaleTokens(String localeString, String[] tokens) {
821823
private static void validateLocalePart(String localePart) {
822824
for (int i = 0; i < localePart.length(); i++) {
823825
char ch = localePart.charAt(i);
824-
if (ch != ' ' && ch != '_' && ch != '#' && !Character.isLetterOrDigit(ch)) {
826+
if (ch != ' ' && ch != '_' && ch != '-' && ch != '#' && !Character.isLetterOrDigit(ch)) {
825827
throw new IllegalArgumentException(
826828
"Locale part \"" + localePart + "\" contains invalid characters");
827829
}

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

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -83,7 +83,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
8383
public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";
8484

8585

86-
private boolean languageTagCompliant = false;
86+
private boolean languageTagCompliant = true;
8787

8888
@Nullable
8989
private Locale defaultLocale;
@@ -104,8 +104,13 @@ public CookieLocaleResolver() {
104104
/**
105105
* Specify whether this resolver's cookies should be compliant with BCP 47
106106
* language tags instead of Java's legacy locale specification format.
107-
* The default is {@code false}.
107+
* <p>The default is {@code true}, as of 5.1. Switch this to {@code false}
108+
* for rendering Java's legacy locale specification format. For parsing,
109+
* this resolver leniently accepts the legacy {@link Locale#toString}
110+
* format as well as BCP 47 language tags in any case.
108111
* @since 4.3
112+
* @see #parseLocaleValue(String)
113+
* @see #toLocaleValue(Locale)
109114
* @see Locale#forLanguageTag(String)
110115
* @see Locale#toLanguageTag()
111116
*/
@@ -193,10 +198,14 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
193198
String value = cookie.getValue();
194199
String localePart = value;
195200
String timeZonePart = null;
196-
int spaceIndex = localePart.indexOf(' ');
197-
if (spaceIndex != -1) {
198-
localePart = value.substring(0, spaceIndex);
199-
timeZonePart = value.substring(spaceIndex + 1);
201+
int separatorIndex = localePart.indexOf('/');
202+
if (separatorIndex == -1) {
203+
// Leniently accept older cookies separated by a space...
204+
separatorIndex = localePart.indexOf(' ');
205+
}
206+
if (separatorIndex >= 0) {
207+
localePart = value.substring(0, separatorIndex);
208+
timeZonePart = value.substring(separatorIndex + 1);
200209
}
201210
try {
202211
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
@@ -205,16 +214,16 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
205214
}
206215
}
207216
catch (IllegalArgumentException ex) {
208-
String reason = "Ignoring invalid locale cookie '" +
209-
cookieName + ":" + value + "' due to: " + ex.getMessage();
217+
String cookieDescription = "invalid locale cookie '" + cookieName +
218+
"': [" + value + "] due to: " + ex.getMessage();
210219
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
211220
// Error dispatch: ignore locale/timezone parse exceptions
212221
if (logger.isDebugEnabled()) {
213-
logger.debug(reason);
222+
logger.debug("Ignoring " + cookieDescription);
214223
}
215224
}
216225
else {
217-
throw new IllegalStateException(reason);
226+
throw new IllegalStateException("Encountered " + cookieDescription);
218227
}
219228
}
220229
if (logger.isTraceEnabled()) {
@@ -250,7 +259,7 @@ public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletRe
250259
timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
251260
}
252261
addCookie(response,
253-
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
262+
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? '/' + timeZone.getID() : ""));
254263
}
255264
else {
256265
removeCookie(response);
@@ -264,16 +273,16 @@ public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletRe
264273

265274
/**
266275
* Parse the given locale value coming from an incoming cookie.
267-
* <p>The default implementation calls {@link StringUtils#parseLocaleString(String)}
268-
* or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the
269-
* {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property.
270-
* @param locale the locale value to parse
276+
* <p>The default implementation calls {@link StringUtils#parseLocale(String)},
277+
* accepting the {@link Locale#toString} format as well as BCP 47 language tags.
278+
* @param localeValue the locale value to parse
271279
* @return the corresponding {@code Locale} instance
272280
* @since 4.3
281+
* @see StringUtils#parseLocale(String)
273282
*/
274283
@Nullable
275-
protected Locale parseLocaleValue(String locale) {
276-
return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale));
284+
protected Locale parseLocaleValue(String localeValue) {
285+
return StringUtils.parseLocale(localeValue);
277286
}
278287

279288
/**
@@ -284,6 +293,7 @@ protected Locale parseLocaleValue(String locale) {
284293
* @param locale the locale to stringify
285294
* @return a String representation for the given locale
286295
* @since 4.3
296+
* @see #isLanguageTagCompliant()
287297
*/
288298
protected String toLocaleValue(Locale locale) {
289299
return (isLanguageTagCompliant() ? locale.toLanguageTag() : locale.toString());

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -57,8 +57,6 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
5757

5858
private boolean ignoreInvalidLocale = false;
5959

60-
private boolean languageTagCompliant = false;
61-
6260

6361
/**
6462
* Set the name of the parameter that contains a locale specification
@@ -113,22 +111,29 @@ public boolean isIgnoreInvalidLocale() {
113111
/**
114112
* Specify whether to parse request parameter values as BCP 47 language tags
115113
* instead of Java's legacy locale specification format.
116-
* The default is {@code false}.
114+
* <p><b>NOTE: As of 5.1, this resolver leniently accepts the legacy
115+
* {@link Locale#toString} format as well as BCP 47 language tags.</b>
117116
* @since 4.3
118117
* @see Locale#forLanguageTag(String)
119118
* @see Locale#toLanguageTag()
119+
* @deprecated as of 5.1 since it only accepts {@code true} now
120120
*/
121+
@Deprecated
121122
public void setLanguageTagCompliant(boolean languageTagCompliant) {
122-
this.languageTagCompliant = languageTagCompliant;
123+
if (!languageTagCompliant) {
124+
throw new IllegalArgumentException("LocaleChangeInterceptor always accepts BCP 47 language tags");
125+
}
123126
}
124127

125128
/**
126129
* Return whether to use BCP 47 language tags instead of Java's legacy
127130
* locale specification format.
128131
* @since 4.3
132+
* @deprecated as of 5.1 since it always returns {@code true} now
129133
*/
134+
@Deprecated
130135
public boolean isLanguageTagCompliant() {
131-
return this.languageTagCompliant;
136+
return true;
132137
}
133138

134139

@@ -176,16 +181,15 @@ private boolean checkHttpMethod(String currentMethod) {
176181

177182
/**
178183
* Parse the given locale value as coming from a request parameter.
179-
* <p>The default implementation calls {@link StringUtils#parseLocaleString(String)}
180-
* or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the
181-
* {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property.
182-
* @param locale the locale value to parse
184+
* <p>The default implementation calls {@link StringUtils#parseLocale(String)},
185+
* accepting the {@link Locale#toString} format as well as BCP 47 language tags.
186+
* @param localeValue the locale value to parse
183187
* @return the corresponding {@code Locale} instance
184188
* @since 4.3
185189
*/
186190
@Nullable
187-
protected Locale parseLocaleValue(String locale) {
188-
return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale));
191+
protected Locale parseLocaleValue(String localeValue) {
192+
return StringUtils.parseLocale(localeValue);
189193
}
190194

191195
}

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

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -83,7 +83,7 @@ public void testResolveLocaleContextWithTimeZone() {
8383
@Test
8484
public void testResolveLocaleContextWithInvalidLocale() {
8585
MockHttpServletRequest request = new MockHttpServletRequest();
86-
Cookie cookie = new Cookie("LanguageKoekje", "n-x GMT+1");
86+
Cookie cookie = new Cookie("LanguageKoekje", "++ GMT+1");
8787
request.setCookies(cookie);
8888

8989
CookieLocaleResolver resolver = new CookieLocaleResolver();
@@ -94,7 +94,7 @@ public void testResolveLocaleContextWithInvalidLocale() {
9494
}
9595
catch (IllegalStateException ex) {
9696
assertTrue(ex.getMessage().contains("LanguageKoekje"));
97-
assertTrue(ex.getMessage().contains("n-x GMT+1"));
97+
assertTrue(ex.getMessage().contains("++ GMT+1"));
9898
}
9999
}
100100

@@ -103,7 +103,7 @@ public void testResolveLocaleContextWithInvalidLocaleOnErrorDispatch() {
103103
MockHttpServletRequest request = new MockHttpServletRequest();
104104
request.addPreferredLocale(Locale.GERMAN);
105105
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, new ServletException());
106-
Cookie cookie = new Cookie("LanguageKoekje", "n-x GMT+1");
106+
Cookie cookie = new Cookie("LanguageKoekje", "++ GMT+1");
107107
request.setCookies(cookie);
108108

109109
CookieLocaleResolver resolver = new CookieLocaleResolver();
@@ -246,7 +246,7 @@ public void testSetAndResolveLocaleWithCountry() {
246246
assertEquals(null, cookie.getDomain());
247247
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath());
248248
assertFalse(cookie.getSecure());
249-
assertEquals("de_AT", cookie.getValue());
249+
assertEquals("de-AT", cookie.getValue());
250250

251251
request = new MockHttpServletRequest();
252252
request.setCookies(cookie);
@@ -258,12 +258,12 @@ public void testSetAndResolveLocaleWithCountry() {
258258
}
259259

260260
@Test
261-
public void testSetAndResolveLocaleWithCountryAsLanguageTag() {
261+
public void testSetAndResolveLocaleWithCountryAsLegacyJava() {
262262
MockHttpServletRequest request = new MockHttpServletRequest();
263263
MockHttpServletResponse response = new MockHttpServletResponse();
264264

265265
CookieLocaleResolver resolver = new CookieLocaleResolver();
266-
resolver.setLanguageTagCompliant(true);
266+
resolver.setLanguageTagCompliant(false);
267267
resolver.setLocale(request, response, new Locale("de", "AT"));
268268

269269
Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME);
@@ -272,13 +272,12 @@ public void testSetAndResolveLocaleWithCountryAsLanguageTag() {
272272
assertEquals(null, cookie.getDomain());
273273
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath());
274274
assertFalse(cookie.getSecure());
275-
assertEquals("de-AT", cookie.getValue());
275+
assertEquals("de_AT", cookie.getValue());
276276

277277
request = new MockHttpServletRequest();
278278
request.setCookies(cookie);
279279

280280
resolver = new CookieLocaleResolver();
281-
resolver.setLanguageTagCompliant(true);
282281
Locale loc = resolver.resolveLocale(request);
283282
assertEquals("de", loc.getLanguage());
284283
assertEquals("AT", loc.getCountry());
@@ -315,7 +314,7 @@ public void testCustomCookie() {
315314
}
316315

317316
@Test
318-
public void testResolveLocaleWithoutCookie() throws Exception {
317+
public void testResolveLocaleWithoutCookie() {
319318
MockHttpServletRequest request = new MockHttpServletRequest();
320319
request.addPreferredLocale(Locale.TAIWAN);
321320

@@ -326,7 +325,7 @@ public void testResolveLocaleWithoutCookie() throws Exception {
326325
}
327326

328327
@Test
329-
public void testResolveLocaleContextWithoutCookie() throws Exception {
328+
public void testResolveLocaleContextWithoutCookie() {
330329
MockHttpServletRequest request = new MockHttpServletRequest();
331330
request.addPreferredLocale(Locale.TAIWAN);
332331

@@ -339,7 +338,7 @@ public void testResolveLocaleContextWithoutCookie() throws Exception {
339338
}
340339

341340
@Test
342-
public void testResolveLocaleWithoutCookieAndDefaultLocale() throws Exception {
341+
public void testResolveLocaleWithoutCookieAndDefaultLocale() {
343342
MockHttpServletRequest request = new MockHttpServletRequest();
344343
request.addPreferredLocale(Locale.TAIWAN);
345344

@@ -351,7 +350,7 @@ public void testResolveLocaleWithoutCookieAndDefaultLocale() throws Exception {
351350
}
352351

353352
@Test
354-
public void testResolveLocaleContextWithoutCookieAndDefaultLocale() throws Exception {
353+
public void testResolveLocaleContextWithoutCookieAndDefaultLocale() {
355354
MockHttpServletRequest request = new MockHttpServletRequest();
356355
request.addPreferredLocale(Locale.TAIWAN);
357356

@@ -366,7 +365,7 @@ public void testResolveLocaleContextWithoutCookieAndDefaultLocale() throws Excep
366365
}
367366

368367
@Test
369-
public void testResolveLocaleWithCookieWithoutLocale() throws Exception {
368+
public void testResolveLocaleWithCookieWithoutLocale() {
370369
MockHttpServletRequest request = new MockHttpServletRequest();
371370
request.addPreferredLocale(Locale.TAIWAN);
372371
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, "");
@@ -379,7 +378,7 @@ public void testResolveLocaleWithCookieWithoutLocale() throws Exception {
379378
}
380379

381380
@Test
382-
public void testResolveLocaleContextWithCookieWithoutLocale() throws Exception {
381+
public void testResolveLocaleContextWithCookieWithoutLocale() {
383382
MockHttpServletRequest request = new MockHttpServletRequest();
384383
request.addPreferredLocale(Locale.TAIWAN);
385384
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, "");
@@ -394,7 +393,7 @@ public void testResolveLocaleContextWithCookieWithoutLocale() throws Exception {
394393
}
395394

396395
@Test
397-
public void testSetLocaleToNull() throws Exception {
396+
public void testSetLocaleToNull() {
398397
MockHttpServletRequest request = new MockHttpServletRequest();
399398
request.addPreferredLocale(Locale.TAIWAN);
400399
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());
@@ -414,7 +413,7 @@ public void testSetLocaleToNull() throws Exception {
414413
}
415414

416415
@Test
417-
public void testSetLocaleContextToNull() throws Exception {
416+
public void testSetLocaleContextToNull() {
418417
MockHttpServletRequest request = new MockHttpServletRequest();
419418
request.addPreferredLocale(Locale.TAIWAN);
420419
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());
@@ -436,7 +435,7 @@ public void testSetLocaleContextToNull() throws Exception {
436435
}
437436

438437
@Test
439-
public void testSetLocaleToNullWithDefault() throws Exception {
438+
public void testSetLocaleToNullWithDefault() {
440439
MockHttpServletRequest request = new MockHttpServletRequest();
441440
request.addPreferredLocale(Locale.TAIWAN);
442441
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());
@@ -457,7 +456,7 @@ public void testSetLocaleToNullWithDefault() throws Exception {
457456
}
458457

459458
@Test
460-
public void testSetLocaleContextToNullWithDefault() throws Exception {
459+
public void testSetLocaleContextToNullWithDefault() {
461460
MockHttpServletRequest request = new MockHttpServletRequest();
462461
request.addPreferredLocale(Locale.TAIWAN);
463462
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());

0 commit comments

Comments
 (0)