1   // ========================================================================
2   // Copyright 1996-2005 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.jetty.servlet;
16  
17  import java.io.DataInputStream;
18  import java.io.DataOutputStream;
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.ObjectInputStream;
25  import java.io.ObjectOutputStream;
26  import java.io.OutputStream;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.Map;
32  import java.util.Timer;
33  import java.util.TimerTask;
34  
35  import java.util.concurrent.ConcurrentHashMap;
36  
37  import javax.servlet.http.HttpServletRequest;
38  
39  import org.mortbay.log.Log;
40  import org.mortbay.util.LazyList;
41  
42  
43  /* ------------------------------------------------------------ */
44  /** An in-memory implementation of SessionManager.
45   *
46   * @author Greg Wilkins (gregw)
47   */
48  public class HashSessionManager extends AbstractSessionManager
49  {
50      private static int __id;
51      private Timer _timer;
52      private TimerTask _task;
53      private int _scavengePeriodMs=30000;
54      private int _savePeriodMs=0; //don't do period saves by default
55      private TimerTask _saveTask;
56      protected Map _sessions;
57      private File _storeDir;
58      
59      /* ------------------------------------------------------------ */
60      public HashSessionManager()
61      {
62          super();
63      }
64  
65      /* ------------------------------------------------------------ */
66      /* (non-Javadoc)
67       * @see org.mortbay.jetty.servlet.AbstractSessionManager#doStart()
68       */
69      public void doStart() throws Exception
70      {
71          _sessions=new ConcurrentHashMap(); // TODO: use syncronizedMap for JDK 1.4
72          super.doStart();
73  
74          _timer=new Timer("HashSessionScavenger-"+__id++, true);
75          
76          setScavengePeriod(getScavengePeriod());
77  
78          if (_storeDir!=null)
79          {
80              if (!_storeDir.exists())
81                  _storeDir.mkdir();
82              restoreSessions();
83          }
84   
85          setSavePeriod(getSavePeriod());
86      }
87  
88      /* ------------------------------------------------------------ */
89      /* (non-Javadoc)
90       * @see org.mortbay.jetty.servlet.AbstractSessionManager#doStop()
91       */
92      public void doStop() throws Exception
93      {
94          
95          if (_storeDir != null)
96              saveSessions();
97          
98          super.doStop();
99   
100         _sessions.clear();
101         _sessions=null;
102 
103         // stop the scavenger
104         synchronized(this)
105         {
106             if (_saveTask!=null)
107                 _saveTask.cancel();
108             if (_task!=null)
109                 _task.cancel();
110             if (_timer!=null)
111                 _timer.cancel();
112             _timer=null;
113         }
114     }
115 
116     /* ------------------------------------------------------------ */
117     /**
118      * @return seconds
119      */
120     public int getScavengePeriod()
121     {
122         return _scavengePeriodMs/1000;
123     }
124 
125     
126     /* ------------------------------------------------------------ */
127     public Map getSessionMap()
128     {
129         return Collections.unmodifiableMap(_sessions);
130     }
131 
132 
133     /* ------------------------------------------------------------ */
134     public int getSessions()
135     {
136         return _sessions.size();
137     }
138 
139 
140     /* ------------------------------------------------------------ */
141     public void setMaxInactiveInterval(int seconds)
142     {
143         super.setMaxInactiveInterval(seconds);
144         if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000)
145             setScavengePeriod((_dftMaxIdleSecs+9)/10);
146     }
147 
148     /* ------------------------------------------------------------ */
149     public void setSavePeriod (int seconds)
150     {
151         int oldSavePeriod = _savePeriodMs;
152         int period = (seconds * 1000);
153         if (period < 0)
154             period=0;
155         _savePeriodMs=period;
156         
157         if (_timer!=null)
158         {
159             synchronized (this)
160             {
161                 if (_saveTask!=null)
162                     _saveTask.cancel();
163                 if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured
164                 {
165                     _saveTask = new TimerTask()
166                     {
167                         public void run()
168                         {
169                             try
170                             {
171                                 saveSessions();
172                             }
173                             catch (Exception e)
174                             {
175                                 Log.warn(e);
176                             }
177                         }   
178                     };
179                     _timer.schedule(_saveTask,_savePeriodMs,_savePeriodMs);
180                 }
181             }
182         }
183     }
184 
185     /* ------------------------------------------------------------ */
186     public int getSavePeriod ()
187     {
188         if (_savePeriodMs<=0)
189             return 0;
190         
191         return _savePeriodMs/1000;
192     }
193     
194     /* ------------------------------------------------------------ */
195     /**
196      * @param seconds
197      */
198     public void setScavengePeriod(int seconds)
199     {
200         if (seconds==0)
201             seconds=60;
202 
203         int old_period=_scavengePeriodMs;
204         int period=seconds*1000;
205         if (period>60000)
206             period=60000;
207         if (period<1000)
208             period=1000;
209 
210         _scavengePeriodMs=period;
211         if (_timer!=null && (period!=old_period || _task==null))
212         {
213             synchronized (this)
214             {
215                 if (_task!=null)
216                     _task.cancel();
217                 _task = new TimerTask()
218                 {
219                     public void run()
220                     {
221                         scavenge();
222                     }   
223                 };
224                 _timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs);
225             }
226         }
227     }
228     
229     /* -------------------------------------------------------------- */
230     /**
231      * Find sessions that have timed out and invalidate them. This runs in the
232      * SessionScavenger thread.
233      */
234     private void scavenge()
235     {
236         //don't attempt to scavenge if we are shutting down
237         if (isStopping() || isStopped())
238             return;
239         
240         Thread thread=Thread.currentThread();
241         ClassLoader old_loader=thread.getContextClassLoader();
242         try
243         {
244             if (_loader!=null)
245                 thread.setContextClassLoader(_loader);
246 
247             long now=System.currentTimeMillis();
248 
249             // Since Hashtable enumeration is not safe over deletes,
250             // we build a list of stale sessions, then go back and invalidate
251             // them
252             Object stale=null;
253 
254             synchronized (HashSessionManager.this)
255             {
256                 // For each session
257                 for (Iterator i=_sessions.values().iterator(); i.hasNext();)
258                 {
259                     Session session=(Session)i.next();
260                     long idleTime=session._maxIdleMs;
261                     if (idleTime>0&&session._accessed+idleTime<now)
262                     {
263                         // Found a stale session, add it to the list
264                         stale=LazyList.add(stale,session);
265                     }
266                 }
267             }
268 
269             // Remove the stale sessions
270             for (int i=LazyList.size(stale); i-->0;)
271             {
272                 // check it has not been accessed in the meantime
273                 Session session=(Session)LazyList.get(stale,i);
274                 long idleTime=session._maxIdleMs;
275                 if (idleTime>0&&session._accessed+idleTime<System.currentTimeMillis())
276                 {
277                     ((Session)session).timeout();
278                     int nbsess=this._sessions.size();
279                     if (nbsess<this._minSessions)
280                         this._minSessions=nbsess;
281                 }
282             }
283         }
284         catch (Throwable t)
285         {
286             if (t instanceof ThreadDeath)
287                 throw ((ThreadDeath)t);
288             else
289                 Log.warn("Problem scavenging sessions", t);
290         }
291         finally
292         {
293             thread.setContextClassLoader(old_loader);
294         }
295     }
296     
297     /* ------------------------------------------------------------ */
298     protected void addSession(AbstractSessionManager.Session session)
299     {
300         _sessions.put(session.getClusterId(),session);
301     }
302     
303     /* ------------------------------------------------------------ */
304     public AbstractSessionManager.Session getSession(String idInCluster)
305     {
306         if (_sessions==null)
307             return null;
308 
309         return (Session)_sessions.get(idInCluster);
310     }
311 
312     /* ------------------------------------------------------------ */
313     protected void invalidateSessions()
314     {
315         // Invalidate all sessions to cause unbind events
316         ArrayList sessions=new ArrayList(_sessions.values());
317         for (Iterator i=sessions.iterator(); i.hasNext();)
318         {
319             Session session=(Session)i.next();
320             session.invalidate();
321         }
322         _sessions.clear();
323         
324     }
325 
326     /* ------------------------------------------------------------ */
327     protected AbstractSessionManager.Session newSession(HttpServletRequest request)
328     {
329         return new Session(request);
330     }
331     
332     /* ------------------------------------------------------------ */
333     protected AbstractSessionManager.Session newSession(long created, String clusterId)
334     {
335         return new Session(created,clusterId);
336     }
337     
338     /* ------------------------------------------------------------ */
339     protected void removeSession(String clusterId)
340     {
341         _sessions.remove(clusterId);
342     }
343     
344 
345     /* ------------------------------------------------------------ */
346     public void setStoreDirectory (File dir)
347     {
348         _storeDir=dir;
349     }
350 
351     /* ------------------------------------------------------------ */
352     public File getStoreDirectory ()
353     {
354         return _storeDir;
355     }
356 
357     /* ------------------------------------------------------------ */
358     public void restoreSessions () throws Exception
359     {
360         if (_storeDir==null || !_storeDir.exists())
361         {
362             return;
363         }
364 
365         if (!_storeDir.canRead())
366         {
367             Log.warn ("Unable to restore Sessions: Cannot read from Session storage directory "+_storeDir.getAbsolutePath());
368             return;
369         }
370 
371         File[] files = _storeDir.listFiles();
372         for (int i=0;files!=null&&i<files.length;i++)
373         {
374             try
375             {
376                 FileInputStream in = new FileInputStream(files[i]);           
377                 Session session = restoreSession(in);
378                 in.close();          
379                 addSession(session, false);
380                 files[i].delete();
381             }
382             catch (Exception e)
383             {
384                 Log.warn("Problem restoring session "+files[i].getName(), e);
385             }
386         }
387     }
388 
389     /* ------------------------------------------------------------ */
390     public void saveSessions () throws Exception
391     {
392         if (_storeDir==null || !_storeDir.exists())
393         {
394             return;
395         }
396         
397         if (!_storeDir.canWrite())
398         {
399             Log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable");
400             return;
401         }
402  
403         synchronized (this)
404         {
405             Iterator itor = _sessions.entrySet().iterator();
406             while (itor.hasNext())
407             {
408                 Map.Entry entry = (Map.Entry)itor.next();
409                 String id = (String)entry.getKey();
410                 Session session = (Session)entry.getValue();
411                 try
412                 {
413                     File file = new File (_storeDir, id);
414                     if (file.exists())
415                         file.delete();
416                     file.createNewFile();
417                     FileOutputStream fos = new FileOutputStream (file);
418                     session.save(fos);
419                     fos.close();
420                 }
421                 catch (Exception e)
422                 {
423                     Log.warn("Problem persisting session "+id, e);
424                 }
425             }
426         }
427     }
428 
429     /* ------------------------------------------------------------ */
430     public Session restoreSession (InputStream is) throws Exception
431     {
432         /*
433          * Take care of this class's fields first by calling 
434          * defaultReadObject
435          */
436         DataInputStream in = new DataInputStream(is);
437         String clusterId = in.readUTF();
438         String nodeId = in.readUTF();
439         boolean idChanged = in.readBoolean();
440         long created = in.readLong();
441         long cookieSet = in.readLong();
442         long accessed = in.readLong();
443         long lastAccessed = in.readLong();
444         //boolean invalid = in.readBoolean();
445         //boolean invalidate = in.readBoolean();
446         //long maxIdle = in.readLong();
447         //boolean isNew = in.readBoolean();
448         int requests = in.readInt();
449         
450         Session session = (Session)newSession(created, clusterId);
451         session._cookieSet = cookieSet;
452         session._lastAccessed = lastAccessed;
453         
454         int size = in.readInt();
455         if (size > 0)
456         {
457             ArrayList keys = new ArrayList();
458             for (int i=0; i<size; i++)
459             {
460                 String key = in.readUTF();
461                 keys.add(key);
462             }
463             ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in);
464             for (int i=0;i<size;i++)
465             {
466                 Object value = ois.readObject();
467                 session.setAttribute((String)keys.get(i),value);
468             }
469             ois.close();
470         }
471         else
472             session.initValues();
473         in.close();
474         return session;
475     }
476 
477     
478     /* ------------------------------------------------------------ */
479     /* ------------------------------------------------------------ */
480     /* ------------------------------------------------------------ */
481     protected class Session extends AbstractSessionManager.Session
482     {
483         /* ------------------------------------------------------------ */
484         private static final long serialVersionUID=-2134521374206116367L;
485         
486         /* ------------------------------------------------------------- */
487         protected Session(HttpServletRequest request)
488         {
489             super(request);
490         }
491 
492         /* ------------------------------------------------------------- */
493         protected Session(long created, String clusterId)
494         {
495             super(created, clusterId);
496         }
497         
498         /* ------------------------------------------------------------- */
499         public void setMaxInactiveInterval(int secs)
500         {
501             super.setMaxInactiveInterval(secs);
502             if (_maxIdleMs>0&&(_maxIdleMs/10)<_scavengePeriodMs)
503                 HashSessionManager.this.setScavengePeriod((secs+9)/10);
504         }
505         
506         /* ------------------------------------------------------------ */
507         protected Map newAttributeMap()
508         {
509             return new HashMap(3);
510         }
511         
512 
513         /* ------------------------------------------------------------ */
514         public void invalidate ()
515         throws IllegalStateException
516         {
517             super.invalidate();
518             
519             remove();
520         }
521 
522         /* ------------------------------------------------------------ */
523         public void remove()
524         {
525             String id=getId();
526             if (id==null)
527                 return;
528             
529             //all sessions are invalidated when jetty is stopped, make sure we don't
530             //remove all the sessions in this case
531             if (isStopping() || isStopped())
532                 return;
533             
534             if (_storeDir==null || !_storeDir.exists())
535             {
536                 return;
537             }
538             
539             File f = new File(_storeDir, id);
540             f.delete();
541         }
542 
543         /* ------------------------------------------------------------ */
544         public void save(OutputStream os)  throws IOException 
545         {
546             DataOutputStream out = new DataOutputStream(os);
547             out.writeUTF(_clusterId);
548             out.writeUTF(_nodeId);
549             out.writeBoolean(_idChanged);
550             out.writeLong( _created);
551             out.writeLong(_cookieSet);
552             out.writeLong(_accessed);
553             out.writeLong(_lastAccessed);
554             /* Don't write these out, as they don't make sense to store because they
555              * either they cannot be true or their value will be restored in the 
556              * Session constructor.
557              */
558             //out.writeBoolean(_invalid);
559             //out.writeBoolean(_doInvalidate);
560             //out.writeLong(_maxIdleMs);
561             //out.writeBoolean( _newSession);
562             out.writeInt(_requests);
563             if (_values != null)
564             {
565                 out.writeInt(_values.size());
566                 Iterator itor = _values.keySet().iterator();
567                 while (itor.hasNext())
568                 {
569                     String key = (String)itor.next();
570                     out.writeUTF(key);
571                 }
572                 itor = _values.values().iterator();
573                 ObjectOutputStream oos = new ObjectOutputStream(out);
574                 while (itor.hasNext())
575                 {
576                     oos.writeObject(itor.next());
577                 }
578                 oos.close();
579             }
580             else
581                 out.writeInt(0);
582             out.close();
583         }
584         
585     }
586 
587     /* ------------------------------------------------------------ */
588     /* ------------------------------------------------------------ */
589     protected class ClassLoadingObjectInputStream extends ObjectInputStream
590     {
591         /* ------------------------------------------------------------ */
592         public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
593         {
594             super(in);
595         }
596 
597         /* ------------------------------------------------------------ */
598         public ClassLoadingObjectInputStream () throws IOException
599         {
600             super();
601         }
602 
603         /* ------------------------------------------------------------ */
604         public Class resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
605         {
606             try
607             {
608                 return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
609             }
610             catch (ClassNotFoundException e)
611             {
612                 return super.resolveClass(cl);
613             }
614         }
615     }
616 
617     
618 }