diff --git a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java index 3cf8b74248f..cba43ff387b 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java @@ -54,7 +54,7 @@ public final class ResponseCookie extends HttpCookie { /** * Private constructor. See {@link #from(String, String)}. */ - private ResponseCookie(String name, String value, Duration maxAge, @Nullable String domain, + private ResponseCookie(String name, @Nullable String value, Duration maxAge, @Nullable String domain, @Nullable String path, boolean secure, boolean httpOnly, @Nullable String sameSite) { super(name, value); @@ -128,6 +128,19 @@ public final class ResponseCookie extends HttpCookie { return this.sameSite; } + /** + * Return a builder pre-populated with values from {@code "this"} instance. + * @since 6.0 + */ + public ResponseCookieBuilder mutate() { + return new DefaultResponseCookieBuilder(getName(), getValue(), false) + .maxAge(this.maxAge) + .domain(this.domain) + .path(this.path) + .secure(this.secure) + .httpOnly(this.httpOnly) + .sameSite(this.sameSite); + } @Override public boolean equals(@Nullable Object other) { @@ -179,6 +192,18 @@ public final class ResponseCookie extends HttpCookie { } + /** + * Factory method to obtain a builder for a server-defined cookie, given its + * name only, and where the value as well as other attributes can be set + * later via builder methods. + * @param name the cookie name + * @return a builder to create the cookie with + * @since 6.0 + */ + public static ResponseCookieBuilder from(final String name) { + return new DefaultResponseCookieBuilder(name, null, false); + } + /** * Factory method to obtain a builder for a server-defined cookie that starts * with a name-value pair and may also include attributes. @@ -187,7 +212,7 @@ public final class ResponseCookie extends HttpCookie { * @return a builder to create the cookie with */ public static ResponseCookieBuilder from(final String name, final String value) { - return from(name, value, false); + return new DefaultResponseCookieBuilder(name, value, false); } /** @@ -201,90 +226,7 @@ public final class ResponseCookie extends HttpCookie { * @since 5.2.5 */ public static ResponseCookieBuilder fromClientResponse(final String name, final String value) { - return from(name, value, true); - } - - - private static ResponseCookieBuilder from(final String name, final String value, boolean lenient) { - - return new ResponseCookieBuilder() { - - private Duration maxAge = Duration.ofSeconds(-1); - - @Nullable - private String domain; - - @Nullable - private String path; - - private boolean secure; - - private boolean httpOnly; - - @Nullable - private String sameSite; - - @Override - public ResponseCookieBuilder maxAge(Duration maxAge) { - this.maxAge = maxAge; - return this; - } - - @Override - public ResponseCookieBuilder maxAge(long maxAgeSeconds) { - this.maxAge = maxAgeSeconds >= 0 ? Duration.ofSeconds(maxAgeSeconds) : Duration.ofSeconds(-1); - return this; - } - - @Override - public ResponseCookieBuilder domain(@Nullable String domain) { - this.domain = initDomain(domain); - return this; - } - - @Nullable - private String initDomain(@Nullable String domain) { - if (lenient && StringUtils.hasLength(domain)) { - String str = domain.trim(); - if (str.startsWith("\"") && str.endsWith("\"")) { - if (str.substring(1, str.length() - 1).trim().isEmpty()) { - return null; - } - } - } - return domain; - } - - @Override - public ResponseCookieBuilder path(@Nullable String path) { - this.path = path; - return this; - } - - @Override - public ResponseCookieBuilder secure(boolean secure) { - this.secure = secure; - return this; - } - - @Override - public ResponseCookieBuilder httpOnly(boolean httpOnly) { - this.httpOnly = httpOnly; - return this; - } - - @Override - public ResponseCookieBuilder sameSite(@Nullable String sameSite) { - this.sameSite = sameSite; - return this; - } - - @Override - public ResponseCookie build() { - return new ResponseCookie(name, value, this.maxAge, this.domain, this.path, - this.secure, this.httpOnly, this.sameSite); - } - }; + return new DefaultResponseCookieBuilder(name, value, true); } @@ -293,6 +235,12 @@ public final class ResponseCookie extends HttpCookie { */ public interface ResponseCookieBuilder { + /** + * Set the cookie value. + * @since 6.0 + */ + ResponseCookieBuilder value(@Nullable String value); + /** * Set the cookie "Max-Age" attribute. * @@ -429,4 +377,106 @@ public final class ResponseCookie extends HttpCookie { } } + + /** + * Default implementation of {@link ResponseCookieBuilder}. + */ + private static class DefaultResponseCookieBuilder implements ResponseCookieBuilder { + + private final String name; + + @Nullable + private String value; + + private final boolean lenient; + + private Duration maxAge = Duration.ofSeconds(-1); + + @Nullable + private String domain; + + @Nullable + private String path; + + private boolean secure; + + private boolean httpOnly; + + @Nullable + private String sameSite; + + public DefaultResponseCookieBuilder(String name, @Nullable String value, boolean lenient) { + this.name = name; + this.value = value; + this.lenient = lenient; + } + + @Override + public ResponseCookieBuilder value(@Nullable String value) { + this.value = value; + return this; + } + + @Override + public ResponseCookieBuilder maxAge(Duration maxAge) { + this.maxAge = maxAge; + return this; + } + + @Override + public ResponseCookieBuilder maxAge(long maxAgeSeconds) { + this.maxAge = (maxAgeSeconds >= 0 ? Duration.ofSeconds(maxAgeSeconds) : Duration.ofSeconds(-1)); + return this; + } + + @Override + public ResponseCookieBuilder domain(@Nullable String domain) { + this.domain = initDomain(domain); + return this; + } + + @Nullable + private String initDomain(@Nullable String domain) { + if (this.lenient && StringUtils.hasLength(domain)) { + String str = domain.trim(); + if (str.startsWith("\"") && str.endsWith("\"")) { + if (str.substring(1, str.length() - 1).trim().isEmpty()) { + return null; + } + } + } + return domain; + } + + @Override + public ResponseCookieBuilder path(@Nullable String path) { + this.path = path; + return this; + } + + @Override + public ResponseCookieBuilder secure(boolean secure) { + this.secure = secure; + return this; + } + + @Override + public ResponseCookieBuilder httpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + @Override + public ResponseCookieBuilder sameSite(@Nullable String sameSite) { + this.sameSite = sameSite; + return this; + } + + @Override + public ResponseCookie build() { + return new ResponseCookie(this.name, this.value, this.maxAge, + this.domain, this.path, this.secure, this.httpOnly, this.sameSite); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java index ea406729137..7c404f50050 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java @@ -91,21 +91,7 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { private static final Log logger = LogFactory.getLog(CookieLocaleResolver.class); - private String cookieName; - - private Duration cookieMaxAge = Duration.ofSeconds(-1); - - @Nullable - private String cookiePath = "/"; - - @Nullable - private String cookieDomain; - - private boolean cookieSecure; - - private boolean cookieHttpOnly; - - private String cookieSameSite = "Lax"; + private ResponseCookie cookie; private boolean languageTagCompliant = true; @@ -124,8 +110,8 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { * @since 6.0 */ public CookieLocaleResolver(String cookieName) { - Assert.notNull(cookieName, "cookieName must not be null"); - this.cookieName = cookieName; + Assert.notNull(cookieName, "'cookieName' must not be null"); + this.cookie = ResponseCookie.from(cookieName).path("/").sameSite("Lax").build(); } /** @@ -144,7 +130,15 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { @Deprecated public void setCookieName(String cookieName) { Assert.notNull(cookieName, "cookieName must not be null"); - this.cookieName = cookieName; + this.cookie = ResponseCookie.from(cookieName) + .maxAge(this.cookie.getMaxAge()) + .domain(this.cookie.getDomain()) + .path(this.cookie.getPath()) + .secure(this.cookie.isSecure()) + .httpOnly(this.cookie.isHttpOnly()) + .sameSite(this.cookie.getSameSite()) + .build(); + } /** @@ -155,8 +149,8 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#maxAge(Duration) */ public void setCookieMaxAge(Duration cookieMaxAge) { - Assert.notNull(cookieMaxAge, "cookieMaxAge must not be null"); - this.cookieMaxAge = cookieMaxAge; + Assert.notNull(cookieMaxAge, "'cookieMaxAge' must not be null"); + this.cookie = this.cookie.mutate().maxAge(cookieMaxAge).build(); } /** @@ -174,7 +168,7 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#path(String) */ public void setCookiePath(@Nullable String cookiePath) { - this.cookiePath = cookiePath; + this.cookie = this.cookie.mutate().path(cookiePath).build(); } /** @@ -182,7 +176,7 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#domain(String) */ public void setCookieDomain(@Nullable String cookieDomain) { - this.cookieDomain = cookieDomain; + this.cookie = this.cookie.mutate().domain(cookieDomain).build(); } /** @@ -190,7 +184,7 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#secure(boolean) */ public void setCookieSecure(boolean cookieSecure) { - this.cookieSecure = cookieSecure; + this.cookie = this.cookie.mutate().secure(cookieSecure).build(); } /** @@ -198,17 +192,18 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#httpOnly(boolean) */ public void setCookieHttpOnly(boolean cookieHttpOnly) { - this.cookieHttpOnly = cookieHttpOnly; + this.cookie = this.cookie.mutate().httpOnly(cookieHttpOnly).build(); } /** * Add the "SameSite" attribute to the cookie. + *

By default, this is set to {@code "Lax"}. * @since 6.0 * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#sameSite(String) */ public void setCookieSameSite(String cookieSameSite) { Assert.notNull(cookieSameSite, "cookieSameSite must not be null"); - this.cookieSameSite = cookieSameSite; + this.cookie = this.cookie.mutate().sameSite(cookieSameSite).build(); } /** @@ -320,7 +315,7 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { TimeZone timeZone = null; // Retrieve and parse cookie value. - Cookie cookie = WebUtils.getCookie(request, this.cookieName); + Cookie cookie = WebUtils.getCookie(request, this.cookie.getName()); if (cookie != null) { String value = cookie.getValue(); String localePart = value; @@ -344,12 +339,12 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { if (isRejectInvalidCookies() && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { throw new IllegalStateException("Encountered invalid locale cookie '" + - this.cookieName + "': [" + value + "] due to: " + ex.getMessage()); + this.cookie.getName() + "': [" + 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 '" + this.cookieName + + logger.debug("Ignoring invalid locale cookie '" + this.cookie.getName() + "': [" + value + "] due to: " + ex.getMessage()); } } @@ -374,40 +369,20 @@ public class CookieLocaleResolver extends AbstractLocaleContextResolver { Assert.notNull(response, "HttpServletResponse is required for CookieLocaleResolver"); Locale locale = null; - TimeZone timeZone = null; - ResponseCookie cookie; + TimeZone zone = null; if (localeContext != null) { locale = localeContext.getLocale(); if (localeContext instanceof TimeZoneAwareLocaleContext timeZoneAwareLocaleContext) { - timeZone = timeZoneAwareLocaleContext.getTimeZone(); + zone = timeZoneAwareLocaleContext.getTimeZone(); } - cookie = ResponseCookie.from(this.cookieName, - (locale != null ? toLocaleValue(locale) : "-") + - (timeZone != null ? '/' + timeZone.getID() : "")) - .maxAge(this.cookieMaxAge) - .path(this.cookiePath) - .domain(this.cookieDomain) - .secure(this.cookieSecure) - .httpOnly(this.cookieHttpOnly) - .sameSite(this.cookieSameSite) - .build(); + String value = (locale != null ? toLocaleValue(locale) : "-") + (zone != null ? '/' + zone.getID() : ""); + this.cookie = this.cookie.mutate().value(value).build(); } - else { - // a cookie with empty value and max age 0 - cookie = ResponseCookie.from(this.cookieName, "") - .maxAge(Duration.ZERO) - .path(this.cookiePath) - .domain(this.cookieDomain) - .secure(this.cookieSecure) - .httpOnly(this.cookieHttpOnly) - .sameSite(this.cookieSameSite) - .build(); - } - response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + response.addHeader(HttpHeaders.SET_COOKIE, this.cookie.toString()); request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, (locale != null ? locale : this.defaultLocaleFunction.apply(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, - (timeZone != null ? timeZone : this.defaultTimeZoneFunction.apply(request))); + (zone != null ? zone : this.defaultTimeZoneFunction.apply(request))); }