| /* |
| * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved. |
| * Copyright (c) 2019 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 |
| // 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.CallableStatement; |
| import java.sql.ResultSet; |
| import java.sql.ResultSetMetaData; |
| import java.sql.SQLException; |
| import java.sql.Statement; |
| 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 jakarta.persistence.FlushModeType; |
| import jakarta.persistence.LockModeType; |
| import jakarta.persistence.LockTimeoutException; |
| import jakarta.persistence.Parameter; |
| import jakarta.persistence.ParameterMode; |
| import jakarta.persistence.PersistenceException; |
| import jakarta.persistence.QueryTimeoutException; |
| import jakarta.persistence.StoredProcedureQuery; |
| import jakarta.persistence.TemporalType; |
| |
| import org.eclipse.persistence.exceptions.DatabaseException; |
| import org.eclipse.persistence.internal.databaseaccess.*; |
| 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.AbstractRecord; |
| 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 StoredProcedureQueryImpl. Initializes basic variables. |
| */ |
| protected StoredProcedureQueryImpl(EntityManagerImpl entityManager) { |
| super(entityManager); |
| } |
| |
| /** |
| * Create an StoredProcedureQueryImpl with a DatabaseQuery. |
| */ |
| public StoredProcedureQueryImpl(DatabaseQuery query, EntityManagerImpl entityManager) { |
| super(query, entityManager); |
| } |
| |
| /** |
| * Create an StoredProcedureQueryImpl with 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<AbstractRecord> 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 == DatasourceCall.INOUT) { |
| field = (DatabaseField) ((Object[]) parameter)[0]; |
| } else if (parameterType == DatasourceCall.IN) { |
| field = (DatabaseField) parameter; |
| } else if (parameterType == DatasourceCall.OUT || parameterType == DatasourceCall.OUT_CURSOR) { |
| if (parameter instanceof OutputParameterForCallableStatement) { |
| field = ((OutputParameterForCallableStatement) parameter).getOutputField(); |
| } else { |
| field = (DatabaseField) parameter; |
| } |
| } |
| |
| // 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 |
| */ |
| @Override |
| public Object getOutputParameterValue(int position) { |
| entityManager.verifyOpen(); |
| |
| if (isValidCallableStatement()) { |
| try { |
| Object obj = executeCall.getOutputParameterValue((CallableStatement) executeStatement, position - 1, entityManager.getAbstractSession()); |
| |
| 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 |
| */ |
| @Override |
| public Object getOutputParameterValue(String parameterName) { |
| entityManager.verifyOpen(); |
| |
| if (isValidCallableStatement()) { |
| try { |
| Object obj = executeCall.getOutputParameterValue((CallableStatement) executeStatement, parameterName, entityManager.getAbstractSession()); |
| |
| if (obj instanceof ResultSet) { |
| // If a result set is returned we have to build the objects. |
| return getResultSetMappingQuery().buildObjectsFromRecords(buildResultRecords((ResultSet) obj), ++executeResultSetIndex); |
| } |
| return obj; |
| } catch (Exception exception) { |
| throw new IllegalArgumentException(ExceptionLocalization.buildMessage("jpa21_invalid_parameter_name", new Object[] { parameterName, exception.getMessage() }), exception); |
| } |
| } |
| |
| return null; |
| } |
| |
| private boolean hasPositionalParameters() { |
| for (Parameter parameter: this.getParameters()) { |
| if (parameter.getName() != null) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * 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 = null; |
| if (hasPositionalParameters()) { |
| results = (List) getOutputParameterValue(getCall().getOutputCursors().get(outputCursorIndex++).getIndex() + 1); |
| } else { |
| 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. |
| if (hasPositionalParameters()) { |
| results = (List) getOutputParameterValue(getCall().getOutputCursors().get(outputCursorIndex++).getIndex() + 1); |
| } else { |
| 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", null)); |
| } else if (results.isEmpty()) { |
| throwNoResultException(ExceptionLocalization.buildMessage("no_entities_retrieved_for_get_single_result", 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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); |
| } else if (mode.equals(ParameterMode.INOUT)) { |
| call.addNamedInOutputArgument(parameterName, parameterName, parameterName, type); |
| } 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 startPosition |
| * position of the first result, numbered from 0 |
| * @return the same query instance |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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 |
| */ |
| @Override |
| 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); |
| } |
| } |
| |