From 0dd320f92e31d516df9c5f85d4ddbe16ffeaa2cc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 21 Dec 2015 18:40:26 +0100 Subject: [PATCH] Support for BCP 47 language tags Issue: SPR-13032 --- .../servlet/i18n/CookieLocaleResolver.java | 65 +++++++++++++++++-- .../servlet/i18n/LocaleChangeInterceptor.java | 41 +++++++++++- .../i18n/CookieLocaleResolverTests.java | 54 ++++++++++++++- 3 files changed, 153 insertions(+), 7 deletions(-) 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 4bf43e2845..8039f6b952 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-2013 the original author or authors. + * Copyright 2002-2015 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. @@ -81,6 +81,8 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; + private boolean languageTagCompliant = false; + private Locale defaultLocale; private TimeZone defaultTimeZone; @@ -94,6 +96,30 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte setCookieName(DEFAULT_COOKIE_NAME); } + + /** + * 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}. + *

Note: This mode requires JDK 7 or higher. Set this flag to {@code true} + * for BCP 47 compliance on JDK 7+ only. + * @since 4.3 + * @see Locale#forLanguageTag(String) + * @see Locale#toLanguageTag() + */ + public void setLanguageTagCompliant(boolean languageTagCompliant) { + this.languageTagCompliant = languageTagCompliant; + } + + /** + * Return whether this resolver's cookies should be compliant with BCP 47 + * language tags instead of Java's legacy locale specification format. + * @since 4.3 + */ + public boolean isLanguageTagCompliant() { + return this.languageTagCompliant; + } + /** * Set a fixed Locale that this resolver will return if no cookie found. */ @@ -111,6 +137,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte /** * Set a fixed TimeZone that this resolver will return if no cookie found. + * @since 4.0 */ public void setDefaultTimeZone(TimeZone defaultTimeZone) { this.defaultTimeZone = defaultTimeZone; @@ -119,6 +146,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte /** * Return the fixed TimeZone that this resolver will return if no cookie found, * if any. + * @since 4.0 */ protected TimeZone getDefaultTimeZone() { return this.defaultTimeZone; @@ -161,7 +189,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte localePart = value.substring(0, spaceIndex); timeZonePart = value.substring(spaceIndex + 1); } - locale = (!"-".equals(localePart) ? StringUtils.parseLocaleString(localePart) : null); + locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null); if (timeZonePart != null) { timeZone = StringUtils.parseTimeZoneString(timeZonePart); } @@ -171,7 +199,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte } } request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, - (locale != null ? locale: determineDefaultLocale(request))); + (locale != null ? locale : determineDefaultLocale(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, (timeZone != null ? timeZone : determineDefaultTimeZone(request))); } @@ -191,18 +219,45 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte if (localeContext instanceof TimeZoneAwareLocaleContext) { timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); } - addCookie(response, (locale != null ? locale : "-") + (timeZone != null ? ' ' + timeZone.getID() : "")); + addCookie(response, + (locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : "")); } else { removeCookie(response); } request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, - (locale != null ? locale: determineDefaultLocale(request))); + (locale != null ? locale : determineDefaultLocale(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, (timeZone != null ? timeZone : determineDefaultTimeZone(request))); } + /** + * 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 + * @return the corresponding {@code Locale} instance + * @since 4.3 + */ + protected Locale parseLocaleValue(String locale) { + return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale)); + } + + /** + * Render the given locale as a text value for inclusion in a cookie. + *

The default implementation calls {@link Locale#toString()} + * or JDK 7's {@link Locale#toLanguageTag()}, depending on the + * {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property. + * @param locale the locale to stringify + * @return a String representation for the given locale + * @since 4.3 + */ + protected String toLocaleValue(Locale locale) { + return (isLanguageTagCompliant() ? locale.toLanguageTag() : locale.toString()); + } + /** * Determine the default locale for the given request, * Called if no locale cookie has been found. 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 099e4b466d..1d0083fc6c 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 @@ -16,6 +16,7 @@ package org.springframework.web.servlet.i18n; +import java.util.Locale; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -54,6 +55,8 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { private boolean ignoreInvalidLocale = false; + private boolean languageTagCompliant = false; + /** * Set the name of the parameter that contains a locale specification @@ -104,6 +107,29 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { return this.ignoreInvalidLocale; } + /** + * 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: This mode requires JDK 7 or higher. Set this flag to {@code true} + * for BCP 47 compliance on JDK 7+ only. + * @since 4.3 + * @see Locale#forLanguageTag(String) + * @see Locale#toLanguageTag() + */ + public void setLanguageTagCompliant(boolean languageTagCompliant) { + this.languageTagCompliant = languageTagCompliant; + } + + /** + * Return whether to use BCP 47 language tags instead of Java's legacy + * locale specification format. + * @since 4.3 + */ + public boolean isLanguageTagCompliant() { + return this.languageTagCompliant; + } + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) @@ -118,7 +144,7 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { "No LocaleResolver found: not in a DispatcherServlet request?"); } try { - localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale)); + localeResolver.setLocale(request, response, parseLocaleValue(newLocale)); } catch (IllegalArgumentException ex) { if (isIgnoreInvalidLocale()) { @@ -147,4 +173,17 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { return false; } + /** + * 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 + * @return the corresponding {@code Locale} instance + * @since 4.3 + */ + protected Locale parseLocaleValue(String locale) { + return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale)); + } + } 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 96b72c9cb4..270f6ab03b 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-2013 the original author or authors. + * Copyright 2002-2015 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. @@ -164,6 +164,58 @@ public class CookieLocaleResolverTests { assertEquals(TimeZone.getTimeZone("GMT+1"), ((TimeZoneAwareLocaleContext) loc).getTimeZone()); } + @Test + public void testSetAndResolveLocaleWithCountry() { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + CookieLocaleResolver resolver = new CookieLocaleResolver(); + resolver.setLocale(request, response, new Locale("de", "AT")); + + Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME); + assertNotNull(cookie); + assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_NAME, cookie.getName()); + assertEquals(null, cookie.getDomain()); + assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath()); + assertFalse(cookie.getSecure()); + assertEquals("de_AT", cookie.getValue()); + + request = new MockHttpServletRequest(); + request.setCookies(cookie); + + resolver = new CookieLocaleResolver(); + Locale loc = resolver.resolveLocale(request); + assertEquals("de", loc.getLanguage()); + assertEquals("AT", loc.getCountry()); + } + + @Test + public void testSetAndResolveLocaleWithCountryAsLanguageTag() { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + CookieLocaleResolver resolver = new CookieLocaleResolver(); + resolver.setLanguageTagCompliant(true); + resolver.setLocale(request, response, new Locale("de", "AT")); + + Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME); + assertNotNull(cookie); + assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_NAME, cookie.getName()); + assertEquals(null, cookie.getDomain()); + assertEquals(CookieLocaleResolver.DEFAULT_COOKIE_PATH, cookie.getPath()); + assertFalse(cookie.getSecure()); + 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()); + } + @Test public void testCustomCookie() { MockHttpServletRequest request = new MockHttpServletRequest();