diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java index 7212068a83d..2cdbfffde04 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java @@ -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 { @Override + @Nullable public Locale convert(String source) { return StringUtils.parseLocale(source); } diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 7f4daf49a44..4b26b0c996a 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -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"); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java index 00af82fea16..8094974b916 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java @@ -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}. + *

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. - *

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 + *

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()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java index bdf80fce815..5770a86fc13 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java @@ -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}. + *

NOTE: As of 5.1, this resolver leniently accepts the legacy + * {@link Locale#toString} format as well as BCP 47 language tags. * @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. - *

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 + *

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); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java index a03c0dd8f44..cffd958aa17 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java @@ -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());