View Javadoc

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