001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.i18n; 018 019import java.util.Locale; 020import java.util.TimeZone; 021import javax.servlet.http.Cookie; 022import javax.servlet.http.HttpServletRequest; 023import javax.servlet.http.HttpServletResponse; 024 025import org.springframework.context.i18n.LocaleContext; 026import org.springframework.context.i18n.SimpleLocaleContext; 027import org.springframework.context.i18n.TimeZoneAwareLocaleContext; 028import org.springframework.lang.UsesJava7; 029import org.springframework.util.StringUtils; 030import org.springframework.web.servlet.LocaleContextResolver; 031import org.springframework.web.servlet.LocaleResolver; 032import org.springframework.web.util.CookieGenerator; 033import org.springframework.web.util.WebUtils; 034 035/** 036 * {@link LocaleResolver} implementation that uses a cookie sent back to the user 037 * in case of a custom setting, with a fallback to the specified default locale 038 * or the request's accept-header locale. 039 * 040 * <p>This is particularly useful for stateless applications without user sessions. 041 * The cookie may optionally contain an associated time zone value as well; 042 * alternatively, you may specify a default time zone. 043 * 044 * <p>Custom controllers can override the user's locale and time zone by calling 045 * {@code #setLocale(Context)} on the resolver, e.g. responding to a locale change 046 * request. As a more convenient alternative, consider using 047 * {@link org.springframework.web.servlet.support.RequestContext#changeLocale}. 048 * 049 * @author Juergen Hoeller 050 * @author Jean-Pierre Pawlak 051 * @since 27.02.2003 052 * @see #setDefaultLocale 053 * @see #setDefaultTimeZone 054 */ 055public class CookieLocaleResolver extends CookieGenerator implements LocaleContextResolver { 056 057 /** 058 * The name of the request attribute that holds the {@code Locale}. 059 * <p>Only used for overriding a cookie value if the locale has been 060 * changed in the course of the current request! 061 * <p>Use {@code RequestContext(Utils).getLocale()} 062 * to retrieve the current locale in controllers or views. 063 * @see org.springframework.web.servlet.support.RequestContext#getLocale 064 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale 065 */ 066 public static final String LOCALE_REQUEST_ATTRIBUTE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; 067 068 /** 069 * The name of the request attribute that holds the {@code TimeZone}. 070 * <p>Only used for overriding a cookie value if the locale has been 071 * changed in the course of the current request! 072 * <p>Use {@code RequestContext(Utils).getTimeZone()} 073 * to retrieve the current time zone in controllers or views. 074 * @see org.springframework.web.servlet.support.RequestContext#getTimeZone 075 * @see org.springframework.web.servlet.support.RequestContextUtils#getTimeZone 076 */ 077 public static final String TIME_ZONE_REQUEST_ATTRIBUTE_NAME = CookieLocaleResolver.class.getName() + ".TIME_ZONE"; 078 079 /** 080 * The default cookie name used if none is explicitly set. 081 */ 082 public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; 083 084 085 private boolean languageTagCompliant = false; 086 087 private Locale defaultLocale; 088 089 private TimeZone defaultTimeZone; 090 091 092 /** 093 * Create a new instance of the {@link CookieLocaleResolver} class 094 * using the {@link #DEFAULT_COOKIE_NAME default cookie name}. 095 */ 096 public CookieLocaleResolver() { 097 setCookieName(DEFAULT_COOKIE_NAME); 098 } 099 100 101 /** 102 * Specify whether this resolver's cookies should be compliant with BCP 47 103 * language tags instead of Java's legacy locale specification format. 104 * The default is {@code false}. 105 * <p>Note: This mode requires JDK 7 or higher. Set this flag to {@code true} 106 * for BCP 47 compliance on JDK 7+ only. 107 * @since 4.3 108 * @see Locale#forLanguageTag(String) 109 * @see Locale#toLanguageTag() 110 */ 111 public void setLanguageTagCompliant(boolean languageTagCompliant) { 112 this.languageTagCompliant = languageTagCompliant; 113 } 114 115 /** 116 * Return whether this resolver's cookies should be compliant with BCP 47 117 * language tags instead of Java's legacy locale specification format. 118 * @since 4.3 119 */ 120 public boolean isLanguageTagCompliant() { 121 return this.languageTagCompliant; 122 } 123 124 /** 125 * Set a fixed locale that this resolver will return if no cookie found. 126 */ 127 public void setDefaultLocale(Locale defaultLocale) { 128 this.defaultLocale = defaultLocale; 129 } 130 131 /** 132 * Return the fixed locale that this resolver will return if no cookie found, 133 * if any. 134 */ 135 protected Locale getDefaultLocale() { 136 return this.defaultLocale; 137 } 138 139 /** 140 * Set a fixed time zone that this resolver will return if no cookie found. 141 * @since 4.0 142 */ 143 public void setDefaultTimeZone(TimeZone defaultTimeZone) { 144 this.defaultTimeZone = defaultTimeZone; 145 } 146 147 /** 148 * Return the fixed time zone that this resolver will return if no cookie found, 149 * if any. 150 * @since 4.0 151 */ 152 protected TimeZone getDefaultTimeZone() { 153 return this.defaultTimeZone; 154 } 155 156 157 @Override 158 public Locale resolveLocale(HttpServletRequest request) { 159 parseLocaleCookieIfNecessary(request); 160 return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); 161 } 162 163 @Override 164 public LocaleContext resolveLocaleContext(final HttpServletRequest request) { 165 parseLocaleCookieIfNecessary(request); 166 return new TimeZoneAwareLocaleContext() { 167 @Override 168 public Locale getLocale() { 169 return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); 170 } 171 @Override 172 public TimeZone getTimeZone() { 173 return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME); 174 } 175 }; 176 } 177 178 private void parseLocaleCookieIfNecessary(HttpServletRequest request) { 179 if (request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME) == null) { 180 Locale locale = null; 181 TimeZone timeZone = null; 182 183 // Retrieve and parse cookie value. 184 Cookie cookie = WebUtils.getCookie(request, getCookieName()); 185 if (cookie != null) { 186 String value = cookie.getValue(); 187 String localePart = value; 188 String timeZonePart = null; 189 int spaceIndex = localePart.indexOf(' '); 190 if (spaceIndex != -1) { 191 localePart = value.substring(0, spaceIndex); 192 timeZonePart = value.substring(spaceIndex + 1); 193 } 194 try { 195 locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null); 196 if (timeZonePart != null) { 197 timeZone = StringUtils.parseTimeZoneString(timeZonePart); 198 } 199 } 200 catch (IllegalArgumentException ex) { 201 if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) { 202 // Error dispatch: ignore locale/timezone parse exceptions 203 if (logger.isDebugEnabled()) { 204 logger.debug("Ignoring invalid locale cookie '" + getCookieName() + 205 "' with value [" + value + "] due to error dispatch: " + ex.getMessage()); 206 } 207 } 208 else { 209 throw new IllegalStateException("Invalid locale cookie '" + getCookieName() + 210 "' with value [" + value + "]: " + ex.getMessage()); 211 } 212 } 213 if (logger.isDebugEnabled()) { 214 logger.debug("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale + 215 "'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : "")); 216 } 217 } 218 219 request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, 220 (locale != null ? locale : determineDefaultLocale(request))); 221 request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, 222 (timeZone != null ? timeZone : determineDefaultTimeZone(request))); 223 } 224 } 225 226 @Override 227 public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { 228 setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null)); 229 } 230 231 @Override 232 public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) { 233 Locale locale = null; 234 TimeZone timeZone = null; 235 if (localeContext != null) { 236 locale = localeContext.getLocale(); 237 if (localeContext instanceof TimeZoneAwareLocaleContext) { 238 timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); 239 } 240 addCookie(response, 241 (locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : "")); 242 } 243 else { 244 removeCookie(response); 245 } 246 request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, 247 (locale != null ? locale : determineDefaultLocale(request))); 248 request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, 249 (timeZone != null ? timeZone : determineDefaultTimeZone(request))); 250 } 251 252 253 /** 254 * Parse the given locale value coming from an incoming cookie. 255 * <p>The default implementation calls {@link StringUtils#parseLocaleString(String)} 256 * or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the 257 * {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property. 258 * @param locale the locale value to parse 259 * @return the corresponding {@code Locale} instance 260 * @since 4.3 261 */ 262 @UsesJava7 263 protected Locale parseLocaleValue(String locale) { 264 return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale)); 265 } 266 267 /** 268 * Render the given locale as a text value for inclusion in a cookie. 269 * <p>The default implementation calls {@link Locale#toString()} 270 * or JDK 7's {@link Locale#toLanguageTag()}, depending on the 271 * {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property. 272 * @param locale the locale to stringify 273 * @return a String representation for the given locale 274 * @since 4.3 275 */ 276 @UsesJava7 277 protected String toLocaleValue(Locale locale) { 278 return (isLanguageTagCompliant() ? locale.toLanguageTag() : locale.toString()); 279 } 280 281 /** 282 * Determine the default locale for the given request, 283 * Called if no locale cookie has been found. 284 * <p>The default implementation returns the specified default locale, 285 * if any, else falls back to the request's accept-header locale. 286 * @param request the request to resolve the locale for 287 * @return the default locale (never {@code null}) 288 * @see #setDefaultLocale 289 * @see javax.servlet.http.HttpServletRequest#getLocale() 290 */ 291 protected Locale determineDefaultLocale(HttpServletRequest request) { 292 Locale defaultLocale = getDefaultLocale(); 293 if (defaultLocale == null) { 294 defaultLocale = request.getLocale(); 295 } 296 return defaultLocale; 297 } 298 299 /** 300 * Determine the default time zone for the given request, 301 * Called if no time zone cookie has been found. 302 * <p>The default implementation returns the specified default time zone, 303 * if any, or {@code null} otherwise. 304 * @param request the request to resolve the time zone for 305 * @return the default time zone (or {@code null} if none defined) 306 * @see #setDefaultTimeZone 307 */ 308 protected TimeZone determineDefaultTimeZone(HttpServletRequest request) { 309 return getDefaultTimeZone(); 310 } 311 312}