/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.queries;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.expressions.BaseExpression;
import org.eclipse.persistence.internal.expressions.ForUpdateOfClause;
import org.eclipse.persistence.internal.expressions.ObjectExpression;
import org.eclipse.persistence.internal.expressions.QueryKeyExpression;
import org.eclipse.persistence.internal.helper.NonSynchronizedSubVector;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.CollectionMapping;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;
import org.eclipse.persistence.queries.Cursor;
import org.eclipse.persistence.queries.FetchGroup;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.sessions.DatabaseRecord;

/**
 * <p><b>Purpose</b>:
 * A common class to be used by ObjectLevelReadQueries and ReportItems.  This
 * Class will be used to store Joined Attribute Expressions.  It will also
 * store the indexes for object construction.
 *
 * @author Gordon Yorke
 * @since EJB3.0 RI
 */

public class JoinedAttributeManager implements Cloneable, Serializable {

    /** Stores AggregateObjectMapping expressions used within local join expressions */
    protected transient List<DatabaseMapping> joinedAggregateMappings = new ArrayList(0);

    /** indexed list of mappings corresponding to */
    protected transient List<DatabaseMapping> joinedAttributeMappings = new ArrayList(0);

    /** Stores the joined attributes added through the query */
    protected List<Expression> joinedAttributeExpressions;

    /** Stores the joined attributes as specified in the descriptor */
    protected List<Expression> joinedMappingExpressions;

    /** PERF: Cache the local joined attribute expressions. */
    protected List<Expression> joinedAttributes;

    /** Used to determine if -m joining has been used. */
    protected boolean isToManyJoin = false;

    /** PERF: Used to avoid null checks for inner attribute joining. */
    protected boolean hasOuterJoinedAttribute = true;

    /** Used internally for joining. */
    protected transient Map<DatabaseMapping, Object> joinedMappingIndexes;

    /** Used internally for joining. */
    protected transient Map<DatabaseMapping, ObjectLevelReadQuery> joinedMappingQueries;

    /** PERF: Stores the cloned joinedMappingQueries. */
    protected transient Map<DatabaseMapping, ObjectLevelReadQuery> joinedMappingQueryClones;

    /** Stored all row results to -m joining. */
    protected transient List<AbstractRecord> dataResults;

    /** Stored all row results to -m joining by cache key. */
    protected transient Map<Object, List<AbstractRecord>> dataResultsByPrimaryKey;

    /** Stores the descriptor that these joins apply on */
    protected transient ClassDescriptor descriptor;

    /** Stores the base builder for resolving joined attributes by name. */
    protected ExpressionBuilder baseExpressionBuilder;

    /** Stores the last used base expression while adding joined attribute expression. */
    protected Expression lastJoinedAttributeBaseExpression;

    /** Stores the base query. */
    protected ObjectBuildingQuery baseQuery;

    /** Stores the result index of the parent, used for oneToMany joins. */
    protected int parentResultIndex;

    /** Determine if duplicate rows should be filter when using 1-m joining. */
    protected boolean shouldFilterDuplicates = true;

    //** Stores orderBy expressions of the joined CollectionMappings - in case the mapping has listFieldOrder */
    protected transient List<Expression> orderByExpressions;

    //** Stores additional field expressions of the joined CollectionMappings - in case the mapping has listFieldOrder */
    protected transient List<Expression> additionalFieldExpressions;

    public JoinedAttributeManager(){
    }

    public JoinedAttributeManager(ClassDescriptor descriptor, ExpressionBuilder baseBuilder, ObjectBuildingQuery baseQuery){
        this.descriptor = descriptor;
        this.baseQuery = baseQuery;
        this.baseExpressionBuilder = baseBuilder;
        this.parentResultIndex = 0;
    }

    /**
     * Return if duplicate rows should be filter when using 1-m joining.
     */
    public boolean shouldFilterDuplicates() {
        return shouldFilterDuplicates;
    }

    /**
     * Set if duplicate rows should be filter when using 1-m joining.
     */
    public void setShouldFilterDuplicates(boolean shouldFilterDuplicates) {
        this.shouldFilterDuplicates = shouldFilterDuplicates;
    }

    public void addJoinedAttribute(Expression attributeExpression) {
        this.getJoinedAttributes().add(attributeExpression);
    }

    public void addJoinedAttributeExpression(Expression attributeExpression) {
        if(!getJoinedAttributeExpressions().contains(attributeExpression)) {
            getJoinedAttributeExpressions().add(attributeExpression);
        }
    }

    /**
     * Add an attribute represented by the given attribute name to the list of joins for this query.
     * Note: Mapping level joins are represented separately from query level joins.
     */
    public void addJoinedMappingExpression(Expression mappingExpression) {
        getJoinedMappingExpressions().add(mappingExpression);
    }

    /**
     * Add an attribute represented by the given attribute name to the list of joins for this query.
     * Note: Mapping level joins are represented separately from query level joins.
     */
    public void addJoinedMapping(String attributeName) {
        addJoinedMappingExpression(this.baseExpressionBuilder.get(attributeName));
    }

    /**
     * Clones the Joined Attribute Manager.  Generally called from Query.clone().
     */
    @Override
    public JoinedAttributeManager clone(){
        JoinedAttributeManager joinManager = null;
        try {
            joinManager = (JoinedAttributeManager)super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new InternalError(exception.toString());
        }
        if (this.joinedAttributeExpressions != null) {
            joinManager.joinedAttributeExpressions = new ArrayList<Expression>(this.joinedAttributeExpressions);
        }
        if (this.joinedMappingExpressions != null) {
            joinManager.joinedMappingExpressions = new ArrayList<Expression>(this.joinedMappingExpressions);
        }
        if (this.joinedAttributes != null) {
            joinManager.joinedAttributes = new ArrayList<Expression>(this.joinedAttributes);
        }
        if (this.joinedMappingIndexes != null) {
            joinManager.joinedMappingIndexes = new HashMap<DatabaseMapping, Object>(this.joinedMappingIndexes);
        }
        if (this.joinedMappingQueries != null) {
            joinManager.joinedMappingQueries = new HashMap<DatabaseMapping, ObjectLevelReadQuery>(this.joinedMappingQueries);
        }
        if (this.orderByExpressions != null) {
            joinManager.orderByExpressions = new ArrayList<Expression>(this.orderByExpressions);
        }
        if (this.additionalFieldExpressions != null) {
            joinManager.additionalFieldExpressions = new ArrayList<Expression>(this.additionalFieldExpressions);
        }
        if (this.joinedAttributeMappings != null) {
            joinManager.joinedAttributeMappings = new ArrayList<DatabaseMapping>(this.joinedAttributeMappings);
        }
        if (this.joinedAggregateMappings !=null) {
            joinManager.joinedAggregateMappings = new ArrayList<DatabaseMapping>(this.joinedAggregateMappings);
        }
        return joinManager;
    }

    /**
     * Copies settings from another manager. Should copy all the attributes that clone method clones.
     */
    public void copyFrom(JoinedAttributeManager otherJoinManager){
        this.joinedAttributeExpressions = otherJoinManager.joinedAttributeExpressions;
        this.joinedMappingExpressions = otherJoinManager.joinedMappingExpressions;
        this.joinedAttributes = otherJoinManager.joinedAttributes;
        this.joinedMappingIndexes = otherJoinManager.joinedMappingIndexes;
        this.joinedMappingQueries = otherJoinManager.joinedMappingQueries;
        this.orderByExpressions = otherJoinManager.orderByExpressions;
        this.additionalFieldExpressions = otherJoinManager.additionalFieldExpressions;
        this.joinedAttributeMappings = otherJoinManager.joinedAttributeMappings;
        this.joinedAggregateMappings = otherJoinManager.joinedAggregateMappings;
    }

    /**
     * Clear the joining state.  This is used to redefine a queries joins for nested joins.
     */
    public void clear(){
        this.joinedAttributeExpressions = null;
        this.joinedMappingExpressions = null;
        this.joinedAttributes = null;
        this.joinedMappingIndexes = null;
        this.isToManyJoin = false;
        this.hasOuterJoinedAttribute = false;
        this.joinedMappingQueries = null;
        this.joinedMappingQueryClones = null;
        this.orderByExpressions = null;
        this.additionalFieldExpressions = null;
        this.joinedAttributeMappings = null;
        this.joinedAggregateMappings = null;
    }

    /**
     * For joining the resulting rows include the field/values for many objects.
     * As some of the objects may have the same field names, these row partitions need to be calculated.
     * The indexes are stored in the query and used later when building the objects.
     */
    public int computeJoiningMappingIndexes(boolean includeAllSubclassFields, AbstractSession session, int offset) {
        if (!hasJoinedExpressions()) {
            return offset;
        }
        setJoinedMappingIndexes_(new HashMap(getJoinedAttributeExpressions().size() + getJoinedMappingExpressions().size()));
        int fieldIndex = 0;
        if (getBaseQuery().hasPartialAttributeExpressions()) {
            fieldIndex = getDescriptor().getPrimaryKeyFields().size(); // Query will select pks
            //next check for any partial attributes that are not joined attributes
            Iterator partialAttributes = ((ObjectLevelReadQuery)getBaseQuery()).getPartialAttributeExpressions().iterator();
            while(partialAttributes.hasNext()){
                Expression expression = (Expression)partialAttributes.next();
                if (expression.isQueryKeyExpression()){
                    if (!getJoinedMappingExpressions().contains(expression) && ! getJoinedAttributeExpressions().contains(expression)){
                        fieldIndex += ((QueryKeyExpression)expression).getFields().size();
                    }
                }
            }
        } else if (getBaseQuery().hasExecutionFetchGroup()) {
            fieldIndex = ((ObjectLevelReadQuery)getBaseQuery()).getFetchGroupNonNestedFieldsSet().size();
        } else {
            if (includeAllSubclassFields) {
                fieldIndex = getDescriptor().getAllSelectionFields((ObjectLevelReadQuery)getBaseQuery()).size();
            } else {
                fieldIndex = getDescriptor().getSelectionFields((ObjectLevelReadQuery)getBaseQuery()).size();
            }
        }
        fieldIndex += offset;
        fieldIndex = computeIndexesForJoinedExpressions(getJoinedAttributeExpressions(), fieldIndex, session);
        fieldIndex = computeIndexesForJoinedExpressions(getJoinedMappingExpressions(), fieldIndex, session);
        return fieldIndex;
    }

    /**
     * This method is used when computing the nested queries for joined mappings.
     * It recurses computing the nested mapping queries and their join indexes.
     */
    protected void computeNestedQueriesForJoinedExpressions(List joinedExpressions, AbstractSession session, ObjectLevelReadQuery readQuery) {
        for (int index = 0; index < joinedExpressions.size(); index++) {
            ObjectExpression objectExpression = (ObjectExpression)joinedExpressions.get(index);

            // Expression may not have been initialized.
            objectExpression.getBuilder().setSession(session.getRootSession(null));
            if (objectExpression.getBuilder().getQueryClass() == null){
                objectExpression.getBuilder().setQueryClass(descriptor.getJavaClass());
            }
            //get the first expression after the builder that is not an aggregate, and populate the aggregateMapping list if there are aggregates
            ObjectExpression baseExpression = objectExpression.getFirstNonAggregateExpressionAfterExpressionBuilder(getJoinedAggregateMappings());

            // PERF: Cache local join attribute Expression.
            this.addJoinedAttribute(baseExpression);

            DatabaseMapping mapping = baseExpression.getMapping();
            this.getJoinedAttributeMappings().add(mapping);

            // focus on the base expression.  Nested queries will handle nested expressions, and only need to be processed once
            if (mapping.isForeignReferenceMapping() && !getJoinedMappingQueries_().containsKey(mapping)) {
                // A nested query must be built to pass to the descriptor that looks like the real query execution would.
                ObjectLevelReadQuery nestedQuery = ((ForeignReferenceMapping)mapping).prepareNestedJoins(this, readQuery, session);
                if (nestedQuery != null) {
                    // Register the nested query to be used by the mapping for all the objects.
                    getJoinedMappingQueries_().put(mapping, nestedQuery);
                }
                if (mapping.isCollectionMapping()){
                    ((CollectionMapping)mapping).getContainerPolicy().addNestedJoinsQueriesForMapKey(this, readQuery, session);
                }
            }
        }
    }

    /**
     * Used to optimize joining by pre-computing the nested join queries for the mappings.
     */
    public void computeJoiningMappingQueries(AbstractSession session) {
        if (hasJoinedExpressions()) {
            this.joinedAttributeMappings = new ArrayList<DatabaseMapping>(getJoinedAttributeExpressions().size() + getJoinedMappingExpressions().size());
            this.joinedAttributes = new ArrayList<Expression>(getJoinedAttributeExpressions().size() + getJoinedMappingExpressions().size());
            setJoinedMappingQueries_(new HashMap(getJoinedAttributeExpressions().size() + getJoinedMappingExpressions().size()));
            computeNestedQueriesForJoinedExpressions(getJoinedAttributeExpressions(), session, (ObjectLevelReadQuery)this.baseQuery);
            computeNestedQueriesForJoinedExpressions(getJoinedMappingExpressions(), session, (ObjectLevelReadQuery)this.baseQuery);
        }
    }

    /**
     * This method is used when computing the indexes for joined mappings. It iterates through a list of join
     * expressions and adds an index that represents where the fields represented by that expression will appear
     * in the row returned by a read query.
     * Method {@link #computeNestedQueriesForJoinedExpressions(List, AbstractSession, ObjectLevelReadQuery)}
     * must be already called.
     * @param joinedExpressions Join expressions {@link List}.
     * @param currentIndex      Current joined mapping index.
     * @param session           Current session.
     * @return Current joined mapping index updated.
     */
    protected int computeIndexesForJoinedExpressions(final List joinedExpressions, int currentIndex,
            final AbstractSession session) {
        for (int index = 0; index < joinedExpressions.size(); index++) {
            final ObjectExpression objectExpression = (ObjectExpression)joinedExpressions.get(index);
            final DatabaseMapping mapping = objectExpression.getMapping();
            // Only store the index if this is the local expression to avoid it being added multiple times.
            // This means the base local expression must be first on the list, followed by nested expressions.
            final ObjectExpression localExpression = objectExpression
                    .getFirstNonAggregateExpressionAfterExpressionBuilder(new ArrayList(1));
            if ((localExpression == objectExpression) && (mapping != null) && mapping.isForeignReferenceMapping()) {
                getJoinedMappingIndexes_().put(mapping, Integer.valueOf(currentIndex));
            }
            final ClassDescriptor descriptor = mapping.getReferenceDescriptor();
            int numberOfFields = 0;
            if (descriptor == null) {
                // Direct-collection mappings do not have descriptor.
                if (mapping.isDirectCollectionMapping()) {
                    numberOfFields = 1;
                }
            } else {
                final ObjectLevelReadQuery nestedQuery = getNestedJoinedMappingQuery(objectExpression);
                FetchGroup fetchGroup = null;
                if(descriptor.hasFetchGroupManager()) {
                    fetchGroup = nestedQuery.getExecutionFetchGroup();
                }
                if(fetchGroup != null) {
                    numberOfFields = nestedQuery.getFetchGroupNonNestedFieldsSet(mapping).size();
                } else {
                    if (objectExpression.isQueryKeyExpression()
                            && objectExpression.isUsingOuterJoinForMultitableInheritance()) {
                        numberOfFields = descriptor.getAllSelectionFields(nestedQuery).size();
                    } else if(objectExpression.isQueryKeyExpression() && objectExpression.getDescriptor() != null
                            && objectExpression.getDescriptor().hasInheritance()
                            && objectExpression.getDescriptor().getInheritancePolicy().shouldReadSubclasses()) {
                        numberOfFields = descriptor.getAllFields().size();
                    } else {
                        numberOfFields = descriptor.getSelectionFields(nestedQuery).size();
                    }
                }
            }
            if (mapping.isCollectionMapping()){
                // map keys are indexed within the collection's row.
                // Therefore we use an offset from within the collections row
                numberOfFields += ((CollectionMapping)mapping).getContainerPolicy()
                        .updateJoinedMappingIndexesForMapKey(getJoinedMappingIndexes_(), numberOfFields);
            }
            currentIndex = currentIndex + numberOfFields;
        }
        return currentIndex;
    }

    /**
     * Get the list of additional field expressions.
     */
    public List<Expression> getAdditionalFieldExpressions() {
        if (this.additionalFieldExpressions == null){
            this.additionalFieldExpressions = new ArrayList<Expression>();
        }
        return additionalFieldExpressions;
    }

    /**
     * Get the list of additional field expressions.
     */
    public List<Expression> getAdditionalFieldExpressions_() {
        return additionalFieldExpressions;
    }

    /**
     * Returns the base expression builder for this query.
     */
    public ExpressionBuilder getBaseExpressionBuilder(){
        return this.baseExpressionBuilder;
    }

    /**
     * Returns the base query.
     */
    public ObjectBuildingQuery getBaseQuery(){
        return this.baseQuery;
    }

    /**
     * Return  all of the rows fetched by the query, used for 1-m joining.
     */
    public List<AbstractRecord> getDataResults_() {
        return dataResults;
    }

    public ClassDescriptor getDescriptor(){
        if (this.descriptor == null){
            this.descriptor = this.baseQuery.getDescriptor();
        }
        return this.descriptor;
    }

    /**
     * Return if there are additional field expressions.
     */
    public boolean hasAdditionalFieldExpressions() {
        return (this.additionalFieldExpressions != null) && (!this.additionalFieldExpressions.isEmpty());
    }

    /**
     * Set the list of additional field expressions.
     */
    public void setAdditionalFieldExpressions_(List<Expression> expressions) {
        this.additionalFieldExpressions = expressions;
    }

    /**
     * Return the attributes that must be joined.
     */
    public List<DatabaseMapping> getJoinedAggregateMappings() {
        if (this.joinedAggregateMappings == null){
            this.joinedAggregateMappings = new ArrayList<DatabaseMapping>();
        }
        return joinedAggregateMappings;
    }

    /**
     * Return the attributes that must be joined.
     */
    public List<Expression> getJoinedAttributeExpressions() {
        if (this.joinedAttributeExpressions == null){
            this.joinedAttributeExpressions = new ArrayList<Expression>();
        }
        return joinedAttributeExpressions;
    }

    /**
     * Return the attributes that must be joined.
     */
    public List<DatabaseMapping> getJoinedAttributeMappings() {
        if (this.joinedAttributeMappings == null){
            this.joinedAttributeMappings = new ArrayList<DatabaseMapping>();
        }
        return this.joinedAttributeMappings;
    }

    /**
     * Return the attributes that must be joined.
     */
    public List<Expression> getJoinedAttributes() {
        if (this.joinedAttributes == null){
            this.joinedAttributes = new ArrayList<Expression>();
        }
        return this.joinedAttributes;
    }

    /**
     * Get the list of expressions that represent elements that are joined because of their
     * mapping for this query.
     */
    public List<Expression> getJoinedMappingExpressions() {
        if (this.joinedMappingExpressions == null){
            this.joinedMappingExpressions = new ArrayList<Expression>();
        }
        return joinedMappingExpressions;
    }

    /**
     * Return the attributes that must be joined.
     */
    public boolean hasJoinedAttributeExpressions() {
        return (this.joinedAttributeExpressions != null) && (!this.joinedAttributeExpressions.isEmpty());
    }

    /**
     * This method checks both attribute expressions and mapping expressions and
     * determines if there are any joins to be made.
     */
    public boolean hasJoinedExpressions() {
        return hasJoinedAttributeExpressions() || hasJoinedMappingExpressions();
    }

    /**
     * Return the attributes that must be joined.
     */
    public boolean hasJoinedMappingExpressions() {
        return (this.joinedMappingExpressions != null) && (!this.joinedMappingExpressions.isEmpty());
    }

    /**
     * Return if any attributes are joined.  This is a convience method that
     * is only valid after prepare.
     */
    public boolean hasJoinedAttributes() {
        return (this.joinedAttributes != null) && (!this.joinedAttributes.isEmpty());
    }

    /**
     * PERF: Return if the query uses any outer attribute joins, used to avoid null checks in building objects.
     */
    public boolean hasOuterJoinedAttributeQuery() {
        return this.hasOuterJoinedAttribute;
    }

    /**
     * Get the list of orderBy expressions.
     */
    public List<Expression> getOrderByExpressions() {
        if (this.orderByExpressions == null){
            this.orderByExpressions = new ArrayList<Expression>();
        }
        return orderByExpressions;
    }

    /**
     * Get the list of orderBy expressions.
     */
    public List<Expression> getOrderByExpressions_() {
        return orderByExpressions;
    }

    /**
     * INTERNAL:
     *  Helper method to get the value from the clone for the expression passed in, triggering joins on
     *  all intermediate steps.
     *  Example expression "emp.project.pk" with a clone Employee will trigger indirection and return
     *  the project pk value.
     * @param session
     * @param clone
     * @param expression
     * @return
     */
    public Object getValueFromObjectForExpression(AbstractSession session, Object clone, ObjectExpression expression){
        if (!expression.isExpressionBuilder()){
            //can only operate over querykeys representing aggregate Objects.  Indirection should not be needed
            Object baseValue = this.getValueFromObjectForExpression(session, clone, (ObjectExpression)expression.getBaseExpression());
            if ( baseValue == null ) {
                return null;
            }
            DatabaseMapping mapping = expression.getMapping();
            Object attributeValue = mapping.getRealAttributeValueFromObject(baseValue, session);
            if (attributeValue != null) {
                if (mapping.isForeignReferenceMapping() && (((ForeignReferenceMapping)mapping).getIndirectionPolicy().usesTransparentIndirection())) {
                    //getRealAttributeValueFromObject does not trigger transparent indirection, but instantiateObject will (it calls size on it)
                    ((ForeignReferenceMapping)mapping).getIndirectionPolicy().instantiateObject(baseValue, attributeValue);
                }
            }
            return attributeValue;

        }
        return clone;
    }

    /**
     * Return if there are orderBy expressions.
     */
    public boolean hasOrderByExpressions() {
        return (this.orderByExpressions != null) && (!this.orderByExpressions.isEmpty());
    }

    /**
     * Set the list of orderBy expressions.
     */
    public void setOrderByExpressions_(List<Expression> expressions) {
        this.orderByExpressions = expressions;
    }

    /**
     * Return if the query uses any -m joins, and thus return duplicate/multiple rows.
     */
    public boolean isToManyJoin() {
        return this.isToManyJoin;
    }

    /**
     * Return if the attribute is specified for joining.
     */
    public boolean isAttributeJoined(ClassDescriptor mappingDescriptor, DatabaseMapping attributeMapping) {
        // Since aggregates share the same query as their parent, must avoid the aggregate thinking
        // the parents mappings is for it, (queries only share if the aggregate was not joined).
        //This isn't taking into account inheritance - a query on a child may use/join parent level mappings
        if (this.hasJoinedAttributes()) {
            //if it has joined attributes, the other collections must also be set and so don't need to be checked
            if (attributeMapping.isAggregateMapping()){
                return this.getJoinedAggregateMappings().contains(attributeMapping);
            } else {
                return this.getJoinedAttributeMappings().contains(attributeMapping);
            }}
        return isAttributeExpressionJoined(attributeMapping) || isAttributeMappingJoined(attributeMapping);
    }

    /**
     * Iterate through a list of expressions searching for the given attribute name.
     * Return true if it is found, false otherwise.  Only use if the query was preprepared so that join expressions
     * were processed.
     */
    protected boolean isMappingInJoinedExpressionList(DatabaseMapping attributeMapping, List joinedExpressionList) {
        for (Iterator joinEnum = joinedExpressionList.iterator(); joinEnum.hasNext();) {
            List aggregateMappings = new ArrayList();
            ObjectExpression expression = ((ObjectExpression)joinEnum.next()).getFirstNonAggregateExpressionAfterExpressionBuilder(aggregateMappings);
            if (attributeMapping.isAggregateObjectMapping() && aggregateMappings.contains(attributeMapping)) {
                return true;
            } else if (attributeMapping.equals(expression.getMapping())) {//expression may not have been processed yet
                return true;
            }
        }
        return false;
    }


   /**
    * Iterate through a list of expressions searching for the given attribute name.
    * Return true if it is found, false otherwise.
    */
    protected boolean isAttributeNameInJoinedExpressionList(String attributeName, List joinedExpressionList) {
        for (Iterator joinEnum = joinedExpressionList.iterator(); joinEnum.hasNext();) {
            QueryKeyExpression expression = (QueryKeyExpression)joinEnum.next();
            while (!expression.getBaseExpression().isExpressionBuilder()) {
                expression = (QueryKeyExpression)expression.getBaseExpression();
            }
            if (expression.getName().equals(attributeName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return if the attribute is specified for joining.
     */
    protected boolean isAttributeExpressionJoined(DatabaseMapping attributeMapping) {
        return isMappingInJoinedExpressionList(attributeMapping, getJoinedAttributeExpressions());
    }

    /**
     * Return whether the given attribute is joined as a result of a join on a mapping
     */
    protected boolean isAttributeMappingJoined(DatabaseMapping attributeMapping) {
        return isAttributeNameInJoinedExpressionList(attributeMapping.getAttributeName(), getJoinedMappingExpressions());
    }

    /**
     * Set the list of expressions that represent elements that are joined because of their
     * mapping for this query.
     */
    public void setJoinedAttributeExpressions_(List joinedExpressions) {
        this.joinedAttributeExpressions = joinedExpressions;
    }

    /**
     * Set the list of expressions that represent elements that are joined because of their
     * mapping for this query.
     */
    public void setJoinedMappingExpressions_(List joinedMappingExpressions) {
        this.joinedMappingExpressions = joinedMappingExpressions;
    }

    /**
     * Return the joined mapping indexes, used to compute mapping row partitions.
     */
    public Map<DatabaseMapping, Object> getJoinedMappingIndexes_() {
        return joinedMappingIndexes;
    }

    /**
     * Return the joined mapping queries, used optimize joining, only compute the nested queries once.
     */
    public Map<DatabaseMapping, ObjectLevelReadQuery> getJoinedMappingQueries_() {
        return joinedMappingQueries;
    }

    /**
     * INTERNAL:
     * Returns the nested query corresponding to the expression.
     * The passed expression should be either join mapping or joined attribute expression.
     */
    public ObjectLevelReadQuery getNestedJoinedMappingQuery(Expression expression) {
        // the first element of the list is the passed expression,
        // next one is its base, ...
        // the last one's base is ExpressionBuilder.
        ObjectExpression currentExpression = (ObjectExpression)expression;
        ArrayList<Expression> expressionBaseList = new ArrayList();
        do {
            //skip aggregates since they do not have nested query objects added to JoinedMappingQueries, instead
            //reference mappings on aggregates are added to the parent's joinAttributeManager
            if (!currentExpression.getMapping().isAggregateObjectMapping()){
                expressionBaseList.add(currentExpression);
            }
            currentExpression = (ObjectExpression)currentExpression.getBaseExpression();
        } while(!currentExpression.isExpressionBuilder());

        // the last expression in the list is not nested - its mapping should have corresponding nestedQuery.
        DatabaseMapping currentMapping = ((QueryKeyExpression)expressionBaseList.get(expressionBaseList.size() - 1)).getMapping();
        ObjectLevelReadQuery nestedQuery = getJoinedMappingQueries_().get(currentMapping);

        // unless the passed expression was not nested, repeat moving up the list.
        // the last step is the passed expression (first on the list) getting nested query corresponding to its mapping.
        for(int i = expressionBaseList.size() - 2; i >= 0; i--) {
            currentMapping = ((QueryKeyExpression)expressionBaseList.get(i)).getMapping();
            nestedQuery = nestedQuery.getJoinedAttributeManager().getJoinedMappingQueries_().get(currentMapping);
        }
        return nestedQuery;
    }

    /**
     * Set the joined mapping queries, used optimize joining, only compute the nested queries once.
     */
    public void setJoinedMappingQueries_(Map joinedMappingQueries) {
        this.joinedMappingQueries = joinedMappingQueries;
    }

    /**
     * Set the joined mapping indexes, used to compute mapping row partitions.
     */
    public void setJoinedMappingIndexes_(Map joinedMappingIndexes) {
        this.joinedMappingIndexes = joinedMappingIndexes;
    }

    /**
     * PERF: Set if the query uses any outer attribute joins, used to avoid null checks in building objects.
     */
    protected void setIsOuterJoinedAttributeQuery(boolean isOuterJoinedAttribute) {
        this.hasOuterJoinedAttribute = isOuterJoinedAttribute;
    }

    /**
     * Set if the query uses any -m joins, and thus return duplicate/multiple rows.
     */
    public void setIsToManyJoinQuery(boolean isToManyJoin) {
        this.isToManyJoin = isToManyJoin;
    }


    /**
     * Validate and prepare join expressions.
     */
    public void prepareJoinExpressions(AbstractSession session) {
        // The prepareJoinExpression check for outer-joins to set this to true.
        setIsOuterJoinedAttributeQuery(false);
        Expression lastJoinedAttributeBaseExpression = null;
        List groupedExpressionList = new ArrayList(getJoinedAttributeExpressions().size());
        for (int index = 0; index < getJoinedAttributeExpressions().size(); index++) {
            Expression expression = getJoinedAttributeExpressions().get(index);
            expression = prepareJoinExpression(expression, session);
            //EL bug 307497: break base expressions out onto the list and sort/group expressions by base expression
            lastJoinedAttributeBaseExpression = addExpressionAndBaseToGroupedList(expression, groupedExpressionList, lastJoinedAttributeBaseExpression);
        }
        //use the grouped list instead of the original
        this.setJoinedAttributeExpressions_(groupedExpressionList);
        for (int index = 0; index < getJoinedMappingExpressions().size(); index++) {
            Expression expression = getJoinedMappingExpressions().get(index);
            expression = prepareJoinExpression(expression, session);
            getJoinedMappingExpressions().set(index, expression);
        }
    }

    /**
     * adds expression and its base expressions recursively to the expressionList in groups, so that an expression is never listed before
     * its base expression
     * @param expression
     * @param expressionlist
     * @param lastJoinedAttributeBaseExpression
     * @return
     */
    protected Expression addExpressionAndBaseToGroupedList(Expression expression, List expressionlist, Expression lastJoinedAttributeBaseExpression){
        if(!expressionlist.contains(expression)) {
            int baseExpressionIndex = -1;
            boolean sameBase = false;//better than using instanceof BaseExpression.  If its not an objectExpression, it will get an exception in prepare anyway
            if((expression.isObjectExpression())) {
                Expression baseExpression = ((BaseExpression)expression).getBaseExpression();
                //filter out aggregate expressions between this and the next node.
                while (!baseExpression.isExpressionBuilder() && ((QueryKeyExpression)baseExpression).getMapping().isAggregateMapping()){
                    baseExpression = ((BaseExpression)baseExpression).getBaseExpression();
                }

                if(baseExpression != null && !baseExpression.isExpressionBuilder()) {
                    addExpressionAndBaseToGroupedList(baseExpression, expressionlist, lastJoinedAttributeBaseExpression);
                    // EL bug 307497
                    if (baseExpression != lastJoinedAttributeBaseExpression) {
                        baseExpressionIndex = getJoinedAttributeExpressions().indexOf(baseExpression);
                    } else {
                        sameBase = true;
                    }
                }
            }

            // EL bug 307497
            if (baseExpressionIndex == -1) {
                expressionlist.add(expression);
                if (!sameBase) {
                    lastJoinedAttributeBaseExpression = expression;
                }
            } else {
                //Add attributeExpression at baseExpressionIndex + 1.
                expressionlist.add(baseExpressionIndex+1, expression);
            }
        }
        return lastJoinedAttributeBaseExpression;
    }

    /**
     * Validate and prepare the join expression.
     */
    protected Expression prepareJoinExpression(Expression expression, AbstractSession session) {
        // Must be query key expression.
        if (!expression.isQueryKeyExpression()) {
            throw QueryException.mappingForExpressionDoesNotSupportJoining(expression);
        }
        QueryKeyExpression objectExpression = (QueryKeyExpression)expression;

        // Expression may not have been initialized.
        if (objectExpression.getBuilder().getQueryClass() == null) {
            objectExpression = (QueryKeyExpression)objectExpression.rebuildOn(this.baseExpressionBuilder);

            if (objectExpression.getBuilder().getQueryClass() == null) {
                objectExpression.getBuilder().setQueryClass(this.descriptor.getJavaClass());
            }
        }
        objectExpression.getBuilder().setSession(session.getRootSession(null));

        // Can only join relationships.
        if ((objectExpression.getMapping() == null) || (!objectExpression.getMapping().isJoiningSupported())) {
            throw QueryException.mappingForExpressionDoesNotSupportJoining(objectExpression);
        }

        // Search if any of the expression traverse a 1-m.
        ObjectExpression baseExpression = objectExpression;
        while (!baseExpression.isExpressionBuilder()) {
            //pulled from prepareJoinExpressions
            baseExpression.setShouldUseOuterJoinForMultitableInheritance(true);

            if (((QueryKeyExpression)baseExpression).shouldQueryToManyRelationship()) {
                setIsToManyJoinQuery(true);
            }
            if (baseExpression.shouldUseOuterJoin()) {
                setIsOuterJoinedAttributeQuery(true);
            }
            baseExpression = (ObjectExpression)baseExpression.getBaseExpression();
        }

        return objectExpression;
    }

    /**
     * This method collects the Joined Mappings from the descriptor and initializes them.
     * Excludes the mapping that are not in the passed mappingsAllowedToJoin set (if it's not null).
     */
    public void processJoinedMappings(AbstractSession session) {
        Set<String> fetchGroupAttributes = null;
        FetchGroup fetchGroup = getBaseQuery().getExecutionFetchGroup();
        if(fetchGroup != null) {
            fetchGroupAttributes = fetchGroup.getAttributeNames();
        }
        ObjectBuilder objectBuilder = getDescriptor().getObjectBuilder();
        if (objectBuilder.hasJoinedAttributes()) {
            List mappingJoinedAttributes = objectBuilder.getJoinedAttributes();
            if (!hasJoinedAttributeExpressions()) {
                for (int i = 0; i < mappingJoinedAttributes.size(); i++) {
                    ForeignReferenceMapping mapping = (ForeignReferenceMapping) mappingJoinedAttributes.get(i);
                    if(fetchGroupAttributes == null || fetchGroupAttributes.contains(mapping.getAttributeName())) {
                        addAndPrepareJoinedMapping(mapping, session);
                    }
                }
            } else {
                for (int i = 0; i < mappingJoinedAttributes.size(); i++) {
                    ForeignReferenceMapping mapping = (ForeignReferenceMapping) mappingJoinedAttributes.get(i);
                    if (!isAttributeExpressionJoined(mapping)) {
                        if(fetchGroupAttributes == null || fetchGroupAttributes.contains(mapping.getAttributeName())) {
                            addAndPrepareJoinedMapping(mapping, session);
                        }
                    }
                }
            }
        }
    }

    /**
     * Add the mapping for join fetch, prepare and return the join expression being used.
     */
    public Expression addAndPrepareJoinedMapping(ForeignReferenceMapping mapping, AbstractSession session) {
        Expression joinMappingExpression = null;
        if (mapping.isCollectionMapping()) {
            if (mapping.isInnerJoinFetched()) {
                joinMappingExpression = getBaseExpressionBuilder().anyOf(mapping.getAttributeName(), false);
            } else if (mapping.isOuterJoinFetched()) {
                joinMappingExpression = getBaseExpressionBuilder().anyOfAllowingNone(mapping.getAttributeName(), false);
            }
        } else {
            if (mapping.isInnerJoinFetched()) {
                joinMappingExpression = getBaseExpressionBuilder().get(mapping.getAttributeName());
            } else if (mapping.isOuterJoinFetched()) {
                joinMappingExpression = getBaseExpressionBuilder().getAllowingNull(mapping.getAttributeName());
            }
        }
        if (joinMappingExpression != null) {
            joinMappingExpression = prepareJoinExpression(joinMappingExpression, session);
            addJoinedMappingExpression(joinMappingExpression);
        }
        return joinMappingExpression;
    }

    /**
     * Reset the JoinedAttributeManager.  This will be called when the Query is re-prepared
     */
    public void reset(){
        this.joinedMappingExpressions = null;
        this.joinedAttributes = null;
        this.isToManyJoin = false;
        this.hasOuterJoinedAttribute = true;
        this.joinedMappingIndexes = null;
        this.joinedMappingQueries = null;
        this.dataResults = null;
        this.joinedAttributeMappings = null;
        this.joinedAggregateMappings = null;
    }

    /**
     * This method is called from within this package it is used when
     * initializing a report Item
     */
    public void setBaseQuery(ObjectLevelReadQuery query){
        this.baseQuery = query;
    }

    /**
     * This method is called from within this package, it is used when
     * initializing a ReportItem
     */
    protected void setBaseExpressionBuilder(ExpressionBuilder builder){
        this.baseExpressionBuilder = builder;
    }

    /**
     * Return all of the rows fetched by the query by cache-key, used for 1-m joining.
     */
    public Map<Object, List<AbstractRecord>> getDataResultsByPrimaryKey() {
        return dataResultsByPrimaryKey;
    }

    /**
     * Set all of the rows fetched by the query by cache-key, used for 1-m joining.
     */
    protected void setDataResultsByPrimaryKey(Map<Object, List<AbstractRecord>> dataResultsByPrimaryKey) {
        this.dataResultsByPrimaryKey = dataResultsByPrimaryKey;
    }

    /**
     * Set all of the rows fetched by the query, used for 1-m joining.
     */
    public void setDataResults(List dataResults, AbstractSession session) {
        this.dataResults = dataResults;
        processDataResults(session);
    }

    /**
     * Process the data-results for joined data for a 1-m join.
     * This allows all the data to be processed once, instead of n times for each object.
     */
    protected void processDataResults(AbstractSession session) {
        this.dataResultsByPrimaryKey = new HashMap();
        int size = this.dataResults.size();
        Object firstKey = null;
        Object lastKey = null;
        List<AbstractRecord> childRows = null;
        ObjectBuilder builder = getDescriptor().getObjectBuilder();
        int parentIndex = getParentResultIndex();
        Vector trimedFields = null;
        for (int dataResultsIndex = 0; dataResultsIndex < size; dataResultsIndex++) {
            AbstractRecord row = this.dataResults.get(dataResultsIndex);
            AbstractRecord parentRow = row;
            // Must adjust for the parent index to ensure the correct pk is extracted.
            if (parentIndex > 0) {
                if (trimedFields == null) { // The fields are always the same, so only build once.
                    trimedFields = new NonSynchronizedSubVector(row.getFields(), parentIndex, row.size());
                }
                Vector trimedValues = new NonSynchronizedSubVector(row.getValues(), parentIndex, row.size());
                parentRow = new DatabaseRecord(trimedFields, trimedValues);
            }
            // Extract the primary key of the source object, to filter only the joined rows for that object.
            Object sourceKey = builder.extractPrimaryKeyFromRow(parentRow, session);
            // May be any outer-join so ignore null.
            if (sourceKey != null) {
                if (firstKey == null) {
                    firstKey = sourceKey;
                }
                if ((lastKey != null) && lastKey.equals(sourceKey)) {
                    childRows.add(row);
                    if (shouldFilterDuplicates()) {
                        // Also null out the row because it is a duplicate to avoid object building processing it.
                        this.dataResults.set(dataResultsIndex, null);
                    }
                } else {
                    childRows =  this.dataResultsByPrimaryKey.get(sourceKey);
                    if (childRows == null) {
                        childRows = new ArrayList();
                        this.dataResultsByPrimaryKey.put(sourceKey, childRows);
                    } else {
                        if (shouldFilterDuplicates()) {
                            // Also null out the row because it is a duplicate to avoid object building processing it.
                            this.dataResults.set(dataResultsIndex, null);
                        }
                    }
                    childRows.add(row);
                    lastKey = sourceKey;
                }
            }
        }
        // If pagination is used, the first and last rows may be missing their 1-m joined rows, so reject them from the results.
        // This will cause them to build normally by executing a query.
        if (this.isToManyJoin) {
            if ((lastKey != null) && (this.baseQuery.getMaxRows() > 0)) {
                this.dataResultsByPrimaryKey.remove(lastKey);
            }
            if ((firstKey != null) && (this.baseQuery.getFirstResult() > 0)) {
                this.dataResultsByPrimaryKey.remove(firstKey);
            }
        }
    }

    /**
     * Clear the data-results for joined data for a 1-m join.
     */
    public void clearDataResults() {
        this.dataResults = null;
        this.dataResultsByPrimaryKey = null;
    }

    /**
     * Process the data-results for joined data for a 1-m join.
     * This allows incremental processing for a cursor.
     */
    public AbstractRecord processDataResults(AbstractRecord row, Cursor cursor, boolean forward) {
        if (this.dataResultsByPrimaryKey == null) {
            this.dataResultsByPrimaryKey = new HashMap();
        }
        AbstractRecord parentRow = row;
        List<AbstractRecord> childRows = new ArrayList<AbstractRecord>();
        childRows.add(row);
        int parentIndex = getParentResultIndex();
        // Must adjust for the parent index to ensure the correct pk is extracted.
        Vector trimedFields = new NonSynchronizedSubVector(row.getFields(), parentIndex, row.size());
        if (parentIndex > 0) {
            Vector trimedValues = new NonSynchronizedSubVector(row.getValues(), parentIndex, row.size());
            parentRow = new DatabaseRecord(trimedFields, trimedValues);
        }
        ObjectBuilder builder = getDescriptor().getObjectBuilder();
        AbstractSession session = cursor.getExecutionSession();
        // Extract the primary key of the source object, to filter only the joined rows for that object.
        Object sourceKey = builder.extractPrimaryKeyFromRow(parentRow, session);
        AbstractRecord extraRow = null;
        while (true) {
            AbstractRecord nextRow = null;
            if (forward) {
                nextRow = cursor.getAccessor().cursorRetrieveNextRow(cursor.getFields(), cursor.getResultSet(), session);
            } else {
                nextRow = cursor.getAccessor().cursorRetrievePreviousRow(cursor.getFields(), cursor.getResultSet(), session);
            }
            if (nextRow == null) {
                break;
            }
            AbstractRecord nextParentRow = nextRow;
            if (parentIndex > 0) {
                Vector trimedValues = new NonSynchronizedSubVector(nextParentRow.getValues(), parentIndex, nextParentRow.size());
                nextParentRow = new DatabaseRecord(trimedFields, trimedValues);
            }
            // Extract the primary key of the source object, to filter only the joined rows for that object.
            Object nextKey = builder.extractPrimaryKeyFromRow(nextParentRow, session);
            if ((sourceKey != null) && sourceKey.equals(nextKey)) {
                childRows.add(nextRow);
            } else {
                extraRow = nextRow;
                break;
            }
        }
        this.dataResultsByPrimaryKey.put(sourceKey, childRows);
        return extraRow;
    }

    /**
     * Called to set the descriptor on a Join Managerwith in a ReportItem, durring
     * initialization, and durring DatabaseQuery.checkDescriptor.
     */
    public void setDescriptor(ClassDescriptor descriptor){
        this.descriptor = descriptor;
    }

    /**
     * Used for joining in conjunction with pessimistic locking.
     * Iterate through a list of joined expressions and ensure expression is set on the locking
     * clause for each expression that represents a pessimisically locked descriptor.
     */
    public ForUpdateOfClause setupLockingClauseForJoinedExpressions(ForUpdateOfClause lockingClause, AbstractSession session) {
        if (hasJoinedAttributeExpressions()){
            return setupLockingClauseForJoinedExpressions(getJoinedAttributeExpressions(), session,lockingClause);
        }
        if (hasJoinedMappingExpressions()){
            return setupLockingClauseForJoinedExpressions(getJoinedMappingExpressions(), session,lockingClause);
        }
        return lockingClause;
    }

    /**
     * Used for joining in conjunction with pessimistic locking.
     * Iterate through a list of joined expressions and ensure expression is set on the locking
     * clause for each expression that represents a pessimisically locked descriptor.
     */
    private ForUpdateOfClause setupLockingClauseForJoinedExpressions(List joinedExpressions, AbstractSession session, ForUpdateOfClause lockingClause) {
        // Must iterate over all of the joined attributes, just check
        // if any of them have pessimistic locking defined on the descriptor.
        for (Iterator e = joinedExpressions.iterator(); e.hasNext();) {
            Expression expression = (Expression)e.next();

            // Expression has not yet been validated.
            if (expression.isObjectExpression()) {
                ObjectExpression joinedAttribute = (ObjectExpression)expression;

                // Expression may not have been initialized.
                joinedAttribute.getBuilder().setSession(session.getRootSession(null));
                if (joinedAttribute.getBuilder().getQueryClass() == null){
                    joinedAttribute.getBuilder().setQueryClass(descriptor.getJavaClass());
                }

                ClassDescriptor nestedDescriptor = joinedAttribute.getDescriptor();

                // expression may not be valid, no descriptor, validation occurs later.
                if (nestedDescriptor == null) {
                    return lockingClause;
                }
                if (nestedDescriptor.hasPessimisticLockingPolicy()) {
                    if (lockingClause == null) {
                        lockingClause = new ForUpdateOfClause();
                        lockingClause.setLockMode(nestedDescriptor.getCMPPolicy().getPessimisticLockingPolicy().getLockingMode());
                    }
                    lockingClause.addLockedExpression(joinedAttribute);
                }
            }
        }
        return lockingClause;
    }

    public void setParentResultIndex(int parentsResultIndex) {
        this.parentResultIndex = parentsResultIndex;
    }

    public int getParentResultIndex() {
        return parentResultIndex;
    }

    public Map<DatabaseMapping, ObjectLevelReadQuery> getJoinedMappingQueryClones() {
        return joinedMappingQueryClones;
    }

    public void setJoinedMappingQueryClones(Map joinedMappingQueryClones) {
        this.joinedMappingQueryClones = joinedMappingQueryClones;
    }
}
