/*******************************************************************************
 * Copyright (c) 1998, 2014 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 *     11/10/2011-2.4 Guy Pelletier 
 *       - 357474: Address primaryKey option from tenant discriminator column
 ******************************************************************************/  
package org.eclipse.persistence.internal.expressions;

import java.io.*;
import java.util.*;

import org.eclipse.persistence.descriptors.FetchGroupManager;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.mappings.foundation.AbstractColumnMapping;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.mappings.querykeys.*;
import org.eclipse.persistence.internal.queries.MapContainerPolicy;
import org.eclipse.persistence.internal.queries.MappedKeyMapContainerPolicy;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.descriptors.ClassDescriptor;

/**
 * Represents expression on query keys or mappings.
 * This includes direct, relationships query keys and mappings.
 */
public class QueryKeyExpression extends ObjectExpression {

    /** The name of the query key. */
    protected String name;

    /** Cache the aliased field. Only applies to attributes. */
    protected DatabaseField aliasedField;

    /** Is this a query across a 1:many or many:many relationship. Does not apply to attributes. */
    protected boolean shouldQueryToManyRelationship;

    /** Cache the query key for performance. Store a boolean so we don't repeat the search if there isn't one. */
    transient protected QueryKey queryKey;
    protected boolean hasQueryKey;

    /** Cache the mapping for performance. Store a boolean so we don't repeat the search if there isn't one. */
    transient protected DatabaseMapping mapping;
    protected boolean hasMapping;
    
    /** PERF: Cache if the expression is an attribute expression. */
    protected Boolean isAttributeExpression;
    
    protected IndexExpression index;
    
    protected boolean isClonedForSubQuery = false;
    
    public QueryKeyExpression() {
        this.shouldQueryToManyRelationship = false;
        this.hasQueryKey = true;
        this.hasMapping = true;
    }

    public QueryKeyExpression(String aName, Expression base) {
        super();
        name = aName;
        baseExpression = base;
        shouldUseOuterJoin = false;
        shouldQueryToManyRelationship = false;
        hasQueryKey = true;
        hasMapping = true;
    }
    
    /**
     * INTERNAL:
     * Return if the expression is equal to the other.
     * This is used to allow dynamic expression's SQL to be cached.
     */
    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!super.equals(object)) {
            return false;
        }
        QueryKeyExpression expression = (QueryKeyExpression) object;        
        // Return false for anyOf expressions, as equality is unknown.
        if (shouldQueryToManyRelationship() || expression.shouldQueryToManyRelationship()) {
            return false;
        }
        return ((getName() == expression.getName()) || ((getName() != null) && getName().equals(expression.getName())));
    }
        
    /**
     * INTERNAL:
     * Compute a consistent hash-code for the expression.
     * This is used to allow dynamic expression's SQL to be cached.
     */
    @Override
    public int computeHashCode() {
        int hashCode = super.computeHashCode();
        if (getName() != null) {
            hashCode = hashCode + getName().hashCode();
        }
        return hashCode;
    }

    /**
     * INTERNAL:
     * Return the expression to join the main table of this node to any auxiliary tables.
     */
    @Override
    public Expression additionalExpressionCriteria() {
        if (getDescriptor() == null) {
            return null;
        }

        Expression criteria = getDescriptor().getQueryManager().getAdditionalJoinExpression();
        if (criteria != null) {
            criteria = this.baseExpression.twist(criteria, this);
            if (shouldUseOuterJoin() && getSession().getPlatform().shouldPrintOuterJoinInWhereClause()) {
                criteria.convertToUseOuterJoin();
            }
        }
        if(getSession().getPlatform().shouldPrintOuterJoinInWhereClause()) {
            if(isUsingOuterJoinForMultitableInheritance()) {
                Expression childrenCriteria = getDescriptor().getInheritancePolicy().getChildrenJoinExpression();
                childrenCriteria = this.baseExpression.twist(childrenCriteria, this);
                childrenCriteria.convertToUseOuterJoin();
                if(criteria == null) {
                    criteria = childrenCriteria;
                } else {
                    criteria = criteria.and(childrenCriteria);
                }
            }
        }
        if (getDescriptor().getHistoryPolicy() != null) {
            Expression historyCriteria = getDescriptor().getHistoryPolicy().additionalHistoryExpression(this, this);
            if (criteria != null) {
                criteria = criteria.and(historyCriteria);
            } else {
                criteria = historyCriteria;
            }
        }
        return criteria;
    }

    /**
     * INTERNAL:
     * Used in case outer joins should be printed in FROM clause.
     * Each of the additional tables mapped to expressions that joins it.
     */
    @Override
    public Map additionalExpressionCriteriaMap() {
        if (getDescriptor() == null) {
            return null;
        }

        HashMap tablesJoinExpressions = new HashMap();
        Vector tables = getDescriptor().getTables();
        // skip the main table - start with i=1
        int tablesSize = tables.size();
        if (shouldUseOuterJoin() || (!getSession().getPlatform().shouldPrintInnerJoinInWhereClause())) {
            for (int i=1; i < tablesSize; i++) {
                DatabaseTable table = (DatabaseTable)tables.elementAt(i);
                Expression joinExpression = getDescriptor().getQueryManager().getTablesJoinExpressions().get(table);
                joinExpression = this.baseExpression.twist(joinExpression, this);
                if (getDescriptor().getHistoryPolicy() != null) {
                    joinExpression = joinExpression.and(getDescriptor().getHistoryPolicy().additionalHistoryExpression(this, this, i));
                }
                tablesJoinExpressions.put(table, joinExpression);
            }
        }
        if (isUsingOuterJoinForMultitableInheritance()) {
            List childrenTables = getDescriptor().getInheritancePolicy().getChildrenTables();
            tablesSize = childrenTables.size();
            for (int i=0; i < tablesSize; i++) {
                DatabaseTable table = (DatabaseTable)childrenTables.get(i);
                Expression joinExpression = getDescriptor().getInheritancePolicy().getChildrenTablesJoinExpressions().get(table);
                joinExpression = this.baseExpression.twist(joinExpression, this);
                tablesJoinExpressions.put(table, joinExpression);
            }
        }
        
        return tablesJoinExpressions;
    }

    /**
     * INTERNAL:
     * Find the alias for a given table
     */
    @Override
    public DatabaseTable aliasForTable(DatabaseTable table) {
        DatabaseMapping mapping = getMapping();
        if (isAttribute() || ((mapping != null) && (mapping.isAggregateObjectMapping() || mapping.isTransformationMapping()))) {
            return ((DataExpression)this.baseExpression).aliasForTable(table);
        }

        //"ref" and "structure" mappings, no table printed in the FROM clause, need to get the table alias form the parent table
        if ((mapping != null) && (mapping.isReferenceMapping() || mapping.isStructureMapping())) {
            DatabaseTable alias = this.baseExpression.aliasForTable(mapping.getDescriptor().getTables().firstElement());
            alias.setName(alias.getName() + "." + mapping.getField().getName());
            return alias;
        }
        
        // For direct-collection mappings the alias is store on the table expression.
        if ((mapping != null) && (mapping.isDirectCollectionMapping())) {
            if (tableAliases != null){
                DatabaseTable aliasedTable = tableAliases.keyAtValue(table);
                if (aliasedTable != null){
                    return aliasedTable;
                }
            }
            return getTable(table).aliasForTable(table);
        }

        return super.aliasForTable(table);
    }

    /**
     * ADVANCED:
     * Return an expression that allows you to treat its base as if it were a subclass of the class returned by the base
     * This can only be called on an ExpressionBuilder, the result of expression.get(String), expression.getAllowingNull(String),
     * the result of expression.anyOf("String") or the result of expression.anyOfAllowingNull("String")
     * 
     * downcast uses Expression.type() internally to guarantee the results are of the specified class.
     * <p>Example:
     * <pre><blockquote>
     *     EclipseLink: employee.get("project").treat(LargeProject.class).get("budget").equal(1000)
     *     Java: ((LargeProject)employee.getProjects().get(0)).getBudget() == 1000
     *     SQL: LPROJ.PROJ_ID (+)= PROJ.PROJ_ID AND L_PROJ.BUDGET = 1000 AND PROJ.TYPE = "L"
     * </blockquote></pre>
     */
    @Override
    public Expression treat(Class castClass){
        //to be used in 'where treat(t as PerformanceTireInfo).speedRating > 100'
        QueryKeyExpression clonedExpression = new TreatAsExpression(castClass, this);
        clonedExpression.shouldQueryToManyRelationship = this.shouldQueryToManyRelationship;
        //using shouldUseOuterJoin to indicate the join to use between the t and PerformanceTireInfo subclass.  
        clonedExpression.hasQueryKey = this.hasQueryKey;
        clonedExpression.hasMapping = this.hasMapping;

        this.addDerivedExpression(clonedExpression);
        return clonedExpression;
    }
    
    /**
     * INTERNAL:
     * Used for cloning.
     */
    @Override
    protected void postCopyIn(Map alreadyDone) {
        super.postCopyIn(alreadyDone);
        if (this.index != null) {
            this.index = (IndexExpression)this.index.copiedVersionFrom(alreadyDone);
        }
    }

    /**
     * INTERNAL:
     * Used for debug printing.
     */
    @Override
    public String descriptionOfNodeType() {
        return "Query Key";
    }

    /**
     * INTERNAL:
     */
    public void doQueryToManyRelationship() {
        shouldQueryToManyRelationship = true;
    }

    /**
     * INTERNAL:
     * Return any additional tables that belong to this expression
     * An example of how this method is used is to return any tables that belong to the map key
     * when this expression traverses a mapping that uses a Map
     */
    @Override
    public List<DatabaseTable> getAdditionalTables() {
        if (mapping != null && mapping.isCollectionMapping()){
            return ((CollectionMapping)mapping).getContainerPolicy().getAdditionalTablesForJoinQuery();
        }
        return null;
    }
    
    /**
     * INTERNAL:
     * Return the field appropriately aliased
     */
    @Override
    public DatabaseField getAliasedField() {
        if (aliasedField == null) {
            initializeAliasedField();
        }
        return aliasedField;

    }

    /**
     * Return the alias for our table
     */
    protected DatabaseTable getAliasedTable() {
        DataExpression base = (DataExpression)this.baseExpression;

        DatabaseTable alias = base.aliasForTable(getField().getTable());
        if (alias == null) {
            return getField().getTable();
        } else {
            return alias;
        }
    }

    /**
     * INTERNAL:
     */
    @Override
    public DatabaseField getField() {
        if (!isAttribute()) {
            return null;
        }
        QueryKey key = getQueryKeyOrNull();
        if ((key != null) && key.isDirectQueryKey()) {
            return ((DirectQueryKey)key).getField();
        }
        DatabaseMapping mapping = getMapping();
        if ((mapping == null) || mapping.getFields().isEmpty()) {
            return null;
        }
        return mapping.getFields().get(0);
    }

    /**
     * INTERNAL:
     * Return all the fields
     */
    @Override
    public Vector getFields() {
        if (isAttribute()) {
            Vector result = new Vector(1);
            DatabaseField field = getField();
            if (field != null) {
                result.addElement(field);
            }
            return result;
        } else {
            Vector result = new Vector();
            result.addAll(super.getFields());
            if ((this.mapping != null) && this.mapping.isCollectionMapping()){
                List<DatabaseField> fields = this.mapping.getContainerPolicy().getAdditionalFieldsForJoin((CollectionMapping)this.mapping);
                if (fields != null){
                    result.addAll(fields);
                }
            }
            return result;
        }
    }

    /**
     * INTERNAL:
     */
    @Override
    public List<DatabaseField> getSelectionFields(ReadQuery query) {
        if (isAttribute()) {
            List<DatabaseField> result = new ArrayList<DatabaseField>(1);
            DatabaseField field = getField();
            if (field != null) {
                result.add(field);
            }
            return result;
        } else {
            List<DatabaseField> result = new ArrayList<DatabaseField>();
            result.addAll(super.getSelectionFields(query));
            if ((this.mapping != null) && this.mapping.isCollectionMapping()){
                List<DatabaseField> fields = this.mapping.getContainerPolicy().getAdditionalFieldsForJoin((CollectionMapping)this.mapping);
                if (fields != null){
                    result.addAll(fields);
                }
            }
            return result;
        }
    }

    /**
     * INTERNAL:
     * Transform the object-level value into a database-level value
     */
    @Override
    public Object getFieldValue(Object objectValue, AbstractSession session) {
        DatabaseMapping mapping = getMapping();
        Object fieldValue = objectValue;
        if (mapping != null) {
            if (mapping.isAbstractDirectMapping() || mapping.isDirectCollectionMapping()) {
                // CR#3623207, check for IN Collection here not in mapping.
                if (objectValue instanceof Collection) {
                    // This can actually be a collection for IN within expressions... however it would be better for expressions to handle this.
                    Collection values = (Collection)objectValue;
                    Vector fieldValues = new Vector(values.size());
                    for (Iterator iterator = values.iterator(); iterator.hasNext();) {
                        Object value = iterator.next();
                        if (!(value instanceof Expression)){
                            value = getFieldValue(value, session);
                        }
                        fieldValues.add(value);
                    }
                    fieldValue = fieldValues;
                } else {
                    if (mapping.isAbstractColumnMapping()) {
                        fieldValue = ((AbstractColumnMapping)mapping).getFieldValue(objectValue, session);
                    } else if (mapping.isDirectCollectionMapping()) {
                        fieldValue = ((DirectCollectionMapping)mapping).getFieldValue(objectValue, session);
                    }
                }
            } else if ((objectValue instanceof Collection) && (mapping.isForeignReferenceMapping())) {
                // Was an IN with a collection of objects, extract their ids.
                List ids = new ArrayList();
                for (Object object : ((Collection)objectValue)) {
                    if ((mapping.getReferenceDescriptor() != null) && (mapping.getReferenceDescriptor().getJavaClass().isInstance(object))) {
                        Object id = mapping.getReferenceDescriptor().getObjectBuilder().extractPrimaryKeyFromObject(object, session);
                        if (id instanceof CacheId) {
                            id = Arrays.asList(((CacheId)id).getPrimaryKey());
                        }
                        ids.add(id);
                    } else {
                        ids.add(object);
                    }
                }
                fieldValue = ids;
            }
        }

        return fieldValue;
    }

    @Override
    public DatabaseMapping getMapping() {
        if (!hasMapping) {
            return null;
        }

        if (mapping == null) {
            mapping = super.getMapping();
            if (mapping == null) {
                hasMapping = false;
            }
        }
        return mapping;
    }

    public DatabaseMapping getMappingFromQueryKey() {
        QueryKey queryKey = getQueryKeyOrNull();
        if ((queryKey == null) || (!(queryKey instanceof DirectQueryKey))) {
            throw QueryException.cannotConformExpression();
        }
        mapping = queryKey.getDescriptor().getObjectBuilder().getMappingForField(((DirectQueryKey)queryKey).getField());
        if (mapping == null) {
            throw QueryException.cannotConformExpression();
        }
        return mapping;
    }

    @Override
    public String getName() {
        return name;
    }

    /**
     * INTERNAL:
     * Returns nested attribute name or null
     */
    public String getNestedAttributeName() {
        if(getMapping() != null) {
            String attributeName = getMapping().getAttributeName();
            if(this.baseExpression.isExpressionBuilder()) {
                return attributeName;
            } else if (this.baseExpression.isQueryKeyExpression()) {
                String nestedAttributeName = ((QueryKeyExpression)this.baseExpression).getNestedAttributeName();
                if(nestedAttributeName == null) {
                    return null;
                } else {
                    return nestedAttributeName + '.' + attributeName;
                }
            } else {
                return null;
            }
        } else {
            return null;
        }
    }
    
    /**
     * INTERNAL:
     */
    @Override
    public List<DatabaseTable> getOwnedTables() {
        if ((getMapping() != null) && getMapping().isNestedTableMapping()) {
            List<DatabaseTable> nestedTable = null;
            if (shouldQueryToManyRelationship()) {
                nestedTable = new ArrayList(super.getOwnedTables());
            } else {
                nestedTable = new ArrayList(1);
            }

            nestedTable.add(new NestedTable(this));
            return nestedTable;
        }
        if ((getMapping() != null) && (getMapping().isReferenceMapping() || getMapping().isStructureMapping())) {
            return null;
        }

        return super.getOwnedTables();

    }

    @Override
    public QueryKey getQueryKeyOrNull() {
        if (!hasQueryKey) {
            return null;
        }

        // Oct 19, 2000 JED
        // Added try/catch. This was throwing a NPE in the following case
        // expresssionBuilder.get("firstName").get("bob")
        //moved by Gordon Yorke to cover validate and normalize
        if (getContainingDescriptor() == null) {
            throw QueryException.invalidQueryKeyInExpression(getName());
        }
        if (queryKey == null) {
            queryKey = getContainingDescriptor().getQueryKeyNamed(getName());
            if (queryKey == null) {
                hasQueryKey = false;
            }
        }
        return queryKey;

    }

    /*
     * PUBLIC:
     * Index method could be applied to QueryKeyExpression corresponding to CollectionMapping
     * that has non-null listOrderField (the field holding the index values).
     * <p>Example:
     * <pre><blockquote>
     *    ReportQuery query = new ReportQuery();
     *    query.setReferenceClass(Employee.class);
     *    ExpressionBuilder builder = query.getExpressionBuilder();
     *    Expression firstNameJohn = builder.get("firstName").equal("John");
     *    Expression anyOfProjects = builder.anyOf("projects");
     *    Expression exp = firstNameJohn.and(anyOfProjects.index().between(2, 4));
     *    query.setSelectionCriteria(exp);
     *    query.addAttribute("projects", anyOfProjects);
     *       
     *    SELECT DISTINCT t0.PROJ_ID, t0.PROJ_TYPE, t0.DESCRIP, t0.PROJ_NAME, t0.LEADER_ID, t0.VERSION, t1.PROJ_ID, t1.BUDGET, t1.MILESTONE 
     *    FROM OL_PROJ_EMP t4, OL_SALARY t3, OL_EMPLOYEE t2, OL_LPROJECT t1, OL_PROJECT t0 
     *    WHERE ((((t2.F_NAME = 'John') AND (t4.PROJ_ORDER BETWEEN 2 AND 4)) AND (t3.OWNER_EMP_ID = t2.EMP_ID)) AND
     *    (((t4.EMP_ID = t2.EMP_ID) AND (t0.PROJ_ID = t4.PROJ_ID)) AND (t1.PROJ_ID (+) = t0.PROJ_ID)))
     * </blockquote></pre>
     */
    @Override
    public Expression index() {
        if(index == null) {
            index = new IndexExpression(this);
        }
        return index;
    }
    
    /**
     * INTERNAL:
     * Alias the database field for our current environment
     */
    protected void initializeAliasedField() {
        DatabaseField tempField = getField().clone();
        DatabaseTable aliasedTable = getAliasedTable();

        //  Put in a special check here so that if the aliasing does nothing we don't cache the
        // result because it's invalid. This saves us from caching premature data if e.g. debugging
        // causes us to print too early"
        //	if (aliasedTable.equals(getField().getTable())) {
        //		return;
        //	} else {
        aliasedField = tempField;
        aliasedField.setTable(aliasedTable);
        //	}
    }

    /**
     * INTERNAL:
     * Return if the expression is for a direct mapped attribute.
     */
    @Override
    public boolean isAttribute() {
        if (isAttributeExpression == null) {
            if (getSession() == null) {
                // We can't tell, so say no.
                return false;
            }
            QueryKey queryKey = getQueryKeyOrNull();
            if (queryKey != null) {
                isAttributeExpression = Boolean.valueOf(queryKey.isDirectQueryKey());
            } else {
                DatabaseMapping mapping = getMapping();
                if (mapping != null) {
                    if (mapping.isVariableOneToOneMapping()) {
                        throw QueryException.cannotQueryAcrossAVariableOneToOneMapping(mapping, mapping.getDescriptor());
                    } else {
                        isAttributeExpression = Boolean.valueOf(mapping.isDirectToFieldMapping());
                    }
                } else {
                    isAttributeExpression = Boolean.FALSE;
                }
            }
        }
        return isAttributeExpression.booleanValue();
    }

    @Override
    public boolean isQueryKeyExpression() {
        return true;
    }

    /*
     * INTERNAL:
     * If this query key represents a foreign reference answer the
     * base expression -> foreign reference join criteria.
     */
    public Expression mappingCriteria(Expression base) {
        Expression selectionCriteria;

        // First look for a query key, then a mapping
        if (getQueryKeyOrNull() == null) {
            if ((getMapping() == null) || (!getMapping().isForeignReferenceMapping())) {
                return null;
            } else {
                // The join criteria is now twisted by the mappings.
                selectionCriteria = ((ForeignReferenceMapping)getMapping()).getJoinCriteria(this, base);
            }
        } else {
            if (!getQueryKeyOrNull().isForeignReferenceQueryKey()) {
                return null;
            } else {
                selectionCriteria = ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getJoinCriteria();
                selectionCriteria = this.baseExpression.twist(selectionCriteria, base);
            }
        }

        if (shouldUseOuterJoin() && getSession().getPlatform().shouldPrintOuterJoinInWhereClause()) {
            selectionCriteria = selectionCriteria.convertToUseOuterJoin();
        }

        return selectionCriteria;
    }

    /**
     * INTERNAL:
     * Normalize the expression into a printable structure.
     * Any joins must be added to form a new root.
     */
    @Override
    public Expression normalize(ExpressionNormalizer normalizer) {
        return normalize(normalizer, this, null);
    }
    
    /**
     * INTERNAL:
     * Check if new expression need to be created for sub queries and re-normalized.
     */
    protected Expression checkJoinForSubSelectWithParent(ExpressionNormalizer normalizer, Expression base, List<Expression> foreignKeyJoinPointer) {
        SQLSelectStatement statement = normalizer.getStatement();
        if(!isClonedForSubQuery && statement.isSubSelect() && statement.getParentStatement().getBuilder().equals(getBuilder())) {
            if (baseExpression.isQueryKeyExpression()) {
                QueryKeyExpression baseQueryKeyExpression = (QueryKeyExpression) baseExpression;
                DatabaseMapping mapping = baseQueryKeyExpression.getMapping();
                if (mapping != null && mapping.isOneToOneMapping()) {
                    if (statement.getOptimizedClonedExpressions().containsKey(this)) {
                        return statement.getOptimizedClonedExpressions().get(this);
                    }
                    
                    QueryKeyExpression clonedBaseExpression = null;
                    if (baseQueryKeyExpression.hasBeenNormalized()) {
                        // Call normalize again to get same expression.
                        clonedBaseExpression = (QueryKeyExpression) baseQueryKeyExpression.normalize(normalizer);
                    }
                    
                    if (clonedBaseExpression == null && baseQueryKeyExpression.getBaseExpression().isQueryKeyExpression()) {
                        DatabaseMapping basebaseExprMapping = ((QueryKeyExpression)baseQueryKeyExpression.getBaseExpression()).getMapping();
                        if (basebaseExprMapping != null && basebaseExprMapping.isOneToOneMapping()) {
                            // Let base expression normalization re-create base base expression and normalize it if needed.
                            clonedBaseExpression = (QueryKeyExpression) baseQueryKeyExpression.normalize(normalizer);
                        }
                    }
                    
                    if (clonedBaseExpression == null) {
                        // Clone base expression & normalize
                        clonedBaseExpression = new QueryKeyExpression(baseQueryKeyExpression.getName(), baseQueryKeyExpression.getBaseExpression());
                        clonedBaseExpression.shouldQueryToManyRelationship = baseQueryKeyExpression.shouldQueryToManyRelationship;
                        clonedBaseExpression.shouldUseOuterJoin = baseQueryKeyExpression.shouldUseOuterJoin;
                        clonedBaseExpression.hasQueryKey = baseQueryKeyExpression.hasQueryKey;
                        clonedBaseExpression.hasMapping = baseQueryKeyExpression.hasMapping;
                        clonedBaseExpression.isAttributeExpression = baseQueryKeyExpression.isAttributeExpression;
                        clonedBaseExpression.isClonedForSubQuery = true;
                        
                        clonedBaseExpression = (QueryKeyExpression) clonedBaseExpression.normalize(normalizer);
                        statement.addOptimizedClonedExpressions(baseQueryKeyExpression, clonedBaseExpression);
                    }
                    
                    // Clone expression, normalize & return.
                    QueryKeyExpression clonedExpression = new QueryKeyExpression(name, clonedBaseExpression);
                    clonedExpression.shouldQueryToManyRelationship = this.shouldQueryToManyRelationship;
                    clonedExpression.shouldUseOuterJoin = this.shouldUseOuterJoin;
                    clonedExpression.hasQueryKey = this.hasQueryKey;
                    clonedExpression.hasMapping = this.hasMapping;
                    clonedExpression.isAttributeExpression = this.isAttributeExpression;
                    clonedExpression.isClonedForSubQuery = true;
                    
                    if (base == this) {
                        clonedExpression = (QueryKeyExpression) clonedExpression.normalize(normalizer, clonedExpression, foreignKeyJoinPointer);
                    } else {
                        // Caller invoked overloaded method with different base, RelationExpression in this case.
                        clonedExpression = (QueryKeyExpression) clonedExpression.normalize(normalizer, base, foreignKeyJoinPointer);
                    }
                    statement.addOptimizedClonedExpressions(this, clonedExpression);
                    return clonedExpression;
                }
            }
        }
        
        return null;
    }

    /**
     * INTERNAL:
     * For CR#2456 if this is part of an objExp.equal(objExp), do not need to add
     * additional expressions to normalizer both times, and the foreign key join
     * replaces the equal expression.
     */
    public Expression normalize(ExpressionNormalizer normalizer, Expression base, List<Expression> foreignKeyJoinPointer) {
        if (this.hasBeenNormalized) {
            return this;
        }
        // Bug 397619 - It should only be normalized by parent.
        // If subselect & not normalized, always clone and normalize
        // if it has parent builder.
        Expression clonedExpression = checkJoinForSubSelectWithParent(normalizer, base, foreignKeyJoinPointer);
        if (clonedExpression != null) {
            return clonedExpression;
        }
        super.normalize(normalizer);
        DatabaseMapping mapping = getMapping();
        SQLSelectStatement statement = normalizer.getStatement();
        if ((mapping != null) && mapping.isDirectToXMLTypeMapping()) {
            statement.setRequiresAliases(true);
        }

        // Check if any joins need to be added.
        if (isAttribute()) {
            return this;
        }

        ReadQuery query = normalizer.getStatement().getQuery();
        // Record any class used in a join to invalidate query results cache.
        if ((query != null) && query.shouldCacheQueryResults()) {
            if ((mapping != null) && (mapping.getReferenceDescriptor() != null) && (mapping.getReferenceDescriptor().getJavaClass() != null)) {
                query.getQueryResultsCachePolicy().getInvalidationClasses().add(mapping.getReferenceDescriptor().getJavaClass());
            } else {
                QueryKey queryKey = getQueryKeyOrNull();
                if ((queryKey != null) && queryKey.isForeignReferenceQueryKey()) {
                    query.getQueryResultsCachePolicy().getInvalidationClasses().add(((ForeignReferenceQueryKey)queryKey).getReferenceClass());
                }
            }
        }

        // If the mapping is 'ref' or 'structure', no join needed.
        if ((mapping != null) && (mapping.isReferenceMapping() || mapping.isStructureMapping())) {
            statement.setRequiresAliases(true);
            return this;
        }

        // Compute if a distinct is required during normalization.
        if (shouldQueryToManyRelationship() && (!statement.isDistinctComputed()) && (!statement.isAggregateSelect())) {
            statement.useDistinct();
        }

        // Turn off DISTINCT if nestedTableMapping is used (not supported by Oracle 8.1.5).
        if ((mapping != null) && mapping.isNestedTableMapping()) {
            // There are two types of nested tables, one used by clients, one used by mappings, do nothing in the mapping case.
            if (!shouldQueryToManyRelationship()) {
                return this;
            }
            statement.dontUseDistinct();
        }

        // Normalize the ON clause if present.  Need to use rebuild, not twist as parameters are real parameters.
        if (this.onClause != null) {
            this.onClause = this.onClause.normalize(normalizer);
        }
        
        Expression mappingExpression = mappingCriteria(base);
        if (mappingExpression != null) {
            mappingExpression = mappingExpression.normalize(normalizer);
        }
        if (mappingExpression != null) {
            // If the join was an outer join we must not add the join criteria to the where clause,
            // if the platform prints the join in the from clause.
            if (shouldUseOuterJoin() && (getSession().getPlatform().isInformixOuterJoin())) {
                setOuterJoinExpIndex(statement.addOuterJoinExpressionsHolders(this, mappingExpression, null, null));
                normalizer.addAdditionalExpression(mappingExpression.and(additionalExpressionCriteria()));
                return this;
            } else if ((shouldUseOuterJoin() && (!getSession().getPlatform().shouldPrintOuterJoinInWhereClause()))
                    || (!getSession().getPlatform().shouldPrintInnerJoinInWhereClause())) {
                setOuterJoinExpIndex(statement.addOuterJoinExpressionsHolders(this, mappingExpression, additionalExpressionCriteriaMap(), null));
                if ((getDescriptor() != null) && (getDescriptor().getHistoryPolicy() != null)) {
                    Expression historyOnClause = getDescriptor().getHistoryPolicy().additionalHistoryExpression(this, this, 0); 
                    if (getOnClause() != null) {
                        setOnClause(getOnClause().and(historyOnClause));
                    } else {
                        setOnClause(historyOnClause);
                    }
                }
                return this;
            } else if (isUsingOuterJoinForMultitableInheritance() && (!getSession().getPlatform().shouldPrintOuterJoinInWhereClause())) {
                setOuterJoinExpIndex(statement.addOuterJoinExpressionsHolders(null, null, additionalExpressionCriteriaMap(), mapping.getReferenceDescriptor()));
                // fall through to the main case
            }
            
            // This must be added even if outer. Actually it should be converted to use a right outer join, but that gets complex
            // so we do not support this current which is a limitation in some cases.
            if (foreignKeyJoinPointer != null) {
                // If this expression is right side of an objExp.equal(objExp), one
                // need not add additionalExpressionCriteria twice.
                // Also the join will replace the original objExp.equal(objExp).
                // For CR#2456.
                foreignKeyJoinPointer.add(mappingExpression.and(this.onClause));
            } else {
                normalizer.addAdditionalExpression(mappingExpression.and(additionalExpressionCriteria()).and(this.onClause));
            }
        }
        // For bug 2900974 special code for DirectCollectionMappings moved to printSQL.
        return this;
    }

    /**
     * INTERNAL:
     * Print SQL onto the stream, using the ExpressionPrinter for context
     */
    @Override
    public void printSQL(ExpressionSQLPrinter printer) {
        if (isAttribute()) {
            printer.printField(getAliasedField());
        }

        // If the mapping is a direct collection then this falls into a gray area.
        // It must be treated as an attribute at this moment for it has a direct field.
        // However it is not an attribute in the sense that it also represents a foreign
        // reference and a mapping criteria has been added.
        // For bug 2900974 these are now handled as non-attributes during normalize but
        // as attributes when printing SQL.
        //
        if ((!isAttribute()) && (getMapping() != null) && getMapping().isDirectCollectionMapping()) {
            DirectCollectionMapping directCollectionMapping = (DirectCollectionMapping)getMapping();

            // The aliased table comes for free as it was a required part of the join criteria.
            TableExpression table = (TableExpression)getTable(directCollectionMapping.getReferenceTable());
            DatabaseTable aliasedTable = table.aliasForTable(table.getTable());
            DatabaseField aliasedField = directCollectionMapping.getDirectField().clone();
            aliasedField.setTable(aliasedTable);
            printer.printField(aliasedField);
        }

        if ((getMapping() != null) && getMapping().isNestedTableMapping()) {
            DatabaseTable tableAlias = aliasForTable(new NestedTable(this));
            printer.printString(tableAlias.getName());
        }
    }

    /**
     * INTERNAL:
     * Print java for project class generation
     */
    @Override
    public void printJava(ExpressionJavaPrinter printer) {
        this.baseExpression.printJava(printer);
        if (!shouldUseOuterJoin()) {
            if (!shouldQueryToManyRelationship()) {
                printer.printString(".get(");
            } else {
                printer.printString(".anyOf(");
            }
        } else {
            if (!shouldQueryToManyRelationship()) {
                printer.printString(".getAllowingNull(");
            } else {
                printer.printString(".anyOfAllowingNone(");
            }
        }
        printer.printString("\"" + getName() + "\")");
    }

    /**
     * INTERNAL:
     * This expression is built on a different base than the one we want. Rebuild it and
     * return the root of the new tree
     */
    @Override
    public Expression rebuildOn(Expression newBase) {
        Expression newLocalBase = this.baseExpression.rebuildOn(newBase);
        QueryKeyExpression result = null;

        // For bug 3096634 rebuild outer joins correctly from the start.
        if (shouldUseOuterJoin) {
            result = (QueryKeyExpression)newLocalBase.getAllowingNull(getName());
        } else {
            result = (QueryKeyExpression)newLocalBase.get(getName());
        }
        if (shouldQueryToManyRelationship) {
            result.doQueryToManyRelationship();
        }
        result.setSelectIfOrderedBy(selectIfOrderedBy());
        if (castClass != null){
            result.setCastClass(castClass);
        }
        return result;
    }

    /**
     * INTERNAL:
     * A special version of rebuildOn where the newBase need not be a new
     * ExpressionBuilder but any expression.
     * <p>
     * For nested joined attributes, the joined attribute query must have
     * its joined attributes rebuilt relative to it.
     */
    public Expression rebuildOn(Expression oldBase, Expression newBase) {
        if (this == oldBase) {
            return newBase;
        }
        Expression newLocalBase = ((QueryKeyExpression)this.baseExpression).rebuildOn(oldBase, newBase);
        QueryKeyExpression result = null;

        // For bug 3096634 rebuild outer joins correctly from the start.
        if (shouldUseOuterJoin) {
            result = (QueryKeyExpression)newLocalBase.getAllowingNull(getName());
        } else {
            result = (QueryKeyExpression)newLocalBase.get(getName());
        }
        if (shouldQueryToManyRelationship) {
            result.doQueryToManyRelationship();
        }
        result.setSelectIfOrderedBy(selectIfOrderedBy());
        return result;
    }

    /**
     * Reset cached information here so that we can be sure we're accurate.
     */
    @Override
    protected void resetCache() {
        hasMapping = true;
        mapping = null;
        hasQueryKey = true;
        queryKey = null;
    }

    public boolean shouldQueryToManyRelationship() {
        return shouldQueryToManyRelationship;
    }

    /**
     * INTERNAL:
     * Rebuild myself against the base, with the values of parameters supplied by the context
     * expression. This is used for transforming a standalone expression (e.g. the join criteria of a mapping)
     * into part of some larger expression. You normally would not call this directly, instead calling twist
     * See the comment there for more details"
     */
    @Override
    public Expression twistedForBaseAndContext(Expression newBase, Expression context, Expression oldBase) {
        if (oldBase == null || this.baseExpression == oldBase) {
            Expression twistedBase = this.baseExpression.twistedForBaseAndContext(newBase, context, oldBase);
            QueryKeyExpression result = (QueryKeyExpression)twistedBase.get(getName());
            if (shouldUseOuterJoin) {
                result.doUseOuterJoin();
            }
            if (shouldQueryToManyRelationship) {
                result.doQueryToManyRelationship();
            }
            return result;
        }
        
        return this;
    }

    /**
     * Do any required validation for this node. Throw an exception if it's incorrect.
     */
    @Override
    public void validateNode() {
        QueryKey queryKey = getQueryKeyOrNull();
        DatabaseMapping mapping = getMapping();
        
        if ((queryKey == null) && (mapping == null)) {
            throw QueryException.invalidQueryKeyInExpression(getName());
        }


        Object theOneThatsNotNull = null;
        boolean qkIsToMany = false;
        if (queryKey != null) {
            theOneThatsNotNull = queryKey;
            qkIsToMany = queryKey.isManyToManyQueryKey() || queryKey.isOneToManyQueryKey();
        }
        boolean isNestedMapping = false;
        if (mapping != null) {
            // Bug 2847621 - Add Aggregate Collection to the list of valid items for outer join.
            if (this.shouldUseOuterJoin && (!(mapping.isOneToOneMapping() || mapping.isOneToManyMapping() || mapping.isManyToManyMapping() || mapping.isAggregateCollectionMapping() || mapping.isDirectCollectionMapping()))) {
                throw QueryException.outerJoinIsOnlyValidForOneToOneMappings(mapping);
            }
            qkIsToMany = mapping.isCollectionMapping();
            if (this.index != null) {
                if (qkIsToMany) {
                    CollectionMapping collectionMapping = (CollectionMapping)mapping;
                    if(collectionMapping.getListOrderField() != null) {
                        this.index.setField(collectionMapping.getListOrderField());
                        addDerivedField(this.index);
                    } else {
                        throw QueryException.indexRequiresCollectionMappingWithListOrderField(this, collectionMapping);
                    }
                } else {
                    throw QueryException.indexRequiresCollectionMappingWithListOrderField(this, mapping);
                }
            }
            isNestedMapping = mapping.isNestedTableMapping();
            theOneThatsNotNull = mapping;
        } else {
            if (this.index != null) {
                throw QueryException.indexRequiresCollectionMappingWithListOrderField(this, null);
            }
        }
        if ((!shouldQueryToManyRelationship()) && qkIsToMany && (!isNestedMapping)) {
            throw QueryException.invalidUseOfToManyQueryKeyInExpression(theOneThatsNotNull);
        }
        if (shouldQueryToManyRelationship() && !qkIsToMany) {
            throw QueryException.invalidUseOfAnyOfInExpression(theOneThatsNotNull);
        }
    }

    /**
     * INTERNAL:
     * Return the value for in memory comparison.
     * This is only valid for valueable expressions.
     */
    @Override
    public Object valueFromObject(Object object, AbstractSession session, AbstractRecord translationRow, int valueHolderPolicy, boolean isObjectUnregistered) {
        // The expression may be across a relationship, in which case it must be traversed.
        if ((!this.baseExpression.isExpressionBuilder()) && this.baseExpression.isQueryKeyExpression()) {
            object = this.baseExpression.valueFromObject(object, session, translationRow, valueHolderPolicy, isObjectUnregistered);

            // toDo: Null means the join filters out the row, returning null is not correct if an inner join,
            // outer/inner joins need to be fixed to filter correctly.
            if (object == null) {
                return null;
            }

            // If from an anyof the object will be a collection of values,
            // A new vector must union the object values and the values extracted from it.
            if (object instanceof Vector) {
                Vector comparisonVector = new Vector(((Vector)object).size() + 2);
                for (Enumeration valuesToIterate = ((Vector)object).elements();
                         valuesToIterate.hasMoreElements();) {
                    Object vectorObject = valuesToIterate.nextElement();
                    if (vectorObject == null) {
                        comparisonVector.addElement(vectorObject);
                    } else {
                        Object valueOrValues = valuesFromCollection(vectorObject, session, valueHolderPolicy, isObjectUnregistered);

                        // If a collection of values were extracted union them.
                        if (valueOrValues instanceof Vector) {
                            for (Enumeration nestedValuesToIterate = ((Vector)valueOrValues).elements();
                                     nestedValuesToIterate.hasMoreElements();) {
                                comparisonVector.addElement(nestedValuesToIterate.nextElement());
                            }
                        } else {
                            comparisonVector.addElement(valueOrValues);
                        }
                    }
                }
                return comparisonVector;
            }
        }
        return valuesFromCollection(object, session, valueHolderPolicy, isObjectUnregistered);
    }

    /**
     * INTERNAL
     * This method iterates through a collection and gets the values from the objects to conform in an in-memory query.
     */
    public Object valuesFromCollection(Object object, AbstractSession session, int valueHolderPolicy, boolean isObjectUnregistered) {
        // in case the mapping is null - this can happen if a query key is being used
        // In this case, check for the query key and find it's mapping.
        boolean readMappingFromQueryKey = false;
        if (getMapping() == null) {
            getMappingFromQueryKey();
            readMappingFromQueryKey = true;
        }

        // For bug 2780817 get the mapping directly from the object.  In EJB 2.0 
        // inheritance, each child must override mappings defined in an abstract 
        // class with its own.
        DatabaseMapping mapping = this.mapping;
        ClassDescriptor descriptor = mapping.getDescriptor();
        if (descriptor.hasInheritance() && (descriptor.getJavaClass() != object.getClass())) {
            descriptor = descriptor.getInheritancePolicy().getDescriptor(object.getClass());
            mapping = descriptor.getObjectBuilder().getMappingForAttributeName(mapping.getAttributeName());
        }

        //fetch group support
        if (descriptor.hasFetchGroupManager()) {
            FetchGroupManager fetchGroupManager = descriptor.getFetchGroupManager();
            if (fetchGroupManager.isPartialObject(object) && (!fetchGroupManager.isAttributeFetched(object, mapping.getAttributeName()))) {
                //the conforming attribute is not fetched, simply throw exception
                throw QueryException.cannotConformUnfetchedAttribute(mapping.getAttributeName());
            }
        }

        if (mapping.isAbstractColumnMapping()) {
            return ((AbstractColumnMapping)mapping).valueFromObject(object, mapping.getField(), session);
        } else if (mapping.isForeignReferenceMapping()) {
            //CR 3677 integration of a ValueHolderPolicy
            Object valueFromMapping = mapping.getAttributeValueFromObject(object);
            if (!((ForeignReferenceMapping)mapping).getIndirectionPolicy().objectIsInstantiated(valueFromMapping)) {
                if (valueHolderPolicy  != InMemoryQueryIndirectionPolicy.SHOULD_TRIGGER_INDIRECTION) {
                    //If the client wishes us to trigger the indirection then we should do so,
                    //Other wise throw the exception
                    throw QueryException.mustInstantiateValueholders();// you should instantiate the valueholder for this to work
                }

                // maybe we should throw this exception from the start, to save time
            }
            Object valueToIterate = mapping.getRealAttributeValueFromObject(object, session);
            UnitOfWorkImpl uow = isObjectUnregistered ? (UnitOfWorkImpl)session : null;

            // First check that object in fact is unregistered.
            // toDo: ?? Why is this commented out? Why are we supporting the unregistered thing at all?
            // Does not seem to be any public API for this, nor every used internally?
            //if (isObjectUnregistered) {
            //	isObjectUnregistered = !uow.getCloneMapping().containsKey(object);
            //}
            if (mapping.isCollectionMapping() && (valueToIterate != null)) {
                // For bug 2766379 must use the correct version of vectorFor to
                // unwrap the result same time.
                valueToIterate = mapping.getContainerPolicy().vectorFor(valueToIterate, session);

                // toDo: If the value is empty, need to support correct inner/outer join filtering symantics.
                // For CR 2612601, try to partially replace the result with already
                // registered objects.
                if (isObjectUnregistered && (uow.getCloneMapping().get(object) == null)) {
                    Vector objectValues = (Vector)valueToIterate;
                    for (int i = 0; i < objectValues.size(); i++) {
                        Object original = objectValues.elementAt(i);
                        Object clone = uow.getIdentityMapAccessorInstance().getIdentityMapManager().getFromIdentityMap(original);
                        if (clone != null) {
                            objectValues.setElementAt(clone, i);
                        }
                    }
                }

                // For CR 2612601, conforming without registering, a query could be
                // bob.get("address").get("city").equal("Ottawa"); where the address
                // has been registered and modified in the UOW, but bob has not.  Thus
                // even though bob does not point to the modified address now, it will
                // as soon as it is registered, so should point to it here.
            } else if (isObjectUnregistered && (uow.getCloneMapping().get(object) == null)) {
                Object clone = uow.getIdentityMapAccessorInstance().getIdentityMapManager().getFromIdentityMap(valueToIterate);
                if (clone != null) {
                    valueToIterate = clone;
                }
            }
            return valueToIterate;
        } else if (mapping.isAggregateMapping()) {
            Object aggregateValue = mapping.getAttributeValueFromObject(object);
            // Bug 3995468 - if this query key is to a mapping in an aggregate object, get the object from actual mapping rather than the aggregate mapping
            while (readMappingFromQueryKey && mapping.isAggregateObjectMapping() && !((AggregateObjectMapping)mapping).getReferenceClass().equals(queryKey.getDescriptor().getJavaClass())) {
                mapping = mapping.getReferenceDescriptor().getObjectBuilder().getMappingForField(((DirectQueryKey)queryKey).getField());
                aggregateValue = mapping.getRealAttributeValueFromObject(aggregateValue, session);
            }
            return aggregateValue;
        } else {
            throw QueryException.cannotConformExpression();
        }
    }
    
    /**
     * INTERNAL:
     * Lookup the descriptor for this item by traversing its expression recursively.
     */
    @Override
    public ClassDescriptor getLeafDescriptor(DatabaseQuery query, ClassDescriptor rootDescriptor, AbstractSession session) {
        Expression baseExpression = getBaseExpression();
        ClassDescriptor baseDescriptor = baseExpression.getLeafDescriptor(query, rootDescriptor, session);
        
        if (isMapEntryExpression()) {
            // get the expression that owns the mapping for the table entry
            Expression owningExpression = ((QueryKeyExpression)baseExpression).getBaseExpression();
            ClassDescriptor owningDescriptor = owningExpression.getLeafDescriptor(query, rootDescriptor, session);
            
            // Get the mapping that owns the table
            CollectionMapping mapping = (CollectionMapping)owningDescriptor.getObjectBuilder().getMappingForAttributeName(baseExpression.getName());
            
            return ((MapContainerPolicy)mapping.getContainerPolicy()).getDescriptorForMapKey();
        }

        ClassDescriptor descriptor = null;
        String attributeName = getName();

        DatabaseMapping mapping = baseDescriptor.getObjectBuilder().getMappingForAttributeName(attributeName);

        if (mapping == null) {
            QueryKey queryKey = baseDescriptor.getQueryKeyNamed(attributeName);
            if (queryKey != null) {
                if (queryKey.isForeignReferenceQueryKey()) {
                    descriptor = session.getDescriptor(((ForeignReferenceQueryKey)queryKey).getReferenceClass());
                } else { // if (queryKey.isDirectQueryKey())
                    descriptor = queryKey.getDescriptor();
                }
            }
            if (descriptor == null) {
                throw QueryException.invalidExpressionForQueryItem(this, query);
            }
        } else if (mapping.isAggregateMapping()) {
            descriptor = ((AggregateMapping)mapping).getReferenceDescriptor();
        } else if (mapping.isForeignReferenceMapping()) {
            descriptor = ((ForeignReferenceMapping)mapping).getReferenceDescriptor();
        }
        return descriptor;
    }
    
    /**
     * INTERNAL:
     * Lookup the mapping for this item by traversing its expression recursively.
     * If an aggregate of foreign mapping is found it is traversed.
     */
    @Override
    public DatabaseMapping getLeafMapping(DatabaseQuery query, ClassDescriptor rootDescriptor, AbstractSession session) {
        if (isMapEntryExpression()){
            MapEntryExpression mapEntryExpression = (MapEntryExpression)this;
            
            // get the expression that we want the table entry for
            QueryKeyExpression baseExpression = (QueryKeyExpression)mapEntryExpression.getBaseExpression();
            
            // get the expression that owns the mapping for the table entry
            Expression owningExpression = baseExpression.getBaseExpression();
            ClassDescriptor owningDescriptor = owningExpression.getLeafDescriptor(query, rootDescriptor, session);
            
            // Get the mapping that owns the table
            CollectionMapping mapping = (CollectionMapping)owningDescriptor.getObjectBuilder().getMappingForAttributeName(baseExpression.getName());
            
            if (mapEntryExpression.shouldReturnMapEntry()) {
                return mapping;
            }
            if (mapping.getContainerPolicy().isMappedKeyMapPolicy()){
                MappedKeyMapContainerPolicy policy = (MappedKeyMapContainerPolicy)mapping.getContainerPolicy();
                return (DatabaseMapping)policy.getKeyMapping();
            }
            return mapping;
        }

        Expression baseExpression = getBaseExpression();

        ClassDescriptor descriptor = baseExpression.getLeafDescriptor(query, rootDescriptor, session);
        if (descriptor == null){
            return null;
        }
        return descriptor.getObjectBuilder().getMappingForAttributeName(getName());
    }
    
    /**
     * INTERNAL:
     * Used to print a debug form of the expression tree.
     */
    @Override
    public void writeDescriptionOn(BufferedWriter writer) throws IOException {
        writer.write(getName());
        if (castClass != null){
            writer.write(" (" + castClass.getName() + ") ");
        }
        writer.write(tableAliasesDescription());
    }
    
    /**
     * INTERNAL:
     * Indicates whether this expression corresponds to DirectCollection.
     */
    @Override
    public boolean isDirectCollection() {
        if(getMapping() != null) {
            return getMapping().isDirectCollectionMapping();
        } else {
            if(getQueryKeyOrNull() != null) {
                return this.queryKey.isDirectCollectionQueryKey();
            } else {
                return false;
            }
        }
    }

    /**
     * INTERNAL:
     * Indicates whether this expression corresponds to OneToOne.
     */
    public boolean isOneToOne() {
        if(getMapping() != null) {
            return getMapping().isOneToOneMapping();
        } else {
            if(getQueryKeyOrNull() != null) {
                return this.queryKey.isOneToOneQueryKey();
            } else {
                return false;
            }
        }
    }

    /**
     * INTERNAL:
     * Indicates whether this expression corresponds to OneToMany.
     */
    public boolean isOneToMany() {
        if(getMapping() != null) {
            return getMapping().isOneToManyMapping();
        } else {
            if(getQueryKeyOrNull() != null) {
                return this.queryKey.isOneToManyQueryKey();
            } else {
                return false;
            }
        }
    }

    /**
     * INTERNAL:
     * Indicates whether this expression corresponds to ManyToMany.
     */
    public boolean isManyToMany() {
        if(getMapping() != null) {
            return getMapping().isManyToManyMapping();
        } else {
            if(getQueryKeyOrNull() != null) {
                return this.queryKey.isManyToManyQueryKey();
            } else {
                return false;
            }
        }
    }

    /**
     * INTERNAL:
     * Return if the expression if for a map key mapping where the key is a OneToOne.
     */
    public boolean isMapKeyObjectRelationship() {
        if (getMapping() != null) {
            return getMapping().isCollectionMapping() && ((CollectionMapping)getMapping()).isMapKeyObjectRelationship();
        } else {
            return false;
        }
    }

    /**
     * INTERNAL:
     * Return if descriptor for the map key mapping where the key is a OneToOne.
     */
    public ClassDescriptor getMapKeyDescriptor() {
        return ((CollectionMapping)getMapping()).getContainerPolicy().getDescriptorForMapKey();
    }
    
    /**
     * Calculate the reference table for based on the various QueryKeyExpression
     * usages (join query keys, custom defined query keys, or query keys for
     * mappings).
     * 
     * Called from {@link SQLSelectStatement#appendFromClauseForOuterJoin}.
     * 
     * @return DatabaseTable
     */    
    public DatabaseTable getReferenceTable() {
        if(getMapping() != null) {
            if (getMapping().isDirectCollectionMapping()) {
                return ((DirectCollectionMapping)getMapping()).getReferenceTable();
            } else {
                return getMapping().getReferenceDescriptor().getTables().firstElement();
            }
        } else {
            return ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getReferenceTable(getDescriptor());
        }
    }

    /**
     * Calculate the source table for based on the various QueryKeyExpression
     * usages (join query keys, custom defined query keys, or query keys for
     * mappings).
     * 
     * Called from {@link SQLSelectStatement#appendFromClauseForOuterJoin}.
     * 
     * @return DatabaseTable
     */    
    public DatabaseTable getSourceTable() {
        if (getBaseExpression().isExpressionBuilder() && getBuilder().hasViewTable()) {
            return getBuilder().getViewTable();
        }
        if (getMapping() != null) {
            // Grab the source table from the mapping not just the first table 
            // from the descriptor. In an joined inheritance hierarchy, the
            // fk used in the outer join may be from a subclasses's table.
            if (getMapping().isObjectReferenceMapping() && ((ObjectReferenceMapping) getMapping()).isForeignKeyRelationship()) {
                 return getMapping().getFields().firstElement().getTable();
            } else {
                return ((ObjectExpression)this.baseExpression).getDescriptor().getTables().firstElement();    
            }
        } else {
            return ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getSourceTable();
        }
    }

    /**
     * Calculate the relation table for based on the various QueryKeyExpression
     * usages (join query keys, custom defined query keys, or query keys for
     * mappings).
     * 
     * Called from {@link SQLSelectStatement#appendFromClauseForOuterJoin}.
     * 
     * @return DatabaseTable
     */    
    public DatabaseTable getRelationTable() {
        if(getMapping() != null) {
            if(getMapping().isManyToManyMapping()) {
                return ((ManyToManyMapping)getMapping()).getRelationTable();
            } else if(getMapping().isOneToOneMapping()) {
                return ((OneToOneMapping)getMapping()).getRelationTable();
            }
        } else {
            if(getQueryKeyOrNull().isForeignReferenceQueryKey()) {
                return ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getRelationTable(getDescriptor());
            }
        }
        return null;
    }
}
