///////////////////////////////////////////////////////////////////////////////
//                                                                             
// JTOpen (IBM Toolbox for Java - OSS version)                              
//                                                                             
// Filename: SQLResultSetData.java
//                                                                             
// The source code contained herein is licensed under the IBM Public License   
// Version 1.0, which has been approved by the Open Source Initiative.         
// Copyright (C) 1997-2000 International Business Machines Corporation and     
// others. All rights reserved.                                                
//                                                                             
///////////////////////////////////////////////////////////////////////////////

package com.ibm.as400.vaccess;

import com.ibm.as400.access.Trace;
import java.io.IOException;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Types;
import java.util.Vector;



/**
The SQLResultSetData class represents the result
set generated by a SQL query using JDBC.
This class handles caching and storing the data fields so they can be
retrieved and displayed by different views.

<p>The data is retrieved from the system when <i>load()</i> is called.

<p>It is up to the user to call <i>close()</i> when the
result set is no longer needed.

<p>Most errors are reported by firing ErrorEvents, rather
than throwing exceptions.  Users should listen for ErrorEvents
in order to diagnose and recover from error conditions.

<p>SQLResultSetData objects generate the following events:
<ul>
  <li>ErrorEvent
  <li>WorkingEvent
</ul>
**/
// Note that this class throws events from within synchronized blocks,
// which could cause hangs if the handlers of these events do
// operations from a different thread an attempt to access another
// synchronized piece of code.
// At this time this seems to be an acceptable risk, since the
// events thrown are not likely to need enough processing to
// require another thread, and getting having the events thrown
// from outside a sychronized block would be nearly impossible.
// The other option is to have the firing of the events be done
// from another thread, but the overhead of creating another thread
// not only takes resources, but also delays the delivery of the event.
class SQLResultSetData
implements Serializable
{
  private static final String copyright = "Copyright (C) 1997-2000 International Business Machines Corporation and others.";


// What this result set represents
private SQLConnection connection_ = null;
private String query_ = null;

// The result set information
transient private Object[][] data_ = null;      // the result set data
transient private ResultSet resultSet_ = null;  // result set for this data
transient private Statement stmt_ = null;
transient private boolean resourceOpen_ = false;// jdbc resources open

// Column information
transient private int numColumns_ = 0;
transient private ResultSetMetaData resultSetMetaData_ = null;  // describes result set columns

// Row information
// Number of rows data_ is bumped by when more space needed. 100 is arbitrary.
private static int ROW_INCREMENT_ = 100;
// Number of rows in the table. Always 0 until allRecordsRead_ = true.
transient private int numRows_ = 0;
// The index of the last record read.  Up to this row is valid in data_.
transient private int lastRecordRead_ = -1;
// If all records in resultSet_ have been transferred to data_.
transient private boolean allRecordsRead_ = true;

// Warnings
transient private SQLWarning warnings_;

// Flag for if an error event was sent.
transient private boolean error_;

// Event support.
transient private ErrorEventSupport errorListeners_
    = new ErrorEventSupport (this);
transient private WorkingEventSupport workingListeners_
    = new WorkingEventSupport (this);

private boolean fireWorkingEvents_ = true;

/**
Constructs a SQLResultSetData object.
**/
public SQLResultSetData ()
{
    super();
}


/**
Adds a listener to be notified when an error occurs.
The listener's errorOccurred method will be called.

@param  listener  The listener.
**/
public void addErrorListener (ErrorListener listener)
{
    errorListeners_.addErrorListener (listener);
}



/**
Adds a listener to be notified when work starts and stops
on potentially long-running operations.

@param  listener  The listener.
**/
public void addWorkingListener (WorkingListener listener)
{
    workingListeners_.addWorkingListener (listener);
}


/**
Closes the JDBC resources.
**/
synchronized public void close ()
{
    if (resourceOpen_)
    {
        try
        {            
            resultSet_.close();
        }
        catch (SQLException e)
        {
            errorListeners_.fireError(e);
        }
    }
    if (stmt_ != null)
    {
        try
        {
            stmt_.close();
        }
        catch (SQLException e)
        {
            errorListeners_.fireError(e);
        }
    }
    resourceOpen_ = false;
}



/**
Clears all SQL warnings.
**/
public void clearWarnings ()
{
    warnings_ = null;
    if (resultSet_ != null)
    {
        try
        {
            resultSet_.clearWarnings();
        }
        catch (SQLException e)
        {
            errorListeners_.fireError(e);
        }
    }
}


/**
Returns whether all records have been processed (stored in a local
cache).

@return true if all records have been processed; false otherwise.
**/
synchronized public boolean getAllRecordsProcessed ()
{
    return allRecordsRead_;
}



/**
Returns the display size for the column at the specified index.
Indices start at 0.

@param index Index of the column.
@return The display size for the column.
**/
synchronized public int getColumnDisplaySize(int index)
{
    if (resultSet_ == null)
    {
        throw new IndexOutOfBoundsException("index");
    }

    int result = 0;
    try
    {
        // For most types, we just consult with JDBC.  However, for times, dates, and          @C1A
        // timestamps, JDBC reports the display size for the server format.  And these         @C1A
        // GUIs actually internationalize the Strings before displaying them.  So for          @C1A
        // these types, we need to compute our own display sizes.                              @C1A
        switch(resultSetMetaData_.getColumnType(index+1)) {                                 // @C1A
        case Types.TIME:                                                                    // @C1A
            result = DBDateCellRenderer.getDisplaySize(DBDateCellRenderer.FORMAT_TIME);     // @C1A
            break;                                                                          // @C1A
        case Types.DATE:                                                                    // @C1A
            result = DBDateCellRenderer.getDisplaySize(DBDateCellRenderer.FORMAT_DATE);     // @C1A
            break;                                                                          // @C1A
        case Types.TIMESTAMP:                                                               // @C1A
            result = DBDateCellRenderer.getDisplaySize(DBDateCellRenderer.FORMAT_TIMESTAMP);// @C1A
            break;                                                                          // @C1A
        default:                                                                            // @C1A
            result = resultSetMetaData_.getColumnDisplaySize(index+1);                     
        }                                                                                   // @C1A
    }
    catch (SQLException e)
    {
        errorListeners_.fireError(e);
    }
    return result;
}



/**
Returns the label for the column at the specified index.
Indices start at 0.

@param index Index of the column.
@return The label for the column.
**/
synchronized public String getColumnLabel(int index)
{
    if (resultSet_ == null)
    {
        throw new IndexOutOfBoundsException("index");
    }

    try
    {
        return resultSetMetaData_.getColumnLabel(index+1);
    }
    catch (SQLException e)
    {
        errorListeners_.fireError(e);
        return "";
    }
}


/**
Returns the name for the column at the specified index.
Indices start at 0.

@param index Index of the column.
@return The name for the column.
**/
synchronized public String getColumnName(int index)
{
    if (resultSet_ == null)
    {
        throw new IndexOutOfBoundsException("index");
    }

    try
    {
        return resultSetMetaData_.getColumnName(index+1);
    }
    catch (SQLException e)
    {
        errorListeners_.fireError(e);
        return "";
    }
}


/**
Returns the SQL type of the data in this column.
Indices start at 0.

@param index Index of the column.
@return The SQL type of data in this column.
**/
synchronized public int getColumnType(int index)
{
    if (resultSet_ == null)
    {
        throw new IndexOutOfBoundsException("index");
    }

    try
    {
        return resultSetMetaData_.getColumnType(index+1);
    }
    catch (SQLException e)
    {
        errorListeners_.fireError(e);
        return Types.CHAR;
    }
}



/**
Returns the SQL connection used when <i>load()</i> is called..

@return The SQL connection.
**/
public SQLConnection getConnection()
{
    return connection_;
}



/**
Returns the index of the last record which had been processed (tored
in the local cache). -1 indicates no records have been
processed.

@return The index of the last record processed.
**/
synchronized public int getLastRecordProcessed ()
{
    return lastRecordRead_;
}


/**
Returns the number of fields in this result set.
If an error occurs, 0 is returned.

@return The number of fields in this result set.
**/
synchronized public int getNumberOfColumns()
{
    return numColumns_;
}


/**
Returns the number of rows in the result set.

@return The number of rows in the result set.
**/
synchronized public int getNumberOfRows()
{
    if (!allRecordsRead_)
    {
        readAllRecords();
    }
    return numRows_;
}


/**
Returns the SQL query used to get the result set when <i>load()</i>
is called.

@return The SQL query.
**/
public String getQuery ()
{
    return query_;
}



/**
Returns the value at the specifed column and row.
Indices start at 0.
If an error occurs, an empty string is returned.

@param  rowIndex            The row index.
@param  columnIndex         The column index.
@return The value at the specified column and row.
**/
synchronized public Object getValueAt (int rowIndex,
                          int columnIndex)
{
    if (resultSet_ == null)
    {
        throw new IndexOutOfBoundsException("rowIndex");
    }

        // If we haven't yet transferred this row from the
        // result set to the table cache, do so.
        if (lastRecordRead_ < rowIndex)
        {
            readMoreRecords(rowIndex - lastRecordRead_);
        }
        if (lastRecordRead_ < rowIndex)
        {
            throw new IndexOutOfBoundsException("rowIndex");
        }

    // return the value
    return data_[rowIndex][columnIndex];
}


/**
Returns the warnings generated by the SQL connection, statement, and
result set.
Warnings are cleared when <i>load</i> or </i>clearWarnings</i>
is called.
The warnings from the result set will be
linked to the end of any statement warnings, which in turn are linked
to the end of any connection warnings.

@return The warnings generated by the connection, statement, and
result set, or null if none.
**/
synchronized public SQLWarning getWarnings ()
{
    SQLWarning result = null;
    // If no result set, we are not in a state to have warnings.
    if (resultSet_ != null)
    {
        // get result set warnings
        SQLWarning rs_warnings = null;
        try
        {
            rs_warnings = resultSet_.getWarnings();
        }
        catch (SQLException e)
        {
            // pass on error, but continue
            errorListeners_.fireError(e);
        }
         // If connection or statement warnings...
        if (warnings_ != null)
        {
            if (rs_warnings != null)
            {
                // We have both connection and statement warnings.
                // Find the last warning in the connection chain.
                SQLWarning last = warnings_;
                SQLWarning next;
                while ((next = last.getNextWarning()) != null)
                {
                    last = next;
                }
                // Add statement warnings to the end of connection chain.
                last.setNextWarning(rs_warnings);
            }
            result = warnings_;
        }
        else
        {
            // otherwise, just return result set warnings
            result = rs_warnings;
        }
    }
    return result;
}


/**
Loads the table based on the state of the system.  This causes the
query to be run.
**/
synchronized public void load ()
{
    Trace.log(Trace.DIAGNOSTIC, "Doing data load.");

    if (connection_ == null || query_ == null)
    {
        Trace.log(Trace.DIAGNOSTIC, "Data not set for load, exiting.");
        return;
    }

    if (fireWorkingEvents_)
        workingListeners_.fireStartWorking ();

    // cleanup old data
    if (resourceOpen_)
    {
        try
        {
            resultSet_.close();
            // leave statement open
        }
        catch (SQLException e)
        {
            errorListeners_.fireError(e);
        }
        resourceOpen_ = false;
    }

    // Set back fields in case there is an error.
    resultSetMetaData_ = null;
    numColumns_ = 0;
    lastRecordRead_ = -1;
    allRecordsRead_ = true;
    data_ = null;
    numRows_ = 0;

    // get data
    try
    {
            // Create statement
            if (stmt_ == null)
            {
                Connection conn = connection_.getConnection();
                stmt_ = conn.createStatement();
                // Save warnings from connection.
                try
                {
                    warnings_ = conn.getWarnings();
                }
                catch (SQLException e)
                {  // pass on errors but continue
                    errorListeners_.fireError(e);
                }
            }
            // Get result set
            resultSet_ = stmt_.executeQuery(query_);        
            resourceOpen_ = true;
            // Save warnings from statement.
            try
            {
                SQLWarning s_warnings = stmt_.getWarnings();
                if (s_warnings != null)
                {
                    if (warnings_ == null)
                    {
                        // statement warnings but no connection warnings
                        warnings_ = s_warnings;
                    }
                    else
                    {
                        // We have both connection and statement warnings.
                        // Find the last warning in the connection chain.
                        SQLWarning last = warnings_;
                        SQLWarning next;
                        while ((next = last.getNextWarning()) != null)
                        {
                            last = next;
                        }
                        // Add statement warnings to the end of connection chain.
                        last.setNextWarning(s_warnings);
                    }
                }
            }
            catch (SQLException e)
            {  // pass on errors but continue
                errorListeners_.fireError(e);
            }
        
        // get info about the columns returned
        resultSetMetaData_ = resultSet_.getMetaData();
        numColumns_ = resultSetMetaData_.getColumnCount();

        // Move cursor to first record in result set.
        // Note that if we can't get to first record, we scrap
        // everything and fail.
        if (!resultSet_.next())
        {
            // no records
            try
            {
                    resultSet_.close();
            }
            catch(SQLException e)
            {
                // pass on error, continue
                errorListeners_.fireError(e);
            }
        }
        else // there are records, but we don't know how many
        {
            allRecordsRead_ = false;
            // Create new array to hold table values.
                data_ = new Object[ROW_INCREMENT_][numColumns_];
        }
    }
    catch (SQLException e)
    {
        // In case of error, set fields to init state
        resultSet_ = null;
        resultSetMetaData_ = null;
        numColumns_ = 0;
        errorListeners_.fireError(e);
        error_ = true;
    }

    // Note we don't process any of the result set until the
    // values are asked for.

    if (fireWorkingEvents_)
        workingListeners_.fireStopWorking ();
}



/**
Processes records in the result set, moving the data into local cache
so they can be retrieved via <i>getValueAt</i>.
**/
synchronized public void readAllRecords()
{                                                                                
        Trace.log(Trace.DIAGNOSTIC, "Reading all rows.");
        workingListeners_.fireStartWorking ();
        error_ = false;
        while (!allRecordsRead_ && !error_)
        {
            fireWorkingEvents_ = false;
            readMoreRecords(ROW_INCREMENT_);
            fireWorkingEvents_ = true;
        }
        workingListeners_.fireStopWorking ();
}


/**
Processes records in the result set, moving the data into local cache
so they can be retrieved via <i>getValueAt</i>.

@param numberToRead The number of records to process.
**/
synchronized public void readMoreRecords(int numberToRead)
{
        Trace.log(Trace.DIAGNOSTIC, "Reading more rows:", numberToRead);
        if (fireWorkingEvents_)
            workingListeners_.fireStartWorking ();
    
        error_ = false;
         // read only load done & more records to read
        if (!allRecordsRead_)
        {
            // Make sure we have room in data_ for records.
            if (lastRecordRead_ + numberToRead  >= data_.length)
            {
                // increase by the greater of our increment or the number
                // of rows needed.
                int sizeNeeded = lastRecordRead_ + numberToRead + 1;
                int increment = ROW_INCREMENT_>sizeNeeded?ROW_INCREMENT_:sizeNeeded;
                Object[][] newData =
                    new Object[data_.length + increment][numColumns_];
                System.arraycopy(data_, 0, newData, 0, data_.length);
                data_ = newData;
            }
    
            // Try to avoid sending many errors if they are all the same.
            java.util.Vector errors = null;
            boolean dupErr;
            for (int i=1; i<=numberToRead; ++i)
            {
                for (int j=1; j<=numColumns_; ++j)
                {
                    try
                    {
                        // Note resultSet_ is always valid since load()
                        // and this loop ensure the next record is valid
                        // but do not transfer the data into data_.
                        data_[lastRecordRead_+1][j-1] = resultSet_.getObject(j);
                    }
                    catch (SQLException e)
                    {
                        // We don't set error_=true, since we are
                        // continuing to process, and are not aborting.
                        data_[lastRecordRead_+1][j-1] = null; // set data to null
                        // Don't send this error if it is a duplicate.
                        dupErr = false;
                        if (errors == null)  // this is the first error
                        {
                            errors = new java.util.Vector();
                            errors.addElement(e);
                        }
                        else  // we have had previous errors, look for duplicate
                        {
                            SQLException e1;
                            for (int k=0; k< errors.size(); ++k)
                            {
                                e1 = (SQLException)errors.elementAt(k);
                                if (e.getSQLState().equals(e1.getSQLState()) &&
                                    e.getErrorCode() == e1.getErrorCode() &&
                                    e.getMessage().equals(e1.getMessage()))
                                {
                                    dupErr = true;
                                    Trace.log(Trace.DIAGNOSTIC,
                                        "Duplicate error getting column value, swallowing error: " +
                                        e.getSQLState() +" : "+  e.getErrorCode() +" : "+  e.getMessage());
                                    break;
                                }
                            }
                            errors.addElement(e);
                        }
                        if (!dupErr)
                            errorListeners_.fireError(e);
                    }
                }
                ++lastRecordRead_;
                // Move cursor to next record.
                boolean valid = false;
                try
                {
                    valid = resultSet_.next();
                }
                catch(SQLException e)
                {
                    errorListeners_.fireError(e);
                    error_ = true;
                    valid = false;
                }
                if (!valid)
                {
                    // No more records or error reading next record.
                    allRecordsRead_ = true;
                    try
                    {
                            resultSet_.close();
                        resourceOpen_ = false;
                        // leave statement open
                    }
                    catch(SQLException e)
                    {
                        // fire error, continue
                        errorListeners_.fireError(e);
                    }
                    numRows_ = lastRecordRead_+1;
                    break;
                }
            }
        } // end if able to read records
    
        if (fireWorkingEvents_)
            workingListeners_.fireStopWorking ();
}


/**
Restore the state of this object from an object input stream.
It is used when deserializing an object.
@param in The input stream of the object being deserialized.
@exception IOException
@exception ClassNotFoundException
**/
private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException
{
    // Restore the non-static and non-transient fields.
    in.defaultReadObject();
    // Initialize the transient fields.
    data_ = null;
    resultSet_ = null;
    stmt_ = null;
    resourceOpen_ = false;
    numColumns_ = 0;
    resultSetMetaData_ = null;
    numRows_ = 0;
    lastRecordRead_ = -1;
    allRecordsRead_ = true;
    errorListeners_ = new ErrorEventSupport (this);
    workingListeners_ = new WorkingEventSupport (this);
}


/**
Removes a listener from being notified when an error occurs.

@param  listener  The listener.
**/
public void removeErrorListener (ErrorListener listener)
{
    errorListeners_.removeErrorListener (listener);
}


/**
Removes a listener from being notified when work starts and stops.

@param  listener  The listener.
**/
public void removeWorkingListener (WorkingListener listener)
{
    workingListeners_.removeWorkingListener (listener);
}

/**
Constructs a SQLResultSetData object.
The new attribute won't go into effect until <i>load()</i>.

@param       connection   The SQL connection.
**/
public void setConnection(SQLConnection connection)
{
    if (connection != connection_)
    {
        connection_ = connection;
        if (stmt_ != null)
        {
            try
            {
                stmt_.close();
            }
            catch (SQLException e)
            {
                errorListeners_.fireError(e);
            }
        }
        stmt_ = null;
        warnings_ = null;
    }
}

/**
Constructs a SQLResultSetData object.
The new attribute won't go into effect until <i>load()</i>.

@param       query        The SQL query.
**/
public void setQuery(String query)
{
    query_ = query;
}




}
