/*
 * Copyright 2002-2020 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.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.i18n;

import java.util.Locale;
import java.util.TimeZone;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.util.CookieGenerator;
import org.springframework.web.util.WebUtils;

LocaleResolver implementation that uses a cookie sent back to the user in case of a custom setting, with a fallback to the specified default locale or the request's accept-header locale.

This is particularly useful for stateless applications without user sessions. The cookie may optionally contain an associated time zone value as well; alternatively, you may specify a default time zone.

Custom controllers can override the user's locale and time zone by calling #setLocale(Context) on the resolver, e.g. responding to a locale change request. As a more convenient alternative, consider using RequestContext.changeLocale.

Author:Juergen Hoeller, Jean-Pierre Pawlak
See Also:
Since:27.02.2003
/** * {@link LocaleResolver} implementation that uses a cookie sent back to the user * in case of a custom setting, with a fallback to the specified default locale * or the request's accept-header locale. * * <p>This is particularly useful for stateless applications without user sessions. * The cookie may optionally contain an associated time zone value as well; * alternatively, you may specify a default time zone. * * <p>Custom controllers can override the user's locale and time zone by calling * {@code #setLocale(Context)} on the resolver, e.g. responding to a locale change * request. As a more convenient alternative, consider using * {@link org.springframework.web.servlet.support.RequestContext#changeLocale}. * * @author Juergen Hoeller * @author Jean-Pierre Pawlak * @since 27.02.2003 * @see #setDefaultLocale * @see #setDefaultTimeZone */
public class CookieLocaleResolver extends CookieGenerator implements LocaleContextResolver {
The name of the request attribute that holds the Locale.

Only used for overriding a cookie value if the locale has been changed in the course of the current request!

Use RequestContext(Utils).getLocale() to retrieve the current locale in controllers or views.

See Also:
/** * The name of the request attribute that holds the {@code Locale}. * <p>Only used for overriding a cookie value if the locale has been * changed in the course of the current request! * <p>Use {@code RequestContext(Utils).getLocale()} * to retrieve the current locale in controllers or views. * @see org.springframework.web.servlet.support.RequestContext#getLocale * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale */
public static final String LOCALE_REQUEST_ATTRIBUTE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";
The name of the request attribute that holds the TimeZone.

Only used for overriding a cookie value if the locale has been changed in the course of the current request!

Use RequestContext(Utils).getTimeZone() to retrieve the current time zone in controllers or views.

See Also:
/** * The name of the request attribute that holds the {@code TimeZone}. * <p>Only used for overriding a cookie value if the locale has been * changed in the course of the current request! * <p>Use {@code RequestContext(Utils).getTimeZone()} * to retrieve the current time zone in controllers or views. * @see org.springframework.web.servlet.support.RequestContext#getTimeZone * @see org.springframework.web.servlet.support.RequestContextUtils#getTimeZone */
public static final String TIME_ZONE_REQUEST_ATTRIBUTE_NAME = CookieLocaleResolver.class.getName() + ".TIME_ZONE";
The default cookie name used if none is explicitly set.
/** * The default cookie name used if none is explicitly set. */
public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; private boolean languageTagCompliant = true; private boolean rejectInvalidCookies = true; @Nullable private Locale defaultLocale; @Nullable private TimeZone defaultTimeZone;
Create a new instance of the CookieLocaleResolver class using the default cookie name.
/** * Create a new instance of the {@link CookieLocaleResolver} class * using the {@link #DEFAULT_COOKIE_NAME default cookie name}. */
public CookieLocaleResolver() { 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 true, as of 5.1. Switch this to false for rendering Java's legacy locale specification format. For parsing, this resolver leniently accepts the legacy Locale.toString format as well as BCP 47 language tags in any case.

See Also:
Since:4.3
/** * Specify whether this resolver's cookies should be compliant with BCP 47 * language tags instead of Java's legacy locale specification format. * <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() */
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
/** * 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; }
Specify whether to reject cookies with invalid content (e.g. invalid format).

The default is true. Turn this off for lenient handling of parse failures, falling back to the default locale and time zone in such a case.

See Also:
Since:5.1.7
/** * Specify whether to reject cookies with invalid content (e.g. invalid format). * <p>The default is {@code true}. Turn this off for lenient handling of parse * failures, falling back to the default locale and time zone in such a case. * @since 5.1.7 * @see #setDefaultLocale * @see #setDefaultTimeZone * @see #determineDefaultLocale * @see #determineDefaultTimeZone */
public void setRejectInvalidCookies(boolean rejectInvalidCookies) { this.rejectInvalidCookies = rejectInvalidCookies; }
Return whether to reject cookies with invalid content (e.g. invalid format).
Since:5.1.7
/** * Return whether to reject cookies with invalid content (e.g. invalid format). * @since 5.1.7 */
public boolean isRejectInvalidCookies() { return this.rejectInvalidCookies; }
Set a fixed locale that this resolver will return if no cookie found.
/** * Set a fixed locale that this resolver will return if no cookie found. */
public void setDefaultLocale(@Nullable Locale defaultLocale) { this.defaultLocale = defaultLocale; }
Return the fixed locale that this resolver will return if no cookie found, if any.
/** * Return the fixed locale that this resolver will return if no cookie found, * if any. */
@Nullable protected Locale getDefaultLocale() { return this.defaultLocale; }
Set a fixed time zone that this resolver will return if no cookie found.
Since:4.0
/** * Set a fixed time zone that this resolver will return if no cookie found. * @since 4.0 */
public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) { this.defaultTimeZone = defaultTimeZone; }
Return the fixed time zone that this resolver will return if no cookie found, if any.
Since:4.0
/** * Return the fixed time zone that this resolver will return if no cookie found, * if any. * @since 4.0 */
@Nullable protected TimeZone getDefaultTimeZone() { return this.defaultTimeZone; } @Override public Locale resolveLocale(HttpServletRequest request) { parseLocaleCookieIfNecessary(request); return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); } @Override public LocaleContext resolveLocaleContext(final HttpServletRequest request) { parseLocaleCookieIfNecessary(request); return new TimeZoneAwareLocaleContext() { @Override @Nullable public Locale getLocale() { return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); } @Override @Nullable public TimeZone getTimeZone() { return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME); } }; } private void parseLocaleCookieIfNecessary(HttpServletRequest request) { if (request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME) == null) { Locale locale = null; TimeZone timeZone = null; // Retrieve and parse cookie value. String cookieName = getCookieName(); if (cookieName != null) { Cookie cookie = WebUtils.getCookie(request, cookieName); if (cookie != null) { String value = cookie.getValue(); String localePart = value; String timeZonePart = null; 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); if (timeZonePart != null) { timeZone = StringUtils.parseTimeZoneString(timeZonePart); } } catch (IllegalArgumentException ex) { if (isRejectInvalidCookies() && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { throw new IllegalStateException("Encountered invalid locale cookie '" + cookieName + "': [" + value + "] due to: " + ex.getMessage()); } else { // Lenient handling (e.g. error dispatch): ignore locale/timezone parse exceptions if (logger.isDebugEnabled()) { logger.debug("Ignoring invalid locale cookie '" + cookieName + "': [" + value + "] due to: " + ex.getMessage()); } } } if (logger.isTraceEnabled()) { logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale + "'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : "")); } } } request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, (locale != null ? locale : determineDefaultLocale(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, (timeZone != null ? timeZone : determineDefaultTimeZone(request))); } } @Override public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) { setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null)); } @Override public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable LocaleContext localeContext) { Assert.notNull(response, "HttpServletResponse is required for CookieLocaleResolver"); Locale locale = null; TimeZone timeZone = null; if (localeContext != null) { locale = localeContext.getLocale(); if (localeContext instanceof TimeZoneAwareLocaleContext) { timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); } addCookie(response, (locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? '/' + timeZone.getID() : "")); } else { removeCookie(response); } request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, (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 StringUtils.parseLocale(String), accepting the Locale.toString format as well as BCP 47 language tags.

Params:
  • localeValue – the locale value to parse
See Also:
Returns:the corresponding Locale instance
Since:4.3
/** * Parse the given locale value coming from an incoming cookie. * <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 localeValue) { return StringUtils.parseLocale(localeValue); }
Render the given locale as a text value for inclusion in a cookie.

The default implementation calls Locale.toString() or JDK 7's Locale.toLanguageTag(), depending on the "languageTagCompliant" configuration property.

Params:
  • locale – the locale to stringify
See Also:
Returns:a String representation for the given locale
Since:4.3
/** * Render the given locale as a text value for inclusion in a cookie. * <p>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 * @see #isLanguageTagCompliant() */
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.

The default implementation returns the specified default locale, if any, else falls back to the request's accept-header locale.

Params:
  • request – the request to resolve the locale for
See Also:
Returns:the default locale (never null)
/** * Determine the default locale for the given request, * Called if no locale cookie has been found. * <p>The default implementation returns the specified default locale, * if any, else falls back to the request's accept-header locale. * @param request the request to resolve the locale for * @return the default locale (never {@code null}) * @see #setDefaultLocale * @see javax.servlet.http.HttpServletRequest#getLocale() */
protected Locale determineDefaultLocale(HttpServletRequest request) { Locale defaultLocale = getDefaultLocale(); if (defaultLocale == null) { defaultLocale = request.getLocale(); } return defaultLocale; }
Determine the default time zone for the given request, Called if no time zone cookie has been found.

The default implementation returns the specified default time zone, if any, or null otherwise.

Params:
  • request – the request to resolve the time zone for
See Also:
Returns:the default time zone (or null if none defined)
/** * Determine the default time zone for the given request, * Called if no time zone cookie has been found. * <p>The default implementation returns the specified default time zone, * if any, or {@code null} otherwise. * @param request the request to resolve the time zone for * @return the default time zone (or {@code null} if none defined) * @see #setDefaultTimeZone */
@Nullable protected TimeZone determineDefaultTimeZone(HttpServletRequest request) { return getDefaultTimeZone(); } }