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"><SQL_TYPE_FROM_YOUR_USER_TABLE></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}