View Javadoc

1   package org.eclipse.jetty.nosql.mongodb;
2   //========================================================================
3   //Copyright (c) 2011 Intalio, Inc.
4   //------------------------------------------------------------------------
5   //All rights reserved. This program and the accompanying materials
6   //are made available under the terms of the Eclipse Public License v1.0
7   //and Apache License v2.0 which accompanies this distribution.
8   //The Eclipse Public License is available at
9   //http://www.eclipse.org/legal/epl-v10.html
10  //The Apache License v2.0 is available at
11  //http://www.opensource.org/licenses/apache2.0.php
12  //You may elect to redistribute this code under either of these licenses.
13  //========================================================================
14  
15  import java.io.ByteArrayInputStream;
16  import java.io.ByteArrayOutputStream;
17  import java.io.IOException;
18  import java.io.ObjectInputStream;
19  import java.io.ObjectOutputStream;
20  import java.net.UnknownHostException;
21  import java.util.Date;
22  import java.util.HashMap;
23  import java.util.Map;
24  import java.util.Set;
25  
26  import org.eclipse.jetty.nosql.NoSqlSession;
27  import org.eclipse.jetty.nosql.NoSqlSessionManager;
28  import org.eclipse.jetty.server.SessionIdManager;
29  import org.eclipse.jetty.util.log.Log;
30  import org.eclipse.jetty.util.log.Logger;
31  import org.omg.CORBA._IDLTypeStub;
32  
33  import com.mongodb.BasicDBObject;
34  import com.mongodb.DBCollection;
35  import com.mongodb.DBObject;
36  import com.mongodb.MongoException;
37  
38  public class MongoSessionManager extends NoSqlSessionManager
39  {  
40      private final static Logger __log = Log.getLogger("org.eclipse.jetty.server.session");
41     
42      /*
43       * strings used as keys or parts of keys in mongo
44       */
45      private final static String __METADATA = "__metadata__";
46  
47      public final static String __ID = "id";
48      private final static String __CREATED = "created";
49      public final static String __VALID = "valid";
50      public final static String __INVALIDATED = "invalidated";
51      public final static String __ACCESSED = "accessed";
52      private final static String __CONTEXT = "context";   
53      public final static String __VERSION = __METADATA + ".version";
54  
55      /**
56      * the context id is only set when this class has been started
57      */
58      private String _contextId = null;
59  
60      
61      private DBCollection _sessions;
62      private DBObject __version_1;
63  
64  
65      /* ------------------------------------------------------------ */
66      public MongoSessionManager() throws UnknownHostException, MongoException
67      {
68          
69      }
70      
71      
72      
73      /*------------------------------------------------------------ */
74      @Override
75      public void doStart() throws Exception
76      {
77          super.doStart();
78          String[] hosts = getContextHandler().getVirtualHosts();
79          if (hosts == null || hosts.length == 0)
80              hosts = getContextHandler().getConnectorNames();
81          if (hosts == null || hosts.length == 0)
82              hosts = new String[]
83              { "::" }; // IPv6 equiv of 0.0.0.0
84  
85          String contextPath = getContext().getContextPath();
86          if (contextPath == null || "".equals(contextPath))
87          {
88              contextPath = "*";
89          }
90  
91          _contextId = createContextId(hosts,contextPath);
92  
93          __version_1 = new BasicDBObject(getContextKey(__VERSION),1);
94      }
95  
96      /* ------------------------------------------------------------ */
97      /* (non-Javadoc)
98       * @see org.eclipse.jetty.server.session.AbstractSessionManager#setSessionIdManager(org.eclipse.jetty.server.SessionIdManager)
99       */
100     @Override
101     public void setSessionIdManager(SessionIdManager metaManager)
102     {
103         MongoSessionIdManager msim = (MongoSessionIdManager)metaManager;
104         _sessions=msim.getSessions();
105         super.setSessionIdManager(metaManager);
106         
107     }
108 
109     /* ------------------------------------------------------------ */
110     @Override
111     protected synchronized Object save(NoSqlSession session, Object version, boolean activateAfterSave)
112     {
113         try
114         {
115             __log.debug("MongoSessionManager:save:" + session);
116             session.willPassivate();
117             ByteArrayOutputStream bout = new ByteArrayOutputStream();
118             ObjectOutputStream out = new ObjectOutputStream(bout);
119 
120             // Form query for upsert
121             BasicDBObject key = new BasicDBObject(__ID,session.getClusterId());
122             key.put(__VALID,true);
123 
124             // Form updates
125             BasicDBObject update = new BasicDBObject();
126             boolean upsert = false;
127             BasicDBObject sets = new BasicDBObject();
128             BasicDBObject unsets = new BasicDBObject();
129 
130             // handle new or existing
131             if (version == null)
132             {
133                 // New session
134                 upsert = true;
135                 version = new Long(1);
136                 sets.put(__CREATED,session.getCreationTime());
137                 sets.put(getContextKey(__VERSION),version);
138             }
139             else
140             {
141                 version = new Long(((Long)version).intValue() + 1);
142                 update.put("$inc",__version_1); 
143             }
144 
145             // handle valid or invalid
146             if (session.isValid())
147             {
148                 sets.put(__ACCESSED,session.getAccessed());
149                 Set<String> names = session.takeDirty();
150                 if (isSaveAllAttributes() || upsert)
151                 {
152                     names.addAll(session.getNames()); // note dirty may include removed names
153                 }
154                     
155                 for (String name : names)
156                 {
157                     Object value = session.getAttribute(name);
158                     if (value == null)
159                         unsets.put(getContextKey() + "." + encodeName(name),1);
160                     else
161                         sets.put(getContextKey() + "." + encodeName(name),encodeName(out,bout,value));
162                 }
163             }
164             else
165             {
166                 sets.put(__VALID,false);
167                 sets.put(__INVALIDATED, System.currentTimeMillis());
168                 unsets.put(getContextKey(),1); 
169             }
170 
171             // Do the upsert
172             if (!sets.isEmpty())
173                 update.put("$set",sets);
174             if (!unsets.isEmpty())
175                 update.put("$unset",unsets);
176 
177             _sessions.update(key,update,upsert,false);
178             __log.debug("MongoSessionManager:save:db.sessions.update(" + key + "," + update + ",true)");
179 
180             if (activateAfterSave)
181                 session.didActivate();
182 
183             return version;
184         }
185         catch (Exception e)
186         {
187             Log.warn(e);
188         }
189         return null;
190     }
191 
192     /*------------------------------------------------------------ */
193     @Override
194     protected Object refresh(NoSqlSession session, Object version)
195     {
196         __log.debug("MongoSessionManager:refresh " + session);
197 
198         // check if our in memory version is the same as what is on the disk
199         if (version != null)
200         {
201             DBObject o = _sessions.findOne(new BasicDBObject(__ID,session.getClusterId()),__version_1);
202 
203             if (o != null)
204             {
205                 Object saved = getNestedValue(o, getContextKey(__VERSION));
206                 
207                 if (saved != null && saved.equals(version))
208                 {
209                     __log.debug("MongoSessionManager:refresh not needed");
210                     return version;
211                 }
212                 version = saved;
213             }
214         }
215 
216         // If we are here, we have to load the object
217         DBObject o = _sessions.findOne(new BasicDBObject(__ID,session.getClusterId()));
218 
219         // If it doesn't exist, invalidate
220         if (o == null)
221         {
222             __log.debug("MongoSessionManager:refresh:marking invalid, no object");
223             session.invalidate();
224             return null;
225         }
226         
227         // If it has been flagged invalid, invalidate
228         Boolean valid = (Boolean)o.get(__VALID);
229         if (valid == null || !valid)
230         {
231             __log.debug("MongoSessionManager:refresh:marking invalid, valid flag " + valid);
232             session.invalidate();
233             return null;
234         }
235 
236         // We need to update the attributes. We will model this as a passivate,
237         // followed by bindings and then activation.
238         session.willPassivate();
239         try
240         {
241             session.clearAttributes();
242             
243             DBObject attrs = (DBObject)getNestedValue(o,getContextKey());
244             
245             if (attrs != null)
246             {
247                 for (String name : attrs.keySet())
248                 {
249                     if ( __METADATA.equals(name) )
250                     {
251                         continue;
252                     }
253                     
254                     String attr = decodeName(name);
255                     Object value = decodeValue(attrs.get(name));
256                     session.doPutOrRemove(attr,value);
257                     session.bindValue(attr,value);
258                 }
259             }
260 
261             session.didActivate();
262             
263             
264             return version;
265         }
266         catch (Exception e)
267         {
268             Log.warn(e);
269         }
270 
271         return null;
272     }
273 
274     /*------------------------------------------------------------ */
275     @Override
276     protected synchronized NoSqlSession loadSession(String clusterId)
277     {
278         DBObject o = _sessions.findOne(new BasicDBObject(__ID,clusterId));
279         
280         __log.debug("MongoSessionManager:loaded " + o);
281         
282         if (o == null)
283         {
284             return null;
285         }
286         
287         Boolean valid = (Boolean)o.get(__VALID);
288         if (valid == null || !valid)
289         {
290             return null;
291         }
292         
293         try
294         {
295             Object version = o.get(getContextKey(__VERSION));
296             Long created = (Long)o.get(__CREATED);
297             Long accessed = (Long)o.get(__ACCESSED);
298           
299             NoSqlSession session = new NoSqlSession(this,created,accessed,clusterId,version);
300 
301             // get the attributes for the context
302             DBObject attrs = (DBObject)getNestedValue(o,getContextKey());
303 
304             __log.debug("MongoSessionManager:attrs: " + attrs);
305             if (attrs != null)
306             {
307                 for (String name : attrs.keySet())
308                 {
309                     if ( __METADATA.equals(name) )
310                     {
311                         continue;
312                     }
313                     
314                     String attr = decodeName(name);
315                     Object value = decodeValue(attrs.get(name));
316 
317                     session.doPutOrRemove(attr,value);
318                     session.bindValue(attr,value);
319                     
320                 }
321             }
322             session.didActivate();
323 
324             return session;
325         }
326         catch (Exception e)
327         {
328             Log.warn(e);
329         }
330         return null;
331     }
332 
333     /*------------------------------------------------------------ */
334     @Override
335     protected boolean remove(NoSqlSession session)
336     {
337         __log.debug("MongoSessionManager:remove:session " + session.getClusterId());
338 
339         /*
340          * Check if the session exists and if it does remove the context
341          * associated with this session
342          */
343         BasicDBObject key = new BasicDBObject(__ID,session.getClusterId());
344         
345         DBObject o = _sessions.findOne(key,__version_1);
346 
347         if (o != null)
348         {
349             BasicDBObject remove = new BasicDBObject();
350             BasicDBObject unsets = new BasicDBObject();
351             unsets.put(getContextKey(),1);
352             remove.put("$unsets",unsets);
353             _sessions.update(key,remove);
354 
355             return true;
356         }
357         else
358         {
359             return false;
360         }
361     }
362 
363     /*------------------------------------------------------------ */
364     @Override
365     protected void invalidateSession(String idInCluster)
366     {
367         __log.debug("MongoSessionManager:invalidateSession:invalidating " + idInCluster);
368         
369         super.invalidateSession(idInCluster);
370         
371         /*
372          * pull back the 'valid' value, we can check if its false, if is we don't need to
373          * reset it to false
374          */
375         DBObject validKey = new BasicDBObject(__VALID, true);       
376         DBObject o = _sessions.findOne(new BasicDBObject(__ID,idInCluster), validKey);
377         
378         if (o != null && (Boolean)o.get(__VALID))
379         {
380             BasicDBObject update = new BasicDBObject();
381             BasicDBObject sets = new BasicDBObject();
382             sets.put(__VALID,false);
383             sets.put(__INVALIDATED, System.currentTimeMillis());
384             update.put("$set",sets);
385                         
386             BasicDBObject key = new BasicDBObject(__ID,idInCluster);
387 
388             _sessions.update(key,update);
389         }       
390     }
391     
392     /*------------------------------------------------------------ */
393     protected String encodeName(String name)
394     {
395         return name.replace("%","%25").replace(".","%2E");
396     }
397 
398     /*------------------------------------------------------------ */
399     protected String decodeName(String name)
400     {
401         return name.replace("%2E",".").replace("%25","%");
402     }
403 
404     /*------------------------------------------------------------ */
405     protected Object encodeName(ObjectOutputStream out, ByteArrayOutputStream bout, Object value) throws IOException
406     {
407         if (value instanceof Number || value instanceof String || value instanceof Boolean || value instanceof Date)
408         {
409             return value;
410         }
411         else if (value.getClass().equals(HashMap.class))
412         {
413             BasicDBObject o = new BasicDBObject();
414             for (Map.Entry<?, ?> entry : ((Map<?, ?>)value).entrySet())
415             {
416                 if (!(entry.getKey() instanceof String))
417                 {
418                     o = null;
419                     break;
420                 }
421                 o.append(encodeName(entry.getKey().toString()),encodeName(out,bout,value));
422             }
423 
424             if (o != null)
425                 return o;
426         }
427 
428         bout.reset();
429         out.reset();
430         out.writeUnshared(value);
431         out.flush();
432         return bout.toByteArray();
433     }
434 
435     /*------------------------------------------------------------ */
436     protected Object decodeValue(Object value) throws IOException, ClassNotFoundException
437     {
438         if (value == null || value instanceof Number || value instanceof String || value instanceof Boolean || value instanceof Date)
439         {
440             return value;
441         }
442         else if (value instanceof byte[])
443         {
444             ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream((byte[])value));
445             return in.readObject();
446         }
447         else if (value instanceof DBObject)
448         {
449             Map<String, Object> map = new HashMap<String, Object>();
450             for (String name : ((DBObject)value).keySet())
451             {
452                 String attr = decodeName(name);
453                 map.put(attr,decodeValue(((DBObject)value).get(name)));
454             }
455             return map;
456         }
457         else
458         {
459             throw new IllegalStateException(value.getClass().toString());
460         }
461     }
462 
463    
464     /*------------------------------------------------------------ */
465     private String getContextKey()
466     {
467     	return __CONTEXT + "." + _contextId;
468     }
469     
470     /*------------------------------------------------------------ */
471     private String getContextKey(String keybit)
472     {
473     	return __CONTEXT + "." + _contextId + "." + keybit;
474     }
475     
476     public void purge()
477     {   
478         ((MongoSessionIdManager)_sessionIdManager).purge();
479     }
480     
481     public void purgeFully()
482     {   
483         ((MongoSessionIdManager)_sessionIdManager).purgeFully();
484     }
485     
486     public void scavenge()
487     {
488         ((MongoSessionIdManager)_sessionIdManager).scavenge();
489     }
490     
491     public void scavengeFully()
492     {
493         ((MongoSessionIdManager)_sessionIdManager).scavengeFully();
494     }
495     
496     /*------------------------------------------------------------ */
497     /**
498      * returns the total number of session objects in the session store
499      * 
500      * the count() operation itself is optimized to perform on the server side
501      * and avoid loading to client side.
502      */
503     public long getSessionStoreCount()
504     {
505         return _sessions.find().count();      
506     }
507     
508     /*------------------------------------------------------------ */
509     /**
510      * MongoDB keys are . delimited for nesting so .'s are protected characters
511      * 
512      * @param virtualHosts
513      * @param contextPath
514      * @return
515      */
516     private String createContextId(String[] virtualHosts, String contextPath)
517     {
518         String contextId = virtualHosts[0] + contextPath;
519         
520         contextId.replace('/', '_');
521         contextId.replace('.','_');
522         contextId.replace('\\','_');
523         
524         return contextId;
525     }
526 
527     /**
528      * Dig through a given dbObject for the nested value
529      */
530     private Object getNestedValue(DBObject dbObject, String nestedKey)
531     {
532         String[] keyChain = nestedKey.split("\\.");
533 
534         DBObject temp = dbObject;
535 
536         for (int i = 0; i < keyChain.length - 1; ++i)
537         {
538             temp = (DBObject)temp.get(keyChain[i]);
539             
540             if ( temp == null )
541             {
542                 return null;
543             }
544         }
545 
546         return temp.get(keyChain[keyChain.length - 1]);
547     }
548 
549 }