// jTDS JDBC Driver for Microsoft SQL Server and Sybase
// Copyright (C) 2004 The jTDS Project
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
package net.sourceforge.jtds.jdbc;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;

/**
 * jTDS implementation of the java.sql.PreparedStatement interface.
 * <p>
 * Implementation notes:
 * <ol>
 * <li>Generally a simple subclass of Statement mainly adding support for the
 *     setting of parameters.
 * <li>The stream logic is taken over from the work Brian did to add Blob support
 *     to the original jTDS.
 * <li>Use of Statement specific method calls eg executeQuery(sql) is blocked by
 *     this version of the driver. This is unlike the original jTDS but inline
 *     with all the other JDBC drivers that I have been able to test.
 * </ol>
 *
 * @author
 *    Brian Heineman, Mike Hutchinson, Holger Rehn
 */
public class JtdsPreparedStatement extends JtdsStatement implements PreparedStatement {
    /** The SQL statement being prepared. */
    protected final String sql;
    /** The original SQL statement provided at construction time. */
    private final String originalSql;
    /** The first SQL keyword in the SQL string.*/
    protected String sqlWord;
    /** The procedure name for CallableStatements. */
    protected String procName;
    /** The parameter list for the call. */
    protected ParamInfo[] parameters;
    /** True to return generated keys. */
    private boolean returnKeys;
    /** The cached parameter meta data. */
    protected ParamInfo[] paramMetaData;
    /** Used to format numeric values when scale is specified. */
    private final static NumberFormat f = NumberFormat.getInstance();
    /** Collection of handles used by this statement */
    Collection handles;

    /**
     * Construct a new preparedStatement object.
     *
     * @param connection The parent connection.
     * @param sql   The SQL statement to prepare.
     * @param resultSetType The result set type eg SCROLLABLE etc.
     * @param concurrency The result set concurrency eg READONLY.
     * @param returnKeys True if generated keys should be returned.
     * @throws SQLException
     */
    JtdsPreparedStatement(JtdsConnection connection,
                          String sql,
                          int resultSetType,
                          int concurrency,
                          boolean returnKeys)
        throws SQLException {
        super(connection, resultSetType, concurrency);

        // returned by toString()
        originalSql = sql;

        // Parse the SQL looking for escapes and parameters
        if (this instanceof JtdsCallableStatement) {
            sql = normalizeCall(sql);
        }

        ArrayList params = new ArrayList();
        String[] parsedSql = SQLParser.parse(sql, params, connection, false);

        if (parsedSql[0].length() == 0) {
            throw new SQLException(Messages.get("error.prepare.nosql"), "07000");
        }

        if (parsedSql[1].length() > 1) {
            if (this instanceof JtdsCallableStatement) {
                // Procedure call
                procName = parsedSql[1];
            }
        }
        sqlWord = parsedSql[2];

        if (returnKeys /*&& "insert".equals(sqlWord) REVIEW: see JtdsStatement.executeImpl */) {
            if (connection.getServerType() == Driver.SQLSERVER
                    && connection.getDatabaseMajorVersion() >= 8) {
                this.sql = parsedSql[0] + " SELECT SCOPE_IDENTITY() AS " + GENKEYCOL;
            } else {
                this.sql = parsedSql[0] + " SELECT @@IDENTITY AS " + GENKEYCOL;
            }
            this.returnKeys = true;
        } else {
            this.sql = parsedSql[0];
            this.returnKeys = false;
        }

        parameters = (ParamInfo[]) params.toArray(new ParamInfo[params.size()]);
    }

    /**
     * Returns the SQL command provided at construction time.
     */
    @Override
    public String toString() {
        return originalSql;
    }

   /**
    * <p> This method converts native call syntax into (hopefully) valid JDBC
    * escape syntax. </p>
    *
    * <p><b>Note:</b> This method is required for backwards compatibility with
    * previous versions of jTDS. Strictly speaking only the JDBC syntax needs to
    * be recognized, constructions such as "?=#testproc ?,?" are neither valid
    * native syntax nor valid escapes. All the substrings and trims below are
    * not as bad as they look. The objects created all refer back to the
    * original SQL string it is just the start and length positions which
    * change. </p>
    *
    * @param sql
    *    the SQL statement to process
    *
    * @return
    *    the SQL, possibly in original form
    *
    * @throws SQLException
    *    if the SQL statement is detected to be a normal SQL INSERT, UPDATE or
    *    DELETE statement instead of a procedure call
    */
   protected static String normalizeCall( final String sql )
      throws SQLException
   {
      try
      {
         return normalize( sql, 0 );
      }
      catch( SQLException sqle )
      {
         // if normalize was giving up due to an unrecognized syntax error it
         // would have thrown an SQLException without state
         if( sqle.getSQLState() != null )
            throw sqle;

         return sql;
      }
   }

   /**
    * <p> This method converts native call syntax into (hopefully) valid JDBC
    * escape syntax. </p>
    *
    * <p><b>Note:</b> This method is required for backwards compatibility with
    * previous versions of jTDS. Strictly speaking only the JDBC syntax needs to
    * be recognized, constructions such as "?=#testproc ?,?" are neither valid
    * native syntax nor valid escapes. All the substrings and trims below are
    * not as bad as they look. The objects created all refer back to the
    * original SQL string it is just the start and length positions which
    * change. </p>
    *
    * @param sql
    *    the SQL statement to process
    *
    * @return
    *    the SQL, possibly in original form
    *
    * @throws SQLException
    *    if the SQL statement is detected to be a normal SQL INSERT, UPDATE or
    *    DELETE statement instead of a procedure call
    */
   private static String normalize( final String sql, int level )
      throws SQLException
   {
      if( level > 1 )
         throw new SQLException();

      int len   = sql.length();
      int qmark = -1;
      int equal = -1;
      int call  = -1;

      // try to isolate the call
      for( int i = 0; i < len && call < 0; i ++ )
      {
         // skip whitespace
         while( Character.isWhitespace( sql.charAt( i ) ) )
         {
            i ++;
         }

         switch( sql.charAt( i ) )
         {
            case '{': // escape syntax with leading comment/white space or syntax error
                      return sql;

            case '?': // should be leading '?'
                      if( qmark == -1 )
                      {
                         qmark = i;
                      }
                      else
                      {
                         // syntax error, give up
                         throw new SQLException();
                      }
                      break;

            case '=': // should be leading '='
                      if( equal == -1 && qmark >= 0 )
                      {
                         equal = i;
                      }
                      else
                      {
                         // syntax error, give up
                         throw new SQLException();
                      }
                      break;

            case '-': // skip single comment
                      if( i + 1 < len && sql.charAt( i + 1 ) == '-' )
                      {
                         i += 2;

                         while( i < len && sql.charAt( i ) != '\n' && sql.charAt( i ) != '\r' )
                         {
                            i ++;
                         }
                      }
                      break;

            case '/': // skip multi line comment
                      if( i + 1 < len && sql.charAt( i + 1 ) == '*' )
                      {
                         i += 1;
                         int block = 1;

                         do
                         {
                            if( i >= len -1 )
                               throw new SQLException( Messages.get( "error.parsesql.missing", "*/" ), "22025" );

                            i ++;

                            if( sql.charAt( i ) == '/' && sql.charAt( i + 1 ) == '*' )
                            {
                               i ++;
                               block ++;
                            }
                            else if( sql.charAt( i ) == '*' && sql.charAt( i + 1 ) == '/' )
                            {
                               i ++;
                               block --;
                            }
                         }
                         while( block > 0 );
                      }
                      break;

            default:  //
                      if( len - i > 4 && ( sql.substring( i, i + 5 ).equalsIgnoreCase( "exec " ) ) || sql.substring( i, i + 5 ).equalsIgnoreCase( "call " ) )
                      {
                         return normalize( sql.substring( 0, i ) + sql.substring( i + 4, sql.length() ), level ++ );
                      }
                      else if( len - i > 7 && sql.substring( i, i + 8 ).equalsIgnoreCase( "execute " ) )
                      {
                         return normalize( sql.substring( 0, i ) + sql.substring( i + 7, sql.length() ), level ++ );
                      }

                      // keep backward compatibility, things like "testproc()" are accepted
                      call = i;
                      break;
         }
      }

      if( equal == -1 && qmark != -1 )
      {
         // syntax error, give up
         throw new SQLException();
      }

      // check for a more or less common mistake to execute a normal statement via CallableStatement (bug #637)
      if( call + 7 < len )
      {
         String sub = sql.substring( call, call + 7 );

         if( sub != null && ( sub.equalsIgnoreCase( "insert " ) || sub.equalsIgnoreCase( "update " ) || sub.equalsIgnoreCase( "delete " ) ) )
            throw new SQLException( Messages.get( "error.parsesql.noprocedurecall" ), "07000" );
      }

      // fast scan whether the statement ends with a single line comment and append an additional line break in that case
      return "{" + sql.substring( 0, call ) + "call " + sql.substring( call ) + ( openComment( sql, call ) ? "\n" : "" ) + "}";
   }

   /**
    *
    */
   private static boolean openComment( String sql, int offset )
      throws SQLException
   {
      int len = sql.length();

      for( int i = offset; i < len; i ++ )
      {
         switch( sql.charAt( i ) )
         {
            case '-': // single comment
                      if( i + 1 < len && sql.charAt( i + 1 ) == '-' )
                      {
                         i += 2;

                         while( i < len && sql.charAt( i ) != '\n' && sql.charAt( i ) != '\r' )
                         {
                            i ++;
                         }

                         if( i == len )
                         {
                            // reached end of statement, comment still open
                            return true;
                         }
                      }
                      break;

            case '/': // multi line comment
                      if( i + 1 < len && sql.charAt( i + 1 ) == '*' )
                      {
                         i += 1;
                         int block = 1;

                         do
                         {
                            if( i >= len -1 )
                               throw new SQLException( Messages.get( "error.parsesql.missing", "*/" ), "22025" );

                            i ++;

                            if( sql.charAt( i ) == '/' && sql.charAt( i + 1 ) == '*' )
                            {
                               i ++;
                               block ++;
                            }
                            else if( sql.charAt( i ) == '*' && sql.charAt( i + 1 ) == '/' )
                            {
                               i ++;
                               block --;
                            }
                         }
                         while( block > 0 );
                      }
                      break;
         }
      }

      return false;
   }

    /**
     * Check that this statement is still open.
     *
     * @throws SQLException if statement closed.
     */
    @Override
    protected void checkOpen() throws SQLException {
        if (isClosed()) {
            throw new SQLException(
                    Messages.get("error.generic.closed", "PreparedStatement"), "HY010");
        }
    }

    /**
     * Report that user tried to call a method not supported on this type of statement.
     *
     * @param method The method name to report in the error message.
     * @throws SQLException
     */
    protected void notSupported(String method) throws SQLException {
        throw new SQLException(
                Messages.get("error.generic.notsup", method), "HYC00");
    }

    /**
     * Execute the SQL batch on a MS server.
     * <p/>
     * When running with <code>prepareSQL=1</code> or <code>3</code>, the driver will first prepare temporary stored
     * procedures or statements for each parameter combination found in the batch. The handles to these pre-preared
     * statements will then be used to execute the actual batch statements.
     *
     * @param size        the total size of the batch
     * @param executeSize the maximum number of statements to send in one request
     * @param counts      the returned update counts
     * @return chained exceptions linked to a <code>SQLException</code>
     * @throws SQLException if a serious error occurs during execution
     */
    @Override
    protected SQLException executeMSBatch(int size, int executeSize, ArrayList counts)
    throws SQLException {
        if (parameters.length == 0) {
            // There are no parameters, each SQL call is the same so execute as a simple batch
            return super.executeMSBatch(size, executeSize, counts);
        }
        SQLException sqlEx = null;
        String procHandle[] = null;

        // Prepare any statements before executing the batch
        if (connection.getPrepareSql() == TdsCore.TEMPORARY_STORED_PROCEDURES ||
                connection.getPrepareSql() == TdsCore.PREPARE) {
            procHandle = new String[size];
            for (int i = 0; i < size; i++) {
                // Prepare the statement
                procHandle[i] = connection.prepareSQL(this, sql, (ParamInfo[]) batchValues.get(i), false, false);
            }
        }

        for (int i = 0; i < size;) {
            Object value = batchValues.get(i);
            String proc = (procHandle == null) ? procName : procHandle[i];
            ++i;
            // Execute batch now if max size reached or end of batch
            boolean executeNow = (i % executeSize == 0) || i == size;

            tds.startBatch();
            tds.executeSQL(sql, proc, (ParamInfo[]) value, false, 0, -1, -1, executeNow);

            // If the batch has been sent, process the results
            if (executeNow) {
                sqlEx = tds.getBatchCounts(counts, sqlEx);

                // If a serious error then we stop execution now as count
                // is too small.
                if (sqlEx != null && counts.size() != i) {
                    break;
                }
            }
        }
        return sqlEx;
    }

    /**
     * Execute the SQL batch on a Sybase server.
     * <p/>
     * Sybase needs to have the SQL concatenated into one TDS language packet followed by up to 1000 parameters. This
     * method will be overridden for <code>CallableStatements</code>.
     *
     * @param size the total size of the batch
     * @param executeSize the maximum number of statements to send in one request
     * @param counts the returned update counts
     * @return chained exceptions linked to a <code>SQLException</code>
     * @throws SQLException if a serious error occurs during execution
     */
    @Override
    protected SQLException executeSybaseBatch(int size, int executeSize, ArrayList counts) throws SQLException {
        if (parameters.length == 0) {
            // There are no parameters each SQL call is the same so
            // execute as a simple batch
            return super.executeSybaseBatch(size, executeSize, counts);
        }
        // Revise the executeSize down if too many parameters will be required.
        // Be conservative the actual maximums are 256 for older servers and 2048.
        int maxParams = (connection.getDatabaseMajorVersion() < 12 ||
                (connection.getDatabaseMajorVersion() == 12 && connection.getDatabaseMinorVersion() < 50)) ?
                200 : 1000;
        StringBuilder sqlBuf = new StringBuilder(size * 32);
        SQLException sqlEx = null;
        if (parameters.length * executeSize > maxParams) {
            executeSize = maxParams / parameters.length;
            if (executeSize == 0) {
                executeSize = 1;
            }
        }
        ArrayList paramList = new ArrayList();
        for (int i = 0; i < size;) {
            Object value = batchValues.get(i);
            ++i;
            // Execute batch now if max size reached or end of batch
            boolean executeNow = (i % executeSize == 0) || i == size;

            int offset = sqlBuf.length();
            sqlBuf.append(sql).append(' ');
            for (int n = 0; n < parameters.length; n++) {
                ParamInfo p = ((ParamInfo[]) value)[n];
                // Allow for the position of the '?' marker in the buffer
                p.markerPos += offset;
                paramList.add(p);
            }
            if (executeNow) {
                ParamInfo args[];
                args = (ParamInfo[]) paramList.toArray(new ParamInfo[paramList.size()]);
                tds.executeSQL(sqlBuf.toString(), null, args, false, 0, -1, -1, true);
                sqlBuf.setLength(0);
                paramList.clear();
                // If the batch has been sent, process the results
                sqlEx = tds.getBatchCounts(counts, sqlEx);

                // If a serious error or a server error then we stop
                // execution now as count is too small.
                if (sqlEx != null && counts.size() != i) {
                    break;
                }
            }
        }
        return sqlEx;
    }

    /**
     * Check the supplied index and return the selected parameter.
     *
     * @param parameterIndex the parameter index 1 to n.
     * @return the parameter as a <code>ParamInfo</code> object.
     * @throws SQLException if the statement is closed;
     *                      if <code>parameterIndex</code> is less than 0;
     *                      if <code>parameterIndex</code> is greater than the
     *                      number of parameters;
     *                      if <code>checkIfSet</code> was <code>true</code>
     *                      and the parameter was not set
     */
    protected ParamInfo getParameter(int parameterIndex) throws SQLException {
        checkOpen();

        if (parameterIndex < 1 || parameterIndex > parameters.length) {
            throw new SQLException(Messages.get("error.prepare.paramindex",
                                                      Integer.toString(parameterIndex)),
                                                      "07009");
        }

        return parameters[parameterIndex - 1];
    }

    /**
     * Generic setObject method.
     *
     * @param parameterIndex Parameter index 1 to n.
     * @param x The value to set.
     * @param targetSqlType The java.sql.Types constant describing the data.
     * @param scale The decimal scale -1 if not set.
     */
    public void setObjectBase(int parameterIndex, Object x, int targetSqlType, int scale)
            throws SQLException {
        checkOpen();

        int length = 0;

        if (targetSqlType == java.sql.Types.CLOB) {
            targetSqlType = java.sql.Types.LONGVARCHAR;
        } else if (targetSqlType == java.sql.Types.BLOB) {
            targetSqlType = java.sql.Types.LONGVARBINARY;
        }

        if (x != null) {
            x = Support.convert(this, x, targetSqlType, connection.getCharset());

            if (scale >= 0) {
                if (x instanceof BigDecimal) {
                    x = ((BigDecimal) x).setScale(scale, BigDecimal.ROUND_HALF_UP);
                } else if (x instanceof Number) {
                    synchronized (f) {
                        f.setGroupingUsed(false);
                        f.setMaximumFractionDigits(scale);
                        x = Support.convert(this, f.format(x), targetSqlType,
                                connection.getCharset());
                    }
                }
            }

            if (x instanceof Blob) {
                Blob blob = (Blob) x;
                length = (int) blob.length();
                x = blob.getBinaryStream();
            } else if (x instanceof Clob) {
                Clob clob = (Clob) x;
                length = (int) clob.length();
                x = clob.getCharacterStream();
            }
        }

        setParameter(parameterIndex, x, targetSqlType, scale, length);
    }

    /**
     * Update the ParamInfo object for the specified parameter.
     *
     * @param parameterIndex Parameter index 1 to n.
     * @param x The value to set.
     * @param targetSqlType The java.sql.Types constant describing the data.
     * @param scale The decimal scale -1 if not set.
     * @param length The length of the data item.
     */
    protected void setParameter(int parameterIndex, Object x, int targetSqlType, int scale, int length)
        throws SQLException {
        ParamInfo pi = getParameter(parameterIndex);

        if ("ERROR".equals(Support.getJdbcTypeName(targetSqlType))) {
            throw new SQLException(Messages.get("error.generic.badtype",
                                Integer.toString(targetSqlType)), "HY092");
        }

        // Update parameter descriptor
        if (targetSqlType == java.sql.Types.DECIMAL
            || targetSqlType == java.sql.Types.NUMERIC) {

            pi.precision = connection.getMaxPrecision();
            if (x instanceof BigDecimal) {
                x = Support.normalizeBigDecimal((BigDecimal) x, pi.precision);
                pi.scale = ((BigDecimal) x).scale();
            } else {
                pi.scale = (scale < 0) ? TdsData.DEFAULT_SCALE : scale;
            }
        } else {
            pi.scale = (scale < 0) ? 0 : scale;
        }

        if (x instanceof String) {
            pi.length = ((String) x).length();
        } else if (x instanceof byte[]) {
            pi.length = ((byte[]) x).length;
        } else {
            pi.length   = length;
        }

        if (x instanceof Date) {
            x = new DateTime((Date) x);
        } else if (x instanceof Time) {
            x = new DateTime((Time) x);
        } else if (x instanceof Timestamp) {
            x = new DateTime((Timestamp) x);
        }

        pi.value = x;
        pi.jdbcType = targetSqlType;
        pi.isSet = true;
        pi.isUnicode = connection.getUseUnicode();
    }

    /**
     * Update the cached column meta data information.
     *
     * @param value The Column meta data array.
     */
    void setColMetaData(ColInfo[] value) {
        colMetaData = value;
    }

    /**
     * Update the cached parameter meta data information.
     *
     * @param value The Column meta data array.
     */
    void setParamMetaData(ParamInfo[] value) {
        for (int i = 0; i < value.length && i < parameters.length; i++) {
            if (!parameters[i].isSet) {
                // Only update parameter descriptors if the user
                // has not yet set them.
                parameters[i].jdbcType = value[i].jdbcType;
                parameters[i].isOutput = value[i].isOutput;
                parameters[i].precision = value[i].precision;
                parameters[i].scale = value[i].scale;
                parameters[i].sqlType = value[i].sqlType;
            }
        }
    }

// -------------------- java.sql.PreparedStatement methods follow -----------------

    @Override
    public void close() throws SQLException {
        try {
            super.close();
        } finally {
            // Null these fields to reduce memory usage while
            // waiting for Statement.finalize() to execute.
            handles = null;
            parameters = null;
        }
    }

    @Override
    public int executeUpdate() throws SQLException {
        checkOpen();
        reset();

        if (procName == null && !(this instanceof JtdsCallableStatement)) {
            // Sync on the connection to make sure rollback() isn't called
            // between the moment when the statement is prepared and the moment
            // when it's executed.
            synchronized (connection) {
                String spName = connection.prepareSQL(this, sql, parameters, returnKeys, false);
                executeSQL(sql, spName, parameters, true, false);
            }
        } else {
            executeSQL(sql, procName, parameters, true, false);
        }

        int res = getUpdateCount();
        return res == -1 ? 0 : res;
    }

    @Override
    public void addBatch() throws SQLException {
        checkOpen();

        if (batchValues == null) {
            batchValues = new ArrayList();
        }

        if (parameters.length == 0) {
            // This is likely to be an error. Batch execution
            // of a prepared statement with no parameters means
            // exactly the same SQL will be executed each time!
            batchValues.add(sql);
        } else {
            batchValues.add(parameters);

            ParamInfo tmp[] = new ParamInfo[parameters.length];

            for (int i = 0; i < parameters.length; ++i) {
                tmp[i] = (ParamInfo) parameters[i].clone();
            }

            parameters = tmp;
        }
    }

    @Override
    public void clearParameters() throws SQLException {
        checkOpen();

        for (int i = 0; i < parameters.length; i++) {
            parameters[i].clearInValue();
        }
    }

    @Override
    public boolean execute() throws SQLException {
        checkOpen();
        reset();
        boolean useCursor = useCursor(returnKeys, sqlWord);

        if (procName == null && !(this instanceof JtdsCallableStatement)) {
            // Sync on the connection to make sure rollback() isn't called
            // between the moment when the statement is prepared and the moment
            // when it's executed.
            synchronized (connection) {
                String spName = connection.prepareSQL(this, sql, parameters, returnKeys, useCursor);
                return executeSQL(sql, spName, parameters, false, useCursor);
            }
        } else {
            return executeSQL(sql, procName, parameters, false, useCursor);
        }
    }

    @Override
    public void setByte(int parameterIndex, byte x) throws SQLException {
        setParameter(parameterIndex, new Integer((x & 0xFF)), java.sql.Types.TINYINT, 0, 0);
    }

    @Override
    public void setDouble(int parameterIndex, double x) throws SQLException {
        setParameter(parameterIndex, new Double(x), java.sql.Types.DOUBLE, 0, 0);
    }

    @Override
    public void setFloat(int parameterIndex, float x) throws SQLException {
        setParameter(parameterIndex, new Float(x), java.sql.Types.REAL, 0, 0);
    }

    @Override
    public void setInt(int parameterIndex, int x) throws SQLException {
        setParameter(parameterIndex, new Integer(x), java.sql.Types.INTEGER, 0, 0);
    }

    @Override
    public void setNull(int parameterIndex, int sqlType) throws SQLException {
        if (sqlType == java.sql.Types.CLOB) {
            sqlType = java.sql.Types.LONGVARCHAR;
        } else if (sqlType == java.sql.Types.BLOB) {
            sqlType = java.sql.Types.LONGVARBINARY;
        }

        setParameter(parameterIndex, null, sqlType, -1, 0);
    }

    @Override
    public void setLong(int parameterIndex, long x) throws SQLException {
        setParameter(parameterIndex, new Long(x), java.sql.Types.BIGINT, 0, 0);
    }

    @Override
    public void setShort(int parameterIndex, short x) throws SQLException {
        setParameter(parameterIndex, new Integer(x), java.sql.Types.SMALLINT, 0, 0);
    }

    @Override
    public void setBoolean(int parameterIndex, boolean x) throws SQLException {
        setParameter(parameterIndex, x ? Boolean.TRUE : Boolean.FALSE, BOOLEAN, 0, 0);
    }

    @Override
    public void setBytes(int parameterIndex, byte[] x) throws SQLException {
        setParameter(parameterIndex, x, java.sql.Types.BINARY, 0, 0);
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream inputStream, int length)
        throws SQLException {
        if (inputStream == null || length < 0) {
            setParameter(parameterIndex, null, java.sql.Types.LONGVARCHAR, 0, 0);
        } else {
            try {
                setCharacterStream(parameterIndex, new InputStreamReader(inputStream, "US-ASCII"), length);
            } catch (UnsupportedEncodingException e) {
                // Should never happen!
            }
        }
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, int length)
        throws SQLException {
        checkOpen();

        if (x == null || length < 0) {
            setBytes(parameterIndex, null);
        } else {
            setParameter(parameterIndex, x, java.sql.Types.LONGVARBINARY, 0, length);
        }
    }

    @Override
    public void setUnicodeStream(int parameterIndex, InputStream inputStream, int length)
        throws SQLException {
        checkOpen();
        if (inputStream == null || length < 0) {
            setString(parameterIndex, null);
        } else {
            try {
               length /= 2;
               char[] tmp = new char[length];
               int pos = 0;
               int b1 = inputStream.read();
               int b2 = inputStream.read();

               while (b1 >= 0 && b2 >= 0 && pos < length) {
                   tmp[pos++] = (char) (((b1 << 8) &0xFF00) | (b2 & 0xFF));
                   b1 = inputStream.read();
                   b2 = inputStream.read();
               }
               setString(parameterIndex, new String(tmp, 0, pos));
            } catch (java.io.IOException e) {
                throw new SQLException(Messages.get("error.generic.ioerror",
                                                           e.getMessage()), "HY000");
            }
        }
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, int length)
        throws SQLException {
        if (reader == null || length < 0) {
            setParameter(parameterIndex, null, java.sql.Types.LONGVARCHAR, 0, 0);
        } else {
            setParameter(parameterIndex, reader, java.sql.Types.LONGVARCHAR, 0, length);
        }
    }

    @Override
    public void setObject(int parameterIndex, Object x) throws SQLException {
        setObjectBase(parameterIndex, x, Support.getJdbcType(x), -1);
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType)
        throws SQLException {
        setObjectBase(parameterIndex, x, targetSqlType, -1);
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType, int scale)
        throws SQLException {
        checkOpen();
        if (scale < 0 || scale > connection.getMaxPrecision()) {
            throw new SQLException(Messages.get("error.generic.badscale"), "HY092");
        }
        setObjectBase(parameterIndex, x, targetSqlType, scale);
    }

    @Override
    public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
        notImplemented("PreparedStatement.setNull(int, int, String)");
    }

    @Override
    public void setString(int parameterIndex, String x) throws SQLException {
        setParameter(parameterIndex, x, java.sql.Types.VARCHAR, 0, 0);
    }

    @Override
    public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
        setParameter(parameterIndex, x, java.sql.Types.DECIMAL, -1, 0);
    }

    @Override
    public void setURL(int parameterIndex, URL url) throws SQLException {
        setString(parameterIndex, (url == null)? null: url.toString());
    }

    @Override
    public void setArray(int arg0, Array arg1) throws SQLException {
        notImplemented("PreparedStatement.setArray");
    }

    @Override
    public void setBlob(int parameterIndex, Blob x) throws SQLException {
        if (x == null) {
            setBytes(parameterIndex, null);
        } else {
            long length = x.length();

            if (length > Integer.MAX_VALUE) {
                throw new SQLException(Messages.get("error.resultset.longblob"), "24000");
            }

            setBinaryStream(parameterIndex, x.getBinaryStream(), (int) x.length());
        }
    }

    @Override
    public void setClob(int parameterIndex, Clob x) throws SQLException {
        if (x == null) {
            setString(parameterIndex, null);
        } else {
            long length = x.length();

            if (length > Integer.MAX_VALUE) {
                throw new SQLException(Messages.get("error.resultset.longclob"), "24000");
            }

            setCharacterStream(parameterIndex, x.getCharacterStream(), (int) x.length());
        }
    }

    @Override
    public void setDate(int parameterIndex, Date x) throws SQLException {
        setParameter(parameterIndex, x, java.sql.Types.DATE, 0, 0);
    }

    @Override
    public ParameterMetaData getParameterMetaData() throws SQLException {
        checkOpen();

        //
        // NB. This is usable only with the JDBC3 version of the interface.
        //
        if (connection.getServerType() == Driver.SYBASE) {
            // Sybase does return the parameter types for prepared sql.
            connection.prepareSQL(this, sql, new ParamInfo[0], false, false);
        }

        try {
            Class pmdClass = Class.forName("net.sourceforge.jtds.jdbc.ParameterMetaDataImpl");
            Class[] parameterTypes = new Class[] {ParamInfo[].class, JtdsConnection.class};
            Object[] arguments = new Object[] {parameters, connection};
            Constructor pmdConstructor = pmdClass.getConstructor(parameterTypes);

            return (ParameterMetaData) pmdConstructor.newInstance(arguments);
        } catch (Exception e) {
            notImplemented("PreparedStatement.getParameterMetaData");
        }

        return null;
    }

    @Override
    public void setRef(int parameterIndex, Ref x) throws SQLException {
        notImplemented("PreparedStatement.setRef");
    }

    @Override
    public ResultSet executeQuery() throws SQLException {
        checkOpen();
        reset();
        boolean useCursor = useCursor(false, null);

        if (procName == null && !(this instanceof JtdsCallableStatement)) {
            // Sync on the connection to make sure rollback() isn't called
            // between the moment when the statement is prepared and the moment
            // when it's executed.
            synchronized (connection) {
                String spName = connection.prepareSQL(this, sql, parameters, false, useCursor);
                return executeSQLQuery(sql, spName, parameters, useCursor);
            }
        } else {
            return executeSQLQuery(sql, procName, parameters, useCursor);
        }
    }

   @Override
   public ResultSetMetaData getMetaData()
      throws SQLException
   {
      checkOpen();

      if( colMetaData == null )
      {
         if( currentResult != null )
         {
            colMetaData = currentResult.columns;
         }
         else if( connection.getServerType() == Driver.SYBASE )
         {
            // Sybase can provide meta data as a by product of preparing the call
            connection.prepareSQL( this, sql, new ParamInfo[0], false, false );
         }
         else
         {
            // For Microsoft set all parameters to null and execute the query.
            // SET FMTONLY ON asks the server just to return meta data.
            // This only works for select statements
            if( "select".equals( sqlWord ) || "with".equals( sqlWord ) )
            {
               // copy parameters to avoid corrupting any values already set
               // by the user as we need to set a flag and null out the data.
               ParamInfo[] params = new ParamInfo[parameters.length];

               for( int i = 0; i < params.length; i ++ )
               {
                  params[i] = new ParamInfo( parameters[i].markerPos, false );
                  params[i].isSet = true;
               }

               // substitute nulls into SQL String
               StringBuilder testSql = new StringBuilder( sql.length() + 128 );
               testSql.append( "SET FMTONLY ON; " );
               testSql.append( Support.substituteParameters( sql, params, connection ) );
               testSql.append( "\r\n; SET FMTONLY OFF" );

               try
               {
                  tds.submitSQL( testSql.toString() );
                  colMetaData = tds.getColumns();
               }
               catch( SQLException e )
               {
                  // ensure FMTONLY is switched off!
                  tds.submitSQL( "SET FMTONLY OFF" );
               }
            }
         }
      }

      return colMetaData == null ? null : new JtdsResultSetMetaData( colMetaData, JtdsResultSet.getColumnCount( colMetaData ), connection.getUseLOBs() );
   }

    @Override
    public void setTime(int parameterIndex, Time x) throws SQLException {
        setParameter( parameterIndex, x, java.sql.Types.TIME, 0, 0 );
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
        setParameter(parameterIndex, x, java.sql.Types.TIMESTAMP, 0, 0);
    }

    @Override
    public void setDate(int parameterIndex, Date x, Calendar cal)
        throws SQLException {

        if (x != null && cal != null) {
            x = new java.sql.Date(Support.timeFromZone(x, cal));
        }

        setDate(parameterIndex, x);
    }

    @Override
    public void setTime(int parameterIndex, Time x, Calendar cal)
        throws SQLException {

        if (x != null && cal != null) {
            x = new Time(Support.timeFromZone(x, cal));
        }

        setTime(parameterIndex, x);
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal)
        throws SQLException {

        if (x != null && cal != null) {
            x = new java.sql.Timestamp(Support.timeFromZone(x, cal));
        }

        setTimestamp(parameterIndex, x);
    }

    @Override
    public int executeUpdate(String sql) throws SQLException {
        notSupported("executeUpdate(String)");
        return 0;
    }

    @Override
    public void addBatch(String sql) throws SQLException {
        notSupported("executeBatch(String)");
    }

    @Override
    public boolean execute(String sql) throws SQLException {
        notSupported("execute(String)");
        return false;
    }

    @Override
    public int executeUpdate(String sql, int getKeys) throws SQLException {
        notSupported("executeUpdate(String, int)");
        return 0;
    }

    @Override
    public boolean execute(String arg0, int arg1) throws SQLException {
        notSupported("execute(String, int)");
        return false;
    }

    @Override
    public int executeUpdate(String arg0, int[] arg1) throws SQLException {
        notSupported("executeUpdate(String, int[])");
        return 0;
    }

    @Override
    public boolean execute(String arg0, int[] arg1) throws SQLException {
        notSupported("execute(String, int[])");
        return false;
    }

    @Override
    public int executeUpdate(String arg0, String[] arg1) throws SQLException {
        notSupported("executeUpdate(String, String[])");
        return 0;
    }

    @Override
    public boolean execute(String arg0, String[] arg1) throws SQLException {
        notSupported("execute(String, String[])");
        return false;
    }

    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
        notSupported("executeQuery(String)");
        return null;
    }

    /////// JDBC4 demarcation, do NOT put any JDBC3 code below this line ///////

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setAsciiStream(int, java.io.InputStream)
     */
    @Override
    public void setAsciiStream(int parameterIndex, InputStream x)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setAsciiStream(int, java.io.InputStream, long)
     */
    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, long length)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setBinaryStream(int, java.io.InputStream)
     */
    @Override
    public void setBinaryStream(int parameterIndex, InputStream x)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setBinaryStream(int, java.io.InputStream, long)
     */
    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, long length)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setBlob(int, java.io.InputStream)
     */
    @Override
    public void setBlob(int parameterIndex, InputStream inputStream)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setBlob(int, java.io.InputStream, long)
     */
    @Override
    public void setBlob(int parameterIndex, InputStream inputStream, long length)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader)
     */
    @Override
    public void setCharacterStream(int parameterIndex, Reader reader)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, long)
     */
    @Override
    public void setCharacterStream(int parameterIndex, Reader reader,
            long length) throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setClob(int, java.io.Reader)
     */
    @Override
    public void setClob(int parameterIndex, Reader reader) throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setClob(int, java.io.Reader, long)
     */
    @Override
    public void setClob(int parameterIndex, Reader reader, long length)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setNCharacterStream(int, java.io.Reader)
     */
    @Override
    public void setNCharacterStream(int parameterIndex, Reader value)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setNCharacterStream(int, java.io.Reader, long)
     */
    @Override
    public void setNCharacterStream(int parameterIndex, Reader value,
            long length) throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setNClob(int, java.sql.NClob)
     */
    @Override
    public void setNClob(int parameterIndex, NClob value) throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setNClob(int, java.io.Reader)
     */
    @Override
    public void setNClob(int parameterIndex, Reader reader) throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setNClob(int, java.io.Reader, long)
     */
    @Override
    public void setNClob(int parameterIndex, Reader reader, long length)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setNString(int, java.lang.String)
     */
    @Override
    public void setNString(int parameterIndex, String value)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setRowId(int, java.sql.RowId)
     */
    @Override
    public void setRowId(int parameterIndex, RowId x) throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

    /* (non-Javadoc)
     * @see java.sql.PreparedStatement#setSQLXML(int, java.sql.SQLXML)
     */
    @Override
    public void setSQLXML(int parameterIndex, SQLXML xmlObject)
            throws SQLException {
        // TODO Auto-generated method stub
        throw new AbstractMethodError();
    }

}