/*
 * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2019, 2020 IBM Corporation. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     05/24/2011-2.3 Guy Pelletier
//       - 345962: Join fetch query when using tenant discriminator column fails.
//     02/08/2012-2.4 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     07/13/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     08/24/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     09/27/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     09/03/2015 - Will Dazey
//       - 456067 : Added support for defining query timeout units
package org.eclipse.persistence.internal.databaseaccess;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Struct;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.expressions.ParameterExpression;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.queries.CallQueryMechanism;
import org.eclipse.persistence.internal.queries.DatabaseQueryMechanism;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.structures.ObjectRelationalDataTypeDescriptor;
import org.eclipse.persistence.mappings.structures.ObjectRelationalDatabaseField;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.ModifyQuery;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.sessions.DatabaseRecord;

/**
 * INTERNAL:
 * <b>Purpose<b>: Used as an abstraction of a database invocation.
 * A call is an SQL string or procedure call with parameters.
 */
public abstract class DatabaseCall extends DatasourceCall {
    /**
     * JPA 2.1 NamedStoredProcedureQuery execute API implementation.
     */
    protected boolean executeReturnValue;

    /**
     * Following fields are used to bind MaxResults and FirstRow settings into
     * the query instead of using the values stored in the call.
     */
    public static final DatabaseField MAXROW_FIELD = new DatabaseField("EclipseLink-MaxResults");
    public static final DatabaseField FIRSTRESULT_FIELD = new DatabaseField("EclipseLink-FirstRow");

    /**
     * Indicates if the FirstRow value in this call object is to be ignored. If
     * true, it should mean it has been built into the SQL statement directly
     * ex: using Oracle Rownum support
     */
    protected boolean ignoreFirstRowSetting;

    /**
     * Indicates if the MaxResults value in this call object is to be ignored.
     * If true, it should mean it has been built into the SQL statement directly
     * ex: using Oracle Rownum support
     */
    protected boolean ignoreMaxResultsSetting;

    // The result and statement are cached for cursor selects.
    transient protected Statement statement;
    transient protected ResultSet result;

    // The call may specify that its parameters should be bound.
    protected Boolean usesBinding;

    // Bound calls can use prepared statement caching.
    protected Boolean shouldCacheStatement;

    // The returned fields.
    transient protected Vector fields;
    // PERF: fields array
    transient protected DatabaseField[] fieldsArray;

    // Field matching is required for custom SQL when the fields order is not known.
    protected boolean isFieldMatchingRequired;

    // optimistic locking determination is required for batch writing
    protected boolean hasOptimisticLock;
    protected boolean isResultSetScrollable;

    // JDK 1.2 supports initial fetch size for the result set.
    protected int resultSetFetchSize;

    // JDK 1.2 supports various types of results set
    protected int resultSetType;

    // JDK 1.2 supports various types of concurrency on results set
    protected int resultSetConcurrency;

    //query timeout limit in seconds
    protected int queryTimeout;

    //query timeout unit
    protected TimeUnit queryTimeoutUnit;

    //max rows returned in the result set by the call
    protected int maxRows;

    //firstResult set into the result set by the call
    protected int firstResult;

    //contain field - value pairs for LOB fields used to the
    //streaming operation during the writing (to the table)
    private transient AbstractRecord contexts;

    /** Allow for a single cursored output parameter. */
    protected boolean isCursorOutputProcedure;

    /** Allow for multiple cursored output parameter. */
    protected boolean isMultipleCursorOutputProcedure;

    // This parameter is here to determine if we should expect a ResultSet back from the call
    // We need to know this information in order to call the correct JDBC API
    protected Boolean returnsResultSet;

    // Whether the call has to build output row
    protected boolean shouldBuildOutputRow;

    // Callable statement is required if there is an output parameter
    protected boolean isCallableStatementRequired;

    /** Support multiple result sets. */
    protected boolean hasMultipleResultSets;

    /**
     * Support returning multiple results sets instead of just one list, i.e.
     * support multiple results set mappings.
     */
    protected boolean returnMultipleResultSetCollections;

    /** The SQL string to execute. */
    protected String sqlString;

    /** Indicates whether the call has allocated connection. May be set if the call has not finished */
    protected boolean hasAllocatedConnection;

    /**
     * Define if this query is compatible with batch writing.
     * Some queries, such as DDL are not compatible.
     */
    protected boolean isBatchExecutionSupported;

    /**
     * Keep a list of the output cursors.
     */
    protected List<DatabaseField> outputCursors;

    public DatabaseCall() {
        super.shouldProcessTokenInQuotes = false;
        this.usesBinding = null;
        this.shouldCacheStatement = null;
        this.isFieldMatchingRequired = false;
        this.queryTimeout = 0;
        this.queryTimeoutUnit = null;
        this.maxRows = 0;
        this.resultSetFetchSize = 0;
        this.isCursorOutputProcedure = false;
        this.shouldBuildOutputRow = false;
        this.returnsResultSet = null;
        this.isBatchExecutionSupported = true;
    }

    /**
     * Return if the call returns multiple result sets.
     */
    public boolean hasMultipleResultSets() {
        return hasMultipleResultSets;
    }

    /**
     * Set if the call returns multiple result sets.
     */
    public void setHasMultipleResultSets(boolean hasMultipleResultSets) {
        this.hasMultipleResultSets = hasMultipleResultSets;
    }

    /**
     * INTERNAL:
     */
    public void appendIn(Object inObject) {
        getParameters().add(inObject);
        getParameterTypes().add(IN);
    }

    /**
     * INTERNAL:
     */
    public void appendInOut(DatabaseField inoutField) {
        Object[] inOut = { inoutField, inoutField };
        getParameters().add(inOut);
        getParameterTypes().add(INOUT);
    }

    /**
     * INTERNAL:
     */
    public void appendInOut(Object inValueOrField, DatabaseField outField) {
        Object[] inOut = { inValueOrField, outField };
        getParameters().add(inOut);
        getParameterTypes().add(INOUT);
    }

    /**
     * INTERNAL:
     */
    public void appendOut(DatabaseField outField) {
        getParameters().add(outField);
        getParameterTypes().add(OUT);
    }

    /**
     * INTERNAL:
     */
    public void appendOutCursor(DatabaseField outField) {
        getParameters().add(outField);
        getParameterTypes().add(OUT_CURSOR);
        getOutputCursors().add(outField);
    }

    /**
     * Add the parameter.
     * If using binding bind the parameter otherwise let the platform print it.
     * The platform may also decide to bind the value.
     */
    public void appendParameter(Writer writer, Object parameter, AbstractSession session) {
        if (Boolean.TRUE.equals(usesBinding)) {
            bindParameter(writer, parameter);
        } else {
            session.getPlatform().appendParameter(this, writer, parameter);
        }
    }

    /**
     * Bind the parameter. Binding is determined by the call and second the platform.
     */
    public void bindParameter(Writer writer, Object parameter) {
        if (parameter instanceof Collection) {
            throw QueryException.inCannotBeParameterized(getQuery());
        }

        try {
            writer.write("?");
        } catch (IOException exception) {
            throw ValidationException.fileError(exception);
        }
        getParameters().add(parameter);
    }

    /**
     * Return the appropriate mechanism,
     * with the call added as necessary.
     */
    public DatabaseQueryMechanism buildNewQueryMechanism(DatabaseQuery query) {
        return new CallQueryMechanism(query, this);
    }

    /**
     * INTERNAL:
     * Return Record containing output fields and values.
     * Called only if shouldBuildOutputRow method returns true.
     */
    public AbstractRecord buildOutputRow(CallableStatement statement, DatabaseAccessor accessor, AbstractSession session) throws SQLException {
        AbstractRecord row = new DatabaseRecord();
        int size = this.parameters.size();
        for (int index = 0; index < size; index++) {
            Object parameter = this.parameters.get(index);
            if (parameter instanceof OutputParameterForCallableStatement) {
                OutputParameterForCallableStatement outParameter = (OutputParameterForCallableStatement)parameter;
                if (!outParameter.isCursor() || !isCursorOutputProcedure()) {
                    Object value = getOutputParameterValue(statement, index, session);
                    DatabaseField field = outParameter.getOutputField();
                    if (value instanceof Struct){
                        ClassDescriptor descriptor = session.getDescriptor(field.getType());
                        if ((descriptor != null) && descriptor.isObjectRelationalDataTypeDescriptor()) {
                            AbstractRecord nestedRow = ((ObjectRelationalDataTypeDescriptor)descriptor).buildRowFromStructure((Struct)value);
                            ReadObjectQuery query = new ReadObjectQuery();
                            query.setSession(session);
                            value = descriptor.getObjectBuilder().buildNewInstance();
                            descriptor.getObjectBuilder().buildAttributesIntoObject(value, null, nestedRow, query, null, null, false, this.getQuery().getSession());
                        }
                    } else if ((value instanceof Array) && (field.isObjectRelationalDatabaseField())) {
                        value = ObjectRelationalDataTypeDescriptor.buildContainerFromArray((Array)value, (ObjectRelationalDatabaseField)field, session);
                    } else if (value instanceof ResultSet) {
                        // Support multiple out cursors, put list of records in row.
                        ResultSet resultSet = (ResultSet)value;
                        setFields(null);
                        matchFieldOrder(resultSet, accessor, session);
                        value = accessor.processResultSet(resultSet, this, statement, session);
                    }
                    row.put(field, value);
                }
            }
        }

        return row;
    }

    /**
     * Return the appropriate mechanism,
     * with the call added as necessary.
     */
    public DatabaseQueryMechanism buildQueryMechanism(DatabaseQuery query, DatabaseQueryMechanism mechanism) {
        if (mechanism.isCallQueryMechanism() && (mechanism instanceof CallQueryMechanism)) {
            // Must also add the call singleton...
            CallQueryMechanism callMechanism = ((CallQueryMechanism)mechanism);
            if (!callMechanism.hasMultipleCalls()) {
                callMechanism.addCall(callMechanism.getCall());
                callMechanism.setCall(null);
            }
            callMechanism.addCall(this);
            return mechanism;
        } else {
            return buildNewQueryMechanism(query);
        }
    }

    /**
     * INTERNAL:
     * Returns INOUT parameter. The first parameter is value to pass in, the second DatabaseField for out.
     */
    protected Object createInOutParameter(Object inValue, Object outParameter, AbstractSession session) {
        if (outParameter instanceof OutputParameterForCallableStatement) {
            return new InOutputParameterForCallableStatement(inValue, (OutputParameterForCallableStatement)outParameter);
        }
        if (outParameter instanceof DatabaseField) {
            return new InOutputParameterForCallableStatement(inValue, (DatabaseField)outParameter, session);
        }

        //should never happen
        return null;
    }

    /**
     * INTERNAL:
     * Return the SQL string for the call.
     */
    public String getCallString() {
        return getSQLString();
    }

    /**
     * The fields expected by the calls result set.
     * null means that the fields are unknown and should be built from the result set.
     */
    public Vector getFields() {
        return fields;
    }

    /**
     * INTERNAL:
     * The array of fields returned by the call.
     */
    public DatabaseField[] getFieldsArray() {
        return fieldsArray;
    }

    /**
     * INTERNAL:
     * Unfortunately can't avoid referencing query and descriptor:
     * the call should be performed after the translateCustomSQL (in SQLCall)
     * in the middle of prepare method (no parameter available earlier).
     *
     */
    protected DatabaseField getFieldWithTypeFromDescriptor(DatabaseField outField) {
        if (getQuery().getDescriptor() != null) {
            return getQuery().getDescriptor().getTypedField(outField);
        } else {
            return null;
        }
    }

    /**
     * INTERNAL:
     * Return 1-based index of out cursor parameter, or -1.
     */
    public int getCursorOutIndex() {
        int size = getParameters().size();
        for (int i = 0; i < size; i++) {
            Object parameter = this.parameters.get(i);
            if (parameter instanceof OutputParameterForCallableStatement) {
                if (((OutputParameterForCallableStatement)parameter).isCursor()) {
                    return i + 1;
                }
            }
        }
        return -1;
    }

    /**
     * After an execute call the return value can be retrieved here.
     */
    public boolean getExecuteReturnValue() {
        return executeReturnValue;
    }

    /**
     * get first result
     */
    public int getFirstResult() {
        return this.firstResult;
    }

    /**
     * Return the SQL string for logging purposes.
     */
    public String getLogString(Accessor accessor) {
        if (hasParameters()) {
            StringWriter writer = new StringWriter();
            writer.write(getSQLString());
            writer.write(Helper.cr());
            if (hasParameters()) {
                AbstractSession session = null;
                if (getQuery() != null) {
                    session = getQuery().getSession();
                }
                appendLogParameters(getParameters(), accessor, writer, session);
            }
            return writer.toString();
        } else {
            return getSQLString();
        }
    }

    /**
     * Print the parameters to the write for logging purposes.
     */
    public static void appendLogParameters(Collection parameters, Accessor accessor, StringWriter writer, AbstractSession session) {
        writer.write("\tbind => [");

        if (session == null || session.shouldDisplayData()) {
            for (Iterator paramsEnum = parameters.iterator(); paramsEnum.hasNext();) {
                Object parameter = paramsEnum.next();
                if (parameter instanceof DatabaseField) {
                    writer.write("null");
                } else {
                    if (session != null) {
                        parameter = session.getPlatform().convertToDatabaseType(parameter);
                    }
                    writer.write(String.valueOf(parameter));
                }
                if (paramsEnum.hasNext()) {
                    writer.write(", ");
                } else {
                    writer.write("]");
                }
            }
        } else {
            String parameterString = parameters.size() == 1 ? " parameter" : " parameters";
            writer.write(parameters.size() + parameterString + " bound]");
        }
    }

    /**
     * get max rows returned from the call
     */
    public int getMaxRows() {
        return this.maxRows;
    }

    /**
     * INTERNAL
     * Returns the fields to be used in output row.
     */
    public Vector getOutputRowFields() {
        Vector fields = new Vector();
        int size = getParameters().size();
        for (int i = 0; i < size; i++) {
            Integer parameterType = this.parameterTypes.get(i);
            Object parameter = this.parameters.get(i);
            if (parameterType == OUT) {
                fields.add(parameter);
            } else if (parameterType == INOUT) {
                fields.add(((Object[])parameter)[1]);
            }
        }
        return fields;
    }

    /**
     * INTERNAL:
     * Return the output cursors for this stored procedure call.
     */
    public List<DatabaseField> getOutputCursors() {
        if (outputCursors == null) {
            outputCursors = new ArrayList<DatabaseField>();
        }

        return outputCursors;
    }

    /**
     * INTERNAL:
     * Return the query string (SQL) of the call.
     */
    public String getQueryString() {
        return getSQLString();
    }

    /**
     * Get timeout limit from the call
     */
    public int getQueryTimeout() {
        return this.queryTimeout;
    }

    /**
     * The result set is stored for the return value of cursor selects.
     */
    public ResultSet getResult() {
        return result;
    }

    /**
     * ADVANCED:
     * This method returns a value that represents if the customer has set whether or not EclipseLink should expect
     * the stored procedure to returning a JDBC ResultSet.  The result of the method corresponds
     * to false, true.
     */
    public boolean getReturnsResultSet() {
        if (returnsResultSet == null) {
            return !shouldBuildOutputRow();
        } else {
            return returnsResultSet.booleanValue();
        }
    }

    public int getResultSetConcurrency() {
        return resultSetConcurrency;
    }

    public int getResultSetFetchSize() {
        return resultSetFetchSize;
    }

    public int getResultSetType() {
        return resultSetType;
    }

    /**
     * Return the SQL string that will be executed.
     */
    public String getSQLString() {
        return sqlString;
    }

    /**
     * The statement is stored for the return value of cursor selects.
     */
    public Statement getStatement() {
        return statement;
    }

    /**
     * This check is needed only when doing batch writing.
     */
    public boolean hasOptimisticLock() {
        return hasOptimisticLock;
    }

    /**
     * Return true if there are output cursors on this call.
     */
    public boolean hasOutputCursors() {
        return outputCursors != null && ! outputCursors.isEmpty();
    }

    /**
     * Callable statement is required if there is an output parameter.
     */
    protected boolean isCallableStatementRequired() {
        return isCallableStatementRequired;
    }

    /**
     * Return if the call is dynamic SQL call.
     * This means the call has no parameters, is not using binding,
     * is not a stored procedure (CallableStatement), or cursor.
     * This means that a Statement, not a PreparedStatement will be used for the call.
     */
    protected boolean isDynamicCall(AbstractSession session) {
        return DatabaseAccessor.shouldUseDynamicStatements && (!usesBinding(session)) && (!isResultSetScrollable()) && (!hasParameters());
    }

    /**
     * Used for Oracle result sets through procedures.
     */
    public boolean isCursorOutputProcedure() {
        return isCursorOutputProcedure;
    }

    /**
     * The return type is one of, NoReturn, ReturnOneRow or ReturnManyRows.
     */
    public boolean isCursorReturned() {
        return this.returnType == RETURN_CURSOR;
    }

    /**
     * Return if field matching is required.
     * Field matching is required for custom SQL statements where the result set field order is not known.
     */
    public boolean isFieldMatchingRequired() {
        return isFieldMatchingRequired;
    }

    /**
     * Return whether all the results of the call have been returned.
     */
    public boolean isFinished() {
        return !isCursorReturned() && !isExecuteUpdate();
    }

    /**
     * Used for Oracle result sets through procedures.
     */
    public boolean isMultipleCursorOutputProcedure() {
        return this.isMultipleCursorOutputProcedure;
    }

    /**
     * Return true for procedures with any output (or in/out) parameters and no cursors
     */
    public boolean isNonCursorOutputProcedure() {
        return !isCursorOutputProcedure() && shouldBuildOutputRow();
    }

    public boolean isResultSetScrollable() {
        return isResultSetScrollable;
    }

    /**
     * Allow for the field order to be matched if required.
     * This is required for custom SQL.
     */
    public void matchFieldOrder(ResultSet resultSet, DatabaseAccessor accessor, AbstractSession session) {
        if ((getFields() != null) && (!isFieldMatchingRequired())) {
            return;
        }
        setFields(accessor.buildSortedFields(getFields(), resultSet, session));
    }

    /**
     * INTERNAL:
     * Allow pre-printing of the SQL string for fully bound calls, to save from reprinting.
     * Should be called before translation.
     */
    @Override
    public void prepare(AbstractSession session) {
        if (this.isPrepared) {
            return;
        }
        prepareInternal(session);
        this.isPrepared = true;
    }

    /**
     * INTERNAL:
     * Called by prepare method only. May be overridden.
     */
    protected void prepareInternal(AbstractSession session) {
        prepareInternalParameters(session);
    }

    /**
     * INTERNAL:
     * Called by prepareInternal method only. May be overridden.
     */
    protected void prepareInternalParameters(AbstractSession session) {
        if (isCursorOutputProcedure()) {
            // 1. If there are no OUT_CURSOR parameters - change the first OUT to OUT_CURSOR;
            // 2. If there are multiple OUT_CURSOR parameters - throw Validation exception
            int nFirstOutParameterIndex = -1;
            boolean hasFoundOutCursor = false;
            int size = this.parameters.size();
            for (int index = 0; index < size; index++) {
                Integer parameterType = this.parameterTypes.get(index);
                if (parameterType == DatasourceCall.OUT_CURSOR) {
                    if (hasFoundOutCursor) {
                        // one cursor has been already found
                        throw ValidationException.multipleCursorsNotSupported(toString());
                    } else {
                        hasFoundOutCursor = true;
                    }
                } else if (parameterType == DatasourceCall.OUT) {
                    if (nFirstOutParameterIndex == -1) {
                        nFirstOutParameterIndex = index;
                    }
                } else if (parameterType == null) {
                    // setCustomSQLArgumentType method was called when custom SQL is not used
                    throw ValidationException.wrongUsageOfSetCustomArgumentTypeMethod(toString());
                }
            }
            if (!hasFoundOutCursor && (nFirstOutParameterIndex >= 0)) {
                this.parameterTypes.set(nFirstOutParameterIndex, DatasourceCall.OUT_CURSOR);
            }
        }

        int size = getParameters().size();
        for (int i = 0; i < size; i++) {
            Object parameter = this.parameters.get(i);
            Integer parameterType = this.parameterTypes.get(i);
            if (parameterType == MODIFY) {
                // in case the field's type is not set, the parameter type is set to CUSTOM_MODIFY.
                DatabaseField field = (DatabaseField)parameter;
                if ((field.getType() == null) || session.getPlatform().shouldUseCustomModifyForCall(field)) {
                    this.parameterTypes.set(i, CUSTOM_MODIFY);
                }
            } else if (parameterType == INOUT) {
                // In case there is a type in outField, outParameter is created.
                // During translate call, either outParameter or outField is used for
                // creating inOut parameter.
                setShouldBuildOutputRow(true);
                setIsCallableStatementRequired(true);
                DatabaseField outField = (DatabaseField)((Object[])parameter)[1];
                if (outField.getType() == null) {
                    DatabaseField typeOutField = getFieldWithTypeFromDescriptor(outField);
                    if (typeOutField != null) {
                        outField = typeOutField.clone();
                    }
                }
                if (outField.getType() != null) {
                    // outParameter contains all the info for registerOutputParameter call.
                    OutputParameterForCallableStatement outParameter = new OutputParameterForCallableStatement(outField, session);
                    ((Object[])parameter)[1] = outParameter;
                }
            } else if ((parameterType == OUT) || (parameterType == OUT_CURSOR)) {
                boolean isCursor = parameterType == OUT_CURSOR;
                if (!isCursor) {
                    setShouldBuildOutputRow(true);
                }
                setIsCallableStatementRequired(true);
                DatabaseField outField = (DatabaseField)parameter;
                if (outField.getType() == null) {
                    DatabaseField typeOutField = getFieldWithTypeFromDescriptor(outField);
                    if (typeOutField != null) {
                        outField = typeOutField.clone();
                    }
                }

                // outParameter contains all the info for registerOutputParameter call.
                OutputParameterForCallableStatement outParameter = new OutputParameterForCallableStatement(outField, session, isCursor);
                this.parameters.set(i, outParameter);
                this.parameterTypes.set(i, parameterType);
            }
        }
        if (this.returnsResultSet == null) {
            setReturnsResultSet(!isCallableStatementRequired());
        }
        // if there is nothing returned and we are not using optimistic locking then batch
        //if it is a StoredProcedure with in/out or out parameters then do not batch
        //logic may be weird but we must not batch if we are not using JDBC batchwriting and we have parameters
        // we may want to refactor this some day
        this.isBatchExecutionSupported = (isNothingReturned()
                && (!hasOptimisticLock() || session.getPlatform().canBatchWriteWithOptimisticLocking(this))
                && (!shouldBuildOutputRow())
                && (session.getPlatform().usesJDBCBatchWriting() || (!hasParameters()))
                && (!isLOBLocatorNeeded()))
                && (getQuery().isModifyQuery() && ((ModifyQuery)getQuery()).isBatchExecutionSupported());
    }

    /**
     * INTERNAL:
     * Prepare the JDBC statement, this may be parameterize or a call statement.
     * If caching statements this must check for the pre-prepared statement and re-bind to it.
     */
    public Statement prepareStatement(DatabaseAccessor accessor, AbstractRecord translationRow, AbstractSession session) throws SQLException {
        //#Bug5200836 pass shouldUnwrapConnection flag to indicate whether or not using unwrapped connection.
        Statement statement = accessor.prepareStatement(this, session);

        // Setup the max rows returned and query timeout limit.
        if (this.queryTimeout > 0 && this.queryTimeoutUnit != null) {
            long timeout = TimeUnit.SECONDS.convert(this.queryTimeout, this.queryTimeoutUnit);

            if(timeout > Integer.MAX_VALUE){
                timeout = Integer.MAX_VALUE;
            }

            //Round up the timeout if SECONDS are larger than the given units
            if(TimeUnit.SECONDS.compareTo(this.queryTimeoutUnit) > 0 && this.queryTimeout % 1000 > 0){
                timeout += 1;
            }
            statement.setQueryTimeout((int)timeout);
        }
        if (!this.ignoreMaxResultsSetting && this.maxRows > 0) {
            statement.setMaxRows(this.maxRows);
        }
        if (this.resultSetFetchSize > 0) {
            statement.setFetchSize(this.resultSetFetchSize);
        }

        if (this.parameters == null) {
            return statement;
        }
        List parameters = getParameters();
        int size = parameters.size();
        for (int index = 0; index < size; index++) {
            session.getPlatform().setParameterValueInDatabaseCall(parameters.get(index), (PreparedStatement)statement, index+1, session);
        }

        return statement;
    }

    /**
     * Return true if the multiple results 'lists' should be returned.
     */
    public boolean returnMultipleResultSetCollections() {
        return returnMultipleResultSetCollections;
    }

    /**
     * The fields expected by the calls result set.
     */
    public void setFields(Vector fields) {
        this.fields = fields;
        if (fields != null) {
            int size = fields.size();
            this.fieldsArray = new DatabaseField[size];
            for (int index = 0; index < size; index++) {
                this.fieldsArray[index] = (DatabaseField)fields.get(index);
            }
        } else {
            this.fieldsArray = null;
        }
    }

    /**
     * The firstResult set on the result set
     */
    public void setFirstResult(int firstResult) {
        this.firstResult = firstResult;
    }

    /**
     * This check is needed only when doing batch writing and we hit on optimistic locking.
     */
    public void setHasOptimisticLock(boolean hasOptimisticLock) {
        this.hasOptimisticLock = hasOptimisticLock;
    }

    /**
     * INTERNAL:
     * Sets the ignoreFirstRowSetting flag. If true, FirstResult option are
     * assumed built into the SQL string and ignored if set in the call, and
     * instead are added as query arguments.
     * Default is false.
     */
    public void setIgnoreFirstRowSetting(boolean ignoreFirstRowSetting){
        this.ignoreFirstRowSetting = ignoreFirstRowSetting;
    }

    /**
     * INTERNAL:
     * Sets the ignoreMaxResultsSetting flag. If true, MaxRows option are
     * assumed built into the SQL string and ignored if set in the call, and
     * instead are added as query arguments.
     * Default is false.
     */
    public void setIgnoreMaxResultsSetting(boolean ignoreMaxResultsSetting){
        this.ignoreMaxResultsSetting = ignoreMaxResultsSetting;
    }

    /**
     * Callable statement is required if there is an output parameter.
     */
    protected void setIsCallableStatementRequired(boolean isCallableStatementRequired) {
        this.isCallableStatementRequired = isCallableStatementRequired;
    }

    /**
     * Used for Oracle result sets through procedures.
     */
    public void setIsCursorOutputProcedure(boolean isCursorOutputProcedure) {
        this.isCursorOutputProcedure = isCursorOutputProcedure;
    }

    /**
     * Field matching is required for custom SQL statements where the result set field order is not known.
     */
    public void setIsFieldMatchingRequired(boolean isFieldMatchingRequired) {
        this.isFieldMatchingRequired = isFieldMatchingRequired;
    }

    /**
     * Used for Oracle result sets through procedures.
     */
    public void setIsMultipleCursorOutputProcedure(boolean isMultipleCursorOutputProcedure) {
        this.isMultipleCursorOutputProcedure = isMultipleCursorOutputProcedure;
    }

    public void setIsResultSetScrollable(boolean isResultSetScrollable) {
        this.isResultSetScrollable = isResultSetScrollable;
    }

    /**
     * set query max returned row size to the JDBC Statement
     */
    public void setMaxRows(int maxRows) {
        this.maxRows = maxRows;
    }

    /**
     * INTERNAL:
     * Set the query string (SQL) of the call.
     */
    public void setQueryString(String queryString) {
        setSQLStringInternal(queryString);
    }

    /**
     * set query timeout limit to the JDBC Statement
     */
    public void setQueryTimeout(int queryTimeout) {
        this.queryTimeout = queryTimeout;
    }

    /**
     * set query timeout limit unit to the JDBC Statement
     */
    public void setQueryTimeoutUnit(TimeUnit queryTimeoutUnit) {
        this.queryTimeoutUnit = queryTimeoutUnit;
    }

    /**
     * The result set is stored for the return value of cursor selects.
     */
    public void setResult(ResultSet result) {
        this.result = result;
    }

    public void setResultSetConcurrency(int resultSetConcurrency) {
        this.resultSetConcurrency = resultSetConcurrency;
    }

    /**
     * INTERNAL:
     * Set the SQL string.
     */
    protected void setSQLStringInternal(String sqlString) {
        this.sqlString = sqlString;
    }

    public void setResultSetFetchSize(int resultSetFetchSize) {
        this.resultSetFetchSize = resultSetFetchSize;
    }

    public void setResultSetType(int resultSetType) {
        this.resultSetType = resultSetType;
    }

    /**
     * PUBLIC:
     * Use this method to tell EclipseLink that the stored procedure will be returning a JDBC ResultSet
     */
    public void setReturnsResultSet(boolean returnsResultSet) {
        this.returnsResultSet = Boolean.valueOf(returnsResultSet);
    }

    /**
     * Set if the call returns multiple result sets.
     */
    public void setReturnMultipleResultSetCollections(boolean returnMultipleResultSetCollections) {
        this.returnMultipleResultSetCollections = returnMultipleResultSetCollections;
    }

    /**
     * INTERNAL:
     * Set whether the call has to build output row
     */
    protected void setShouldBuildOutputRow(boolean shouldBuildOutputRow) {
        this.shouldBuildOutputRow = shouldBuildOutputRow;
    }

    /**
     * Bound calls can use prepared statement caching.
     */
    public void setShouldCacheStatement(boolean shouldCacheStatement) {
        this.shouldCacheStatement = Boolean.valueOf(shouldCacheStatement);
    }

    /**
     * The statement is stored for the return value of cursor selects.
     */
    public void setStatement(Statement statement) {
        this.statement = statement;
    }

    /**
     * The call may specify that its parameters should be bound.
     */
    public void setUsesBinding(boolean usesBinding) {
        this.usesBinding = Boolean.valueOf(usesBinding);
    }

    /**
     * Set whether the call has to build output row
     */
    public boolean shouldBuildOutputRow() {
        return this.shouldBuildOutputRow;
    }

    /**
     * Bound calls can use prepared statement caching.
     */
    public boolean shouldCacheStatement(AbstractSession session) {
        return shouldCacheStatement(session.getPlatform());
    }

    /**
     * Bound calls can use prepared statement caching.
     */
    public boolean shouldCacheStatement(DatabasePlatform databasePlatform) {
        //CR4272  If result set is scrollable, do not cache statement since scrollable cursor can not be used for cached statement
        if (isResultSetScrollable()) {
            return false;
        }
        if (this.shouldCacheStatement == null) {
            return databasePlatform.shouldCacheAllStatements();
        } else {
            return this.shouldCacheStatement.booleanValue();
        }
    }

    /**
     * INTERNAL:
     * Returns the ignoreFirstRowSetting flag. If true, FirstResult option is
     * assumed built into the SQL string and ignored if set in the call.
     */
    public boolean shouldIgnoreFirstRowSetting(){
        return this.ignoreFirstRowSetting;
    }

    /**
     * INTERNAL:
     * Returns the ignoreMaxResultsSetting flag. If true, MaxRows option is
     * assumed built into the SQL string and ignored if set in the call.
     */
    public boolean shouldIgnoreMaxResultsSetting(){
        return this.ignoreMaxResultsSetting;
    }

    /**
     * INTERNAL:
     * Print the SQL string.
     */
    public String toString() {
        String str = Helper.getShortClassName(getClass());
        if (getSQLString() == null) {
            return str;
        } else {
            return str + "(" + getSQLString() + ")";
        }
    }

    /**
     * INTERNAL:
     * Allow the call to translate from the translation for predefined calls.
     */
    @Override
    public void translate(AbstractRecord translationRow, AbstractRecord modifyRow, AbstractSession session) {
        if (!isPrepared()) {
            throw ValidationException.cannotTranslateUnpreparedCall(toString());
        }
        if (usesBinding(session) && (this.parameters != null)) {
            boolean hasParameterizedIN = false;
            List parameters = getParameters();
            List<Integer> parameterTypes = getParameterTypes();
            int size = parameters.size();
            List parametersValues = new ArrayList(size);
            for (int index = 0; index < size; index++) {
                Object parameter = parameters.get(index);
                Object parameterType = parameterTypes.get(index);
                if (parameterType == MODIFY) {
                    DatabaseField field = (DatabaseField)parameter;
                    Object value = modifyRow.get(field);
                    // If the value is null, the field is passed as the value so the type can be obtained from the field.
                    if (value == null) {
                        // The field from the modify row is used, as the calls field may not have the type,
                        // but if the field is missing the calls field may also have the type.
                        value = modifyRow.getField(field);
                        if (value == null) {
                            value = field;
                        }
                    }
                    parametersValues.add(value);
                } else if (parameterType == CUSTOM_MODIFY) {
                    DatabaseField field = (DatabaseField)parameter;
                    Object value = modifyRow.get(field);
                    value = session.getPlatform().getCustomModifyValueForCall(this, value, field, true);
                    //Bug#8200836 needs use unwrapped connection
                    if ((value!=null) && (value instanceof BindCallCustomParameter) &&  (((BindCallCustomParameter)value).shouldUseUnwrappedConnection())){
                        this.isNativeConnectionRequired=true;
                    }

                    // If the value is null, the field is passed as the value so the type can be obtained from the field.
                    if (value == null) {
                        // The field from the modify row is used, as the calls field may not have the type,
                        // but if the field is missing the calls field may also have the type.
                        value = modifyRow.getField(field);
                        if (value == null) {
                            value = field;
                        }
                    }
                    parametersValues.add(value);
                } else if (parameterType == TRANSLATION) {
                    Object value = null;
                    DatabaseField field = null;
                    if (parameter instanceof ParameterExpression) {
                        field = ((ParameterExpression)parameter).getField();
                        value = ((ParameterExpression)parameter).getValue(translationRow, query, session);
                    } else {
                        field = (DatabaseField)parameter;
                        value = translationRow.get(field);
                        if (value == null) {// Backward compatibility double check.
                            value = modifyRow.get(field);
                        }
                    }
                    if (value instanceof Collection) {
                        // Must re-translate IN parameters.
                        hasParameterizedIN = true;
                    }
                    // If the value is null, the field is passed as the value so the type can be obtained from the field.
                    if ((value == null) && (field != null)) {
                        if (!this.query.hasNullableArguments() || !this.query.getNullableArguments().contains(field)) {
                            value = translationRow.getField(field);
                            // The field from the row is used, as the calls field may not have the type,
                            // but if the field is missing the calls field may also have the type.
                            if (value == null) {
                                value = field;
                            }
                            parametersValues.add(value);
                        }
                    } else {
                        parametersValues.add(value);
                    }
                } else if (parameterType == LITERAL) {
                    parametersValues.add(parameter);
                } else if (parameterType == IN) {
                    Object value = getValueForInParameter(parameter, translationRow, modifyRow, session, true);
                    // Returning this means the parameter was optional and should not be included.
                    if (value != this) {
                        parametersValues.add(value);
                    }
                } else if (parameterType == INOUT) {
                    Object value = getValueForInOutParameter(parameter, translationRow, modifyRow, session);
                    parametersValues.add(value);
                } else if (parameterType == OUT || parameterType == OUT_CURSOR) {
                    if (parameter != null) {
                        ((OutputParameterForCallableStatement) parameter).getOutputField().setIndex(index);
                    }
                    parametersValues.add(parameter);
                }
            }
            setParameters(parametersValues);
            // If an IN parameter was found must translate SQL.
            if (hasParameterizedIN) {
                translateQueryStringForParameterizedIN(translationRow, modifyRow, session);
            }
            return;
        }

        translateQueryString(translationRow, modifyRow, session);
    }

    /**
     * INTERNAL:
     * Translate only IN() parameter values (List parameters).
     */
    public void translateQueryStringForParameterizedIN(AbstractRecord translationRow, AbstractRecord modifyRow, AbstractSession session) {
        int lastIndex = 0;
        int parameterIndex = 0;
        String queryString = getQueryString();
        Writer writer = new CharArrayWriter(queryString.length() + 50);
        try {
            // PERF: This method is heavily optimized do not touch anything unless you know "very well" what your doing.
            List parameters = getParameters();
            List parametersValues = new ArrayList(parameters.size());
            while (lastIndex != -1) {
                int tokenIndex = queryString.indexOf(argumentMarker(), lastIndex);
                String token;
                if (tokenIndex == -1) {
                    token = queryString.substring(lastIndex, queryString.length());
                    lastIndex = -1;
                } else {
                    token = queryString.substring(lastIndex, tokenIndex);
                }
                writer.write(token);
                if (tokenIndex != -1) {
                    // Process next parameter.
                    Object parameter = parameters.get(parameterIndex);
                    // Parameter expressions are used for nesting and correct mapping conversion of the value.
                    if (parameter instanceof Collection) {
                        Collection values = (Collection)parameter;
                        writer.write("(");
                        if ((values.size() > 0) && (values.iterator().next() instanceof List)) {
                            // Support nested lists.
                            int size = values.size();
                            Iterator valuesIterator = values.iterator();
                            for (int index = 0; index < size; index++) {
                                List nestedValues = (List)valuesIterator.next();
                                parametersValues.addAll(nestedValues);
                                int nestedSize = nestedValues.size();
                                writer.write("(");
                                for (int nestedIndex = 0; nestedIndex < nestedSize; nestedIndex++) {
                                    writer.write("?");
                                    if ((nestedIndex + 1) < nestedSize) {
                                        writer.write(",");
                                    }
                                }
                                writer.write(")");
                                if ((index + 1) < size) {
                                    writer.write(",");
                                }
                            }
                        } else {
                            parametersValues.addAll(values);
                            int size = values.size();

                            int limit = ((DatasourcePlatform)session.getDatasourcePlatform()).getINClauseLimit();
                            //The database platform has a limit for the IN clause so we need to reformat the clause
                            if(limit > 0) {
                                boolean not = token.endsWith(" NOT IN ");
                                String subToken = token.substring(0, token.length() - (not ? " NOT IN " : " IN ").length());
                                int spaceIndex = subToken.lastIndexOf(' ');
                                int braceIndex = subToken.lastIndexOf('(');
                                String fieldName = subToken.substring((spaceIndex > braceIndex ? spaceIndex : braceIndex) + 1);
                                String inToken = not ? ") AND " + fieldName + " NOT IN (" : ") OR " + fieldName + " IN (";

                                for (int index = 0; index < size; index++) {
                                    writer.write("?");
                                    if ((index + 1) < size) {
                                        if (index > 0 && (index + 1) % limit == 0) {
                                            writer.write(inToken);
                                        } else  {
                                            writer.write(",");
                                        }
                                    }
                                }
                            } else {
                                for (int index = 0; index < size; index++) {
                                    writer.write("?");
                                    if ((index + 1) < size) {
                                        writer.write(",");
                                    }
                                }
                            }
                        }
                        writer.write(")");
                    } else {
                        parametersValues.add(parameter);
                        writer.write("?");
                    }
                    lastIndex = tokenIndex + 1;
                    parameterIndex++;
                }
            }
            setParameters(parametersValues);
            setQueryString(writer.toString());

        } catch (IOException exception) {
            throw ValidationException.fileError(exception);
        }
    }

    /**
     * The call may specify that its parameters should be bound.
     */
    public boolean usesBinding(AbstractSession session) {
        return usesBinding(session.getPlatform());
    }

    /**
     * The call may specify that its parameters should be bound.
     */
    public boolean usesBinding(DatabasePlatform databasePlatform) {
        if (this.usesBinding == null) {
            return databasePlatform.shouldBindAllParameters();
        } else {
            return this.usesBinding.booleanValue();
        }
    }

    /**
     * INTERNAL
     * Indicates whether usesBinding has been set.
     */
    public boolean isUsesBindingSet() {
        return this.usesBinding != null;
    }

    /**
     * INTERNAL:
     * Return if the locator is required for the LOB (BLOB and CLOB) writing.
     */
    public boolean isLOBLocatorNeeded() {
        return contexts != null;
    }

    /**
     * INTERNAL:
     * Add a field - value pair for LOB field into the context.
     */
    public void addContext(DatabaseField field, Object value) {
        if (this.contexts == null) {
            this.contexts = new DatabaseRecord(2);
        }
        this.contexts.add(field, value);
        this.isBatchExecutionSupported = false;
    }

    /**
     * INTERNAL:
     * Return the contexts (for LOB)
     */
    public AbstractRecord getContexts() {
        return contexts;
    }

    /**
     * INTERNAL:
     * Set the contexts (for LOB)
     */
    public void setContexts(AbstractRecord contexts) {
        this.contexts = contexts;
    }

    /**
     * An execute return value will be set here after the call.
     */
    public void setExecuteReturnValue(boolean value) {
        executeReturnValue = value;
    }

    /**
     * PUBLIC:
     * Used for Oracle result sets through procedures.
     * The first OUT parameter is set as a cursor output.
     */
    public void useUnnamedCursorOutputAsResultSet() {
        setIsCursorOutputProcedure(true);
    }

    /**
     * INTERNAL:
     * Return if this query is compatible with batch writing.
     * Some queries, such as DDL are not compatible.
     */
    public boolean isBatchExecutionSupported() {
        return isBatchExecutionSupported;
    }

    /**
     * INTERNAL:
     * Set if this query is compatible with batch writing.
     * Some queries, such as DDL are not compatible.
     */
    public void setBatchExecutionSupported(boolean isBatchExecutionSupported) {
        this.isBatchExecutionSupported = isBatchExecutionSupported;
    }

    /**
     * INTERNAL:
     */
    public boolean hasAllocatedConnection() {
        return this.hasAllocatedConnection;
    }

    /**
     * INTERNAL:
     */
    public void setHasAllocatedConnection(boolean hasAllocatedConnection) {
        this.hasAllocatedConnection = hasAllocatedConnection;
    }

    /**
     * 
     * INTERNAL:
     * 
     * Get the return object from the statement. Use the parameter index to determine what return object to get.
     * @param statement SQL/JDBC statement to call stored procedure/function
     * @param index 0-based index in the argument list
     * @param session Active database session (in connected state).
     * @return
     */
    public Object getOutputParameterValue(CallableStatement statement, int index, AbstractSession session) throws SQLException {
        return session.getPlatform().getParameterValueFromDatabaseCall(statement, index + 1, session);
    }

    /**
     * 
     * INTERNAL:
     * 
     * Get the return object from the statement. Use the parameter name to determine what return object to get.
     * @param statement SQL/JDBC statement to call stored procedure/function
     * @param name parameter name
     * @param session Active database session (in connected state).
     * @return
     */
    public Object getOutputParameterValue(CallableStatement statement, String name, AbstractSession session) throws SQLException {
        return session.getPlatform().getParameterValueFromDatabaseCall(statement, name, session);
    }
}
