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