11package restx .security ;
22
33import com .google .common .base .Optional ;
4- import com .google .common .cache .CacheBuilder ;
5- import com .google .common .cache .CacheLoader ;
6- import com .google .common .cache .LoadingCache ;
74import com .google .common .collect .ImmutableMap ;
5+ import com .google .common .collect .ImmutableSet ;
86import com .google .common .collect .Maps ;
97import org .joda .time .Duration ;
108import restx .factory .Component ;
119
1210import java .util .Map ;
13- import java .util .concurrent . ExecutionException ;
11+ import java .util .Map . Entry ;
1412
1513/**
1614 * RestxSession is used to store information which can be used across several HTTP requests from the same client.
1715 *
1816 * It is organized as a Map, information is stored by keys.
1917 *
20- * It doesn't use the JEE Session mechanism, but a more lightweight system relying on a signed session cookie
21- * (therefore it cannot be tampered by the client).
18+ * It doesn't use the JEE Session mechanism, but a more lightweight system relying on signed session data stored
19+ * on the client (being signed, it cannot be tampered by the client).
2220 *
23- * The session cookie doesn't store the whole values (which could put a high load on the network and even cause
24- * problems related to cookie size limit), but rather stores a a value id for each session key.
21+ * The session data doesn't store the whole values (which could put a high load on the network and even cause
22+ * problems related to data storage limit on the client ), but rather stores a a value id for each session key.
2523 *
2624 * A value id MUST identify uniquely a value when used for a given session key, and the session MUST be configured
2725 * with a CacheLoader per key, able to load the value corresponding to the value id for a particular key.
2826 *
2927 * Therefore on the server the session enables to access arbitrary large objects, it will only put pressure on a
30- * server cache, and on cache loaders if requests are heavily distributed. Indeed the cache is not distributed,
31- * so a in a large clustered environment cache miss will be very likely and cache loaders will often be called.
32- * Hence in such environment you should be careful to use very efficient cache loaders if you rely heavily on
33- * session.
28+ * server cache, and on cache loaders if requests are heavily distributed and depending on cache implementation.
3429 *
3530 * An example (using an arbitrary json like notation):
3631 * <pre>
3934 * "USER": (valueId) -> { return db.findOne("{_id: #}", valueId).as(User.class); }
4035 * }
4136 * "valueIdsByKeys": {
42- * "USER": "johndoe@acme.com" // valued from session cookie
37+ * "USER": "johndoe@acme.com" // valued from client session data
4338 * }
4439 * }
4540 * </pre>
4641 * With such a restx session, when you call a #get(User.class, "USER"), the session will first check its
4742 * valueIdsByKeys map to find the corresponding valueId ("johndoe@acme.com"). Then it will check the cache for
4843 * this valueId, and in case of cache miss will use the provided cache loader which will load the user from db.
44+ *
45+ * If you want to define your own session keys, you should define a Definition.Entry component for it allowing
46+ * to load values based on their ids. You don't have to take care of caching in the Entry, caching is performed
47+ * by EntryCacheManager.
4948 */
5049public class RestxSession {
5150 @ Component
5251 public static class Definition {
53- public static class Entry <T > {
54- private final Class <T > clazz ;
55- private final String key ;
56- private final CacheLoader <String , T > loader ;
57-
58- public Entry (Class <T > clazz , String key , CacheLoader <String , T > loader ) {
59- this .clazz = clazz ;
60- this .key = key ;
61- this .loader = loader ;
62- }
52+ /**
53+ * A session definition entry is responsible for loading session values by session value id, for a
54+ * particular definition key.
55+ *
56+ * They don't implement caching themselves, see CachedEntry for entries supporting caching.
57+ *
58+ * @param <T> the type of values this Entry handles.
59+ */
60+ public static interface Entry <T > {
61+ /**
62+ * Returns the definition key that this entry handles.
63+ * @return the definition key that this entry handles.
64+ */
65+ String getKey ();
66+
67+ /**
68+ * Gives the value corresponding to the given valueId.
69+ *
70+ * @param valueId the id of the value to get.
71+ *
72+ * @return the value, or absent if not found.
73+ */
74+ Optional <? extends T > getValueForId (String valueId );
6375 }
64- private final ImmutableMap <String , LoadingCache <String , ?>> caches ;
76+
77+ /**
78+ * A cached version of session definition entry.
79+ *
80+ * This does not derive from Entry, because its its getting method does not have the same semantic as the
81+ * original one: here it returns a value which may not be the freshest one, while an Entry should always
82+ * return current one.
83+ *
84+ * CachedEntry instances are usually created from a Entry instance using a EntryCacheManager.
85+ *
86+ * @param <T> the type of values this CachedEntry handles.
87+ */
88+ public static interface CachedEntry <T > {
89+ /**
90+ * Returns the definition key that this entry handles.
91+ * @return the definition key that this entry handles.
92+ */
93+ String getKey ();
94+
95+ /**
96+ * Gives the value corresponding to the given valueId.
97+ * This value may come from a cache, so its freshness depends on configuration and implementation.
98+ *
99+ * @param valueId the id of the value to get.
100+ *
101+ * @return the value, or absent if not found.
102+ */
103+ Optional <? extends T > getValueForId (String valueId );
104+
105+ /**
106+ * Invalidates the cache for a single value id.
107+ *
108+ * @param valueId the value id for which cache should be invalidated.
109+ */
110+ void invalidateCacheFor (String valueId );
111+
112+ /**
113+ * Invalidates the full cache of this entry.
114+ *
115+ * This may impact more than this single entry if this entry cache is backed by a broader cache.
116+ */
117+ void invalidateCache ();
118+ }
119+
120+ /**
121+ * A cache manager for session definition entry.
122+ *
123+ * It transforms Entry into CachedEntry
124+ */
125+ public static interface EntryCacheManager {
126+ <T > CachedEntry <T > getCachedEntry (Entry <T > entry );
127+ }
128+
129+ private final ImmutableMap <String , CachedEntry <?>> entries ;
65130
66131 @ SuppressWarnings ("unchecked" ) // can't use Iterable<Entry<?> as parameter in injectable constructor ATM
67- public Definition (Iterable <Entry > entries ) {
68- ImmutableMap .Builder <String , LoadingCache < String , ?>> builder = ImmutableMap .builder ();
132+ public Definition (EntryCacheManager cacheManager , Iterable <Entry > entries ) {
133+ ImmutableMap .Builder <String , CachedEntry < ?>> builder = ImmutableMap .builder ();
69134 for (Entry <?> entry : entries ) {
70- builder .put (entry .key , CacheBuilder . newBuilder (). maximumSize ( 1000 ). build ( entry . loader ));
135+ builder .put (entry .getKey (), cacheManager . getCachedEntry ( entry ));
71136 }
72- caches = builder .build ();
137+ this .entries = builder .build ();
138+ }
139+
140+ public ImmutableSet <String > entriesKeySet () {
141+ return entries .keySet ();
142+ }
143+
144+ public boolean hasEntryForKey (String key ) {
145+ return entries .containsKey (key );
73146 }
74147
75148 @ SuppressWarnings ("unchecked" )
76- public <T > LoadingCache <String , T > getCache (Class <T > clazz , String key ) {
77- return (LoadingCache <String , T >) caches .get (key );
149+ public <T > Optional <CachedEntry <T >> getEntry (String key ) {
150+ return Optional .fromNullable ((CachedEntry <T >) entries .get (key ));
151+ }
152+
153+ public void invalidateAllCaches () {
154+ for (CachedEntry <?> entry : entries .values ()) {
155+ entry .invalidateCache ();
156+ }
78157 }
79158 }
80159
@@ -105,33 +184,24 @@ public static RestxSession current() {
105184 this .expires = expires ;
106185 }
107186
108- public RestxSession cleanUpCaches () {
109- for (LoadingCache <String , ?> cache : definition . caches . values ()) {
110- cache . cleanUp ( );
187+ public RestxSession invalidateCaches () {
188+ for (Entry <String , String > entry : valueidsByKey . entrySet ()) {
189+ definition . getEntry ( entry . getKey ()). get (). invalidateCacheFor ( entry . getValue () );
111190 }
112191 return this ;
113192 }
114193
115-
116194 public <T > Optional <T > get (Class <T > clazz , String key ) {
117195 return getValue (definition , clazz , key , valueidsByKey .get (key ));
118196 }
119197
198+ @ SuppressWarnings ("unchecked" )
120199 static <T > Optional <T > getValue (Definition definition , Class <T > clazz , String key , String valueid ) {
121200 if (valueid == null ) {
122201 return Optional .absent ();
123202 }
124203
125- try {
126- return Optional .fromNullable (definition .getCache (clazz , key ).get (valueid ));
127- } catch (CacheLoader .InvalidCacheLoadException e ) {
128- // this exception is raised when cache loader returns null, which may happen if the object behind the key
129- // is deleted. Therefore we just return an absent value
130- return Optional .absent ();
131- } catch (ExecutionException e ) {
132- throw new RuntimeException (
133- "impossible to load object from cache using valueid " + valueid + " for " + key + ": " + e .getMessage (), e );
134- }
204+ return (Optional <T >) definition .getEntry (key ).get ().getValueForId (valueid );
135205 }
136206
137207 public RestxSession expires (Duration duration ) {
@@ -143,9 +213,9 @@ public Duration getExpires() {
143213 }
144214
145215 public <T > RestxSession define (Class <T > clazz , String key , String valueid ) {
146- if (!definition .caches . containsKey (key )) {
216+ if (!definition .hasEntryForKey (key )) {
147217 throw new IllegalArgumentException ("undefined context key: " + key + "." +
148- " Keys defined are: " + definition .caches . keySet ());
218+ " Keys defined are: " + definition .entriesKeySet ());
149219 }
150220 // create new map by using a mutable map, not a builder, in case the the given entry overrides a previous one
151221 Map <String ,String > newValueidsByKey = Maps .newHashMap ();
0 commit comments