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 new or existing
141             if (version == null)
142             {
143                 // New session
144                 upsert = true;
145                 version = new Long(1);
146                 sets.put(__CREATED,session.getCreationTime());
147                 sets.put(__VALID,true);
148                 sets.put(getContextKey(__VERSION),version);
149             }
150             else
151             {
152                 version = new Long(((Long)version).intValue() + 1);
153                 update.put("$inc",__version_1); 
154             }
155 
156             // handle valid or invalid
157             if (session.isValid())
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             if (attrs != null)
257             {
258                 for (String name : attrs.keySet())
259                 {
260                     if (__METADATA.equals(name))
261                     {
262                         continue;
263                     }
264 
265                     String attr = decodeName(name);
266                     Object value = decodeValue(attrs.get(name));
267 
268                     if (attrs.keySet().contains(name))
269                     {
270                         session.doPutOrRemove(attr,value);
271                         session.bindValue(attr,value);
272                     }
273                     else
274                     {
275                         session.doPutOrRemove(attr,value);
276                     }
277                 }
278                 // cleanup, remove values from session, that don't exist in data anymore:
279                 for (String name : session.getNames())
280                 {
281                     if (!attrs.keySet().contains(name))
282                     {
283                         session.doPutOrRemove(name,null);
284                         session.unbindValue(name,session.getAttribute(name));
285                     }
286                 }
287             }
288 
289             session.didActivate();
290 
291             return version;
292         }
293         catch (Exception e)
294         {
295             LOG.warn(e);
296         }
297 
298         return null;
299     }
300 
301     /*------------------------------------------------------------ */
302     @Override
303     protected synchronized NoSqlSession loadSession(String clusterId)
304     {
305         DBObject o = _sessions.findOne(new BasicDBObject(__ID,clusterId));
306         
307         __log.debug("MongoSessionManager:loaded " + o);
308         
309         if (o == null)
310         {
311             return null;
312         }
313         
314         Boolean valid = (Boolean)o.get(__VALID);
315         if (valid == null || !valid)
316         {
317             return null;
318         }
319         
320         try
321         {
322             Object version = o.get(getContextKey(__VERSION));
323             Long created = (Long)o.get(__CREATED);
324             Long accessed = (Long)o.get(__ACCESSED);
325           
326             NoSqlSession session = new NoSqlSession(this,created,accessed,clusterId,version);
327 
328             // get the attributes for the context
329             DBObject attrs = (DBObject)getNestedValue(o,getContextKey());
330 
331             __log.debug("MongoSessionManager:attrs: " + attrs);
332             if (attrs != null)
333             {
334                 for (String name : attrs.keySet())
335                 {
336                     if ( __METADATA.equals(name) )
337                     {
338                         continue;
339                     }
340                     
341                     String attr = decodeName(name);
342                     Object value = decodeValue(attrs.get(name));
343 
344                     session.doPutOrRemove(attr,value);
345                     session.bindValue(attr,value);
346                     
347                 }
348             }
349             session.didActivate();
350 
351             return session;
352         }
353         catch (Exception e)
354         {
355             LOG.warn(e);
356         }
357         return null;
358     }
359 
360     /*------------------------------------------------------------ */
361     @Override
362     protected boolean remove(NoSqlSession session)
363     {
364         __log.debug("MongoSessionManager:remove:session " + session.getClusterId());
365 
366         /*
367          * Check if the session exists and if it does remove the context
368          * associated with this session
369          */
370         BasicDBObject key = new BasicDBObject(__ID,session.getClusterId());
371         
372         DBObject o = _sessions.findOne(key,__version_1);
373 
374         if (o != null)
375         {
376             BasicDBObject remove = new BasicDBObject();
377             BasicDBObject unsets = new BasicDBObject();
378             unsets.put(getContextKey(),1);
379             remove.put("$unset",unsets);
380             _sessions.update(key,remove);
381 
382             return true;
383         }
384         else
385         {
386             return false;
387         }
388     }
389 
390     /*------------------------------------------------------------ */
391     @Override
392     protected void invalidateSession(String idInCluster)
393     {
394         __log.debug("MongoSessionManager:invalidateSession:invalidating " + idInCluster);
395         
396         super.invalidateSession(idInCluster);
397         
398         /*
399          * pull back the 'valid' value, we can check if its false, if is we don't need to
400          * reset it to false
401          */
402         DBObject validKey = new BasicDBObject(__VALID, true);       
403         DBObject o = _sessions.findOne(new BasicDBObject(__ID,idInCluster), validKey);
404         
405         if (o != null && (Boolean)o.get(__VALID))
406         {
407             BasicDBObject update = new BasicDBObject();
408             BasicDBObject sets = new BasicDBObject();
409             sets.put(__VALID,false);
410             sets.put(__INVALIDATED, System.currentTimeMillis());
411             update.put("$set",sets);
412                         
413             BasicDBObject key = new BasicDBObject(__ID,idInCluster);
414 
415             _sessions.update(key,update);
416         }       
417     }
418     
419     /*------------------------------------------------------------ */
420     @Override
421     protected void update(NoSqlSession session, String newClusterId, String newNodeId) throws Exception
422     {
423         // Form query for update - use object's existing session id
424         BasicDBObject key = new BasicDBObject(__ID, session.getClusterId());
425         BasicDBObject sets = new BasicDBObject();
426         BasicDBObject update = new BasicDBObject(__ID, newClusterId);
427         sets.put("$set", update);
428         _sessions.update(key, sets, false, false);
429     }
430 
431     /*------------------------------------------------------------ */
432     protected String encodeName(String name)
433     {
434         return name.replace("%","%25").replace(".","%2E");
435     }
436 
437     /*------------------------------------------------------------ */
438     protected String decodeName(String name)
439     {
440         return name.replace("%2E",".").replace("%25","%");
441     }
442 
443     /*------------------------------------------------------------ */
444     protected Object encodeName(Object value) throws IOException
445     {
446         if (value instanceof Number || value instanceof String || value instanceof Boolean || value instanceof Date)
447         {
448             return value;
449         }
450         else if (value.getClass().equals(HashMap.class))
451         {
452             BasicDBObject o = new BasicDBObject();
453             for (Map.Entry<?, ?> entry : ((Map<?, ?>)value).entrySet())
454             {
455                 if (!(entry.getKey() instanceof String))
456                 {
457                     o = null;
458                     break;
459                 }
460                 o.append(encodeName(entry.getKey().toString()),encodeName(entry.getValue()));
461             }
462 
463             if (o != null)
464                 return o;
465         }
466         
467         ByteArrayOutputStream bout = new ByteArrayOutputStream();
468         ObjectOutputStream out = new ObjectOutputStream(bout);
469         out.reset();
470         out.writeUnshared(value);
471         out.flush();
472         return bout.toByteArray();
473     }
474 
475     /*------------------------------------------------------------ */
476     protected Object decodeValue(final Object valueToDecode) throws IOException, ClassNotFoundException
477     {
478         if (valueToDecode == null || valueToDecode instanceof Number || valueToDecode instanceof String || valueToDecode instanceof Boolean || valueToDecode instanceof Date)
479         {
480             return valueToDecode;
481         }
482         else if (valueToDecode instanceof byte[])
483         {
484             final byte[] decodeObject = (byte[])valueToDecode;
485             final ByteArrayInputStream bais = new ByteArrayInputStream(decodeObject);
486             final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais);
487             return objectInputStream.readUnshared();
488         }
489         else if (valueToDecode instanceof DBObject)
490         {
491             Map<String, Object> map = new HashMap<String, Object>();
492             for (String name : ((DBObject)valueToDecode).keySet())
493             {
494                 String attr = decodeName(name);
495                 map.put(attr,decodeValue(((DBObject)valueToDecode).get(name)));
496             }
497             return map;
498         }
499         else
500         {
501             throw new IllegalStateException(valueToDecode.getClass().toString());
502         }
503     }
504 
505    
506     /*------------------------------------------------------------ */
507     private String getContextKey()
508     {
509     	return __CONTEXT + "." + _contextId;
510     }
511     
512     /*------------------------------------------------------------ */
513     private String getContextKey(String keybit)
514     {
515     	return __CONTEXT + "." + _contextId + "." + keybit;
516     }
517     
518     @ManagedOperation(value="purge invalid sessions in the session store based on normal criteria", impact="ACTION")
519     public void purge()
520     {   
521         ((MongoSessionIdManager)_sessionIdManager).purge();
522     }
523     
524     
525     @ManagedOperation(value="full purge of invalid sessions in the session store", impact="ACTION")
526     public void purgeFully()
527     {   
528         ((MongoSessionIdManager)_sessionIdManager).purgeFully();
529     }
530     
531     @ManagedOperation(value="scavenge sessions known to this manager", impact="ACTION")
532     public void scavenge()
533     {
534         ((MongoSessionIdManager)_sessionIdManager).scavenge();
535     }
536     
537     @ManagedOperation(value="scanvenge all sessions", impact="ACTION")
538     public void scavengeFully()
539     {
540         ((MongoSessionIdManager)_sessionIdManager).scavengeFully();
541     }
542     
543     /*------------------------------------------------------------ */
544     /**
545      * returns the total number of session objects in the session store
546      * 
547      * the count() operation itself is optimized to perform on the server side
548      * and avoid loading to client side.
549      */
550     @ManagedAttribute("total number of known sessions in the store")
551     public long getSessionStoreCount()
552     {
553         return _sessions.find().count();      
554     }
555     
556     /*------------------------------------------------------------ */
557     /**
558      * MongoDB keys are . delimited for nesting so .'s are protected characters
559      * 
560      * @param virtualHosts
561      * @param contextPath
562      * @return
563      */
564     private String createContextId(String[] virtualHosts, String contextPath)
565     {
566         String contextId = virtualHosts[0] + contextPath;
567         
568         contextId.replace('/', '_');
569         contextId.replace('.','_');
570         contextId.replace('\\','_');
571         
572         return contextId;
573     }
574 
575     /**
576      * Dig through a given dbObject for the nested value
577      */
578     private Object getNestedValue(DBObject dbObject, String nestedKey)
579     {
580         String[] keyChain = nestedKey.split("\\.");
581 
582         DBObject temp = dbObject;
583 
584         for (int i = 0; i < keyChain.length - 1; ++i)
585         {
586             temp = (DBObject)temp.get(keyChain[i]);
587             
588             if ( temp == null )
589             {
590                 return null;
591             }
592         }
593 
594         return temp.get(keyChain[keyChain.length - 1]);
595     }
596 
597     
598      /**
599      * ClassLoadingObjectInputStream
600      *
601      *
602      */
603     protected class ClassLoadingObjectInputStream extends ObjectInputStream
604     {
605         public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
606         {
607             super(in);
608         }
609 
610         public ClassLoadingObjectInputStream () throws IOException
611         {
612             super();
613         }
614 
615         @Override
616         public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
617         {
618             try
619             {
620                 return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
621             }
622             catch (ClassNotFoundException e)
623             {
624                 return super.resolveClass(cl);
625             }
626         }
627     }
628 
629 
630 }