All Downloads are FREE. Search and download functionalities are using the official Maven repository.

restx.security.RestxSession Maven / Gradle / Ivy

There is a newer version: 1.2.0-rc2
Show newest version
package restx.security;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import org.joda.time.Duration;
import restx.factory.Component;

import java.util.Map;
import java.util.Map.Entry;

/**
 * RestxSession is used to store information which can be used across several HTTP requests from the same client.
 *
 * It is organized as a Map, information is stored by keys.
 *
 * It doesn't use the JEE Session mechanism, but a more lightweight system relying on signed session data stored
 * on the client (being signed, it cannot be tampered by the client).
 *
 * The session data doesn't store the whole values (which could put a high load on the network and even cause
 * problems related to data storage limit on the client), but rather stores a a value id for each session key.
 *
 * A value id MUST identify uniquely a value when used for a given session key, and the session MUST be configured
 * with a CacheLoader per key, able to load the value corresponding to the value id for a particular key.
 *
 * Therefore on the server the session enables to access arbitrary large objects, it will only put pressure on a
 * server cache, and on cache loaders if requests are heavily distributed and depending on cache implementation.
 *
 * An example (using an arbitrary json like notation):
 * 
 *     "RestxSession": {
 *          "definition": {  // this is configured once at application level
 *              "USER": (valueId) -> { return db.findOne("{_id: #}", valueId).as(User.class); }
 *          }
 *          "valueIdsByKeys": {
 *              "USER": "[email protected]" // valued from client session data
 *          }
 *     }
 * 
* With such a restx session, when you call a #get(User.class, "USER"), the session will first check its * valueIdsByKeys map to find the corresponding valueId ("[email protected]"). Then it will check the cache for * this valueId, and in case of cache miss will use the provided cache loader which will load the user from db. * * If you want to define your own session keys, you should define a Definition.Entry component for it allowing * to load values based on their ids. You don't have to take care of caching in the Entry, caching is performed * by EntryCacheManager. */ public class RestxSession { @Component public static class Definition { /** * A session definition entry is responsible for loading session values by session value id, for a * particular definition key. * * They don't implement caching themselves, see CachedEntry for entries supporting caching. * * @param the type of values this Entry handles. */ public static interface Entry { /** * Returns the definition key that this entry handles. * @return the definition key that this entry handles. */ String getKey(); /** * Gives the value corresponding to the given valueId. * * @param valueId the id of the value to get. * * @return the value, or absent if not found. */ Optional getValueForId(String valueId); } /** * A cached version of session definition entry. * * This does not derive from Entry, because its its getting method does not have the same semantic as the * original one: here it returns a value which may not be the freshest one, while an Entry should always * return current one. * * CachedEntry instances are usually created from a Entry instance using a EntryCacheManager. * * @param the type of values this CachedEntry handles. */ public static interface CachedEntry { /** * Returns the definition key that this entry handles. * @return the definition key that this entry handles. */ String getKey(); /** * Gives the value corresponding to the given valueId. * This value may come from a cache, so its freshness depends on configuration and implementation. * * @param valueId the id of the value to get. * * @return the value, or absent if not found. */ Optional getValueForId(String valueId); /** * Invalidates the cache for a single value id. * * @param valueId the value id for which cache should be invalidated. */ void invalidateCacheFor(String valueId); /** * Invalidates the full cache of this entry. * * This may impact more than this single entry if this entry cache is backed by a broader cache. */ void invalidateCache(); } /** * A cache manager for session definition entry. * * It transforms Entry into CachedEntry */ public static interface EntryCacheManager { CachedEntry getCachedEntry(Entry entry); } private final ImmutableMap> entries; @SuppressWarnings("unchecked") // can't use Iterable as parameter in injectable constructor ATM public Definition(EntryCacheManager cacheManager, Iterable entries) { ImmutableMap.Builder> builder = ImmutableMap.builder(); for (Entry entry : entries) { builder.put(entry.getKey(), cacheManager.getCachedEntry(entry)); } this.entries = builder.build(); } public ImmutableSet entriesKeySet() { return entries.keySet(); } public boolean hasEntryForKey(String key) { return entries.containsKey(key); } @SuppressWarnings("unchecked") public Optional> getEntry(String key) { return Optional.fromNullable((CachedEntry) entries.get(key)); } public void invalidateAllCaches() { for (CachedEntry entry : entries.values()) { entry.invalidateCache(); } } } private static final ThreadLocal current = new ThreadLocal<>(); static void setCurrent(RestxSession ctx) { if (ctx == null) { current.remove(); } else { current.set(ctx); } } public static RestxSession current() { return current.get(); } private final Definition definition; private final ImmutableMap valueidsByKey; private final Duration expires; private final Optional principal; RestxSession(Definition definition, ImmutableMap valueidsByKey, Optional principal, Duration expires) { this.definition = definition; this.valueidsByKey = valueidsByKey; this.principal = principal; this.expires = expires; } public RestxSession invalidateCaches() { for (Entry entry : valueidsByKey.entrySet()) { definition.getEntry(entry.getKey()).get().invalidateCacheFor(entry.getValue()); } return this; } public Optional get(Class clazz, String key) { return getValue(definition, clazz, key, valueidsByKey.get(key)); } @SuppressWarnings("unchecked") static Optional getValue(Definition definition, Class clazz, String key, String valueid) { if (valueid == null) { return Optional.absent(); } return (Optional) definition.getEntry(key).get().getValueForId(valueid); } public RestxSession expires(Duration duration) { return mayUpdateCurrent(new RestxSession(definition, valueidsByKey, principal, duration)); } public Duration getExpires() { return expires; } public RestxSession define(Class clazz, String key, String valueid) { if (!definition.hasEntryForKey(key)) { throw new IllegalArgumentException("undefined context key: " + key + "." + " Keys defined are: " + definition.entriesKeySet()); } // create new map by using a mutable map, not a builder, in case the the given entry overrides a previous one Map newValueidsByKey = Maps.newHashMap(); newValueidsByKey.putAll(valueidsByKey); if (valueid == null) { newValueidsByKey.remove(key); } else { newValueidsByKey.put(key, valueid); } return mayUpdateCurrent(new RestxSession(definition, ImmutableMap.copyOf(newValueidsByKey), principal, expires)); } public RestxSession authenticateAs(RestxPrincipal principal) { return mayUpdateCurrent(new RestxSession(definition, valueidsByKey, Optional.of(principal), expires)) .define(RestxPrincipal.class, RestxPrincipal.SESSION_DEF_KEY, principal.getName()); } public RestxSession clearPrincipal() { return mayUpdateCurrent(new RestxSession(definition, valueidsByKey, Optional.absent(), expires)) .define(RestxPrincipal.class, RestxPrincipal.SESSION_DEF_KEY, null); } public Optional getPrincipal() { return principal; } private RestxSession mayUpdateCurrent(RestxSession newSession) { if (this == current()) { current.set(newSession); } return newSession; } ImmutableMap valueidsByKeyMap() { return valueidsByKey; } /** * Executes a runnable with this session set as current session. * * Inside the runnable, the current session can be accessed with RestxSession.current(). * * This method takes care of restoring the current session after the call. So if the current session * is altered inside the runnable it won't have effect on the caller. * * @param runnable the runnable to execute. */ public void runIn(Runnable runnable) { RestxSession current = current(); setCurrent(this); try { runnable.run(); } finally { setCurrent(current); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy