Skip to content

Commit 7ea49fa

Browse files
vpavicrstoyanchev
authored andcommitted
Refactor CookieLocaleResolver
At present, CookieLocaleResolver extends CookieGenerator instead of AbstractLocale(Context)Resolver like other LocaleResolver implementations. This means it duplicates some common aspects of LocaleResolver hierarchy while also exposing some CookieGenerator operations, such as #addCookie and #removeCookie. Additionally, CookieGenerator's support for writing cookies is based on Servlet support which at current baseline doesn't support SameSite directive. This commit refactors CookieLocaleResolver to make it extend AbstractLocaleContextResolver and also replaces CookieGenerator's cookie writing support with newer and more capable ResponseCookie. Simplify creation of CookieLocaleResolver with custom cookie name This commit introduces CookieLocaleResolver constructor that accepts cookie name thus allowing for a simpler creation of an instance with the desired cookie name. See gh-28779
1 parent f6d1806 commit 7ea49fa

File tree

4 files changed

+200
-105
lines changed

4 files changed

+200
-105
lines changed

spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,13 +30,12 @@
3030
* given response.
3131
*
3232
* <p>Can serve as base class for components that generate specific cookies,
33-
* such as CookieLocaleResolver and CookieThemeResolver.
33+
* such as CookieThemeResolver.
3434
*
3535
* @author Juergen Hoeller
3636
* @since 1.1.4
3737
* @see #addCookie
3838
* @see #removeCookie
39-
* @see org.springframework.web.servlet.i18n.CookieLocaleResolver
4039
* @see org.springframework.web.servlet.theme.CookieThemeResolver
4140
*/
4241
public class CookieGenerator {

spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java

Lines changed: 172 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,25 @@
1616

1717
package org.springframework.web.servlet.i18n;
1818

19+
import java.time.Duration;
1920
import java.util.Locale;
2021
import java.util.TimeZone;
2122
import java.util.function.Function;
2223

2324
import jakarta.servlet.http.Cookie;
2425
import jakarta.servlet.http.HttpServletRequest;
2526
import jakarta.servlet.http.HttpServletResponse;
27+
import org.apache.commons.logging.Log;
28+
import org.apache.commons.logging.LogFactory;
2629

2730
import org.springframework.context.i18n.LocaleContext;
28-
import org.springframework.context.i18n.SimpleLocaleContext;
2931
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
32+
import org.springframework.http.HttpHeaders;
33+
import org.springframework.http.ResponseCookie;
3034
import org.springframework.lang.Nullable;
3135
import org.springframework.util.Assert;
3236
import org.springframework.util.StringUtils;
33-
import org.springframework.web.servlet.LocaleContextResolver;
3437
import org.springframework.web.servlet.LocaleResolver;
35-
import org.springframework.web.util.CookieGenerator;
3638
import org.springframework.web.util.WebUtils;
3739

3840
/**
@@ -57,7 +59,7 @@
5759
* @see #setDefaultLocale
5860
* @see #setDefaultTimeZone
5961
*/
60-
public class CookieLocaleResolver extends CookieGenerator implements LocaleContextResolver {
62+
public class CookieLocaleResolver extends AbstractLocaleContextResolver {
6163

6264
/**
6365
* The name of the request attribute that holds the {@code Locale}.
@@ -86,16 +88,39 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
8688
*/
8789
public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";
8890

91+
/**
92+
* The default cookie max age (persists until browser shutdown) if none is
93+
* explicitly set.
94+
*/
95+
public static final Duration DEFAULT_COOKIE_MAX_AGE = Duration.ofSeconds(-1);
8996

90-
private boolean languageTagCompliant = true;
97+
/**
98+
* The default cookie path used if none is explicitly set.
99+
*/
100+
public static final String DEFAULT_COOKIE_PATH = "/";
91101

92-
private boolean rejectInvalidCookies = true;
102+
private static final Log logger = LogFactory.getLog(CookieLocaleResolver.class);
103+
104+
105+
private String cookieName;
106+
107+
private Duration cookieMaxAge = DEFAULT_COOKIE_MAX_AGE;
93108

94109
@Nullable
95-
private Locale defaultLocale;
110+
private String cookiePath = DEFAULT_COOKIE_PATH;
96111

97112
@Nullable
98-
private TimeZone defaultTimeZone;
113+
private String cookieDomain;
114+
115+
private boolean cookieSecure;
116+
117+
private boolean cookieHttpOnly;
118+
119+
private String cookieSameSite = "Lax";
120+
121+
private boolean languageTagCompliant = true;
122+
123+
private boolean rejectInvalidCookies = true;
99124

100125
private Function<HttpServletRequest, Locale> defaultLocaleFunction = request -> {
101126
Locale defaultLocale = getDefaultLocale();
@@ -104,15 +129,99 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
104129

105130
private Function<HttpServletRequest, TimeZone> defaultTimeZoneFunction = request -> getDefaultTimeZone();
106131

132+
133+
/**
134+
* Create a new instance of {@link CookieLocaleResolver} using the supplied
135+
* cookie name.
136+
* @param cookieName the cookie name
137+
* @since 6.0
138+
*/
139+
public CookieLocaleResolver(String cookieName) {
140+
Assert.notNull(cookieName, "cookieName must not be null");
141+
this.cookieName = cookieName;
142+
}
143+
107144
/**
108145
* Create a new instance of {@link CookieLocaleResolver} using the
109146
* {@linkplain #DEFAULT_COOKIE_NAME default cookie name}.
110147
*/
111148
public CookieLocaleResolver() {
112-
setCookieName(DEFAULT_COOKIE_NAME);
149+
this(DEFAULT_COOKIE_NAME);
113150
}
114151

115152

153+
/**
154+
* Set the name of cookie created by this resolver.
155+
* @param cookieName the cookie name
156+
* @deprecated since 6.0, in favor of {@link #CookieLocaleResolver(String)}
157+
*/
158+
@Deprecated
159+
public void setCookieName(String cookieName) {
160+
Assert.notNull(cookieName, "cookieName must not be null");
161+
this.cookieName = cookieName;
162+
}
163+
164+
/**
165+
* Set the cookie "Max-Age" attribute.
166+
* @since 6.0
167+
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#maxAge(Duration)
168+
*/
169+
public void setCookieMaxAge(Duration cookieMaxAge) {
170+
Assert.notNull(cookieMaxAge, "cookieMaxAge must not be null");
171+
this.cookieMaxAge = cookieMaxAge;
172+
}
173+
174+
/**
175+
* Variant of {@link #setCookieMaxAge(Duration)} accepting a value in
176+
* seconds.
177+
*/
178+
@Deprecated
179+
public void setCookieMaxAge(@Nullable Integer cookieMaxAge) {
180+
setCookieMaxAge(Duration.ofSeconds((cookieMaxAge != null) ? cookieMaxAge : -1));
181+
}
182+
183+
/**
184+
* Set the cookie "Path" attribute.
185+
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#path(String)
186+
*/
187+
public void setCookiePath(@Nullable String cookiePath) {
188+
this.cookiePath = cookiePath;
189+
}
190+
191+
/**
192+
* Set the cookie "Domain" attribute.
193+
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#domain(String)
194+
*/
195+
public void setCookieDomain(@Nullable String cookieDomain) {
196+
this.cookieDomain = cookieDomain;
197+
}
198+
199+
/**
200+
* Add the "Secure" attribute to the cookie.
201+
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#secure(boolean)
202+
*/
203+
public void setCookieSecure(boolean cookieSecure) {
204+
this.cookieSecure = cookieSecure;
205+
}
206+
207+
/**
208+
* Add the "HttpOnly" attribute to the cookie.
209+
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#httpOnly(boolean)
210+
*/
211+
public void setCookieHttpOnly(boolean cookieHttpOnly) {
212+
this.cookieHttpOnly = cookieHttpOnly;
213+
}
214+
215+
/**
216+
* Add the "SameSite" attribute to the cookie.
217+
* @since 6.0
218+
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#sameSite(String)
219+
*/
220+
public void setCookieSameSite(String cookieSameSite) {
221+
Assert.notNull(cookieSameSite, "cookieSameSite must not be null");
222+
this.cookieSameSite = cookieSameSite;
223+
}
224+
116225
/**
117226
* Specify whether this resolver's cookies should be compliant with BCP 47
118227
* language tags instead of Java's legacy locale specification format.
@@ -161,40 +270,6 @@ public boolean isRejectInvalidCookies() {
161270
return this.rejectInvalidCookies;
162271
}
163272

164-
/**
165-
* Set a fixed locale that this resolver will return if no cookie is found.
166-
*/
167-
public void setDefaultLocale(@Nullable Locale defaultLocale) {
168-
this.defaultLocale = defaultLocale;
169-
}
170-
171-
/**
172-
* Return the fixed locale that this resolver will return if no cookie is found,
173-
* if any.
174-
*/
175-
@Nullable
176-
protected Locale getDefaultLocale() {
177-
return this.defaultLocale;
178-
}
179-
180-
/**
181-
* Set a fixed time zone that this resolver will return if no cookie is found.
182-
* @since 4.0
183-
*/
184-
public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) {
185-
this.defaultTimeZone = defaultTimeZone;
186-
}
187-
188-
/**
189-
* Return the fixed time zone that this resolver will return if no cookie is found,
190-
* if any.
191-
* @since 4.0
192-
*/
193-
@Nullable
194-
protected TimeZone getDefaultTimeZone() {
195-
return this.defaultTimeZone;
196-
}
197-
198273
/**
199274
* Set the function used to determine the default locale for the given request,
200275
* called if no locale cookie has been found.
@@ -256,46 +331,43 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
256331
TimeZone timeZone = null;
257332

258333
// Retrieve and parse cookie value.
259-
String cookieName = getCookieName();
260-
if (cookieName != null) {
261-
Cookie cookie = WebUtils.getCookie(request, cookieName);
262-
if (cookie != null) {
263-
String value = cookie.getValue();
264-
String localePart = value;
265-
String timeZonePart = null;
266-
int separatorIndex = localePart.indexOf('/');
267-
if (separatorIndex == -1) {
268-
// Leniently accept older cookies separated by a space...
269-
separatorIndex = localePart.indexOf(' ');
270-
}
271-
if (separatorIndex >= 0) {
272-
localePart = value.substring(0, separatorIndex);
273-
timeZonePart = value.substring(separatorIndex + 1);
334+
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
335+
if (cookie != null) {
336+
String value = cookie.getValue();
337+
String localePart = value;
338+
String timeZonePart = null;
339+
int separatorIndex = localePart.indexOf('/');
340+
if (separatorIndex == -1) {
341+
// Leniently accept older cookies separated by a space...
342+
separatorIndex = localePart.indexOf(' ');
343+
}
344+
if (separatorIndex >= 0) {
345+
localePart = value.substring(0, separatorIndex);
346+
timeZonePart = value.substring(separatorIndex + 1);
347+
}
348+
try {
349+
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
350+
if (timeZonePart != null) {
351+
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
274352
}
275-
try {
276-
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
277-
if (timeZonePart != null) {
278-
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
279-
}
353+
}
354+
catch (IllegalArgumentException ex) {
355+
if (isRejectInvalidCookies() &&
356+
request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
357+
throw new IllegalStateException("Encountered invalid locale cookie '" +
358+
this.cookieName + "': [" + value + "] due to: " + ex.getMessage());
280359
}
281-
catch (IllegalArgumentException ex) {
282-
if (isRejectInvalidCookies() &&
283-
request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
284-
throw new IllegalStateException("Encountered invalid locale cookie '" +
285-
cookieName + "': [" + value + "] due to: " + ex.getMessage());
286-
}
287-
else {
288-
// Lenient handling (e.g. error dispatch): ignore locale/timezone parse exceptions
289-
if (logger.isDebugEnabled()) {
290-
logger.debug("Ignoring invalid locale cookie '" + cookieName +
291-
"': [" + value + "] due to: " + ex.getMessage());
292-
}
360+
else {
361+
// Lenient handling (e.g. error dispatch): ignore locale/timezone parse exceptions
362+
if (logger.isDebugEnabled()) {
363+
logger.debug("Ignoring invalid locale cookie '" + this.cookieName +
364+
"': [" + value + "] due to: " + ex.getMessage());
293365
}
294366
}
295-
if (logger.isTraceEnabled()) {
296-
logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale +
297-
"'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : ""));
298-
}
367+
}
368+
if (logger.isTraceEnabled()) {
369+
logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale +
370+
"'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : ""));
299371
}
300372
}
301373

@@ -306,11 +378,6 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
306378
}
307379
}
308380

309-
@Override
310-
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
311-
setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
312-
}
313-
314381
@Override
315382
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,
316383
@Nullable LocaleContext localeContext) {
@@ -319,17 +386,35 @@ public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletRe
319386

320387
Locale locale = null;
321388
TimeZone timeZone = null;
389+
ResponseCookie cookie;
322390
if (localeContext != null) {
323391
locale = localeContext.getLocale();
324392
if (localeContext instanceof TimeZoneAwareLocaleContext timeZoneAwareLocaleContext) {
325393
timeZone = timeZoneAwareLocaleContext.getTimeZone();
326394
}
327-
addCookie(response,
328-
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? '/' + timeZone.getID() : ""));
395+
cookie = ResponseCookie.from(this.cookieName,
396+
(locale != null ? toLocaleValue(locale) : "-") +
397+
(timeZone != null ? '/' + timeZone.getID() : ""))
398+
.maxAge(this.cookieMaxAge)
399+
.path(this.cookiePath)
400+
.domain(this.cookieDomain)
401+
.secure(this.cookieSecure)
402+
.httpOnly(this.cookieHttpOnly)
403+
.sameSite(this.cookieSameSite)
404+
.build();
329405
}
330406
else {
331-
removeCookie(response);
407+
// a cookie with empty value and max age 0
408+
cookie = ResponseCookie.from(this.cookieName, "")
409+
.maxAge(Duration.ZERO)
410+
.path(this.cookiePath)
411+
.domain(this.cookieDomain)
412+
.secure(this.cookieSecure)
413+
.httpOnly(this.cookieHttpOnly)
414+
.sameSite(this.cookieSameSite)
415+
.build();
332416
}
417+
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
333418
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
334419
(locale != null ? locale : this.defaultLocaleFunction.apply(request)));
335420
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,

0 commit comments

Comments
 (0)