/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.jooby;
import static java.util.Objects.requireNonNull;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.google.common.io.BaseEncoding;
/**
* <p>
* Sessions are created on demand via: {@link Request#session()}.
* </p>
*
* <p>
* Sessions have a lot of uses cases but most commons are: auth, store information about current
* user, etc.
* </p>
*
* <p>
* A session attribute must be {@link String} or a primitive. Session doesn't allow to store
* arbitrary objects. It is a simple mechanism to store basic data.
* </p>
*
* <h1>Session configuration</h1>
*
* <h2>No timeout</h2>
* <p>
* There is no timeout for sessions from server perspective. By default, a session will expire when
* the user close the browser (a.k.a session cookie).
* </p>
*
* <h2>Session store</h2>
* <p>
* A {@link Session.Store} is responsible for saving session data. Sessions are kept in memory, by
* default using the {@link Session.Mem} store, which is useful for development, but wont scale well
* on production environments. An redis, memcached, ehcache store will be a better option.
* </p>
*
* <h3>Store life-cycle</h3>
* <p>
* Sessions are persisted every time a request exit, if they are dirty. A session get dirty if an
* attribute is added or removed from it.
* </p>
* <p>
* The <code>session.saveInterval</code> property indicates how frequently a session will be
* persisted (in millis).
* </p>
* <p>
* In short, a session is persisted when: 1) it is dirty; or 2) save interval has expired it.
* </p>
*
* <h1>Cookie configuration</h1>
* <p>
* Next session describe the most important options:
* </p>
*
* <h2>max-age</h2>
* <p>
* The <code>session.cookie.maxAge</code> sets the maximum age in seconds. A positive value
* indicates that the cookie will expire after that many seconds have passed. Note that the value is
* the <i>maximum</i> age when the cookie will expire, not the cookie's current age.
*
* A negative value means that the cookie is not stored persistently and will be deleted when the
* Web browser exits.
*
* Default maxAge is: <code>-1</code>.
*
* </p>
*
* <h2>signed cookie</h2>
* <p>
* If the <code>application.secret</code> property has been set, then the session cookie will be
* signed it with it.
* </p>
*
* <h2>cookie's name</h2>
* <p>
* The <code>session.cookie.name</code> indicates the name of the cookie that hold the session ID,
* by defaults: <code>jooby.sid</code>. Cookie's name can be explicitly set with
* {@link Cookie.Definition#name(String)} on {@link Session.Definition#cookie()}.
* </p>
*
* @author edgar
* @since 0.1.0
*/
public interface Session {
/** Global/Shared id of cookie sessions. */
String COOKIE_SESSION = "cookieSession";
/**
* Hold session related configuration parameters.
*
* @author edgar
* @since 0.1.0
*/
class Definition {
/** Session store. */
private Object store;
/** Session cookie. */
private Cookie.Definition cookie;
/** Save interval. */
private Long saveInterval;
/**
* Creates a new session definition.
*
* @param store A session store.
*/
public Definition(final Class<? extends Store> store) {
this.store = requireNonNull(store, "A session store is required.");
cookie = new Cookie.Definition();
}
/**
* Creates a new session definition with a client store.
*/
Definition() {
cookie = new Cookie.Definition();
}
/**
* Creates a new session definition.
*
* @param store A session store.
*/
public Definition(final Store store) {
this.store = requireNonNull(store, "A session store is required.");
cookie = new Cookie.Definition();
}
/**
* Indicates how frequently a no-dirty session should be persisted (in millis).
*
* @return A save interval that indicates how frequently no dirty session should be persisted.
*/
public Optional<Long> saveInterval() {
return Optional.ofNullable(saveInterval);
}
/**
* Set/override how frequently a no-dirty session should be persisted (in millis).
*
* @param saveInterval Save interval in millis or <code>-1</code> for turning it off.
* @return This definition.
*/
public Definition saveInterval(final long saveInterval) {
this.saveInterval = saveInterval;
return this;
}
/**
* @return A session store instance or class.
*/
public Object store() {
return store;
}
/**
* @return Configure cookie session.
*/
public Cookie.Definition cookie() {
return cookie;
}
}
/**
* Read, save and delete sessions from a persistent storage.
*
* @author edgar
* @since 0.1.0
*/
interface Store {
/** Single secure random instance. */
SecureRandom rnd = new SecureRandom();
/**
* Get a session by ID (if any).
*
* @param builder A session builder.
* @return A session or <code>null</code>.
*/
Session get(Session.Builder builder);
/**
* Save/persist a session.
*
* @param session A session to be persisted.
*/
void save(Session session);
void create(final Session session);
/**
* Delete a session by ID.
*
* @param id A session ID.
*/
void delete(String id);
/**
* Generate a session ID.
*
* @return A unique session ID.
*/
default String generateID() {
byte[] bytes = new byte[30];
rnd.nextBytes(bytes);
return BaseEncoding.base64Url().encode(bytes);
}
}
/**
* A keep in memory session store.
*
* @author edgar
*/
class Mem implements Store {
private ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<String, Session>();
@Override
public void create(final Session session) {
sessions.putIfAbsent(session.id(), session);
}
@Override
public void save(final Session session) {
sessions.put(session.id(), session);
}
@Override
public Session get(final Session.Builder builder) {
return sessions.get(builder.sessionId());
}
@Override
public void delete(final String id) {
sessions.remove(id);
}
}
/**
* Build or restore a session from a persistent storage.
*
* @author edgar
*/
interface Builder {
/**
* @return Session ID.
*/
String sessionId();
/**
* Set a session local attribute.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This builder.
*/
Builder set(final String name, final String value);
/**
* Set one ore more session local attributes.
*
* @param attributes Attributes to add.
* @return This builder.
*/
Builder set(final Map<String, String> attributes);
/**
* Set session created date.
*
* @param createdAt Session created date.
* @return This builder.
*/
Builder createdAt(long createdAt);
/**
* Set session last accessed date.
*
* @param accessedAt Session last accessed date.
* @return This builder.
*/
Builder accessedAt(long accessedAt);
/**
* Set session last saved it date.
*
* @param savedAt Session last saved it date.
* @return This builder.
*/
Builder savedAt(final long savedAt);
/**
* Final step to build a new session.
*
* @return A session.
*/
Session build();
}
/**
* A session ID for server side sessions. Otherwise {@link #COOKIE_SESSION} for client side sessions.
*
* Session ID on client sessions doesn't make sense because resolution of session is done via
* cookie name.
*
* Another reason of not saving the session ID inside the cookie, is the cookie size (up to 4kb).
* If the session ID is persisted then users lost space to save business data.
*
* @return Session ID.
*/
String id();
/**
* The time when this session was created, measured in milliseconds since midnight January 1, 1970
* GMT for server side sessions. Or <code>-1</code> for client side sessions.
*
* @return The time when this session was created, measured in milliseconds since midnight January
* 1, 1970 GMT for server side sessions. Or <code>-1</code> for client side sessions.
*/
long createdAt();
/**
* Last time the session was save it as epoch millis or <code>-1</code> for client side sessions.
*
* @return Last time the session was save it as epoch millis or <code>-1</code> for client side
* sessions.
*/
long savedAt();
/**
* The last time the client sent a request associated with this session, as the number of
* milliseconds since midnight January 1, 1970 GMT, and marked by the time the container
* received the request. Or <code>-1</code> for client side sessions.
*
* <p>
* Actions that your application takes, such as getting or setting a value associated with the
* session, do not affect the access time.
* </p>
*
* @return Last time the client sent a request. Or <code>-1</code> for client side sessions.
*/
long accessedAt();
/**
* The time when this session is going to expire, measured in milliseconds since midnight
* January 1, 1970 GMT. Or <code>-1</code> for client side sessions.
*
* @return The time when this session is going to expire, measured in milliseconds since midnight
* January 1, 1970 GMT. Or <code>-1</code> for client side sessions.
*/
long expiryAt();
/**
* Get a object from this session. If the object isn't found this method returns an empty
* optional.
*
* @param name Attribute's name.
* @return Value as mutant.
*/
Mutant get(final String name);
/**
* @return An immutable copy of local attributes.
*/
Map<String, String> attributes();
/**
* Test if the var name exists inside the session local attributes.
*
* @param name A local var's name.
* @return True, for existing locals.
*/
boolean isSet(final String name);
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final byte value) {
return set(name, Byte.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final char value) {
return set(name, Character.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final boolean value) {
return set(name, Boolean.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final short value) {
return set(name, Short.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final int value) {
return set(name, Integer.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final long value) {
return set(name, Long.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final float value) {
return set(name, Float.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final double value) {
return set(name, Double.toString(value));
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
default Session set(final String name, final CharSequence value) {
return set(name, value.toString());
}
/**
* Set a session local using a the given name. If a local already exists, it will be replaced
* with the new value. Keep in mind that null values are NOT allowed.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This session.
*/
Session set(final String name, final String value);
/**
* Remove a local value (if any) from session locals.
*
* @param name Attribute's name.
* @return Existing value or empty optional.
*/
Mutant unset(final String name);
/**
* Unset/remove all the session data.
*
* @return This session.
*/
Session unset();
/**
* Invalidates this session then unset any objects bound to it.
*/
void destroy();
}