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 5021b62614..81a762f352 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,6 +15,8 @@
*/
package org.springframework.http;
+import java.time.Duration;
+
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -34,7 +36,7 @@ public final class HttpCookie {
private final String value;
- private final int maxAge;
+ private final Duration maxAge;
private final String domain;
@@ -46,17 +48,17 @@ public final class HttpCookie {
private HttpCookie(String name, String value) {
- this(name, value, -1, null, null, false, false);
+ this(name, value, Duration.ofSeconds(-1), null, null, false, false);
}
- private HttpCookie(String name, String value, int maxAge, String domain, String path,
+ private HttpCookie(String name, String value, Duration maxAge, String domain, String path,
boolean secure, boolean httpOnly) {
Assert.hasLength(name, "'name' is required and must not be empty.");
- Assert.hasLength(value, "'value' is required and must not be empty.");
+ Assert.notNull(maxAge);
this.name = name;
- this.value = value;
- this.maxAge = (maxAge > -1 ? maxAge : -1);
+ this.value = (value != null ? value : "");
+ this.maxAge = maxAge;
this.domain = domain;
this.path = path;
this.secure = secure;
@@ -85,7 +87,7 @@ public final class HttpCookie {
* A negative value means no "Max-Age" attribute in which case the cookie
* is removed when the browser is closed.
*/
- public int getMaxAge() {
+ public Duration getMaxAge() {
return this.maxAge;
}
@@ -162,7 +164,7 @@ public final class HttpCookie {
return new HttpCookieBuilder() {
- private int maxAge = -1;
+ private Duration maxAge = Duration.ofSeconds(-1);
private String domain;
@@ -174,7 +176,7 @@ public final class HttpCookie {
@Override
- public HttpCookieBuilder maxAge(int maxAge) {
+ public HttpCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
return this;
}
@@ -217,14 +219,14 @@ public final class HttpCookie {
public interface HttpCookieBuilder {
/**
- * Set the cookie "Max-Age" attribute in seconds.
+ * 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(int maxAge);
+ HttpCookieBuilder maxAge(Duration maxAge);
/**
* Set the cookie "Path" attribute.
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 9dbcf0a2bb..704ddf4db6 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
@@ -66,25 +66,23 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
}
private Mono applyBeforeCommit() {
- return Stream.defer(() -> {
- Mono mono = Mono.empty();
- if (this.state.compareAndSet(State.NEW, State.COMMITTING)) {
- for (Supplier extends Mono> action : this.beforeCommitActions) {
- mono = mono.after(() -> action.get());
- }
- mono = mono.otherwise(ex -> {
- // Ignore errors from beforeCommit actions
- return Mono.empty();
- });
- mono = mono.after(() -> {
- this.state.set(State.COMITTED);
- writeHeaders();
- writeCookies();
- return Mono.empty();
- });
+ Mono mono = Mono.empty();
+ if (this.state.compareAndSet(State.NEW, State.COMMITTING)) {
+ for (Supplier extends Mono> action : this.beforeCommitActions) {
+ mono = mono.after(() -> action.get());
}
- return mono;
- }).after();
+ mono = mono.otherwise(ex -> {
+ // Ignore errors from beforeCommit actions
+ return Mono.empty();
+ });
+ mono = mono.after(() -> {
+ this.state.set(State.COMITTED);
+ writeHeaders();
+ writeCookies();
+ return Mono.empty();
+ });
+ }
+ return mono;
}
/**
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 8b0f3bcd19..011b3d599a 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
@@ -15,6 +15,8 @@
*/
package org.springframework.http.server.reactive;
+import java.time.Duration;
+
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -73,55 +75,59 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
protected void writeCookies() {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
- Cookie cookie = new ReactorCookie(name, httpCookie);
- this.channel.addResponseCookie(name, cookie);
+ Cookie reactorCookie = new ReactorCookie(httpCookie);
+ this.channel.addResponseCookie(name, reactorCookie);
}
}
}
+
+ /**
+ * At present Reactor does not provide a {@link Cookie} implementation.
+ */
private final static class ReactorCookie extends Cookie {
- final HttpCookie httpCookie;
- final String name;
+ private final HttpCookie httpCookie;
- public ReactorCookie(String name, HttpCookie httpCookie) {
- this.name = name;
+
+ public ReactorCookie(HttpCookie httpCookie) {
this.httpCookie = httpCookie;
}
@Override
public String name() {
- return name;
+ return this.httpCookie.getName();
}
@Override
public String value() {
- return httpCookie.getValue();
+ return this.httpCookie.getValue();
}
@Override
public boolean httpOnly() {
- return httpCookie.isHttpOnly();
+ return this.httpCookie.isHttpOnly();
}
@Override
public long maxAge() {
- return httpCookie.getMaxAge() > -1 ? httpCookie.getMaxAge() : -1;
+ Duration maxAge = this.httpCookie.getMaxAge();
+ return (!maxAge.isNegative() ? maxAge.getSeconds() : -1);
}
@Override
public String domain() {
- return httpCookie.getDomain();
+ return this.httpCookie.getDomain();
}
@Override
public String path() {
- return httpCookie.getPath();
+ return this.httpCookie.getPath();
}
@Override
public boolean secure() {
- return httpCookie.isSecure();
+ return this.httpCookie.isSecure();
}
}
}
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 417e6b0a50..e1ad9ad646 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
@@ -82,8 +82,8 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
Cookie cookie = new DefaultCookie(name, httpCookie.getValue());
- if (httpCookie.getMaxAge() > -1) {
- cookie.setMaxAge(httpCookie.getMaxAge());
+ if (!httpCookie.getMaxAge().isNegative()) {
+ cookie.setMaxAge(httpCookie.getMaxAge().getSeconds());
}
cookie.setDomain(httpCookie.getDomain());
cookie.setPath(httpCookie.getPath());
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 5bc34daa71..cbfb6e7342 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
@@ -113,14 +113,17 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest {
@Override
protected void initCookies(Map> map) {
- for (Cookie cookie : this.request.getCookies()) {
- String name = cookie.getName();
- List list = map.get(name);
- if (list == null) {
- list = new ArrayList<>();
- map.put(name, list);
+ 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()));
}
- list.add(HttpCookie.clientCookie(name, cookie.getValue()));
}
}
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 887baa567a..eeb50d96c8 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
@@ -91,8 +91,8 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
Cookie cookie = new Cookie(name, httpCookie.getValue());
- if (httpCookie.getMaxAge() > -1) {
- cookie.setMaxAge(httpCookie.getMaxAge());
+ if (!httpCookie.getMaxAge().isNegative()) {
+ cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
if (httpCookie.getDomain() != null) {
cookie.setDomain(httpCookie.getDomain());
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 c4d8929b34..dcaa12d233 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
@@ -83,8 +83,8 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
Cookie cookie = new CookieImpl(name, httpCookie.getValue());
- if (httpCookie.getMaxAge() > -1) {
- cookie.setMaxAge(httpCookie.getMaxAge());
+ if (!httpCookie.getMaxAge().isNegative()) {
+ cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
cookie.setDomain(httpCookie.getDomain());
cookie.setPath(httpCookie.getPath());
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java
index fe5cb4f5db..49703cddb6 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java
@@ -18,9 +18,14 @@ package org.springframework.web.server;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import reactor.core.publisher.FluxProcessor;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.Processors;
+
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
+import org.springframework.web.server.session.WebSessionManager;
/**
* Default implementation of {@link WebServerExchange}.
@@ -33,14 +38,26 @@ public class DefaultWebServerExchange implements WebServerExchange {
private final ServerHttpResponse response;
+ private final WebSessionManager sessionManager;
+
+
private final Map attributes = new ConcurrentHashMap<>();
+ private final Object createSessionLock = new Object();
+
+ private Mono sessionMono;
+
+
+
+ public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response,
+ WebSessionManager sessionManager) {
- public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) {
Assert.notNull(request, "'request' is required.");
Assert.notNull(response, "'response' is required.");
+ Assert.notNull(response, "'sessionManager' is required.");
this.request = request;
this.response = response;
+ this.sessionManager = sessionManager;
}
@@ -59,4 +76,17 @@ public class DefaultWebServerExchange implements WebServerExchange {
return this.attributes;
}
+ @Override
+ public Mono getSession() {
+ if (this.sessionMono == null) {
+ synchronized (this.createSessionLock) {
+ if (this.sessionMono == null) {
+ FluxProcessor replay = Processors.replay(1);
+ this.sessionMono = this.sessionManager.getSession(this).subscribeWith(replay).next();
+ }
+ }
+ }
+ return this.sessionMono;
+ }
+
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java
index 87fbf7ac3c..53d1006e0a 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java
@@ -17,6 +17,8 @@ package org.springframework.web.server;
import java.util.Map;
+import reactor.core.publisher.Mono;
+
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
@@ -30,18 +32,23 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
public interface WebServerExchange {
/**
- * @return the current HTTP request
+ * Return the current HTTP request.
*/
ServerHttpRequest getRequest();
/**
- * @return the current HTTP response
+ * Return the current HTTP response.
*/
ServerHttpResponse getResponse();
/**
- * @return mutable map of request attributes for the current exchange
+ * Return a mutable map of request attributes for the current exchange.
*/
Map getAttributes();
+ /**
+ *
+ */
+ Mono getSession();
+
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java
new file mode 100644
index 0000000000..ca6554cbcf
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java
@@ -0,0 +1,109 @@
+/*
+ * 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.web.server;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * Main contract for using a server-side session that provides access to session
+ * attributes across HTTP requests.
+ *
+ * The creation of a {@code WebSession} instance does not automatically start
+ * a session thus causing the session id to be sent to the client (typically via
+ * a cookie). A session starts implicitly when session attributes are added.
+ * A session may also be created explicitly via {@link #start()}.
+ *
+ * @author Rossen Stoyanchev
+ */
+public interface WebSession {
+
+ /**
+ * Return a unique session identifier.
+ */
+ String getId();
+
+ /**
+ * Return a map that holds session attributes.
+ */
+ Map getAttributes();
+
+ /**
+ * Force the creation of a session causing the session id to be sent when
+ * {@link #save()} is called.
+ */
+ void start();
+
+ /**
+ * Whether a session with the client has been started explicitly via
+ * {@link #start()} or implicitly by adding session attributes.
+ * If "false" then the session id is not sent to the client and the
+ * {@link #save()} method is essentially a no-op.
+ */
+ boolean isStarted();
+
+ /**
+ * Save the session persisting attributes (e.g. if stored remotely) and also
+ * sending the session id to the client if the session is new.
+ * Note that a session must be started explicitly via {@link #start()} or
+ * implicitly by adding attributes or otherwise this method has no effect.
+ * @return {@code Mono} to indicate completion with success or error
+ *
Typically this method should be automatically invoked just before the
+ * response is committed so applications don't have to by default.
+ */
+ Mono save();
+
+ /**
+ * Return {@code true} if the session expired after {@link #getMaxIdleTime()
+ * maxIdleTime} elapsed.
+ * Typically expiration checks should be automatically made when a session
+ * is accessed, a new {@code WebSession} instance created if necessary, at
+ * the start of request processing so that applications don't have to worry
+ * about expired session by default.
+ */
+ boolean isExpired();
+
+ /**
+ * Return the time when the session was created.
+ */
+ Instant getCreationTime();
+
+ /**
+ * Return the last time of session access as a result of user activity such
+ * as an HTTP request. Together with {@link #getMaxIdleTime()
+ * maxIdleTimeInSeconds} this helps to determine when a session is
+ * {@link #isExpired() expired}.
+ */
+ Instant getLastAccessTime();
+
+ /**
+ * Configure the max amount of time that may elapse after the
+ * {@link #getLastAccessTime() lastAccessTime} before a session is considered
+ * expired. A negative value indicates the session should not expire.
+ */
+ void setMaxIdleTime(Duration maxIdleTime);
+
+ /**
+ * Return the maximum time after the {@link #getLastAccessTime()
+ * lastAccessTime} before a session expires. A negative time indicates the
+ * session doesn't expire.
+ */
+ Duration getMaxIdleTime();
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java
index 6b6b203327..c85ba349b4 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java
@@ -23,6 +23,9 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.util.Assert;
+import org.springframework.web.server.session.DefaultWebSessionManager;
+import org.springframework.web.server.session.WebSessionManager;
/**
* Adapt {@link WebHandler} to {@link HttpHandler} also creating the
@@ -35,11 +38,31 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http
private static Log logger = LogFactory.getLog(WebToHttpHandlerAdapter.class);
+ private WebSessionManager sessionManager = new DefaultWebSessionManager();
+
+
public WebToHttpHandlerAdapter(WebHandler delegate) {
super(delegate);
}
+ /**
+ *
+ * @param sessionManager
+ */
+ public void setSessionManager(WebSessionManager sessionManager) {
+ Assert.notNull(sessionManager, "'sessionManager' must not be null.");
+ this.sessionManager = sessionManager;
+ }
+
+ /**
+ * Return the configured {@link WebSessionManager}.
+ */
+ public WebSessionManager getSessionManager() {
+ return this.sessionManager;
+ }
+
+
@Override
public Mono handle(ServerHttpRequest request, ServerHttpResponse response) {
WebServerExchange exchange = createWebServerExchange(request, response);
@@ -55,7 +78,7 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http
}
protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) {
- return new DefaultWebServerExchange(request, response);
+ return new DefaultWebServerExchange(request, response, this.sessionManager);
}
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java
index 62a133a35e..55dc3960f5 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java
@@ -21,6 +21,7 @@ import java.util.List;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
+import org.springframework.web.server.session.WebSessionManager;
/**
* Assist with building an
@@ -43,6 +44,8 @@ public class WebToHttpHandlerBuilder {
private final List exceptionHandlers = new ArrayList<>();
+ private WebSessionManager sessionManager;
+
private WebToHttpHandlerBuilder(WebHandler targetHandler) {
Assert.notNull(targetHandler, "'targetHandler' must not be null");
@@ -68,17 +71,26 @@ public class WebToHttpHandlerBuilder {
return this;
}
+ public WebToHttpHandlerBuilder sessionManager(WebSessionManager sessionManager) {
+ this.sessionManager = sessionManager;
+ return this;
+ }
+
public WebToHttpHandlerAdapter build() {
- WebHandler webHandler = this.targetHandler;
+ WebHandler handler = this.targetHandler;
if (!this.exceptionHandlers.isEmpty()) {
WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()];
- webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array));
+ handler = new ExceptionHandlingWebHandler(handler, this.exceptionHandlers.toArray(array));
}
if (!this.filters.isEmpty()) {
WebFilter[] array = new WebFilter[this.filters.size()];
- webHandler = new FilteringWebHandler(webHandler, this.filters.toArray(array));
+ handler = new FilteringWebHandler(handler, this.filters.toArray(array));
}
- return new WebToHttpHandlerAdapter(webHandler);
+ WebToHttpHandlerAdapter adapter = new WebToHttpHandlerAdapter(handler);
+ if (this.sessionManager != null) {
+ adapter.setSessionManager(this.sessionManager);
+ }
+ return adapter;
}
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/ConfigurableWebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/ConfigurableWebSession.java
new file mode 100644
index 0000000000..6a91b3f219
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/ConfigurableWebSession.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.time.Instant;
+import java.util.function.Supplier;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.web.server.WebSession;
+
+/**
+ * Extend {@link WebSession} with management operations meant for internal use
+ * for example by implementations of {@link WebSessionManager}.
+ *
+ * @author Rossen Stoyanchev
+ */
+public interface ConfigurableWebSession extends WebSession {
+
+ /**
+ * Update the last access time for user-related session activity.
+ * @param time the time of access
+ */
+ void setLastAccessTime(Instant time);
+
+ /**
+ * Set the operation to invoke when {@link WebSession#save()} is invoked.
+ * @param saveOperation the save operation
+ */
+ void setSaveOperation(Supplier> saveOperation);
+
+}
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
new file mode 100644
index 0000000000..2584487c2b
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.http.HttpCookie;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.server.WebServerExchange;
+
+/**
+ * Cookie-based {@link WebSessionIdResolver}.
+ *
+ * @author Rossen Stoyanchev
+ */
+public class CookieWebSessionIdResolver implements WebSessionIdResolver {
+
+ private String cookieName = "SESSION";
+
+ private Duration cookieMaxAge = Duration.ofSeconds(-1);
+
+
+ /**
+ * Set the name of the cookie to use for the session id.
+ * By default set to "SESSION".
+ * @param cookieName the cookie name
+ */
+ public void setCookieName(String cookieName) {
+ Assert.hasText(cookieName, "'cookieName' must not be empty.");
+ this.cookieName = cookieName;
+ }
+
+ /**
+ * Return the configured cookie name.
+ */
+ public String getCookieName() {
+ return this.cookieName;
+ }
+
+ /**
+ * 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()}.
+ *
By default set to -1.
+ * @param maxAge the maxAge duration value
+ */
+ public void setCookieMaxAge(Duration maxAge) {
+ this.cookieMaxAge = maxAge;
+ }
+
+ /**
+ * Return the configured "Max-Age" attribute value for the session cookie.
+ */
+ public Duration getCookieMaxAge() {
+ return this.cookieMaxAge;
+ }
+
+
+ @Override
+ public Optional resolveSessionId(WebServerExchange exchange) {
+ HttpHeaders headers = exchange.getRequest().getHeaders();
+ List cookies = headers.getCookies().get(getCookieName());
+ return (CollectionUtils.isEmpty(cookies) ?
+ Optional.empty() : Optional.of(cookies.get(0).getValue()));
+ }
+
+ @Override
+ public void setSessionId(WebServerExchange 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));
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java
new file mode 100644
index 0000000000..0301563994
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.io.Serializable;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.util.Assert;
+
+/**
+ * @author Rossen Stoyanchev
+ */
+public class DefaultWebSession implements ConfigurableWebSession, Serializable {
+
+ private final String id;
+
+ private final Map attributes;
+
+ private final Clock clock;
+
+ private final Instant creationTime;
+
+ private volatile Instant lastAccessTime;
+
+ private volatile Duration maxIdleTime;
+
+ private AtomicReference state = new AtomicReference<>();
+
+ private volatile transient Supplier> saveOperation;
+
+
+ /**
+ * Constructor to create a new session.
+ * @param id the session id
+ * @param clock for access to current time
+ */
+ public DefaultWebSession(String id, Clock clock) {
+ Assert.notNull(id, "'id' is required.");
+ Assert.notNull(clock, "'clock' is required.");
+ this.id = id;
+ this.clock = clock;
+ this.attributes = new ConcurrentHashMap<>();
+ this.creationTime = Instant.now(clock);
+ this.lastAccessTime = this.creationTime;
+ this.maxIdleTime = Duration.ofMinutes(30);
+ this.state.set(State.NEW);
+ }
+
+ /**
+ * Constructor to load existing session.
+ * @param id the session id
+ * @param attributes the attributes of the session
+ * @param clock for access to current time
+ * @param creationTime the creation time
+ * @param lastAccessTime the last access time
+ * @param maxIdleTime the configured maximum session idle time
+ */
+ public DefaultWebSession(String id, Map attributes, Clock clock,
+ Instant creationTime, Instant lastAccessTime, Duration maxIdleTime) {
+
+ Assert.notNull(id, "'id' is required.");
+ Assert.notNull(clock, "'clock' is required.");
+ this.id = id;
+ this.attributes = new ConcurrentHashMap<>(attributes);
+ this.clock = clock;
+ this.creationTime = creationTime;
+ this.lastAccessTime = lastAccessTime;
+ this.maxIdleTime = maxIdleTime;
+ this.state.set(State.STARTED);
+ }
+
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public Map getAttributes() {
+ return this.attributes;
+ }
+
+ @Override
+ public Instant getCreationTime() {
+ return this.creationTime;
+ }
+
+ @Override
+ public void setLastAccessTime(Instant lastAccessTime) {
+ this.lastAccessTime = lastAccessTime;
+ }
+
+ @Override
+ public Instant getLastAccessTime() {
+ return this.lastAccessTime;
+ }
+
+ /**
+ * By default this is set to 30 minutes.
+ * @param maxIdleTime the max idle time
+ */
+ @Override
+ public void setMaxIdleTime(Duration maxIdleTime) {
+ this.maxIdleTime = maxIdleTime;
+ }
+
+ @Override
+ public Duration getMaxIdleTime() {
+ return this.maxIdleTime;
+ }
+
+ @Override
+ public void setSaveOperation(Supplier> saveOperation) {
+ Assert.notNull(saveOperation, "'saveOperation' is required.");
+ this.saveOperation = saveOperation;
+ }
+
+ protected Supplier> getSaveOperation() {
+ return this.saveOperation;
+ }
+
+
+ @Override
+ public void start() {
+ this.state.compareAndSet(State.NEW, State.STARTED);
+ }
+
+ @Override
+ public boolean isStarted() {
+ State value = this.state.get();
+ return (State.STARTED.equals(value) || (State.NEW.equals(value) && !getAttributes().isEmpty()));
+ }
+
+ @Override
+ public Mono save() {
+ return this.saveOperation.get();
+ }
+
+ @Override
+ public boolean isExpired() {
+ return (isStarted() && !this.maxIdleTime.isNegative() &&
+ Instant.now(this.clock).minus(this.maxIdleTime).isAfter(this.lastAccessTime));
+ }
+
+
+ private enum State { NEW, STARTED }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java
new file mode 100644
index 0000000000..f374449215
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.UUID;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.util.Assert;
+import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.WebSession;
+
+
+/**
+ * Default implementation of {@link WebSessionManager} with a cookie-based web
+ * session id resolution strategy and simple in-memory session persistence.
+ *
+ * @author Rossen Stoyanchev
+ */
+public class DefaultWebSessionManager implements WebSessionManager {
+
+ private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
+
+ private WebSessionStore sessionStore = new InMemoryWebSessionStore();
+
+ private Clock clock = Clock.systemDefaultZone();
+
+
+ /**
+ * Configure the session id resolution strategy to use.
+ * By default {@link CookieWebSessionIdResolver} is used.
+ * @param sessionIdResolver the resolver
+ */
+ public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) {
+ Assert.notNull(sessionIdResolver, "'sessionIdResolver' is required.");
+ this.sessionIdResolver = sessionIdResolver;
+ }
+
+ /**
+ * Return the configured {@link WebSessionIdResolver}.
+ */
+ public WebSessionIdResolver getSessionIdResolver() {
+ return this.sessionIdResolver;
+ }
+
+ /**
+ * Configure the session persistence strategy to use.
+ *
By default {@link InMemoryWebSessionStore} is used.
+ * @param sessionStore the persistence strategy
+ */
+ public void setSessionStore(WebSessionStore sessionStore) {
+ Assert.notNull(sessionStore, "'sessionStore' is required.");
+ this.sessionStore = sessionStore;
+ }
+
+ /**
+ * Return the configured {@link WebSessionStore}.
+ */
+ public WebSessionStore getSessionStore() {
+ return this.sessionStore;
+ }
+
+ /**
+ * Configure the {@link Clock} for access to current time. During tests you
+ * may use {code Clock.offset(clock, Duration.ofMinutes(-31))} to set the
+ * clock back for example to test changes after sessions expire.
+ *
By default {@link Clock#systemDefaultZone()} is used.
+ * @param clock the clock to use
+ */
+ public void setClock(Clock clock) {
+ Assert.notNull(clock, "'clock' is required.");
+ this.clock = clock;
+ }
+
+ /**
+ * Return the configured clock for access to current time.
+ */
+ public Clock getClock() {
+ return this.clock;
+ }
+
+
+ @Override
+ public Mono getSession(WebServerExchange exchange) {
+ return Mono.fromCallable(() -> getSessionIdResolver().resolveSessionId(exchange))
+ .where(Optional::isPresent)
+ .map(Optional::get)
+ .then(this.sessionStore::retrieveSession)
+ .then(session -> validateSession(exchange, session))
+ .otherwiseIfEmpty(createSession(exchange))
+ .map(session -> extendSession(exchange, session));
+ }
+
+ protected Mono validateSession(WebServerExchange exchange, WebSession session) {
+ if (session.isExpired()) {
+ this.sessionIdResolver.setSessionId(exchange, "");
+ return this.sessionStore.removeSession(session.getId()).after(Mono::empty);
+ }
+ else {
+ return Mono.just(session);
+ }
+ }
+
+ protected Mono createSession(WebServerExchange exchange) {
+ String sessionId = UUID.randomUUID().toString();
+ WebSession session = new DefaultWebSession(sessionId, getClock());
+ return Mono.just(session);
+ }
+
+ protected WebSession extendSession(WebServerExchange exchange, WebSession session) {
+ if (session instanceof ConfigurableWebSession) {
+ ConfigurableWebSession managed = (ConfigurableWebSession) session;
+ managed.setSaveOperation(() -> saveSession(exchange, session));
+ managed.setLastAccessTime(Instant.now(getClock()));
+ }
+ exchange.getResponse().beforeCommit(session::save);
+ return session;
+ }
+
+ protected Mono saveSession(WebServerExchange exchange, WebSession session) {
+
+ Assert.isTrue(!session.isExpired(), "Sessions are checked for expiration and have their " +
+ "access time updated when first accessed during request processing. " +
+ "However this session is expired meaning that maxIdleTime elapsed " +
+ "since then and before the call to session.save().");
+
+ if (!session.isStarted()) {
+ return Mono.empty();
+ }
+
+ // Force explicit start
+ session.start();
+
+ Optional requestedId = getSessionIdResolver().resolveSessionId(exchange);
+ if (!requestedId.isPresent() || !session.getId().equals(requestedId.get())) {
+ this.sessionIdResolver.setSessionId(exchange, session.getId());
+ }
+ return this.sessionStore.storeSession(session);
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java
new file mode 100644
index 0000000000..7e1bb388a3
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.web.server.WebSession;
+
+/**
+ * Simple Map-based storage for {@link WebSession} instances.
+ *
+ * @author Rossen Stoyanchev
+ */
+public class InMemoryWebSessionStore implements WebSessionStore {
+
+ private final Map sessions = new ConcurrentHashMap<>();
+
+
+ @Override
+ public Mono storeSession(WebSession session) {
+ this.sessions.put(session.getId(), session);
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono retrieveSession(String id) {
+ return (this.sessions.containsKey(id) ? Mono.just(this.sessions.get(id)) : Mono.empty());
+ }
+
+ @Override
+ public Mono removeSession(String id) {
+ this.sessions.remove(id);
+ return Mono.empty();
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java
new file mode 100644
index 0000000000..61c28260f2
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.util.Optional;
+
+import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.WebSession;
+
+
+/**
+ * Contract for session id resolution strategies. Allows for session id
+ * resolution through the request and for sending the session id to the
+ * client through the response.
+ *
+ * @author Rossen Stoyanchev
+ * @see CookieWebSessionIdResolver
+ */
+public interface WebSessionIdResolver {
+
+ /**
+ * Resolve the session id associated with the request.
+ * @param exchange the current exchange
+ * @return the session id if present
+ */
+ Optional resolveSessionId(WebServerExchange exchange);
+
+ /**
+ * Send the given session id to the client or if the session id is "null"
+ * instruct the client to end the current session.
+ * @param exchange the current exchange
+ * @param sessionId the session id
+ */
+ void setSessionId(WebServerExchange exchange, String sessionId);
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java
new file mode 100644
index 0000000000..63909b072e
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.WebSession;
+
+/**
+ * Main contract abstracting support for access to {@link WebSession} instances
+ * associated with HTTP requests as well as the subsequent management such as
+ * persistence and others.
+ *
+ * The {@link DefaultWebSessionManager} implementation in turn delegates to
+ * {@link WebSessionIdResolver} and {@link WebSessionStore} which abstract
+ * underlying concerns related to the management of web sessions.
+ *
+ * @author Rossen Stoyanchev
+ * @see WebSessionIdResolver
+ * @see WebSessionStore
+ */
+public interface WebSessionManager {
+
+ /**
+ * Return the {@link WebSession} for the given exchange. Always guaranteed
+ * to return an instance either matching to the session id requested by the
+ * client, or with a new session id either because the client did not
+ * specify one or because the underlying session had expired.
+ * @param exchange the current exchange
+ * @return {@code Mono} for async access to the session
+ */
+ Mono getSession(WebServerExchange exchange);
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionStore.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionStore.java
new file mode 100644
index 0000000000..998e298e69
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionStore.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.web.server.WebSession;
+
+/**
+ * Strategy for {@link WebSession} persistence.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ */
+public interface WebSessionStore {
+
+ /**
+ * Store the given session.
+ * @param session the session to store
+ * @return {@code Mono} for completion notification
+ */
+ Mono storeSession(WebSession session);
+
+ /**
+ * Load the session for the given session id.
+ * @param sessionId the session to load
+ * @return {@code Mono} for async access to the loaded session
+ */
+ Mono retrieveSession(String sessionId);
+
+ /**
+ * Remove the session with the given id.
+ * @param sessionId the session to remove
+ * @return {@code Mono} for completion notification
+ */
+ Mono removeSession(String sessionId);
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java
new file mode 100644
index 0000000000..57b15561b0
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Support for a user session.
+ */
+package org.springframework.web.server.session;
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java
index 033148a846..c609536cde 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java
@@ -57,9 +57,11 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.session.WebSessionManager;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
/**
* Test the effect of exceptions at different stages of request processing by
@@ -91,9 +93,11 @@ public class DispatcherHandlerErrorTests {
this.dispatcherHandler = new DispatcherHandler();
this.dispatcherHandler.setApplicationContext(appContext);
+ WebSessionManager sessionManager = mock(WebSessionManager.class);
+
this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/"));
this.response = new MockServerHttpResponse();
- this.exchange = new DefaultWebServerExchange(this.request, this.response);
+ this.exchange = new DefaultWebServerExchange(this.request, this.response, sessionManager);
}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java
index 8a992da11b..6c48305a88 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java
@@ -31,10 +31,12 @@ import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.web.ResponseStatusException;
import org.springframework.web.server.DefaultWebServerExchange;
import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.session.WebSessionManager;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
/**
* @author Rossen Stoyanchev
@@ -52,8 +54,9 @@ public class ResponseStatusExceptionHandlerTests {
public void setUp() throws Exception {
this.handler = new ResponseStatusExceptionHandler();
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
+ WebSessionManager sessionManager = mock(WebSessionManager.class);
this.response = new MockServerHttpResponse();
- this.exchange = new DefaultWebServerExchange(request, this.response);
+ this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager);
}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java
index c0385cde11..903de4bb19 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java
@@ -37,6 +37,7 @@ import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver;
import org.springframework.web.server.DefaultWebServerExchange;
import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.session.WebSessionManager;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
@@ -56,8 +57,9 @@ public class InvocableHandlerMethodTests {
@Before
public void setUp() throws Exception {
+ WebSessionManager sessionManager = mock(WebSessionManager.class);
this.request = mock(ServerHttpRequest.class);
- this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class));
+ this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class), sessionManager);
}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java
index 8ce0f57628..c14153c954 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java
@@ -36,10 +36,12 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.DefaultWebServerExchange;
import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.session.WebSessionManager;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
/**
* @author Sebastien Deleuze
@@ -62,7 +64,9 @@ public class RequestMappingHandlerMappingTests {
@Test
public void path() throws Exception {
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo"));
- WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse());
+ MockServerHttpResponse response = new MockServerHttpResponse();
+ WebSessionManager sessionManager = mock(WebSessionManager.class);
+ WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager);
Publisher> handlerPublisher = this.mapping.getHandler(exchange);
HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher);
assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod());
@@ -71,13 +75,15 @@ public class RequestMappingHandlerMappingTests {
@Test
public void method() throws Exception {
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo"));
- WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse());
+ MockServerHttpResponse response = new MockServerHttpResponse();
+ WebSessionManager sessionManager = mock(WebSessionManager.class);
+ WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager);
Publisher> handlerPublisher = this.mapping.getHandler(exchange);
HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher);
assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod());
request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo"));
- exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse());
+ exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse(), sessionManager);
handlerPublisher = this.mapping.getHandler(exchange);
handlerMethod = toHandlerMethod(handlerPublisher);
assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod());
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java
index 0360c17ace..e640ceb486 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java
@@ -26,8 +26,10 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
+import org.springframework.web.server.session.WebSessionManager;
import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
/**
* @author Rossen Stoyanchev
@@ -45,9 +47,10 @@ public class ExceptionHandlingHttpHandlerTests {
@Before
public void setUp() throws Exception {
URI uri = new URI("http://localhost:8080");
+ WebSessionManager sessionManager = mock(WebSessionManager.class);
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, uri);
this.response = new MockServerHttpResponse();
- this.exchange = new DefaultWebServerExchange(request, this.response);
+ this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager);
this.targetHandler = new StubWebHandler(new IllegalStateException("boo"));
}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java
new file mode 100644
index 0000000000..37c676fc6e
--- /dev/null
+++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.net.URI;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.http.server.reactive.MockServerHttpRequest;
+import org.springframework.http.server.reactive.MockServerHttpResponse;
+import org.springframework.web.server.DefaultWebServerExchange;
+import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.WebSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Rossen Stoyanchev
+ */
+public class DefaultWebSessionManagerTests {
+
+ private DefaultWebSessionManager manager;
+
+ private TestWebSessionIdResolver idResolver;
+
+ private DefaultWebServerExchange exchange;
+
+
+ @Before
+ public void setUp() throws Exception {
+ this.idResolver = new TestWebSessionIdResolver();
+ this.manager = new DefaultWebSessionManager();
+ this.manager.setSessionIdResolver(this.idResolver);
+
+ MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
+ MockServerHttpResponse response = new MockServerHttpResponse();
+ this.exchange = new DefaultWebServerExchange(request, response, this.manager);
+ }
+
+
+ @Test
+ public void getSessionPassive() throws Exception {
+ this.idResolver.setIdToResolve(Optional.empty());
+ WebSession session = this.manager.getSession(this.exchange).get();
+
+ assertNotNull(session);
+ assertFalse(session.isStarted());
+ assertFalse(session.isExpired());
+
+ session.save();
+
+ assertFalse(this.idResolver.getId().isPresent());
+ assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).get());
+ }
+
+ @Test
+ public void getSessionForceCreate() throws Exception {
+ this.idResolver.setIdToResolve(Optional.empty());
+ WebSession session = this.manager.getSession(this.exchange).get();
+ session.start();
+ session.save();
+
+ String id = session.getId();
+ assertTrue(this.idResolver.getId().isPresent());
+ assertEquals(id, this.idResolver.getId().get());
+ assertSame(session, this.manager.getSessionStore().retrieveSession(id).get());
+ }
+
+ @Test
+ public void getSessionAddAttribute() throws Exception {
+ this.idResolver.setIdToResolve(Optional.empty());
+ WebSession session = this.manager.getSession(this.exchange).get();
+ session.getAttributes().put("foo", "bar");
+ session.save();
+
+ assertTrue(this.idResolver.getId().isPresent());
+ }
+
+ @Test
+ public void getSessionExisting() throws Exception {
+ DefaultWebSession existing = new DefaultWebSession("1", Clock.systemDefaultZone());
+ this.manager.getSessionStore().storeSession(existing);
+
+ this.idResolver.setIdToResolve(Optional.of("1"));
+ WebSession actual = this.manager.getSession(this.exchange).get();
+ assertSame(existing, actual);
+ }
+
+ @Test
+ public void getSessionExistingExpired() throws Exception {
+ Clock clock = Clock.systemDefaultZone();
+ DefaultWebSession existing = new DefaultWebSession("1", clock);
+ existing.start();
+ existing.setLastAccessTime(Instant.now(clock).minus(Duration.ofMinutes(31)));
+ this.manager.getSessionStore().storeSession(existing);
+
+ this.idResolver.setIdToResolve(Optional.of("1"));
+ WebSession actual = this.manager.getSession(this.exchange).get();
+ assertNotSame(existing, actual);
+ }
+
+
+ private static class TestWebSessionIdResolver implements WebSessionIdResolver {
+
+ private Optional idToResolve = Optional.empty();
+
+ private Optional id = Optional.empty();
+
+
+ public void setIdToResolve(Optional idToResolve) {
+ this.idToResolve = idToResolve;
+ }
+
+ public Optional getId() {
+ return this.id;
+ }
+
+ @Override
+ public Optional resolveSessionId(WebServerExchange exchange) {
+ return this.idToResolve;
+ }
+
+ @Override
+ public void setSessionId(WebServerExchange exchange, Optional sessionId) {
+ this.id = sessionId;
+ }
+ }
+
+}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java
new file mode 100644
index 0000000000..c4e80f0d14
--- /dev/null
+++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2002-2016 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.web.server.session;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Test;
+import reactor.core.publisher.Mono;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
+import org.springframework.http.server.reactive.HttpHandler;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.server.WebHandler;
+import org.springframework.web.server.WebServerExchange;
+import org.springframework.web.server.WebToHttpHandlerBuilder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+
+/**
+ * @author Rossen Stoyanchev
+ */
+public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTests {
+
+ private RestTemplate restTemplate;
+
+ private DefaultWebSessionManager sessionManager;
+
+ private TestWebHandler handler;
+
+
+ @Override
+ public void setup() throws Exception {
+ super.setup();
+ this.restTemplate = new RestTemplate();
+ }
+
+ protected URI createUri(String pathAndQuery) throws URISyntaxException {
+ boolean prefix = !StringUtils.hasText(pathAndQuery) || !pathAndQuery.startsWith("/");
+ pathAndQuery = (prefix ? "/" + pathAndQuery : pathAndQuery);
+ return new URI("http://localhost:" + port + pathAndQuery);
+ }
+
+ @Override
+ protected HttpHandler createHttpHandler() {
+ this.sessionManager = new DefaultWebSessionManager();
+ this.handler = new TestWebHandler();
+ return WebToHttpHandlerBuilder.webHandler(this.handler).sessionManager(this.sessionManager).build();
+ }
+
+ @Test
+ public void createSession() throws Exception {
+ RequestEntity request = RequestEntity.get(createUri("/")).build();
+ ResponseEntity response = this.restTemplate.exchange(request, Void.class);
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ String id = extractSessionId(response.getHeaders());
+ assertNotNull(id);
+ assertEquals(1, this.handler.getCount());
+
+ request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build();
+ response = this.restTemplate.exchange(request, Void.class);
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertNull(response.getHeaders().get("Set-Cookie"));
+ assertEquals(2, this.handler.getCount());
+ }
+
+ @Test
+ public void expiredSession() throws Exception {
+ RequestEntity request = RequestEntity.get(createUri("/")).build();
+ ResponseEntity response = this.restTemplate.exchange(request, Void.class);
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ String id = extractSessionId(response.getHeaders());
+ assertNotNull(id);
+ assertEquals(1, this.handler.getCount());
+
+ // Set clock back 31 minutes
+ Clock clock = this.sessionManager.getClock();
+ this.sessionManager.setClock(Clock.offset(clock, Duration.ofMinutes(-31)));
+
+ // Access again to update lastAccessTime
+ request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build();
+ response = this.restTemplate.exchange(request, Void.class);
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertNull(response.getHeaders().get("Set-Cookie"));
+ assertEquals(2, this.handler.getCount());
+
+ // Now it should be expired
+ request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build();
+ response = this.restTemplate.exchange(request, Void.class);
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ id = extractSessionId(response.getHeaders());
+ assertNotNull("Expected new session id", id);
+ assertEquals("Expected new session attribute", 1, this.handler.getCount());
+ }
+
+
+ // No client side HttpCookie support yet
+
+ private String extractSessionId(HttpHeaders headers) {
+ List headerValues = headers.get("Set-Cookie");
+ assertNotNull(headerValues);
+ assertEquals(1, headerValues.size());
+
+ List data = new ArrayList<>();
+ for (String s : headerValues.get(0).split(";")){
+ if (s.startsWith("SESSION=")) {
+ return s.substring("SESSION=".length());
+ }
+ }
+ return null;
+ }
+
+ private static class TestWebHandler implements WebHandler {
+
+ private AtomicInteger currentValue = new AtomicInteger();
+
+
+ public int getCount() {
+ return this.currentValue.get();
+ }
+
+ @Override
+ public Mono handle(WebServerExchange exchange) {
+ return exchange.getSession().map(session -> {
+ Map map = session.getAttributes();
+ int value = (map.get("counter") != null ? (int) map.get("counter") : 0);
+ value++;
+ map.put("counter", value);
+ this.currentValue.set(value);
+ return session;
+ }).after();
+ }
+ }
+
+}