16
16
17
17
package org .springframework .web .servlet .i18n ;
18
18
19
+ import java .time .Duration ;
19
20
import java .util .Locale ;
20
21
import java .util .TimeZone ;
21
22
import java .util .function .Function ;
22
23
23
24
import jakarta .servlet .http .Cookie ;
24
25
import jakarta .servlet .http .HttpServletRequest ;
25
26
import jakarta .servlet .http .HttpServletResponse ;
27
+ import org .apache .commons .logging .Log ;
28
+ import org .apache .commons .logging .LogFactory ;
26
29
27
30
import org .springframework .context .i18n .LocaleContext ;
28
- import org .springframework .context .i18n .SimpleLocaleContext ;
29
31
import org .springframework .context .i18n .TimeZoneAwareLocaleContext ;
32
+ import org .springframework .http .HttpHeaders ;
33
+ import org .springframework .http .ResponseCookie ;
30
34
import org .springframework .lang .Nullable ;
31
35
import org .springframework .util .Assert ;
32
36
import org .springframework .util .StringUtils ;
33
- import org .springframework .web .servlet .LocaleContextResolver ;
34
37
import org .springframework .web .servlet .LocaleResolver ;
35
- import org .springframework .web .util .CookieGenerator ;
36
38
import org .springframework .web .util .WebUtils ;
37
39
38
40
/**
57
59
* @see #setDefaultLocale
58
60
* @see #setDefaultTimeZone
59
61
*/
60
- public class CookieLocaleResolver extends CookieGenerator implements LocaleContextResolver {
62
+ public class CookieLocaleResolver extends AbstractLocaleContextResolver {
61
63
62
64
/**
63
65
* The name of the request attribute that holds the {@code Locale}.
@@ -86,16 +88,39 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
86
88
*/
87
89
public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver .class .getName () + ".LOCALE" ;
88
90
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 );
89
96
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 = "/" ;
91
101
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 ;
93
108
94
109
@ Nullable
95
- private Locale defaultLocale ;
110
+ private String cookiePath = DEFAULT_COOKIE_PATH ;
96
111
97
112
@ 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 ;
99
124
100
125
private Function <HttpServletRequest , Locale > defaultLocaleFunction = request -> {
101
126
Locale defaultLocale = getDefaultLocale ();
@@ -104,15 +129,99 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
104
129
105
130
private Function <HttpServletRequest , TimeZone > defaultTimeZoneFunction = request -> getDefaultTimeZone ();
106
131
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
+
107
144
/**
108
145
* Create a new instance of {@link CookieLocaleResolver} using the
109
146
* {@linkplain #DEFAULT_COOKIE_NAME default cookie name}.
110
147
*/
111
148
public CookieLocaleResolver () {
112
- setCookieName (DEFAULT_COOKIE_NAME );
149
+ this (DEFAULT_COOKIE_NAME );
113
150
}
114
151
115
152
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
+
116
225
/**
117
226
* Specify whether this resolver's cookies should be compliant with BCP 47
118
227
* language tags instead of Java's legacy locale specification format.
@@ -161,40 +270,6 @@ public boolean isRejectInvalidCookies() {
161
270
return this .rejectInvalidCookies ;
162
271
}
163
272
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
-
198
273
/**
199
274
* Set the function used to determine the default locale for the given request,
200
275
* called if no locale cookie has been found.
@@ -256,46 +331,43 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
256
331
TimeZone timeZone = null ;
257
332
258
333
// 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 );
274
352
}
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 ());
280
359
}
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 ());
293
365
}
294
366
}
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 () + "'" : "" ));
299
371
}
300
372
}
301
373
@@ -306,11 +378,6 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
306
378
}
307
379
}
308
380
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
-
314
381
@ Override
315
382
public void setLocaleContext (HttpServletRequest request , @ Nullable HttpServletResponse response ,
316
383
@ Nullable LocaleContext localeContext ) {
@@ -319,17 +386,35 @@ public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletRe
319
386
320
387
Locale locale = null ;
321
388
TimeZone timeZone = null ;
389
+ ResponseCookie cookie ;
322
390
if (localeContext != null ) {
323
391
locale = localeContext .getLocale ();
324
392
if (localeContext instanceof TimeZoneAwareLocaleContext timeZoneAwareLocaleContext ) {
325
393
timeZone = timeZoneAwareLocaleContext .getTimeZone ();
326
394
}
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 ();
329
405
}
330
406
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 ();
332
416
}
417
+ response .addHeader (HttpHeaders .SET_COOKIE , cookie .toString ());
333
418
request .setAttribute (LOCALE_REQUEST_ATTRIBUTE_NAME ,
334
419
(locale != null ? locale : this .defaultLocaleFunction .apply (request )));
335
420
request .setAttribute (TIME_ZONE_REQUEST_ATTRIBUTE_NAME ,
0 commit comments