TableResultSet.java

/*
** Module   : TableResultSet.java
** Abstract : A result set used to serialize a table data and send it to a remote side. 
**
** Copyright (c) 2013-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 CA  20130610 Created initial version.
** 002 CA  20130628 Added possibility to retrieve the data as a ResultSet instance.
** 003 OM  20130705 Fixed date/datetime results with time functions.
** 004 CA  20140110 Added access to the metadata received from the remote side.
** 005 ECF 20140623 Removed Hibernate dependency from read capabilities. This is a quick and dirty
**                  workaround which removes the ResultSetMetaData.getColumnType implementation.
**                  Further work is required to re-implement this feature without direct reliance
**                  on the P2J UserType subclasses.
** 006 ECF 20140624 Re-enabled ResultSetMetaData.getColumnType with new implementation.
** 007 ECF 20140626 Replaced GCD proxy with J2SE proxy in cases where we have no concrete parent.
** 008 CA  20140722 Improved the ResultSet proxy to return only Java types.  Some other misc 
**                  fixes/improvements: check if result set is closed before executing APIs, 
**                  allow only P2J wrapper types to be resolved by the getters, better handling of
**                  null/unknown values.
** 009 CA  20140812 Added warnings to APIs which should not be used outside of P2J internal code.
**                  Fixed asResultSet problem which did not allow more than one walk over the 
**                  results. Throw SQLException if the received index is not valid.
** 010 OM  20141216 Replaced the returned ColumnName as legacy name instead of internal "fieldN".
** 011 OM  20150316 Adjusted the BigDecimal scale to match the P4GL message precision in
**                  getObject() method.
** 012 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 013 IAS 20160331 Fixed setScale()
** 014 CA  20190628 Added legacy table name.
** 015 OM  20190708 Extracted common interface with DataSetContainer in MassiveContainer.
** 016 CA  20191119 Added support for before-table.
** 017 OM  20200906 New ORM implementation.
** 018 SVL 20210701 Added clearRows.
** 019 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist;

import java.lang.reflect.*;
import java.math.*;
import java.sql.*;
import java.util.*;

import com.goldencode.p2j.persist.hql.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;

/**
 * Provides the main mechanism of passing table-like metadata and data which the remote side will
 * interpret as a temporary table. The remote side will use the sent properties (and their other
 * settings, as type and extent) to dynamically build the DMO interface, implementation class and
 * .hbm.xml configuration file.
 * <p>
 * As transport, any instance of this class will be wrapped in a {@link TableWrapper} instance
 * internally by P2J.
 * <p>
 * <strong>Note:</strong> In the process of building the internal data structures for dynamic
 * temp-tables, internal generic indexed names are used for columns. To obtain the expected column
 * name, the metadata will return the column's legacy name instead of internal database name.
 */
public abstract class TableResultSet
extends MassiveContainer
{
   /** logger */
   private static final CentralLogger LOG = CentralLogger.get(TableResultSet.class.getName());

   /**
    * The data sent by a remote side, after the remote appserver call, when this table is in 
    * OUTPUT or INPUT-OUTPUT mode.
    */
   private List<Object[]> rows;

   /** A set of meta information for each row, as given by {@link TempRecord#toArray()}. */
   private List<Object[]> rowsMeta;
   
   /**
    * The metadata sent by a remote side, after the remote appserver call, when this table is in 
    * OUTPUT or INPUT-OUTPUT mode.
    */
   private List<PropertyDefinition> properties;
   
   /**
    * Default c'tor, explicitly added to allow instances of this class to be created on 
    * deserialization. Not for public use.
    */
   public TableResultSet()
   {
   }
   
   /**
    * Create a new instance of this result set.
    * 
    * @param    input
    *           Flag indicating this table is sent in INPUT or INPUT-OUTPUT mode.
    * @param    output
    *           Flag indicating this table is sent in OUTPUT or INPUT-OUTPUT mode.
    * @param    append
    *           Flag indicating this table is sent in APPEND mode.
    */
   public TableResultSet(boolean input, boolean output, boolean append)
   {
      super(input, output, append);
   }

   /**
    * Used in conjunction with {@link #nextProperty()}, it provides a mechanism for subclasses
    * to send the property list in an iterator-like form.
    * <p>
    * Override this method so that it returns <code>false</code> when {@link #nextProperty} has
    * sent all properties.
    * <p>
    * WARNING: this allows only one iteration over the properties and is for internal usage
    * only.  Use {@link #asResultSet()} and the associated {@link ResultSetMetaData} instead.
    * 
    * @return   Default it returns <code>false</code>.
    */
   public abstract boolean hasMoreProperties();
   
   /**
    * Used in conjunction with {@link #hasMoreProperties()}, it provides a mechanism for 
    * subclasses to send the property list in an iterator-like form.
    * <p>
    * Override this method so that when {@link #hasMoreProperties} returns <code>true</code>, this
    * will return the name of the next property in the list.
    * <p>
    * WARNING: this allows only one iteration over the properties and is for internal usage
    * only.  Use {@link #asResultSet()} and the associated {@link ResultSetMetaData} instead.
    * 
    * @return   Default it returns <code>null</code>.
    */
   public abstract PropertyDefinition nextProperty();
   
   /**
    * Used in conjunction with {@link #hasMoreRows()}, it provides a mechanism for subclasses to
    * send the data in an iterator-like form.
    * <p>
    * Override this method so that it returns <code>false</code> when {@link #nextRow} has sent 
    * all data.
    * <p>
    * WARNING: this allows only one iteration over the properties and is for internal usage
    * only.  Use {@link #asResultSet()} instead.
    * 
    * @return   Default it returns <code>false</code>.
    */
   public abstract boolean hasMoreRows();
   
   /**
    * Used in conjunction with {@link #hasMoreRows()}, it provides a mechanism for  subclasses to
    * send the data in an iterator-like form.
    * <p>
    * Override this method so that when {@link #hasMoreRows} returns <code>true</code>, this will
    * return the next row in the table.
    * <p>
    * WARNING: this allows only one iteration over the properties and is for internal usage
    * only.  Use {@link #asResultSet()} and instead.
    * 
    * @return   Default it returns <code>null</code>.
    */
   public abstract Object[] nextRow();
   
   /**
    * Used in conjunction with {@link #hasMoreRows()}, it provides a mechanism for  subclasses to
    * send the row meta information in an iterator-like form.
    * <p>
    * Override this method so that when {@link #hasMoreRows} returns <code>true</code>, this will
    * return the next row in the table.
    * <p>
    * WARNING: this allows only one iteration over the properties and is for internal usage
    * only.  Use {@link #asResultSet()} and instead.
    * 
    * @return   Default it returns <code>null</code>.
    */
   public abstract Object[] nextRowMeta();
   
   /**
    * Set the data sent by a remote side, after the remote appserver call.
    * 
    * @param    rows
    *           The data sent by the remote side.
    */
   public void setRows(List<Object[]> rows)
   {
      this.rows = rows;
   }
   
   /**
    * Set the meta row information sent by a remote side, after the remote appserver call.
    * 
    * @param    rowsMeta
    *           The meta information sent by the remote side.
    */
   public void setRowsMeta(List<Object[]> rowsMeta)
   {
      this.rowsMeta = rowsMeta;
   }
   
   /**
    * Get the data sent by a remote side, after the remote appserver call. This will be non-null
    * only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
    * 
    * @return   See above.
    */
   public List<Object[]> getRows()
   {
      return rows;
   }
   
   /**
    * Get the row meta information sent by a remote side, after the remote appserver call. 
    * This will be non-null only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
    * 
    * @return   See above.
    */
   public List<Object[]> getRowsMeta()
   {
      return rowsMeta;
   }
   
   /**
    * Set the metadata sent by a remote side, after the remote appserver call.
    * 
    * @param    properties
    *           The metadata sent by the remote side.
    */
   public void setProperties(List<PropertyDefinition> properties)
   {
      this.properties = properties;
   }
   
   /**
    * Get the metadata sent by a remote side, after the remote appserver call. This will be 
    * non-null only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
    * 
    * @return   See above.
    */
   public List<PropertyDefinition> getProperties()
   {
      return properties;
   }
   
   /** 
    * Get the data sent by a remote side, after the remote appserver call, as a {@link ResultSet}
    * instance.  This will be non-null only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
    * 
    * @return   See above.
    */
   public ResultSet asResultSet()
   {
      if (!isOutput())
      {
         return null;
      }
      
      Class<?>[] interfaces = { ResultSet.class };
      
      ResultSet proxy = (ResultSet) Proxy.newProxyInstance(getClass().getClassLoader(),
                                                           interfaces,
                                                           new DataHandler());
      
      return proxy;
   }

   /**
    * Delete row data.
    */
   public void clearRows()
   {
      rows = null;
      rowsMeta = null;
   }

   /**
    * Handler for the {@link ResultSetMetaData} proxy.
    * Only some of the methods are supported: ({@link ResultSetMetaData#getColumnCount},
    * {@link ResultSetMetaData#getColumnName}, {@link ResultSetMetaData#getColumnTypeName} and
    * {@link ResultSetMetaData#getColumnType}. Note that, when working with dynamically generated
    * tables, internal generic indexed names are used for columns. To obtain the expected column
    * name, the column's legacy name is returned instead by <code>getColumnName()</code>.
    */
   private final class MetaDataHandler
   implements InvocationHandler
   {
      /** The property list. */
      private final List<PropertyDefinition> props;
      
      /**
       * Build a new instance which extracts metadata from the given property list.
       * 
       * @param    props
       *           The property list.
       */
      public MetaDataHandler(List<PropertyDefinition> props)
      {
         this.props = props;
      }
      
      /**
       * Handler for {@link ResultSetMetaData} APIs. Only the following are supported:
       * <ul>
       *    <li>{@link ResultSetMetaData#getColumnCount} - Returns the number of columns in this
       *          ResultSet object.</li>
       *    <li>{@link ResultSetMetaData#getColumnName} - Get the designated column's name.<br>
       *          <strong>Note:</strong>For the in-memory tables that are dynamicaly generated,
       *          this will return the legacy name not actual column name from the database.
       *          Since the record structure built dynamicaly, the fields names are generic and
       *          the returned name is obtained by mapping the legacy name.</li>
       *    <li>{@link ResultSetMetaData#getColumnTypeName} - Retrieves the designated column's
       *          database-specific type name.</li>
       *    <li>{@link ResultSetMetaData#getColumnType} - Retrieves the designated column's SQL
       *          type.</li>
       * </ul>
       */
      public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable
      {
         String mname = method.getName();
         
         if ("getColumnCount".equals(mname))
         {
            return props.size();
         }
         
         if (mname.startsWith("get") && args.length == 1 && args[0] instanceof Integer)
         {
            int idx = (Integer) args[0] - 1;
            PropertyDefinition pd = props.get(idx);
            
            if ("getColumnName".equals(mname))
            {
               return pd.getLegacyName(); // use legacy name instead of internal pd.getName();
            }
            
            if ("getColumnTypeName".equals(mname))
            {
               return pd.getType().getName();
            }
            
            if ("getColumnType".equals(mname))
            {
               // TODO: investigate this: Class.forName("util.pack" + type.lowercase())?
               FqlType type = DataTypeHelper.getTypeClass(pd.getType());
               return type.getSqlType();
            }
         }
         
         throw new SQLFeatureNotSupportedException("Method " + mname + " is not supported!");
      }
   }

   /**
    * Handler for the {@link ResultSet} proxy.  This will provide the data in a 
    * {@link ResultSet#TYPE_FORWARD_ONLY forward-only} manner.
    */
   private final class DataHandler
   implements InvocationHandler
   {
      /** The {@link ResultSetMetaData} proxy. */
      private ResultSetMetaData rsmd;

      /** The list of properties. */
      private List<PropertyDefinition> props;

      /** An iterator over the data. */
      private Iterator<Object[]> iter;
      
      /** Flag indicating if the last retrieved column was SQL-null. */
      private boolean wasNull = false;
      
      /** The data for the current row. */
      private Object[] data = null;
      
      /**
       * Create a new instance. This initializes the iterator, builds the 
       * {@link #props property list} and the {@link #rsmd metadata}.
       */
      public DataHandler()
      {
         iter = rows.iterator();

         props = new ArrayList<>(properties);

         Class<?>[] interfaces = { ResultSetMetaData.class };
         rsmd = (ResultSetMetaData) Proxy.newProxyInstance(getClass().getClassLoader(),
                                                           interfaces,
                                                           new MetaDataHandler(props));
      }
      
      /**
       * Handler for the {@link ResultSet} APIs. Only the following are supported:
       * <ul>
       *    <li>{@link ResultSet#next}</li>
       *    <li>{@link ResultSet#close}</li>
       *    <li>{@link ResultSet#isClosed}</li>
       *    <li>{@link ResultSet#wasNull}</li>
       *    <li>{@link ResultSet#findColumn}</li>
       *    <li>{@link ResultSet#relative}</li>
       *    <li>{@link ResultSet#getType}</li>
       *    <li>{@link ResultSet#getMetaData}</li>
       *    <li>{@link ResultSet#getObject}</li>
       *    <li>{@link ResultSet#getBoolean}</li>
       *    <li>{@link ResultSet#getDouble}</li>
       *    <li>{@link ResultSet#getBigDecimal}</li>
       *    <li>{@link ResultSet#getFloat}</li>
       *    <li>{@link ResultSet#getInt}</li>
       *    <li>{@link ResultSet#getLong}</li>
       *    <li>{@link ResultSet#getShort}</li>
       *    <li>{@link ResultSet#getByte}</li>
       *    <li>{@link ResultSet#getString}</li>
       *    <li>{@link ResultSet#getDate}</li>
       *    <li>{@link ResultSet#getTime}</li>
       *    <li>{@link ResultSet#getTimestamp}</li>
       * </ul>
       */
      public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable
      {
         String mname = method.getName();

         if ("close".equals(mname))
         {
            // discard all data
            iter = null;
            rsmd = null;
            props = null;
            
            return null;
         }
         
         if ("isClosed".equals(mname))
         {
            return (iter == null);
         }
         
         if (iter == null)
         {
            // all next APIs require for the result set to be opened.
            throw new SQLException("The ResultSet is closed!");
         }

         if ("next".equals(mname))
         {
            boolean next = iter.hasNext();
            data = (next ? iter.next() : null);
            
            return next;
         }

         if ("wasNull".equals(mname))
         {
            return wasNull;
         }
         
         if ("findColumn".equals(mname))
         {
            return getPropertyIndex(mname);
         }
         
         if ("relative".equals(mname))
         {
            int rel = (Integer) args[0];
            if (rel < 0)
            {
               throw new SQLFeatureNotSupportedException("The result set is forward only!");
            }
            else
            {
               for (int i = 0; i < rel; i++)
               {
                  if (iter.hasNext())
                  {
                     data = iter.next();
                  }
                  else
                  {
                     // passed last record
                     data = null;
                     return false;
                  }
               }
            }
            
            return true;
         }

         if ("getType".equals(mname))
         {
            return ResultSet.TYPE_FORWARD_ONLY;
         }
         
         if ("getMetaData".equals(mname))
         {
            return rsmd;
         }
         
         Object res = null;
         if (mname.startsWith("get"))
         {
            if (data == null)
            {
               throw new SQLException("Invalid cursor! next() was not called or the last record" +
                                      "was reached.");
            }
            
            if (data.length == 0)
            {
               throw new SQLException("The current record has no fields!");
            }
            
            wasNull = false;

            int idx = 0;
            if (args[0] instanceof String)
            {
               idx = getPropertyIndex((String) args[0]);
            }
            else
            {
               idx = (Integer) args[0];
            }

            // make this 0-based index
            idx = idx - 1;

            if (idx >= data.length || idx < 0)
            {
               final String msg = "Invalid column index: %d; valid indexes are 1 to %d";
               throw new SQLException(String.format(msg, idx + 1, data.length));
            }
            
            Object val = data[idx];
            if (val == null)
            {
               LOG.warning("Received NULL value for field at index " + (idx + 1) + 
                           " when invoking " + mname + ".");
            }
            
            boolean resolved = true;
            boolean isNull = (val == null || 
                              (val instanceof BaseDataType && ((BaseDataType) val).isUnknown()));

            // the remote side always sends P2J wrapper types - do not resolve other types
            if ("getObject".equals(mname))
            {
               if (args.length != 1)
               {
                  throw new SQLException("getObject supports only one argument.");
               }

               if (isNull)
               {
                  // this API allows NULL values.
                  res = null;
               }
               else
               {
                  int type = rsmd.getColumnType(idx + 1);
                  switch (type)
                  {
                     case Types.VARCHAR:
                        res = ((BaseDataType) val).toStringMessage();
                        break;
                     case Types.DATE:
                        res =  new java.sql.Date(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
                        break;
                     case Types.TIMESTAMP:
                        res = new java.sql.Timestamp(((date) val).dateValue().getTime());  // TODO: what if [val] is unknown?
                        break;
                     case Types.NUMERIC:
                        res = new BigDecimal(((decimal) val).toStringMessage());
                        break;
                     case Types.BIGINT:
                        res = (val instanceof int64 ? ((int64) val).toJavaLongType()
                                                    : ((rowid) val).getValue());
                        break;
                     case Types.INTEGER:
                        res = ((integer) val).toJavaIntegerType();
                        break;
                     case Types.BIT:
                        res = ((logical) val).toJavaType();
                        break;
                     case Types.VARBINARY:
                        res = ((raw) val).getByteArray();
                        break;
                        
                     default:
                        resolved = false;
                        break;
                  }
               }
            }
            // start of APIs which can return null
            else if ("getString".equals(mname))
            {
               if (isNull)
               {
                  res = null;
               }
               else if (val instanceof BaseDataType)
               {
                  res = ((BaseDataType) val).toStringMessage();
               }
               else 
               {
                  resolved = false;
               }
            }
            else if ("getBytes".equals(mname))
            {
               if (isNull)
               {
                  res = null;
               }
               else if (val instanceof BinaryData)
               {
                  res = ((BinaryData) val).getByteArray();
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getBigDecimal".equals(mname))
            { 
               if (isNull)
               {
                  res = null;
               }
               else if (val instanceof NumberType)
               {
                  res = new BigDecimal(((NumberType) val).doubleValue());
                  
                  if (args.length == 2)
                  {
                     int scale = (Integer) args[1];
                     res = ((BigDecimal) res).setScale(scale);
                  }
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getDate".equals(mname))
            {
               // TODO: Calendar parameter?
               if (isNull)
               {
                  res = null;
               }
               else if (val instanceof date)
               {
                  res = new java.sql.Date(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getTime".equals(mname))
            {
               // TODO: Calendar parameter?
               if (isNull)
               {
                  res = null;
               }
               else if (val instanceof date)
               {
                  res = new java.sql.Time(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getTimestamp".equals(mname))
            {
               // TODO: Calendar parameter?
               if (isNull)
               {
                  res = null;
               }
               else if (val instanceof date)
               {
                  res = new java.sql.Timestamp(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
               }
               else
               {
                  resolved = false;
               }
            }
            // start of APIs which return a non-null value
            else if (isNull)
            {
               // do not allow NULLs from here on
               throw new NullPointerException("Value for column " + args[0].toString() + 
                                              " is null or unknown, when calling " + mname + "!");
            }
            else if ("getBoolean".equals(mname))
            {
               if (val instanceof logical)
               {
                  res = ((logical) val).booleanValue();
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getDouble".equals(mname))
            {
               if (val instanceof NumberType)
               {
                  res = ((NumberType) val).doubleValue();
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getFloat".equals(mname))
            {
               if (val instanceof NumberType)
               {
                  res = (float) ((NumberType) val).doubleValue();
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getInt".equals(mname))
            {
               if (val instanceof NumberType)
               {
                  res = ((NumberType) val).intValue();
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getLong".equals(mname))
            {
               if (val instanceof NumberType)
               {
                  res = ((NumberType) val).longValue();
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getShort".equals(mname))
            {
               if (val instanceof NumberType)
               {
                  res = (short) ((NumberType) val).intValue();
               }
               else
               {
                  resolved = false;
               }
            }
            else if ("getByte".equals(mname))
            {
               if (val instanceof NumberType)
               {
                  res = (byte) ((NumberType) val).intValue();
               }
               else
               {
                  resolved = false;
               }
            }
            
            if (!resolved)
            {
               throw new SQLException("Value '" + (val == null ? "null" : val.toString()) + "'" +
                                      " for column " + args[0].toString() + 
                                      "[" + rsmd.getColumnType(idx + 1) + "] is not compatible with method " + 
                                      mname + "!");
            }
            else
            {
               wasNull = (res != null);
            }
            
            return res;
         }
         
         throw new SQLFeatureNotSupportedException("Method " + mname + " is not supported!");
      }

      /**
       * Get the 1-based index for the property with the specified name. Search is done 
       * case-insensitive.  If not found, an {@link SQLException} is throw.
       * 
       * @param    name
       *           The property name.
       *           
       * @return   The 1-based index in the property list.
       * 
       * @throws   SQLException
       *           If the property was not found.
       */
      private int getPropertyIndex(String name)
      throws SQLException
      {
         for (int i = 0; i < props.size(); i++)
         {
            PropertyDefinition pd = props.get(i);
            if (pd.getName().equalsIgnoreCase(name))
            {
               return i + 1;
            }
         }
            
         throw new SQLException("Property " + name + " was not found!");
      }
   }
}