View Javadoc

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