/* * Copyright 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 ratpack.session; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; import com.google.inject.*; import com.google.inject.name.Names; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.AsciiString; import ratpack.func.Action; import ratpack.guice.BindingsSpec; import ratpack.guice.ConfigurableModule; import ratpack.guice.RequestScoped; import ratpack.http.Request; import ratpack.http.Response; import ratpack.session.internal.*; import ratpack.util.Types; import javax.inject.Named; import java.io.Serializable; import java.util.function.Consumer; /** * Provides support for HTTP sessions. * <p> * This module provides the general session API (see {@link Session}), and a default {@link SessionStore} implementation that stores session data in local memory. * * <h3>The session store</h3> * <p> * It is expected that most applications will provide alternative bindings for the {@link SessionStore} type, overriding the default. * This allows arbitrary stores to be used to persist session data. * <p> * The default, in memory, implementation stores the data in a {@link Cache}{@code <}{@link AsciiString}, {@link ByteBuf}{@code >}. * This cache instance is provided by this module and defaults to storing a maximum of 1000 entries, discarding least recently used. * The {@link #memoryStore} methods are provided to conveniently construct alternative cache configurations, if necessary. * <h3>Serialization</h3> * <p> * Objects must be serialized to be stored in the session. * The get/set methods {@link SessionData} allow supplying a {@link SessionSerializer} to be used for the specific value. * For variants of the get/set methods where a serializer is not provided, the implementation of {@link SessionSerializer} bound with Guice will be used. * The default implementation provided by this module uses Java's in built serialization mechanism. * Users of this module may choose to override this binding with an alternative serialization strategy. * <p> * However, other Ratpack extensions may require session storage any rely on Java serialization. * For this reason, there is also always a {@link JavaSessionSerializer} implementation available that is guaranteed to be able to serialize any {@link Serializable} * object (that conforms to the {@link Serializable} contract. * Users of this module may also choose to override this binding with another implementation (e.g. one based on <a href="https://github.com/EsotericSoftware/kryo">Kryo</a>), * but this implementation must be able to serialize any object implementing {@link Serializable}. * * It is also often desirable to provide alternative implementations for {@link SessionSerializer} and {@link JavaSessionSerializer}. * The default binding for both types is an implementation that uses out-of-the-box Java serialization (which is neither fast nor efficient). * * <h3>Example usage</h3> * <pre class="java">{@code * import ratpack.guice.Guice; * import ratpack.path.PathTokens; * import ratpack.session.Session; * import ratpack.session.SessionModule; * import ratpack.test.embed.EmbeddedApp; * * import static org.junit.Assert.assertEquals; * * public class Example { * public static void main(String... args) throws Exception { * EmbeddedApp.of(a -> a * .registry(Guice.registry(b -> b * .module(SessionModule.class) * )) * .handlers(c -> c * .get("set/:name/:value", ctx -> * ctx.get(Session.class).getData().then(sessionData -> { * PathTokens pathTokens = ctx.getPathTokens(); * sessionData.set(pathTokens.get("name"), pathTokens.get("value")); * ctx.render("ok"); * }) * ) * .get("get/:name", ctx -> { * ctx.get(Session.class).getData() * .map(d -> d.require(ctx.getPathTokens().get("name"))) * .then(ctx::render); * }) * ) * ).test(httpClient -> { * assertEquals("ok", httpClient.getText("set/foo/bar")); * assertEquals("bar", httpClient.getText("get/foo")); * * assertEquals("ok", httpClient.getText("set/foo/baz")); * assertEquals("baz", httpClient.getText("get/foo")); * }); * } * } * }</pre> */ public class SessionModule extends ConfigurableModule<SessionCookieConfig> { /** * The name of the binding for the {@link Cache} implementation that backs the in memory session store. * * @see #memoryStore(Consumer) */ public static final String LOCAL_MEMORY_SESSION_CACHE_BINDING_NAME = "localMemorySessionCache"; /** * The key of the binding for the {@link Cache} implementation that backs the in memory session store. * * @see #memoryStore(Consumer) */ public static final Key<Cache<AsciiString, ByteBuf>> LOCAL_MEMORY_SESSION_CACHE_BINDING_KEY = Key.get( new TypeLiteral<Cache<AsciiString, ByteBuf>>() {}, Names.named(LOCAL_MEMORY_SESSION_CACHE_BINDING_NAME) ); /** * A builder for an alternative cache for the default in memory store. * <p> * This method is intended to be used with the {@link BindingsSpec#binder(Action)} method. * <pre class="java">{@code * import ratpack.guice.Guice; * import ratpack.session.SessionModule; * * public class Example { * public static void main(String... args) { * Guice.registry(b -> b * .binder(SessionModule.memoryStore(c -> c.maximumSize(100))) * ); * } * } * }</pre> * * @param config the cache configuration * @return an action that binds the cache * @see #memoryStore(Binder, Consumer) */ public static Action<Binder> memoryStore(Consumer<? super CacheBuilder<AsciiString, ByteBuf>> config) { return b -> memoryStore(b, config); } /** * A builder for an alternative cache for the default in memory store. * <p> * This method can be used from within a custom {@link Module}. * <pre class="java">{@code * import com.google.inject.AbstractModule; * import ratpack.session.SessionModule; * * public class CustomSessionModule extends AbstractModule { * protected void configure() { * SessionModule.memoryStore(binder(), c -> c.maximumSize(100)); * } * } * }</pre> * }<p> * This method binds the built cache with the {@link #LOCAL_MEMORY_SESSION_CACHE_BINDING_KEY} key. * It also implicitly registers a {@link RemovalListener}, that releases the byte buffers as they are discarded. * * @param binder the guice binder * @param config the cache configuration */ public static void memoryStore(Binder binder, Consumer<? super CacheBuilder<AsciiString, ByteBuf>> config) { binder.bind(LOCAL_MEMORY_SESSION_CACHE_BINDING_KEY).toProvider(() -> { CacheBuilder<AsciiString, ByteBuf> cacheBuilder = Types.cast(CacheBuilder.newBuilder()); cacheBuilder.removalListener(n -> n.getValue().release()); config.accept(cacheBuilder); return cacheBuilder.build(); }).in(Scopes.SINGLETON); } @Override protected void configure() { memoryStore(binder(), s -> s.maximumSize(1000)); } @Provides @Singleton SessionStore sessionStoreAdapter(@Named(LOCAL_MEMORY_SESSION_CACHE_BINDING_NAME) Cache<AsciiString, ByteBuf> cache) { return new LocalMemorySessionStore(cache); } @Provides SessionIdGenerator sessionIdGenerator() { return new DefaultSessionIdGenerator(); } @Provides @RequestScoped SessionId sessionId(Request request, Response response, SessionIdGenerator idGenerator, SessionCookieConfig cookieConfig) { return new CookieBasedSessionId(request, response, idGenerator, cookieConfig); } @Provides SessionSerializer sessionValueSerializer(JavaSessionSerializer sessionSerializer) { return sessionSerializer; } @Provides JavaSessionSerializer javaSessionSerializer() { return new JavaBuiltinSessionSerializer(); } @Provides @RequestScoped Session sessionAdapter(SessionId sessionId, SessionStore store, Response response, ByteBufAllocator bufferAllocator, SessionSerializer defaultSerializer, JavaSessionSerializer javaSerializer) { return new DefaultSession(sessionId, bufferAllocator, store, response, defaultSerializer, javaSerializer); } }