/* $Id: JDBCConnection.java 988245 2010-08-23 18:39:35Z kwright $ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.jdbc;

import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.core.database.*;
import org.apache.manifoldcf.core.jdbcpool.*;
import org.apache.manifoldcf.agents.interfaces.*;

import java.nio.charset.StandardCharsets;
import java.sql.*;
import javax.naming.*;
import javax.sql.*;

import java.io.*;
import java.util.*;

/** This object describes a connection to a particular JDBC instance.
*/
public class JDBCConnection
{
  public static final String _rcsid = "@(#)$Id: JDBCConnection.java 988245 2010-08-23 18:39:35Z kwright $";

  protected String jdbcProvider = null;
  protected boolean useName;
  protected String driverString = null;
  protected String userName = null;
  protected String password = null;

  /** Constructor.
  */
  public JDBCConnection(String jdbcProvider, boolean useName, String host, String databaseName, String rawDriverString,
    String userName, String password)
    throws ManifoldCFException
  {
    this.jdbcProvider = jdbcProvider;
    this.useName = useName;
    this.driverString = JDBCConnectionFactory.getJDBCDriverString(jdbcProvider, host, databaseName, rawDriverString);
    this.userName = userName;
    this.password = password;
  }

  protected static IDynamicResultRow readNextResultRowViaThread(ResultSet rs, ResultSetMetaData rsmd, String[] resultCols)
    throws ManifoldCFException, ServiceInterruption
  {
    NextResultRowThread t = new NextResultRowThread(rs,rsmd,resultCols);
    try
    {
      t.start();
      return t.finishUp();
    }
    catch (InterruptedException e)
    {
      t.interrupt();
      throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
    }
  }

  protected static class NextResultRowThread extends Thread
  {
    protected ResultSet rs;
    protected ResultSetMetaData rsmd;
    protected String[] resultCols;

    protected Throwable exception = null;
    protected IDynamicResultRow response = null;

    public NextResultRowThread(ResultSet rs, ResultSetMetaData rsmd, String[] resultCols)
    {
      super();
      setDaemon(true);
      this.rs = rs;
      this.rsmd = rsmd;
      this.resultCols = resultCols;
    }

    public void run()
    {
      try
      {
        response = readNextResultRow(rs,rsmd,resultCols);
      }
      catch (Throwable e)
      {
        this.exception = e;
      }
    }

    public IDynamicResultRow finishUp()
      throws ManifoldCFException, ServiceInterruption, InterruptedException
    {
      join();
      Throwable thr = exception;
      if (thr != null)
      {
        if (thr instanceof java.sql.SQLException)
          throw new ManifoldCFException("Error fetching next JDBC result row: "+thr.getMessage(),thr);
        else if (thr instanceof ManifoldCFException)
          throw (ManifoldCFException)thr;
        else if (thr instanceof ServiceInterruption)
          throw (ServiceInterruption)thr;
        else if (thr instanceof RuntimeException)
          throw (RuntimeException)thr;
        else
          throw (Error)thr;
      }
      return response;
    }
    
  }

  protected static IDynamicResultRow readNextResultRow(ResultSet rs, ResultSetMetaData rsmd, String[] resultCols)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      if (rs.next())
      {
        return readResultRow(rs,rsmd,resultCols);
      }
      return null;
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Result set error: "+e.getMessage(),e);
    }
  }

  protected static void closeResultset(ResultSet rs)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      rs.close();
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Exception closing resultset: "+e.getMessage(),e);
    }
  }

  protected static void closeStmt(Statement stmt)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      stmt.close();
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Exception closing statement: "+e.getMessage(),e);
    }
  }

  protected static void closePS(PreparedStatement ps)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      ps.close();
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Exception closing statement: "+e.getMessage(),e);
    }
  }


  /** Test connection.
  */
  public void testConnection()
    throws ManifoldCFException, ServiceInterruption
  {
    TestConnectionThread t = new TestConnectionThread();
    try
    {
      t.start();
      t.finishUp();
    }
    catch (InterruptedException e)
    {
      t.interrupt();
      throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
    }
  }

  protected class TestConnectionThread extends Thread
  {
    protected Throwable exception = null;

    public TestConnectionThread()
    {
      super();
      setDaemon(true);
    }

    public void run()
    {
      try
      {
        WrappedConnection tempConnection = JDBCConnectionFactory.getConnection(jdbcProvider,driverString,userName,password);
        JDBCConnectionFactory.releaseConnection(tempConnection);
      }
      catch (Throwable e)
      {
        this.exception = e;
      }
    }

    public void finishUp()
      throws ManifoldCFException, ServiceInterruption, InterruptedException
    {
      join();
      Throwable thr = exception;
      if (thr != null)
      {
        if (thr instanceof java.sql.SQLException)
          throw new ManifoldCFException("Error doing JDBC connection test: "+thr.getMessage(),thr);
        else if (thr instanceof ManifoldCFException)
          throw (ManifoldCFException)thr;
        else if (thr instanceof ServiceInterruption)
          throw (ServiceInterruption)thr;
        else if (thr instanceof RuntimeException)
          throw (RuntimeException)thr;
        else
          throw (Error)thr;
      }
    }
  }

  /** Execute query.
  */
  public IDynamicResultSet executeUncachedQuery(String query, ArrayList params, int maxResults)
    throws ManifoldCFException, ServiceInterruption
  {
    if (params == null)
      return new JDBCResultSet(query,maxResults);
    else
      return new JDBCPSResultSet(query,params,maxResults);
  }

  /** Execute operation.
  */
  public void executeOperation(String query, ArrayList params)
    throws ManifoldCFException, ServiceInterruption
  {
    ExecuteOperationThread t = new ExecuteOperationThread(query,params);
    try
    {
      t.start();
      t.finishUp();
    }
    catch (InterruptedException e)
    {
      t.interrupt();
      throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
    }
  }

  /** Read object as a string */
  public static String readAsString(Object o)
    throws ManifoldCFException
  {
    if (o instanceof BinaryInput)
    {
      // Convert this input to a string, since mssql can mess us up with the wrong column types here.
      BinaryInput bi = (BinaryInput)o;
      try
      {
        InputStream is = bi.getStream();
        try
        {
          InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
          StringBuilder sb = new StringBuilder();
          while (true)
          {
            int x = reader.read();
            if (x == -1)
              break;
            sb.append((char)x);
          }
          return sb.toString();
        }
        finally
        {
          is.close();
        }
      }
      catch (IOException e)
      {
        throw new ManifoldCFException(e.getMessage(),e);
      }
      finally
      {
        bi.doneWithStream();
      }
    }
    else if (o instanceof CharacterInput)
    {
      CharacterInput ci = (CharacterInput)o;
      try
      {
        Reader reader = ci.getStream();
        try
        {
          StringBuilder sb = new StringBuilder();
          while (true)
          {
            int x = reader.read();
            if (x == -1)
              break;
            sb.append((char)x);
          }
          return sb.toString();
        }
        finally
        {
          reader.close();
        }
      }
      catch (IOException e)
      {
        throw new ManifoldCFException(e.getMessage(),e);
      }
      finally
      {
        ci.doneWithStream();
      }
    }
    else
    {
      return o.toString();
    }
  }

  protected class ExecuteOperationThread extends Thread
  {
    protected String query;
    protected ArrayList params;

    protected Throwable exception = null;

    public ExecuteOperationThread(String query, ArrayList params)
    {
      super();
      setDaemon(true);
      this.query = query;
      this.params = params;
    }

    public void run()
    {
      try
      {
        WrappedConnection tempConnection = JDBCConnectionFactory.getConnection(jdbcProvider,driverString,userName,password);
        try
        {
          execute(tempConnection.getConnection(),query,params,false,0,useName);
        }
        finally
        {
          JDBCConnectionFactory.releaseConnection(tempConnection);
        }
      }
      catch (Throwable e)
      {
        this.exception = e;
      }
    }

    public void finishUp()
      throws ManifoldCFException, ServiceInterruption, InterruptedException
    {
      join();
      Throwable thr = exception;
      if (thr != null)
      {
        if (thr instanceof java.sql.SQLException)
          throw new ManifoldCFException("Exception doing connector query '"+query+"': "+thr.getMessage(),thr);
        else if (thr instanceof ManifoldCFException)
          throw (ManifoldCFException)thr;
        else if (thr instanceof ServiceInterruption)
          throw (ServiceInterruption)thr;
        else if (thr instanceof RuntimeException)
          throw (RuntimeException)thr;
        else
          throw (Error)thr;
      }
    }
  }

  /** Run a query.  No caching is involved at all at this level.
  * @param query String the query string
  * @param maxResults is the maximum number of results to load: -1 if all
  * @param params ArrayList if params !=null, use preparedStatement
  */
  protected static IResultSet execute(Connection connection, String query, ArrayList params, boolean bResults, int maxResults, boolean useName)
    throws ManifoldCFException, ServiceInterruption
  {

    ResultSet rs;

    try
    {

      if (params==null)
      {
        // lightest statement type
        Statement stmt = connection.createStatement();
        try
        {
          stmt.execute(query);
          rs = stmt.getResultSet();
          try
          {
            // Suck data from resultset
            if (bResults)
              return getData(rs,maxResults,useName);
            return null;
          }
          finally
          {
            if (rs != null)
              rs.close();
          }
        }
        finally
        {
          stmt.close();
        }
      }
      else
      {
        PreparedStatement ps = connection.prepareStatement(query);
        try
        {
          loadPS(ps, params);

          if (bResults)
          {
            rs = ps.executeQuery();
            try
            {
              // Suck data from resultset
              return getData(rs,maxResults,useName);
            }
            finally
            {
              if (rs != null)
                rs.close();
            }
          }
          else
          {
            ps.executeUpdate();
            return null;
          }

        }
        finally
        {
          ps.close();
          cleanupParameters(params);
        }
      }

    }
    catch (ManifoldCFException e)
    {
      throw e;
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Exception doing connector query '"+query+"': "+e.getMessage(),e);
    }
  }

  /** Read the current row from the resultset */
  protected static IDynamicResultRow readResultRow(ResultSet rs, ResultSetMetaData rsmd, String[] resultCols)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      Object value = null;
      RDynamicRow m = new RDynamicRow();

      // We have 'colcount' cols to look thru
      for (int i = 0; i < resultCols.length; i++)
      {
        String key = resultCols[i];
        // System.out.println("Key = "+key);
        int colnum = findColumn(rs,key);
        if (colnum > -1)
        {
          if (isBinaryData(rsmd,colnum))
          {
            InputStream bis = rs.getBinaryStream(colnum);
            if (bis != null)
            {
              try
              {
                value = new TempFileInput(bis);
              }
              catch (IOException e)
              {
                handleIOException(e,"reading binary data");
              }
            }
          }
          else if (isBLOB(rsmd,colnum))
          {
            // System.out.println("It's a blob!");
            Blob blob = getBLOB(rs,colnum);
            // Create a tempfileinput object!
            // Cleanup should happen by the user of the resultset.
            // System.out.println(" Blob length = "+Long.toString(blob.length()));
            if (blob != null)
            {
              try
              {
                value = new TempFileInput(blob.getBinaryStream(),blob.length());
              }
              catch (IOException e)
              {
                handleIOException(e,"reading blob");
              }
            }
          }
          else if (isCLOB(rsmd,colnum))
          {
            Clob clob = getCLOB(rs,colnum);
            if (clob != null)
            {
              try
              {
                value = new TempFileCharacterInput(clob.getCharacterStream(),clob.length());
              }
              catch (IOException e)
              {
                handleIOException(e,"reading clob");
              }
            }
          }
          else
          {
            // System.out.println("It's not a blob");
            value = getObject(rs,rsmd,colnum);
          }
        }
        if (value != null)
          m.put(key, value);
      }
      return m;

    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Resultset error: "+e.getMessage(),e);
    }
  }

  protected static void handleIOException(IOException e, String context)
    throws ManifoldCFException
  {
    if (e instanceof InterruptedIOException)
      throw new ManifoldCFException(e.getMessage(),e,ManifoldCFException.INTERRUPTED);
    throw new ManifoldCFException("IO exception while "+context+": "+e.getMessage(),e);
  }
  
  protected static String[] readColumnNames(ResultSetMetaData rsmd, boolean useName)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      String[] resultCols;
      if (rsmd != null)
      {
        int colcount = rsmd.getColumnCount();
        resultCols = new String[colcount];
        for (int i = 0; i < colcount; i++)
        {
          String name;
          if (useName)
            name = rsmd.getColumnName(i+1);
          else
            name = rsmd.getColumnLabel(i+1);
          resultCols[i] = name;
        }
      }
      else
        resultCols = new String[0];
      return resultCols;
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Sql exception reading column names: "+e.getMessage(),e);
    }
  }

  // Read data from a resultset
  protected static IResultSet getData(ResultSet rs, int maxResults, boolean useName)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      RSet results = new RSet();  // might be empty but not an error

      if (rs != null)
      {
        // Optionally we're going to suck the data
        // out of the db and return it in a
        // readonly structure
        ResultSetMetaData rsmd = rs.getMetaData();
        String[] resultCols = readColumnNames(rsmd, useName);
        if (resultCols.length == 0)
        {
          // This is an error situation; if a result with no columns is
          // necessary, bResults must be false!!!
          throw new ManifoldCFException("Empty query, no columns returned",ManifoldCFException.GENERAL_ERROR);
        }

        while (rs.next() && (maxResults == -1 || maxResults > 0))
        {
          IResultRow m = readResultRow(rs,rsmd,resultCols);
          if (maxResults != -1)
            maxResults--;
          results.addRow(m);
        }
      }
      return results;
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Resultset error: "+e.getMessage(),e);
    }
  }

  // pass params to preparedStatement
  protected static void loadPS(PreparedStatement ps, ArrayList data)
    throws java.sql.SQLException, ManifoldCFException
  {
    if (data!=null)
    {
      for (int i = 0; i < data.size(); i++)
      {
        // If the input type is a string, then set it as such.
        // Otherwise, if it's an input stream, we make a blob out of it.
        Object x = data.get(i);
        if (x instanceof String)
        {
          String value = (String)x;
          // letting database do lame conversion!
          ps.setString(i+1, value);
        }
        else if (x instanceof BinaryInput)
        {
          BinaryInput value = (BinaryInput)x;
          ps.setBinaryStream(i+1,value.getStream(),value.getLength());
          // Hopefully with the introduction of CharacterInput below, this hackery is no longer needed.
          // System.out.println("Blob length on write = "+Long.toString(value.getLength()));
          // The oracle driver does a binary conversion to base 64 when writing data
          // into a clob column using a binary stream operator.  Since at this
          // point there is no way to distinguish the two, and since our tests use CLOB,
          // this code doesn't work for them.
          // So, for now, use the ascii stream method.
          //ps.setAsciiStream(i+1,value.getStream(),value.getLength());
        }
        else if (x instanceof CharacterInput)
        {
          CharacterInput value = (CharacterInput)x;
          ps.setCharacterStream(i+1,value.getStream(),value.getCharacterLength());
        }
        else if (x instanceof java.util.Date)
        {
          ps.setDate(i+1,new java.sql.Date(((java.util.Date)x).getTime()));
        }
        else if (x instanceof Long)
        {
          ps.setLong(i+1,((Long)x).longValue());
        }
        else if (x instanceof TimeMarker)
        {
          ps.setTimestamp(i+1,new java.sql.Timestamp(((Long)x).longValue()));
        }
        else if (x instanceof Double)
        {
          ps.setDouble(i+1,((Double)x).doubleValue());
        }
        else if (x instanceof Integer)
        {
          ps.setInt(i+1,((Integer)x).intValue());
        }
        else if (x instanceof Float)
        {
          ps.setFloat(i+1,((Float)x).floatValue());
        }
        else
          throw new ManifoldCFException("Unknown data type: "+x.getClass().getName());
      }
    }
  }

  /** Permanently discard database object.
  */
  protected static void discardDatabaseObject(Object x)
    throws ManifoldCFException
  {
    if (x instanceof PersistentDatabaseObject)
    {
      PersistentDatabaseObject value = (PersistentDatabaseObject)x;
      value.discard();
    }
  }
  
  /** Call this method on every parameter or result object, when we're done with it, if it's possible that the object is a BLOB
  * or CLOB.
  */
  protected static void cleanupDatabaseObject(Object x)
    throws ManifoldCFException
  {
    if (x instanceof PersistentDatabaseObject)
    {
      PersistentDatabaseObject value = (PersistentDatabaseObject)x;
      value.doneWithStream();
    }
  }
  
  /** Clean up parameters after query has been triggered.
  */
  protected static void cleanupParameters(ArrayList data)
    throws ManifoldCFException
  {
    if (data != null)
    {
      for (Object x : data)
      {
        cleanupDatabaseObject(x);
      }
    }
  }

  protected static int findColumn(ResultSet rs, String name)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      return rs.findColumn(name);
    }
    catch (java.sql.SQLException e)
    {
      return -1;
    }
  }

  protected static Blob getBLOB(ResultSet rs, int col)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      return rs.getBlob(col);
    }
    catch (java.sql.SQLException sqle)
    {
      throw new ManifoldCFException("Error in getBlob("+col+"): "+sqle.getMessage(),sqle,ManifoldCFException.DATABASE_ERROR);
    }
  }

  protected static Clob getCLOB(ResultSet rs, int col)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      return rs.getClob(col);
    }
    catch (java.sql.SQLException sqle)
    {
      throw new ManifoldCFException("Error in getClob("+col+"): "+sqle.getMessage(),sqle,ManifoldCFException.DATABASE_ERROR);
    }
  }

  protected static boolean isBLOB(ResultSetMetaData rsmd, int col)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      int type = rsmd.getColumnType(col);
      return (type == java.sql.Types.BLOB);
    }
    catch (java.sql.SQLException sqle)
    {
      throw new ManifoldCFException("Error in isBlob("+col+"): "+sqle.getMessage(),sqle,ManifoldCFException.DATABASE_ERROR);
    }
  }

  protected static boolean isBinaryData(ResultSetMetaData rsmd, int col)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      int type = rsmd.getColumnType(col);
      return (type == java.sql.Types.VARBINARY ||
        type == java.sql.Types.BINARY || type == java.sql.Types.LONGVARBINARY);
    }
    catch (java.sql.SQLException sqle)
    {
      throw new ManifoldCFException("Error in isBinaryData("+col+"): "+sqle.getMessage(),sqle,ManifoldCFException.DATABASE_ERROR);
    }
  }

  protected static boolean isCLOB(ResultSetMetaData rsmd, int col)
    throws ManifoldCFException, ServiceInterruption
  {
    try
    {
      int type = rsmd.getColumnType(col);
      return (type == java.sql.Types.CLOB || type == java.sql.Types.LONGVARCHAR);
    }
    catch (java.sql.SQLException sqle)
    {
      throw new ManifoldCFException("Error in isClob("+col+"): "+sqle.getMessage(),sqle,ManifoldCFException.DATABASE_ERROR);
    }
  }

  protected static Object getObject(ResultSet rs, ResultSetMetaData rsmd, int col)
    throws ManifoldCFException, ServiceInterruption
  {
    Object result = null;

    try
    {
      Timestamp timestamp;
      java.sql.Date date;
      Clob clob;
      String resultString;

      switch (rsmd.getColumnType(col))
      {
      case java.sql.Types.CHAR :
        if ((resultString = rs.getString(col)) != null)
        {
          if (rsmd.getColumnDisplaySize(col) < resultString.length())
          {
            result = resultString.substring(0,rsmd.getColumnDisplaySize(col));
          }
          else
            result = resultString;
        }
        break;
      case java.sql.Types.CLOB :
        if ((clob = rs.getClob(col)) != null)
        {
          result = clob.getSubString(1, (int) clob.length());
        }
        break;

      case java.sql.Types.BIGINT :
        long l = rs.getLong(col);
        if (!rs.wasNull())
          result = new Long(l);
        break;

      case java.sql.Types.INTEGER :
        int i = rs.getInt(col);
        if (!rs.wasNull())
          result = new Integer(i);
        break;

      case java.sql.Types.REAL :
      case java.sql.Types.FLOAT :
        float f = rs.getFloat(col);
        if (!rs.wasNull())
          result = new Float(f);
        break;

      case java.sql.Types.DOUBLE :
        double d = rs.getDouble(col);
        if (!rs.wasNull())
          result = new Double(d);
        break;

      case java.sql.Types.DATE :
        if ((date = rs.getDate(col)) != null)
        {
          result = new java.util.Date(date.getTime());
        }
        break;

      case java.sql.Types.TIMESTAMP :
        if ((timestamp = rs.getTimestamp(col)) != null)
        {
          result = new TimeMarker(timestamp.getTime());
        }
        break;

      case java.sql.Types.BLOB:
      case java.sql.Types.VARBINARY:
      case java.sql.Types.BINARY:
      case java.sql.Types.LONGVARBINARY:
        throw new ManifoldCFException("Binary type is not a string, column = " + col,ManifoldCFException.GENERAL_ERROR);
        //break

      default :
        result = rs.getString(col);
        break;
      }
      if (rs.wasNull())
      {
        result = null;
      }
    }
    catch (java.sql.SQLException e)
    {
      throw new ManifoldCFException("Exception in getString(): "+e.getMessage(),e,ManifoldCFException.DATABASE_ERROR);
    }
    return result;
  }

  protected class JDBCResultSet implements IDynamicResultSet
  {
    protected WrappedConnection connection;
    protected Statement stmt;
    protected ResultSet rs;
    protected ResultSetMetaData rsmd;
    protected String[] resultCols;
    protected int maxResults;

    /** Constructor */
    public JDBCResultSet(String query, int maxResults)
      throws ManifoldCFException, ServiceInterruption
    {
      this.maxResults = maxResults;
      StatementQueryThread t = new StatementQueryThread(query);
      try
      {
        t.start();
        t.finishUp();
        connection = t.getConnection();
        stmt = t.getStatement();
        rs = t.getResultSet();
        rsmd = t.getResultSetMetaData();
        resultCols = t.getColumnNames();
      }
      catch (InterruptedException e)
      {
        t.interrupt();
        throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
      }
    }

    /** Get the next row from the resultset.
    *@return the immutable row description, or null if there is no such row.
    */
    public IDynamicResultRow getNextRow()
      throws ManifoldCFException, ServiceInterruption
    {
      if (maxResults == -1 || maxResults > 0)
      {
        IDynamicResultRow row = readNextResultRowViaThread(rs,rsmd,resultCols);
        if (row != null && maxResults != -1)
          maxResults--;
        return row;
      }
      return null;
    }

    /** Close this resultset.
    */
    public void close()
      throws ManifoldCFException, ServiceInterruption
    {
      ManifoldCFException rval = null;
      Error error = null;
      RuntimeException rtException = null;
      if (rs != null)
      {
        try
        {
          closeResultset(rs);
        }
        catch (ManifoldCFException e)
        {
          if (rval == null || e.getErrorCode() == ManifoldCFException.INTERRUPTED)
            rval = e;
        }
        catch (Error e)
        {
          error = e;
        }
        catch (RuntimeException e)
        {
          rtException = e;
        }
        finally
        {
          rs = null;
        }
      }
      if (stmt != null)
      {
        try
        {
          closeStmt(stmt);
        }
        catch (ManifoldCFException e)
        {
          if (rval == null || e.getErrorCode() == ManifoldCFException.INTERRUPTED)
            rval = e;
        }
        catch (Error e)
        {
          error = e;
        }
        catch (RuntimeException e)
        {
          rtException = e;
        }
        finally
        {
          stmt = null;
        }
      }
      if (connection != null)
      {
        try
        {
          JDBCConnectionFactory.releaseConnection(connection);
        }
        catch (Error e)
        {
          error = e;
        }
        catch (RuntimeException e)
        {
          rtException = e;
        }
        finally
        {
          connection = null;
        }
      }
      if (error != null)
        throw error;
      if (rtException != null)
        throw rtException;
      if (rval != null)
        throw rval;
    }

  }

  protected class StatementQueryThread extends Thread
  {
    protected String query;

    protected Throwable exception = null;
    protected WrappedConnection connection = null;
    protected Statement stmt = null;
    protected ResultSet rs = null;
    protected ResultSetMetaData rsmd = null;
    protected String[] resultCols = null;

    public StatementQueryThread(String query)
    {
      super();
      setDaemon(true);
      this.query = query;
    }

    public void run()
    {
      try
      {
        connection = JDBCConnectionFactory.getConnection(jdbcProvider,driverString,userName,password);
        // lightest statement type
        stmt = connection.getConnection().createStatement();
        stmt.execute(query);
        rs = stmt.getResultSet();
        rsmd = rs.getMetaData();
        resultCols = readColumnNames(rsmd,useName);
      }
      catch (Throwable e)
      {
        this.exception = e;
        if (rs != null)
        {
          try
          {
            closeResultset(rs);
          }
          catch (ManifoldCFException e2)
          {
            if (e2.getErrorCode() == ManifoldCFException.INTERRUPTED)
              this.exception = e2;
            // Ignore
          }
          catch (Throwable e2)
          {
            // We already have an exception to report.
            // Eat any other exceptions from closing
          }
          finally
          {
            rs = null;
          }
        }
        if (stmt != null)
        {
          try
          {
            closeStmt(stmt);
          }
          catch (ManifoldCFException e2)
          {
            if (e2.getErrorCode() == ManifoldCFException.INTERRUPTED)
              this.exception = e2;
            // Ignore
          }
          catch (Throwable e2)
          {
            // We already have an exception to report.
            // Eat any other exceptions from closing statements
          }
          finally
          {
            stmt = null;
          }
        }
        if (connection != null)
        {
          JDBCConnectionFactory.releaseConnection(connection);
          connection = null;
        }
      }
    }

    public void finishUp()
      throws ManifoldCFException, ServiceInterruption, InterruptedException
    {
      join();
      Throwable thr = exception;
      if (thr != null)
      {
        if (thr instanceof java.sql.SQLException)
          throw new ManifoldCFException("Exception doing connector query '"+query+"': "+thr.getMessage(),thr);
        else if (thr instanceof ManifoldCFException)
          throw (ManifoldCFException)thr;
        else if (thr instanceof ServiceInterruption)
          throw (ServiceInterruption)thr;
        else if (thr instanceof RuntimeException)
          throw (RuntimeException)thr;
        else
          throw (Error)thr;
      }
    }

    public WrappedConnection getConnection()
    {
      return connection;
    }

    public Statement getStatement()
    {
      return stmt;
    }

    public ResultSet getResultSet()
    {
      return rs;
    }

    public ResultSetMetaData getResultSetMetaData()
    {
      return rsmd;
    }

    public String[] getColumnNames()
    {
      return resultCols;
    }
  }

  protected class JDBCPSResultSet implements IDynamicResultSet
  {
    protected WrappedConnection connection;
    protected PreparedStatement ps;
    protected ResultSet rs;
    protected ResultSetMetaData rsmd;
    protected String[] resultCols;
    protected int maxResults;
    protected ArrayList params;

    /** Constructor */
    public JDBCPSResultSet(String query, ArrayList params, int maxResults)
      throws ManifoldCFException, ServiceInterruption
    {
      this.maxResults = maxResults;
      this.params = params;
      PreparedStatementQueryThread t = new PreparedStatementQueryThread(query,params);
      try
      {
        t.start();
        t.finishUp();
        connection = t.getConnection();
        ps = t.getPreparedStatement();
        rs = t.getResultSet();
        rsmd = t.getResultSetMetaData();
        resultCols = t.getColumnNames();
      }
      catch (InterruptedException e)
      {
        cleanupParameters(params);
        t.interrupt();
        throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
      }
    }

    /** Get the next row from the resultset.
    *@return the immutable row description, or null if there is no such row.
    */
    public IDynamicResultRow getNextRow()
      throws ManifoldCFException, ServiceInterruption
    {
      if (maxResults == -1 || maxResults > 0)
      {
        IDynamicResultRow row = readNextResultRowViaThread(rs,rsmd,resultCols);
        if (row != null && maxResults != -1)
          maxResults--;
        return row;
      }
      return null;
    }

    /** Close this resultset.
    */
    public void close()
      throws ManifoldCFException, ServiceInterruption
    {
      ManifoldCFException rval = null;
      Error error = null;
      RuntimeException rtException = null;
      if (rs != null)
      {
        try
        {
          closeResultset(rs);
        }
        catch (ServiceInterruption e)
        {
        }
        catch (ManifoldCFException e)
        {
          if (rval == null || e.getErrorCode() == ManifoldCFException.INTERRUPTED)
            rval = e;
        }
        catch (Error e)
        {
          error = e;
        }
        catch (RuntimeException e)
        {
          rtException = e;
        }
        finally
        {
          rs = null;
        }
      }
      if (ps != null)
      {
        try
        {
          closePS(ps);
        }
        catch (ServiceInterruption e)
        {
        }
        catch (ManifoldCFException e)
        {
          if (rval == null || e.getErrorCode() == ManifoldCFException.INTERRUPTED)
            rval = e;
        }
        catch (Error e)
        {
          error = e;
        }
        catch (RuntimeException e)
        {
          rtException = e;
        }
        finally
        {
          ps = null;
        }
      }
      if (connection != null)
      {
        try
        {
          JDBCConnectionFactory.releaseConnection(connection);
        }
        catch (Error e)
        {
          error = e;
        }
        catch (RuntimeException e)
        {
          rtException = e;
        }
        finally
        {
          connection = null;
        }
      }
      if (params != null)
      {
        try
        {
          cleanupParameters(params);
        }
        catch (ManifoldCFException e)
        {
          if (rval == null || e.getErrorCode() == ManifoldCFException.INTERRUPTED)
            rval = e;
        }
        catch (Error e)
        {
          error = e;
        }
        catch (RuntimeException e)
        {
          rtException = e;
        }
        finally
        {
          params = null;
        }
      }
      if (error != null)
        throw error;
      if (rtException != null)
        throw rtException;
      if (rval != null)
        throw rval;

    }

  }

  protected class PreparedStatementQueryThread extends Thread
  {
    protected ArrayList params;
    protected String query;

    protected WrappedConnection connection = null;
    protected Throwable exception = null;
    protected PreparedStatement ps = null;
    protected ResultSet rs = null;
    protected ResultSetMetaData rsmd = null;
    protected String[] resultCols = null;

    public PreparedStatementQueryThread(String query, ArrayList params)
    {
      super();
      setDaemon(true);
      this.query = query;
      this.params = params;
    }

    public void run()
    {
      try
      {
        connection = JDBCConnectionFactory.getConnection(jdbcProvider,driverString,userName,password);
        ps = connection.getConnection().prepareStatement(query);
        loadPS(ps, params);
        rs = ps.executeQuery();
        rsmd = rs.getMetaData();
        resultCols = readColumnNames(rsmd,useName);
      }
      catch (Throwable e)
      {
        this.exception = e;
        if (rs != null)
        {
          try
          {
            closeResultset(rs);
          }
          catch (ManifoldCFException e2)
          {
            if (e2.getErrorCode() == ManifoldCFException.INTERRUPTED)
              this.exception = e2;
          }
          catch (Throwable e2)
          {
          }
          finally
          {
            rs = null;
          }
        }
        if (ps != null)
        {
          try
          {
            closePS(ps);
          }
          catch (ManifoldCFException e2)
          {
            if (e2.getErrorCode() == ManifoldCFException.INTERRUPTED)
              this.exception = e2;
          }
          catch (Throwable e2)
          {
          }
          finally
          {
            ps = null;
          }
        }
        if (connection != null)
        {
          JDBCConnectionFactory.releaseConnection(connection);
          connection = null;
        }
      }
    }

    public void finishUp()
      throws ManifoldCFException, ServiceInterruption, InterruptedException
    {
      join();
      Throwable thr = exception;
      if (thr != null)
      {
        // Cleanup of parameters happens even if exception doing query
        cleanupParameters(params);
        if (thr instanceof java.sql.SQLException)
          throw new ManifoldCFException("Exception doing connector query '"+query+"': "+thr.getMessage(),thr);
        else if (thr instanceof ManifoldCFException)
          throw (ManifoldCFException)thr;
        else if (thr instanceof ServiceInterruption)
          throw (ServiceInterruption)thr;
        else if (thr instanceof RuntimeException)
          throw (RuntimeException)thr;
        else
          throw (Error)thr;
      }
    }

    public WrappedConnection getConnection()
    {
      return connection;
    }

    public PreparedStatement getPreparedStatement()
    {
      return ps;
    }

    public ResultSet getResultSet()
    {
      return rs;
    }

    public ResultSetMetaData getResultSetMetaData()
    {
      return rsmd;
    }

    public String[] getColumnNames()
    {
      return resultCols;
    }
  }

  /** Dynamic result row implementation */
  protected static class RDynamicRow extends RRow implements IDynamicResultRow
  {
    public RDynamicRow()
    {
      super();
    }
    
    /** Close this resultrow.
    */
    public void close()
      throws ManifoldCFException
    {
      // Discard everything permanently from the row
      Iterator<String> columns = getColumns();
      while (columns.hasNext())
      {
        String column = columns.next();
        Object o = getValue(column);
        discardDatabaseObject(o);
      }
    }

  }
  
}
