From 08d1f6daf5b29af880de214e31543debe49a33f1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 16 Jul 2015 17:11:18 -0700 Subject: [PATCH] Allow persistent servlet sessions across restarts Update Tomcat, Jetty and Undertow to serialize session data when the application is stopped and load it again when the application restarts. Persistent session are opt-in; either by setting `persistentSession` on the ConfigurableEmbeddedServletContainer or by using the property `server.session.persistent=true`. Fixes gh-2490 --- .../autoconfigure/web/ServerProperties.java | 14 ++ .../appendix-application-properties.adoc | 1 + .../main/asciidoc/spring-boot-features.adoc | 2 +- ...tConfigurableEmbeddedServletContainer.java | 11 ++ .../ConfigurableEmbeddedServletContainer.java | 6 + .../JettyEmbeddedServletContainerFactory.java | 27 ++- ...TomcatEmbeddedServletContainerFactory.java | 36 +++- .../undertow/FileSessionPersistence.java | 156 ++++++++++++++++++ .../UndertowEmbeddedServletContainer.java | 10 +- ...dertowEmbeddedServletContainerFactory.java | 5 + ...tEmbeddedServletContainerFactoryTests.java | 60 ++++++- .../undertow/FileSessionPersistenceTests.java | 109 ++++++++++++ 12 files changed, 424 insertions(+), 13 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistence.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistenceTests.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 38935c76fe..5adf201a40 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -262,6 +262,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord if (getSession().getTimeout() != null) { container.setSessionTimeout(getSession().getTimeout()); } + container.setPersistSession(getSession().isPersistent()); if (getSsl() != null) { container.setSsl(getSsl()); } @@ -322,6 +323,11 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord */ private Set trackingModes; + /** + * Persist session data between restarts. + */ + private boolean persistent; + private Cookie cookie = new Cookie(); public Cookie getCookie() { @@ -344,6 +350,14 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord this.trackingModes = trackingModes; } + public boolean isPersistent() { + return this.persistent; + } + + public void setPersistent(boolean persistent) { + this.persistent = persistent; + } + public static class Cookie { /** diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 6fb2183e27..2fdd5624ab 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -78,6 +78,7 @@ content into your application; rather pick only the properties that you need. server.jsp-servlet.registered=true # Whether or not the JSP servlet is registered server.servlet-path= # the servlet path, defaults to '/' server.display-name= # the display name of the application + server.session.persistent= # true if session should be saved across restarts server.session.timeout= # session timeout in seconds server.session.tracking-modes= # tracking modes (one or more of "cookie" ,"url", "ssl") server.session.cookie.name= # session cookie name diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index d4a3cb3fbb..88ed094311 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1581,7 +1581,7 @@ Common server settings include: * `server.port` -- The listen port for incoming HTTP requests. * `server.address` -- The interface address to bind to. -* `server.sessionTimeout` -- A session timeout. +* `server.session.timeout` -- A session timeout. See the {sc-spring-boot-autoconfigure}/web/ServerProperties.{sc-ext}[`ServerProperties`] class for a complete list. diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java index 642a6ad075..1b8f351e50 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java @@ -64,6 +64,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer implements private int sessionTimeout = DEFAULT_SESSION_TIMEOUT; + private boolean persistSession; + private Ssl ssl; private JspServlet jspServlet = new JspServlet(); @@ -178,6 +180,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer implements return this.sessionTimeout; } + @Override + public void setPersistSession(boolean persistSession) { + this.persistSession = persistSession; + } + + public boolean isPersistSession() { + return this.persistSession; + } + @Override public void setInitializers(List initializers) { Assert.notNull(initializers, "Initializers must not be null"); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java index fc2732edf8..d3c0bec158 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java @@ -73,6 +73,12 @@ public interface ConfigurableEmbeddedServletContainer { */ void setSessionTimeout(int sessionTimeout, TimeUnit timeUnit); + /** + * Sets if session data should be persisted between restarts. + * @param persistSession {@code true} if session data should be persisted + */ + void setPersistSession(boolean persistSession); + /** * Sets the specific network address that the server should bind to. * @param address the address to set (defaults to {@code null}) diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index c812ab52fd..64e576a9f8 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.server.SessionManager; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.server.session.HashSessionManager; import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; @@ -50,6 +51,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.webapp.AbstractConfiguration; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; +import org.springframework.boot.ApplicationTemp; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.EmbeddedServletContainer; @@ -277,12 +279,31 @@ public class JettyEmbeddedServletContainerFactory extends Configuration[] configurations = getWebAppContextConfigurations(context, initializersToUse); context.setConfigurations(configurations); - int sessionTimeout = (getSessionTimeout() > 0 ? getSessionTimeout() : -1); - SessionManager sessionManager = context.getSessionHandler().getSessionManager(); - sessionManager.setMaxInactiveInterval(sessionTimeout); + configureSession(context); postProcessWebAppContext(context); } + private void configureSession(WebAppContext context) { + SessionManager sessionManager = context.getSessionHandler().getSessionManager(); + int sessionTimeout = (getSessionTimeout() > 0 ? getSessionTimeout() : -1); + sessionManager.setMaxInactiveInterval(sessionTimeout); + if (isPersistSession()) { + Assert.isInstanceOf(HashSessionManager.class, sessionManager, + "Unable to use persistent sessions"); + configurePersistSession(sessionManager); + } + } + + private void configurePersistSession(SessionManager sessionManager) { + try { + File storeDirectory = new ApplicationTemp().getFolder("jetty-sessions"); + ((HashSessionManager) sessionManager).setStoreDirectory(storeDirectory); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + private File getTempDirectory() { String temp = System.getProperty("java.io.tmpdir"); return (temp == null ? null : new File(temp)); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index d76cc6ac02..641ad0ac95 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -39,10 +39,12 @@ import org.apache.catalina.Host; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Manager; import org.apache.catalina.Valve; import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; import org.apache.catalina.loader.WebappLoader; +import org.apache.catalina.session.StandardManager; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat.FixContextListener; import org.apache.coyote.AbstractProtocol; @@ -50,6 +52,7 @@ import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.beans.BeanUtils; +import org.springframework.boot.ApplicationTemp; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.EmbeddedServletContainer; @@ -379,17 +382,38 @@ public class TomcatEmbeddedServletContainerFactory extends for (MimeMappings.Mapping mapping : getMimeMappings()) { context.addMimeMapping(mapping.getExtension(), mapping.getMimeType()); } - long sessionTimeout = getSessionTimeout(); - if (sessionTimeout > 0) { - // Tomcat timeouts are in minutes - sessionTimeout = Math.max(TimeUnit.SECONDS.toMinutes(sessionTimeout), 1L); - } - context.setSessionTimeout((int) sessionTimeout); + configureSession(context); for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) { customizer.customize(context); } } + private void configureSession(Context context) { + long sessionTimeout = getSessionTimeoutInMinutes(); + context.setSessionTimeout((int) sessionTimeout); + if (isPersistSession()) { + Manager manager = context.getManager(); + if (manager == null) { + manager = new StandardManager(); + context.setManager(manager); + } + Assert.state(manager instanceof StandardManager, + "Unable to persist HTTP session state using manager type " + + manager.getClass().getName()); + File folder = new ApplicationTemp().getFolder("tomcat-sessions"); + File file = new File(folder, "SESSIONS.ser"); + ((StandardManager) manager).setPathname(file.getAbsolutePath()); + } + } + + private long getSessionTimeoutInMinutes() { + long sessionTimeout = getSessionTimeout(); + if (sessionTimeout > 0) { + sessionTimeout = Math.max(TimeUnit.SECONDS.toMinutes(sessionTimeout), 1L); + } + return sessionTimeout; + } + /** * Post process the Tomcat {@link Context} before it used with the Tomcat Server. * Subclasses can override this method to apply additional processing to the diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistence.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistence.java new file mode 100644 index 0000000000..4de8a7b19d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistence.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-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.boot.context.embedded.undertow; + +import io.undertow.servlet.UndertowServletLogger; +import io.undertow.servlet.api.SessionPersistenceManager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * {@link SessionPersistenceManager} that stores session information in a file. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public class FileSessionPersistence implements SessionPersistenceManager { + + private final File folder; + + public FileSessionPersistence(File folder) { + this.folder = folder; + } + + @Override + public void persistSessions(String deploymentName, + Map sessionData) { + try { + save(sessionData, getSessionFile(deploymentName)); + } + catch (Exception ex) { + UndertowServletLogger.ROOT_LOGGER.failedToPersistSessions(ex); + } + } + + private void save(Map sessionData, File file) + throws IOException { + ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(file)); + try { + save(sessionData, stream); + } + finally { + stream.close(); + } + } + + private void save(Map sessionData, + ObjectOutputStream stream) throws IOException { + Map session = new LinkedHashMap(); + for (Map.Entry entry : sessionData.entrySet()) { + session.put(entry.getKey(), + new SerializablePersistentSession(entry.getValue())); + } + stream.writeObject(session); + } + + @Override + public Map loadSessionAttributes(String deploymentName, + final ClassLoader classLoader) { + try { + File file = getSessionFile(deploymentName); + if (file.exists()) { + return load(file); + } + } + catch (Exception ex) { + UndertowServletLogger.ROOT_LOGGER.failedtoLoadPersistentSessions(ex); + } + return null; + } + + private Map load(File file) throws IOException, + ClassNotFoundException { + ObjectInputStream stream = new ObjectInputStream(new FileInputStream(file)); + try { + return load(stream); + } + finally { + stream.close(); + } + } + + private Map load(ObjectInputStream stream) + throws ClassNotFoundException, IOException { + Map session = readSession(stream); + long time = System.currentTimeMillis(); + Map result = new LinkedHashMap(); + for (Map.Entry entry : session.entrySet()) { + PersistentSession entrySession = entry.getValue().getPersistentSession(); + if (entrySession.getExpiration().getTime() > time) { + result.put(entry.getKey(), entrySession); + } + } + return result; + } + + @SuppressWarnings("unchecked") + private Map readSession( + ObjectInputStream stream) throws ClassNotFoundException, IOException { + return ((Map) stream.readObject()); + } + + private File getSessionFile(String deploymentName) { + if (!this.folder.exists()) { + this.folder.mkdirs(); + } + return new File(this.folder, deploymentName + ".session"); + } + + @Override + public void clear(String deploymentName) { + getSessionFile(deploymentName).delete(); + } + + static class SerializablePersistentSession implements Serializable { + + private static final long serialVersionUID = 0L; + + private final Date expiration; + + private final Map sessionData; + + public SerializablePersistentSession(PersistentSession session) { + this.expiration = session.getExpiration(); + this.sessionData = new LinkedHashMap(session.getSessionData()); + } + + public PersistentSession getPersistentSession() { + return new PersistentSession(this.expiration, this.sessionData); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java index 128c6ac9e8..e9fe09f535 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java @@ -213,8 +213,14 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine @Override public synchronized void stop() throws EmbeddedServletContainerException { if (this.started) { - this.started = false; - this.undertow.stop(); + try { + this.started = false; + this.manager.stop(); + this.undertow.stop(); + } + catch (Exception ex) { + throw new EmbeddedServletContainerException("Unable to stop undertow", ex); + } } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java index 6fef6b9ba8..47c5806029 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java @@ -62,6 +62,7 @@ import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; +import org.springframework.boot.ApplicationTemp; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; @@ -355,6 +356,10 @@ public class UndertowEmbeddedServletContainerFactory extends if (isAccessLogEnabled()) { configureAccessLog(deployment); } + if (isPersistSession()) { + File folder = new ApplicationTemp().getFolder("undertow-sessions"); + deployment.setSessionPersistenceManager(new FileSessionPersistence(folder)); + } DeploymentManager manager = Servlets.defaultContainer().addDeployment(deployment); manager.deploy(); SessionManager sessionManager = manager.getDeployment().getSessionManager(); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index 95444b6b53..fc37491cd0 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -21,6 +21,7 @@ import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; @@ -38,13 +39,17 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.InputStreamFactory; +import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; +import org.apache.http.protocol.HttpContext; import org.apache.http.ssl.SSLContextBuilder; import org.junit.After; import org.junit.Rule; @@ -96,6 +101,8 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { protected EmbeddedServletContainer container; + private final HttpClientContext httpClientContext = HttpClientContext.create(); + @After public void teardown() { if (this.container != null) { @@ -529,6 +536,28 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { assertThat(getFactory().getSessionTimeout(), equalTo(30 * 60)); } + @Test + public void persistSession() throws Exception { + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setPersistSession(true); + this.container = factory + .getEmbeddedServletContainer(sessionServletRegistration()); + this.container.start(); + String s1 = getResponse(getLocalUrl("/session")); + String s2 = getResponse(getLocalUrl("/session")); + this.container.stop(); + this.container = factory + .getEmbeddedServletContainer(sessionServletRegistration()); + this.container.start(); + String s3 = getResponse(getLocalUrl("/session")); + System.out.println(s1); + System.out.println(s2); + System.out.println(s3); + String message = "Session error s1=" + s1 + " s2=" + s2 + " s3=" + s3; + assertThat(message, s2.split(":")[0], equalTo(s1.split(":")[1])); + assertThat(message, s3.split(":")[0], equalTo(s2.split(":")[1])); + } + @Test public void compression() throws Exception { assertTrue(doTestCompression(10000, null, null)); @@ -632,7 +661,14 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { protected ClientHttpResponse getClientResponse(String url) throws IOException, URISyntaxException { - return getClientResponse(url, new HttpComponentsClientHttpRequestFactory()); + return getClientResponse(url, new HttpComponentsClientHttpRequestFactory() { + + @Override + protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { + return AbstractEmbeddedServletContainerFactoryTests.this.httpClientContext; + } + + }); } protected ClientHttpResponse getClientResponse(String url, @@ -640,6 +676,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { URISyntaxException { ClientHttpRequest request = requestFactory.createRequest(new URI(url), HttpMethod.GET); + request.getHeaders().add("Cookie", "JSESSIONID=" + "123"); ClientHttpResponse response = request.execute(); return response; } @@ -655,16 +692,37 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { @SuppressWarnings("serial") private ServletContextInitializer errorServletRegistration() { ServletRegistrationBean bean = new ServletRegistrationBean(new ExampleServlet() { + @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { throw new RuntimeException("Planned"); } + }, "/bang"); bean.setName("error"); return bean; } + private ServletContextInitializer sessionServletRegistration() { + ServletRegistrationBean bean = new ServletRegistrationBean(new ExampleServlet() { + + @Override + public void service(ServletRequest request, ServletResponse response) + throws ServletException, IOException { + HttpSession session = ((HttpServletRequest) request).getSession(true); + long value = System.currentTimeMillis(); + Object existing = session.getAttribute("boot"); + session.setAttribute("boot", value); + PrintWriter writer = response.getWriter(); + writer.append(String.valueOf(existing) + ":" + value); + } + + }, "/session"); + bean.setName("session"); + return bean; + } + private class TestGzipInputStreamFactory implements InputStreamFactory { private final AtomicBoolean requested = new AtomicBoolean(false); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistenceTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistenceTests.java new file mode 100644 index 0000000000..c8a64a488d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/FileSessionPersistenceTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-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.boot.context.embedded.undertow; + +import io.undertow.servlet.api.SessionPersistenceManager.PersistentSession; + +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link FileSessionPersistence}. + * + * @author Phillip Webb + */ +public class FileSessionPersistenceTests { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private File folder; + + private FileSessionPersistence persistence; + + private ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + private Date expiration = new Date(System.currentTimeMillis() + 10000); + + @Before + public void setup() throws IOException { + this.folder = this.temp.newFolder(); + this.persistence = new FileSessionPersistence(this.folder); + } + + @Test + public void loadsNullForMissingFile() throws Exception { + Map attributes = this.persistence + .loadSessionAttributes("test", this.classLoader); + assertThat(attributes, nullValue()); + } + + @Test + public void presistAndLoad() throws Exception { + Map sessionData = new LinkedHashMap(); + Map data = new LinkedHashMap(); + data.put("spring", "boot"); + PersistentSession session = new PersistentSession(this.expiration, data); + sessionData.put("abc", session); + this.persistence.persistSessions("test", sessionData); + Map restored = this.persistence.loadSessionAttributes( + "test", this.classLoader); + assertThat(restored, notNullValue()); + assertThat(restored.get("abc").getExpiration(), equalTo(this.expiration)); + assertThat(restored.get("abc").getSessionData().get("spring"), + equalTo((Object) "boot")); + } + + @Test + public void dontRestoreExpired() throws Exception { + Date expired = new Date(System.currentTimeMillis() - 1000); + Map sessionData = new LinkedHashMap(); + Map data = new LinkedHashMap(); + data.put("spring", "boot"); + PersistentSession session = new PersistentSession(expired, data); + sessionData.put("abc", session); + this.persistence.persistSessions("test", sessionData); + Map restored = this.persistence.loadSessionAttributes( + "test", this.classLoader); + assertThat(restored, notNullValue()); + assertThat(restored.containsKey("abc"), equalTo(false)); + } + + @Test + public void deleteFileOnClear() throws Exception { + File sessionFile = new File(this.folder, "test.session"); + Map sessionData = new LinkedHashMap(); + this.persistence.persistSessions("test", sessionData); + assertThat(sessionFile.exists(), equalTo(true)); + this.persistence.clear("test"); + assertThat(sessionFile.exists(), equalTo(false)); + } + +}