All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.sqlserver.jdbc.Parameter Maven / Gradle / Ivy

There is a newer version: 12.7.0.jre11-preview
Show newest version
/*
 * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
 * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
 */

package com.microsoft.sqlserver.jdbc;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.ResultSet;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.Calendar;
import java.util.Locale;


/**
 * Parameter represents a JDBC parameter value that is supplied with a prepared or callable statement or an updatable
 * result set. Parameter is JDBC type specific and is capable of representing any Java native type as well as a number
 * of Java object types including binary and character streams.
 */

final class Parameter {
    // Value type info for OUT parameters (excluding return status)
    private TypeInfo typeInfo;

    // For unencrypted parameters cryptometa will be null. For encrypted parameters it will hold encryption metadata.
    CryptoMetadata cryptoMeta = null;

    TypeInfo getTypeInfo() {
        return typeInfo;
    }

    final CryptoMetadata getCryptoMetadata() {
        return cryptoMeta;
    }

    private boolean shouldHonorAEForParameter = false;
    private boolean userProvidesPrecision = false;
    private boolean userProvidesScale = false;

    // The parameter type definition
    private String typeDefinition = null;
    boolean renewDefinition = false;

    // updated if sendStringParametersAsUnicode=true for setNString, setNCharacterStream, and setNClob methods
    private JDBCType jdbcTypeSetByUser = null;

    // set length of value for variable length type (String)
    private int valueLength = 0;

    private boolean forceEncryption = false;

    Parameter(boolean honorAE) {
        shouldHonorAEForParameter = honorAE;
    }

    // Flag set to true if this is a registered OUTPUT parameter.
    boolean isOutput() {
        return null != registeredOutDTV;
    }

    // Since a parameter can have only one type definition for both sending its value to the server (IN)
    // and getting its value from the server (OUT), we use the JDBC type of the IN parameter value if there
    // is one; otherwise we use the registered OUT param JDBC type.
    JDBCType getJdbcType() throws SQLServerException {
        return (null != inputDTV) ? inputDTV.getJdbcType() : JDBCType.UNKNOWN;
    }

    /**
     * Used when sendStringParametersAsUnicode=true to derive the appropriate National Character Set JDBC type
     * corresponding to the specified JDBC type.
     */
    private static JDBCType getSSPAUJDBCType(JDBCType jdbcType) {
        switch (jdbcType) {
            case CHAR:
                return JDBCType.NCHAR;
            case VARCHAR:
                return JDBCType.NVARCHAR;
            case LONGVARCHAR:
                return JDBCType.LONGNVARCHAR;
            case CLOB:
                return JDBCType.NCLOB;
            default:
                return jdbcType;
        }
    }

    // For parameters whose underlying type is not represented by a JDBC type
    // the transport type reflects how the value is sent to the
    // server (e.g. JDBCType.CHAR for GUID parameters).
    void registerForOutput(JDBCType jdbcType, SQLServerConnection con) throws SQLServerException {
        // DateTimeOffset is not supported with SQL Server versions earlier than Katmai
        if (JDBCType.DATETIMEOFFSET == jdbcType && !con.isKatmaiOrLater()) {
            throw new SQLServerException(SQLServerException.getErrString("R_notSupported"),
                    SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET, null);
        }

        // sendStringParametersAsUnicode
        // If set to true, this connection property tells the driver to send textual parameters
        // to the server as Unicode rather than MBCS. This is accomplished here by re-tagging
        // the value with the appropriate corresponding Unicode type.
        if (con.sendStringParametersAsUnicode()) {

            if (shouldHonorAEForParameter) {
                setJdbcTypeSetByUser(jdbcType);
            }

            jdbcType = getSSPAUJDBCType(jdbcType);
        }

        registeredOutDTV = new DTV();
        registeredOutDTV.setJdbcType(jdbcType);

        if (null == setterDTV)
            inputDTV = registeredOutDTV;

        resetOutputValue();
    }

    int scale = 0;

    // Scale requested for a DECIMAL and NUMERIC OUT parameter. If the OUT parameter
    // is also non-null IN parameter, the scale will be the larger of this value and
    // the value of the IN parameter's scale.
    private int outScale = 4;

    int getOutScale() {
        return outScale;
    }

    void setOutScale(int outScale) {
        this.outScale = outScale;
        userProvidesScale = true;
    }

    // The parameter name
    private String name;
    private String schemaName;

    /*
     * The different DTVs representing the parameter's value: getterDTV - The OUT value, if set, of the parameter after
     * execution. This is the value retrieved by CallableStatement getter methods. registeredOutDTV - The "IN" value
     * corresponding to a SQL NULL with a JDBC type that was passed to the CallableStatement.registerOutParameter
     * method. Since SQL Server does not directly support OUT-only parameters (just IN and IN/OUT), the driver sends a
     * null IN value for an OUT parameter, unless the application set an input value (setterDTV) as well. setterDTV -
     * The IN value, if set, of the parameter. This is the value set by PreparedStatement and CallableStatement setter
     * methods. inputDTV - If set, refers to either setterDTV or registeredOutDTV depending on whether the parameter is
     * IN, IN/OUT, or OUT-only. If cleared (i.e. set to null), it means that no value is set for the parameter and that
     * execution of the PreparedStatement or CallableStatement should throw a "parameter not set" exception. Note that
     * if the parameter value is a stream, the driver consumes its contents it at execution and clears inputDTV and
     * setterDTV so that the application must reset the parameter prior to the next execution to avoid getting a
     * "parameter not set" exception.
     */
    private DTV getterDTV;
    private DTV registeredOutDTV = null;
    private DTV setterDTV = null;
    private DTV inputDTV = null;

    /**
     * Clones this Parameter object for use in a batch.
     *
     * The clone method creates a shallow clone of the Parameter object. That is, the cloned instance references all of
     * the same internal objects and state as the original.
     *
     * Note: this method is purposely NOT the Object.clone() method, as that method has specific requirements and
     * semantics that we don't need here.
     */
    final Parameter cloneForBatch() {
        Parameter clonedParam = new Parameter(shouldHonorAEForParameter);
        clonedParam.typeInfo = typeInfo;
        clonedParam.typeDefinition = typeDefinition;
        clonedParam.outScale = outScale;
        clonedParam.name = name;
        clonedParam.getterDTV = getterDTV;
        clonedParam.registeredOutDTV = registeredOutDTV;
        clonedParam.setterDTV = setterDTV;
        clonedParam.inputDTV = inputDTV;
        clonedParam.cryptoMeta = cryptoMeta;
        clonedParam.jdbcTypeSetByUser = jdbcTypeSetByUser;
        clonedParam.valueLength = valueLength;
        clonedParam.userProvidesPrecision = userProvidesPrecision;
        clonedParam.userProvidesScale = userProvidesScale;
        return clonedParam;
    }

    /**
     * Skip value.
     */
    final void skipValue(TDSReader tdsReader, boolean isDiscard) throws SQLServerException {
        if (null == getterDTV)
            getterDTV = new DTV();

        deriveTypeInfo(tdsReader);

        getterDTV.skipValue(typeInfo, tdsReader, isDiscard);
    }

    /**
     * Skip value.
     */
    final void skipRetValStatus(TDSReader tdsReader) throws SQLServerException {

        StreamRetValue srv = new StreamRetValue();
        srv.setFromTDS(tdsReader);
    }

    // Clear an INPUT parameter value
    void clearInputValue() {
        setterDTV = null;
        inputDTV = registeredOutDTV;
    }

    // reset output value for re -execution
    // if there was old value reset it to a new DTV
    void resetOutputValue() {
        getterDTV = null;
        typeInfo = null;
    }

    void deriveTypeInfo(TDSReader tdsReader) throws SQLServerException {
        if (null == typeInfo) {
            typeInfo = TypeInfo.getInstance(tdsReader, true);

            if (shouldHonorAEForParameter && typeInfo.isEncrypted()) {
                // In this case, method getCryptoMetadata(tdsReader) retrieves baseTypeInfo without cryptoMetadata,
                // so save cryptoMetadata first.
                CekTableEntry cekEntry = cryptoMeta.getCekTableEntry();
                cryptoMeta = (new StreamRetValue()).getCryptoMetadata(tdsReader);
                cryptoMeta.setCekTableEntry(cekEntry);
            }
        }
    }

    void setFromReturnStatus(int returnStatus, SQLServerConnection con) throws SQLServerException {
        if (null == getterDTV)
            getterDTV = new DTV();

        getterDTV.setValue(null, JDBCType.INTEGER, returnStatus, JavaType.INTEGER, null, null, null, con,
                getForceEncryption());
    }

    void setValue(JDBCType jdbcType, Object value, JavaType javaType, StreamSetterArgs streamSetterArgs,
            Calendar calendar, Integer precision, Integer scale, SQLServerConnection con, boolean forceEncrypt,
            SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting, int parameterIndex, String userSQL,
            String tvpName) throws SQLServerException {

        if (shouldHonorAEForParameter) {
            userProvidesPrecision = false;
            userProvidesScale = false;

            if (null != precision) {
                userProvidesPrecision = true;
            }

            if (null != scale) {
                userProvidesScale = true;
            }

            // for encrypted tinyint, we need to convert short value to byte value,
            // otherwise it would be sent as smallint
            // Also, for setters, we are able to send tinyint to smallint
            // However, for output parameter, it might cause error.
            if (!isOutput()) {
                if ((JavaType.SHORT == javaType)
                        && ((JDBCType.TINYINT == jdbcType) || (JDBCType.SMALLINT == jdbcType))) {
                    // value falls in the TINYINT range
                    if (((Short) value) >= 0 && ((Short) value) <= 255) {
                        value = ((Short) value).byteValue();
                        javaType = JavaType.of(value);
                        jdbcType = javaType.getJDBCType(SSType.UNKNOWN, jdbcType);
                    }
                    // value falls outside tinyint range. Throw an error if the user intends to send as tinyint.
                    else {
                        // This is for cases like setObject(1, Short.valueOf("-1"), java.sql.Types.TINYINT);
                        if (JDBCType.TINYINT == jdbcType) {
                            MessageFormat form = new MessageFormat(
                                    SQLServerException.getErrString("R_InvalidDataForAE"));
                            Object[] msgArgs = {javaType.toString().toLowerCase(Locale.ENGLISH),
                                    jdbcType.toString().toLowerCase(Locale.ENGLISH)};
                            throw new SQLServerException(form.format(msgArgs), null);
                        }
                    }
                }
            }
        }

        // forceEncryption is true, shouldhonorae is false
        if (forceEncrypt && !Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, con)) {

            MessageFormat form = new MessageFormat(
                    SQLServerException.getErrString("R_ForceEncryptionTrue_HonorAEFalse"));
            Object[] msgArgs = {parameterIndex, userSQL};
            SQLServerException.makeFromDriverError(con, this, form.format(msgArgs), null, true);

        }

        // DateTimeOffset is not supported with SQL Server versions earlier than Katmai
        if ((JDBCType.DATETIMEOFFSET == jdbcType || JavaType.DATETIMEOFFSET == javaType) && !con.isKatmaiOrLater()) {
            throw new SQLServerException(SQLServerException.getErrString("R_notSupported"),
                    SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET, null);
        }

        if (JavaType.TVP == javaType) {
            TVP tvpValue;
            if (null == value) {
                tvpValue = new TVP(tvpName);
            } else if (value instanceof SQLServerDataTable) {
                tvpValue = new TVP(tvpName, (SQLServerDataTable) value);
            } else if (value instanceof ResultSet) {
                tvpValue = new TVP(tvpName, (ResultSet) value);
            } else if (value instanceof ISQLServerDataRecord) {
                tvpValue = new TVP(tvpName, (ISQLServerDataRecord) value);
            } else {
                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_TVPInvalidValue"));
                Object[] msgArgs = {parameterIndex};
                throw new SQLServerException(form.format(msgArgs), null);
            }

            if (!tvpValue.isNull() && (0 == tvpValue.getTVPColumnCount())) {
                throw new SQLServerException(SQLServerException.getErrString("R_TVPEmptyMetadata"), null);
            }
            name = (tvpValue).getTVPName();
            schemaName = tvpValue.getOwningSchemaNameTVP();

            value = tvpValue;
        }

        // setting JDBCType and exact length needed for AE stored procedure
        if (shouldHonorAEForParameter) {
            setForceEncryption(forceEncrypt);

            // set it if it is not output parameter or jdbcTypeSetByUser is null
            if (!(this.isOutput() && this.jdbcTypeSetByUser != null)) {
                setJdbcTypeSetByUser(jdbcType);
            }

            // skip it if is (character types or binary type) & is output parameter && value is already set,
            if ((!(jdbcType.isTextual() || jdbcType.isBinary())) || !(this.isOutput()) || (this.valueLength == 0)) {
                this.valueLength = Util.getValueLengthBaseOnJavaType(value, javaType, precision, scale, jdbcType);
            }

            if (null != scale) {
                this.outScale = scale;
            }
        }

        // sendStringParametersAsUnicode
        // If set to true, this connection property tells the driver to send textual parameters
        // to the server as Unicode rather than MBCS. This is accomplished here by re-tagging
        // the value with the appropriate corresponding Unicode type.
        // JavaType.OBJECT == javaType when calling setNull()
        if (con.sendStringParametersAsUnicode() && (JavaType.STRING == javaType || JavaType.READER == javaType
                || JavaType.CLOB == javaType || JavaType.OBJECT == javaType)) {
            jdbcType = getSSPAUJDBCType(jdbcType);
        }

        DTV newDTV = new DTV();
        newDTV.setValue(con.getDatabaseCollation(), jdbcType, value, javaType, streamSetterArgs, calendar, scale, con,
                forceEncrypt);

        if (!con.sendStringParametersAsUnicode()) {
            newDTV.sendStringParametersAsUnicode = false;
        }

        inputDTV = setterDTV = newDTV;
    }

    boolean isNull() {
        if (null != getterDTV)
            return getterDTV.isNull();

        return false;
    }

    boolean isValueGotten() {
        return null != getterDTV;

    }

    Object getValue(JDBCType jdbcType, InputStreamGetterArgs getterArgs, Calendar cal,
            TDSReader tdsReader, SQLServerStatement statement) throws SQLServerException {
        if (null == getterDTV)
            getterDTV = new DTV();

        deriveTypeInfo(tdsReader);
        // If the parameter is not encrypted or column encryption is turned off (either at connection or
        // statement level), cryptoMeta would be null.
        return getterDTV.getValue(jdbcType, outScale, getterArgs, cal, typeInfo, cryptoMeta, tdsReader, statement);
    }

    Object getSetterValue() {
        return setterDTV.getSetterValue();
    }

    int getInt(TDSReader tdsReader, SQLServerStatement statement) throws SQLServerException {
        Integer value = (Integer) getValue(JDBCType.INTEGER, null, null, tdsReader, statement);
        return null != value ? value : 0;
    }

    /**
     * DTV execute op to determine the parameter type definition.
     */
    final class GetTypeDefinitionOp extends DTVExecuteOp {
        private static final String NVARCHAR_MAX = "nvarchar(max)";
        private static final String NVARCHAR_4K = "nvarchar(4000)";
        private static final String NTEXT = "ntext";

        private static final String VARCHAR_MAX = "varchar(max)";
        private static final String VARCHAR_8K = "varchar(8000)";
        private static final String TEXT = "text";

        private static final String VARBINARY_MAX = "varbinary(max)";
        private static final String VARBINARY_8K = "varbinary(8000)";
        private static final String IMAGE = "image";

        private final Parameter param;
        private final SQLServerConnection con;

        GetTypeDefinitionOp(Parameter param, SQLServerConnection con) {
            this.param = param;
            this.con = con;
        }

        private void setTypeDefinition(DTV dtv) {
            switch (dtv.getJdbcType()) {
                case TINYINT:
                    param.typeDefinition = SSType.TINYINT.toString();
                    break;

                case SMALLINT:
                    param.typeDefinition = SSType.SMALLINT.toString();
                    break;

                case INTEGER:
                    param.typeDefinition = SSType.INTEGER.toString();
                    break;

                case BIGINT:
                    param.typeDefinition = SSType.BIGINT.toString();
                    break;

                case REAL:
                    // sp_describe_parameter_encryption must be queried as real for AE
                    if (param.shouldHonorAEForParameter && (null != jdbcTypeSetByUser)
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        param.typeDefinition = SSType.REAL.toString();
                    } else {
                        // use FLOAT if column is not encrypted
                        param.typeDefinition = SSType.FLOAT.toString();
                    }
                    break;

                case FLOAT:
                case DOUBLE:
                    param.typeDefinition = SSType.FLOAT.toString();
                    break;

                case DECIMAL:
                case NUMERIC:
                    // First, bound the scale by the maximum allowed by SQL Server
                    if (scale > SQLServerConnection.maxDecimalPrecision)
                        scale = SQLServerConnection.maxDecimalPrecision;

                    // Next, prepare with the largest of:
                    // - the value's scale (initial value, as limited above)
                    // - the specified input scale (if any)
                    // - the registered output scale
                    Integer inScale = dtv.getScale();
                    if (null != inScale && scale < inScale)
                        scale = inScale;

                    if (param.isOutput() && scale < param.getOutScale())
                        scale = param.getOutScale();

                    if (param.shouldHonorAEForParameter && (null != jdbcTypeSetByUser)
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        if (0 == valueLength) {
                            // for prepared statement and callable statement, There are only two cases where valueLength
                            // is 0:
                            // 1. when the parameter is output parameter
                            // 2. for input parameter, the value is null
                            // so, here, if the decimal parameter is encrypted and it is null and it is not outparameter
                            // then we set precision as the default precision instead of max precision
                            if (!isOutput()) {
                                param.typeDefinition = "decimal(" + SQLServerConnection.defaultDecimalPrecision + ", "
                                        + scale + ")";
                            }
                        } else {
                            if (SQLServerConnection.defaultDecimalPrecision >= valueLength) {
                                param.typeDefinition = "decimal(" + SQLServerConnection.defaultDecimalPrecision + ","
                                        + scale + ")";

                                if (SQLServerConnection.defaultDecimalPrecision < (valueLength + scale)) {
                                    param.typeDefinition = "decimal("
                                            + (SQLServerConnection.defaultDecimalPrecision + scale) + "," + scale + ")";
                                }
                            } else {
                                param.typeDefinition = "decimal(" + SQLServerConnection.maxDecimalPrecision + ","
                                        + scale + ")";
                            }
                        }

                        if (isOutput()) {
                            param.typeDefinition = "decimal(" + SQLServerConnection.maxDecimalPrecision + ", " + scale
                                    + ")";
                        }

                        if (userProvidesPrecision) {
                            param.typeDefinition = "decimal(" + valueLength + "," + scale + ")";
                        }
                    } else
                        param.typeDefinition = "decimal(" + SQLServerConnection.maxDecimalPrecision + "," + scale + ")";

                    break;

                case MONEY:
                    param.typeDefinition = SSType.MONEY.toString();
                    break;
                case SMALLMONEY:
                    param.typeDefinition = SSType.MONEY.toString();

                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        param.typeDefinition = SSType.SMALLMONEY.toString();
                    }

                    break;
                case BIT:
                case BOOLEAN:
                    param.typeDefinition = SSType.BIT.toString();
                    break;

                case LONGVARBINARY:
                case BLOB:
                    param.typeDefinition = VARBINARY_MAX;
                    break;

                case BINARY:
                case VARBINARY:
                    // To avoid the server side cost of re-preparing, once a "long" type, always a "long" type...
                    if (VARBINARY_MAX.equals(param.typeDefinition) || IMAGE.equals(param.typeDefinition))
                        break;
                    if (param.shouldHonorAEForParameter && (null != jdbcTypeSetByUser)
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        if (0 == valueLength) {
                            // Workaround for the issue when inserting empty string and null into encrypted columns
                            param.typeDefinition = "varbinary(1)";
                            valueLength++;
                        } else {
                            param.typeDefinition = "varbinary(" + valueLength + ")";
                        }

                        if (JDBCType.LONGVARBINARY == jdbcTypeSetByUser) {
                            param.typeDefinition = VARBINARY_MAX;
                        }
                    } else
                        param.typeDefinition = VARBINARY_8K;
                    break;

                case DATE:
                    // Bind DATE values to pre-Katmai servers as DATETIME (which has no DATE-only type).
                    param.typeDefinition = con.isKatmaiOrLater() ? SSType.DATE.toString() : SSType.DATETIME.toString();
                    break;

                case TIME:
                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {

                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */

                        if (userProvidesScale) {
                            param.typeDefinition = SSType.TIME.toString() + "(" + outScale + ")";
                        } else {
                            param.typeDefinition = SSType.TIME.toString() + "(" + valueLength + ")";
                        }
                    } else {
                        param.typeDefinition = con.getSendTimeAsDatetime() ? SSType.DATETIME.toString()
                                                                           : SSType.TIME.toString();
                    }
                    break;

                case TIMESTAMP:
                    // Bind TIMESTAMP values to pre-Katmai servers as DATETIME. Bind TIMESTAMP values to
                    // Katmai and later servers as DATETIME2 to take advantage of increased precision.
                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        if (userProvidesScale) {
                            param.typeDefinition = con
                                    .isKatmaiOrLater() ? (SSType.DATETIME2.toString() + "(" + outScale + ")")
                                                       : (SSType.DATETIME.toString());
                        } else {
                            param.typeDefinition = con.isKatmaiOrLater()
                                                                         ? (SSType.DATETIME2.toString() + "("
                                                                                 + valueLength + ")")
                                                                         : SSType.DATETIME.toString();
                        }
                    } else {
                        param.typeDefinition = con.isKatmaiOrLater() ? SSType.DATETIME2.toString()
                                                                     : SSType.DATETIME.toString();
                    }
                    break;

                case DATETIME:
                    // send as Datetime by default
                    param.typeDefinition = SSType.DATETIME2.toString();

                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        param.typeDefinition = SSType.DATETIME.toString();
                    }

                    if (!param.shouldHonorAEForParameter) {
                        // if AE is off and it is output parameter of stored procedure, sent it as datetime2(3)
                        // otherwise it returns incorrect milliseconds.
                        if (param.isOutput()) {
                            param.typeDefinition = SSType.DATETIME2.toString() + "(" + outScale + ")";
                        }
                    } else {
                        // when AE is on, set it to Datetime by default,
                        // However, if column is not encrypted and it is output parameter of stored procedure,
                        // renew it to datetime2(3)
                        if (null == param.getCryptoMetadata() && param.renewDefinition) {
                            if (param.isOutput()) {
                                param.typeDefinition = SSType.DATETIME2.toString() + "(" + outScale + ")";
                            }
                            break;
                        }
                    }
                    break;

                case SMALLDATETIME:
                    param.typeDefinition = SSType.DATETIME2.toString();

                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        param.typeDefinition = SSType.SMALLDATETIME.toString();
                    }

                    break;

                case TIME_WITH_TIMEZONE:
                case TIMESTAMP_WITH_TIMEZONE:
                case DATETIMEOFFSET:
                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        if (userProvidesScale) {
                            param.typeDefinition = SSType.DATETIMEOFFSET.toString() + "(" + outScale + ")";
                        } else {
                            param.typeDefinition = SSType.DATETIMEOFFSET.toString() + "(" + valueLength + ")";
                        }
                    } else {
                        param.typeDefinition = SSType.DATETIMEOFFSET.toString();
                    }
                    break;

                case LONGVARCHAR:
                case CLOB:
                    param.typeDefinition = VARCHAR_MAX;
                    break;

                case CHAR:
                case VARCHAR:
                    // To avoid the server side cost of re-preparing, once a "long" type, always a "long" type...
                    if (VARCHAR_MAX.equals(param.typeDefinition) || TEXT.equals(param.typeDefinition))
                        break;

                    // Adding for case useColumnEncryption=true & sendStringParametersAsUnicode=false
                    if (param.shouldHonorAEForParameter && (null != jdbcTypeSetByUser)
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        if (0 == valueLength) {
                            // Workaround for the issue when inserting empty string and null into encrypted columns
                            param.typeDefinition = "varchar(1)";
                            valueLength++;
                        } else {
                            param.typeDefinition = "varchar(" + valueLength + ")";

                            if (DataTypes.SHORT_VARTYPE_MAX_BYTES <= valueLength) {
                                param.typeDefinition = VARCHAR_MAX;
                            }
                        }
                    } else
                        param.typeDefinition = VARCHAR_8K;
                    break;

                case LONGNVARCHAR:
                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        if ((null != jdbcTypeSetByUser)
                                && ((jdbcTypeSetByUser == JDBCType.VARCHAR) || (jdbcTypeSetByUser == JDBCType.CHAR)
                                        || (jdbcTypeSetByUser == JDBCType.LONGVARCHAR))) {
                            if (0 == valueLength) {
                                // Workaround for the issue when inserting empty string and null into encrypted columns
                                param.typeDefinition = "varchar(1)";
                                valueLength++;
                            } else if (DataTypes.SHORT_VARTYPE_MAX_BYTES < valueLength) {
                                param.typeDefinition = VARCHAR_MAX;
                            } else {
                                param.typeDefinition = "varchar(" + valueLength + ")";
                            }

                            if (jdbcTypeSetByUser == JDBCType.LONGVARCHAR) {
                                param.typeDefinition = VARCHAR_MAX;
                            }
                        } else if ((null != jdbcTypeSetByUser) && (jdbcTypeSetByUser == JDBCType.NVARCHAR
                                || jdbcTypeSetByUser == JDBCType.LONGNVARCHAR)) {
                            if (0 == valueLength) {
                                // Workaround for the issue when inserting empty string and null into encrypted columns
                                param.typeDefinition = "nvarchar(1)";
                                valueLength++;
                            } else if (DataTypes.SHORT_VARTYPE_MAX_CHARS < valueLength) {
                                param.typeDefinition = NVARCHAR_MAX;
                            } else {
                                param.typeDefinition = "nvarchar(" + valueLength + ")";
                            }

                            if (jdbcTypeSetByUser == JDBCType.LONGNVARCHAR) {
                                param.typeDefinition = NVARCHAR_MAX;
                            }
                        } else { // used if setNull() is called with java.sql.Types.NCHAR
                            if (0 == valueLength) {
                                // Workaround for the issue when inserting empty string and null into encrypted columns
                                param.typeDefinition = "nvarchar(1)";
                                valueLength++;
                            } else {
                                param.typeDefinition = "nvarchar(" + valueLength + ")";

                                if (DataTypes.SHORT_VARTYPE_MAX_BYTES <= valueLength) {
                                    param.typeDefinition = NVARCHAR_MAX;
                                }
                            }
                        }
                        break;
                    } else
                        param.typeDefinition = NVARCHAR_MAX;
                    break;

                case NCLOB:
                    // do not need to check if AE is enabled or not,
                    // because NCLOB does not work with it
                    param.typeDefinition = NVARCHAR_MAX;
                    break;

                case NCHAR:
                case NVARCHAR:
                    // To avoid the server side cost of re-preparing, once a "long" type, always a "long" type...
                    if (NVARCHAR_MAX.equals(param.typeDefinition) || NTEXT.equals(param.typeDefinition))
                        break;

                    if (param.shouldHonorAEForParameter
                            && !(null == param.getCryptoMetadata() && param.renewDefinition)) {
                        /*
                         * This means AE is ON in the connection, and (1) this is either the first round to SQL Server
                         * to get encryption meta data, or (2) this is the second round of renewing meta data and
                         * parameter is encrypted In both of these cases we need to send specific type info, otherwise
                         * generic type info can be used as before.
                         */
                        if ((null != jdbcTypeSetByUser)
                                && ((jdbcTypeSetByUser == JDBCType.VARCHAR) || (jdbcTypeSetByUser == JDBCType.CHAR)
                                        || (JDBCType.LONGVARCHAR == jdbcTypeSetByUser))) {
                            if (0 == valueLength) {
                                // Workaround for the issue when inserting empty string and null into encrypted columns
                                param.typeDefinition = "varchar(1)";
                                valueLength++;
                            } else {
                                param.typeDefinition = "varchar(" + valueLength + ")";

                                if (DataTypes.SHORT_VARTYPE_MAX_BYTES < valueLength) {
                                    param.typeDefinition = VARCHAR_MAX;
                                }
                            }

                            if (JDBCType.LONGVARCHAR == jdbcTypeSetByUser) {
                                param.typeDefinition = VARCHAR_MAX;
                            }
                        } else if ((null != jdbcTypeSetByUser)
                                && ((jdbcTypeSetByUser == JDBCType.NVARCHAR) || (jdbcTypeSetByUser == JDBCType.NCHAR)
                                        || (JDBCType.LONGNVARCHAR == jdbcTypeSetByUser))) {
                            if (0 == valueLength) {
                                // Workaround for the issue when inserting empty string and null into encrypted columns
                                param.typeDefinition = "nvarchar(1)";
                                valueLength++;
                            } else {
                                param.typeDefinition = "nvarchar(" + valueLength + ")";

                                if (DataTypes.SHORT_VARTYPE_MAX_BYTES <= valueLength) {
                                    param.typeDefinition = NVARCHAR_MAX;
                                }
                            }

                            if (JDBCType.LONGNVARCHAR == jdbcTypeSetByUser) {
                                param.typeDefinition = NVARCHAR_MAX;
                            }
                        } else { // used if setNull() is called with java.sql.Types.NCHAR
                            if (0 == valueLength) {
                                // Workaround for the issue when inserting empty string and null into encrypted columns
                                param.typeDefinition = "nvarchar(1)";
                                valueLength++;
                            } else {
                                param.typeDefinition = "nvarchar(" + valueLength + ")";

                                if (DataTypes.SHORT_VARTYPE_MAX_BYTES <= valueLength) {
                                    param.typeDefinition = NVARCHAR_MAX;
                                }
                            }
                        }
                        break;
                    } else
                        param.typeDefinition = NVARCHAR_4K;
                    break;
                case SQLXML:
                    param.typeDefinition = SSType.XML.toString();
                    break;

                case TVP:
                    // definition should contain the TVP name and the keyword READONLY
                    String schema = param.schemaName;

                    if (null != schema) {
                        param.typeDefinition = "[" + schema + "].[" + param.name + "] READONLY";
                    } else {
                        param.typeDefinition = "[" + param.name + "] READONLY";
                    }

                    break;

                case GUID:
                    param.typeDefinition = SSType.GUID.toString();
                    break;

                case SQL_VARIANT:
                    param.typeDefinition = SSType.SQL_VARIANT.toString();
                    break;

                case GEOMETRY:
                    param.typeDefinition = SSType.GEOMETRY.toString();
                    break;

                case GEOGRAPHY:
                    param.typeDefinition = SSType.GEOGRAPHY.toString();
                    break;
                default:
                    assert false : "Unexpected JDBC type " + dtv.getJdbcType();
                    break;
            }
        }

        void execute(DTV dtv, String strValue) throws SQLServerException {
            if (null != strValue && strValue.length() > DataTypes.SHORT_VARTYPE_MAX_CHARS)
                dtv.setJdbcType(JDBCType.LONGNVARCHAR);

            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Clob clobValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Byte byteValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Integer intValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, java.sql.Time timeValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, java.sql.Date dateValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, java.sql.Timestamp timestampValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, java.util.Date utildateValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, java.util.Calendar calendarValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, LocalDate localDateValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, LocalTime localTimeValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, LocalDateTime localDateTimeValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, OffsetTime offsetTimeValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, OffsetDateTime OffsetDateTimeValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, microsoft.sql.DateTimeOffset dtoValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Float floatValue) throws SQLServerException {
            scale = 4;
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Double doubleValue) throws SQLServerException {
            scale = 4;
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, BigDecimal bigDecimalValue) throws SQLServerException {
            if (null != bigDecimalValue) {
                scale = bigDecimalValue.scale();

                // BigDecimal in JRE 1.5 and later JVMs exposes an implementation detail
                // that allows representation of large values in small space by interpreting
                // a negative value for scale to imply scientific notation (e.g. 1 E 10^n)
                // would have a scale of -n. A BigDecimal value with a negative scale has
                // no fractional component.
                if (scale < 0)
                    scale = 0;
            }

            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Long longValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, java.math.BigInteger bigIntegerValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Short shortValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Boolean booleanValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {
            // exclude JDBC typecasting for Geometry/Geography as these datatypes don't have a size limit.
            if (null != byteArrayValue && byteArrayValue.length > DataTypes.SHORT_VARTYPE_MAX_BYTES
                    && (dtv.getJdbcType() != JDBCType.GEOMETRY && dtv.getJdbcType() != JDBCType.GEOGRAPHY))
                dtv.setJdbcType(dtv.getJdbcType().isBinary() ? JDBCType.LONGVARBINARY : JDBCType.LONGVARCHAR);

            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Blob blobValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, InputStream inputStreamValue) throws SQLServerException {
            StreamSetterArgs streamSetterArgs = dtv.getStreamSetterArgs();

            JDBCType jdbcType = dtv.getJdbcType();

            // If the JDBC type is currently a "short" type, then figure out if needs to be bumped up to a "long" type
            if (JDBCType.CHAR == jdbcType || JDBCType.VARCHAR == jdbcType || JDBCType.BINARY == jdbcType
                    || JDBCType.VARBINARY == jdbcType) {
                // If we know the length is too long for a "short" type, then convert to a "long" type.
                if (streamSetterArgs.getLength() > DataTypes.SHORT_VARTYPE_MAX_BYTES)
                    dtv.setJdbcType(jdbcType.isBinary() ? JDBCType.LONGVARBINARY : JDBCType.LONGVARCHAR);

                // If the length of the value is unknown, then figure out whether it is at least longer
                // than what will fit into a "short" type.
                else if (DataTypes.UNKNOWN_STREAM_LENGTH == streamSetterArgs.getLength()) {
                    byte[] vartypeBytes = new byte[1 + DataTypes.SHORT_VARTYPE_MAX_BYTES];
                    BufferedInputStream bufferedStream = new BufferedInputStream(inputStreamValue, vartypeBytes.length);

                    int bytesRead = 0;

                    try {
                        bufferedStream.mark(vartypeBytes.length);

                        bytesRead = bufferedStream.read(vartypeBytes, 0, vartypeBytes.length);

                        if (-1 == bytesRead)
                            bytesRead = 0;

                        bufferedStream.reset();
                    } catch (IOException e) {
                        MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
                        Object[] msgArgs = {e.toString()};
                        SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true);
                    }

                    dtv.setValue(bufferedStream, JavaType.INPUTSTREAM);

                    // If the stream is longer than what can fit into the "short" type, then use the "long" type
                    // instead.
                    // Otherwise, we know the exact stream length since we reached end of stream before reading
                    // SHORT_VARTYPE_MAX_BYTES + 1
                    // bytes. So adjust the setter args to reflect the known length to avoid unnecessarily copying the
                    // stream again in SendByRPCOp.
                    if (bytesRead > DataTypes.SHORT_VARTYPE_MAX_BYTES)
                        dtv.setJdbcType(jdbcType.isBinary() ? JDBCType.LONGVARBINARY : JDBCType.LONGVARCHAR);
                    else
                        streamSetterArgs.setLength(bytesRead);
                }
            }

            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, Reader readerValue) throws SQLServerException {
            // If the JDBC type is currently a "short" type, then figure out if needs to be bumped up to a "long" type
            if (JDBCType.NCHAR == dtv.getJdbcType() || JDBCType.NVARCHAR == dtv.getJdbcType()) {
                StreamSetterArgs streamSetterArgs = dtv.getStreamSetterArgs();

                // If we know the length is too long for a "short" type, then convert to a "long" type.
                if (streamSetterArgs.getLength() > DataTypes.SHORT_VARTYPE_MAX_CHARS)
                    dtv.setJdbcType(JDBCType.LONGNVARCHAR);

                // If the length of the value is unknown, then figure out whether it is at least longer
                // than what will fit into a "short" type.
                else if (DataTypes.UNKNOWN_STREAM_LENGTH == streamSetterArgs.getLength()) {
                    char[] vartypeChars = new char[1 + DataTypes.SHORT_VARTYPE_MAX_CHARS];
                    BufferedReader bufferedReader = new BufferedReader(readerValue, vartypeChars.length);

                    int charsRead = 0;

                    try {
                        bufferedReader.mark(vartypeChars.length);

                        charsRead = bufferedReader.read(vartypeChars, 0, vartypeChars.length);

                        if (-1 == charsRead)
                            charsRead = 0;

                        bufferedReader.reset();
                    } catch (IOException e) {
                        MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
                        Object[] msgArgs = {e.toString()};
                        SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true);
                    }

                    dtv.setValue(bufferedReader, JavaType.READER);

                    if (charsRead > DataTypes.SHORT_VARTYPE_MAX_CHARS)
                        dtv.setJdbcType(JDBCType.LONGNVARCHAR);
                    else
                        streamSetterArgs.setLength(charsRead);
                }
            }

            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, SQLServerSQLXML xmlValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        void execute(DTV dtv, com.microsoft.sqlserver.jdbc.TVP tvpValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

        /*
         * (non-Javadoc)
         * @see com.microsoft.sqlserver.jdbc.DTVExecuteOp#execute(com.microsoft.sqlserver.jdbc.DTV,
         * microsoft.sql.SqlVariant)
         */
        @Override
        void execute(DTV dtv, SqlVariant SqlVariantValue) throws SQLServerException {
            setTypeDefinition(dtv);
        }

    }

    /**
     * Returns a string used to define the parameter type for the server; null if no value for the parameter has been
     * set or registered.
     */
    String getTypeDefinition(SQLServerConnection con, TDSReader tdsReader) throws SQLServerException {
        if (null == inputDTV)
            return null;

        inputDTV.executeOp(new GetTypeDefinitionOp(this, con));
        return typeDefinition;
    }

    void sendByRPC(TDSWriter tdsWriter, SQLServerStatement statement) throws SQLServerException {
        assert null != inputDTV : "Parameter was neither set nor registered";
        SQLServerConnection conn = statement.connection;
        
        try {
            inputDTV.sendCryptoMetaData(this.cryptoMeta, tdsWriter);
            inputDTV.setJdbcTypeSetByUser(getJdbcTypeSetByUser(), getValueLength());
            inputDTV.sendByRPC(name, null, conn.getDatabaseCollation(), valueLength, isOutput() ? outScale : scale,
                    isOutput(), tdsWriter, statement);
        } finally {
            // reset the cryptoMeta in IOBuffer
            inputDTV.sendCryptoMetaData(null, tdsWriter);
        }
        // Per JDBC spec:
        // "If in the execution of a PreparedStatement object, the JDBC driver reads values set
        // for the parameter markers by the methods setAsciiStream, setBinaryStream, setCharacterStream,
        // setNCharacterStream, or setUnicodeStream, those parameters must be reset prior to the next
        // execution of the PreparedStatement object otherwise a SQLException will be thrown."
        //
        // Clear the input and setter DTVs to relinquish their hold on the stream resource and ensure
        // that the next call to execute will throw a SQLException (from getTypeDefinitionOp).
        // Don't clear the registered output DTV so that the parameter will still be an OUT (IN/OUT) parameter.
        if (JavaType.INPUTSTREAM == inputDTV.getJavaType() || JavaType.READER == inputDTV.getJavaType()) {
            inputDTV = setterDTV = null;
        }
    }

    JDBCType getJdbcTypeSetByUser() {
        return jdbcTypeSetByUser;
    }

    void setJdbcTypeSetByUser(JDBCType jdbcTypeSetByUser) {
        this.jdbcTypeSetByUser = jdbcTypeSetByUser;
    }

    int getValueLength() {
        return valueLength;
    }

    void setValueLength(int valueLength) {
        this.valueLength = valueLength;
        userProvidesPrecision = true;
    }

    boolean getForceEncryption() {
        return forceEncryption;
    }

    void setForceEncryption(boolean forceEncryption) {
        this.forceEncryption = forceEncryption;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy