001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002// The Molly framework is freely distributable under the terms of an
003// MIT-style license. For details, see the molly pages web site at:
004// http://www.mollypages.org/. Use, modify, have fun !
005
006package fc.web.servlet;
007
008import java.io.*;
009import java.sql.*;
010import java.util.*;
011import jakarta.servlet.*;
012import jakarta.servlet.http.*;
013
014import fc.io.*;
015import fc.jdbc.*;
016import fc.util.*;
017
018/** 
019Stores session data into a database. This approach is <i>almost</i>
020<u>always</u> the right thing to do. Don't store session data in the servlet
021container memory (memory backed sessions) and don't use serialization
022based session persistence as implemented by various session containers.
023Use database sessions for storing secure and/or large amounts of data 
024and store non-secure data directly in cookies.
025<p>
026Database sessions enable front-end webservers to scale easily. (No
027messy/brittle session migration hacks are required !). Scaling can be near
028infinite, but after a point (say 100 front ends), the back-end databases may
029also have to be partitioned (and the appropriate partition/machine invoked for
030a given session-id).
031<p>
032Note, memory sessions are not needed even for maintaining html form-state
033because each form's state can be uniquely created from an initial form
034state (common to all users) along with the per-usr data submitted per
035request. This is exactly how the {@link fc.web.forms.Form form} API works.
036However, memory sessions <u>are</u> useful for certain transient things/caches 
037or for very <i>quick'n'dirty</i> apps, where using a database is more trouble 
038than it's worth.
039<p>
040Given fast ethernet connections and forward marching processor/peripheral
041speeds, JDBC session data is typically retrieved on the order of 1-2
042milliseconds (or even lesser).
043<p>
044Using this class requires the following database table schema in
045the database:
046<pre>
047create table <b>sessiondata</b> ( 
048  session_id    varchar(50),
049  name         varchar(100) not null,
050  value        text  -- [or some other character/text type]
051  );
052  
053create table <b>sessionmaster</b> (
054  session_id    varchar(50) primary key,  -- the session id
055  created      timestamp not null,   -- session create time
056  accessed     timestamp not null,   -- session last accessed       
057  is_expired   bool default 'f',     -- true if expired 
058  -- a reference to the user table that associates a user_id
059  -- with one or more session_id's. Optional so should be 
060  -- nullable if used.
061  user_id    <span style="color:green">&lt;SQL_TYPE_FROM_YOUR_USER_TABLE&gt;</span>
062  );
063  
064alter table sessiondata add 
065   FOREIGN KEY (session_id) REFERENCES sessionmaster (session_id) 
066   on delete cascade;
067
068-- The following if a user table is present
069alter table sessionmaster add
070   FOREIGN KEY (user_id) REFERENCES <span style="color:green">
071    NAME_OF_USER_TABLE</span>;
072</pre>
073<p>
074When creating these tables, it is also advisable to <b>index</b> the
075<code>session_id</code> and <code>user_id</code> columns for faster performance.
076<p>
077The above tables can also be created by this class itself. Invoke the main
078method without any arguments to see usage information. The table names
079<span style="color:blue">sessionmaster</span> and <span style="color:blue">sessiondata</span>
080come from final static variables in this class. If you want to use different
081names, change these variables and recompile this class.
082<p>
083More than one [name, value] pair can be stored in the <span style="color:blue">sessiondata</span> table. This class will automatically store
084[name, value] pairs as seperate rows and can return a particular [name,
085value] pair or all [name, value] pairs for the specified session.
086<p>
087Note: This class allows saving data only as Strings/text. However,
088arbitrary binary data can also be stored but the caller should first
089base64 encode that data and then save it as a string. Upon retrieval, that
090data can be base64 decoded.
091<p>
092Note 2: Under some scenarios, it is <b>important</b> to have a separate
093cleaner process that periodically deletes expired sessions from the
094database. This process should run via cron, some other stand-alone java
095code etc., and should delete sessions which are marked expired or whose
096<code>create_time - last_used_time</code> is greater than the session expiry
097time. Under other scenarios, sessions may be deleted after a set amount of
098time from the creation data regardless of when the session was last
099accessed.
100<p>
101Note 3: Typically, expired session data is simply deleted from the session
102tables in the database. (for example, amazon.com users have to persist
103their cart manually by clicking on a "save-for-later" button - which moves
104data to more persistent tables -- otherwise their cart session is deleted
105from the database when the session expires). It is however possible that
106instead of deleting sessions, the sessions are instead marked as "expired"
107but not deleted from the database. (this is done via the {@link
108#setDeleteExpiredSessions} method.
109
110@author hursh jain
111*/
112public final class JDBCSession
113{
114Log log = Log.get("fc.web.servlet.JDBCSession");
115
116public static final String SESSIONDATA_TABLE  = "sessiondata";
117public static final String SESSIONMASTER_TABLE  = "sessionmaster";
118
119//queries
120String  data_insert;
121String  data_update;  
122String  data_get; 
123String  data_getall;   
124String  data_delete;
125String  delete_session;    
126String  expire_session; 
127String  new_session_with_user; 
128String  new_session; 
129String  tie_session_to_user; 
130String  session_exists;
131String  session_for_user;
132String  session_access_update;
133
134//by default expired sessions are deleted from the db. if this is set to false,
135//they are instead marked as expired in the db.
136boolean deleteExpiredSessions = true;
137
138//sessions eligible for removal after this time of inactivity.
139int   expireSeconds = 60*60*8; //8 hours default
140
141private static JDBCSession instance;
142private JDBCSession()
143  {
144  }
145
146/*
147Impl note: haven't used the fc.jdbc.dbo API to abstract our two database
148sesssion-tables, maybe later but for right now it's simpler (and
149more orthogonal) to handcode the sql for our prepared statements directly.
150*/
151
152/**
153Returns an instance of JDBCSession.
154*/
155public static JDBCSession getInstance() 
156  {
157  return init(null);
158  }
159
160
161/**
162Returns an instance of JDBCSession.
163
164@param  logger          the logging destination for methods in this class. Specify
165                <code>null</code> to use a default logger.
166*/
167public static JDBCSession getInstance(Log logger) 
168  {
169  return init(logger);
170  }
171
172private static JDBCSession init(Log logger)
173  {
174  if (instance != null) {
175    return instance;
176    }
177    
178  instance = new JDBCSession();
179  instance.configure(logger);
180  return instance;
181  }
182
183private void configure(Log logger)
184  {
185  if (logger != null)
186    log = logger;
187  
188  data_insert =
189   "insert into " + SESSIONDATA_TABLE +  
190    " (session_id, name, value) values (?, ?, ?) ";
191  
192  data_update =  
193   "update " + SESSIONDATA_TABLE + 
194    " set value=? where session_id=? and name=?";
195
196  data_get = 
197   "select sd.name, sd.value from " 
198    + SESSIONDATA_TABLE   + " as sd, "  
199    + SESSIONMASTER_TABLE + " as sma "
200    + " where sd.session_id=? "
201    + " and sd.name=? "
202    + " and sd.session_id = sma.session_id "
203    + " and sma.is_expired=false"; 
204      
205  data_getall =    
206   "select name, value from " + SESSIONDATA_TABLE +
207    " where session_id=?";
208
209  data_delete =
210   "delete from " + SESSIONDATA_TABLE +  
211    " where session_id=? and name=?";
212    
213  //cascades to sessiondata
214  delete_session =   
215   "delete from " + SESSIONMASTER_TABLE +  
216    " where session_id=? "; 
217
218  expire_session = 
219      "update " + SESSIONMASTER_TABLE +  " set is_expired=? where session_id=? "; 
220      
221  new_session_with_user = 
222   "insert into " + SESSIONMASTER_TABLE + 
223    " (session_id, created, accessed, username) values " 
224    + " (?, ?, ?, ?)";
225
226  new_session = 
227   "insert into " + SESSIONMASTER_TABLE + 
228    " (session_id, created, accessed) values " + 
229    " (?, ?, ?)";
230  
231  tie_session_to_user = 
232    "update " + SESSIONMASTER_TABLE + 
233      " set username=? where session_id=?";
234
235  session_exists =
236   "select session_id, created, accessed, is_expired from " 
237    + SESSIONMASTER_TABLE + " where session_id=?";
238    
239  session_for_user =
240   "select session_id, created, accessed, is_expired from " 
241    + SESSIONMASTER_TABLE + " where username=?";
242
243  session_access_update =
244    "update " + SESSIONMASTER_TABLE + 
245    " set accessed=? where session_id=?";
246    
247  }
248
249/**
250By default expired sessions are deleted from the db. If this is set to
251false, they are instead marked as expired in the db.
252*/
253public void setDeleteExpiredSessions(final boolean val)
254  {
255  deleteExpiredSessions = val;
256  }
257
258//session mgmt
259
260/**
261Creates a new session.
262<p>
263
264@param  con     a jdbc connection
265@param  sessionID value for sessionID, the {@link SessionUtil#newSessionID()}
266          can be used to generate this. Should not be null. It is
267          <b>not</b> recommended that the servlet container
268          generated <code>jsession_id</code> cookie be used for this
269          value. This sessionID is then later used to retrieve and
270          work with the created session and this sessionID will
271          typically be stored as a cookie or URL encoded on the
272          client.
273@param  userID    a userID to associate with this session. Note,
274          userID's in user tables are typically auto generated
275          numerical sequences. <b>Stringify numerical values before
276          passing them into this method</b>. This value should
277          <b>not</b> be <code>null</code>, otherwise a runtime
278          exception will be thrown.
279@throws SQLException 
280          if a SQL error occurs. Note, also thrown if the
281          session_id already exists in the database (since all
282          session_id's must be unique).
283*/
284public void create(
285  final Connection con, final String sessionID, final String userID) 
286throws SQLException 
287  {
288  Argcheck.notnull(sessionID, "sessionID arg was null");
289  Argcheck.notnull(userID, "userID argument was null");
290  
291  /*
292  insert into sessionmaster 
293  (session_id, created, accessed, username) values (?, ?, ?, ?)
294  */
295
296  final PreparedStatement ps = prepareStatement(con, new_session_with_user);
297  ps.setString(1, sessionID);
298
299  final long now = System.currentTimeMillis();
300  final java.sql.Timestamp ts = new java.sql.Timestamp(now);
301  ps.setTimestamp(2, ts);
302  ps.setTimestamp(3, ts);
303  ps.setString(4, userID);
304    
305  log.bug("Query to run: ", ps);
306  
307  final int n = ps.executeUpdate();
308  if (n != 1) {
309    log.warn("Query:", ps, "should have returned 1 but returned:", new Integer(n));
310    }
311  }
312
313/**
314Creates a new session. The specified session can later be optionally to a userID
315by invoking the {@link #tieToUser} method.
316<p>
317Note, sessionID's are typically be stored as a cookie or URL encoded on
318the client and are thus unique per browser/client). A user is not
319required to login to have session data.
320
321@param  con      a jdbc connection
322@param  sessionID  value for sessionID, the {@link SessionUtil#newSessionID()}
323           can be used to generate this. Should not be null. It is
324           <b>not</b> recommended that the servlet container
325           generated <code>jsession_id</code> cookie be used for this
326           value. This sessionID is then later used to retrieve and
327           work with the created session.
328@throws SQLException if a SQL error occurs. Note, also thrown if the
329           session_id already exists in the database (since all
330           session_id's must be unique).
331*/
332public void create(final Connection con, final String sessionID) 
333throws SQLException 
334  {
335  Argcheck.notnull(sessionID, "sessionID arg was null");
336
337  /*
338  insert into sessionmaster 
339  (session_id, created, accessed) values (?, ?, ?)
340  */
341
342  final PreparedStatement ps = prepareStatement(con, new_session);
343  ps.setString(1, sessionID);
344
345  final long now = System.currentTimeMillis();
346  final java.sql.Timestamp ts = new java.sql.Timestamp(now);
347  ps.setTimestamp(2, ts);
348  ps.setTimestamp(3, ts);
349    
350  log.bug("Query to run: ", ps);
351  
352  final int n = ps.executeUpdate();
353  if (n != 1) {
354    log.warn("Query:", ps, "should have returned 1 but returned:", new Integer(n));
355    }
356  }
357
358/**
359Expires the session. By default, deletes all session data associated with
360the specified sessionID from the database. If {@link
361#deleteExpiredSessions} is set to <code>true</code>, then the session is marked
362as expired in the database but the rows are not deleted from the db.
363<p>
364Either way, after this method returns, the sessionID will not longer be
365valid.
366*/
367public void expire(final Connection con, final String sessionID) 
368throws SQLException 
369  {
370  Argcheck.notnull(sessionID, "sessionID arg was null");
371
372  PreparedStatement ps = null;
373  
374  if (deleteExpiredSessions)
375    {
376    /* delete from sessionmaster where session_id=? */
377    ps = prepareStatement(con, delete_session);
378    ps.setString(1, sessionID);
379    }
380  else {
381     /* update sessionmaster set is_expired=?[=>true] where session_id=? */ 
382    ps = prepareStatement(con, expire_session);
383    ps.setBoolean(1, true);
384    ps.setString(2, sessionID);
385    }
386
387  log.bug("Query to run: ", ps);
388  
389  final int n = ps.executeUpdate();
390  if (n != 1) {
391    log.warn("Query:", ps, "should have returned 1 but returned: ", new Integer(n), " [This can happen if the sessionID did not exist in the database]");
392    } 
393  }
394
395
396/**
397Associates the specified sessionID with the specified userID. <p> Note:
398Depending on the application, more than 1 sessionID can be associated with
399the same userID in the session master table.
400*/
401public void tieToUser(
402  final Connection con, final String sessionID, final String userID) 
403throws SQLException
404  {
405  Argcheck.notnull(sessionID, "sessionID arg was null");
406  Argcheck.notnull(userID, "userID arg was null");
407  
408  /* update sessionmaster set username=? where session_id=? */  
409  
410  final PreparedStatement ps = prepareStatement(con, tie_session_to_user);
411  ps.setString(1, userID);
412  ps.setString(2, sessionID); 
413
414  log.bug("Query to run: ", ps);
415  final int n = ps.executeUpdate();
416  if (n != 1) {
417    log.warn("Query:", ps, " should have returned 1 but returned: ", new Integer(n), " [This can happen if the sessionID did not exist in the database]");
418    }
419  }
420
421
422/**
423Utility method that deletes (or marked as expired depending on {@link
424#setDeleteExpiredSessions}) all sessions in the database that have exceeded
425the maximum inactive time.
426*/
427public void expireInactiveSessions(final Connection con) 
428throws SQLException
429  {
430  long now = System.currentTimeMillis(); //in gmt
431  Timestamp ts = new Timestamp(now);
432  
433  String query = null;
434      
435  /* 
436  this works fine in postgres: 
437    timestamp1 - timestamp2 > interval X second 
438  mysql doesn't like it, to subtract dates/timestamps, we need
439  some mysql specific functions.
440  
441  however, 
442    timestamp + interval X second 
443  gives us a timestamp (in both db's) and can be directly compared
444  to another timestamp.
445  */
446  
447  if (deleteExpiredSessions) {
448    query  = "delete from " + SESSIONMASTER_TABLE + " where " +
449    /*
450    "('" + ts + "' - accessed ) > interval '" +expireSeconds+ "' second"; 
451    */
452    "('" + ts + "' + interval '" + expireSeconds + "' second ) > accessed ";
453    }
454  else {
455    query = "update " + SESSIONMASTER_TABLE 
456    + " set is_expired=? where " +
457    /*
458    + "('" + ts + "' - accessed ) > interval '" +expireSeconds+ "' second"; 
459    */
460    "(timestamp '" + ts + "' + interval '" + expireSeconds + "' second ) > accessed ";
461    }
462  
463  final PreparedStatement ps = con.prepareStatement(query);
464  ps.setBoolean(1, true);
465  log.bug("Query to run: ", ps);
466  final int n = ps.executeUpdate();
467  log.info(new Integer(n), " sessions reaped by: ", query);
468  }
469  
470/**
471Sessions inactive for greater than these number of seconds will be 
472eligible for expiry. <p><b>Note:</b> these expired sessions will still not
473be expired until the {@link #expireInactiveSessions(java.sql.Connection)} method is invoked.
474<p>Defaults to <code>8 hours (=60*60*8 seconds)</code>
475*/
476public void setExpireTime(final int seconds) {
477  expireSeconds = seconds;
478  }
479
480/**
481Returns the current expire interval (seconds after which
482sessions can be considered eligible for removal).
483*/
484public int getExpireTime() {
485  return expireSeconds;
486  }
487
488/** 
489Returns true is the specified sessionID is valid (i.e., the specified
490sessionID exists in the database and has not expired). 
491<p> 
492Note: this method does <b>not</b> expire the session itself or check for
493non-expired validity. Sessions should be expired as/when needed by calling the
494{@link expire} method.
495*/ 
496public boolean exists(final Connection con, final String sessionID) 
497throws SQLException 
498  {
499  Argcheck.notnull(sessionID, "sessionID arg was null");
500  
501  /*
502  select session_id, created, accessed, is_expired from sessionmaster 
503  where session_id=?
504  */
505  final PreparedStatement ps = prepareStatement(con, session_exists);
506  ps.setString(1, sessionID);
507  
508  log.bug("Query to run: ", ps);
509  
510  final ResultSet rs = ps.executeQuery();
511  boolean exists = false; 
512    if (rs.next()) 
513      {  //sessionID exists
514    boolean is_expired = rs.getBoolean(4); 
515    if (! is_expired) { //and is unexpired
516      exists = true;
517      }
518      }
519    
520    return exists;
521  }
522
523
524/**
525Returns session information from the session master table. This information
526is returned as a {@link JDBCSession.Info info} object encapsulating the master
527row for the given sessionID. <p> Returns <code>null</code> if the given
528sessionID has expired and/or was not found in the database.
529*/
530public JDBCSession.Info sessionInfo(
531  final Connection con, final String sessionID) 
532throws SQLException
533  {
534  Argcheck.notnull(sessionID, "sessionID arg was null");
535  
536  /*
537  select session_id, created, accessed, is_expired from 
538      sessionmaster where session_id=?
539  */
540  final PreparedStatement ps = prepareStatement(con, session_exists);
541  ps.setString(1, sessionID);
542  
543  log.bug("Query to run: ", ps);
544
545  JDBCSession.Info info = null;
546  
547  ResultSet rs = ps.executeQuery();
548
549    if (rs.next()) {  //sessionID exists
550    info = new JDBCSession.Info();
551    info.sessionID = sessionID;
552    info.created = rs.getTimestamp(2);
553    info.accessed = rs.getTimestamp(3);   
554    info.is_expired = rs.getBoolean(4);
555      }
556    
557    return info;
558  }
559
560/**
561Returns a List containing {@link Info session information} about all
562sessions associated with the specified ID. Returns an empty list if no
563sessions are found for the specified userID.
564<p>
565Note, the specified userID can be null in which case all sessions with null
566userID's will be returned.
567*/
568public List getForUser(final Connection con, final String userID) 
569throws SQLException
570  { 
571  if (userID == null)
572    log.warn("userID arg was null, was this intentional ?");
573
574  /*
575  select session_id, created, accessed, is_expired from 
576      sessionmaster where username=?
577  */
578  final PreparedStatement ps = prepareStatement(con, session_for_user);
579  ps.setString(1, userID);
580  
581  log.bug("Query to run: ", ps);
582
583  List list = new ArrayList();
584  ResultSet rs = ps.executeQuery();
585
586    while (rs.next()) {  //sessionID exists
587    JDBCSession.Info info = new JDBCSession.Info();
588    info.sessionID = rs.getString(1);
589    info.created = rs.getTimestamp(2);
590    info.accessed = rs.getTimestamp(3);   
591    info.is_expired = rs.getBoolean(4);
592      list.add(info);
593      }
594    
595    return list;
596  }
597
598/**
599Same as {@link #getForUser(Connection, String)} but takes a numeric userID.
600*/
601public List getForUser(final Connection con, final int userID) 
602throws SQLException
603  {
604  return getForUser(con, String.valueOf(userID));
605  }
606
607/**
608Information about a session.
609*/
610public static class Info
611  {
612  String sessionID;
613  Timestamp created;
614  Timestamp accessed;
615  boolean is_expired;
616  
617  public String getSessionID() { return sessionID; }
618  public Timestamp getCreated() { return created; }
619  public Timestamp getAccessed() { return accessed; }
620  public boolean getIsExpired() { return is_expired; }
621  
622  private ToString tostr;
623  {
624  tostr = new ToString(this, 
625      ToString.Style.VisibleLevel.DEFAULT);   
626  }
627  public String toString() {
628    return tostr.reflect().render();
629    }
630  }
631 
632
633//--------- value management ----------------
634
635/** 
636Returns a map of all [key, value] pairs associated with the specified
637sessionID. Returns <code>null</code> if the specified sessionID is not found 
638in the database or if the specified session has expired.
639*/
640public Map getAll(final Connection con, final String sessionID) 
641throws SQLException 
642  {
643  Argcheck.notnull(con, "con argument was null");
644  Argcheck.notnull(sessionID, "sessionID argument was null");
645
646  if (! exists(con, sessionID))
647    return null;
648
649  /* 
650  select name, value from sessiondata where session_id=? 
651  */
652
653  final PreparedStatement ps = prepareStatement(con, data_getall);
654  ps.setString(1, sessionID);
655  log.bug("Query to run: ", ps);
656
657  final Map map = new HashMap();
658  final ResultSet rs = ps.executeQuery();
659  boolean hasdata = false;
660
661  if (rs.next())
662    {
663    hasdata = true;
664    String name = rs.getString(1);
665    String value = rs.getString(2); //can be null
666    map.put(name, value);
667    }
668
669  while (rs.next()) 
670    {
671    String name = rs.getString(1);
672    String value = rs.getString(2); //can be null
673    map.put(name, value);
674    }
675
676  if (hasdata) {
677    updateSessionAccessTime(con, sessionID);
678    }
679    
680  return map;
681  }
682
683/**
684Returns the value associated with the specified sessionID and key.
685<p>
686Returns <code>null</code> if the specified session has expired, or the specified
687key does not exist in the database or exists but contains a <code>null</code> in
688the database.
689*/
690public String get(
691  final Connection con, final String sessionID, final String key) 
692throws SQLException 
693  {
694  Argcheck.notnull(con, "con argument was null");
695  Argcheck.notnull(sessionID, "sessionID argument was null");
696  Argcheck.notnull(key, "key argument was null");
697
698   /*
699   "select sd.name, sd.value from " 
700    + sessiondata   + " as sd, " 
701    + sessionmaster + " as sma "
702    + " where sd.session_id=? " +
703    + " and sd.name=? "
704    + " and sd.session_id = sma.session_id " +
705    + " and sma.is_expired=false"; 
706  */
707
708  final PreparedStatement ps = prepareStatement(con, data_get);
709  ps.setString(1, sessionID);
710  ps.setString(2, key);   //key param is called "name" in db
711
712  log.bug("Query to run: ", ps);
713
714  String result = null;
715  final ResultSet rs = ps.executeQuery();
716  if (rs.next()) 
717    {
718    result = rs.getString(2); //can be null if that IS the associated value
719    updateSessionAccessTime(con, sessionID);
720    }
721  
722  return result;
723  }
724  
725
726/**
727Deletes <b>both the key and value</b> specified by the sessionID and key.
728Does nothing if the sessionID or key does not exist in the database.
729*/
730public void delete(
731  Connection con, final String sessionID, final String key) 
732throws SQLException 
733  {
734  Argcheck.notnull(con, "con argument was null");
735  Argcheck.notnull(sessionID, "sessionID argument was null");
736  Argcheck.notnull(key, "key argument was null");
737    
738  /* Delete from sessiondata where session_id=? and name=? */
739    
740  PreparedStatement ps = prepareStatement(con, data_delete);
741  ps.setString(1, sessionID);
742  ps.setString(2, key);
743
744  log.bug("Query to run: ", ps);
745  int n = ps.executeUpdate();
746  if (n == 0) {
747    log.warn(ps, "key=", key, "[key not deleted, does it exist in the database ?]");
748    }
749  
750  updateSessionAccessTime(con, sessionID);
751  }
752  
753/**
754Saves the tuple [sessionID, key, value] in the database. <p> The specified
755sessionID must exist and be valid in the database otherwise a SQLException
756will be thrown.
757*/
758public void add(
759 final Connection con, final String sessionID, final String key, final String value) 
760throws  SQLException 
761  {
762  Argcheck.notnull(con, "con argument was null");
763  Argcheck.notnull(sessionID, "sessionID argument was null");
764  Argcheck.notnull(key, "key argument was null");
765  
766  if (! exists(con, sessionID))
767    throw new SQLException("The specified sessionID:[" + sessionID + "] has expired");
768
769  /*
770  insert into sessiondata (session_id, name, value) values (?, ?, ?) ";
771  */
772  
773  final PreparedStatement ps = prepareStatement(con, data_insert);
774  ps.setString(1, sessionID);
775  ps.setString(2, key);
776  ps.setString(3, value);
777  
778  log.bug("Query to run: ", ps);
779
780  final int n = ps.executeUpdate();
781  
782  if (n != 1) {
783    log.bug("insert error, preparedstatment", ps,  " returned", new Integer(n));
784    throw new SQLException("Error saving data, inserted row count != 1");
785    }
786  
787  updateSessionAccessTime(con, sessionID);
788  }
789  
790/**
791Adds all [key, value] pairs in the specified map to the session with the
792specified sessionID.
793*/
794public void addAll(
795  final Connection con, final String sessionID, final Map data)
796throws SQLException 
797  {
798  Argcheck.notnull(con, "con argument was null");
799  Argcheck.notnull(sessionID, "sessionID argument was null");
800  Argcheck.notnull(data, "data argument was null");
801
802  Set set = data.entrySet();      
803  int size = set.size();
804  if (size == 0) {
805    log.warn("nothing to do, map contains no data");
806    return;
807    }
808
809  if (! exists(con, sessionID))
810    throw new SQLException("The specified sessionID:[" + sessionID + "] has expired");
811
812  /*
813   insert into sessiondata 
814   (session_id, name, value) values (?, ?, ?) ";
815  */
816  
817  final PreparedStatement ps = prepareStatement(con, data_insert);
818
819  final Iterator entries = set.iterator();
820  while (entries.hasNext()) {
821    Map.Entry e = (Map.Entry) entries.next();
822    String key = e.getKey().toString();
823    String val = e.getValue().toString();
824    ps.setString(1, sessionID);
825    ps.setString(2, key);
826    ps.setString(3, val);
827    ps.addBatch();
828    }
829    
830  log.bug("Query to run: ", ps);
831
832  final int[] result = ps.executeBatch();
833  
834  /*  
835  if (result) {
836    log.bug("insert error, preparedstatment", ps,  " returned 0 items inserted");
837    throw new SQLException("Error saving data, inserted row count == 0");
838    }
839  */
840  updateSessionAccessTime(con, sessionID);
841  }
842
843/**
844An alias for the {@link add #add} method. 
845*/
846public void put(Connection con, 
847  final String sessionID, final String key, final String value) 
848throws  SQLException 
849  {
850  add(con, sessionID, key, value);
851  }
852
853/**
854An alias for the {@link #addAll addAll} method. 
855*/
856public void putAll(final Connection con, final String sessionID, final Map data) 
857throws SQLException 
858  {
859  addAll(con, sessionID, data);
860  }
861
862/**
863Updates the value for the specified sessionID and key in the database.
864<p>
865The specified sessionID and keys must exist in the database prior to
866calling this method,otherwise a SQLException will be thrown.
867*/
868public String update(
869  final Connection con, 
870  final String sessionID, final String key, final String newvalue) 
871throws SQLException 
872  {
873  Argcheck.notnull(con, "con argument was null");
874  Argcheck.notnull(sessionID, "sessionID argument was null");
875  Argcheck.notnull(key, "key argument was null");
876
877  if (! exists(con, sessionID))
878    throw new SQLException("The specified sessionID:[" + sessionID + "] has expired");
879        
880  /* 
881  update sessiondata set value=? 
882      where session_id=? and name=?;
883  */
884              
885  final PreparedStatement ps = prepareStatement(con, data_update);
886  ps.setString(1, newvalue);
887  ps.setString(2, sessionID);
888  ps.setString(3, key);
889  
890  log.bug("Query to run: ", ps);
891
892  String result = null;
893  final ResultSet rs = ps.executeQuery();
894  if (rs.next()) 
895    {
896    result = rs.getString(2); //can be null
897    updateSessionAccessTime(con, sessionID);
898    }
899
900  return result;
901  }
902
903
904/**
905Internal function: updates the session accessed time. should be called from
906any method that gets/sets session data.
907*/
908private void updateSessionAccessTime(final Connection con, final String sessionID) 
909throws SQLException
910  {
911  Argcheck.notnull(sessionID, "sessionID arg was null");
912  
913  final PreparedStatement ps = prepareStatement(con,session_access_update); 
914  long now = System.currentTimeMillis(); //in gmt
915  Timestamp ts = new Timestamp(now);
916    /*
917    update sessionmaster set accessed=? where session_id=?
918    */
919  ps.setTimestamp(1, ts);
920  ps.setString(2, sessionID); 
921  
922  log.bug("Query to run: ", ps);
923  
924  int n = ps.executeUpdate();
925  if (n != 1) {
926    log.warn("Query:", ps, "should have returned 1 but returned", new Integer(n), "[This can happen if the sessionID did not exist in the database]");
927    }
928  }
929
930//returns a preparedstatement. clearsParameters() for
931//recycled statements
932private final PreparedStatement prepareStatement(
933    final Connection con, final String sql) 
934throws SQLException
935  {
936  if (! (con instanceof fc.jdbc.PooledConnection) ) { 
937    return con.prepareStatement(sql);
938    }
939  PooledConnection pc = (PooledConnection) con;
940  //System.out.println("sql = " + sql);
941  PreparedStatement ps = pc.getCachedPreparedStatement(sql);
942  return ps;
943  }
944
945public static void main(String args[]) throws Exception
946  {
947  Args myargs = new Args(args);
948  myargs.setUsage(
949  "java fc.web.servlet.JDBCSession -conf conf-file [-loglevel level]\n"
950  + "-test  [tests the database for session tables]\n"
951  + "   -- or --\n"
952  + "-create  [creates session tables in the database]\n"
953  + "   -userTableName=<name of Users table in the database> \n"
954  + "   -userIDColName=the name of username/userid column in Users table.)] \n"
955  );
956  
957  if (myargs.flagExists("loglevel")) {
958    String level = myargs.get("loglevel");
959    Log.getDefault().setLevel(level);
960    }
961
962  String propfile = myargs.getRequired("conf");
963
964  FilePropertyMgr fprops = new FilePropertyMgr(new File(propfile));
965  ConnectionMgr mgr = new SimpleConnectionMgr(fprops);
966
967  String url = fprops.get("jdbc.url");
968  boolean mysql = false;
969  if (url.indexOf("mysql") != -1)
970    mysql = true;
971
972  Connection con = mgr.getConnection();
973
974  if (myargs.flagExists("test")) {
975    test(con);
976    }
977  else if (myargs.flagExists("create")) {
978    String userTableName = myargs.get("userTableName", "users");
979    createJDBCTables(con, userTableName, mysql);  
980    }
981  else
982    myargs.showError();
983  
984  con.close();
985  }
986
987/**
988Creates database tables for storing session data. This method can be
989called programmatically but is typically invoked by the main method.
990Invoke the main method without any flags to get usage information.
991*/
992public static void createJDBCTables(
993  Connection con, String  userTableName, boolean mysql)
994throws Exception
995  {
996  Argcheck.notnull(con, "connection param was null");
997  Argcheck.notnull(userTableName, "userTableName param was null");
998
999  Log log = Log.getDefault(); 
1000  try {
1001    QueryUtil.startTransaction(con);
1002    
1003    Statement st = null;
1004    String tmp = null;
1005    
1006    tmp = 
1007    "create table " + SESSIONDATA_TABLE + " ("
1008     + " session_id varchar(50),"
1009     + " name  varchar(100) not null,"
1010     + " value text )"
1011     ;
1012    
1013    st = con.createStatement();
1014    log.info("Running: " + tmp);
1015    st.execute(tmp);
1016
1017    tmp = "create INDEX IDX_" + SESSIONDATA_TABLE 
1018      + " ON " + SESSIONDATA_TABLE + "(session_id)";
1019
1020    st = con.createStatement();
1021    log.info("Running: " + tmp);
1022    st.execute(tmp);
1023
1024    tmp = 
1025    "create table " + SESSIONMASTER_TABLE + " ("
1026     + " session_id   varchar(50) primary key, "
1027     + " created      timestamp not null,"
1028     + " accessed     timestamp not null,"
1029     
1030     /*mysql == myshit but then you already knew that*/
1031     + ((mysql) ? 
1032      " is_expired  bool default 0," :    
1033      " is_expired  bool default 'f',") 
1034  
1035     + " username     varchar(255)"
1036     + " )"
1037     ;
1038
1039    st = con.createStatement();
1040    log.info("Running: " + tmp);
1041    st.execute(tmp);
1042
1043    tmp = "create INDEX IDX_" + SESSIONMASTER_TABLE + "_1"
1044      + " ON " + SESSIONMASTER_TABLE + "(username)";  
1045
1046    st = con.createStatement();
1047    log.info("Running: " + tmp);
1048    st.execute(tmp);
1049        
1050    tmp = 
1051    "alter table " + SESSIONDATA_TABLE + " add \n"  
1052    + " FOREIGN KEY (session_id) REFERENCES "
1053    +  SESSIONMASTER_TABLE + " (session_id) "
1054    + " on delete cascade "
1055    ;
1056
1057    st = con.createStatement();
1058    log.info("Running: " + tmp);
1059    st.execute(tmp);
1060  
1061    /* this will only work, generally, if the userid is the same
1062    type in both tables, which is not necessarily the case
1063    */
1064    /*
1065    tmp =
1066    "alter table " + SESSIONMASTER_TABLE + " add "
1067    + " FOREIGN KEY (" + userIDColName + ") REFERENCES "
1068    + userTableName;
1069    ;
1070    st = con.createStatement();
1071    log.info("Running: " + tmp);
1072    st.execute(tmp);
1073    */
1074    QueryUtil.endTransaction(con);
1075    }
1076  catch (Exception e) {
1077    QueryUtil.abortTransaction(con);
1078    /* of course, myshit 5.x does not rollback create table sql statements, 
1079    even when using InnoDB */
1080    log.error("*** JDBC SESSION TABLES WERE NOT CREATED PROPERLY (IF AT ALL)****");
1081    throw e;
1082    }
1083  
1084  log.info("*** JDBC SESSION TABLE SUCCESSFULLY CREATED ***");
1085  }
1086
1087static void test(Connection con) throws Exception {
1088  long start = 0;
1089  String val = null;
1090  
1091  try {
1092    Log log = Log.getDefault();
1093    //log.setLevel(SystemLog.DEBUG);
1094    JDBCSession sess = getInstance(log);
1095    
1096    String id = SessionUtil.newSessionID();
1097    //create
1098    System.out.println(">>>creating new session");
1099    sess.create(con, id); 
1100    System.out.println("....done");
1101    
1102    System.out.println(">>>session exists ?");
1103    System.out.println(sess.exists(con, id));   
1104    
1105    System.out.println(">>>session info");
1106    System.out.println(sess.sessionInfo(con, id));    
1107    
1108    System.out.println(">>>expiring session");
1109    sess.expire(con, id);   
1110
1111    System.out.println(">>>session info");
1112    System.out.println(sess.sessionInfo(con, id));    
1113
1114    System.out.println(">>>session exists ?");
1115    System.out.println(sess.exists(con, id));   
1116
1117    System.out.println(">>>creating another session");
1118    id = SessionUtil.newSessionID();
1119    sess.create(con, id); 
1120    System.out.println("....done");
1121    
1122    System.out.println(">>>session info");
1123    System.out.println(sess.sessionInfo(con, id));    
1124    
1125    System.out.println(">>>setting session expire seconds to 5 seconds");
1126    sess.setExpireTime(5);    
1127    
1128    System.out.println(">>>set delete expired seconds to false");
1129    sess.setDeleteExpiredSessions(false);   
1130
1131    System.out.println(">>>sleeping 5 seconds");
1132    Thread.currentThread().sleep(5000);   
1133    System.out.println(">>>expiring all invalid sessions...");
1134    sess.expireInactiveSessions(con);   
1135  
1136    System.out.println(">>>adding value foo=bar to an expired session");
1137    try {
1138      sess.add(con, id, "foo", "bar");    
1139      }
1140    catch (SQLException e) {
1141      ;System.out.println("Expecting the following exception");
1142      e.printStackTrace();
1143      }
1144      
1145    System.out.println(">>>getting value for foo");
1146    for (int n = 0; n < 10; n++) {
1147      start = System.currentTimeMillis();
1148      val = sess.get(con, id, "foo");
1149      System.out.println("time: " + (System.currentTimeMillis() - start) + " ms");
1150      }
1151    System.out.println(val);    
1152
1153    id = SessionUtil.newSessionID();
1154    sess.create(con, id); 
1155
1156    Map map = new HashMap();
1157    map.put("foo2", "bar2");
1158    map.put("foo3", "bar3");
1159    System.out.println(">>>adding map " + map);
1160    sess.addAll(con, id, map);    
1161
1162    System.out.println(">>>getting all values");
1163    System.out.println(sess.getAll(con, id));   
1164
1165      System.out.println(">>> tie session to user");
1166    sess.tieToUser(con, id, "1");     
1167    //sess.tieToUser(con, id, 1);     
1168
1169      System.out.println(">>> session for userID=1");
1170    start = System.currentTimeMillis();
1171      List list = sess.getForUser(con, 1);
1172    System.out.println("time: " + (System.currentTimeMillis() - start) + " ms");
1173    System.out.println(list);     
1174    
1175/*
1176    sess.setExpireSeconds(1);
1177    sess.setDeleteExpiredSessions(true);    
1178    Thread.currentThread().sleep(1100);   
1179    sess.expireInactiveSessions(con);   
1180*/    
1181    }
1182  catch (Exception e) {
1183    e.printStackTrace();
1184    } 
1185  }
1186}