/*
 * Copyright (c) 1998, 2019 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
//     10/15/2010-2.2 Guy Pelletier
//       - 322008: Improve usability of additional criteria applied to queries at the session/EM
//     04/01/2011-2.3 Guy Pelletier
//       - 337323: Multi-tenant with shared schema support (part 2)
//     05/24/2011-2.3 Guy Pelletier
//       - 345962: Join fetch query when using tenant discriminator column fails.
//     08/18/2011-2.3.1 Guy Pelletier
//       - 355093: Add new 'includeCriteria' flag to Multitenant metadata
//     09/09/2011-2.3.1 Guy Pelletier
//       - 356197: Add new VPD type to MultitenantType
//     08/01/2012-2.5 Chris Delahunt
//       - 371950: JPA Metadata caching
//     09/03/2015 - Will Dazey
//       - 456067 : Added support for defining query timeout units
//     IBM - Bug 537795: CASE THEN and ELSE scalar expression Constants should not be casted to CASE operand type
package org.eclipse.persistence.descriptors;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.databaseaccess.DatasourceCall;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.expressions.CompoundExpression;
import org.eclipse.persistence.internal.expressions.FunctionExpression;
import org.eclipse.persistence.internal.expressions.ParameterExpression;
import org.eclipse.persistence.internal.expressions.SubSelectExpression;
import org.eclipse.persistence.internal.helper.ConcurrentFixedCache;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.helper.NonSynchronizedVector;
import org.eclipse.persistence.internal.queries.ReportItem;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.ChangeRecord;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.queries.Call;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.DeleteObjectQuery;
import org.eclipse.persistence.queries.DoesExistQuery;
import org.eclipse.persistence.queries.InsertObjectQuery;
import org.eclipse.persistence.queries.JPAQueryBuilder;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.QueryResultsCachePolicy;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.queries.ReadQuery;
import org.eclipse.persistence.queries.ReportQuery;
import org.eclipse.persistence.queries.UpdateObjectQuery;
import org.eclipse.persistence.queries.WriteObjectQuery;

/**
 * <p><b>Purpose</b>: The query manager allows for the database operations that EclipseLink
 * performs to be customized by the application.  For each descriptor a query can be
 * given that controls how a operation will occur.  A common example is if the application
 * requires a stored procedure to be used to insert the object, it can override the SQL call
 * in the insert query that EclipseLink will use to insert the object.
 * Queries can be customized to extend EclipseLink behavior, access non-relational data or use stored
 * procedures or customized SQL calls.
 * <p>
 * The queries that can be customized include:
 * <ul>
 * <li> insertQuery - used to insert the object
 * <li> updateQuery - used to update the object
 * <li> readObjectQuery - used to read a single object by primary key
 * <li> readAllQuery - used to read all of the objects of the class
 * <li> doesExistQuery - used to determine whether an insert or update should occur
 * <li> deleteQuery - used to delete the object
 * </ul>
 *
 * @see ClassDescriptor
 */
public class DescriptorQueryManager implements Cloneable, Serializable {
    protected InsertObjectQuery insertQuery;
    protected UpdateObjectQuery updateQuery;
    protected ReadObjectQuery readObjectQuery;
    protected ReadAllQuery readAllQuery;
    protected DeleteObjectQuery deleteQuery;
    protected DoesExistQuery doesExistQuery;
    protected ClassDescriptor descriptor;
    protected boolean hasCustomMultipleTableJoinExpression;
    protected String additionalCriteria;
    protected transient Expression additionalJoinExpression;
    protected transient Expression multipleTableJoinExpression;
    protected Map<String, List<DatabaseQuery>> queries;
    protected transient Map<DatabaseTable, Expression> tablesJoinExpressions;
    /** PERF: Update call cache for avoiding regenerated update SQL. */
    protected transient ConcurrentFixedCache cachedUpdateCalls;
    /** PERF: Expression query call cache for avoiding regenerated dynamic query SQL. */
    protected transient ConcurrentFixedCache cachedExpressionQueries;

    /**
     * queryTimeout has three possible settings: DefaultTimeout, NoTimeout, and 1..N
     * This applies to both DatabaseQuery.queryTimeout and DescriptorQueryManager.queryTimeout
     *
     * DatabaseQuery.queryTimeout:
     * - DefaultTimeout: get queryTimeout from DescriptorQueryManager
     * - NoTimeout, 1..N: overrides queryTimeout in DescriptorQueryManager
     *
     * DescriptorQueryManager.queryTimeout:
     * - DefaultTimeout: get queryTimeout from parent DescriptorQueryManager. If there is no
     * parent, default to NoTimeout
     * - NoTimeout, 1..N: overrides parent queryTimeout
     */
    public static final int NoTimeout = 0;
    public static final int DefaultTimeout = -1;
    protected int queryTimeout;

    public static final TimeUnit DefaultTimeoutUnit = TimeUnit.MILLISECONDS;
    protected TimeUnit queryTimeoutUnit;

    /**
     * INTERNAL:
     * Initialize the state of the descriptor query manager
     */
    public DescriptorQueryManager() {
        this.queries = new LinkedHashMap(5);
        setDoesExistQuery(new DoesExistQuery());// Always has a does exist.
        this.setQueryTimeout(DefaultTimeout);
        this.setQueryTimeoutUnit(DefaultTimeoutUnit);
    }

    /**
     * ADVANCED:
     * Set the max size of the expression query cache for avoiding regenerated dynamic query SQL.
     */
    public void setExpressionQueryCacheMaxSize(int maxSize) {
        this.cachedExpressionQueries = new ConcurrentFixedCache(maxSize);
    }

    /**
     * ADVANCED:
     * Return the max size of the expression query cache for avoiding regenerated dynamic query SQL.
     */
    public int getExpressionQueryCacheMaxSize() {
        return getCachedExpressionQueries().getMaxSize();
    }

    /**
     * PUBLIC:
     * Add the query to the descriptor queries with the given name
     * @param name This is the name of the query.  It will be set on the query and used to look it up.
     * @param query This is the query that will be added.  If the query being added has parameters, the
     * existing list of queries will be checked for matching queries.  If a matching query exists,
     * it will be replaced.
     */
    public void addQuery(String name, DatabaseQuery query) {
        query.setName(name);
        addQuery(query);
    }

    /**
     * PUBLIC:
     * Add the query to the session queries
     * @param query DatabaseQuery This is the query that will be added.  If the query being added has parameters, the
     * existing list of queries will be checked for matching queries.  If a matching query exists,
     * it will be replaced.
     */
    public synchronized void addQuery(DatabaseQuery query) {
        if (query instanceof ObjectLevelReadQuery && (((ObjectLevelReadQuery)query).getReferenceClassName() == null)) {
            ((ObjectLevelReadQuery)query).setReferenceClassName(getDescriptor().getJavaClassName());

            // try to set the reference ClassNotFoundException since it should only happen on the MW in which
            // case we will lazily initialize the reference class at a later point.
            try {
                ((ObjectLevelReadQuery)query).setReferenceClass(getDescriptor().getJavaClass());
            } catch (ConversionException exception) {
            }

            //this is an optimization
            query.setDescriptor(getDescriptor());
        }

        // Add query has been synchronized for bug 3355199.
        // Additionally code has been added to ensure that the same query is not added twice.
        Vector queriesByName = (Vector)getQueries().get(query.getName());
        if (queriesByName == null) {
            // lazily create Vector in Hashtable.
            queriesByName = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance();
            getQueries().put(query.getName(), queriesByName);
        } else {
            int argumentTypesSize = 0;
            if (query.getArguments() != null) {
                argumentTypesSize = query.getArguments().size();
            }
            List<String> argumentTypes = new ArrayList(argumentTypesSize);
            for (int i = 0; i < argumentTypesSize; i++) {
                argumentTypes.add(query.getArgumentTypeNames().get(i));
            }

            // Search for a query with the same parameters and replace it if one is found
            for (int i = 0; i < queriesByName.size(); i++) {
                DatabaseQuery currentQuery = (DatabaseQuery)queriesByName.get(i);

                // Here we are checking equality instead of assignability.  If you look at getQuery()
                // it is the other way around.
                // The reason we do this is we are replacing a query and we want to make sure we are
                // replacing the exact same one. - TW
                if (argumentTypes.equals(currentQuery.getArgumentTypeNames())) {
                    queriesByName.set(i, query);
                    return;
                }
            }
        }
        queriesByName.add(query);
    }

    /**
     * PUBLIC:
     * Assume that if the objects primary key does not include null then it must exist.
     * This may be used if the application guarantees or does not care about the existence check.
     */
    public void assumeExistenceForDoesExist() {
        getDoesExistQuery().assumeExistenceForDoesExist();
    }

    /**
     * PUBLIC:
     * Assume that the object does not exist. This may be used if the application guarantees or
     * does not care about the existence check.  This will always force an insert to be called.
     */
    public void assumeNonExistenceForDoesExist() {
        getDoesExistQuery().assumeNonExistenceForDoesExist();
    }

    /**
     * PUBLIC:
     * Default behavior.
     * Assume that if the objects primary key does not include null and it
     * is in the cache, then is must exist.
     */
    public void checkCacheForDoesExist() {
        getDoesExistQuery().checkCacheForDoesExist();
    }

    /**
     * PUBLIC:
     * Perform does exist check on the database
     */
    public void checkDatabaseForDoesExist() {
        getDoesExistQuery().checkDatabaseForDoesExist();
    }

    /**
     * INTERNAL:
     * Clone the query manager
     */
    @Override
    public Object clone() {
        DescriptorQueryManager manager = null;
        try {
            manager = (DescriptorQueryManager)super.clone();
        } catch (Exception exception) {
            throw new AssertionError(exception);
        }

        // Bug 3037701 - clone the queries
        manager.setQueries(new LinkedHashMap(getQueries().size()));//bug5677655
        Iterator iterator = queries.values().iterator();
        while (iterator.hasNext()) {
            Iterator queriesForKey = ((Vector)iterator.next()).iterator();
            while (queriesForKey.hasNext()) {
                DatabaseQuery initialQuery = (DatabaseQuery)queriesForKey.next();
                DatabaseQuery clonedQuery = (DatabaseQuery)initialQuery.clone();
                clonedQuery.setDescriptor(manager.getDescriptor());
                manager.addQuery(clonedQuery);
            }
        }
        manager.setDoesExistQuery((DoesExistQuery)getDoesExistQuery().clone());
        if (getReadAllQuery() != null) {
            manager.setReadAllQuery((ReadAllQuery)getReadAllQuery().clone());
        }
        if (getReadObjectQuery() != null) {
            manager.setReadObjectQuery((ReadObjectQuery)getReadObjectQuery().clone());
        }
        if (getUpdateQuery() != null) {
            manager.setUpdateQuery((UpdateObjectQuery)getUpdateQuery().clone());
        }
        if (getInsertQuery() != null) {
            manager.setInsertQuery((InsertObjectQuery)getInsertQuery().clone());
        }
        if (getDeleteQuery() != null) {
            manager.setDeleteQuery((DeleteObjectQuery)getDeleteQuery().clone());
        }

        return manager;
    }

    /**
     * PUBLIC:
     * Return true if the query is defined on the session
     */
    public boolean containsQuery(String queryName) {
        return queries.containsKey(queryName);
    }

    /**
     * INTERNAL:
     * Convert all the class-name-based settings in this Query Manager to actual class-based
     * settings
     * This method is implemented by subclasses as necessary.
     * @param classLoader
     */
    public void convertClassNamesToClasses(ClassLoader classLoader){
        Iterator queryVectors = getQueries().values().iterator();
        while (queryVectors.hasNext()){
            Iterator queries = ((Vector)queryVectors.next()).iterator();;
            while (queries.hasNext()){
                ((DatabaseQuery)queries.next()).convertClassNamesToClasses(classLoader);
            }
        }
        if (getReadObjectQuery() != null) {
            getReadObjectQuery().convertClassNamesToClasses(classLoader);
        }
        if (getReadAllQuery() != null) {
            getReadAllQuery().convertClassNamesToClasses(classLoader);
        }
    };

    /**
     * ADVANCED:
     * Returns the join expression that should be appended to all of the descriptors expressions
     * Contains any multiple table or inheritance dependencies
     */
    public Expression getAdditionalJoinExpression() {
        return additionalJoinExpression;
    }

    /**
     * ADVANCED:
     * Return the receiver's delete query.
     * This should be an instance of a valid subclass of DeleteObjectQuery.
     * If specified this is used by the descriptor to delete itself and its private parts from the database.
     * This gives the user the ability to define exactly how to delete the data from the database,
     * or access data external from the database or from some other framework.
     */
    public DeleteObjectQuery getDeleteQuery() {
        return deleteQuery;
    }

    /**
     * ADVANCED:
     * Return the receiver's delete SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the source row,
     * through replacing the field names marked by '#' with the values for those fields.
     * <p>
     *    Example, "delete from EMPLOYEE where EMPLOYEE_ID = #EMPLOYEE_ID".
     */
    public String getDeleteSQLString() {
        if (getDeleteQuery() == null) {
            return null;
        }

        return getDeleteQuery().getSQLString();
    }

    /**
     * INTERNAL:
     * Return the descriptor associated with this descriptor query manager
     */
    public ClassDescriptor getDescriptor() {
        return descriptor;
    }

    /**
     * ADVANCED:
     * Return the receiver's  does exist query.
     * This should be an instance of a valid subclass of DoesExistQuery.
     * If specified this is used by the descriptor to query existence of an object in the database.
     * This gives the user the ability to define exactly how to query existence from the database,
     * or access data external from the database or from some other framework.
     */
    public DoesExistQuery getDoesExistQuery() {
        return doesExistQuery;
    }

    /**

     * ADVANCED:
     * Return the receiver's does exist SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with there own SQL or procedure call.
     * The arguments are translated from the fields of the source row, through replacing the field names marked by '#'
     * with the values for those fields.
     * This must return null if the object does not exist, otherwise return a database row.
     * <p>
     * Example, "select EMPLOYEE_ID from EMPLOYEE where EMPLOYEE_ID = #EMPLOYEE_ID".
     */
    public String getDoesExistSQLString() {
        if (getDoesExistQuery() == null) {
            return null;
        }

        return getDoesExistQuery().getSQLString();
    }

    /**
     * INTERNAL:
     * This method is explicitly used by the Builder only.
     */
    public String getExistenceCheck() {
        if (getDoesExistQuery().shouldAssumeExistenceForDoesExist()) {
            return "Assume existence";
        } else if (getDoesExistQuery().shouldAssumeNonExistenceForDoesExist()) {
            return "Assume non-existence";
        } else if (getDoesExistQuery().shouldCheckCacheForDoesExist()) {
            return "Check cache";
        } else if (getDoesExistQuery().shouldCheckDatabaseForDoesExist()) {
            return "Check database";
        } else {
            // Default.
            return "Check cache";
        }
    }

    /**
      * ADVANCED:
      * Return the receiver's insert query.
      * This should be an instance of a valid subclass of InsertObjectQuery.
      * If specified this is used by the descriptor to insert itself into the database.
      * If the receiver uses sequence numbers, this query must return the updated sequence value.
      * This gives the user the ability to define exactly how to insert the data into the database,
      * or access data external from the database or from some other framework.
      */
    public InsertObjectQuery getInsertQuery() {
        return insertQuery;
    }

    /**
     * ADVANCED:
     * Return the receiver's insert SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the source row,
     * through replacing the field names marked by '#' with the values for those fields.
     * <p>
     * Example, "insert into EMPLOYEE (F_NAME, L_NAME) values (#F_NAME, #L_NAME)".
     */
    public String getInsertSQLString() {
        if (getInsertQuery() == null) {
            return null;
        }

        return getInsertQuery().getSQLString();
    }

    /**
     * ADVANCED:
     * This is normally generated for descriptors that have multiple tables.
     * However, if the additional table does not reference the primary tables primary key,
     * this expression may be set directly.
     */
    public Expression getMultipleTableJoinExpression() {
        return multipleTableJoinExpression;
    }

    /**
     * PUBLIC:
     * Return the pre-defined queries for the descriptor.
     * The Map returned contains Lists of queries.
     *
     * @see #getAllQueries()
     */
    public Map<String, List<DatabaseQuery>> getQueries() {
        return queries;
    }

    /**
     * PUBLIC:
     * Return the pre-defined queries for the descriptor.  The Vector returned
     * contains all queries for this descriptor.
     *
     * @see #getQueries()
     */
    public Vector getAllQueries() {
        Vector allQueries = new Vector();
        for (Iterator vectors = getQueries().values().iterator(); vectors.hasNext();) {
            allQueries.addAll((Vector)vectors.next());
        }
        return allQueries;
    }

    /**
     * INTERNAL:
     * Set pre-defined queries for the descriptor.  Converts the Vector to a hashtable
     */
    public void setAllQueries(Vector vector) {
        for (Enumeration enumtr = vector.elements(); enumtr.hasMoreElements();) {
            addQuery((DatabaseQuery)enumtr.nextElement());
        }
    }

    /**
     * PUBLIC:
     * set the pre-defined queries for the descriptor.  Used to write out deployment XML
     */
    public void setQueries(Map map) {
        queries = map;
    }

    /**
     * PUBLIC:
     * Return the query name from the set of pre-defined queries
     * If only one query exists with this name, it will be returned.
     * If there are multiple queries of this name, this method will search for a query
     * with no arguments and return the first one it finds.
     *
     * @see #getQuery(String, Vector)
     */
    public DatabaseQuery getQuery(String queryName) {
        return getQuery(queryName, null);
    }

    /**
     * PUBLIC:
     * Return the query from the set of pre-defined queries with the given name and argument types.
     * This allows for common queries to be pre-defined, reused and executed by name.
     * This method should be used if the Session has multiple queries with the same name but
     * different arguments.
     * If only one query exists, it will be returned regardless of the arguments.
     * If multiple queries exist, the first query that has corresponding argument types will be returned
     *
     * @see #getQuery(String)
     */
    public DatabaseQuery getQuery(String name, Vector arguments) {
        DatabaseQuery query = getLocalQuery(name, arguments);

        // CR#3711: Check if a query with the same name exists for this descriptor.
        // If not, recursively check descriptors of parent classes.  If nothing is
        // found in parents, return null.
        if (query == null) {
            DatabaseQuery parentQuery = getQueryFromParent(name, arguments);
            if ((parentQuery != null) && parentQuery.isReadQuery()) {
                parentQuery = (DatabaseQuery)parentQuery.clone();
                ((ObjectLevelReadQuery)parentQuery).setReferenceClass(this.descriptor.getJavaClass());
                addQuery(name, parentQuery);
            }
            return parentQuery;
        }
        return query;
    }

    /**
     * INTENAL:
     * Return the query from the set of pre-defined queries with the given name and argument types.
     * This allows for common queries to be pre-defined, reused and executed by name.
     * Only returns those queries locally defined, not superclass's queries
     * If only one query exists, it will be returned regardless of the arguments.
     * If multiple queries exist, the first query that has corresponding argument types will be returned
     *
     * @see #getQuery(String)
     */
    public DatabaseQuery getLocalQuery(String name, Vector arguments) {
        Vector queries = (Vector)getQueries().get(name);

        if (queries == null) {
            return null;
        }

        // Short circuit the simple, most common case of only one query.
        if (queries.size() == 1) {
            return (DatabaseQuery)queries.firstElement();
        }

        // CR#3754; Predrag; mar 19/2002;
        // We allow multiple named queries with the same name but
        // different argument set; we can have only one query with
        // no arguments; Vector queries is not sorted;
        // When asked for the query with no parameters the
        // old version did return the first query - wrong:
        // return (DatabaseQuery) queries.firstElement();
        int argumentTypesSize = 0;
        if (arguments != null) {
            argumentTypesSize = arguments.size();
        }
        Vector argumentTypes = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(argumentTypesSize);
        for (int i = 0; i < argumentTypesSize; i++) {
            argumentTypes.addElement(arguments.elementAt(i).getClass());
        }
        return getLocalQueryByArgumentTypes(name, argumentTypes);

    }

    /**
     * INTERNAL:
     * Return the query from the set of pre-defined queries with the given name and argument types.
     * This allows for common queries to be pre-defined, reused and executed by name.
     * Only returns those queries locally defined, not superclass's queries
     * If only one query exists, it will be returned regardless of the arguments.
     * If multiple queries exist, the first query that has corresponding argument types will be returned
     *
     * @see #getQuery(String)
     */
    public DatabaseQuery getLocalQueryByArgumentTypes(String name, List argumentTypes) {
        List<DatabaseQuery> queries = getQueries().get(name);

        if (queries == null) {
            return null;
        }

        // Short circuit the simple, most common case of only one query.
        if (queries.size() == 1) {
            return queries.get(0);
        }

        for (DatabaseQuery query : queries) {
            // BUG#2698755
            // This check was backward, we default the type to Object
            // Was checking Object is descendant of String not other way.
            if (Helper.areTypesAssignable(query.getArgumentTypes(), argumentTypes)) {
                return query;
            }
        }

        return null;

    }

    /**
     * INTERNAL:
     * CR#3711: Check if the class for this descriptor has a parent class.
     * Then search this parent's descriptor for a query with the same name
     * and arguments.  If nothing found, return null.
     *
     * This method should only be used recursively by getQuery().
     */
    protected DatabaseQuery getQueryFromParent(String name, Vector arguments) {
        ClassDescriptor descriptor = this.descriptor;
        if (descriptor.hasInheritance()) {
            InheritancePolicy inheritancePolicy = descriptor.getInheritancePolicy();
            ClassDescriptor parent = inheritancePolicy.getParentDescriptor();

            // if parent exists, check for the query
            if (parent != null) {
                return parent.getQueryManager().getQuery(name, arguments);
            }
        }
        return null;
    }

    /**
      * ADVANCED:
      * Return the receiver's read query.
      * This should be an instance of a valid subclass of ReadAllQuery.
      */
    public ReadAllQuery getReadAllQuery() {
        return readAllQuery;
    }

    /**
     * ADVANCED:
     * Return the receiver's read SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the read arguments row,
     * through replacing the field names marked by '#' with the values for those fields.
     * Note that this is only used on readAllObjects(Class), and not when an expression is provided.
     * <p>
     * Example, "select * from EMPLOYEE"
     */
    public String getReadAllSQLString() {
        if (getReadAllQuery() == null) {
            return null;
        }

        return getReadAllQuery().getSQLString();
    }

    /**
     * ADVANCED:
     * Return the receiver's read query.
     * This should be an instance of a valid subclass of ReadObjectQuery.
     * If specified this is used by the descriptor to read itself from the database.
     * The read arguments must be the primary key of the object only.
     * This gives the user the ability to define exactly how to read the object from the database,
     * or access data external from the database or from some other framework.
     */
    public ReadObjectQuery getReadObjectQuery() {
        return readObjectQuery;
    }

    /**
     * ADVANCED:
     * Return the receiver's read SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the read arguments row,
     * through replacing the field names marked by '#' with the values for those fields.
     * This must accept only the primary key of the object as arguments.
     * <p>
     * Example, "select * from EMPLOYEE where EMPLOYEE_ID = #EMPLOYEE_ID"
     */
    public String getReadObjectSQLString() {
        if (getReadObjectQuery() == null) {
            return null;
        }

        return getReadObjectQuery().getSQLString();
    }

    /**
      * ADVANCED:
      * Return the receiver's update query.
      * This should be an instance of a valid subclass of UpdateObjectQuery.
      * If specified this is used by the descriptor to insert itself into the database.
      * If the receiver uses optimistic locking this must raise an error on optimistic lock failure.
      * This gives the user the ability to define exactly how to update the data into the database,
      * or access data external from the database or from some other framework.
      */
    public UpdateObjectQuery getUpdateQuery() {
        return updateQuery;
    }

    /**
     * ADVANCED:
     * Return the receiver's update SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with there own SQL or procedure call.
     * The arguments are translated from the fields of the source row,
     * through replacing the field names marked by '#' with the values for those fields.
     * This must check the optimistic lock field and raise an error on optimistic lock failure.
     * <p>
     * Example, "update EMPLOYEE set F_NAME to #F_NAME, L_NAME to #L_NAME where EMPLOYEE_ID = #EMPLOYEE_ID".
     */
    public String getUpdateSQLString() {
        if (getUpdateQuery() == null) {
            return null;
        }

        return getUpdateQuery().getSQLString();
    }

    /**
     * ADVANCED:
     * Return true if an additional criteria has been set on this query manager.
     */
    public boolean hasAdditionalCriteria() {
        return additionalCriteria != null;
    }

    /**
     * INTERNAL:
     * Return if a custom join expression is used.
     */
    public boolean hasCustomMultipleTableJoinExpression() {
        return hasCustomMultipleTableJoinExpression;
    }

    /**
     * INTERNAL:
     * Flag that specifies if a delete query is available
     */
    public boolean hasDeleteQuery() {
        return (deleteQuery != null);
    }

    /**
     * INTERNAL:
     * Flag that specifies if a does exist query is available
     */
    public boolean hasDoesExistQuery() {
        return (doesExistQuery != null);
    }

    /**
     * INTERNAL:
     * Flag that specifies if a insert query is available
     */
    public boolean hasInsertQuery() {
        return (insertQuery != null);
    }

    /**
     * INTERNAL:
     * Flag that specifies if a read all query is available
     */
    public boolean hasReadAllQuery() {
        return (readAllQuery != null);
    }

    /**
     * INTERNAL:
     * Flag that specifies if a read object  query is available
     */
    public boolean hasReadObjectQuery() {
        return (readObjectQuery != null);
    }

    /**
     * INTERNAL:
     * Flag that specifies if a update query is available
     */
    public boolean hasUpdateQuery() {
        return (updateQuery != null);
    }

    /**
     * INTERNAL:
     * populate the queries with the descriptor.
     */
    private void populateQueries() {

        /* CR2260
         * Description:
         *   NullPointerException accessing null descriptor
         * Fix:
         *   Initialize queries with an instantiated descriptor at this point
         */
        if (getInsertQuery() != null) {
            getInsertQuery().setDescriptor(descriptor);
        }
        if (getUpdateQuery() != null) {
            getUpdateQuery().setDescriptor(descriptor);
        }
        if (getReadObjectQuery() != null) {
            getReadObjectQuery().setReferenceClass(getDescriptor().getJavaClass());
            getReadObjectQuery().setDescriptor(descriptor);
        }
        if (getDeleteQuery() != null) {
            getDeleteQuery().setDescriptor(descriptor);
        }
        if (getReadAllQuery() != null) {
            getReadAllQuery().setReferenceClass(getDescriptor().getJavaClass());
            getReadAllQuery().setDescriptor(descriptor);
        }
        for (Iterator it = getAllQueries().iterator(); it.hasNext();) {
            ((DatabaseQuery)it.next()).setDescriptor(descriptor);
        }
    }

    /**
     * INTERNAL:
     * Post initialize the mappings
     */
    public void initialize(AbstractSession session) {
        this.initializeQueryTimeout(session);

        if (getDescriptor().isAggregateDescriptor()) {
            return;
        }

        if (getMultipleTableJoinExpression() != null) {
            // Combine new multiple table expression to additional join expression
            setAdditionalJoinExpression(getMultipleTableJoinExpression().and(getAdditionalJoinExpression()));
        }

        if (getDescriptor().isAggregateCollectionDescriptor()) {
            return;
        }

        // Configure default query cache for all named queries.
        QueryResultsCachePolicy defaultQueryCachePolicy = session.getProject().getDefaultQueryResultsCachePolicy();
        if (defaultQueryCachePolicy != null && !getDescriptor().getCachePolicy().isIsolated()) {
            for (List<DatabaseQuery> queries : getQueries().values()) {
                for (DatabaseQuery query : queries) {
                    if (query.isReadQuery()) {
                        ReadQuery readQuery = (ReadQuery)query;
                        if (!readQuery.shouldCacheQueryResults()) {
                            readQuery.setQueryResultsCachePolicy(defaultQueryCachePolicy.clone());
                        }
                    }
                }
            }
        }

        getDescriptor().initialize(this, session);
    }

    /**
     * INTERNAL:
     * Initialize the queryTimeout to:
     *
     * NoTimeout: If queryTimeout is DefaultTimeout, either directly or via inheritance.
     * Parent's Timeout: If queryTimeout is something other than DefaultTimeout via my parent.
     */
    public void initializeQueryTimeout(AbstractSession session) {
        //if queryTimeout is DefaultTimeout, try to get my parent's queryTimeout
        if (getQueryTimeout() == DefaultTimeout) {
            if (getDescriptor().hasInheritance() && (this.getDescriptor().getInheritancePolicy().getParentDescriptor() != null)) {
                setQueryTimeout(this.getParentDescriptorQueryManager().getQueryTimeout());
            }
        }
        if (getQueryTimeoutUnit().equals(DefaultTimeoutUnit)) {
            if (getDescriptor().hasInheritance() && (this.getDescriptor().getInheritancePolicy().getParentDescriptor() != null)) {
                setQueryTimeoutUnit(this.getParentDescriptorQueryManager().getQueryTimeoutUnit());
            }
        }

        //if I have DefaultTimeout (via parent or not), set to NoTimeout
        if (getQueryTimeout() == DefaultTimeout) {
            setQueryTimeout(NoTimeout);
        }
    }

    /**
     * INTERNAL:
     * Get the parent DescriptorQueryManager.
     * Caution must be used in using this method as it expects the descriptor
     * to have inheritance.
     * Calling this when the descriptor that does not use inheritance will cause problems, #hasInheritance() must
     * always first be called.
     */
    public DescriptorQueryManager getParentDescriptorQueryManager() {
        return this.descriptor.getInheritancePolicy().getParentDescriptor().getQueryManager();
    }

    /**
     * INTERNAL:
     * Execute the post delete operation for the query
     */
    public void postDelete(DeleteObjectQuery query) {
        ObjectBuilder builder = this.descriptor.getObjectBuilder();
        // PERF: Only process relationships.
        if (!builder.isSimple()) {
            List<DatabaseMapping> mappings = builder.getRelationshipMappings();
            int size = mappings.size();
            for (int index = 0; index < size; index++) {
                mappings.get(index).postDelete(query);
            }
        }
    }

    /**
     * INTERNAL:
     * Post initializations after mappings are initialized.
     */
    public void postInitialize(AbstractSession session) throws DescriptorException {
        // If the additional criteria is specified, append it to the additional
        // join expression. We do this in postInitialize after all the mappings
        // have been fully initialized.
        if (additionalCriteria != null) {
            if (getDescriptor().hasInheritance() && getDescriptor().getInheritancePolicy().hasView()) {
                throw DescriptorException.additionalCriteriaNotSupportedWithInheritanceViews(getDescriptor());
            }

            JPAQueryBuilder queryBuilder = session.getQueryBuilder();
            Expression selectionCriteria = queryBuilder.buildSelectionCriteria(
               getDescriptor().getAlias(),
               additionalCriteria,
               session
            );

            updatePropertyParameterExpression(selectionCriteria);
            additionalJoinExpression = selectionCriteria.and(additionalJoinExpression);
        }

        if (additionalJoinExpression != null) {
            // The make sure the additional join expression has the correct
            // context, rebuild the additional join expression on a new
            // expression builder.
            additionalJoinExpression = additionalJoinExpression.rebuildOn(new ExpressionBuilder());
        }
    }

    /**
     * INTERNAL:
     * This method will walk the given expression and mark any parameter
     * expressions as property expressions. This is done when additional
     * criteria has been specified and parameter values must be resolved
     * through session properties.
     *
     * @see #postInitialize
     */
    protected void updatePropertyParameterExpression(Expression exp) {
        if (exp.isCompoundExpression()) {
            updatePropertyParameterExpression(((CompoundExpression) exp).getFirstChild());
            updatePropertyParameterExpression(((CompoundExpression) exp).getSecondChild());
        } else if (exp.isFunctionExpression()) {
        	for (Expression e : ((FunctionExpression) exp).getChildren()) {
                updatePropertyParameterExpression(e);
            }
        } else if (exp.isSubSelectExpression()) {
            ReportQuery subSelectQuery = ((SubSelectExpression) exp).getSubQuery();
            for (ReportItem item : subSelectQuery.getItems()) {
                updatePropertyParameterExpression(item.getAttributeExpression());
            }
        }

        if (exp.isParameterExpression()) {
            ((ParameterExpression) exp).setIsProperty(true);
        }
    }

    /**
     * INTERNAL:
     * Execute the post insert operation for the query
     */
    public void postInsert(WriteObjectQuery query) {
        ObjectBuilder builder = this.descriptor.getObjectBuilder();
        // PERF: Only process relationships.
        if (!builder.isSimple()) {
            List<DatabaseMapping> mappings = builder.getRelationshipMappings();
            int size = mappings.size();
            for (int index = 0; index < size; index++) {
                mappings.get(index).postInsert(query);
            }
        }
    }

    /**
     * INTERNAL:
     * Execute the post update operation for the query
     */
    public void postUpdate(WriteObjectQuery query) {
        ObjectBuilder builder = this.descriptor.getObjectBuilder();
        // PERF: Only process relationships.
        if (!builder.isSimple()) {
            // PERF: Only process changed mappings.
            ObjectChangeSet changeSet = query.getObjectChangeSet();
            if ((changeSet != null) && (!changeSet.isNew())) {
                List changeRecords = changeSet.getChanges();
                int size = changeRecords.size();
                for (int index = 0; index < size; index++) {
                    ChangeRecord record = (ChangeRecord)changeRecords.get(index);
                    record.getMapping().postUpdate(query);
                }
            } else {
                List<DatabaseMapping> mappings = builder.getRelationshipMappings();
                int size = mappings.size();
                for (int index = 0; index < size; index++) {
                    mappings.get(index).postUpdate(query);
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Execute the pre delete operation for the query
     */
    public void preDelete(DeleteObjectQuery query) {
        ObjectBuilder builder = this.descriptor.getObjectBuilder();
        // PERF: Only process relationships.
        if (!builder.isSimple()) {
            List<DatabaseMapping> mappings = builder.getRelationshipMappings();
            int size = mappings.size();
            for (int index = 0; index < size; index++) {
                mappings.get(index).preDelete(query);
            }
        }
    }

    /**
     * INTERNAL:
     * Initialize the query manager.
     * Any custom queries must be inherited from the parent before any initialization.
     */
    public void preInitialize(AbstractSession session) {
        if (getDescriptor().isAggregateDescriptor()) {
            return;
        }

        // Must inherit parent query customization if not redefined.
        if (getDescriptor().isChildDescriptor()) {
            DescriptorQueryManager parentQueryManager = getDescriptor().getInheritancePolicy().getParentDescriptor().getQueryManager();

            if ((!hasInsertQuery()) && (parentQueryManager.hasInsertQuery())) {
                setInsertQuery((InsertObjectQuery)parentQueryManager.getInsertQuery().clone());
            }
            if ((!hasUpdateQuery()) && (parentQueryManager.hasUpdateQuery())) {
                setUpdateQuery((UpdateObjectQuery)parentQueryManager.getUpdateQuery().clone());
            }
            if ((!hasDeleteQuery()) && (parentQueryManager.hasDeleteQuery())) {
                setDeleteQuery((DeleteObjectQuery)parentQueryManager.getDeleteQuery().clone());
            }
            if ((!hasReadObjectQuery()) && (parentQueryManager.hasReadObjectQuery())) {
                setReadObjectQuery((ReadObjectQuery)parentQueryManager.getReadObjectQuery().clone());
            }
            if ((!hasReadAllQuery()) && (parentQueryManager.hasReadAllQuery())) {
                setReadAllQuery((ReadAllQuery)parentQueryManager.getReadAllQuery().clone());
            }
            if ((!getDoesExistQuery().isUserDefined()) && getDoesExistQuery().shouldCheckCacheForDoesExist()) {
                setDoesExistQuery(((DoesExistQuery)parentQueryManager.getDoesExistQuery().clone()));
            }
        }
    }

    /**
     * INTERNAL:
     * Execute the pre insert  operation for the query.
     */
    public void preInsert(WriteObjectQuery query) {
        ObjectBuilder builder = this.descriptor.getObjectBuilder();
        // PERF: Only process relationships.
        if (!builder.isSimple()) {
            List<DatabaseMapping> mappings = builder.getRelationshipMappings();
            int size = mappings.size();
            for (int index = 0; index < size; index++) {
                mappings.get(index).preInsert(query);
            }
        }
    }

    /**
     * INTERNAL:
     * Execute the pre update operation for the query
     */
    public void preUpdate(WriteObjectQuery query) {
        ObjectBuilder builder = this.descriptor.getObjectBuilder();
        // PERF: Only process relationships.
        if (!builder.isSimple()) {
            // PERF: Only process changed mappings.
            ObjectChangeSet changeSet = query.getObjectChangeSet();
            if ((changeSet != null) && (!changeSet.isNew())) {
                List changeRecords = changeSet.getChanges();
                int size = changeRecords.size();
                for (int index = 0; index < size; index++) {
                    ChangeRecord record = (ChangeRecord)changeRecords.get(index);
                    record.getMapping().preUpdate(query);
                }
            } else {
                List<DatabaseMapping> mappings = builder.getRelationshipMappings();
                int size = mappings.size();
                for (int index = 0; index < size; index++) {
                    mappings.get(index).preUpdate(query);
                }
            }
        }
    }

    /**
     * PUBLIC:
     * Remove all queries with the given query name from the set of pre-defined queries
     *
     * @see #removeQuery(String, Vector)
     */
    public void removeQuery(String queryName) {
        queries.remove(queryName);
    }

    /**
     * PUBLIC:
     * Remove the specific query with the given queryName and argumentTypes.
     *
     * @see #removeQuery(String)
     */
    public void removeQuery(String queryName, Vector argumentTypes) {
        Vector queries = (Vector)getQueries().get(queryName);
        if (queries == null) {
            return;
        } else {
            DatabaseQuery query = null;
            for (Enumeration enumtr = queries.elements(); enumtr.hasMoreElements();) {
                query = (DatabaseQuery)enumtr.nextElement();
                if (Helper.areTypesAssignable(argumentTypes, query.getArgumentTypes())) {
                    break;
                }
            }
            if (query != null) {
                queries.remove(query);
            }
        }
    }

    /**
     * ADVANCED:
     * Set the additional join criteria that will be used to form the additional
     * join expression. The additionalCriteria is a jpql fragment at this point.
     * @see #setAdditionalJoinExpression
     */
    public void setAdditionalCriteria(String additionalCriteria) {
        this.additionalCriteria = additionalCriteria;
    }

    /**
     * ADVANCED:
     * Set the additional join expression. Used in conjunction with
     * multiple tables and inheritance relationships.
     * This can also be used if a sub-expression is always required to be
     * appended to all queries.  Such as tables that are shared based on a type field
     * without inheritance.
     */
    public void setAdditionalJoinExpression(Expression additionalJoinExpression) {
        this.additionalJoinExpression = additionalJoinExpression;
    }

    /**
     * ADVANCED:
     * Set the receiver's delete query.
     * This should be an instance of a valid subclass of DeleteObjectQuery.
     * If specified this is used by the descriptor to delete itself and its private parts from the database.
     * This gives the user the ability to define exactly how to delete the data from the database,
     * or access data external from the database or from some other framework.
     */
    public void setDeleteQuery(DeleteObjectQuery query) {
        this.deleteQuery = query;
        if (query == null) {
            return;
        }
        query.setIsUserDefined(true);
        query.setDescriptor(getDescriptor());
        if (query.isCallQuery()) {
            query.setIsFullRowRequired(true);
        }
    }

    /**
     * ADVANCED:
     * Set the receiver's delete SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the source row,
     * through replacing the field names marked by '#' with the values for those fields.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     * <p>
     *    Example, "delete from EMPLOYEE where EMPLOYEE_ID = #EMPLOYEE_ID".
     */
    public void setDeleteSQLString(String sqlString) {
        if (sqlString == null) {
            return;
        }

        DeleteObjectQuery query = new DeleteObjectQuery();
        query.setSQLString(sqlString);
        setDeleteQuery(query);
    }

    /**
     * ADVANCED:
     * Set the receiver's delete call.
     * This allows the user to override the delete operation.
     */
    public void setDeleteCall(Call call) {
        if (call == null) {
            return;
        }
        DeleteObjectQuery query = new DeleteObjectQuery();
        query.setCall(call);
        setDeleteQuery(query);
    }

    /**
     * INTERNAL:
     * Set the descriptor.
     */
    public void setDescriptor(ClassDescriptor descriptor) {
        this.descriptor = descriptor;
        //Gross alert: This is for the case when we are reading from XML, and
        //we have to compensate for no descriptor available at read time.  - JL
        populateQueries();

    }

    /**
     * ADVANCED:
     * Set the receiver's  does exist query.
     * This should be an instance of a valid subclass of DoesExistQuery.
     * If specified this is used by the descriptor to query existence of an object in the database.
     * This gives the user the ability to define exactly how to query existence from the database,
     * or access data external from the database or from some other framework.
     */
    public void setDoesExistQuery(DoesExistQuery query) {
        this.doesExistQuery = query;
        if (query == null) {
            return;
        }
        this.doesExistQuery.setIsUserDefined(true);
        this.doesExistQuery.setDescriptor(getDescriptor());
    }

    /**
     * ADVANCED:
     * Set the receiver's does exist SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with there own SQL or procedure call.
     * The arguments are translated from the fields of the source row, through replacing the field names marked by '#'
     * with the values for those fields.
     * This must return null if the object does not exist, otherwise return a database row.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     * <p>
     * Example, "select EMPLOYEE_ID from EMPLOYEE where EMPLOYEE_ID = #EMPLOYEE_ID".
     */
    public void setDoesExistSQLString(String sqlString) {
        if (sqlString == null) {
            return;
        }
        getDoesExistQuery().setSQLString(sqlString);
        getDoesExistQuery().checkDatabaseForDoesExist();
    }

    /**
     * ADVANCED:
     * Set the receiver's does exist call.
     * This allows the user to override the does exist operation.
     */
    public void setDoesExistCall(Call call) {
        if (call == null) {
            return;
        }
        getDoesExistQuery().setCall(call);
    }

    /**
     * INTERNAL:
     * This method is explicitly used by the Builder only.
     */
    public void setExistenceCheck(String token) throws DescriptorException {
        if (token.equals("Check cache")) {
            checkCacheForDoesExist();
        } else if (token.equals("Check database")) {
            checkDatabaseForDoesExist();
        } else if (token.equals("Assume existence")) {
            assumeExistenceForDoesExist();
        } else if (token.equals("Assume non-existence")) {
            assumeNonExistenceForDoesExist();
        } else {
            throw DescriptorException.setExistenceCheckingNotUnderstood(token, getDescriptor());
        }
    }

    /**
     * INTENAL:
     * Set if a custom join expression is used.
     */
    protected void setHasCustomMultipleTableJoinExpression(boolean hasCustomMultipleTableJoinExpression) {
        this.hasCustomMultipleTableJoinExpression = hasCustomMultipleTableJoinExpression;
    }

    /**
     * ADVANCED:
     * Set the receiver's insert query.
     * This should be an instance of a valid subclass of InsertObjectQuery.
     * If specified this is used by the descriptor to insert itself into the database.
     * This gives the user the ability to define exactly how to insert the data into the database,
     * or access data external from the database or from some other framework.
     */
    public void setInsertQuery(InsertObjectQuery insertQuery) {
        this.insertQuery = insertQuery;
        if (insertQuery == null) {
            return;
        }
        this.insertQuery.setIsUserDefined(true);
        this.insertQuery.setDescriptor(getDescriptor());
    }

    /**
     * ADVANCED:
     * Set the receiver's insert call.
     * This allows the user to override the insert operation.
     */
    public void setInsertCall(Call call) {
        if (call == null) {
            return;
        }
        InsertObjectQuery query = new InsertObjectQuery();
        query.setCall(call);
        setInsertQuery(query);
    }

    /**
     * ADVANCED:
     * Set the receiver's insert SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the source row,
     * through replacing the field names marked by '#' with the values for those fields.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     * <p>
     * Example, "insert into EMPLOYEE (F_NAME, L_NAME) values (#F_NAME, #L_NAME)".
     */
    public void setInsertSQLString(String sqlString) {
        if (sqlString == null) {
            return;
        }

        InsertObjectQuery query = new InsertObjectQuery();
        query.setSQLString(sqlString);
        setInsertQuery(query);
    }

    /**
     * ADVANCED:
     * Return the receiver's insert call.
     * This allows the user to override the insert operation.
     */
    public Call getInsertCall() {
        if (getInsertQuery() == null) {
            return null;
        }
        return getInsertQuery().getDatasourceCall();
    }

    /**
     * ADVANCED:
     * Return the receiver's update call.
     * This allows the user to override the update operation.
     */
    public Call getUpdateCall() {
        if (getUpdateQuery() == null) {
            return null;
        }
        return getUpdateQuery().getDatasourceCall();
    }

    /**
     * ADVANCED:
     * Return the receiver's delete call.
     * This allows the user to override the delete operation.
     */
    public Call getDeleteCall() {
        if (getDeleteQuery() == null) {
            return null;
        }
        return getDeleteQuery().getDatasourceCall();
    }

    /**
     * ADVANCED:
     * Return the receiver's read-object call.
     * This allows the user to override the read-object operation.
     */
    public Call getReadObjectCall() {
        if (getReadObjectQuery() == null) {
            return null;
        }
        return getReadObjectQuery().getDatasourceCall();
    }

    /**
     * ADVANCED:
     * Return the receiver's read-all call.
     * This allows the user to override the read-all operation.
     */
    public Call getReadAllCall() {
        if (getReadAllQuery() == null) {
            return null;
        }
        return getReadAllQuery().getDatasourceCall();
    }

    /**
     * ADVANCED:
     * Return the receiver's does-exist call.
     * This allows the user to override the does-exist operation.
     */
    public Call getDoesExistCall() {
        if (getDoesExistQuery() == null) {
            return null;
        }
        return getDoesExistQuery().getDatasourceCall();
    }

    /**
     * INTERNAL:
     * Used in case descriptor has additional tables:
     * each additional table mapped to an expression joining it.
     */
    public Map<DatabaseTable, Expression> getTablesJoinExpressions() {
        if (tablesJoinExpressions == null) {
            tablesJoinExpressions = new HashMap<DatabaseTable, Expression>();
        }
        return tablesJoinExpressions;
    }

    /**
     * INTERNAL:
     * Used to set the multiple table join expression that was generated by EclipseLink as opposed
     * to a custom one supplied by the user.
     * @see #setMultipleTableJoinExpression(Expression)
     */
    public void setInternalMultipleTableJoinExpression(Expression multipleTableJoinExpression) {
        this.multipleTableJoinExpression = multipleTableJoinExpression;
    }

    /**
     * ADVANCED:
     * This is normally generated for descriptors that have multiple tables.
     * However, if the additional table does not reference the primary table's primary key,
     * this expression may be set directly.
     */
    public void setMultipleTableJoinExpression(Expression multipleTableJoinExpression) {
        this.multipleTableJoinExpression = multipleTableJoinExpression;
        setHasCustomMultipleTableJoinExpression(true);
    }

    /**
     * ADVANCED:
     * Set the receiver's read all query.
     * This should be an instance of a valid subclass of ReadAllQuery.
     * If specified this is used by the descriptor to read all instances of its class from the database.
     * This gives the user the ability to define exactly how to read all objects from the database,
     * or access data external from the database or from some other framework.
     * Note that this is only used on readAllObjects(Class), and not when an expression is provided.
     */
    public void setReadAllQuery(ReadAllQuery query) {
        this.readAllQuery = query;
        if (query == null) {
            return;
        }

        this.readAllQuery.setIsUserDefined(true);

        /* CR2260 - Steven Vo
         * Description:
         *  NullPointerException accessing null descriptor
         * Fix:
         *   Setting query's descriptor and reference class when descriptor is not null.
         *   Otherwise, wait until the descriptor is set.See populateQueries() that is
         *   called by setDescriptor()
         */
        if (this.getDescriptor() != null) {
            this.readAllQuery.setDescriptor(getDescriptor());
            this.readAllQuery.setReferenceClassName(getDescriptor().getJavaClassName());
            try {
                readAllQuery.setReferenceClass(getDescriptor().getJavaClass());
            } catch (ConversionException exception) {
            }
        }
    }

    /**
     * ADVANCED:
     * Set the receiver's read SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the read arguments row,
     * through replacing the field names marked by '#' with the values for those fields.
     * Note that this is only used on readAllObjects(Class), and not when an expression is provided.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     * <p>
     * Example, "select * from EMPLOYEE"
     */
    public void setReadAllSQLString(String sqlString) {
        if (sqlString == null) {
            return;
        }

        ReadAllQuery query = new ReadAllQuery();
        query.setSQLString(sqlString);
        setReadAllQuery(query);
    }

    /**
     * ADVANCED:
     * Set the receiver's read all call.
     * This allows the user to override the read all operation.
     * Note that this is only used on readAllObjects(Class), and not when an expression is provided.
     */
    public void setReadAllCall(Call call) {
        if (call == null) {
            return;
        }
        ReadAllQuery query = new ReadAllQuery();
        query.setCall(call);
        setReadAllQuery(query);
    }

    /**
     * ADVANCED:
     * Set the receiver's read query.
     * This should be an instance of a valid subclass of ReadObjectQuery
     * If specified this is used by the descriptor to read itself from the database.
     * The read arguments must be the primary key of the object only.
     * This gives the user the ability to define exactly how to read the object from the database,
     * or access data external from the database or from some other framework.
     */
    public void setReadObjectQuery(ReadObjectQuery query) {
        this.readObjectQuery = query;
        if (query == null) {
            return;
        }
        this.readObjectQuery.setIsUserDefined(true);

        /* CR2260 - Steven Vo
         * Description:
         *  NullPointerException accessing null descriptor
         * Fix:
         *   Setting query's descriptor and reference class when descriptor is not null.
         *   Otherwise, wait until the descriptor is set.See populateQueries() that is
         *   called by setDescriptor()
         */
        if (this.getDescriptor() != null) {
            this.readObjectQuery.setDescriptor(getDescriptor());
            this.readObjectQuery.setReferenceClassName(getDescriptor().getJavaClassName());
            try {
                readObjectQuery.setReferenceClass(getDescriptor().getJavaClass());
            } catch (ConversionException exception) {
            }
        }
    }

    /**
     * ADVANCED:
     * Set the receiver's read SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with their own SQL or procedure call.
     * The arguments are translated from the fields of the read arguments row,
     * through replacing the field names marked by '#' with the values for those fields.
     * This must accept only the primary key of the object as arguments.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     * <p>
     * Example, "select * from EMPLOYEE where EMPLOYEE_ID = #EMPLOYEE_ID"
     */
    public void setReadObjectSQLString(String sqlString) {
        if (sqlString == null) {
            return;
        }

        ReadObjectQuery query = new ReadObjectQuery();
        query.setSQLString(sqlString);
        setReadObjectQuery(query);
    }

    /**
     * ADVANCED:
     * Set the receiver's read object call.
     * This allows the user to override the read object operation.
     * This must accept only the primary key of the object as arguments.
     */
    public void setReadObjectCall(Call call) {
        if (call == null) {
            return;
        }
        ReadObjectQuery query = new ReadObjectQuery();
        query.setCall(call);
        setReadObjectQuery(query);
    }

    /**
     * ADVANCED:
     * Set the receiver's update query.
     * This should be an instance of a valid subclass of UpdateObjectQuery.
     * If specified this is used by the descriptor to update itself in the database.
     * If the receiver uses optimistic locking this must raise an error on optimistic lock failure.
     * This gives the user the ability to define exactly how to update the data into the database,
     * or access data external from the database or from some other framework.
     */
    public void setUpdateQuery(UpdateObjectQuery updateQuery) {
        this.updateQuery = updateQuery;
        if (updateQuery == null) {
            return;
        }
        this.updateQuery.setIsUserDefined(true);
        this.updateQuery.setDescriptor(getDescriptor());
    }

    /**
     * ADVANCED:
     * Set the receiver's update SQL string.
     * This allows the user to override the SQL generated by EclipseLink, with there own SQL or procedure call.
     * The arguments are translated from the fields of the source row,
     * through replacing the field names marked by '#' with the values for those fields.
     * This must check the optimistic lock field and raise an error on optimistic lock failure.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     * <p>
     * Example, "update EMPLOYEE set F_NAME to #F_NAME, L_NAME to #L_NAME where EMPLOYEE_ID = #EMPLOYEE_ID".
     */
    public void setUpdateSQLString(String sqlString) {
        if (sqlString == null) {
            return;
        }

        UpdateObjectQuery query = new UpdateObjectQuery();
        query.setSQLString(sqlString);
        setUpdateQuery(query);
    }

    /**
     * ADVANCED:
     * Set the receiver's update call.
     * This allows the user to override the update operation.
     */
    public void setUpdateCall(Call call) {
        if (call == null) {
            return;
        }
        UpdateObjectQuery query = new UpdateObjectQuery();
        query.setCall(call);
        setUpdateQuery(query);
    }

    /**
     * PUBLIC:
     * Return the number of seconds queries will wait for their Statement to execute.
     *
     * - DefaultTimeout: get queryTimeout from parent DescriptorQueryManager. If there is no
     * parent, default to NoTimeout
     * - NoTimeout, 1..N: overrides parent queryTimeout
     */
    public int getQueryTimeout() {
        return queryTimeout;
    }

    public TimeUnit getQueryTimeoutUnit() {
        return queryTimeoutUnit;
    }

    /**
     * PUBLIC:
     * Set the number of seconds that queries will wait for their Statement to execute.
     * If the limit is exceeded, a DatabaseException is thrown.
     *
     * - DefaultTimeout: get queryTimeout from parent DescriptorQueryManager. If there is no
     * parent, default to NoTimeout
     * - NoTimeout, 1..N: overrides parent queryTimeout
     */
    public void setQueryTimeout(int queryTimeout) {
        this.queryTimeout = queryTimeout;
    }

    public void setQueryTimeoutUnit(TimeUnit queryTimeoutUnit) {
        this.queryTimeoutUnit = queryTimeoutUnit;
    }

    /**
     * INTERNAL:
     * Returns the collection of cached Update calls.
     */
    private ConcurrentFixedCache getCachedUpdateCalls() {
        if (cachedUpdateCalls == null) {
            this.cachedUpdateCalls = new ConcurrentFixedCache(10);
        }
        return this.cachedUpdateCalls;
    }

    /**
     * INTERNAL:
     * Returns the collection of cached expression queries.
     */
    private ConcurrentFixedCache getCachedExpressionQueries() {
        if (cachedExpressionQueries == null) {
            this.cachedExpressionQueries = new ConcurrentFixedCache(20);
        }
        return this.cachedExpressionQueries;
    }

    /**
     * ADVANCED:
     * Return the size of the update call cache.
     * The update call cache is used to cache the update SQL to avoid regeneration.
     * Since every update with different fields produces different SQL,
     * this cache allows caching of the update SQL based on the fields being updated.
     * The default cache size is 10, the update call cache can be disabled through setting the size to 0.
     */
    public int getUpdateCallCacheSize() {
        return getCachedUpdateCalls().getMaxSize();
    }

    /**
     * ADVANCED:
     * Set the size of the update call cache.
     * The update call cache is used to cache the update SQL to avoid regeneration.
     * Since every update with different fields produces different SQL,
     * this cache allows caching of the update SQL based on the fields being updated.
     * The default cache size is 10, the update call cache can be disabled through setting the size to 0.
     */
    public void setUpdateCallCacheSize(int updateCallCacheSize) {
        getCachedUpdateCalls().setMaxSize(updateCallCacheSize);
    }

    /**
     * INTERNAL:
     * Return the cached update SQL call based on the updated fields.
     * PERF: Allow caching of the update SQL call to avoid regeneration.
     */
    public Vector getCachedUpdateCalls(Vector updateFields) {
        return (Vector) getCachedUpdateCalls().get(updateFields);
    }

    /**
     * INTERNAL:
     * Cache a clone of the update SQL calls based on the updated fields.
     * If the max size is reached, do not cache the call.
     * The call's query must be dereferenced in order to allow the GC of a related session.
     * PERF: Allow caching of the update SQL call to avoid regeneration.
     */
    public void putCachedUpdateCalls(Vector updateFields, Vector updateCalls) {
        Vector vectorToCache = updateCalls;
        if (!updateCalls.isEmpty()) {
            int updateCallsSize = updateCalls.size();
            vectorToCache = new NonSynchronizedVector(updateCallsSize);
            for (int i = 0; i < updateCallsSize; i++) {
                DatasourceCall updateCall = (DatasourceCall)updateCalls.get(i);
                // clone call and dereference query for DatasourceCall and EJBQLCall
                DatasourceCall clonedUpdateCall = (DatasourceCall) updateCall.clone();
                clonedUpdateCall.setQuery(null);
                vectorToCache.add(clonedUpdateCall);
            }
        }
        getCachedUpdateCalls().put(updateFields, vectorToCache);
    }

    /**
     * INTERNAL:
     * Return the cached SQL call for the expression query.
     * PERF: Allow caching of expression query SQL call to avoid regeneration.
     */
    public DatabaseQuery getCachedExpressionQuery(DatabaseQuery query) {
        return (DatabaseQuery) getCachedExpressionQueries().get(query);
    }

    /**
     * INTERNAL:
     * Set the cached SQL call for the expression query.
     * PERF: Allow caching of expression query SQL call to avoid regeneration.
     */
    public void putCachedExpressionQuery(DatabaseQuery query) {
        getCachedExpressionQueries().put(query, query);
    }

    /**
     * INTERNAL:
     * Remove the cached expression query.
     * PERF: Allow caching of expression query SQL call to avoid regeneration.
     */
    public void removeCachedExpressionQuery(DatabaseQuery query) {
        getCachedExpressionQueries().remove(query);
    }

}
