Better integrate ResponseCookie in CookieLocaleResolver

Improve ResponseCookie to allow an existing instance to be mutated
and also to set the cookie value through the builder. This allows
CookieLocaleResolver to avoid duplicating all the fields of
ResponseCookie and to have only a ResponseCookie field instead.

Closes gh-28779
This commit is contained in:
rstoyanchev 2022-09-28 15:57:40 +01:00
parent 075fccca94
commit e6c2d44646
2 changed files with 165 additions and 140 deletions

View File

@ -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);
}
}
}

View File

@ -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.
* <p>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)));
}