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