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.jdbc; 007 008import java.io.*; 009import java.util.*; 010import java.sql.*; 011import javax.servlet.*; 012 013import fc.io.*; 014import fc.web.*; 015import fc.util.*; 016 017/** 018Various JDBC utility methods. 019<p> 020@author hursh jain 021**/ 022public final class QueryUtil 023{ 024private static long txID = 1; 025 026private QueryUtil() { /*no construction*/ } 027 028static Log log = Log.get("fc.web.servlet.QueryUtil"); 029 030/** 031This method can be called to initialize a logger for this class (else 032a default logger will used). 033 034@param logger Sets the log to which methods in this class 035 will send logging output. 036**/ 037public static final void init(Log logger) { 038 log = logger; 039 } 040 041/** 042I don't think this is needed for anything. 043 044Often foreign keys are cascaded to null when the referenced key is deleted. 045Many times keys are integral types; the JDBC getInt(..) method returns a 046integer with value 0 when the actual value is null (since primitive java 047types cannot be null). When saving a previously retrieved record with some 048key value of 0, we would like to save that key back as null in the 049database. 050<p> 051This method examines the given keyvalue and if it is 0, sets the value in 052the specified position as null (otherwise sets it to the specified value). 053<p> 054This does imply a record creation convention whereby keys never have the 055value of 0 in normal circumstances. 056<p> 057We could alternatively use object types such as Integer (and not primitive 058types). This would have the advantage of natively being capable of being 059null. However, that has the big disadvantage of making html form/GUI code 060more complicated. 061 062public static final void setNullableKey(PreparedStatement pstmt, 063 int pos, int keyvalue) 064throws SQLException 065 { 066 if (keyvalue == 0) { 067 pstmt.setObject(pos, null); 068 } 069 else { 070 pstmt.setInt(pos, keyvalue); 071 } 072 } 073*/ 074 075/** 076Returns a montonically increasing number starting from 1. This is true for 077a given JVM invocation, this value will start from 1 again the next time 078the JVM is invoked. 079**/ 080public static final long getNewTxID() { 081 synchronized (QueryUtil.class) { 082 return txID++; 083 } 084 } 085 086/** 087Returns the dbname corresponding that the database for the specified 088jdbc connection. Useful for writing database specific code as/when applicable. 089A similar method is also available in {@link ConnectionMgr} 090*/ 091public static DBName getDBName(final Connection con) throws SQLException 092 { 093 final DatabaseMetaData md = con.getMetaData(); 094 return DBName.fromDriver(md.getDriverName()); 095 } 096 097/** 098Checks to see if the specified result set is scrollable. 099 100@throws SQLException if the specified result set is <b>not</b> 101 scrollable. 102**/ 103public static final void ensureScrollable(ResultSet rs) 104throws SQLException 105 { 106 if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { 107 throw new SQLException("Specified ResultSet not scrollable; need scrollable to proceed. [ResultSet=" + rs + "]"); 108 } 109 } 110/** 111Checks to see if actual row count was same as expected row count, for some 112query. Takes 2 integers as parameters and simply sees if the first equals 113the second. 114 115@param rowcount the actual count (returned by executeUpdate() etc.) 116@param expected the expected count 117@throws SQLException if the actual and expected counts don't match 118**/ 119public static final void ensureCount(int rowcount, int expected) 120throws SQLException 121 { 122 if (rowcount != expected) { 123 throw new SQLException("row count mismatch, recieved [" + rowcount + "], expected [" + expected + "]"); 124 } 125 } 126 127/** 128Returns the number of rows in the specified ResultSet. If the ResultSet is 129of type {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, then this method moves 130the result set pointer back to the beginning, after it is finished. If the 131result set is not scroll insensitive, then this method will still work 132properly, but the contents of the result set will not be usable again 133(since it cannot be rewinded). 134<p> 135<b>Note</b>: the count can also be retreived directly (and more 136efficiently) for many queries by including the <tt>COUNT()</tt> SQL 137function as part of the query (in which case one would read the returned 138count column directly and NOT call this method) 139 140@param rset the result set to examine 141@return the number of rows in the rowset 142**/ 143public static final long getRowCount(ResultSet rset) 144throws SQLException 145 { 146 int rowcount = 0; 147 148 if (rset != null) 149 { 150 int type = rset.getType(); 151 if (type == ResultSet.TYPE_FORWARD_ONLY) 152 { 153 while (rset.next()) { 154 rowcount++; 155 } 156 } 157 else { //scrollable 158 rset.last(); //note, afterlast() will return 0 for getRow 159 rowcount = rset.getRow(); 160 rset.beforeFirst(); 161 } 162 } 163 return rowcount; 164 } 165 166/** 167Gets the last inserted id, typically auto-increment(mysql) or serial 168(postgresql) columns. Since auto increment values can be integers or longs, 169this method always returns it's value as a long. The caller can narrow this 170down to an int if that's what's defined in the database. 171 172@param con the connection to get the last id for. Both 173 mysql and postgresql (and probably other db's) treat the 174 last inserted id as a per connection value (associated with 175 the last statement on that connection that returned such a 176 value). 177@param dbname the name of the target database. Needed because 178 this must be implemented in a database specific 179 way. 180@param info optional further info needed to implement this 181 method for some databases. This currently is: 182 <blockquote> 183 <ul> 184 <li>mysql: not needed, specify <tt>null</tt> 185 <li>postgresql: specify the name of the sequence, which 186 for serial columns is (by default) 187 <tt>tablename_colname_seq</tt>, for example 188 <tt>"tablefoo_myserialid_seq"</tt> 189 </blockquote> 190 191@throws SQLException if the last insert id could not be found 192 or if some other datbase problem occurred. 193*/ 194public static final long getLastInsertID( 195 Connection con, DBName dbname, Object info) 196throws SQLException 197 { 198 //at some point we can refactor this into separate classes 199 //for each db but that's overkill for us now 200 201 if (dbname == DBName.mysql) { 202 String query = "select last_insert_id()"; 203 Statement stmt = con.createStatement(); 204 ResultSet rs = stmt.executeQuery(query); 205 boolean found = rs.next(); 206 if (! found) throw new SQLException("No last inserted id returned"); 207 return rs.getLong(1); 208 } 209 if (dbname == DBName.postgres) { 210 if (! (info instanceof String)) throw new SQLException("postgres requires the info parameter as a String"); 211 String query = "select currval('" + info + "')"; 212 Statement stmt = con.createStatement(); 213 ResultSet rs = stmt.executeQuery(query); 214 boolean found = rs.next(); 215 if (! found) throw new SQLException("No last inserted id returned"); 216 return rs.getLong(1); 217 } 218 else { 219 throw new SQLException("Method not yet implemented for database of type: " + dbname); 220 } 221 } 222 223/** 224Convenience method that calls {@link #executeQuery(Connection, String, boolean)} 225specifying no header options (i.e., column headers are not returned as part 226of the query results). 227 228@param con the database connection to use. 229@param query the query to be performed 230@param headers if true, the first row contains the column headers 231**/ 232public static final List executeQuery( 233 Connection con, String query) throws SQLException 234 { 235 return executeQuery(con, query, false); 236 } 237 238/** 239Performs the specified query and returns a <tt>List</tt> (the result of 240converting the ResultSet via the {@link #rsToList} method). 241<p> 242<b>Important Note</b>: this method is useful for program generated queries, 243but should not be used for queries where unknown data could be send by a 244malicious user (since the query string is sent as-is to the server). For 245secure queries, use PreparedStatements instead. 246 247@param con the database connection to use. 248@param query the query to be peformed 249@param headers if true, the first row contains the column headers 250**/ 251public static final List executeQuery( 252 Connection con, String query, boolean headers) 253throws SQLException 254 { 255 Statement stmt = con.createStatement(); 256 ResultSet rs = stmt.executeQuery(query); 257 List list = rsToList(rs, headers); 258 return list; 259 } 260 261/** 262Peforms the specified query and returns true if the query has 263only one row of data. 264 265@param con the database connection to use. 266**/ 267public static final boolean hasExactlyOneRow( 268 Connection con, String query) throws SQLException 269 { 270 Statement stmt = con.createStatement(); 271 ResultSet rs = stmt.executeQuery(query); 272 boolean valid = getRowCount(rs) == 1; 273 return valid; 274 } 275 276/** 277Peforms the specified query and returns true if the query returns 278no data. 279 280@param con the database connection to use. 281**/ 282public static final boolean hasExactlyZeroRows( 283 Connection con, String query) throws SQLException 284 { 285 Statement stmt = con.createStatement(); 286 ResultSet rs = stmt.executeQuery(query); 287 boolean valid = getRowCount(rs) == 0; 288 return valid; 289 } 290 291/** 292Creates a new connection that will return ResultSet's of 293<tt>TYPE_SCROLL_INSENSITIVE</tt> and <tt>CONCUR_READ_ONLY</tt>. 294**/ 295public static final Statement getRewindableStmt(Connection con) 296throws SQLException 297 { 298 Statement stmt = con.createStatement( 299 ResultSet.TYPE_SCROLL_INSENSITIVE, 300 ResultSet.CONCUR_READ_ONLY); 301 return stmt; 302 } 303 304/** 305Creates a new connection that will return ResultSet's of 306<tt>TYPE_SCROLL_INSENSITIVE</tt> and <tt>CONCUR_READ_ONLY</tt>. (i.e., the 307PreparedStatement returned by this method should be rewindable). 308<p> 309Note, by default a java.sql.Connection.prepareStatment(String) method 310returns statements that support ResultSet's of forward_only. This means we 311will not be able to determine the row count via the {@link #getRowCount} 312and <b>also</b> be able to then rewind the ResultSet and read it's 313contents. 314<p> 315**/ 316public static final PreparedStatement 317getRewindablePreparedStmt(Connection con, String sql) 318throws SQLException 319 { 320 PreparedStatement stmt = con.prepareStatement( 321 sql, 322 ResultSet.TYPE_SCROLL_INSENSITIVE, 323 ResultSet.CONCUR_READ_ONLY); 324 return stmt; 325 } 326 327/** 328Starts a transaction on the specified connection. The 329connection is set to <i>not</i> autocommit any statements from 330here onwards and the {@link #endTransaction endTransaction} 331method must be called to end this transaction. 332<p> 333If the transaction cannot be started, the connection is closed. 334 335@param con the connection to commit 336@param transactionIsolation the transaction isolation level 337 for this transaction 338@param message commit description; shown when debug 339 logging is turned on 340 341@return <tt>true</tt> if the transaction was started successfully, 342 <tt>false</tt> if the transaction could not start for some reason. 343 344@throws IllegalArgumentException if the transaction isolation level is not a valid value (as defined in {@link java.sql.Connection} 345*/ 346public static final boolean startTransaction(Connection con, int transactionIsolation, String message) 347 { 348 switch (transactionIsolation) 349 { 350 case Connection.TRANSACTION_NONE: 351 case Connection.TRANSACTION_READ_COMMITTED: 352 case Connection.TRANSACTION_READ_UNCOMMITTED: 353 case Connection.TRANSACTION_REPEATABLE_READ: 354 case Connection.TRANSACTION_SERIALIZABLE: 355 break; 356 default: 357 throw new IllegalArgumentException("The specfied transaction isolation level " + transactionIsolation + " is not a valid value"); 358 } 359 try { 360 con.setTransactionIsolation(transactionIsolation); 361 } 362 catch (Exception e) { 363 log.error("Could not set transaction isolation;", IOUtil.throwableToString(e)); 364 return false; 365 } 366 367 return startTransaction(con, message); 368 } 369 370/** 371Starts a transaction on the specified connection. The connection is set to 372not autocommit any statements from here onwards and the {@link 373#endTransaction} method must be called to end this transaction. The 374transaction isolation is whatever the default transaction isolation is for 375this connection, driver or database (this method does not explicitly set 376the isolation level). See {@link #startTransaction(Connection, int, 377String)}. 378<p> 379If the transaction cannot be started, the connection is closed. 380 381@param con the connection to commit 382@param message commit description; shown if debug logging 383 is turned on 384 385@return <tt>true</tt> if the transaction was started successfully, 386 <tt>false</tt> if the transaction could not start for some reason. 387*/ 388public static final boolean startTransaction(Connection con, String message) 389 { 390 boolean result = true; 391 392 try { 393 con.setAutoCommit(false); 394 } 395 catch (Exception e) 396 { 397 log.error(message, "Could not set autoCommit(false) on this connection; Transaction will not be started and the connection will be closed. ", con, IOUtil.throwableToString(e)); 398 try { 399 con.close(); 400 } 401 catch (Exception e2) { 402 log.error(message, "Connection does not allow autoCommit(false) and connetion cannot even be closed. ", con, IOUtil.throwableToString(e2)); 403 } 404 result = false; 405 } 406 407 log.bug(message, "/START: Transaction [Isolation Level=", getTransactionLevelString(con), "];", con); 408 return result; 409 } 410 411 412private static final String t_str = "*** "; 413 414/** 415Calls {@link startTransaction(Connection, String) startTransaction} with 416a empty message string. 417*/ 418public static final boolean startTransaction(Connection con) 419 { 420 return startTransaction(con, t_str); 421 } 422 423/** 424Aborts (rolls back) the current transaction on the specified connection. 425<p> 426After the transaction is rolled back, the connection is set to 427autoCommit(true). 428 429@param con the connection to rollback 430@param message a description; shown if logging is turned on 431 432@return <tt>true</tt> if the transaction was rolledback successful, 433 <tt>false</tt> if the transaction rollback for some reason. 434**/ 435public static final boolean abortTransaction(Connection con, String message) 436 { 437 boolean result = true; 438 439 try { 440 if (con.getAutoCommit() == true) //throws an Exception 441 { 442 log.error("Connection not in transaction, autoCommit is true. Did you call QueryUtil.startTransaction() on this connection ?", message, con); 443 return false; 444 } 445 } 446 catch (Exception e) { 447 log.error(IOUtil.throwableToString(e)); 448 return false; 449 } 450 451 try { 452 con.rollback(); 453 } 454 /*catch problems, exception, incl. all runtime related*/ 455 catch (Throwable e) 456 { 457 result = false; 458 log.error(message, "*** Transaction could not be aborted/rolled back ***", IOUtil.throwableToString(e)); 459 } 460 finally 461 { 462 try { 463 con.setAutoCommit(true); 464 } 465 catch (Exception e) { 466 log.error(message, "Could NOT reset connection to be autoCommit=true", con, IOUtil.throwableToString(e)); 467 result = false; 468 } 469 } 470 471 if (log.canLog(Log.DEBUG)) 472 log.bug(message, "/ABORT: Transaction rolled back [Time=", new java.util.Date(), "]; connection=", con); 473 474 return result; 475 } //~doCommit 476 477 478/** 479Calls {@link abortTransaction(Connection, String) abortTransaction} with 480an empty message string. 481*/ 482public static final boolean abortTransaction(Connection con) 483 { 484 return abortTransaction(con, t_str); 485 } 486 487/** 488Commits the specified connection. Attemps a rollback if the 489<tt>commit()</tt> call fails for some reason. This method should only be 490called after all queries in the transaction have been sent to the 491database (so if any of those fail, the entire transaction can be rolled 492back). 493<p> 494After the transaction completes, the connection is set to 495autoCommit(true). 496 497@param con the connection to commit 498@param message commit description; shown if logging is turned on 499 500@return <tt>true</tt> if the transaction was committed successful, 501 <tt>false</tt> if the transaction failed for some reason. 502**/ 503public static final boolean endTransaction(Connection con, String message) 504 { 505 boolean result = true; 506 507 try { 508 if (con.getAutoCommit() == true) //throws an Exception 509 { 510 log.error("Connection not in transaction, autoCommit is true. Did you prior call QueryUtil.startTransaction() on this connection ?", message, con); 511 return false; 512 } 513 } 514 catch (Exception e) { 515 log.error(IOUtil.throwableToString(e)); 516 return false; 517 } 518 519 try { 520 con.commit(); 521 } 522 /*catch problems, exception, incl. all runtime related*/ 523 catch (Throwable e) 524 { 525 result = false; 526 log.error(message, "*** Transaction could not complete: attempting roll back ***", IOUtil.throwableToString(e)); 527 try { 528 con.rollback(); 529 } 530 catch (SQLException e2) { 531 log.error(message, "*** Transaction could not be rolled back ***", IOUtil.throwableToString(e2)); 532 } 533 //throw e; 534 } 535 finally 536 { 537 try { 538 con.setAutoCommit(true); 539 } 540 catch (Exception e) { 541 log.error(message, "Could NOT reset connection to be autoCommit=true", con, IOUtil.throwableToString(e)); 542 result = false; 543 } 544 } 545 546 log.bug(message, "/FINISH: Transaction completed. Connection=", con); 547 548 return result; 549 } 550 551/** 552Calls {@link endTransaction(Connection, String) endTransaction} with 553an empty message string. 554*/ 555public static final boolean endTransaction(Connection con) 556 { 557 return endTransaction(con, t_str); 558 } 559 560private static String getTransactionLevelString(Connection con) 561 { 562 try { 563 final int level = con.getTransactionIsolation(); 564 switch (level) 565 { 566 case Connection.TRANSACTION_NONE: 567 return "TRANSACTION_NONE"; 568 case Connection.TRANSACTION_READ_COMMITTED: 569 return "TRANSACTION_READ_COMMITTED"; 570 case Connection.TRANSACTION_READ_UNCOMMITTED: 571 return "TRANSACTION_READ_UNCOMMITTED"; 572 case Connection.TRANSACTION_REPEATABLE_READ: 573 return "TRANSACTION_REPEATABLE_READ"; 574 case Connection.TRANSACTION_SERIALIZABLE: 575 return "TRANSACTION_SERIALIZABLE"; 576 default: 577 return "Unknown level (not a legal value)"; 578 } 579 } 580 catch (Exception e) { 581 log.error(IOUtil.throwableToString(e)); 582 } 583 return ""; 584 } 585 586/** 587Rollsback the transaction, ignoring any errors in the rollback itself. 588**/ 589public static final void rollback(Connection con) 590 { 591 try { 592 if (con.isClosed()) { 593 log.bug("tried to rollback a already closed connection: ", con); 594 return; 595 } 596 597 con.rollback(); 598 } 599 catch (Exception e) { 600 log.error(IOUtil.throwableToString(e)); 601 } 602 } 603 604 605/** 606Closes the specified connection, statement and resultset, logging any 607errors to the stderr. Ignores any parameters with <tt>null</tt> values. 608**/ 609public static final void close(ResultSet rs, Statement stmt, Connection con) 610 { 611 //objects _must_ be closed in the following order 612 if (rs != null) 613 try { 614 rs.close(); 615 } 616 catch (Exception e) { 617 log.warn("", e); 618 } 619 620 if (stmt != null) try { 621 stmt.close(); 622 } 623 catch (Exception e) { 624 log.warn("", e); 625 } 626 627 if (con != null) try { 628 con.close(); 629 } 630 catch (Exception e) { 631 log.warn("", e); 632 } 633 } 634 635 636/** 637Closes the specified connection, ignoring any exceptions encountered 638in the connection.close() method itself. 639**/ 640public static final void close(Connection con) 641 { 642 close(null, null, con); 643 } 644 645 646/** 647Converts a java.sql.ResultSet into a List of Object[], where each Object[] 648represents all the columns in one row. All column values are stored in the 649Object[] via the getObject() method of the ResultSet. If the ResultSet is 650empty, this method returns <tt>null</tt> (or returns only the headers if 651headers are to be printed). 652 653@param rs the ResultSet 654@param headers if set to true, the first row of the returned List 655 will contain an Object[] of the column header names. 656*/ 657public static final List rsToList( 658 java.sql.ResultSet rs, boolean headers) throws SQLException 659 { 660 List list = new ArrayList(); 661 ResultSetMetaData metadata = rs.getMetaData(); 662 int numcols = metadata.getColumnCount(); 663 Object[] arow = null; 664 int i; 665 666 if (headers) 667 { 668 // get column header info 669 arow = new Object[numcols]; 670 for ( i = 1; i <= numcols; i++) { 671 arow[i-1] = metadata.getColumnLabel(i); 672 } 673 list.add(arow); 674 } 675 676 while (rs.next()) { 677 arow = new Object[numcols]; 678 for (i=1; i <= numcols; i++) { 679 arow[i-1] = rs.getObject(i); 680 } 681 list.add(arow); 682 } 683 684 return list; 685 } //~rsToList() 686 687/** 688Converts the list returned by the {@link #rsToList method} to a String, 689consisting of all the rows contained in the list. Each row is rendered 690within brackets <tt>[..row1..]</tt>, with different rows seperated by 691commas (<tt>[..row1..], [..row2..], ...</tt>) but this format may be 692changed in the future. 693**/ 694public static final String rsListToString(List list) 695 { 696 String result = ""; 697 if (list == null) { 698 return result; 699 } 700 int size = list.size(); 701 result += "Total records: " + size + IOUtil.LINE_SEP; 702 for (int n = 0; n < size; n++) 703 { 704 Object[] row = (Object[]) list.get(n); 705 int rowlen = row.length; 706 result += "["; 707 //1 row 708 for (int k= 0; k < rowlen; k++) { 709 result += row[k]; 710 if ((k + 1) != rowlen) { 711 result += ", "; 712 } 713 } 714 //end 1 row 715 result += "]"; 716 if ((n + 1) != size) { 717 result += ", "; 718 } 719 } 720 return result; 721 } //~rsListToString() 722 723 724/** 725Prints the given java.sql.ResultSet (including result set headers) in a 726simple straightforward fashion to <tt>System.out</tt> 727 728@param rs the result set to print 729*/ 730public static final void printRS(ResultSet rs) 731throws SQLException 732 { 733 ResultSetMetaData metadata = rs.getMetaData(); 734 int numcols = metadata.getColumnCount(); 735 736 System.out.print("[Headers] "); 737 for (int i = 1; i <= numcols; i++) { 738 System.out.print(metadata.getColumnLabel(i)); 739 if (i != numcols) System.out.print(", "); 740 } 741 System.out.println(""); 742 int rowcount = 0; 743 while (rs.next()) 744 { 745 System.out.print("[Row #" + ++rowcount + " ] "); 746 for(int n=1 ; n <= numcols; n++) { 747 Object obj = rs.getObject(n); 748 String str = (obj != null) ? obj.toString() : "null"; 749 if (str.length() > 0) 750 System.out.print(str); 751 else 752 System.out.print("\"\""); 753 if (n != numcols) System.out.print(", "); 754 } 755 System.out.println(""); 756 } 757 } 758 759 760/** 761Delegates to {@link printResultSetTable(ResultSet, PrintStream, 762ResultSetPrintDirection, TablePrinter.PrintConfig, boolean)} so that the 763table is printed {@link ResultSetPrintDirection.HORIZONTAL horizontally} 764with the default table style and headers set to true. 765**/ 766public static final void printResultSetTable(ResultSet rs, PrintStream ps) 767throws SQLException 768 { 769 printResultSetTable(rs, ps, ResultSetPrintDirection.HORIZONTAL, null, true); 770 } 771 772/** 773Prints the specified result set in a tabular format. The printed table 774style is according specified {@link TablePrinter.PrintConfig PrintConfig} 775object. 776 777@param rs the ResultSet 778@param ps the destination print stream 779@param direction the result set printing orientation. 780@param config the printing configuration. 781 <b>Specify <tt>null</tt>for to use the 782 default style</b>. 783@param headers <tt>true</tt> to print headers, <tt>false</tt> to omit 784 headers. Headers are obtained from the 785 ResultSet's Meta Data. 786**/ 787public static final void printResultSetTable( 788 ResultSet rs, PrintStream ps, ResultSetPrintDirection direction, 789 TablePrinter.PrintConfig config, boolean headers) 790throws SQLException 791 { 792 ResultSetMetaData metadata = rs.getMetaData(); 793 int numcols = metadata.getColumnCount(); 794 TablePrinter fprint = null; 795 if (config == null) { 796 config = new TablePrinter.PrintConfig(); 797 config.setCellPadding(1); 798 } 799 800 if (direction == ResultSetPrintDirection.HORIZONTAL) 801 { 802 fprint = new TablePrinter(numcols, ps, config); 803 fprint.startTable(); 804 805 //print headers 806 if (headers) 807 { 808 fprint.startRow(); 809 for (int i = 1; i <= numcols; i++) { 810 fprint.printCell(metadata.getColumnLabel(i)); 811 } 812 fprint.endRow(); 813 } 814 815 while (rs.next()) { 816 fprint.startRow(); 817 for (int i = 1; i <= numcols; i++) { 818 fprint.printCell(rs.getString(i)); 819 } 820 fprint.endRow(); 821 } 822 fprint.endTable(); 823 } 824 825 else if (direction == ResultSetPrintDirection.VERTICAL) 826 { 827 if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { 828 ps.println("QueryUtil.printResultSet(): Vertical print orientation requires a scrollable resultset"); 829 return; 830 } 831 832 rs.last(); 833 int rows = rs.getRow(); //number of row in rs == our columns 834 int printcols = (headers) ? rows + 1 : rows; 835 836 //we don't support this cause we don't know how many times 837 //the header will be repeated etc., and we need to know 838 //the exact column size (which equals number of rows in Vertical) 839 //before printing (and number of rows includes and extra headers 840 //possibly repeated 841 if (config.getHeader() != null) { 842 ps.println("QueryUtil.printResultSet(): Seperate headers not supported when using Vertical orientation. To print Result Set headers, specify 'true' for the 'header' parameter when invoking this method"); 843 return; 844 } 845 846 fprint = new TablePrinter(printcols, ps, config); 847 rs.beforeFirst(); 848 849 fprint.startTable(); 850 for (int n = 1; n <= numcols; n++) 851 { 852 fprint.startRow(); 853 854 if (headers) { 855 fprint.printCell(metadata.getColumnLabel(n)); 856 } 857 858 while (rs.next()) { 859 fprint.printCell(rs.getString(n)); 860 } 861 862 fprint.endRow(); 863 rs.beforeFirst(); 864 } 865 fprint.endTable(); 866 } 867 868 else 869 ps.println("QueryUtil.printResultSet(): PrintConfig not understood"); 870 871 } 872 873 874//we have to implement this method identically twice --once 875//for printstream, once for jspwriter 876//because darn it, jspwriter is NOT a subclass of printwriter 877//or printstream, freaking idiots designed jsp's 878 879/** 880Prints the given ResultSet as a HTML table to the specified JspWriter. The 881ResultSet is transversed/printed based on the direction parameter (normal 882output where each row is printed horizontally is specified via {@link 883QueryUtil.ResultSetPrintDirection#HORIZONTAL}). 884<p> 885The output table has the following CSS styles added to it: 886<ul> 887<li>For the table: class <tt>QueryUtil_Table</tt> 888<li>For the Header row: class <tt>QueryUtil_HeaderRow</tt> 889<li>For the Header cells: class <tt>QueryUtil_HeaderCell</tt> 890<li>For a normal row: class <tt>QueryUtil_Row</tt> 891<li>For a normal cell: class <tt>QueryUtil_Cell</tt> 892</ul> 893**/ 894public static final void printResultSetHTMLTable( 895 ResultSet rs, javax.servlet.jsp.JspWriter out, 896 ResultSetPrintDirection direction) throws IOException, SQLException 897 { 898 boolean headers = true; 899 ResultSetMetaData metadata = rs.getMetaData(); 900 int numcols = metadata.getColumnCount(); 901 902 out.println("<table class=\"QueryUtil_Table\">"); 903 if (direction == ResultSetPrintDirection.HORIZONTAL) 904 { 905 //print headers 906 if (headers) 907 { 908 out.print("<tr class=\"QueryUtil_HeaderRow\">"); 909 for (int i = 1; i <= numcols; i++) { 910 out.print("<td class=\"QueryUtil_HeaderCell\">"); 911 out.print(metadata.getColumnLabel(i)); 912 out.print("</td>"); 913 } 914 out.println("</tr>"); 915 } 916 917 while (rs.next()) { 918 out.println("<tr class=\"QueryUtil_Row\">"); 919 for (int i = 1; i <= numcols; i++) { 920 out.print("<td class=\"QueryUtil_Cell\">"); 921 out.print(rs.getString(i)); 922 out.print("</td>"); 923 } 924 out.println("</tr>"); 925 } 926 out.println("</table>"); 927 } 928 929 else if (direction == ResultSetPrintDirection.VERTICAL) 930 { 931 if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { 932 out.println("QueryUtil.printResultSet(): Vertical print orientation requires a scrollable resultset"); 933 return; 934 } 935 936 rs.last(); 937 int rows = rs.getRow(); //number of row in rs == our columns 938 int printcols = (headers) ? rows + 1 : rows; 939 940 rs.beforeFirst(); 941 942 out.println("<table class=\"QueryUtil_Table\">"); 943 for (int n = 1; n <= numcols; n++) 944 { 945 out.println("<tr>"); 946 947 if (headers) { 948 out.print("<td class=\"QueryUtil_HeaderCell\">"); 949 out.println(metadata.getColumnLabel(n)); 950 out.println("</td>"); 951 } 952 953 while (rs.next()) { 954 out.println("<td class=\"QueryUtil_Cell\">"); 955 out.println(rs.getString(n)); 956 out.println("</td>"); 957 } 958 959 out.println("</tr>"); 960 rs.beforeFirst(); 961 } 962 out.println("</table>"); 963 } 964 965 } 966 967 968/** 969Prints the given ResultSet as a HTML table to the specified PrintWriter. 970The ResultSet is transversed/printed based on the direction parameter 971(normal output where each row is printed horizontally is specified via 972{@link QueryUtil.ResultSetPrintDirection#HORIZONTAL}). 973<p> 974The output table has the following CSS styles added to it: 975<ul> 976<li>For the table: class <tt>QueryUtil_Table</tt> 977<li>For the Header row: class <tt>QueryUtil_HeaderRow</tt> 978<li>For the Header cells: class <tt>QueryUtil_HeaderCell</tt> 979<li>For a normal row: class <tt>QueryUtil_Row</tt> 980<li>For a normal cell: class <tt>QueryUtil_Cell</tt> 981</ul> 982**/ 983public static final void printResultSetHTMLTable( 984 ResultSet rs, PrintWriter out, 985 ResultSetPrintDirection direction) throws IOException, SQLException 986 { 987 boolean headers = true; 988 ResultSetMetaData metadata = rs.getMetaData(); 989 int numcols = metadata.getColumnCount(); 990 991 out.println("<table class=\"QueryUtil_Table\">"); 992 if (direction == ResultSetPrintDirection.HORIZONTAL) 993 { 994 //print headers 995 if (headers) 996 { 997 out.print("<tr class=\"QueryUtil_HeaderRow\">"); 998 for (int i = 1; i <= numcols; i++) { 999 out.print("<td class=\"QueryUtil_HeaderCell\">"); 1000 out.print(metadata.getColumnLabel(i)); 1001 out.print("</td>"); 1002 } 1003 out.println("</tr>"); 1004 } 1005 1006 while (rs.next()) { 1007 out.println("<tr class=\"QueryUtil_Row\">"); 1008 for (int i = 1; i <= numcols; i++) { 1009 out.print("<td class=\"QueryUtil_Cell\">"); 1010 out.print(rs.getString(i)); 1011 out.print("</td>"); 1012 } 1013 out.println("</tr>"); 1014 } 1015 out.println("</table>"); 1016 } 1017 1018 else if (direction == ResultSetPrintDirection.VERTICAL) 1019 { 1020 if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { 1021 out.println("QueryUtil.printResultSet(): Vertical print orientation requires a scrollable resultset"); 1022 return; 1023 } 1024 1025 rs.last(); 1026 int rows = rs.getRow(); //number of row in rs == our columns 1027 int printcols = (headers) ? rows + 1 : rows; 1028 1029 rs.beforeFirst(); 1030 1031 out.println("<table class=\"QueryUtil_Table\">"); 1032 for (int n = 1; n <= numcols; n++) 1033 { 1034 out.println("<tr>"); 1035 1036 if (headers) { 1037 out.print("<td class=\"QueryUtil_HeaderCell\">"); 1038 out.println(metadata.getColumnLabel(n)); 1039 out.println("</td>"); 1040 } 1041 1042 while (rs.next()) { 1043 out.println("<td class=\"QueryUtil_Cell\">"); 1044 out.println(rs.getString(n)); 1045 out.println("</td>"); 1046 } 1047 1048 out.println("</tr>"); 1049 rs.beforeFirst(); 1050 } 1051 out.println("</table>"); 1052 } 1053 } 1054 1055 1056/** 1057Specifies the orientation of the result set when printed (via 1058methods like {@link printResultSet()}). 1059**/ 1060public static final class ResultSetPrintDirection 1061 { 1062 /** 1063 The result set will be printed in the typical table format 1064 with the columns going across the page and the rows going 1065 downwards. 1066 **/ 1067 public static final ResultSetPrintDirection HORIZONTAL = 1068 new ResultSetPrintDirection("QueryUtil.ResultSetPrintDirection.HORIZONTAL"); 1069 1070 /** 1071 The result set will be printed with columns going downwards 1072 and rows going across the page. 1073 **/ 1074 public static final ResultSetPrintDirection VERTICAL = 1075 new ResultSetPrintDirection("QueryUtil.ResultSetPrintDirection.VERTICAL"); 1076 1077 private String name; 1078 private ResultSetPrintDirection(String name) { 1079 this.name = name; 1080 } 1081 public String toString() { 1082 return name; 1083 } 1084 } //~ResultSetPrintDirection 1085 1086/* this is not really needed for anything so taken out 1087 1088//Used to specify the printing options (used by methods that print 1089//result sets for example). Printed fields and records are simply seperated 1090//by field and record seperators respectively. 1091 1092public static class SimplePrintConfig 1093 { 1094 The newline for this JVM's platform. Used to separate horizontal 1095 records by default. 1096 public static final String NEWLINE = IOUtil.LINE_SEP; 1097 1098 Used to separate vertical records by default. 1099 public static final String COMMA = ","; 1100 1101 private String fieldsep = COMMA; 1102 private String recsep = NEWLINE; 1103 private String name; 1104 1105 public SimplePrintConfig() { 1106 } 1107 1108 Sets the string used to separate records. The default value is: 1109 {@link #NEWLINE}. 1110 public void setRecordSeperator(String str) { 1111 recsep = str; 1112 } 1113 1114 Sets the string used to separate fields. The default value is: 1115 {@link #COMMA}. 1116 public void setFieldSeperator(String str) { 1117 fieldsep = str; 1118 } 1119 1120 public String getRecordSeperator() { 1121 return recsep; 1122 } 1123 1124 public String getFieldSeperator() { 1125 return fieldsep; 1126 } 1127 1128 Prints a short description of this object. Field and record 1129 seperators containing control characters are printed as 1130 readable equivalents. 1131 public String toString() { 1132 return "SimplePrintConfig: Field seperator=" + 1133 StringUtil.viewableAscii(fieldsep) + 1134 "; Record seperator=" + 1135 StringUtil.viewableAscii(recsep); 1136 } 1137 1138 } //~SimplePrintConfig 1139 1140*/ 1141 1142} //~class QueryUtil