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