diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java index 81a762f352..3f1675a4a9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java @@ -15,54 +15,28 @@ */ package org.springframework.http; -import java.time.Duration; - import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** - * Representation for an HTTP Cookie. + * Represents an HTTP Cookie with a name and value. * - *

Use the {@link #clientCookie} factory method to create a client-to-server, - * name-value pair cookie and the {@link #serverCookie} factory method to build - * a server-to-client cookie with additional attributes. + *

The {@link ServerHttpCookie} sub-class exposes the extra attributes that + * a server can include in a Set-Cookie response header. * * @author Rossen Stoyanchev * @see RFC 6265 */ -public final class HttpCookie { +public class HttpCookie { private final String name; private final String value; - private final Duration maxAge; - - private final String domain; - - private final String path; - - private final boolean secure; - - private final boolean httpOnly; - - - private HttpCookie(String name, String value) { - this(name, value, Duration.ofSeconds(-1), null, null, false, false); - } - - private HttpCookie(String name, String value, Duration maxAge, String domain, String path, - boolean secure, boolean httpOnly) { + public HttpCookie(String name, String value) { Assert.hasLength(name, "'name' is required and must not be empty."); - Assert.notNull(maxAge); this.name = name; this.value = (value != null ? value : ""); - this.maxAge = maxAge; - this.domain = domain; - this.path = path; - this.secure = secure; - this.httpOnly = httpOnly; } /** @@ -73,59 +47,16 @@ public final class HttpCookie { } /** - * Return the cookie value. + * Return the cookie value or an empty string, never {@code null}. */ public String getValue() { return this.value; } - /** - * Return the cookie "Max-Age" attribute in seconds. - * - *

A positive value indicates when the cookie expires relative to the - * current time. A value of 0 means the cookie should expire immediately. - * A negative value means no "Max-Age" attribute in which case the cookie - * is removed when the browser is closed. - */ - public Duration getMaxAge() { - return this.maxAge; - } - - /** - * Return the cookie "Domain" attribute. - */ - public String getDomain() { - return this.domain; - } - - /** - * Return the cookie "Path" attribute. - */ - public String getPath() { - return this.path; - } - - /** - * Return {@code true} if the cookie has the "Secure" attribute. - */ - public boolean isSecure() { - return this.secure; - } - - /** - * Return {@code true} if the cookie has the "HttpOnly" attribute. - * @see http://www.owasp.org/index.php/HTTPOnly - */ - public boolean isHttpOnly() { - return this.httpOnly; - } @Override public int hashCode() { - int result = this.name.hashCode(); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.path); - return result; + return this.name.hashCode(); } @Override @@ -137,122 +68,7 @@ public final class HttpCookie { return false; } HttpCookie otherCookie = (HttpCookie) other; - return (this.name.equalsIgnoreCase(otherCookie.getName()) && - ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) && - ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain())); - } - - /** - * Factory method to create a cookie sent from a client to a server. - * Client cookies are name-value pairs only without attributes. - * @param name the cookie name - * @param value the cookie value - * @return the created cookie instance - */ - public static HttpCookie clientCookie(String name, String value) { - return new HttpCookie(name, value); - } - - /** - * Factory method to obtain a builder for a server-defined cookie that starts - * with a name-value pair and may also include attributes. - * @param name the cookie name - * @param value the cookie value - * @return the created cookie instance - */ - public static HttpCookieBuilder serverCookie(final String name, final String value) { - - return new HttpCookieBuilder() { - - private Duration maxAge = Duration.ofSeconds(-1); - - private String domain; - - private String path; - - private boolean secure; - - private boolean httpOnly; - - - @Override - public HttpCookieBuilder maxAge(Duration maxAge) { - this.maxAge = maxAge; - return this; - } - - @Override - public HttpCookieBuilder domain(String domain) { - this.domain = domain; - return this; - } - - @Override - public HttpCookieBuilder path(String path) { - this.path = path; - return this; - } - - @Override - public HttpCookieBuilder secure() { - this.secure = true; - return this; - } - - @Override - public HttpCookieBuilder httpOnly() { - this.httpOnly = true; - return this; - } - - @Override - public HttpCookie build() { - return new HttpCookie(name, value, this.maxAge, this.domain, this.path, - this.secure, this.httpOnly); - } - }; - } - - /** - * A builder for a server-defined HttpCookie with attributes. - */ - public interface HttpCookieBuilder { - - /** - * Set the cookie "Max-Age" attribute. - * - *

A positive value indicates when the cookie should expire relative - * to the current time. A value of 0 means the cookie should expire - * immediately. A negative value results in no "Max-Age" attribute in - * which case the cookie is removed when the browser is closed. - */ - HttpCookieBuilder maxAge(Duration maxAge); - - /** - * Set the cookie "Path" attribute. - */ - HttpCookieBuilder path(String path); - - /** - * Set the cookie "Domain" attribute. - */ - HttpCookieBuilder domain(String domain); - - /** - * Add the "Secure" attribute to the cookie. - */ - HttpCookieBuilder secure(); - - /** - * Add the "HttpOnly" attribute to the cookie. - * @see http://www.owasp.org/index.php/HTTPOnly - */ - HttpCookieBuilder httpOnly(); - - /** - * Create the HttpCookie. - */ - HttpCookie build(); + return (this.name.equalsIgnoreCase(otherCookie.getName())); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java deleted file mode 100644 index 419e94b947..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java +++ /dev/null @@ -1,1158 +0,0 @@ -/* - * Copyright 2002-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.http; - -import java.io.Serializable; -import java.net.URI; -import java.nio.charset.Charset; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; - -import org.springframework.util.Assert; -import org.springframework.util.LinkedCaseInsensitiveMap; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; - -// A copy of HttpHeaders with additional support for: -// - HTTP cookies - -// To be merged into HttpHeaders from spring-web - -public class HttpHeaders implements MultiValueMap, Serializable { - - private static final long serialVersionUID = -8578554704772377436L; - - /** - * The HTTP {@code Accept} header field name. - * @see Section 5.3.2 of RFC 7231 - */ - public static final String ACCEPT = "Accept"; - /** - * The HTTP {@code Accept-Charset} header field name. - * @see Section 5.3.3 of RFC 7231 - */ - public static final String ACCEPT_CHARSET = "Accept-Charset"; - /** - * The HTTP {@code Accept-Encoding} header field name. - * @see Section 5.3.4 of RFC 7231 - */ - public static final String ACCEPT_ENCODING = "Accept-Encoding"; - /** - * The HTTP {@code Accept-Language} header field name. - * @see Section 5.3.5 of RFC 7231 - */ - public static final String ACCEPT_LANGUAGE = "Accept-Language"; - /** - * The HTTP {@code Accept-Ranges} header field name. - * @see Section 5.3.5 of RFC 7233 - */ - public static final String ACCEPT_RANGES = "Accept-Ranges"; - /** - * The CORS {@code Access-Control-Allow-Credentials} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; - /** - * The CORS {@code Access-Control-Allow-Headers} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; - /** - * The CORS {@code Access-Control-Allow-Methods} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; - /** - * The CORS {@code Access-Control-Allow-Origin} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; - /** - * The CORS {@code Access-Control-Expose-Headers} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; - /** - * The CORS {@code Access-Control-Max-Age} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; - /** - * The CORS {@code Access-Control-Request-Headers} request header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; - /** - * The CORS {@code Access-Control-Request-Method} request header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - /** - * The HTTP {@code Age} header field name. - * @see Section 5.1 of RFC 7234 - */ - public static final String AGE = "Age"; - /** - * The HTTP {@code Allow} header field name. - * @see Section 7.4.1 of RFC 7231 - */ - public static final String ALLOW = "Allow"; - /** - * The HTTP {@code Authorization} header field name. - * @see Section 4.2 of RFC 7235 - */ - public static final String AUTHORIZATION = "Authorization"; - /** - * The HTTP {@code Cache-Control} header field name. - * @see Section 5.2 of RFC 7234 - */ - public static final String CACHE_CONTROL = "Cache-Control"; - /** - * The HTTP {@code Connection} header field name. - * @see Section 6.1 of RFC 7230 - */ - public static final String CONNECTION = "Connection"; - /** - * The HTTP {@code Content-Encoding} header field name. - * @see Section 3.1.2.2 of RFC 7231 - */ - public static final String CONTENT_ENCODING = "Content-Encoding"; - /** - * The HTTP {@code Content-Disposition} header field name - * @see RFC 6266 - */ - public static final String CONTENT_DISPOSITION = "Content-Disposition"; - /** - * The HTTP {@code Content-Language} header field name. - * @see Section 3.1.3.2 of RFC 7231 - */ - public static final String CONTENT_LANGUAGE = "Content-Language"; - /** - * The HTTP {@code Content-Length} header field name. - * @see Section 3.3.2 of RFC 7230 - */ - public static final String CONTENT_LENGTH = "Content-Length"; - /** - * The HTTP {@code Content-Location} header field name. - * @see Section 3.1.4.2 of RFC 7231 - */ - public static final String CONTENT_LOCATION = "Content-Location"; - /** - * The HTTP {@code Content-Range} header field name. - * @see Section 4.2 of RFC 7233 - */ - public static final String CONTENT_RANGE = "Content-Range"; - /** - * The HTTP {@code Content-Type} header field name. - * @see Section 3.1.1.5 of RFC 7231 - */ - public static final String CONTENT_TYPE = "Content-Type"; - /** - * The HTTP {@code Cookie} header field name. - * @see Section 4.3.4 of RFC 2109 - */ - public static final String COOKIE = "Cookie"; - /** - * The HTTP {@code Date} header field name. - * @see Section 7.1.1.2 of RFC 7231 - */ - public static final String DATE = "Date"; - /** - * The HTTP {@code ETag} header field name. - * @see Section 2.3 of RFC 7232 - */ - public static final String ETAG = "ETag"; - /** - * The HTTP {@code Expect} header field name. - * @see Section 5.1.1 of RFC 7231 - */ - public static final String EXPECT = "Expect"; - /** - * The HTTP {@code Expires} header field name. - * @see Section 5.3 of RFC 7234 - */ - public static final String EXPIRES = "Expires"; - /** - * The HTTP {@code From} header field name. - * @see Section 5.5.1 of RFC 7231 - */ - public static final String FROM = "From"; - /** - * The HTTP {@code Host} header field name. - * @see Section 5.4 of RFC 7230 - */ - public static final String HOST = "Host"; - /** - * The HTTP {@code If-Match} header field name. - * @see Section 3.1 of RFC 7232 - */ - public static final String IF_MATCH = "If-Match"; - /** - * The HTTP {@code If-Modified-Since} header field name. - * @see Section 3.3 of RFC 7232 - */ - public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; - /** - * The HTTP {@code If-None-Match} header field name. - * @see Section 3.2 of RFC 7232 - */ - public static final String IF_NONE_MATCH = "If-None-Match"; - /** - * The HTTP {@code If-Range} header field name. - * @see Section 3.2 of RFC 7233 - */ - public static final String IF_RANGE = "If-Range"; - /** - * The HTTP {@code If-Unmodified-Since} header field name. - * @see Section 3.4 of RFC 7232 - */ - public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; - /** - * The HTTP {@code Last-Modified} header field name. - * @see Section 2.2 of RFC 7232 - */ - public static final String LAST_MODIFIED = "Last-Modified"; - /** - * The HTTP {@code Link} header field name. - * @see RFC 5988 - */ - public static final String LINK = "Link"; - /** - * The HTTP {@code Location} header field name. - * @see Section 7.1.2 of RFC 7231 - */ - public static final String LOCATION = "Location"; - /** - * The HTTP {@code Max-Forwards} header field name. - * @see Section 5.1.2 of RFC 7231 - */ - public static final String MAX_FORWARDS = "Max-Forwards"; - /** - * The HTTP {@code Origin} header field name. - * @see RFC 6454 - */ - public static final String ORIGIN = "Origin"; - /** - * The HTTP {@code Pragma} header field name. - * @see Section 5.4 of RFC 7234 - */ - public static final String PRAGMA = "Pragma"; - /** - * The HTTP {@code Proxy-Authenticate} header field name. - * @see Section 4.3 of RFC 7235 - */ - public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; - /** - * The HTTP {@code Proxy-Authorization} header field name. - * @see Section 4.4 of RFC 7235 - */ - public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; - /** - * The HTTP {@code Range} header field name. - * @see Section 3.1 of RFC 7233 - */ - public static final String RANGE = "Range"; - /** - * The HTTP {@code Referer} header field name. - * @see Section 5.5.2 of RFC 7231 - */ - public static final String REFERER = "Referer"; - /** - * The HTTP {@code Retry-After} header field name. - * @see Section 7.1.3 of RFC 7231 - */ - public static final String RETRY_AFTER = "Retry-After"; - /** - * The HTTP {@code Server} header field name. - * @see Section 7.4.2 of RFC 7231 - */ - public static final String SERVER = "Server"; - /** - * The HTTP {@code Set-Cookie} header field name. - * @see Section 4.2.2 of RFC 2109 - */ - public static final String SET_COOKIE = "Set-Cookie"; - /** - * The HTTP {@code Set-Cookie2} header field name. - * @see RFC 2965 - */ - public static final String SET_COOKIE2 = "Set-Cookie2"; - /** - * The HTTP {@code TE} header field name. - * @see Section 4.3 of RFC 7230 - */ - public static final String TE = "TE"; - /** - * The HTTP {@code Trailer} header field name. - * @see Section 4.4 of RFC 7230 - */ - public static final String TRAILER = "Trailer"; - /** - * The HTTP {@code Transfer-Encoding} header field name. - * @see Section 3.3.1 of RFC 7230 - */ - public static final String TRANSFER_ENCODING = "Transfer-Encoding"; - /** - * The HTTP {@code Upgrade} header field name. - * @see Section 6.7 of RFC 7230 - */ - public static final String UPGRADE = "Upgrade"; - /** - * The HTTP {@code User-Agent} header field name. - * @see Section 5.5.3 of RFC 7231 - */ - public static final String USER_AGENT = "User-Agent"; - /** - * The HTTP {@code Vary} header field name. - * @see Section 7.1.4 of RFC 7231 - */ - public static final String VARY = "Vary"; - /** - * The HTTP {@code Via} header field name. - * @see Section 5.7.1 of RFC 7230 - */ - public static final String VIA = "Via"; - /** - * The HTTP {@code Warning} header field name. - * @see Section 5.5 of RFC 7234 - */ - public static final String WARNING = "Warning"; - /** - * The HTTP {@code WWW-Authenticate} header field name. - * @see Section 4.1 of RFC 7235 - */ - public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; - - /** - * Date formats as specified in the HTTP RFC - * @see Section 7.1.1.1 of RFC 7231 - */ - private static final String[] DATE_FORMATS = new String[] { - "EEE, dd MMM yyyy HH:mm:ss zzz", - "EEE, dd-MMM-yy HH:mm:ss zzz", - "EEE MMM dd HH:mm:ss yyyy" - }; - - private static TimeZone GMT = TimeZone.getTimeZone("GMT"); - - - private final Map> headers; - - private final Map> cookies; - - - /** - * Constructs a new, empty instance of the {@code HttpHeaders} object. - */ - public HttpHeaders() { - this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), null, false); - } - - /** - * Constructor with a map of HTTP input cookies (e.g. cookies sent by client) - * that enables lazy initialization on first access of the map. - * @param inputCookies input cookies - */ - public HttpHeaders(Map> inputCookies) { - this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), inputCookies, false); - Assert.notNull(cookies, "'inputCookies' is required."); - } - - /** - * Private constructor that can create read-only {@code HttpHeader} instances. - */ - private HttpHeaders(Map> headers, Map> cookies, - boolean readOnly) { - - Assert.notNull(headers, "'headers' must not be null"); - if (readOnly) { - Map> map = - new LinkedCaseInsensitiveMap>(headers.size(), Locale.ENGLISH); - for (Entry> entry : headers.entrySet()) { - List values = Collections.unmodifiableList(entry.getValue()); - map.put(entry.getKey(), values); - } - this.headers = Collections.unmodifiableMap(map); - this.cookies = (cookies != null ? Collections.unmodifiableMap(cookies) : Collections.emptyMap()); - } - else { - this.headers = headers; - this.cookies = (cookies != null ? cookies : new LinkedCaseInsensitiveMap<>()); - } - } - - /** - * Set the list of acceptable {@linkplain MediaType media types}, - * as specified by the {@code Accept} header. - */ - public void setAccept(List acceptableMediaTypes) { - set(ACCEPT, MediaType.toString(acceptableMediaTypes)); - } - - /** - * Return the list of acceptable {@linkplain MediaType media types}, - * as specified by the {@code Accept} header. - *

Returns an empty list when the acceptable media types are unspecified. - */ - public List getAccept() { - String value = getFirst(ACCEPT); - List result = (value != null ? MediaType.parseMediaTypes(value) : Collections.emptyList()); - - // Some containers parse 'Accept' into multiple values - if (result.size() == 1) { - List acceptHeader = get(ACCEPT); - if (acceptHeader.size() > 1) { - value = StringUtils.collectionToCommaDelimitedString(acceptHeader); - result = MediaType.parseMediaTypes(value); - } - } - - return result; - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Credentials} response header. - */ - public void setAccessControlAllowCredentials(boolean allowCredentials) { - set(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(allowCredentials)); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Credentials} response header. - */ - public boolean getAccessControlAllowCredentials() { - return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Headers} response header. - */ - public void setAccessControlAllowHeaders(List allowedHeaders) { - set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders)); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Headers} response header. - */ - public List getAccessControlAllowHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS); - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Methods} response header. - */ - public void setAccessControlAllowMethods(List allowedMethods) { - set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods)); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Methods} response header. - */ - public List getAccessControlAllowMethods() { - List result = new ArrayList(); - String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - result.add(HttpMethod.valueOf(token)); - } - } - return result; - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Origin} response header. - */ - public void setAccessControlAllowOrigin(String allowedOrigin) { - set(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Origin} response header. - */ - public String getAccessControlAllowOrigin() { - return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN); - } - - /** - * Set the (new) value of the {@code Access-Control-Expose-Headers} response header. - */ - public void setAccessControlExposeHeaders(List exposedHeaders) { - set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders)); - } - - /** - * Returns the value of the {@code Access-Control-Expose-Headers} response header. - */ - public List getAccessControlExposeHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS); - } - - /** - * Set the (new) value of the {@code Access-Control-Max-Age} response header. - */ - public void setAccessControlMaxAge(long maxAge) { - set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge)); - } - - /** - * Returns the value of the {@code Access-Control-Max-Age} response header. - *

Returns -1 when the max age is unknown. - */ - public long getAccessControlMaxAge() { - String value = getFirst(ACCESS_CONTROL_MAX_AGE); - return (value != null ? Long.parseLong(value) : -1); - } - - /** - * Set the (new) value of the {@code Access-Control-Request-Headers} request header. - */ - public void setAccessControlRequestHeaders(List requestHeaders) { - set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders)); - } - - /** - * Returns the value of the {@code Access-Control-Request-Headers} request header. - */ - public List getAccessControlRequestHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS); - } - - /** - * Set the (new) value of the {@code Access-Control-Request-Method} request header. - */ - public void setAccessControlRequestMethod(HttpMethod requestedMethod) { - set(ACCESS_CONTROL_REQUEST_METHOD, requestedMethod.name()); - } - - /** - * Returns the value of the {@code Access-Control-Request-Method} request header. - */ - public HttpMethod getAccessControlRequestMethod() { - String value = getFirst(ACCESS_CONTROL_REQUEST_METHOD); - return (value != null ? HttpMethod.valueOf(value) : null); - } - - /** - * Set the list of acceptable {@linkplain Charset charsets}, - * as specified by the {@code Accept-Charset} header. - */ - public void setAcceptCharset(List acceptableCharsets) { - StringBuilder builder = new StringBuilder(); - for (Iterator iterator = acceptableCharsets.iterator(); iterator.hasNext();) { - Charset charset = iterator.next(); - builder.append(charset.name().toLowerCase(Locale.ENGLISH)); - if (iterator.hasNext()) { - builder.append(", "); - } - } - set(ACCEPT_CHARSET, builder.toString()); - } - - /** - * Return the list of acceptable {@linkplain Charset charsets}, - * as specified by the {@code Accept-Charset} header. - */ - public List getAcceptCharset() { - List result = new ArrayList(); - String value = getFirst(ACCEPT_CHARSET); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - int paramIdx = token.indexOf(';'); - String charsetName; - if (paramIdx == -1) { - charsetName = token; - } - else { - charsetName = token.substring(0, paramIdx); - } - if (!charsetName.equals("*")) { - result.add(Charset.forName(charsetName)); - } - } - } - return result; - } - - /** - * Set the set of allowed {@link HttpMethod HTTP methods}, - * as specified by the {@code Allow} header. - */ - public void setAllow(Set allowedMethods) { - set(ALLOW, StringUtils.collectionToCommaDelimitedString(allowedMethods)); - } - - /** - * Return the set of allowed {@link HttpMethod HTTP methods}, - * as specified by the {@code Allow} header. - *

Returns an empty set when the allowed methods are unspecified. - */ - public Set getAllow() { - String value = getFirst(ALLOW); - if (!StringUtils.isEmpty(value)) { - List allowedMethod = new ArrayList(5); - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - allowedMethod.add(HttpMethod.valueOf(token)); - } - return EnumSet.copyOf(allowedMethod); - } - else { - return EnumSet.noneOf(HttpMethod.class); - } - } - - /** - * Set the (new) value of the {@code Cache-Control} header. - */ - public void setCacheControl(String cacheControl) { - set(CACHE_CONTROL, cacheControl); - } - - /** - * Returns the value of the {@code Cache-Control} header. - */ - public String getCacheControl() { - return getFirst(CACHE_CONTROL); - } - - /** - * Set the (new) value of the {@code Connection} header. - */ - public void setConnection(String connection) { - set(CONNECTION, connection); - } - - /** - * Set the (new) value of the {@code Connection} header. - */ - public void setConnection(List connection) { - set(CONNECTION, toCommaDelimitedString(connection)); - } - - /** - * Returns the value of the {@code Connection} header. - */ - public List getConnection() { - return getFirstValueAsList(CONNECTION); - } - - /** - * Set the (new) value of the {@code Content-Disposition} header - * for {@code form-data}. - * @param name the control name - * @param filename the filename (may be {@code null}) - */ - public void setContentDispositionFormData(String name, String filename) { - Assert.notNull(name, "'name' must not be null"); - StringBuilder builder = new StringBuilder("form-data; name=\""); - builder.append(name).append('\"'); - if (filename != null) { - builder.append("; filename=\""); - builder.append(filename).append('\"'); - } - set(CONTENT_DISPOSITION, builder.toString()); - } - - /** - * Set the length of the body in bytes, as specified by the - * {@code Content-Length} header. - */ - public void setContentLength(long contentLength) { - set(CONTENT_LENGTH, Long.toString(contentLength)); - } - - /** - * Return the length of the body in bytes, as specified by the - * {@code Content-Length} header. - *

Returns -1 when the content-length is unknown. - */ - public long getContentLength() { - String value = getFirst(CONTENT_LENGTH); - return (value != null ? Long.parseLong(value) : -1); - } - - /** - * Set the {@linkplain MediaType media type} of the body, - * as specified by the {@code Content-Type} header. - */ - public void setContentType(MediaType mediaType) { - Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'"); - Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'"); - set(CONTENT_TYPE, mediaType.toString()); - } - - /** - * Return the {@linkplain MediaType media type} of the body, as specified - * by the {@code Content-Type} header. - *

Returns {@code null} when the content-type is unknown. - */ - public MediaType getContentType() { - String value = getFirst(CONTENT_TYPE); - return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null); - } - - /** - * Add an HTTP cookie. - */ - public void addCookie(HttpCookie cookie) { - String name = cookie.getName(); - List set = this.cookies.get(name); - if (set == null) { - set = new ArrayList<>(); - this.cookies.put(name, set); - } - set.add(cookie); - } - - /** - * Return a map with {@link HttpCookie}s. When reading input cookies this map - * cannot be modified. When writing output cookies, this map is mutable. - */ - public Map> getCookies() { - return this.cookies; - } - - /** - * Set the date and time at which the message was created, as specified - * by the {@code Date} header. - *

The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setDate(long date) { - setDate(DATE, date); - } - - /** - * Return the date and time at which the message was created, as specified - * by the {@code Date} header. - *

The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - * @throws IllegalArgumentException if the value can't be converted to a date - */ - public long getDate() { - return getFirstDate(DATE); - } - - /** - * Set the (new) entity tag of the body, as specified by the {@code ETag} header. - */ - public void setETag(String eTag) { - if (eTag != null) { - Assert.isTrue(eTag.startsWith("\"") || eTag.startsWith("W/"), - "Invalid eTag, does not start with W/ or \""); - Assert.isTrue(eTag.endsWith("\""), "Invalid eTag, does not end with \""); - } - set(ETAG, eTag); - } - - /** - * Return the entity tag of the body, as specified by the {@code ETag} header. - */ - public String getETag() { - return getFirst(ETAG); - } - - /** - * Set the date and time at which the message is no longer valid, - * as specified by the {@code Expires} header. - *

The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setExpires(long expires) { - setDate(EXPIRES, expires); - } - - /** - * Return the date and time at which the message is no longer valid, - * as specified by the {@code Expires} header. - *

The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - */ - public long getExpires() { - try { - return getFirstDate(EXPIRES); - } - catch (IllegalArgumentException ex) { - return -1; - } - } - - /** - * Set the (new) value of the {@code If-Modified-Since} header. - *

The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setIfModifiedSince(long ifModifiedSince) { - setDate(IF_MODIFIED_SINCE, ifModifiedSince); - } - - /** - * Return the value of the {@code If-Modified-Since} header. - *

The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - */ - public long getIfModifiedSince() { - return getFirstDate(IF_MODIFIED_SINCE); - } - - /** - * Set the (new) value of the {@code If-None-Match} header. - */ - public void setIfNoneMatch(String ifNoneMatch) { - set(IF_NONE_MATCH, ifNoneMatch); - } - - /** - * Set the (new) values of the {@code If-None-Match} header. - */ - public void setIfNoneMatch(List ifNoneMatchList) { - set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); - } - - protected String toCommaDelimitedString(List list) { - StringBuilder builder = new StringBuilder(); - for (Iterator iterator = list.iterator(); iterator.hasNext();) { - String ifNoneMatch = iterator.next(); - builder.append(ifNoneMatch); - if (iterator.hasNext()) { - builder.append(", "); - } - } - return builder.toString(); - } - - /** - * Return the value of the {@code If-None-Match} header. - */ - public List getIfNoneMatch() { - return getFirstValueAsList(IF_NONE_MATCH); - } - - protected List getFirstValueAsList(String header) { - List result = new ArrayList(); - String value = getFirst(header); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - result.add(token); - } - } - return result; - } - - /** - * Set the time the resource was last changed, as specified by the - * {@code Last-Modified} header. - *

The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setLastModified(long lastModified) { - setDate(LAST_MODIFIED, lastModified); - } - - /** - * Return the time the resource was last changed, as specified by the - * {@code Last-Modified} header. - *

The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - */ - public long getLastModified() { - return getFirstDate(LAST_MODIFIED); - } - - /** - * Set the (new) location of a resource, - * as specified by the {@code Location} header. - */ - public void setLocation(URI location) { - set(LOCATION, location.toASCIIString()); - } - - /** - * Return the (new) location of a resource - * as specified by the {@code Location} header. - *

Returns {@code null} when the location is unknown. - */ - public URI getLocation() { - String value = getFirst(LOCATION); - return (value != null ? URI.create(value) : null); - } - - /** - * Set the (new) value of the {@code Origin} header. - */ - public void setOrigin(String origin) { - set(ORIGIN, origin); - } - - /** - * Return the value of the {@code Origin} header. - */ - public String getOrigin() { - return getFirst(ORIGIN); - } - - /** - * Set the (new) value of the {@code Pragma} header. - */ - public void setPragma(String pragma) { - set(PRAGMA, pragma); - } - - /** - * Return the value of the {@code Pragma} header. - */ - public String getPragma() { - return getFirst(PRAGMA); - } - - /** - * Sets the (new) value of the {@code Range} header. - */ - public void setRange(List ranges) { - String value = HttpRange.toString(ranges); - set(RANGE, value); - } - - /** - * Returns the value of the {@code Range} header. - *

Returns an empty list when the range is unknown. - */ - public List getRange() { - String value = getFirst(RANGE); - return HttpRange.parseRanges(value); - } - - /** - * Set the (new) value of the {@code Upgrade} header. - */ - public void setUpgrade(String upgrade) { - set(UPGRADE, upgrade); - } - - /** - * Returns the value of the {@code Upgrade} header. - */ - public String getUpgrade() { - return getFirst(UPGRADE); - } - - /** - * Parse the first header value for the given header name as a date, - * return -1 if there is no value, or raise {@link IllegalArgumentException} - * if the value cannot be parsed as a date. - */ - public long getFirstDate(String headerName) { - String headerValue = getFirst(headerName); - if (headerValue == null) { - return -1; - } - for (String dateFormat : DATE_FORMATS) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); - simpleDateFormat.setTimeZone(GMT); - try { - return simpleDateFormat.parse(headerValue).getTime(); - } - catch (ParseException ex) { - // ignore - } - } - throw new IllegalArgumentException("Cannot parse date value \"" + headerValue + - "\" for \"" + headerName + "\" header"); - } - - /** - * Set the given date under the given header name after formatting it as a string - * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of - * {@link #set(String, String)} but for date headers. - */ - public void setDate(String headerName, long date) { - SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); - dateFormat.setTimeZone(GMT); - set(headerName, dateFormat.format(new Date(date))); - } - - /** - * Return the first header value for the given header name, if any. - * @param headerName the header name - * @return the first header value, or {@code null} if none - */ - @Override - public String getFirst(String headerName) { - List headerValues = this.headers.get(headerName); - return (headerValues != null ? headerValues.get(0) : null); - } - - /** - * Add the given, single header value under the given name. - * @param headerName the header name - * @param headerValue the header value - * @throws UnsupportedOperationException if adding headers is not supported - * @see #put(String, List) - * @see #set(String, String) - */ - @Override - public void add(String headerName, String headerValue) { - List headerValues = this.headers.get(headerName); - if (headerValues == null) { - headerValues = new LinkedList(); - this.headers.put(headerName, headerValues); - } - headerValues.add(headerValue); - } - - /** - * Set the given, single header value under the given name. - * @param headerName the header name - * @param headerValue the header value - * @throws UnsupportedOperationException if adding headers is not supported - * @see #put(String, List) - * @see #add(String, String) - */ - @Override - public void set(String headerName, String headerValue) { - List headerValues = new LinkedList(); - headerValues.add(headerValue); - this.headers.put(headerName, headerValues); - } - - @Override - public void setAll(Map values) { - for (Entry entry : values.entrySet()) { - set(entry.getKey(), entry.getValue()); - } - } - - @Override - public Map toSingleValueMap() { - LinkedHashMap singleValueMap = new LinkedHashMap(this.headers.size()); - for (Entry> entry : this.headers.entrySet()) { - singleValueMap.put(entry.getKey(), entry.getValue().get(0)); - } - return singleValueMap; - } - - - // Map implementation - - @Override - public int size() { - return this.headers.size(); - } - - @Override - public boolean isEmpty() { - return this.headers.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.headers.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return this.headers.containsValue(value); - } - - @Override - public List get(Object key) { - return this.headers.get(key); - } - - @Override - public List put(String key, List value) { - return this.headers.put(key, value); - } - - @Override - public List remove(Object key) { - return this.headers.remove(key); - } - - @Override - public void putAll(Map> map) { - this.headers.putAll(map); - } - - @Override - public void clear() { - this.headers.clear(); - } - - @Override - public Set keySet() { - return this.headers.keySet(); - } - - @Override - public Collection> values() { - return this.headers.values(); - } - - @Override - public Set>> entrySet() { - return this.headers.entrySet(); - } - - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof HttpHeaders)) { - return false; - } - HttpHeaders otherHeaders = (HttpHeaders) other; - return this.headers.equals(otherHeaders.headers); - } - - @Override - public int hashCode() { - return this.headers.hashCode(); - } - - @Override - public String toString() { - return this.headers.toString(); - } - - - /** - * Return a {@code HttpHeaders} object that can only be read, not written to. - */ - public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { - return new HttpHeaders(headers, headers.getCookies(), true); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java new file mode 100644 index 0000000000..4d7d698d6e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.http; + +import java.time.Duration; +import java.util.Optional; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Represents a server-side cookie with extra attributes that a server can + * include in a Set-Cookie response header. + * + *

Use {@link #with} to create a {@code ServerHttpCookie}. + * + * @author Rossen Stoyanchev + * @see RFC 6265 + */ +public final class ServerHttpCookie extends HttpCookie { + + private final Duration maxAge; + + private final Optional domain; + + private final Optional path; + + private final boolean secure; + + private final boolean httpOnly; + + + /** + * Private constructor. See {@link #with(String, String)}. + */ + private ServerHttpCookie(String name, String value, Duration maxAge, String domain, + String path, boolean secure, boolean httpOnly) { + + super(name, value); + Assert.notNull(maxAge); + this.maxAge = maxAge; + this.domain = Optional.ofNullable(domain); + this.path = Optional.ofNullable(path); + this.secure = secure; + this.httpOnly = httpOnly; + } + + + /** + * Return the cookie "Max-Age" attribute in seconds. + * + *

A positive value indicates when the cookie expires relative to the + * current time. A value of 0 means the cookie should expire immediately. + * A negative value means no "Max-Age" attribute in which case the cookie + * is removed when the browser is closed. + */ + public Duration getMaxAge() { + return this.maxAge; + } + + /** + * Return the cookie "Domain" attribute. + */ + public Optional getDomain() { + return this.domain; + } + + /** + * Return the cookie "Path" attribute. + */ + public Optional getPath() { + return this.path; + } + + /** + * Return {@code true} if the cookie has the "Secure" attribute. + */ + public boolean isSecure() { + return this.secure; + } + + /** + * Return {@code true} if the cookie has the "HttpOnly" attribute. + * @see http://www.owasp.org/index.php/HTTPOnly + */ + public boolean isHttpOnly() { + return this.httpOnly; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.path); + return result; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ServerHttpCookie)) { + return false; + } + ServerHttpCookie otherCookie = (ServerHttpCookie) other; + return (getName().equalsIgnoreCase(otherCookie.getName()) && + ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) && + ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain())); + } + + + /** + * Factory method to obtain a builder for a server-defined cookie that starts + * with a name-value pair and may also include attributes. + * @param name the cookie name + * @param value the cookie value + * @return the created cookie instance + */ + public static ServerHttpCookieBuilder with(final String name, final String value) { + + return new ServerHttpCookieBuilder() { + + private Duration maxAge = Duration.ofSeconds(-1); + + private String domain; + + private String path; + + private boolean secure; + + private boolean httpOnly; + + + @Override + public ServerHttpCookieBuilder maxAge(Duration maxAge) { + this.maxAge = maxAge; + return this; + } + + @Override + public ServerHttpCookieBuilder domain(String domain) { + this.domain = domain; + return this; + } + + @Override + public ServerHttpCookieBuilder path(String path) { + this.path = path; + return this; + } + + @Override + public ServerHttpCookieBuilder secure() { + this.secure = true; + return this; + } + + @Override + public ServerHttpCookieBuilder httpOnly() { + this.httpOnly = true; + return this; + } + + @Override + public ServerHttpCookie build() { + return new ServerHttpCookie(name, value, this.maxAge, this.domain, this.path, + this.secure, this.httpOnly); + } + }; + } + + /** + * A builder for a server-defined HttpCookie with attributes. + */ + public interface ServerHttpCookieBuilder { + + /** + * Set the cookie "Max-Age" attribute. + * + *

A positive value indicates when the cookie should expire relative + * to the current time. A value of 0 means the cookie should expire + * immediately. A negative value results in no "Max-Age" attribute in + * which case the cookie is removed when the browser is closed. + */ + ServerHttpCookieBuilder maxAge(Duration maxAge); + + /** + * Set the cookie "Path" attribute. + */ + ServerHttpCookieBuilder path(String path); + + /** + * Set the cookie "Domain" attribute. + */ + ServerHttpCookieBuilder domain(String domain); + + /** + * Add the "Secure" attribute to the cookie. + */ + ServerHttpCookieBuilder secure(); + + /** + * Add the "HttpOnly" attribute to the cookie. + * @see http://www.owasp.org/index.php/HTTPOnly + */ + ServerHttpCookieBuilder httpOnly(); + + /** + * Create the HttpCookie. + */ + ServerHttpCookie build(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index ebc5641cb1..f9301eab6d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -17,14 +17,14 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Set; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Common base class for {@link ServerHttpRequest} implementations. @@ -37,6 +37,8 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers; + private MultiValueMap cookies; + @Override public URI getURI() { @@ -61,7 +63,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { @Override public HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = new HttpHeaders(new HttpCookieInputMap()); + this.headers = new HttpHeaders(); initHeaders(this.headers); } return this.headers; @@ -74,89 +76,20 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { */ protected abstract void initHeaders(HttpHeaders headers); + @Override + public MultiValueMap getCookies() { + if (this.cookies == null) { + this.cookies = new LinkedMultiValueMap(); + initCookies(this.cookies); + } + return this.cookies; + } + /** * Initialize the cookies from the underlying request. Invoked lazily on the * first access to cookies via {@link #getHeaders()} and then cached. * @param cookies the map to add cookies to */ - protected abstract void initCookies(Map> cookies); - - - /** - * Read-only map of input cookies with lazy initialization. - */ - private class HttpCookieInputMap implements Map> { - - private Map> cookies; - - - private Map> getCookies() { - if (this.cookies == null) { - this.cookies = new LinkedCaseInsensitiveMap<>(); - initCookies(this.cookies); - } - return this.cookies; - } - - @Override - public int size() { - return getCookies().size(); - } - - @Override - public boolean isEmpty() { - return getCookies().isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return getCookies().containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return getCookies().containsValue(value); - } - - @Override - public List get(Object key) { - return getCookies().get(key); - } - - @Override - public Set keySet() { - return getCookies().keySet(); - } - - @Override - public Collection> values() { - return getCookies().values(); - } - - @Override - public Set>> entrySet() { - return getCookies().entrySet(); - } - - @Override - public List put(String key, List value) { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - - @Override - public List remove(Object key) { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - - @Override - public void putAll(Map> map) { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - } + protected abstract void initCookies(MultiValueMap cookies); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 42dbf1c200..e4eb3e4feb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -17,6 +17,7 @@ package org.springframework.http.server.reactive; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -24,8 +25,13 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** @@ -37,6 +43,8 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; + private final MultiValueMap cookies; + private AtomicReference state = new AtomicReference<>(State.NEW); private final List>> beforeCommitActions = new ArrayList<>(4); @@ -44,6 +52,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { protected AbstractServerHttpResponse() { this.headers = new HttpHeaders(); + this.cookies = new LinkedMultiValueMap(); } @@ -55,6 +64,14 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return this.headers; } + @Override + public MultiValueMap getCookies() { + if (State.COMITTED.equals(this.state.get())) { + return CollectionUtils.unmodifiableMultiValueMap(this.cookies); + } + return this.cookies; + } + @Override public Mono setBody(Publisher publisher) { return new WriteWithOperator<>(publisher, writePublisher -> diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 8f40ed958d..db3bb6186e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -18,9 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; @@ -33,6 +30,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * Adapt {@link ServerHttpRequest} to the Reactor Net {@link HttpChannel}. @@ -76,15 +74,11 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> cookies) { + protected void initCookies(MultiValueMap cookies) { for (String name : this.channel.cookies().keySet()) { - List list = cookies.get(name); - if (list == null) { - list = new ArrayList<>(); - cookies.put(name, list); - } for (Cookie cookie : this.channel.cookies().get(name)) { - list.add(HttpCookie.clientCookie(name, cookie.value())); + HttpCookie httpCookie = new HttpCookie(name, cookie.value()); + cookies.add(name, httpCookie); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 011b3d599a..37ec2b309e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,6 +16,7 @@ package org.springframework.http.server.reactive; import java.time.Duration; +import java.util.Optional; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -26,8 +27,8 @@ import reactor.io.net.http.model.Cookie; import reactor.io.net.http.model.Status; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -73,10 +74,10 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { - Cookie reactorCookie = new ReactorCookie(httpCookie); - this.channel.addResponseCookie(name, reactorCookie); + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { + Cookie cookie = new ReactorCookie(httpCookie); + this.channel.addResponseCookie(name, cookie); } } } @@ -87,10 +88,10 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { */ private final static class ReactorCookie extends Cookie { - private final HttpCookie httpCookie; + private final ServerHttpCookie httpCookie; - public ReactorCookie(HttpCookie httpCookie) { + public ReactorCookie(ServerHttpCookie httpCookie) { this.httpCookie = httpCookie; } @@ -117,12 +118,14 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override public String domain() { - return this.httpCookie.getDomain(); + Optional domain = this.httpCookie.getDomain(); + return (domain.isPresent() ? domain.get() : null); } @Override public String path() { - return this.httpCookie.getPath(); + Optional path = this.httpCookie.getPath(); + return (path.isPresent() ? path.get() : null); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 7fd9e01d2a..e3b1562005 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -18,9 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.Cookie; @@ -35,6 +32,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * Adapt {@link ServerHttpRequest} to the RxNetty {@link HttpServerRequest}. @@ -79,15 +77,11 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(MultiValueMap cookies) { for (String name : this.request.getCookies().keySet()) { - List list = map.get(name); - if (list == null) { - list = new ArrayList<>(); - map.put(name, list); - } for (Cookie cookie : this.request.getCookies().get(name)) { - list.add(HttpCookie.clientCookie(name, cookie.value())); + HttpCookie httpCookie = new HttpCookie(name, cookie.value()); + cookies.add(name, httpCookie); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index ca064ad2b5..9cb04d6719 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -29,8 +29,8 @@ import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -85,14 +85,18 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge(httpCookie.getMaxAge().getSeconds()); } - cookie.setDomain(httpCookie.getDomain()); - cookie.setPath(httpCookie.getPath()); + if (httpCookie.getDomain().isPresent()) { + cookie.setDomain(httpCookie.getDomain().get()); + } + if (httpCookie.getPath().isPresent()) { + cookie.setPath(httpCookie.getPath().get()); + } cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.response.addCookie(cookie); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 90a52dd13c..695e944b0a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -16,8 +16,13 @@ package org.springframework.http.server.reactive; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpCookie; import org.springframework.http.HttpRequest; import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.util.MultiValueMap; /** * Represents a reactive server-side HTTP request @@ -26,4 +31,9 @@ import org.springframework.http.ReactiveHttpInputMessage; */ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage { + /** + * Return a read-only map of cookies sent by the client. + */ + MultiValueMap getCookies(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index c7449eca34..c6d53276e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -16,10 +16,16 @@ package org.springframework.http.server.reactive; +import java.util.List; +import java.util.Map; + import reactor.core.publisher.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.ServerHttpCookie; +import org.springframework.util.MultiValueMap; /** * Represents a reactive server-side HTTP response. @@ -34,6 +40,11 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { */ void setStatusCode(HttpStatus status); + /** + * Return a mutable map with cookies to be sent to the client. + */ + MultiValueMap getCookies(); + /** * Indicate that request handling is complete, allowing for any cleanup or * end-of-processing tasks to be performed such as applying header changes diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 2374d86231..289d20b8c8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -20,9 +20,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.Enumeration; -import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ReadListener; @@ -45,6 +43,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -127,17 +126,13 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(MultiValueMap httpCookies) { Cookie[] cookies = this.request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { String name = cookie.getName(); - List list = map.get(name); - if (list == null) { - list = new ArrayList<>(); - map.put(name, list); - } - list.add(HttpCookie.clientCookie(name, cookie.getValue())); + HttpCookie httpCookie = new HttpCookie(name, cookie.getValue()); + httpCookies.add(name, httpCookie); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 2ee70f42a9..3c0a5fd9cf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -34,9 +34,9 @@ import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -98,17 +98,17 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { Cookie cookie = new Cookie(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } - if (httpCookie.getDomain() != null) { - cookie.setDomain(httpCookie.getDomain()); + if (httpCookie.getDomain().isPresent()) { + cookie.setDomain(httpCookie.getDomain().get()); } - if (httpCookie.getPath() != null) { - cookie.setPath(httpCookie.getPath()); + if (httpCookie.getPath().isPresent()) { + cookie.setPath(httpCookie.getPath().get()); } cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index da38edd041..cebcc776ed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -18,9 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; @@ -33,6 +30,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * Adapt {@link ServerHttpRequest} to the Underow {@link HttpServerExchange}. @@ -79,15 +77,11 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(MultiValueMap cookies) { for (String name : this.exchange.getRequestCookies().keySet()) { - List list = map.get(name); - if (list == null) { - list = new ArrayList<>(); - map.put(name, list); - } Cookie cookie = this.exchange.getRequestCookies().get(name); - list.add(HttpCookie.clientCookie(name, cookie.getValue())); + HttpCookie httpCookie = new HttpCookie(name, cookie.getValue()); + cookies.add(name, httpCookie); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index dcaa12d233..091bf80fdd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -28,8 +28,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -80,14 +80,18 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { Cookie cookie = new CookieImpl(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } - cookie.setDomain(httpCookie.getDomain()); - cookie.setPath(httpCookie.getPath()); + if (httpCookie.getDomain().isPresent()) { + cookie.setDomain(httpCookie.getDomain().get()); + } + if (httpCookie.getPath().isPresent()) { + cookie.setPath(httpCookie.getPath().get()); + } cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.exchange.getResponseCookies().putIfAbsent(name, cookie); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java index f325be9e67..51aeaae895 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -18,12 +18,14 @@ package org.springframework.web.server.session; import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import org.springframework.http.HttpCookie; -import org.springframework.http.HttpHeaders; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; @@ -58,7 +60,7 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { /** * Set the value for the "Max-Age" attribute of the cookie that holds the - * session id. For the range of values see {@link HttpCookie#getMaxAge()}. + * session id. For the range of values see {@link ServerHttpCookie#getMaxAge()}. *

By default set to -1. * @param maxAge the maxAge duration value */ @@ -76,18 +78,17 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @Override public Optional resolveSessionId(ServerWebExchange exchange) { - HttpHeaders headers = exchange.getRequest().getHeaders(); - List cookies = headers.getCookies().get(getCookieName()); - return (CollectionUtils.isEmpty(cookies) ? - Optional.empty() : Optional.of(cookies.get(0).getValue())); + MultiValueMap cookieMap = exchange.getRequest().getCookies(); + HttpCookie cookie = cookieMap.getFirst(getCookieName()); + return (cookie != null ? Optional.of(cookie.getValue()) : Optional.empty()); } @Override public void setSessionId(ServerWebExchange exchange, String id) { Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0)); - HttpCookie cookie = HttpCookie.serverCookie(getCookieName(), id).maxAge(maxAge).build(); - HttpHeaders headers = exchange.getResponse().getHeaders(); - headers.getCookies().put(getCookieName(), Collections.singletonList(cookie)); + ServerHttpCookie cookie = ServerHttpCookie.with(getCookieName(), id).maxAge(maxAge).build(); + MultiValueMap cookieMap = exchange.getResponse().getCookies(); + cookieMap.set(getCookieName(), cookie); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index 3a20401b74..547a6f0457 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -28,6 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.http.HttpCookie; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.ServerHttpCookie; import org.springframework.web.client.RestTemplate; import static org.hamcrest.CoreMatchers.equalTo; @@ -98,12 +99,12 @@ public class CookieIntegrationTests extends AbstractHttpHandlerIntegrationTests @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - this.requestCookies = request.getHeaders().getCookies(); + this.requestCookies = request.getCookies(); this.requestCookies.size(); // Cause lazy loading - response.getHeaders().addCookie(HttpCookie.serverCookie("SID", "31d4d96e407aad42") + response.getCookies().add("SID", ServerHttpCookie.with("SID", "31d4d96e407aad42") .path("/").secure().httpOnly().build()); - response.getHeaders().addCookie(HttpCookie.serverCookie("lang", "en-US") + response.getCookies().add("lang", ServerHttpCookie.with("lang", "en-US") .domain("example.com").path("/").build()); return response.setComplete(); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index 01612d73d1..7678536a38 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -21,8 +21,11 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * @author Rossen Stoyanchev @@ -35,6 +38,8 @@ public class MockServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers = new HttpHeaders(); + private MultiValueMap cookies = new LinkedMultiValueMap<>(); + private Flux body; @@ -74,8 +79,9 @@ public class MockServerHttpRequest implements ServerHttpRequest { return this.headers; } - public void setHeaders(HttpHeaders headers) { - this.headers = headers; + @Override + public MultiValueMap getCookies() { + return this.cookies; } @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 4fd1d1e2b2..995846464b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -24,6 +24,9 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * @author Rossen Stoyanchev @@ -34,6 +37,8 @@ public class MockServerHttpResponse implements ServerHttpResponse { private HttpHeaders headers = new HttpHeaders(); + private MultiValueMap cookies = new LinkedMultiValueMap<>(); + private Publisher body; @@ -51,6 +56,11 @@ public class MockServerHttpResponse implements ServerHttpResponse { return this.headers; } + @Override + public MultiValueMap getCookies() { + return this.cookies; + } + public Publisher getBody() { return this.body; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index af049f0f29..260e18bdb2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -27,8 +27,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; @@ -81,17 +81,17 @@ public class ServerHttpResponseTests { @Test public void beforeCommitWithSetBody() throws Exception { - HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build(); + ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { - response.getHeaders().addCookie(cookie); + response.getCookies().add(cookie.getName(), cookie); return Mono.empty(); }); response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); - assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0)); + assertSame(cookie, response.getCookies().getFirst("ID")); assertEquals(3, response.content.size()); assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); @@ -108,7 +108,7 @@ public class ServerHttpResponseTests { assertTrue("beforeCommit action errors should be ignored", response.headersWritten); assertTrue("beforeCommit action errors should be ignored", response.cookiesWritten); - assertNull(response.getHeaders().getCookies().get("ID")); + assertNull(response.getCookies().get("ID")); assertEquals(3, response.content.size()); assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); @@ -118,10 +118,10 @@ public class ServerHttpResponseTests { @Test public void beforeCommitActionWithSetComplete() throws Exception { - HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build(); + ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { - response.getHeaders().addCookie(cookie); + response.getCookies().add(cookie.getName(), cookie); return Mono.empty(); }); response.setComplete().get(); @@ -129,7 +129,7 @@ public class ServerHttpResponseTests { assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); assertTrue(response.content.isEmpty()); - assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0)); + assertSame(cookie, response.getCookies().getFirst("ID")); }