///////////////////////////////////////////////////////////////////////////////
//
// JTOpen (IBM Toolbox for Java - OSS version)
//
// Filename: AS400JDBCResultSetMetaData.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-2010 International Business Machines Corporation and
// others. All rights reserved.
//
///////////////////////////////////////////////////////////////////////////////

package com.ibm.as400.access;

import java.sql.Connection;
/* ifdef JDBC40
import java.sql.DatabaseMetaData;
endif */
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
/* ifdef JDBC40
import java.sql.Statement;
endif */


/**
<p>The AS400JDBCResultSetMetaData class describes the
columns in a result set.
**/
//
// Implementation notes:
//
// * I lifted the restriction that a result set be open when
//   using this object, specifically so it could be used after
//   preparing but before executing (via PreparedStatement.getMetaData()).
//
// * I also removed the reference to the result set as private
//   data.  This is again because of the need to create this object
//   before executing a query (via PreparedStatement.getMetaData()).
//
public class AS400JDBCResultSetMetaData
/* ifdef JDBC40
extends ToolboxWrapper
endif */
implements ResultSetMetaData
{
    static final String copyright = "Copyright (C) 1997-2010 International Business Machines Corporation and others.";

    // Private static final ints
    // Searchable constants
    //@G1A @G2C
    static final int SQL_UNSEARCHABLE       = 0xF0;  // isSearchable = false
    static final int SQL_LIKE_ONLY          = 0xF1;  // will not be returned by our IBM i system
    static final int SQL_ALL_EXCEPT_LIKE    = 0xF2;  // isSearchable = true
    static final int SQL_SEARCHABLE         = 0xF3;  // isSearchable = true

    // Updateable constants
    //@G1A @G2C
    static final int SQL_READ_ONLY          = 0xF0;  // isReadOnly = true, isWriteable = false
    static final int SQL_WRITE_CAPABLE      = 0xF1;  // isReadOnly = false, isWriteable = true
    static final int SQL_READ_WRITE_UNKNOWN = 0xF2;  // will not be returned by our IBM i system

    // Private data.
    private String              catalog_;
    private int                 concurrency_;
    private String              cursorName_;
    private JDRow               row_;
    private DBExtendedColumnDescriptors extendedColumnDescriptors_;   //@G1A
    private ConvTable           convTable_;                           //@G1A
    private Connection          con_;                                 //@in1
    SQLConversionSettings       settings_;  /*@Q8A*/
    
    /**
    Constructs an AS400JDBCResultSetMetaData object.
    @param  catalog                     The catalog.
    @param  concurrency                 The result set concurrency.
    @param  cursorName                  The cursor name.
    @param  row                         The row.
    @param  extendedColumnDescriptors   The extended column descriptors.
    @param  convTable                   The converter table to use to convert column descriptors.
     * @throws SQLException 
    **/
    AS400JDBCResultSetMetaData(String catalog,
                                int concurrency,
                                String cursorName,
                                JDRow row,
                                DBExtendedColumnDescriptors extendedColumnDescriptors,   //@G1A
                                ConvTable convTable,                                     //@G1A
                                Connection con) throws SQLException                                 //@in1
    {
        catalog_        = catalog;
        concurrency_    = concurrency;
        cursorName_     = cursorName;
        row_            = row;
        extendedColumnDescriptors_ = extendedColumnDescriptors;                          //@G1A
        convTable_      = convTable;                                                     //@G1A
        con_            = con;                                                           //@in1
        settings_       = SQLConversionSettings.getConversionSettings ((AS400JDBCConnection) con_); /*@Q8A*/
    }

    /**
    Throws an exception if the specified column index is not
    valid.
    @param  columnIndex   The column index (1-based).
    @exception  SQLException    If the column index is not valid.
    **/
    private void checkIndex(int columnIndex)
    throws SQLException
    {
        // Validate the column index.
        if((columnIndex < 1))
            JDError.throwSQLException(this, JDError.EXC_DESCRIPTOR_INDEX_INVALID,columnIndex+"<1");
        if((columnIndex > row_.getFieldCount()))
          JDError.throwSQLException(this, JDError.EXC_DESCRIPTOR_INDEX_INVALID, columnIndex+">"+row_.getFieldCount());
    }

    /**
    Returns the catalog name of the table for a column.
    @param  columnIndex     The column index (1-based).
    @return                 The catalog name.
    @exception  SQLException    If the column index is not valid.
    **/
    public String getCatalogName(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return catalog_;
    }

    // JDBC 2.0
    /**
    Returns the name of a Java class whose instances are
    created if ResultSet.getObject() is called to retrieve
    from the column.  The actual class created may be a subclass
    of the returned class.
    @param  columnIndex     The column index (1-based).
    @return                 The class name.
    @exception  SQLException    If the column index is not valid.
    **/
    //
    // Implementation note:
    //
    // * In the case where the column returns a byte array, this
    //   returns "[B".  See the javadoc for java.lang.Class.getName()
    //   for the reason why.  The JDBC 2.0 specification does not
    //   really go into detail, so it is unclear if this is okay
    //   to return.  I will assume so in the mean time.
    //
    public String getColumnClassName(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        SQLData sqlData = row_.getSQLType(columnIndex);
        String className = sqlData.getObject().getClass().getName();
        return className;
    }

    /**
    Returns the number of columns in the result set.
    @return     The number of columns.
    @exception  SQLException    If an error occurs.
    **/
    public int getColumnCount()
    throws SQLException
    {
        return row_.getFieldCount();
    }

    /**
    Returns the normal maximum width of a column.
    @param  columnIndex     The column index (1-based).
    @return                 The normal maximum width
                            (in characters).
    @exception  SQLException    If the column index is not valid.
    **/
    public int getColumnDisplaySize(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return row_.getSQLType(columnIndex).getDisplaySize();
    }

    /**
    Returns the suggested label for use in printouts
    or displays for a column.
    @param  columnIndex     The column index (1-based).
    @return                 The column label if the user set the
                            driver property "extended metadata" to true and
                            the system returns us a column label,
                            otherwise the column name.
    @exception  SQLException    If the column index is not valid.
    **/
    //
    // Implementation note:
    //
    // For now, this is the same as the field name.  In
    // order to get the field label, we would have to make
    // an extra request to the ROI server and perform a lot
    // of extra processing.  This performance hit is not
    // worth it.  If the caller really needs the label, then
    // they can use DatabaseMetaData.getColumns.
    //
    // As of mod5 of Toolbox, we can use extended column descriptors
    // (if the user asked for the by setting the "extended metadata" property
    // to true) which will flow back to us without an extra call to the ROI server.
    //
    public String getColumnLabel(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        // @G1A If we have column descriptors, use them to get the column label.                             //@G1A
        if(extendedColumnDescriptors_ != null)                                                              //@G1A
        {
            //@G1A
            DBColumnDescriptorsDataFormat dataFormat = extendedColumnDescriptors_.getColumnDescriptors(columnIndex, convTable_, settings_);    //@KBA //@Q8C  // Fix for JTOpen Bug 4034 The ccsid for the column may be 65535, if that is the case, we want to use the job's ccsid
            if(dataFormat != null)  //@KBA  The data format returned by the host server will be null for columns created by expressions, use old way
            {                      //@D9A
                String columnLabel = dataFormat.getColumnLabel(convTable_); //@D9A
                  // Only return if the returned label length is greater than zero 
                if(columnLabel != null  && columnLabel.length() > 0)        //@D9A@P6C
                    return columnLabel;                                     //@D9A
                //@D9D return dataFormat.getColumnLabel(convTable_);   //@KBA
                //@KBD return extendedColumnDescriptors_.getColumnDescriptors(columnIndex).getColumnLabel(convTable_);  //@G1A
            }
        }                                                                                                    //@G1A

        //else use the "old way".
        return row_.getFieldName(columnIndex);
    }

    /**
    Returns the name of a column.
    @param  columnIndex     The column index (1-based).
    @return                 The column name.
    @exception  SQLException    If the column index is not valid.
    **/
    public String getColumnName(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        String columnName =row_.getFieldName(columnIndex);
        if (JDTrace.isTraceOn()) {
          JDTrace.logInformation (this, "getColumnName("+columnIndex+") returned " + columnName);
        }
        return columnName;
    }

    /**
    Returns the type of a column.  If the type is a distinct type,
    this returns the underlying type.
    @param  columnIndex     The column index (1-based).
    @return                 The SQL type code defined in java.sql.Types.
    @exception  SQLException    If the column index is not valid.
    **/
    public int getColumnType(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return row_.getSQLType(columnIndex).getType();
    }

    /**
    Returns the type name of a column.  If the type is a distinct
    type, this returns the underlying type name.
    @param  columnIndex     The column index (1-based).
    @return                 The column type name.
    @exception  SQLException    If the column index is not valid.
    **/
    public String getColumnTypeName(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return row_.getSQLTypeName(columnIndex);    /*@L1C*/ 
    }

    /**
    Returns the precision of a column.  This is the number
    of decimal digits the column may hold.
    @param  columnIndex     The column index (1-based).
    @return                 The precision.
    @exception  SQLException    If the column index is not valid.
    **/
    public int getPrecision(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return row_.getSQLType(columnIndex).getPrecision();
    }

    /**
    Returns the scale of a column.  This is number of digits
    to the right of the decimal point.
    @param  columnIndex     The column index (1-based).
    @return                 The scale.
    @exception  SQLException    If the column index is not valid.
    **/
    public int getScale(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return row_.getSQLType(columnIndex).getScale();
    }

    /**
    Returns the schema name of the table for a column.
    This method is supported only if the user has set the
    driver property "extended metadata" to true.
    @param  columnIndex     The column index (1-based).
    @return                 The schema name if the user set the
                            driver property "extended metadata" to true and
                            the system returns us a schema name,
                            otherwise "".
    @exception  SQLException    If the column index is not valid.
    **/
    public String getSchemaName(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);

        // @G1A If we have column descriptors, use them to get the schema name.  //@G1A
        if(extendedColumnDescriptors_ != null)                                  //@G1A
        {
            //@G1A
            DBColumnDescriptorsDataFormat dataFormat = extendedColumnDescriptors_.getColumnDescriptors(columnIndex, convTable_,settings_);    //@KBA //@ss1//@Q8C
            if(dataFormat != null) //@KBA  Depending on the query, dataFormat returned by the host server may be null.  For example, if a union was used or an expression
                return dataFormat.getBaseTableSchemaName(convTable_);   //@G1A
        }                                                                        //@G1A

        //else return ""
        return "";
    }

    /**
    Returns the column's table name.
    This method is supported only if the user has set the
    driver property "extended metadata" to true.
    @param  columnIndex     The column index (1-based).
    @return                 The base table name if the user set the
                            driver property "extended metadata" to true and
                            the system returns us a table name,
                            otherwise "".
    @exception  SQLException    If the column index is not valid.
    **/
    public String getTableName(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);

        // Changed to get the Base Table Name for a column if the extended metadata property is true
        // because we already have the information, we should return it to the user if they want it...
        if(extendedColumnDescriptors_ != null)
        {
            DBColumnDescriptorsDataFormat dataFormat = extendedColumnDescriptors_.getColumnDescriptors(columnIndex, convTable_, settings_);    //@KBA //@ss1 //@Q8C
            if(dataFormat != null)                                                                                      //@KBA  Depending on the query, dataFormat returned by the host server may be null.  For example, if a union was used or an expression
                return dataFormat.getBaseTableName(convTable_);                                                         //@KBA
            //@KBD return extendedColumnDescriptors_.getColumnDescriptors(columnIndex).getBaseTableName(convTable_);       //K1C  use to call getBaseTableSchemaName
        }

        // we still return "" if we don't have the Base Table Name
        return "";
    }

    /**
    Indicates if the column is automatically numbered.
    @param  columnIndex     The column index (1-based).
    @return                 True if column is autoincrement, false otherwise.
    @exception  SQLException    If the column index is not valid.
    Note:  connection property "extended metadata" must be true for this method to be return accurate information.
    If the "extended metadata" connection property is not set to true, then this method will always return false.
    **/
    public boolean isAutoIncrement(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);

        // Only run the query to get the information if the table name can be found.  The table name
        // can only be found if "extended metadata" == true
        // @A9A

        String tableName = this.getTableName(columnIndex);
        if ((tableName == null) || (tableName.length() == 0) ) {
         	return false;
        }

        //return false; //@in1 add implementation instead of always returning false

        PreparedStatement ps = null;
        ResultSet rs = null;
        try
        {
            ps = con_.prepareStatement("SELECT identity_generation " +
                    "FROM QSYS2" + getCatalogSeparator() + "SYSCOLUMNS" +
                    " WHERE identity_generation is not null" +
                    " AND column_name = ?" +
                    " AND table_name = ?" +
                    " AND table_schema = ?");
            ps.setString(1, this.getColumnName(columnIndex));
            ps.setString(2, tableName);
            ps.setString(3, this.getSchemaName(columnIndex));

            rs = ps.executeQuery();
            if ( rs.next())
                return true;
            else
                return false;

        }
        catch (SQLException e)
        {
            throw e;
        }
        finally
        {
            try{
            if(rs != null)
                rs.close();
            }catch(Exception e){
              JDTrace.logException(this, "isAutoIncrement rs.close()", e);  
            } //allow next close to execute
            if(ps != null)
                ps.close();
        }
    }

    /**
    Indicates if the column is case sensitive.
    @param  columnIndex     The column index (1-based).
    @return                 true if the column is case sensitive;
                            false otherwise.
    @exception  SQLException    If the column index is not valid.
    **/
    public boolean isCaseSensitive(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);

        // In DB2 for IBM i, all text types
        // are case sensitive.
        return row_.getSQLType(columnIndex).isText();
    }

    /**
    Indicates if the column is a currency value.
    @param  columnIndex     The column index (1-based).
    @return                 Always false.  DB2 for IBM i
                            does not directly support currency
                            values.
    @exception  SQLException    If the column index is not valid.
    **/
    public boolean isCurrency(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return false;
    }

    /**
    Indicates if a write on the column will definitely succeed.
    @param  columnIndex     The column index (1-based).
    @return                 Always false.  The driver does
                            not check if the user has the
                            necessary authority to write to
                            the column.
    @exception  SQLException    If the column index is not valid.
    **/
    public boolean isDefinitelyWritable(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return false;
    }

    /**
    Indicates if the column can contain an SQL NULL value.
    @param  columnIndex     The column index (1-based).
    @return                 true if the column is can contain
                            an SQL NULL value; false otherwise.
    @exception  SQLException    If the column index is not valid.
    **/
    public int isNullable(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return row_.isNullable(columnIndex);
    }

    /**
    Indicates if the column is read-only.
    @param  columnIndex     The column index (1-based).
    @return                 true if the column is read-only;
                            false otherwise.
    @exception  SQLException    If the column index is not valid.
    **/
    public boolean isReadOnly(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);

        // @G1A If we have column descriptors, use them to get searchable label.              //@G1A
        if(extendedColumnDescriptors_ != null)// && concurrency_ != ResultSet.CONCUR_READ_ONLY) //@G1A @G3C @36072
        {
            //@G1A
            if(extendedColumnDescriptors_.getUpdateable(columnIndex) == (byte)SQL_READ_ONLY) //@G1A @G2C
                return true;                                                                  //@G1A
            else                                                                              //@G1A
                return false;                                                                 //@G1A
        }                                                                                     //@G1A

        // else use "old" way
        return(concurrency_ == ResultSet.CONCUR_READ_ONLY);
    }

    /**
    Indicates if the column be used in a where clause.
    @param  columnIndex     The column index (1-based).
    @return                 If the user has set the "extended metadata" driver property to true,
                            returns true if the column can be used in a where clause
                            with any comparison operator except LIKE, returns
                            false if the column cannot be used in a where clause.
                            If the "extended metadata" driver property is set to false,
                            true will always be returned.
    @exception  SQLException    If the column index is not valid.
    **/
    public boolean isSearchable(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        // @G1A If we have column descriptors, use them to get searchable label.           //@G1A
        if(extendedColumnDescriptors_ != null)                                            //@G1A
        {
            //@G1A
            if(extendedColumnDescriptors_.getSearchable(columnIndex) == (byte)SQL_UNSEARCHABLE) //@G1A @G2C
                return false;                                                              //@G1A
            else                                                                           //@G1A
                return true;                                                               //@G1A
        }                                                                                  //@G1A

        // Else, return true
        return true;
    }

    /**
    Indicates if the column can contain a signed value.
    @param  columnIndex     The column index (1-based).
    @return                 true if the column is signed;
                            false otherwise.
    @exception  SQLException    If the column index is not valid.
    **/
    public boolean isSigned(int columnIndex)
    throws SQLException
    {
        checkIndex(columnIndex);
        return(row_.getSQLType(columnIndex).isSigned());
    }

    /**
    Indicates if it is possible for a write on the column to succeed.
    The write may fail even if this method returns true.
    The accuracy of this method will be improved if the "extended metadata"
    property is set to true.
    @param  columnIndex     The column index (1-based).
    @return                 true if it is possible for a write on
                            the column to succeed; false otherwise.
    @exception  SQLException    If the column index is not valid.
    **/
    public boolean isWritable(int columnIndex)
    throws SQLException
    {
        //@G1D checkIndex (columnIndex);
        //@G1D return(concurrency_ != ResultSet.CONCUR_READ_ONLY);
        return !isReadOnly(columnIndex);   //@G1A
    }

    /**
    Returns the name of the SQL cursor in use by this result set.
    @return     The cursor name.
    **/
    public String toString()
    {
        return cursorName_;
    }


    //@pda jdbc40
    protected String[] getValidWrappedList()
    {
        return new String[] {  "com.ibm.as400.access.AS400JDBCResultSetMetaData", "java.sql.ResultSetMetaData" };
    }

    //@in1 (copied from AS400JDBCDatabaseMetadata)
    /**
    Returns the naming convention used when referring to tables.
    This depends on the naming convention specified in the connection
    properties.

    @return     If using SQL naming convention, "." is returned. If
                using system naming convention, "/" is returned.

    @exception  SQLException    This exception is never thrown.
    **/
    private String getCatalogSeparator ()
    throws SQLException
    {
        String catalogSeparator;
        if (((AS400JDBCConnection)con_).getProperties().equals (JDProperties.NAMING, JDProperties.NAMING_SQL))
            catalogSeparator = ".";
        else
            catalogSeparator = "/";

        return catalogSeparator;
    }
}
