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. Supported only when writing output cookies.
+ */
+ public void addCookie(HttpCookie cookie) {
+ String name = cookie.getName();
+ Set set = this.cookies.get(name);
+ if (set == null) {
+ set = new LinkedHashSet<>();
+ 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 extends String, ? extends List> 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/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java
index e7a35a8e6b..24eeeb61c1 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,8 +17,13 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpCookie;
/**
* Common base class for {@link ServerHttpRequest} implementations.
@@ -46,8 +51,8 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
}
/**
- * Initialize a URI that represents the request.
- * Invoked lazily on the first call to {@link #getURI()} and then cached.
+ * Initialize a URI that represents the request. Invoked lazily on the first
+ * call to {@link #getURI()} and then cached.
* @throws URISyntaxException
*/
protected abstract URI initUri() throws URISyntaxException;
@@ -55,15 +60,102 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
@Override
public HttpHeaders getHeaders() {
if (this.headers == null) {
- this.headers = HttpHeaders.readOnlyHttpHeaders(initHeaders());
+ this.headers = new HttpHeaders(new HttpCookieInputMap());
+ initHeaders(this.headers);
}
return this.headers;
}
/**
- * Initialize the headers from the underlying request.
- * Invoked lazily on the first call to {@link #getHeaders()} and then cached.
+ * Initialize the headers from the underlying request. Invoked lazily on the
+ * first call to {@link #getHeaders()} and then cached.
+ * @param headers the map to add headers to
*/
- protected abstract HttpHeaders initHeaders();
+ protected abstract void initHeaders(HttpHeaders headers);
+
+ /**
+ * 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 LinkedHashMap<>();
+ 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 Set 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 Set put(String key, Set value) {
+ throw new UnsupportedOperationException("Read-only map of cookies.");
+ }
+
+ @Override
+ public Set remove(Object key) {
+ throw new UnsupportedOperationException("Read-only map of cookies.");
+ }
+
+ @Override
+ public void putAll(Map extends String, ? extends Set> m) {
+ throw new UnsupportedOperationException("Read-only map of cookies.");
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException("Read-only map of 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 2c87f89c46..f75f4be2ad 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
@@ -42,7 +42,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
@Override
public HttpHeaders getHeaders() {
- return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
+ return (this.headersWritten ? org.springframework.http.HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
@Override
@@ -64,6 +64,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
if (!this.headersWritten) {
try {
writeHeadersInternal();
+ writeCookies();
}
finally {
this.headersWritten = true;
@@ -73,9 +74,14 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
/**
* Implement this method to apply header changes from {@link #getHeaders()}
- * to the underlying response. This method is protected from being called
- * more than once.
+ * to the underlying response. This method is called once only.
*/
protected abstract void writeHeadersInternal();
+ /**
+ * Implement this method to add cookies from {@link #getHeaders()} to the
+ * underlying response. This method is called once only.
+ */
+ protected abstract void writeCookies();
+
}
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 e1f62514c7..4ae9fc9f0a 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,11 +18,14 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Set;
import reactor.Flux;
import reactor.io.buffer.Buffer;
import reactor.io.net.http.HttpChannel;
+import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
@@ -58,12 +61,15 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest {
}
@Override
- protected HttpHeaders initHeaders() {
- HttpHeaders headers = new HttpHeaders();
+ protected void initHeaders(HttpHeaders headers) {
for (String name : this.channel.headers().names()) {
headers.put(name, this.channel.headers().getAll(name));
}
- return headers;
+ }
+
+ @Override
+ protected void initCookies(Map> cookies) {
+ // https://github.com/reactor/reactor/issues/614
}
@Override
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 64a44b94f6..b7cbed3b8c 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
@@ -67,4 +67,9 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
}
}
+ @Override
+ protected void writeCookies() {
+ // https://github.com/reactor/reactor/issues/614
+ }
+
}
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 afdabae668..3e6c541f66 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
@@ -19,13 +19,18 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
import io.netty.buffer.ByteBuf;
+import io.netty.handler.codec.http.cookie.Cookie;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
import reactor.Flux;
import reactor.core.publisher.convert.RxJava1Converter;
import rx.Observable;
+import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
@@ -53,26 +58,43 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest {
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.getRxNettyRequest().getHttpMethod().name());
+ return HttpMethod.valueOf(this.request.getHttpMethod().name());
}
@Override
protected URI initUri() throws URISyntaxException {
- return new URI(this.getRxNettyRequest().getUri());
+ return new URI(this.request.getUri());
}
@Override
- protected HttpHeaders initHeaders() {
- HttpHeaders headers = new HttpHeaders();
- for (String name : this.getRxNettyRequest().getHeaderNames()) {
- headers.put(name, this.getRxNettyRequest().getAllHeaderValues(name));
+ protected void initHeaders(HttpHeaders headers) {
+ for (String name : this.request.getHeaderNames()) {
+ headers.put(name, this.request.getAllHeaderValues(name));
+ }
+ }
+
+ @Override
+ protected void initCookies(Map> map) {
+ for (String name : this.request.getCookies().keySet()) {
+ Set set = map.get(name);
+ if (set == null) {
+ set = new LinkedHashSet<>();
+ map.put(name, set);
+ }
+ for (Cookie cookie : this.request.getCookies().get(name)) {
+ set.add(new HttpCookie(name, cookie.value())
+ .setDomain(cookie.domain())
+ .setPath(cookie.path())
+ .setMaxAge(cookie.maxAge())
+ .setSecure(cookie.isSecure())
+ .setHttpOnly(cookie.isHttpOnly()));
+ }
}
- return headers;
}
@Override
public Flux getBody() {
- Observable content = this.getRxNettyRequest().getContent().map(ByteBuf::nioBuffer);
+ Observable content = this.request.getContent().map(ByteBuf::nioBuffer);
content = content.concatWith(Observable.empty()); // See GH issue #58
return RxJava1Converter.from(content);
}
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 5f47e78ced..467c1d6dfd 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
@@ -19,14 +19,15 @@ package org.springframework.http.server.reactive;
import java.nio.ByteBuffer;
import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.cookie.Cookie;
+import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
import org.reactivestreams.Publisher;
-import reactor.Flux;
import reactor.Mono;
import reactor.core.publisher.convert.RxJava1Converter;
import rx.Observable;
-import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
@@ -53,13 +54,13 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
@Override
public void setStatusCode(HttpStatus status) {
- getRxNettyResponse().setStatus(HttpResponseStatus.valueOf(status.value()));
+ this.response.setStatus(HttpResponseStatus.valueOf(status.value()));
}
@Override
protected Mono setBodyInternal(Publisher publisher) {
Observable content = RxJava1Converter.from(publisher).map(this::toBytes);
- Observable completion = getRxNettyResponse().writeBytes(content);
+ Observable completion = this.response.writeBytes(content);
return RxJava1Converter.from(completion).after();
}
@@ -77,4 +78,19 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
}
}
+ @Override
+ protected void writeCookies() {
+ for (String name : getHeaders().getCookies().keySet()) {
+ for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
+ Cookie cookie = new DefaultCookie(name, httpCookie.getValue());
+ cookie.setDomain(httpCookie.getDomain());
+ cookie.setPath(httpCookie.getPath());
+ cookie.setMaxAge(httpCookie.getMaxAge());
+ cookie.setSecure(httpCookie.isSecure());
+ cookie.setHttpOnly(httpCookie.isHttpOnly());
+ this.response.addCookie(cookie);
+ }
+ }
+ }
+
}
\ No newline at end of file
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 a076768c7f..867c9356a2 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
@@ -21,12 +21,16 @@ import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Enumeration;
+import java.util.LinkedHashSet;
import java.util.Map;
+import java.util.Set;
+import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.reactivestreams.Publisher;
import reactor.Flux;
+import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
@@ -73,8 +77,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest {
}
@Override
- protected HttpHeaders initHeaders() {
- HttpHeaders headers = new HttpHeaders();
+ protected void initHeaders(HttpHeaders headers) {
for (Enumeration> names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) {
String name = (String) names.nextElement();
for (Enumeration> values = getServletRequest().getHeaders(name); values.hasMoreElements(); ) {
@@ -105,7 +108,24 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest {
headers.setContentLength(contentLength);
}
}
- return headers;
+ }
+
+ @Override
+ protected void initCookies(Map> map) {
+ for (Cookie cookie : this.request.getCookies()) {
+ String name = cookie.getName();
+ Set set = map.get(name);
+ if (set == null) {
+ set = new LinkedHashSet<>();
+ map.put(name, set);
+ }
+ set.add(new HttpCookie(name, cookie.getValue())
+ .setDomain(cookie.getDomain())
+ .setPath(cookie.getPath())
+ .setMaxAge(cookie.getMaxAge() == -1 ? Long.MIN_VALUE : cookie.getMaxAge())
+ .setHttpOnly(cookie.isHttpOnly())
+ .setSecure(cookie.getSecure()));
+ }
}
@Override
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 a292b44f9b..a4e7e91446 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
@@ -21,11 +21,13 @@ import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
+import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.reactivestreams.Publisher;
import reactor.Mono;
+import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
@@ -84,4 +86,23 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse {
}
}
+ @Override
+ protected void writeCookies() {
+ for (String name : getHeaders().getCookies().keySet()) {
+ for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
+ Cookie cookie = new Cookie(name, httpCookie.getValue());
+ if (httpCookie.getDomain() != null) {
+ cookie.setDomain(httpCookie.getDomain());
+ }
+ if (httpCookie.getPath() != null) {
+ cookie.setPath(httpCookie.getPath());
+ }
+ cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? -1 : (int) httpCookie.getMaxAge());
+ cookie.setSecure(httpCookie.isSecure());
+ cookie.setHttpOnly(httpCookie.isHttpOnly());
+ this.response.addCookie(cookie);
+ }
+ }
+ }
+
}
\ No newline at end of file
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 6b9b3c32f4..53013fbe54 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
@@ -19,12 +19,17 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
import io.undertow.server.HttpServerExchange;
+import io.undertow.server.handlers.Cookie;
import io.undertow.util.HeaderValues;
import org.reactivestreams.Publisher;
import reactor.Flux;
+import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
@@ -67,12 +72,28 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest {
}
@Override
- protected HttpHeaders initHeaders() {
- HttpHeaders headers = new HttpHeaders();
+ protected void initHeaders(HttpHeaders headers) {
for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) {
headers.put(values.getHeaderName().toString(), values);
}
- return headers;
+ }
+
+ @Override
+ protected void initCookies(Map> map) {
+ for (String name : this.exchange.getRequestCookies().keySet()) {
+ Set set = map.get(name);
+ if (set == null) {
+ set = new LinkedHashSet<>();
+ map.put(name, set);
+ }
+ Cookie cookie = this.exchange.getRequestCookies().get(name);
+ set.add(new HttpCookie(name, cookie.getValue())
+ .setDomain(cookie.getDomain())
+ .setPath(cookie.getPath())
+ .setMaxAge(cookie.getMaxAge() != null ? cookie.getMaxAge() : Long.MIN_VALUE)
+ .setSecure(cookie.isSecure())
+ .setHttpOnly(cookie.isHttpOnly()));
+ }
}
@Override
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 b32255f461..27c38342f8 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
@@ -22,10 +22,13 @@ import java.util.Map;
import java.util.function.Function;
import io.undertow.server.HttpServerExchange;
+import io.undertow.server.handlers.Cookie;
+import io.undertow.server.handlers.CookieImpl;
import io.undertow.util.HttpString;
import org.reactivestreams.Publisher;
import reactor.Mono;
+import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
@@ -75,4 +78,19 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse {
}
}
+ @Override
+ protected void writeCookies() {
+ for (String name : getHeaders().getCookies().keySet()) {
+ for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
+ Cookie cookie = new CookieImpl(name, httpCookie.getValue());
+ cookie.setDomain(httpCookie.getDomain());
+ cookie.setPath(httpCookie.getPath());
+ cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? null : (int) httpCookie.getMaxAge());
+ cookie.setSecure(httpCookie.isSecure());
+ cookie.setHttpOnly(httpCookie.isHttpOnly());
+ this.exchange.getResponseCookies().putIfAbsent(name, 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
new file mode 100644
index 0000000000..6a1dccb4ae
--- /dev/null
+++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java
@@ -0,0 +1,160 @@
+/*
+ * 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.server.reactive;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import reactor.Mono;
+
+import org.springframework.http.HttpCookie;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.server.reactive.boot.HttpServer;
+import org.springframework.http.server.reactive.boot.JettyHttpServer;
+import org.springframework.http.server.reactive.boot.RxNettyHttpServer;
+import org.springframework.http.server.reactive.boot.TomcatHttpServer;
+import org.springframework.http.server.reactive.boot.UndertowHttpServer;
+import org.springframework.util.SocketUtils;
+import org.springframework.web.client.RestTemplate;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalToIgnoringCase;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Temporarily does not extend AbstractHttpHandlerIntegrationTests in order to
+ * exclude Reactor Net due to https://github.com/reactor/reactor/issues/614.
+ *
+ * @author Rossen Stoyanchev
+ */
+@RunWith(Parameterized.class)
+public class CookieIntegrationTests {
+
+ protected int port;
+
+ @Parameterized.Parameter(0)
+ public HttpServer server;
+
+ private CookieHandler cookieHandler;
+
+
+ @Parameterized.Parameters(name = "server [{0}]")
+ public static Object[][] arguments() {
+ return new Object[][] {
+ {new JettyHttpServer()},
+ {new RxNettyHttpServer()},
+// {new ReactorHttpServer()},
+ {new TomcatHttpServer()},
+ {new UndertowHttpServer()}
+ };
+ }
+
+
+ @Before
+ public void setup() throws Exception {
+ this.port = SocketUtils.findAvailableTcpPort();
+ this.server.setPort(this.port);
+ this.server.setHandler(createHttpHandler());
+ this.server.afterPropertiesSet();
+ this.server.start();
+ }
+
+ protected HttpHandler createHttpHandler() {
+ this.cookieHandler = new CookieHandler();
+ return this.cookieHandler;
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ this.server.stop();
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void basicTest() throws Exception {
+ URI url = new URI("http://localhost:" + port);
+ String header = "SID=31d4d96e407aad42; lang=en-US";
+ ResponseEntity response = new RestTemplate().exchange(
+ RequestEntity.get(url).header("Cookie", header).build(), Void.class);
+
+ Map> requestCookies = this.cookieHandler.requestCookies;
+ assertEquals(2, requestCookies.size());
+
+ Set set = requestCookies.get("SID");
+ assertEquals(1, set.size());
+ assertEquals("31d4d96e407aad42", set.iterator().next().getValue());
+
+ set = requestCookies.get("lang");
+ assertEquals(1, set.size());
+ assertEquals("en-US", set.iterator().next().getValue());
+
+ List headerValues = response.getHeaders().get("Set-Cookie");
+ assertEquals(2, headerValues.size());
+
+ List parts = splitCookieHeader(headerValues.get(0));
+ assertThat(parts, containsInAnyOrder(equalTo("SID=31d4d96e407aad42"),
+ equalToIgnoringCase("Path=/"), equalToIgnoringCase("Secure"),
+ equalToIgnoringCase("HttpOnly")));
+
+ parts = splitCookieHeader(headerValues.get(1));
+ assertThat(parts, containsInAnyOrder(equalTo("lang=en-US"),
+ equalToIgnoringCase("Path=/"), equalToIgnoringCase("Domain=example.com")));
+ }
+
+ // No client side HttpCookie support yet
+ private List splitCookieHeader(String value) {
+ List list = new ArrayList<>();
+ for (String s : value.split(";")){
+ list.add(s.trim());
+ }
+ return list;
+ }
+
+
+ private class CookieHandler implements HttpHandler {
+
+ private Map> requestCookies;
+
+
+ @Override
+ public Mono handle(ServerHttpRequest request, ServerHttpResponse response) {
+
+ this.requestCookies = request.getHeaders().getCookies();
+ this.requestCookies.size(); // Cause lazy loading
+
+ response.getHeaders().addCookie(new HttpCookie("SID", "31d4d96e407aad42")
+ .setPath("/").setHttpOnly(true).setSecure(true));
+ response.getHeaders().addCookie(new HttpCookie("lang", "en-US")
+ .setDomain("example.com").setPath("/"));
+ response.writeHeaders();
+
+ return Mono.empty();
+ }
+ }
+
+}