/*
* 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 io.netty.buffer.ByteBuf;
import io.netty.util.AsciiString;
import ratpack.exec.Operation;
import ratpack.exec.Promise;
/**
* A persistent store of session data.
* <p>
* Ratpack's session support cooperates with the implementation of this type found in the context registry.
* The {@link SessionModule} provides a default implementation that stores the data in local memory.
* In order to persist session data in the store of your choice, simply override the binding for this type with your own implementation.
* <p>
* The store methods return {@link Promise} and {@link Operation} in order to support non blocking IO.
* <p>
* The store should not make any attempt to interpret the bytes that it is storing/loading.
*
* <h3>Example implementation</h3>
* <p>
* Here is an example implementation that uses files on the filesystem to store session data.
* <pre class="java">{@code
* import com.google.common.io.Files;
* import com.google.inject.Singleton;
* import io.netty.buffer.ByteBuf;
* import io.netty.buffer.ByteBufAllocator;
* import io.netty.buffer.ByteBufInputStream;
* import io.netty.buffer.ByteBufOutputStream;
* import io.netty.util.AsciiString;
* import ratpack.exec.Operation;
* import ratpack.exec.Promise;
* import ratpack.exec.Blocking;
* import ratpack.guice.ConfigurableModule;
* import ratpack.guice.Guice;
* import ratpack.session.Session;
* import ratpack.session.SessionModule;
* import ratpack.session.SessionStore;
* import ratpack.test.embed.EphemeralBaseDir;
* import ratpack.test.embed.EmbeddedApp;
*
* import javax.inject.Inject;
* import java.io.File;
* import java.io.IOException;
* import java.util.Arrays;
*
* import static org.junit.Assert.assertEquals;
*
* public class Example {
*
* static class FileSessionStore implements SessionStore {
* private final ByteBufAllocator bufferAllocator;
* private final File dir;
*
* {@literal @}Inject
* public FileSessionStore(ByteBufAllocator bufferAllocator, FileSessionModule.Config config) {
* this.bufferAllocator = bufferAllocator;
* this.dir = config.dir;
* }
*
* {@literal @}Override
* public void onStart({@literal @}SuppressWarnings("deprecation") ratpack.server.StartEvent event) throws Exception {
* Blocking.op(() -> {
* assert dir.mkdirs() || dir.exists();
* }).then();
* }
*
* {@literal @}Override
* public void onStop({@literal @}SuppressWarnings("deprecation") ratpack.server.StopEvent event) throws Exception {
* Blocking.op(() -> {
* Arrays.asList(dir.listFiles()).forEach(File::delete);
* dir.delete();
* }).then();
* }
*
* {@literal @}Override
* public Operation store(AsciiString sessionId, ByteBuf sessionData) {
* return Blocking.op(() ->
* Files.asByteSink(file(sessionId)).writeFrom(new ByteBufInputStream(sessionData))
* );
* }
*
* {@literal @}Override
* public Promise<ByteBuf> load(AsciiString sessionId) {
* File sessionFile = file(sessionId);
* return Blocking.get(() -> {
* if (sessionFile.exists()) {
* ByteBuf buffer = bufferAllocator.buffer((int) sessionFile.length());
* try {
* Files.asByteSource(sessionFile).copyTo(new ByteBufOutputStream(buffer));
* return buffer;
* } catch (IOException e) {
* buffer.release();
* throw e;
* }
* } else {
* return bufferAllocator.buffer(0, 0);
* }
* });
* }
*
* private File file(AsciiString sessionId) {
* return new File(dir, sessionId.toString());
* }
*
* {@literal @}Override
* public Operation remove(AsciiString sessionId) {
* return Blocking.op(() -> file(sessionId).delete());
* }
*
* {@literal @}Override
* public Promise<Long> size() {
* return Blocking.get(() -> (long) dir.listFiles(File::isFile).length);
* }
* }
*
* public static class FileSessionModule extends ConfigurableModule<FileSessionModule.Config> {
* public static class Config {
* File dir;
* }
*
* {@literal @}Override
* protected void configure() {
* bind(SessionStore.class).to(FileSessionStore.class).in(Singleton.class);
* }
* }
*
* public static void main(String... args) throws Exception {
* EphemeralBaseDir.tmpDir().use(baseDir -> {
* EmbeddedApp.of(s -> s
* .registry(Guice.registry(b -> b
* .module(SessionModule.class)
* .module(FileSessionModule.class, c -> c.dir = baseDir.getRoot().toFile())
* ))
* .handlers(c -> c
* .get("set/:name/:value", ctx ->
* ctx.get(Session.class).getData().then(sessionData -> {
* sessionData.set(ctx.getPathTokens().get("name"), ctx.getPathTokens().get("value"));
* ctx.render("ok");
* })
* )
* .get("get/:name", ctx ->
* ctx.render(ctx.get(Session.class).getData().map(sessionData -> sessionData.require(ctx.getPathTokens().get("name"))))
* )
* )
* ).test(httpClient -> {
* assertEquals("ok", httpClient.getText("set/foo/bar"));
* assertEquals("bar", httpClient.getText("get/foo"));
* });
* });
* }
* }
* }</pre>
*
* @see SessionModule
*/
@SuppressWarnings("deprecation")
public interface SessionStore extends ratpack.server.Service {
/**
* Writes the session data for the given id.
* <p>
* The given byte buffer will not be modified by the caller, and will be released by the caller after the returned operation has completed (with error or without).
*
* @param sessionId the identifier for the session
* @param sessionData the session data
* @return the store operation
*/
Operation store(AsciiString sessionId, ByteBuf sessionData);
/**
* Reads the session data for the given id.
* <p>
* The caller will release the promised byte buffer.
*
* @param sessionId the identifier for the session
* @return a promise for the session data
*/
Promise<ByteBuf> load(AsciiString sessionId);
/**
* Removes the session data for the given id.
*
* @param sessionId the session id
* @return the remove operation
*/
Operation remove(AsciiString sessionId);
/**
* The current number of sessions.
* <p>
* The exact meaning of this value is implementation dependent.
* {@code -1} may be returned if the store does not support getting the size.
*
* @return a promise for the store size
*/
Promise<Long> size();
}