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