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