/*******************************************************************************
 * Copyright (c) 2012, 2013 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 *     Zoltan NAGY & tware - updated support for MaxRows
 *     11/01/2010-2.2 Guy Pelletier 
 *       - 322916: getParameter on Query throws NPE
 *     11/09/2010-2.1 Michael O'Brien 
 *       - 329089: PERF: EJBQueryImpl.setParamenterInternal() move indexOf check inside non-native block
 *     02/08/2012-2.4 Guy Pelletier 
 *       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
 *     06/20/2012-2.5 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/13/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
 *     11/05/2012-2.5 Guy Pelletier 
 *       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
 ******************************************************************************/
package org.eclipse.persistence.internal.jpa;

import java.sql.Statement;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.LockTimeoutException;
import javax.persistence.Parameter;
import javax.persistence.ParameterMode;
import javax.persistence.PersistenceException;
import javax.persistence.QueryTimeoutException;
import javax.persistence.StoredProcedureQuery;
import javax.persistence.TemporalType;

import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.internal.databaseaccess.Accessor;
import org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor;
import org.eclipse.persistence.internal.databaseaccess.DatabaseCall;
import org.eclipse.persistence.internal.databaseaccess.OutputParameterForCallableStatement;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.jpa.querydef.ParameterExpressionImpl;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.queries.DataReadQuery;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ResultSetMappingQuery;
import org.eclipse.persistence.queries.SQLResultSetMapping;
import org.eclipse.persistence.queries.StoredProcedureCall;

/**
 * Concrete JPA query class. The JPA query wraps a StoredProcesureQuery which 
 * is executed.
 */
public class StoredProcedureQueryImpl extends QueryImpl implements StoredProcedureQuery {
    protected boolean hasMoreResults;
    
    // Call will be returned from an execute. From it you can get the result set.
    protected DatabaseCall executeCall;
    protected Statement executeStatement;
    protected int executeResultSetIndex = -1;
    
    // If the procedure returns output cursor(s), we'll use them to satisfy
    // getResultList and getSingleResult calls so keep track of our index.
    protected int outputCursorIndex = -1;
    protected boolean isOutputCursorResultSet = false;
    
    /**
     * Base constructor for EJBQueryImpl. Initializes basic variables.
     */
    protected StoredProcedureQueryImpl(EntityManagerImpl entityManager) {
        super(entityManager);
    }

    /**
     * Create an EJBQueryImpl with a DatabaseQuery.
     */
    public StoredProcedureQueryImpl(DatabaseQuery query, EntityManagerImpl entityManager) {
        super(query, entityManager);
    }
    
    /**
     * Create an EJBQueryImpl with either a query name or an jpql string.
     * 
     * @param isNamedQuery
     *            determines whether to treat the queryDescription as jpql or a
     *            query name.
     */
    public StoredProcedureQueryImpl(String name, EntityManagerImpl entityManager) {
        super(entityManager);
        this.queryName = name;
    }
    
    /**
     * Build the given result set into a list objects. Assumes there is an 
     * execute call available and therefore should not be called unless an
     * execute statement was issued by the user.
     */
    protected List buildResultRecords(ResultSet resultSet) {
        try {
            AbstractSession session = (AbstractSession) getActiveSession();
            DatabaseAccessor accessor = (DatabaseAccessor) executeCall.getQuery().getAccessor();
            
            executeCall.setFields(null);
            executeCall.matchFieldOrder(resultSet, accessor, session);
            ResultSetMetaData metaData = resultSet.getMetaData();
            
            List result =  new Vector();
            while (resultSet.next()) {
                result.add(accessor.fetchRow(executeCall.getFields(), executeCall.getFieldsArray(), resultSet, metaData, session));
            }
    
            // The result set must be closed in case the statement is cached and not closed.
            resultSet.close();
            
            return result;
        } catch (Exception e) {
            setRollbackOnly();
            throw new PersistenceException(e);
        }
    }
    
    /**
     * Build a ResultSetMappingQuery from a sql result set mapping name and a
     * stored procedure call.
     * 
     * This is called from a named stored procedure that employs result set
     * mapping name(s) which should be available from the session.
     */
    public static DatabaseQuery buildResultSetMappingNameQuery(List<String> resultSetMappingNames, StoredProcedureCall call) {
        ResultSetMappingQuery query = new ResultSetMappingQuery();
        call.setReturnMultipleResultSetCollections(call.hasMultipleResultSets() && ! call.isMultipleCursorOutputProcedure());
        query.setCall(call);
        query.setIsUserDefined(true);
        query.setSQLResultSetMappingNames(resultSetMappingNames);
        return query;
    }
    
    /**
     * Build a ResultSetMappingQuery from a sql result set mapping name and a
     * stored procedure call.
     * 
     * This is called from a named stored procedure that employs result set
     * mapping name(s) which should be available from the session.
     */
    public static DatabaseQuery buildResultSetMappingNameQuery(List<String> resultSetMappingNames, StoredProcedureCall call, Map<String, Object> hints, ClassLoader classLoader, AbstractSession session) {
        // apply any query hints
        DatabaseQuery hintQuery = applyHints(hints, buildResultSetMappingNameQuery(resultSetMappingNames, call) , classLoader, session);

        // apply any query arguments
        applyArguments(call, hintQuery);

        return hintQuery;
    }
    
    /**
     * Build a ResultSetMappingQuery from the sql result set mappings given
     *  a stored procedure call.
     * 
     * This is called from a named stored procedure query that employs result
     * class name(s). The resultSetMappings are build from these class name(s)
     * and are not available from the session.
     */
    public static DatabaseQuery buildResultSetMappingQuery(List<SQLResultSetMapping> resultSetMappings, StoredProcedureCall call) {
        ResultSetMappingQuery query = new ResultSetMappingQuery();
        call.setReturnMultipleResultSetCollections(call.hasMultipleResultSets() && ! call.isMultipleCursorOutputProcedure());
        query.setCall(call);
        query.setIsUserDefined(true);
        query.setSQLResultSetMappings(resultSetMappings);
        return query;
    }
    
    /**
     * Build a ResultSetMappingQuery from the sql result set mappings given
     *  a stored procedure call.
     * 
     * This is called from a named stored procedure query that employs result
     * class name(s). The resultSetMappings are build from these class name(s)
     * and are not available from the session.
     */
    public static DatabaseQuery buildResultSetMappingQuery(List<SQLResultSetMapping> resultSetMappings, StoredProcedureCall call, Map<String, Object> hints, ClassLoader classLoader, AbstractSession session) {
        // apply any query hints
        DatabaseQuery hintQuery = applyHints(hints, buildResultSetMappingQuery(resultSetMappings, call), classLoader, session);

        // apply any query arguments
        applyArguments(call, hintQuery);

        return hintQuery;
    }
    
    /**
     * Build a ReadAllQuery from a class and stored procedure call.
     */
    public static DatabaseQuery buildStoredProcedureQuery(Class resultClass, StoredProcedureCall call, Map<String, Object> hints, ClassLoader classLoader, AbstractSession session) {
        DatabaseQuery query = new ReadAllQuery(resultClass);
        query.setCall(call);
        query.setIsUserDefined(true);

        // apply any query hints
        query = applyHints(hints, query, classLoader, session);

        // apply any query arguments
        applyArguments(call, query);

        return query;
    }
    
    /**
     * Build a DataReadQuery with the stored procedure call given.
     */
    public static DatabaseQuery buildStoredProcedureQuery(StoredProcedureCall call, Map<String, Object> hints, ClassLoader classLoader, AbstractSession session) {
        DataReadQuery query = new DataReadQuery();
        query.setResultType(DataReadQuery.AUTO);

        query.setCall(call);
        query.setIsUserDefined(true);

        // apply any query hints
        DatabaseQuery hintQuery = applyHints(hints, query, classLoader, session);

        // apply any query arguments
        applyArguments(call, hintQuery);

        return hintQuery;
    }

    /**
     * Build a ResultSetMappingQuery from a sql result set mapping name and a
     * stored procedure call.
     */
    public static DatabaseQuery buildStoredProcedureQuery(String sqlResultSetMappingName, StoredProcedureCall call, Map<String, Object> hints, ClassLoader classLoader, AbstractSession session) {
        ResultSetMappingQuery query = new ResultSetMappingQuery();
        query.setSQLResultSetMappingName(sqlResultSetMappingName);
        query.setCall(call);
        query.setIsUserDefined(true);

        // apply any query hints
        DatabaseQuery hintQuery = applyHints(hints, query, classLoader, session);

        // apply any query arguments
        applyArguments(call, hintQuery);

        return hintQuery;
    }
    
    /** 
     * Call this method to close any open connections to the database.
     */
    @Override
    public void close() {
        if (executeCall != null) {
            DatabaseQuery query = executeCall.getQuery();
            AbstractSession session = query.getSession();
           
            // Release the accessors acquired for the query.
            for (Accessor accessor : query.getAccessors()) {
                session.releaseReadConnection(accessor);
            }
        
            try {
                if (executeStatement != null) {
                    DatabaseAccessor accessor = (DatabaseAccessor) query.getAccessor();
                    accessor.releaseStatement(executeStatement, query.getSQLString(), executeCall, session);
                }
            } catch (SQLException exception) {
                // Catch the exception and log a message.
                session.log(SessionLog.WARNING, SessionLog.CONNECTION, "exception_caught_closing_statement", exception);
            }
        }
        
        executeCall = null;
        executeStatement = null;
    }
    
    /**
     * Returns true if the first result corresponds to a result set, and false 
     * if it is an update count or if there are no results other than through 
     * INOUT and OUT parameters, if any.
     * @return true if first result corresponds to result set
     * @throws QueryTimeoutException if the query execution exceeds the query 
     * timeout value set and only the statement is rolled back
     * @throws PersistenceException if the query execution exceeds the query 
     * timeout value set and the transaction is rolled back
     */
    public boolean execute() {
        try {
            entityManager.verifyOpen();
            
            if (! getDatabaseQueryInternal().isResultSetMappingQuery()) {
                throw new IllegalStateException(ExceptionLocalization.buildMessage("incorrect_spq_query_for_execute"));
            }
        
            getResultSetMappingQuery().setIsExecuteCall(true);
            executeCall = (DatabaseCall) executeReadQuery();
            executeStatement = executeCall.getStatement();
            
            // Add this query to the entity manager open queries list.
            // The query will be closed in the following cases:
            // Within a transaction:
            //  - on commit
            //  - on rollback
            // Outside of a transaction:
            //  - em close
            // Other safeguards, we will close the query if/when 
            //  - we hit the end of the results.
            //  - this query is garbage collected (finalize method)
            //
            // Deferring closing the call avoids having to go through all the 
            // results now (and building all the result objects) and things 
            // remain on a as needed basis from the statement.
            entityManager.addOpenQuery(this);
            
            hasMoreResults = executeCall.getExecuteReturnValue();
            
            // If execute returned false but we have output cursors then return
            // true and build the results from the output cursors. 
            if (!hasMoreResults && getCall().hasOutputCursors()) {
                hasMoreResults = true;
                outputCursorIndex = 0;
                isOutputCursorResultSet = true;
            }
            
            return hasMoreResults;
        } catch (LockTimeoutException exception) {
            throw exception;
        } catch (PersistenceException exception) {
            setRollbackOnly();
            throw exception;
        } catch (IllegalStateException e){
            setRollbackOnly();
            throw e;
        } catch (RuntimeException exception) {
            setRollbackOnly();
            throw new PersistenceException(exception);
        } 
    }
    
    /**
     * Execute an update or delete statement (from a stored procedure query).
     * @return the number of entities updated or deleted
     */
    public int executeUpdate() {
        try {
            // Need to throw TransactionRequiredException if there is no active transaction
            entityManager.checkForTransaction(true);
            
            // Legacy: we could have a data read query or a read all query, so 
            // clearly we shouldn't be executing an update on it. As of JPA 2.1 
            // API we always create a result set mapping query to interact with 
            // a stored procedure.
            // Also if the result set mapping query has result set mappings
            // defined, then it's clearly expecting result sets and we can be
            // preemptive in throwing an exception.
            if (! getDatabaseQueryInternal().isResultSetMappingQuery() || getResultSetMappingQuery().hasResultSetMappings()) {
                throw new IllegalStateException(ExceptionLocalization.buildMessage("incorrect_spq_query_for_execute_update"));
            }
            
            // If the return value is true indicating a result set then throw an exception.
            if (execute()) {
                if (getActiveSession().getPlatform().isJDBCExecuteCompliant()) {
                    throw new IllegalStateException(ExceptionLocalization.buildMessage("incorrect_spq_query_for_execute_update"));
                } else {
                    return getUpdateCount();
                }
            } else {
                return getUpdateCount();
            }
        } catch (LockTimeoutException exception) {
            throw exception;
        } catch (PersistenceException e) {
            setRollbackOnly();
            throw e;
        } catch (IllegalStateException e){
            setRollbackOnly();
            throw e;
        } catch (RuntimeException exception) {
            setRollbackOnly();
            throw new PersistenceException(exception);
        } finally {
            close(); // Close the connection once we're done.
        }
    }
    
    /**
     * Finalize method in case the query is not closed.
     */
    @Override
    public void finalize() {
        close();
    }
    
    /**
     * Return the stored procedure call associated with this query.
     */
    protected StoredProcedureCall getCall() {
        return (StoredProcedureCall) getDatabaseQueryInternal().getCall();
    }
    
    /**
     * Return the internal map of parameters.
     */
    @Override
    protected Map<String, Parameter<?>> getInternalParameters() {
        if (parameters == null) {
            parameters = new HashMap<String, Parameter<?>>();
    
            int index = 0;
                
            for (Object parameter : getCall().getParameters()) {
                Integer parameterType = getCall().getParameterTypes().get(index);
                String argumentName = getCall().getProcedureArgumentNames().get(index);
                    
                DatabaseField field = null;
                    
                if (parameterType == getCall().INOUT) {
                    field = (DatabaseField) ((Object[]) parameter)[0];
                } else if (parameterType == getCall().IN || parameterType == getCall().OUT || parameterType == getCall().OUT_CURSOR) {
                    field = (DatabaseField) parameter;
                } else if (parameterType == getCall().LITERAL) {
                    if (parameter instanceof OutputParameterForCallableStatement) {
                        // Case: Oracle OUT_CURSOR after execution.
                        field = ((OutputParameterForCallableStatement) parameter).getOutputField();
                    }
                }
                    
                // If field is not null (one we care about) then add it, otherwise continue.
                if (field != null) {
                    // If the argument name is null then it is a positional parameter.
                    if (argumentName == null) {
                        parameters.put(field.getName(), new ParameterExpressionImpl(null, field.getType(), Integer.parseInt(field.getName())));
                    } else {
                        parameters.put(field.getName(), new ParameterExpressionImpl(null, field.getType(), field.getName()));
                    }
                }
                        
                ++index;
            }
        }
        
        return parameters;
    }
    
    /**
     * Used to retrieve the values passed back from the procedure through INOUT 
     * and OUT parameters. For portability, all results corresponding to result 
     * sets and update counts must be retrieved before the values of output 
     * parameters. 
     * @param position parameter position
     * @return the result that is passed back through the parameter
     * @throws IllegalArgumentException if the position does not correspond to a 
     * parameter of the query or is not an INOUT or OUT parameter
     */
    public Object getOutputParameterValue(int position) {
        entityManager.verifyOpen();
        
        if (isValidCallableStatement()) {
            try {
                Object obj = ((CallableStatement) executeStatement).getObject(position);

                if (obj instanceof ResultSet) {
                    // If a result set is returned we have to build the objects.
                    return getResultSetMappingQuery().buildObjectsFromRecords(buildResultRecords((ResultSet) obj), ++executeResultSetIndex);
                } else {
                    return obj;
                }
            } catch (Exception exception) {
                throw new IllegalArgumentException(ExceptionLocalization.buildMessage("jpa21_invalid_parameter_position", new Object[] { position, exception.getMessage() }), exception);
            }
        }
        
        return null;
    }
    
    /**
     * Used to retrieve the values passed back from the procedure through INOUT 
     * and OUT parameters. For portability, all results corresponding to result 
     * sets and update counts must be retrieved before the values of output 
     * parameters.
     * @param parameterName name of the parameter as registered or specified in 
     *        metadata
     * @return the result that is passed back through the parameter
     * @throws IllegalArgumentException if the parameter name does not 
     * correspond to a parameter of the query or is not an INOUT or OUT parameter
     */
    public Object getOutputParameterValue(String parameterName) {
        entityManager.verifyOpen();
        
        if (isValidCallableStatement()) {
            try {
                Integer position = getCall().getCursorOrdinalPosition(parameterName);
                
                if (position == null) {
                    return ((CallableStatement) executeStatement).getObject(parameterName);
                } else {
                    return getOutputParameterValue(position);
                }
            } catch (Exception exception) {
                throw new IllegalArgumentException(ExceptionLocalization.buildMessage("jpa21_invalid_parameter_name", new Object[] { parameterName, exception.getMessage() }), exception);
            }
        }

        return null;
    }
    
    /**
     * Execute the query and return the query results as a List.
     * @return a list of the results
     */
    @Override
    public List getResultList() {
        // bug51411440: need to throw IllegalStateException if query
        // executed on closed em
        this.entityManager.verifyOpenWithSetRollbackOnly();
        try {
            // If there is no execute statement, the user has not called
            // execute and is simply calling getResultList directly on the query.
            if (executeStatement == null) {
                // If it's not a result set mapping query (as of JPA 2.1 we 
                // always create a result set mapping query to interact with a 
                // stored procedure) then throw an exception.
                if (! getDatabaseQueryInternal().isResultSetMappingQuery()) {
                    throw new IllegalStateException(ExceptionLocalization.buildMessage("incorrect_spq_query_for_get_result_list"));
                }
                        
                // If the return value is false indicating no result set then throw an exception.
                if (execute()) {
                    return getResultList();
                } else {
                    throw new IllegalStateException(ExceptionLocalization.buildMessage("incorrect_spq_query_for_get_result_list")); 
                }
            } else {
                if (hasMoreResults()) {
                    if (isOutputCursorResultSet) {
                        // Return result set list for the current outputCursorIndex.
                        List results = (List) getOutputParameterValue(getCall().getOutputCursors().get(outputCursorIndex++).getName());
                        
                        // Update the hasMoreResults flag.
                        hasMoreResults = (outputCursorIndex < getCall().getOutputCursors().size());
                        
                        return results;
                    } else {
                        // Build the result records first.
                        List result = buildResultRecords(executeStatement.getResultSet());
                    
                        // Move the result pointer.
                        moveResultPointer();
                    
                        return getResultSetMappingQuery().buildObjectsFromRecords(result, ++executeResultSetIndex);
                    }
                } else {
                    return null;
                }
            }
        } catch (LockTimeoutException e) {
            throw e;
        } catch (PersistenceException e) {
            setRollbackOnly();
            throw e;
        } catch (IllegalStateException e) {
            setRollbackOnly();
            throw e;
        } catch (Exception e) {
            setRollbackOnly();
            throw new PersistenceException(e);
        }
    }
    
    /**
     * Return the ResultSetMappingQuery for this stored procedure query. 
     * NOTE: Methods assumes associated database query is a ResultSetMappingQuery.
     */
    protected ResultSetMappingQuery getResultSetMappingQuery() {
        if (executeCall != null) {
            return (ResultSetMappingQuery) executeCall.getQuery();
        } else {
            return (ResultSetMappingQuery) getDatabaseQuery();
        }
    }
    
    /**
     * Execute the query and return the single query result.
     * @return a single result object.
     */
    @Override
    public Object getSingleResult() {
        // bug51411440: need to throw IllegalStateException if query
        // executed on closed em
        this.entityManager.verifyOpenWithSetRollbackOnly();
        try {
            // If there is no execute statement, the user has not called
            // execute and is simply calling getSingleResult directly on the query.
            if (executeStatement == null) {
                // If it's not a result set mapping query (as of JPA 2.1 we 
                // always create a result set mapping query to interact with a 
                // stored procedure) then throw an exception.
                if (! getDatabaseQueryInternal().isResultSetMappingQuery()) {
                    throw new IllegalStateException(ExceptionLocalization.buildMessage("incorrect_spq_query_for_get_single_result"));
                }
                        
                // If the return value is true indicating a result set then
                // build and return the single result.
                if (execute()) {
                    return getSingleResult();
                } else {
                    throw new IllegalStateException(ExceptionLocalization.buildMessage("incorrect_spq_query_for_get_result_list")); 
                }
            } else {
                if (hasMoreResults()) {
                    // Build the result records first.
                    List results;
                    
                    if (isOutputCursorResultSet) {
                        // Return result set list for the current outputCursorIndex.
                        results = (List) getOutputParameterValue(getCall().getOutputCursors().get(outputCursorIndex++).getName());
                        
                        // Update the hasMoreResults flag.
                        hasMoreResults = (outputCursorIndex < getCall().getOutputCursors().size());
                    } else {
                        // Build the result records first.
                        List result = buildResultRecords(executeStatement.getResultSet());
                    
                        // Move the result pointer.
                        moveResultPointer();
                        
                        results = getResultSetMappingQuery().buildObjectsFromRecords(result, ++executeResultSetIndex);
                    }
                    
                    if (results.size() > 1) {
                        throwNonUniqueResultException(ExceptionLocalization.buildMessage("too_many_results_for_get_single_result", (Object[]) null));
                    } else if (results.isEmpty()) {
                        throwNoResultException(ExceptionLocalization.buildMessage("no_entities_retrieved_for_get_single_result", (Object[]) null));
                    }
                        
                    // TODO: if hasMoreResults is true, we 'could' and maybe should throw an exception here.
                    
                    return results.get(0);
                } else {
                    return null;
                }
            }
        } catch (LockTimeoutException e) {
            throw e;
        } catch (PersistenceException e) {
            setRollbackOnly();
            throw e;
        } catch (IllegalStateException e) {
            setRollbackOnly();
            throw e;
        } catch (Exception e) {
            setRollbackOnly();
            throw new PersistenceException(e);
        } finally {
            close(); // Close the connection once we're done.
        }
    }
    
    /**
     * Returns the update count or -1 if there is no pending result
     * or if the next result is not an update count.
     * @return update count or -1 if there is no pending result or
     * if the next result is not an update count
     * @throws QueryTimeoutException if the query execution exceeds
     * the query timeout value set and only the statement is
     * rolled back
     * @throws PersistenceException if the query execution exceeds
     * the query timeout value set and the transaction
     * is rolled back
     */
    public int getUpdateCount() {
        entityManager.verifyOpenWithSetRollbackOnly();
        
        if (executeStatement != null) {
            try {
                int updateCount = executeStatement.getUpdateCount();
                
                // Moving the result pointer when -1 is reached doesn't seem
                // to be an issue for the jbdc driver, however as a safeguard,
                // once -1 is reached don't bother trying to move the pointer
                // as there is no need to do so.
                if (updateCount > -1) {
                    moveResultPointer();
                }
                return updateCount;
            } catch (SQLException e) {
                throw getDetailedException(DatabaseException.sqlException(e, executeCall, executeCall.getQuery().getAccessor(), executeCall.getQuery().getSession(), false));
            }
        }
        
        return -1;
    }
    
    /**
     * Returns true if the next result corresponds to a result set, and false if 
     * it is an update count or if there are no results other than through INOUT 
     * and OUT parameters, if any.
     * 
     * @return true if next result corresponds to result set
     * @throws QueryTimeoutException if the query execution exceeds the query 
     * timeout value set and only the statement is rolled back
     * @throws PersistenceException if the query execution exceeds the query 
     * timeout value set and the transaction is rolled back
     */
    public boolean hasMoreResults() {
        entityManager.verifyOpen();
        
        return hasMoreResults;
    }
    
    /**
     * Returns true if the execute statement for this query is 1) not null (i.e.
     * query has been executed and 2) is an instance of callable statement, 
     * meaning there are out parameters associated with it.
     */
    protected boolean isValidCallableStatement() {
        if (executeStatement == null) {
            throw new IllegalStateException(ExceptionLocalization.buildMessage("jpa21_invalid_call_on_un_executed_query"));
        }
        
        if (! (executeStatement instanceof CallableStatement)) {
            throw new IllegalStateException(ExceptionLocalization.buildMessage("jpa21_invalid_call_with_no_output_parameters"));
        }
        
        return true;
    }
    
    /**
     * INTERNAL:
     * Move the pointer up and update our has more results flag.
     * Once there are no result sets left, this will always return false.
     */
    private void moveResultPointer() {
        try {
            hasMoreResults = executeStatement.getMoreResults();
        } catch (SQLException e) {
            // swallow it.
            hasMoreResults = false;
        }
    }
    
    /**
     * Register a positional parameter. All positional parameters must be 
     * registered.
     * 
     * @param position parameter position
     * @param type type of the parameter
     * @param mode parameter mode
     * @return the same query instance
     */
    public StoredProcedureQuery registerStoredProcedureParameter(int position, Class type, ParameterMode mode) {
        entityManager.verifyOpenWithSetRollbackOnly();
        StoredProcedureCall call = (StoredProcedureCall) getDatabaseQuery().getCall();
        
        if (mode.equals(ParameterMode.IN)) {
            call.addUnamedArgument(String.valueOf(position), type);
        } else if (mode.equals(ParameterMode.OUT)) {
            call.addUnamedOutputArgument(String.valueOf(position), type);
        } else if (mode.equals(ParameterMode.INOUT)) {
            call.addUnamedInOutputArgument(String.valueOf(position), String.valueOf(position), type);
        } else if (mode.equals(ParameterMode.REF_CURSOR)) {
            call.useUnnamedCursorOutputAsResultSet(position);
        }
        
        // Force a re-calculate of the parameters.
        this.parameters = null;
        
        return this;
    }

    /**
     * Register a named parameter. When using parameter names, all parameters 
     * must be registered in the order in which they occur in the parameter list 
     * of the stored procedure.
     * 
     * @param parameterName name of the parameter as registered or
     *        specified in metadata
     * @param type type of the parameter
     * @param mode parameter mode  
     * @return the same query instance
     */
    public StoredProcedureQuery registerStoredProcedureParameter(String parameterName, Class type, ParameterMode mode) {
        entityManager.verifyOpenWithSetRollbackOnly();
        StoredProcedureCall call = (StoredProcedureCall) getDatabaseQuery().getCall();

        if (mode.equals(ParameterMode.IN)) {
            call.addNamedArgument(parameterName, parameterName, type);
        } else if (mode.equals(ParameterMode.OUT)) {
            call.addNamedOutputArgument(parameterName, parameterName, type);
            call.setCursorOrdinalPosition(parameterName, call.getParameters().size());
        } else if (mode.equals(ParameterMode.INOUT)) {
            call.addNamedInOutputArgument(parameterName, parameterName, parameterName, type);
            call.setCursorOrdinalPosition(parameterName, call.getParameters().size());
        } else if (mode.equals(ParameterMode.REF_CURSOR)) {
            call.useNamedCursorOutputAsResultSet(parameterName);
        }

        // Force a re-calculate of the parameters.
        this.parameters = null;
        
        return this;
    }
    
    /**
     * Set the position of the first result to retrieve.
     * 
     * @param start
     *            position of the first result, numbered from 0
     * @return the same query instance
     */
    public StoredProcedureQueryImpl setFirstResult(int startPosition) {
        throw new IllegalStateException(ExceptionLocalization.buildMessage("operation_not_supported", new Object[]{"setFirstResult", "StoredProcedureQuery"}));
    }
    
    /**
     * Set the flush mode type to be used for the query execution.
     * The flush mode type applies to the query regardless of the
     * flush mode type in use for the entity manager.
     * @param flushMode flush mode
     * @return the same query instance
     */
    public StoredProcedureQueryImpl setFlushMode(FlushModeType flushMode) {
        return (StoredProcedureQueryImpl) super.setFlushMode(flushMode);
    }
    
    /**
     * Set a query property or hint. The hints elements may be used to specify 
     * query properties and hints. Properties defined by this specification must 
     * be observed by the provider. Vendor-specific hints that are not 
     * recognized by a provider must be silently ignored. Portable applications 
     * should not rely on the standard timeout hint. Depending on the database
     * in use, this hint may or may not be observed.
     * 
     * @param hintName name of the property or hint
     * @param value value for the property or hint
     * @return the same query instance
     * @throws IllegalArgumentException if the second argument is not valid for 
     * the implementation
     */
    public StoredProcedureQuery setHint(String hintName, Object value) {
        try {
            entityManager.verifyOpen();
            setHintInternal(hintName, value);
            return this;
        } catch (RuntimeException e) {
            setRollbackOnly();
            throw e;
        }
    }
    
    /**
     * Set the lock mode type to be used for the query execution.
     * 
     * @param lockMode
     * @throws IllegalStateException
     *             if not a Java Persistence query language SELECT query
     */
    public StoredProcedureQueryImpl setLockMode(LockModeType lockMode) {
        return (StoredProcedureQueryImpl) super.setLockMode(lockMode);
    }
    
    /**
     * Set the maximum number of results to retrieve.
     * 
     * @param maxResult
     * @return the same query instance
     */
    public StoredProcedureQueryImpl setMaxResults(int maxResult) {
        throw new IllegalStateException(ExceptionLocalization.buildMessage("operation_not_supported", new Object[]{"setMaxResults", "StoredProcedureQuery"}));
    }

    /**
     * Bind an instance of java.util.Calendar to a positional parameter.
     * 
     * @param position
     * @param value
     * @param temporalType
     * @return the same query instance
     * @throws IllegalArgumentException if position does not correspond to a 
     * positional parameter of the query or if the value argument is of 
     * incorrect type
     */
    public StoredProcedureQuery setParameter(int position, Calendar value, TemporalType temporalType) {
        entityManager.verifyOpenWithSetRollbackOnly();
        return setParameter(position, convertTemporalType(value, temporalType));
    }
    
    /**
     * Bind an instance of java.util.Date to a positional parameter.
     * 
     * @param position
     * @param value
     * @param temporalType
     * @return the same query instance
     * @throws IllegalArgumentException if position does not correspond to a 
     * positional parameter of the query or if the value argument is of 
     * incorrect type
     */
    public StoredProcedureQuery setParameter(int position, Date value, TemporalType temporalType) {
        entityManager.verifyOpenWithSetRollbackOnly();
        return setParameter(position, convertTemporalType(value, temporalType));
    }
    
    /**
     * Bind an argument to a positional parameter.
     * 
     * @param position
     * @param value
     * @return the same query instance
     * @throws IllegalArgumentException if position does not correspond to a 
     * positional parameter of the query or if the argument is of incorrect type
     */
    public StoredProcedureQuery setParameter(int position, Object value) {
        try {
            entityManager.verifyOpen();
            setParameterInternal(position, value);
            return this;
        } catch (RuntimeException e) {
            setRollbackOnly();
            throw e;
        }
    }
    
    /**
     * Bind an instance of java.util.Calendar to a Parameter object.
     * 
     * @param param 
     * @param value 
     * @param temporalType
     * @return the same query instance
     * @throws IllegalArgumentException if the parameter does not correspond to 
     * a parameter of the query
     */
    public StoredProcedureQuery setParameter(Parameter<Calendar> param, Calendar value, TemporalType temporalType) {
        if (param == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("NULL_PARAMETER_PASSED_TO_SET_PARAMETER"));
        }
        //bug 402686: type validation
        String position = getParameterId(param);
        ParameterExpressionImpl parameter = (ParameterExpressionImpl) this.getInternalParameters().get(position);
        if (parameter == null ) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("NO_PARAMETER_WITH_NAME", new Object[] { param.toString(), this.databaseQuery }));
        }
        if (!parameter.getParameterType().equals(param.getParameterType())) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("INCORRECT_PARAMETER_TYPE", new Object[] { position, param.getParameterType() }));
        }
        return this.setParameter(position, value, temporalType);
    }
    
    /**
     * Bind an instance of java.util.Date to a Parameter object.
     * 
     * @param param 
     * @param value 
     * @param temporalType 
     * @return the same query instance
     * @throws IllegalArgumentException if the parameter does not correspond to 
     * a parameter of the query
     */
    public StoredProcedureQuery setParameter(Parameter<Date> param, Date value, TemporalType temporalType) {
        if (param == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("NULL_PARAMETER_PASSED_TO_SET_PARAMETER"));
        }
        //bug 402686: type validation
        String position = getParameterId(param);
        ParameterExpressionImpl parameter = (ParameterExpressionImpl) this.getInternalParameters().get(position);
        if (parameter == null ) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("NO_PARAMETER_WITH_NAME", new Object[] { param.toString(), this.databaseQuery }));
        }
        if (!parameter.getParameterType().equals(param.getParameterType())) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("INCORRECT_PARAMETER_TYPE", new Object[] { position, param.getParameterType() }));
        }
        return this.setParameter(position, value, temporalType);
    }
    
    /**
     * Bind the value of a Parameter object.
     * 
     * @param param
     * @param value
     * @return the same query instance
     * @throws IllegalArgumentException if the parameter does not correspond to 
     * a parameter of the query
     */
    public <T> StoredProcedureQuery setParameter(Parameter<T> param, T value) {
        if (param == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("NULL_PARAMETER_PASSED_TO_SET_PARAMETER"));
        }
        //bug 402686: type validation
        String position = getParameterId(param);
        ParameterExpressionImpl parameter = (ParameterExpressionImpl) this.getInternalParameters().get(position);
        if (parameter == null ) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("NO_PARAMETER_WITH_NAME", new Object[] { param.toString(), this.databaseQuery }));
        }
        if (!parameter.getParameterType().equals(param.getParameterType())) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("INCORRECT_PARAMETER_TYPE", new Object[] { position, param.getParameterType() }));
        }
        return this.setParameter(position, value);
    }
    
    /**
     * Bind an instance of java.util.Calendar to a named parameter.
     * 
     * @param name
     * @param value
     * @param temporalType
     * @return the same query instance
     * @throws IllegalArgumentException if the parameter name does not 
     * correspond to a parameter of the query or if the value argument is of 
     * incorrect type
     */
    public StoredProcedureQuery setParameter(String name, Calendar value, TemporalType temporalType) {
        entityManager.verifyOpenWithSetRollbackOnly();
        return setParameter(name, convertTemporalType(value, temporalType));
    }

    /**
     * Bind an instance of java.util.Date to a named parameter.
     * 
     * @param name
     * @param value
     * @param temporalType
     * @return the same query instance
     * @throws IllegalArgumentException if the parameter name does not 
     * correspond to a parameter of the query or if the value argument is of 
     * incorrect type
     */
    public StoredProcedureQuery setParameter(String name, Date value, TemporalType temporalType) {
        entityManager.verifyOpenWithSetRollbackOnly();
        return setParameter(name, convertTemporalType(value, temporalType));
    }
    
    /**
     * Bind an argument to a named parameter.
     * 
     * @param name 
     * @param value
     * @return the same query instance
     * @throws IllegalArgumentException if the parameter name does not 
     * correspond to a parameter of the query or if the argument is of incorrect 
     * type
     */
    public StoredProcedureQuery setParameter(String name, Object value) {
        try {
            entityManager.verifyOpen();
            setParameterInternal(name, value, false);
            return this;
        } catch (RuntimeException e) {
            setRollbackOnly();
            throw e;
        }
    }

    /**
     * Bind an argument to a named or indexed parameter.
     * 
     * @param name
     *            the parameter name
     * @param value
     *            to bind
     * @param isIndex
     *            defines if index or named
     */
    protected void setParameterInternal(String name, Object value, boolean isIndex) {
        Parameter parameter = this.getInternalParameters().get(name);
        StoredProcedureCall call = (StoredProcedureCall) getDatabaseQuery().getCall();
        if (parameter == null) {
            if (isIndex) {
                throw new IllegalArgumentException(ExceptionLocalization.buildMessage("ejb30-wrong-argument-index", new Object[] { name, call.getProcedureName() }));
            } else {
                throw new IllegalArgumentException(ExceptionLocalization.buildMessage("ejb30-wrong-argument-name", new Object[] { name, call.getProcedureName() }));
            }
        }
        if (!isValidActualParameter(value, parameter.getParameterType())) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("ejb30-incorrect-parameter-type", new Object[] { name, value.getClass(), parameter.getParameterType(), call.getProcedureName() }));
        }
        this.parameterValues.put(name, value);
    }
}

