# 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!");
          }
       }
    }