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
This commit is contained in:
Juergen Hoeller 2018-07-17 17:57:59 +02:00
parent 955665b419
commit 88e4006790
5 changed files with 68 additions and 51 deletions

View File

@ -19,6 +19,7 @@ package org.springframework.core.convert.support;
import java.util.Locale;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
@ -35,6 +36,7 @@ import org.springframework.util.StringUtils;
final class StringToLocaleConverter implements Converter<String, Locale> {
@Override
@Nullable
public Locale convert(String source) {
return StringUtils.parseLocale(source);
}

View File

@ -772,7 +772,9 @@ public abstract class StringUtils {
public static Locale parseLocale(String localeValue) {
String[] tokens = tokenizeLocaleSource(localeValue);
if (tokens.length == 1) {
return Locale.forLanguageTag(localeValue);
validateLocalePart(localeValue);
Locale resolved = Locale.forLanguageTag(localeValue);
return (resolved.getLanguage().length() > 0 ? resolved : null);
}
return parseLocaleTokens(localeValue, tokens);
}
@ -821,7 +823,7 @@ public abstract class StringUtils {
private static void validateLocalePart(String localePart) {
for (int i = 0; i < localePart.length(); i++) {
char ch = localePart.charAt(i);
if (ch != ' ' && ch != '_' && ch != '#' && !Character.isLetterOrDigit(ch)) {
if (ch != ' ' && ch != '_' && ch != '-' && ch != '#' && !Character.isLetterOrDigit(ch)) {
throw new IllegalArgumentException(
"Locale part \"" + localePart + "\" contains invalid characters");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@ -83,7 +83,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";
private boolean languageTagCompliant = false;
private boolean languageTagCompliant = true;
@Nullable
private Locale defaultLocale;
@ -104,8 +104,13 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
/**
* Specify whether this resolver's cookies should be compliant with BCP 47
* language tags instead of Java's legacy locale specification format.
* The default is {@code false}.
* <p>The default is {@code true}, as of 5.1. Switch this to {@code false}
* for rendering Java's legacy locale specification format. For parsing,
* this resolver leniently accepts the legacy {@link Locale#toString}
* format as well as BCP 47 language tags in any case.
* @since 4.3
* @see #parseLocaleValue(String)
* @see #toLocaleValue(Locale)
* @see Locale#forLanguageTag(String)
* @see Locale#toLanguageTag()
*/
@ -193,10 +198,14 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
String value = cookie.getValue();
String localePart = value;
String timeZonePart = null;
int spaceIndex = localePart.indexOf(' ');
if (spaceIndex != -1) {
localePart = value.substring(0, spaceIndex);
timeZonePart = value.substring(spaceIndex + 1);
int separatorIndex = localePart.indexOf('/');
if (separatorIndex == -1) {
// Leniently accept older cookies separated by a space...
separatorIndex = localePart.indexOf(' ');
}
if (separatorIndex >= 0) {
localePart = value.substring(0, separatorIndex);
timeZonePart = value.substring(separatorIndex + 1);
}
try {
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
@ -205,16 +214,16 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
}
}
catch (IllegalArgumentException ex) {
String reason = "Ignoring invalid locale cookie '" +
cookieName + ":" + value + "' due to: " + ex.getMessage();
String cookieDescription = "invalid locale cookie '" + cookieName +
"': [" + value + "] due to: " + ex.getMessage();
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
// Error dispatch: ignore locale/timezone parse exceptions
if (logger.isDebugEnabled()) {
logger.debug(reason);
logger.debug("Ignoring " + cookieDescription);
}
}
else {
throw new IllegalStateException(reason);
throw new IllegalStateException("Encountered " + cookieDescription);
}
}
if (logger.isTraceEnabled()) {
@ -250,7 +259,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
}
addCookie(response,
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? '/' + timeZone.getID() : ""));
}
else {
removeCookie(response);
@ -264,16 +273,16 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
/**
* Parse the given locale value coming from an incoming cookie.
* <p>The default implementation calls {@link StringUtils#parseLocaleString(String)}
* or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the
* {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property.
* @param locale the locale value to parse
* <p>The default implementation calls {@link StringUtils#parseLocale(String)},
* accepting the {@link Locale#toString} format as well as BCP 47 language tags.
* @param localeValue the locale value to parse
* @return the corresponding {@code Locale} instance
* @since 4.3
* @see StringUtils#parseLocale(String)
*/
@Nullable
protected Locale parseLocaleValue(String locale) {
return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale));
protected Locale parseLocaleValue(String localeValue) {
return StringUtils.parseLocale(localeValue);
}
/**
@ -284,6 +293,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
* @param locale the locale to stringify
* @return a String representation for the given locale
* @since 4.3
* @see #isLanguageTagCompliant()
*/
protected String toLocaleValue(Locale locale) {
return (isLanguageTagCompliant() ? locale.toLanguageTag() : locale.toString());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@ -57,8 +57,6 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
private boolean ignoreInvalidLocale = false;
private boolean languageTagCompliant = false;
/**
* Set the name of the parameter that contains a locale specification
@ -113,22 +111,29 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
/**
* Specify whether to parse request parameter values as BCP 47 language tags
* instead of Java's legacy locale specification format.
* The default is {@code false}.
* <p><b>NOTE: As of 5.1, this resolver leniently accepts the legacy
* {@link Locale#toString} format as well as BCP 47 language tags.</b>
* @since 4.3
* @see Locale#forLanguageTag(String)
* @see Locale#toLanguageTag()
* @deprecated as of 5.1 since it only accepts {@code true} now
*/
@Deprecated
public void setLanguageTagCompliant(boolean languageTagCompliant) {
this.languageTagCompliant = languageTagCompliant;
if (!languageTagCompliant) {
throw new IllegalArgumentException("LocaleChangeInterceptor always accepts BCP 47 language tags");
}
}
/**
* Return whether to use BCP 47 language tags instead of Java's legacy
* locale specification format.
* @since 4.3
* @deprecated as of 5.1 since it always returns {@code true} now
*/
@Deprecated
public boolean isLanguageTagCompliant() {
return this.languageTagCompliant;
return true;
}
@ -176,16 +181,15 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
/**
* Parse the given locale value as coming from a request parameter.
* <p>The default implementation calls {@link StringUtils#parseLocaleString(String)}
* or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the
* {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property.
* @param locale the locale value to parse
* <p>The default implementation calls {@link StringUtils#parseLocale(String)},
* accepting the {@link Locale#toString} format as well as BCP 47 language tags.
* @param localeValue the locale value to parse
* @return the corresponding {@code Locale} instance
* @since 4.3
*/
@Nullable
protected Locale parseLocaleValue(String locale) {
return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale));
protected Locale parseLocaleValue(String localeValue) {
return StringUtils.parseLocale(localeValue);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@ -83,7 +83,7 @@ public class CookieLocaleResolverTests {
@Test
public void testResolveLocaleContextWithInvalidLocale() {
MockHttpServletRequest request = new MockHttpServletRequest();
Cookie cookie = new Cookie("LanguageKoekje", "n-x GMT+1");
Cookie cookie = new Cookie("LanguageKoekje", "++ GMT+1");
request.setCookies(cookie);
CookieLocaleResolver resolver = new CookieLocaleResolver();
@ -94,7 +94,7 @@ public class CookieLocaleResolverTests {
}
catch (IllegalStateException ex) {
assertTrue(ex.getMessage().contains("LanguageKoekje"));
assertTrue(ex.getMessage().contains("n-x GMT+1"));
assertTrue(ex.getMessage().contains("++ GMT+1"));
}
}
@ -103,7 +103,7 @@ public class CookieLocaleResolverTests {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.GERMAN);
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, new ServletException());
Cookie cookie = new Cookie("LanguageKoekje", "n-x GMT+1");
Cookie cookie = new Cookie("LanguageKoekje", "++ GMT+1");
request.setCookies(cookie);
CookieLocaleResolver resolver = new CookieLocaleResolver();
@ -246,7 +246,7 @@ public class CookieLocaleResolverTests {
assertEquals(null, cookie.getDomain());
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath());
assertFalse(cookie.getSecure());
assertEquals("de_AT", cookie.getValue());
assertEquals("de-AT", cookie.getValue());
request = new MockHttpServletRequest();
request.setCookies(cookie);
@ -258,12 +258,12 @@ public class CookieLocaleResolverTests {
}
@Test
public void testSetAndResolveLocaleWithCountryAsLanguageTag() {
public void testSetAndResolveLocaleWithCountryAsLegacyJava() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setLanguageTagCompliant(true);
resolver.setLanguageTagCompliant(false);
resolver.setLocale(request, response, new Locale("de", "AT"));
Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME);
@ -272,13 +272,12 @@ public class CookieLocaleResolverTests {
assertEquals(null, cookie.getDomain());
assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath());
assertFalse(cookie.getSecure());
assertEquals("de-AT", cookie.getValue());
assertEquals("de_AT", cookie.getValue());
request = new MockHttpServletRequest();
request.setCookies(cookie);
resolver = new CookieLocaleResolver();
resolver.setLanguageTagCompliant(true);
Locale loc = resolver.resolveLocale(request);
assertEquals("de", loc.getLanguage());
assertEquals("AT", loc.getCountry());
@ -315,7 +314,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testResolveLocaleWithoutCookie() throws Exception {
public void testResolveLocaleWithoutCookie() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
@ -326,7 +325,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testResolveLocaleContextWithoutCookie() throws Exception {
public void testResolveLocaleContextWithoutCookie() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
@ -339,7 +338,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testResolveLocaleWithoutCookieAndDefaultLocale() throws Exception {
public void testResolveLocaleWithoutCookieAndDefaultLocale() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
@ -351,7 +350,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testResolveLocaleContextWithoutCookieAndDefaultLocale() throws Exception {
public void testResolveLocaleContextWithoutCookieAndDefaultLocale() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
@ -366,7 +365,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testResolveLocaleWithCookieWithoutLocale() throws Exception {
public void testResolveLocaleWithCookieWithoutLocale() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, "");
@ -379,7 +378,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testResolveLocaleContextWithCookieWithoutLocale() throws Exception {
public void testResolveLocaleContextWithCookieWithoutLocale() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, "");
@ -394,7 +393,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testSetLocaleToNull() throws Exception {
public void testSetLocaleToNull() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());
@ -414,7 +413,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testSetLocaleContextToNull() throws Exception {
public void testSetLocaleContextToNull() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());
@ -436,7 +435,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testSetLocaleToNullWithDefault() throws Exception {
public void testSetLocaleToNullWithDefault() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());
@ -457,7 +456,7 @@ public class CookieLocaleResolverTests {
}
@Test
public void testSetLocaleContextToNullWithDefault() throws Exception {
public void testSetLocaleContextToNullWithDefault() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(Locale.TAIWAN);
Cookie cookie = new Cookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME, Locale.UK.toString());