/*
 * Copyright (c) 1998, 2018 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 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
//     04/01/2011-2.3 Guy Pelletier
//       - 337323: Multi-tenant with shared schema support (part 2)
//     09/09/2011-2.3.1 Guy Pelletier
//       - 356197: Add new VPD type to MultitenantType
//     01/06/2011-2.3 Guy Pelletier
//       - 371453: JPA Multi-Tenancy in Bidirectional OneToOne Relation throws ArrayIndexOutOfBoundsException
package org.eclipse.persistence.queries;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
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.DatabasePlatform;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.helper.InvalidObject;
import org.eclipse.persistence.internal.helper.NonSynchronizedVector;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.indirection.ProxyIndirectionPolicy;
import org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.ResultSetRecord;
import org.eclipse.persistence.internal.sessions.SimpleResultSetRecord;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.internal.sessions.remote.RemoteSessionController;
import org.eclipse.persistence.internal.sessions.remote.Transporter;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.sessions.SessionProfiler;
import org.eclipse.persistence.sessions.remote.DistributedSession;
import org.eclipse.persistence.tools.profiler.QueryMonitor;

/**
 * <p><b>Purpose</b>:
 * Concrete class for all read queries involving a single object.
 *
 * <p><b>Responsibilities</b>:
 * Return a single object for the query.
 * Implements the inheritance feature when dealing with abstract descriptors.
 *
 * @author Yvon Lavoie
 * @since TOPLink/Java 1.0
 */
public class ReadObjectQuery extends ObjectLevelReadQuery {
    /** Object that can be used in place of a selection criteria. */
    protected transient Object selectionObject;

    /** Key that can be used in place of a selection criteria. */
    protected Object selectionId;

    /** Can be used to refresh a specific non-cached instance from the database. */
    protected boolean shouldLoadResultIntoSelectionObject = false;

    /**
     * PUBLIC:
     * Return a new read object query.
     * A reference class must be specified before execution.
     * It is better to provide the class and expression builder on construction to esnure a single expression builder is used.
     * If no selection criteria is specified this will reads the first object found in the database.
     */
    public ReadObjectQuery() {
        super();
    }

    /**
     * PUBLIC:
     * Return a new read object query.
     * By default, the query has no selection criteria. Executing this query without
     * selection criteria will always result in a database access to read the first
     * instance of the specified Class found in the database. This is true no
     * matter how cache usage is configured and even if an instance of the
     * specified Class exists in the cache.
     * Executing a query with selection criteria allows you to avoid a database
     * access if the selected instance is in the cache. For this reason, you may whish to use a ReadObjectQuery constructor that takes selection criteria, such as: {@link #ReadObjectQuery(Class, Call)}, {@link #ReadObjectQuery(Class, Expression)}, {@link #ReadObjectQuery(Class, ExpressionBuilder)}, {@link #ReadObjectQuery(ExpressionBuilder)}, {@link #ReadObjectQuery(Object)}, or {@link #ReadObjectQuery(Object, QueryByExamplePolicy)}.
     */
    public ReadObjectQuery(Class classToRead) {
        this();
        this.referenceClass = classToRead;
    }

    /**
     * PUBLIC:
     * Return a new read object query for the class and the selection criteria.
     */
    public ReadObjectQuery(Class classToRead, Expression selectionCriteria) {
        this();
        this.referenceClass = classToRead;
        setSelectionCriteria(selectionCriteria);
    }

    /**
     * PUBLIC:
     * Return a new read object query for the class.
     * The expression builder must be used for all associated expressions used with the query.
     */
    public ReadObjectQuery(Class classToRead, ExpressionBuilder builder) {
        this();
        this.defaultBuilder = builder;
        this.referenceClass = classToRead;
    }

    /**
     * PUBLIC:
     * Return a new read object query.
     * The call represents a database interaction such as SQL, Stored Procedure.
     */
    public ReadObjectQuery(Class classToRead, Call call) {
        this();
        this.referenceClass = classToRead;
        setCall(call);
    }

    /**
     * PUBLIC:
     * Return a new read object query.
     * The call represents a database interaction such as SQL, Stored Procedure.
     */
    public ReadObjectQuery(Call call) {
        this();
        setCall(call);
    }

    /**
     * PUBLIC:
     * Return a query to read the object with the same primary key as the provided object.
     * Note: This is not a query by example object, only the primary key will be used for the selection criteria.
     */
    public ReadObjectQuery(Object objectToRead) {
        this();
        setSelectionObject(objectToRead);
    }

    /**
     * PUBLIC:
     * Return a query by example query to find an object matching the attributes of the example object.
     */
    public ReadObjectQuery(Object exampleObject, QueryByExamplePolicy policy) {
        this();
        setExampleObject(exampleObject);
        setQueryByExamplePolicy(policy);
    }

    /**
     * PUBLIC:
     * The expression builder should be provide on creation to ensure only one is used.
     */
    public ReadObjectQuery(ExpressionBuilder builder) {
        this();
        this.defaultBuilder = builder;
    }

    /**
     * INTERNAL:
     * <P> This method is called by the object builder when building an original.
     * It will cause the original to be cached in the query results if the query
     * is set to do so.
     */
    public void cacheResult(Object object) {
        Object cachableObject = object;
        if (object == null) {
            this.temporaryCachedQueryResults = InvalidObject.instance();
        } else {
            if (this.shouldUseWrapperPolicy) {
                cachableObject = this.session.wrapObject(object);
            }
            this.temporaryCachedQueryResults = cachableObject;
        }
    }


    /**
     * PUBLIC:
     * The cache will be checked only if the query contains exactly the primary key.
     * Queries can be configured to use the cache at several levels.
     * Other caching option are available.
     * @see #setCacheUsage(int)
     */
    public void checkCacheByExactPrimaryKey() {
        setCacheUsage(CheckCacheByExactPrimaryKey);
    }

    /**
     * PUBLIC:
     * This is the default, the cache will be checked only if the query contains the primary key.
     * Queries can be configured to use the cache at several levels.
     * Other caching option are available.
     * @see #setCacheUsage(int)
     */
    public void checkCacheByPrimaryKey() {
        setCacheUsage(CheckCacheByPrimaryKey);
    }

    /**
     * PUBLIC:
     * The cache will be checked completely, then if the object is not found or the query too complex the database will be queried.
     * Queries can be configured to use the cache at several levels.
     * Other caching option are available.
     * @see #setCacheUsage(int)
     */
    public void checkCacheThenDatabase() {
        setCacheUsage(CheckCacheThenDatabase);
    }

    /**
     * INTERNAL:
     * Ensure that the descriptor has been set.
     */
    public void checkDescriptor(AbstractSession session) throws QueryException {
        if (this.descriptor == null) {
            if (getReferenceClass() == null) {
                throw QueryException.referenceClassMissing(this);
            }
            ClassDescriptor referenceDescriptor;
            //Bug#3947714  In case getSelectionObject() is proxy
            if (getSelectionObject() != null && session.getProject().hasProxyIndirection()) {
                referenceDescriptor = session.getDescriptor(getSelectionObject());
            } else {
                referenceDescriptor = session.getDescriptor(getReferenceClass());
            }
            if (referenceDescriptor == null) {
                throw QueryException.descriptorIsMissing(getReferenceClass(), this);
            }
            setDescriptor(referenceDescriptor);
        }
    }

    /**
     * INTERNAL:
     * The cache check is done before the prepare as a hit will not require the work to be done.
     */
    protected Object checkEarlyReturnLocal(AbstractSession session, AbstractRecord translationRow) {
        if (shouldCheckCache() && shouldMaintainCache() && (!shouldRefreshIdentityMapResult() && (!shouldRetrieveBypassCache()))
                && (!(session.isRemoteSession() && (shouldRefreshRemoteIdentityMapResult() || this.descriptor.shouldDisableCacheHitsOnRemote())))
                && (!(shouldCheckDescriptorForCacheUsage() && this.descriptor.shouldDisableCacheHits())) && (!this.descriptor.isDescriptorForInterface())) {
            Object cachedObject = getQueryMechanism().checkCacheForObject(translationRow, session);
            this.isCacheCheckComplete = true;

            // Optimization: If find deleted object by exact primary
            // key expression or selection object/key just abort.
            if (cachedObject == InvalidObject.instance) {
                return cachedObject;
            }
            if (cachedObject != null) {
                if (shouldLoadResultIntoSelectionObject()) {
                    ObjectBuilder builder = this.descriptor.getObjectBuilder();
                    builder.copyInto(cachedObject, getSelectionObject());
                    //put this object into the cache.  This may cause some loss of identity
                    session.getIdentityMapAccessorInstance().putInIdentityMap(getSelectionObject());
                    cachedObject = getSelectionObject();
                }

                // check locking.  If clone has not been locked, do not early return cached object
                if (isLockQuery() && (session.isUnitOfWork() && !((UnitOfWorkImpl)session).isPessimisticLocked(cachedObject))) {
                    return null;
                }
                if (QueryMonitor.shouldMonitor()) {
                    QueryMonitor.incrementReadObjectHits(this);
                }
                session.incrementProfile(SessionProfiler.CacheHits, this);
            } else {
                if (!session.isUnitOfWork()) {
                    if (QueryMonitor.shouldMonitor()) {
                        QueryMonitor.incrementReadObjectMisses(this);
                    }
                    session.incrementProfile(SessionProfiler.CacheMisses, this);
                }
            }
            if (shouldUseWrapperPolicy()) {
                cachedObject = this.descriptor.getObjectBuilder().wrapObject(cachedObject, session);
            }
            return cachedObject;
        } else {
            if (!session.isUnitOfWork()) {
                if (QueryMonitor.shouldMonitor()) {
                    QueryMonitor.incrementReadObjectMisses(this);
                }
                session.incrementProfile(SessionProfiler.CacheMisses, this);
            }
            return null;
        }
    }

    /**
     * INTERNAL:
     * Check and return custom query flag. Custom query flag value is initialized when stored value is {@code null}.
     * Called from {@link #checkForCustomQuery(AbstractSession, AbstractRecord)} to retrieve custom query flag.
     * @param session        Current session.
     * @param translationRow Database record.
     * @return Current custom query flag. Value will never be {@code null}.
     */
    @Override
    protected Boolean checkCustomQueryFlag(final AbstractSession session, final AbstractRecord translationRow) {
        // #436871 - Use local copy to avoid NPE from concurrent modification.
        Boolean useCustomQuery = isCustomQueryUsed;
        if (useCustomQuery != null) {
            return useCustomQuery;
        }
        // Check if user defined a custom query in the query manager.
        if (!this.isUserDefined) {
            if (!isCallQuery()
                    // By default all descriptors have a custom ("static") read-object query.
                    // This allows the read-object query and SQL to be prepare once.
                    && this.descriptor.getQueryManager().hasReadObjectQuery()) {
                // If the query require special SQL generation or execution do not use the static read object query.
                // PERF: the read-object query should always be static to ensure no regeneration of SQL.
                if ((!hasJoining() || !this.joinedAttributeManager.hasJoinedAttributeExpressions())
                        && (!hasPartialAttributeExpressions()) && (redirector == null) && !doNotRedirect
                        && (!hasAsOfClause()) && (!hasNonDefaultFetchGroup())
                        && (this.shouldUseSerializedObjectPolicy == shouldUseSerializedObjectPolicyDefault)
                        && this.wasDefaultLockMode && (shouldBindAllParameters == null) && (this.hintString == null)) {
                    if ((this.selectionId != null) || (this.selectionObject != null)) {// Must be primary key.
                        return Boolean.TRUE;
                    } else {
                        Expression selectionCriteria = getSelectionCriteria();
                        if (selectionCriteria != null) {
                            AbstractRecord primaryKeyRow =
                                    this.descriptor.getObjectBuilder().extractPrimaryKeyRowFromExpression(
                                            selectionCriteria, translationRow, session);
                            // Only execute the query if the selection criteria has the primary key fields set
                            if (primaryKeyRow != null) {
                                return Boolean.TRUE;
                            }
                        }
                    }
                }
            }
        }
        return useCustomQuery;
    }

    /**
     * INTERNAL:
     * Get custom single object read query from query manager.
     * Called from {@link #checkForCustomQuery(AbstractSession, AbstractRecord)} to retrieve custom read query.
     * @return Custom single object read query from query manager.
     */
    @Override
    protected ObjectLevelReadQuery getReadQuery() {
        return descriptor.getQueryManager().getReadObjectQuery();
    }

    /**
     * INTERNAL:
     * Conform the result in the UnitOfWork.
     */
    protected Object conformResult(Object result, UnitOfWorkImpl unitOfWork, AbstractRecord databaseRow, boolean buildDirectlyFromRows) {
        // Note that if the object does not conform even though other objects might exist on the database null is returned.
        // Note that new objects is checked before the read is executed so does not have to be re-checked.
        // Must unwrap as the built object is always wrapped.
        // Note the object is unwrapped on the parent which it belongs to, as we
        // do not want to trigger a registration just yet.
        Object clone = null;
        if (buildDirectlyFromRows) {
            clone = buildObject((AbstractRecord)result);
        } else {
            clone = registerIndividualResult(
                    this.descriptor.getObjectBuilder().unwrapObject(result, unitOfWork.getParent()),
                    null, unitOfWork, null, null);
        }
        Expression selectionCriteria = getSelectionCriteria();
        if ((selectionCriteria != null) && (this.selectionId == null) && (this.selectionObject == null)) {
            ExpressionBuilder builder = selectionCriteria.getBuilder();
            builder.setSession(unitOfWork.getRootSession(null));
            builder.setQueryClass(getReferenceClass());
        }

        clone = conformIndividualResult(clone, unitOfWork, databaseRow, selectionCriteria, null);
        if (clone == null) {
            return clone;
        }

        if (shouldUseWrapperPolicy()) {
            return this.descriptor.getObjectBuilder().wrapObject(clone, unitOfWork);
        } else {
            return clone;
        }
    }

    /**
     * PUBLIC:
     * Do not refesh/load into the selection object, this is the default.
     * This property allows for the selection object of the query to be refreshed or put into the TopLink cache.
     * By default on a read or refresh the object in the cache is refreshed and returned or a new object is built from the database,
     * in some cases such as EJB BMP it is desirable to refresh or load into the object passed into the read object query.
     * <p>Note: This forces the selection object into the cache a replaces any existing object that may already be there,
     * this is a strict violation of object identity and other objects can still be refering to the old object.
     */
    public void dontLoadResultIntoSelectionObject() {
        setShouldLoadResultIntoSelectionObject(false);
    }

    /**
     * INTERNAL:
     * Execute the query. If there are cached results return those.
     * This must override the super to support result caching.
     *
     * @param session the session in which the receiver will be executed.
     * @return An object or vector, the result of executing the query.
     * @exception DatabaseException - an error has occurred on the database
     */
    public Object execute(AbstractSession session, AbstractRecord row) throws DatabaseException {
        if (shouldCacheQueryResults()) {
            if (shouldConformResultsInUnitOfWork()) {
                throw QueryException.cannotConformAndCacheQueryResults(this);
            }
            if (isPrepared()) {// only prepared queries can have cached results.
                Object result = getQueryResults(session, row, true);
                // Bug6138532 - if result is "cached no results", return null immediately
                if (result == InvalidObject.instance) {
                    return null;
                }
                if (result != null) {
                    if (session.isUnitOfWork()) {
                        result = ((UnitOfWorkImpl)session).registerExistingObject(result);
                    }
                    return result;
                }
            }
        }
        return super.execute(session, row);
    }

    /**
     * INTERNAL:
     * Execute the query.
     * Do a cache lookup and build object from row if required.
     * @exception  DatabaseException - an error has occurred on the database
     * @return object - the first object found or null if none.
     */
    protected Object executeObjectLevelReadQuery() throws DatabaseException {
        if (this.descriptor.isDescriptorForInterface()  || this.descriptor.hasTablePerClassPolicy()) {
            Object returnValue = this.descriptor.getInterfacePolicy().selectOneObjectUsingMultipleTableSubclassRead(this);

            if (this.descriptor.hasTablePerClassPolicy() && (!this.descriptor.isAbstract()) && (returnValue == null)) {
                // let it fall through to query the root.
            } else {
                this.executionTime = System.currentTimeMillis();
                return returnValue;
            }
        }

        boolean shouldSetRowsForJoins = hasJoining() && this.joinedAttributeManager.isToManyJoin();
        AbstractSession session = getSession();
        Object result = null;
        AbstractRecord row = null;

        Object sopObject = getTranslationRow().getSopObject();
        boolean useOptimization = false;
        if (sopObject == null) {
            useOptimization = usesResultSetAccessOptimization();
        }

        if (useOptimization) {
            DatabaseCall call = ((DatasourceCallQueryMechanism)this.queryMechanism).selectResultSet();
            this.executionTime = System.currentTimeMillis();
            boolean exceptionOccured = false;
            ResultSet resultSet = call.getResult();
            DatabaseAccessor dbAccessor = (DatabaseAccessor)getAccessor();
            try {
                if (resultSet.next()) {
                    ResultSetMetaData metaData = call.getResult().getMetaData();
                    boolean useSimple = this.descriptor.getObjectBuilder().isSimple();
                    DatabasePlatform platform = dbAccessor.getPlatform();
                    boolean optimizeData = platform.shouldOptimizeDataConversion();
                    if (useSimple) {
                        row = new SimpleResultSetRecord(call.getFields(), call.getFieldsArray(), resultSet, metaData, dbAccessor, getExecutionSession(), platform, optimizeData);
                        if (this.descriptor.isDescriptorTypeAggregate()) {
                            // Aggregate Collection may have an unmapped primary key referencing the owner, the corresponding field will not be used when the object is populated and therefore may not be cleared.
                            ((SimpleResultSetRecord)row).setShouldKeepValues(true);
                        }
                    } else {
                        row = new ResultSetRecord(call.getFields(), call.getFieldsArray(), resultSet, metaData, dbAccessor, getExecutionSession(), platform, optimizeData);
                    }
                    if (session.isUnitOfWork()) {
                        result = registerResultInUnitOfWork(row, (UnitOfWorkImpl)session, this.translationRow, true);
                    } else {
                        result = buildObject(row);
                    }

                    if (!useSimple && this.descriptor.getObjectBuilder().shouldKeepRow()) {
                        if (((ResultSetRecord)row).hasResultSet()) {
                            // ResultSet has not been fully triggered - that means the cached object was used.
                            // Yet the row still may be cached in a value holder (see loadBatchReadAttributes and loadJoinedAttributes methods).
                            // Remove ResultSet to avoid attempt to trigger it (already closed) when pk or fk values (already extracted) accessed when the value holder is instantiated.
                            ((ResultSetRecord)row).removeResultSet();
                        } else {
                            ((ResultSetRecord)row).removeNonIndirectionValues();
                        }
                    }
                }
            } catch (SQLException exception) {
                exceptionOccured = true;
                DatabaseException commException = dbAccessor.processExceptionForCommError(session, exception, call);
                if (commException != null) {
                    throw commException;
                }
                throw DatabaseException.sqlException(exception, call, getAccessor(), session, false);
            } finally {
                try {
                    if (resultSet != null) {
                        resultSet.close();
                    }
                    if (dbAccessor != null) {
                        if (call.getStatement() != null) {
                            dbAccessor.releaseStatement(call.getStatement(), call.getSQLString(), call, session);
                        }
                    }
                    if (call.hasAllocatedConnection()) {
                        getExecutionSession().releaseConnectionAfterCall(this);
                    }
                } catch (RuntimeException cleanupException) {
                    if (!exceptionOccured) {
                        throw cleanupException;
                    }
                } catch (SQLException cleanupSQLException) {
                    if (!exceptionOccured) {
                        throw DatabaseException.sqlException(cleanupSQLException, call, dbAccessor, session, false);
                    }
                }
            }
        } else {
            if (sopObject != null) {
                row = new DatabaseRecord(0);
                row.setSopObject(sopObject);
            } else {
                // If using 1-m joins, must select all rows.
                if (shouldSetRowsForJoins) {
                    List rows = getQueryMechanism().selectAllRows();
                    if (rows.size() > 0) {
                        row = (AbstractRecord)rows.get(0);
                    }
                    getJoinedAttributeManager().setDataResults(rows, session);
                } else {
                    row = getQueryMechanism().selectOneRow();
                }
            }

            this.executionTime = System.currentTimeMillis();
            if (row != null) {
                if (session.isUnitOfWork()) {
                    result = registerResultInUnitOfWork(row, (UnitOfWorkImpl)session, this.translationRow, true);
                } else {
                    result = buildObject(row);
                }
                if (sopObject != null) {
                    // remove sopObject so it's not stuck in a value holder.
                    row.setSopObject(null);
                }
            }
        }
        if ((result == null) && shouldCacheQueryResults()) {
            cacheResult(null);
        }
        if ((result == null) && this.shouldRefreshIdentityMapResult) {
            // bug5955326, should invalidate the shared cached if refreshed object no longer exists.
            if (this.selectionId != null) {
                session.getParentIdentityMapSession(this.descriptor, true, true).getIdentityMapAccessor().invalidateObject(this.selectionId, this.referenceClass);
            } else if (this.selectionObject != null) {
                session.getParentIdentityMapSession(this.descriptor, true, true).getIdentityMapAccessor().invalidateObject(this.selectionObject);
            }
        }

        if (this.shouldIncludeData && (sopObject == null)) {
            ComplexQueryResult complexResult = new ComplexQueryResult();
            complexResult.setResult(result);
            complexResult.setData(row);
            return complexResult;
        }

        return result;
    }

    /**
     * INTERNAL:
     * Execute the query building the objects directly from the database result-set.
     * @exception  DatabaseException - an error has occurred on the database
     * @return object - the first object found or null if none.
     */
    protected Object executeObjectLevelReadQueryFromResultSet() throws DatabaseException {
        AbstractSession session = this.session;
        DatabasePlatform platform = session.getPlatform();
        DatabaseCall call = ((DatasourceCallQueryMechanism)this.queryMechanism).selectResultSet();
        Statement statement = call.getStatement();
        ResultSet resultSet = call.getResult();
        DatabaseAccessor accessor = (DatabaseAccessor)((List<Accessor>)this.accessors).get(0);
        boolean exceptionOccured = false;
        try {
            if (!resultSet.next()) {
                return null;
            }
            ResultSetMetaData metaData = resultSet.getMetaData();
            return this.descriptor.getObjectBuilder().buildObjectFromResultSet(this, null, resultSet, session, accessor, metaData, platform, call.getFields(), call.getFieldsArray());
        } catch (SQLException exception) {
            exceptionOccured = true;
            DatabaseException commException = accessor.processExceptionForCommError(session, exception, call);
            if (commException != null) {
                throw commException;
            }
            throw DatabaseException.sqlException(exception, call, accessor, session, false);
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (statement != null) {
                    accessor.releaseStatement(statement, call.getSQLString(), call, session);
                }
                if (accessor != null) {
                    session.releaseReadConnection(accessor);
                }
            } catch (SQLException exception) {
                if (!exceptionOccured) {
                    //in the case of an external connection pool the connection may be null after the statement release
                    // if it is null we will be unable to check the connection for a comm error and
                    //therefore must return as if it was not a comm error.
                    DatabaseException commException = accessor.processExceptionForCommError(session, exception, call);
                    if (commException != null) {
                        throw commException;
                    }
                    throw DatabaseException.sqlException(exception, call, accessor, session, false);
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Extract the correct query result from the transporter.
     */
    public Object extractRemoteResult(Transporter transporter) {
        return ((DistributedSession)getSession()).getObjectCorrespondingTo(transporter.getObject(), transporter.getObjectDescriptors(), new IdentityHashMap(), this);
    }

    /**
     * INTERNAL:
     * Returns the specific default redirector for this query type.  There are numerous default query redirectors.
     * See ClassDescriptor for their types.
     */
    protected QueryRedirector getDefaultRedirector(){
        return descriptor.getDefaultReadObjectQueryRedirector();
    }

    /**
     * PUBLIC:
     * The primary key can be specified if used instead of an expression or selection object.
     * If composite the primary must be in the same order as defined in the descriptor.
     * @deprecated since EclipseLink 2.1, replaced by getSelectionId()
     * @see #getSelectionId()
     */
    @Deprecated
    public Vector getSelectionKey() {
        if (this.selectionId instanceof CacheId) {
            return new Vector(Arrays.asList(((CacheId)this.selectionId).getPrimaryKey()));
        }
        Vector primaryKey = new Vector(1);
        primaryKey.add(this.selectionId);
        return (Vector)selectionId;

    }

    /**
     * PUBLIC:
     * Return the selection object of the query.
     * This can be used instead of a where clause expression for single object primary key queries.
     * The selection object given should have a primary key defined,
     * this primary key will be used to query the database instance of the same object.
     * This is a basic form of query by example where only the primary key is required,
     * it can be used for simple query forms, or testing.
     */
    public Object getSelectionObject() {
        return selectionObject;
    }

    /**
     * PUBLIC:
     * Return if this is a read object query.
     */
    public boolean isReadObjectQuery() {
        return true;
    }

    /**
     * INTERNAL:
     * Return if the query is by primary key.
     */
    public boolean isPrimaryKeyQuery() {
        return (this.selectionId != null) || (this.selectionObject != null);
    }

    /**
     * PUBLIC:
     * Allow for the selection object of the query to be refreshed or put into the EclipseLink cache.
     * By default on a read or refresh the object in the cache is refreshed and returned or a new object is built from the database,
     * in some cases such as EJB BMP it is desirable to refresh or load into the object passed into the read object query.
     * <p>Note: This forces the selection object into the cache a replaces any existing object that may already be there,
     * this is a strict violation of object identity and other objects can still be referring to the old object.
     */
    public void loadResultIntoSelectionObject() {
        setShouldLoadResultIntoSelectionObject(true);
    }

    /**
     * INTERNAL:
     * Copy all setting from the query.
     * This is used to morph queries from one type to the other.
     * By default this calls prepareFromQuery, but additional properties may be required
     * to be copied as prepareFromQuery only copies properties that affect the SQL.
     */
    public void copyFromQuery(DatabaseQuery query) {
        super.copyFromQuery(query);
        if (query.isReadObjectQuery()) {
            ReadObjectQuery readQuery = (ReadObjectQuery)query;
            this.selectionId = readQuery.selectionId;
            this.selectionObject = readQuery.selectionObject;
            this.shouldLoadResultIntoSelectionObject = readQuery.shouldLoadResultIntoSelectionObject;
        }
    }

    /**
     * INTERNAL:
     * Prepare the receiver for execution in a session.
     */
    protected void prepare() throws QueryException {
        if (prepareFromCachedQuery()) {
            return;
        }
        super.prepare();

        if ((this.selectionId != null) || (this.selectionObject != null)) {
            // The expression is set in the prepare as params.
            setSelectionCriteria(this.descriptor.getObjectBuilder().getPrimaryKeyExpression());
            setExpressionBuilder(getSelectionCriteria().getBuilder());
            extendPessimisticLockScope();
            // For bug 2989998 the translation row is required to be set at this point.
            if (!shouldPrepare()) {
                if (this.selectionId != null) {
                    // Row must come from the key.
                    setTranslationRow(this.descriptor.getObjectBuilder().buildRowFromPrimaryKeyValues(this.selectionId, this.session));
                } else {//(getSelectionObject() != null)
                    setTranslationRow(this.descriptor.getObjectBuilder().buildRowForTranslation(this.selectionObject, this.session));
                }
            }
        }

        if (this.descriptor.isDescriptorForInterface()) {
            return;
        }

        // PERF: Disable cache check if not a primary key query.
        if (isExpressionQuery()) {
            Expression selectionCriteria = getSelectionCriteria();
            if (selectionCriteria != null) {
                if (((this.cacheUsage == CheckCacheByPrimaryKey)
                                && (!this.descriptor.getObjectBuilder().isPrimaryKeyExpression(false, selectionCriteria, this.session)))
                        || ((this.cacheUsage == CheckCacheByExactPrimaryKey)
                                && (!this.descriptor.getObjectBuilder().isPrimaryKeyExpression(true, selectionCriteria, this.session)))) {
                    this.cacheUsage = DoNotCheckCache;
                }
            }
        }

        // If using 1-m joining select all rows.
        if ((this.joinedAttributeManager != null) && this.joinedAttributeManager.isToManyJoin()) {
            getQueryMechanism().prepareSelectAllRows();
        } else {
            getQueryMechanism().prepareSelectOneRow();
        }

        // should be called after prepareSelectRow so that the call knows whether it returns ResultSet
        prepareResultSetAccessOptimization();
    }

    /**
     * INTERNAL:
     * Set the properties needed to be cascaded into the custom query including the translation row.
     * This is used only for primary key queries, as the descriptor query manager
     * stores a predefined query for this query to avoid having to re-prepare and allow for customization.
     */
    protected void prepareCustomQuery(DatabaseQuery customQuery) {
        super.prepareCustomQuery(customQuery);
        ReadObjectQuery customReadQuery = (ReadObjectQuery)customQuery;
        customReadQuery.shouldRefreshIdentityMapResult = this.shouldRefreshIdentityMapResult;
        customReadQuery.cascadePolicy = this.cascadePolicy;
        customReadQuery.shouldMaintainCache = this.shouldMaintainCache;
        customReadQuery.shouldUseWrapperPolicy = this.shouldUseWrapperPolicy;
        // CR... was missing some values, execution could cause infinite loop.
        customReadQuery.queryId = this.queryId;
        customReadQuery.executionTime = this.executionTime;
        customReadQuery.shouldLoadResultIntoSelectionObject = this.shouldLoadResultIntoSelectionObject;
        AbstractRecord primaryKeyRow;
        if (this.selectionObject != null) {
            // CR#... Must also set the selection object as may be loading into the object (refresh)
            customReadQuery.selectionObject = this.selectionObject;
            // The translation/primary key row will be set in prepareForExecution.
        } else if (this.selectionId != null) {
            customReadQuery.selectionId = this.selectionId;
        } else {
            // The primary key row must be used.
            primaryKeyRow = customQuery.getDescriptor().getObjectBuilder().extractPrimaryKeyRowFromExpression(getSelectionCriteria(), customQuery.getTranslationRow(), customReadQuery.getSession());
            customReadQuery.setTranslationRow(primaryKeyRow);
        }
    }

    /**
     * INTERNAL:
     * Prepare the receiver for execution in a session.
     */
    public void prepareForExecution() throws QueryException {
        super.prepareForExecution();

        // For bug 2989998 the translation row now sometimes set earlier in prepare.
        if (shouldPrepare()) {
            if (this.selectionId != null) {
                // Row must come from the key.
                this.translationRow = this.descriptor.getObjectBuilder().buildRowFromPrimaryKeyValues(this.selectionId, getSession());
            } else if (this.selectionObject != null) {
                // The expression is set in the prepare as params.
                this.translationRow = this.descriptor.getObjectBuilder().buildRowForTranslation(this.selectionObject, getSession());
            }
        }

        // If we have tenant discriminator fields we need to add them to the
        // database row when doing a primary key query.
        // Modifying the translation row here will modify it on the original
        // query which is not good (will append the tenant field to the sql call
        // for subsequent queries) The translation row must be cloned to isolate
        // this.
        if (getDescriptor().hasMultitenantPolicy()) {
            this.translationRow = this.translationRow.clone();
            getDescriptor().getMultitenantPolicy().addFieldsToRow(this.translationRow, getSession());
        }
    }

    /**
     * INTERNAL:
     * Prepare the receiver for execution in a session.
     */
    protected void prePrepare() throws QueryException {
        super.prePrepare();
        //Bug#3947714  In case getSelectionObject() is proxy
        if (getSelectionObject() != null && getSession().getProject().hasProxyIndirection()) {
            setSelectionObject(ProxyIndirectionPolicy.getValueFromProxy(getSelectionObject()));
        }
    }

    /**
     * INTERNAL:
     * All objects queried via a UnitOfWork get registered here.  If the query
     * went to the database.
     * <p>
     * Involves registering the query result individually and in totality, and
     * hence refreshing / conforming is done here.
     * @param result may be collection (read all) or an object (read one),
     * or even a cursor.  If in transaction the shared cache will
     * be bypassed, meaning the result may not be originals from the parent
     * but raw database rows.
     * @param unitOfWork the unitOfWork the result is being registered in.
     * @param arguments the original arguments/parameters passed to the query
     * execution.  Used by conforming
     * @param buildDirectlyFromRows If in transaction must construct
     * a registered result from raw database rows.
     * @return the final (conformed, refreshed, wrapped) UnitOfWork query result
     */
    public Object registerResultInUnitOfWork(Object result, UnitOfWorkImpl unitOfWork, AbstractRecord arguments, boolean buildDirectlyFromRows) {
        if (result == null) {
            return null;
        }
        if (unitOfWork.hasCloneMapping() // PERF: Avoid conforming empty uow.
                && (shouldConformResultsInUnitOfWork() || this.descriptor.shouldAlwaysConformResultsInUnitOfWork())) {
            return conformResult(result, unitOfWork, arguments, buildDirectlyFromRows);
        }
        Object clone = null;
        if (buildDirectlyFromRows) {
            clone = buildObject((AbstractRecord)result);
        } else {
            clone = registerIndividualResult(result, null, unitOfWork, null, null);
        }

        if (shouldUseWrapperPolicy()) {
            clone = this.descriptor.getObjectBuilder().wrapObject(clone, unitOfWork);
        }
        return clone;
    }

    protected Object remoteExecute() {
        // Do a cache lookup.
        checkDescriptor(session);
        // As the selection object is transient, compute the key.
        if (getSelectionObject() != null) {
            // Must be checked separately as the expression and row is not yet set.
            setSelectionId(getDescriptor().getObjectBuilder().extractPrimaryKeyFromObject(getSelectionObject(), session));
        }

        Object cacheHit = checkEarlyReturn(getSession(), getTranslationRow());
        if ((cacheHit != null) || shouldCheckCacheOnly()) {
            if (cacheHit == InvalidObject.instance) {
                return null;
            }
            return cacheHit;
        }

        return super.remoteExecute();
    }

    /**
     * INTERNAL:
     * replace the value holders in the specified result object(s)
     */
    public Map replaceValueHoldersIn(Object object, RemoteSessionController controller) {
        return controller.replaceValueHoldersIn(object);
    }

    /**
     * INTERNAL:
     * Return the primary key stored in this query
     *
     * @return the selection id of this ReadObjectQuery
     */
    @Override
    protected Object getQueryPrimaryKey(){
        return getSelectionId();
    }

    /**
     * PUBLIC:
     * Return Id of the object to be selected by the query.
     */
    public Object getSelectionId() {
        return this.selectionId;
    }

    /**
     * INTERNAL:
     * Clear the selection id and object.
     * This is done after cloning queries to prepare them in inheritance.
     */
    public void clearSelectionId() {
        this.selectionId = null;
        this.selectionObject = null;
    }

    /**
     * PUBLIC:
     * The Id of the object to be selected by the query.
     * This will generate a query by primary key.
     * This can be used instead of an Expression, SQL, or JPQL, or example object.
     * The Id is the Id value for a singleton primary key,
     * for a composite it is an instance of CacheId.
     * @see CacheId
     */
    public void setSelectionId(Object id) {
        this.selectionId = id;
    }

    /**
     * PUBLIC:
     * The primary key can be specified if used instead of an expression or selection object.
     * If composite the primary must be in the same order as defined in the descriptor.
     * @deprecated since EclipseLink 2.1, replaced by setSelectionId(Object)
     * @see #setSelectionId(Object)
     */
    @Deprecated
    public void setSelectionKey(List selectionKey) {
        if (selectionKey == null) {
            this.selectionId = null;
        } else if (selectionKey.size() == 1) {
            this.selectionId = selectionKey.get(0);
        } else {
            this.selectionId = new CacheId(selectionKey.toArray());
        }
    }

    /**
     * PUBLIC:
     * Used to set the where clause of the query.
     * This can be used instead of a where clause expression for single object primary key queries.
     * The selection object given should have a primary key defined,
     * this primary key will be used to query the database instance of the same object.
     * This is a basic form of query by example where only the primary key is required,
     * it can be used for simple query forms, or testing.
     */
    public void setSelectionObject(Object selectionObject) {
        if (selectionObject == null) {
            throw QueryException.selectionObjectCannotBeNull(this);
        }
        setSelectionId(null);
        // Check if the query needs to be unprepared.
        if ((this.selectionObject == null) || (this.selectionObject.getClass() != selectionObject.getClass())) {
            setIsPrepared(false);
        }
        setReferenceClass(selectionObject.getClass());
        this.selectionObject = selectionObject;
    }

    /**
     * PUBLIC:
     * Allow for the selection object of the query to be refreshed or put into the EclipseLink cache.
     * By default on a read or refresh the object in the cache is refreshed and returned or a new object is built from the database,
     * in some cases such as EJB BMP it is desirable to refresh or load into the object passed into the read object query.
     * <p>Note: This forces the selection object into the cache a replaces any existing object that may already be there,
     * this is a strict violation of object identity and other objects can still be referring to the old object.
     */
    public void setShouldLoadResultIntoSelectionObject(boolean shouldLoadResultIntoSelectionObject) {
        this.shouldLoadResultIntoSelectionObject = shouldLoadResultIntoSelectionObject;
    }

    /**
     * PUBLIC:
     * The primary key can be specified if used instead of an expression or selection object.
     * @deprecated since EclipseLink 2.1, replaced by setSelectionId(Object)
     * @see #setSelectionId(Object)
     */
    @Deprecated
    public void setSingletonSelectionKey(Object selectionKey) {
        Vector key = new NonSynchronizedVector();
        key.add(selectionKey);
        setSelectionKey(key);
    }

    /**
     * PUBLIC:
     * Return if cache should be checked.
     */
    public boolean shouldCheckCacheByExactPrimaryKey() {
        return this.cacheUsage == CheckCacheByExactPrimaryKey;
    }

    /**
     * PUBLIC:
     * Return if cache should be checked.
     */
    public boolean shouldCheckCacheByPrimaryKey() {
        return (this.cacheUsage == CheckCacheByPrimaryKey) || (this.cacheUsage == UseDescriptorSetting);
    }

    /**
     * PUBLIC:
     * Return if cache should be checked.
     */
    public boolean shouldCheckCacheThenDatabase() {
        return this.cacheUsage == CheckCacheThenDatabase;
    }

    /**
     * PUBLIC:
     * return true if the result should be loaded into the passed in selection Object
     */
    public boolean shouldLoadResultIntoSelectionObject() {
        return shouldLoadResultIntoSelectionObject;
    }

    /**
     * INTERNAL:
     * Return if the query has an non-default fetch group defined for itself.
     */
    protected boolean hasNonDefaultFetchGroup() {
        return this.descriptor.hasFetchGroupManager() && ((this.fetchGroup != null) || (this.fetchGroupName != null)
                || (!this.shouldUseDefaultFetchGroup && (this.descriptor.getFetchGroupManager().getDefaultFetchGroup() != null)));

    }
}
