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