/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

/*
 * RetrieveDescImpl.java
 *
 * Created on March 3, 2000
 *
 */

package com.sun.jdo.spi.persistence.support.sqlstore.sql;

import com.sun.jdo.api.persistence.support.JDOFatalInternalException;
import com.sun.jdo.spi.persistence.support.sqlstore.ActionDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.RetrieveDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.SQLStoreManager;
import com.sun.jdo.spi.persistence.support.sqlstore.model.ClassDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.model.LocalFieldDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.concurrency.Concurrency;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.constraint.Constraint;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.constraint.ConstraintFieldName;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.generator.SelectQueryPlan;
import com.sun.jdo.spi.persistence.utility.FieldTypeEnumeration;
import com.sun.jdo.spi.persistence.utility.I18NHelper;
import com.sun.jdo.spi.persistence.utility.ParameterInfo;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ResourceBundle;

/**
 */
public class RetrieveDescImpl extends Object implements RetrieveDesc, Cloneable {

    private static final int OPINFO_FIELD_DISALLOWED =  0x01; // 1

    private static final int OPINFO_FIELD_REQUIRED =    0x02; // 2

    private static final int OPINFO_VAL_DISALLOWED =    0x04; // 4

    private static final int OPINFO_VAL_REQUIRED =      0x08; // 8

    private static final int OPINFO_VAL_IS_PCOUNT =     0x10; // 16

    private static final int OPINFO_ILLEGAL =           0x20; // 32

    private static final int OPINFO_NO_OPERATION =      0x40; // 64

    /** Indicates, that the query projects on this RD. */
    public static final int OPT_PROJECTION =    0x1; // 1

    /** Indicates, that a distinct query should be run. */
    public static final int OPT_DISTINCT =      0x2; // 2

    /** Indicates, that the selected rows should be locked for update. */
    public static final int OPT_FOR_UPDATE =    0x4; // 4

    /** Indicates, that an avg aggregate query should be run. */
    public static final int OPT_AVG =           0x08; // 8

    /** Indicates, that a min aggregate query should be run. */
    public static final int OPT_MIN =           0x10; // 16

    /** Indicates, that a max aggregate query should be run. */
    public static final int OPT_MAX =           0x20; // 32

    /** Indicates, that a sum aggregate query should be run. */
    public static final int OPT_SUM =           0x40; // 64

    /** Indicates, that a count aggregate query should be run. */
    public static final int OPT_COUNT =         0x80; // 128

    /** Special treatment for count on persistent capable objects. */
    public static final int OPT_COUNT_PC =      0x100; // 256

    /** Sum of all aggregate options. */
    public static final int OPT_AGGREGATE = OPT_AVG + OPT_MIN + OPT_MAX +
            OPT_SUM + OPT_COUNT + OPT_COUNT_PC;

    /** Sum of all aggregate options excluding count on persistent capable objects. */
    public static final int OPT_AGGREGATE_NON_COUNT_PC = OPT_AGGREGATE - OPT_COUNT_PC;

    /** Indicates, that fetch group fields should be added. */
    public static final int OPT_ADD_FETCHGROUPS = 0x200; // 512

    /**
     * Indicates, that only key fields should be added. When this option is set,
     * it modifies meaning of OPT_ADD_FETCHGROUPS. It is assumed that
     * only key fieldes from the fetch group will be added.
     */
    public static final int OPT_ADD_KEYS_ONLY = 0x400; // 1024

    /**
     * Indicates, that even if relationship fields are in DFG, they should not
     * be prefetched. The runtime will attempt to fetch relationhip fields only
     * when OPT_ADD_FETCH_GROUPS is set.
     * @see #OPT_ADD_FETCHGROUPS
     * @see #OPT_ADD_KEYS_ONLY
     */
    public static final int OPT_DISABLE_RELATIONSHIP_PREFETCH =    0x800; // 2048

    /** Indicates special treatment for version consistency verifications. */
    public static final int OPT_VERIFY =       0x1000; // 4024

    /** Array of ConstraintFieldName. */
    private ArrayList fields;

    /** The constraint stack */
    private Constraint constraint;

    /** Bitmask of options for this instance as specified by the OPT_* constants */
    private int options;

    /** Candidate class of this instance */
    private Class pcClass;

    /** Config for candidate class of this instance */
    private ClassDesc config;

    /** SelectQueryPlan for this instance */
    private SelectQueryPlan plan;

    /**
     * Discriminates different retrieve descriptors which use the same
     * navigational field.
     */
    private Object navigationalId;

    /** Result type for an aggregate query. */
    private int aggregateResultType = FieldTypeEnumeration.NOT_ENUMERATED;

    /**
     * I18N message handler
     */
    private final static ResourceBundle messages = I18NHelper.loadBundle(
            "com.sun.jdo.spi.persistence.support.sqlstore.Bundle", // NOI18N
            RetrieveDescImpl.class.getClassLoader());

    public RetrieveDescImpl(Class pcClass, ClassDesc config) {
        super();

        this.pcClass = pcClass;
        this.config = config;
        fields = new ArrayList();
        constraint = new Constraint();
    }

    /**
     * The addResult method is used to specify which fields should be
     * returned in a persistent object. If the field requested is a
     * reference to another persistent object then a RetrieveDesc may be
     * provided which describes which fields of the referenced object
     * should be returned and, optionally, constraints on it.
     * If the parameter <code>projection</code> is true, the field
     * specified by <code>name</code> should be projected.
     *
     * @param name The name of the field to return.
     * @param foreignConstraint
     * RetrieveDesc describing fields and constraints for a referenced object.
     * @param projection Specifies, if this is a projection.
     */
    public void addResult(String name,
                          RetrieveDesc foreignConstraint,
                          boolean projection) {
        ConstraintFieldName cfName = new ConstraintFieldName(name, foreignConstraint);

        if (projection) {
            if ((options & OPT_PROJECTION) > 0) {
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "sqlstore.retrievedesc.toomanyprojections")); // NOI18N
            }

            // For projections on foreign fields, mark the foreign constraint.
            // For local fields, set the property on the field constraint.
            if (foreignConstraint != null) {
                ((RetrieveDescImpl) foreignConstraint).options |= OPT_PROJECTION;
            } else {
                cfName.setProjection();

                // Set this property if you want to have DFG fields added for
                // projections on local fields.
                //      options = options | OPT_PROJECTION;
            }
        }
        fields.add(cfName);
    }

    /**
     * The addResult method can be used to specify <it>global</it>
     * query attributes that don't end up in the where clause.
     * Aggregate functions and the distinct op code are examples for
     * those query options. The result type defines the object to be
     * returned by an aggregate query. In case of distinct the result
     * type should be FieldTypeEnumeration.NOT_ENUMERATED. The method
     * might be called twice, in case of a JDOQL query having both an
     * aggregate and distinct:
     * query.setResult("avg (distinct salary)");
     * ->
     * retrieveDesc.addResult(OP_AVG, FieldTypeEnumeration.DOUBLE);
     * retrieveDesc.addResult(OP_DISTINCT, FieldTypeEnumeration.NOT_ENUMERATED);
     * retrieveDesc.addResult("salary", null, true);
     *
     * @param opCode The operation code.
     * @param aggregateResultType The object type returned by aggregate queries.
     * @see com.sun.jdo.spi.persistence.utility.FieldTypeEnumeration
     */
    public void addResult(int opCode, int aggregateResultType) {
        switch (opCode) {
            case ActionDesc.OP_DISTINCT:
                options = options | OPT_DISTINCT;
                break;
            case ActionDesc.OP_AVG:
                options = options | OPT_AVG;
                break;
            case ActionDesc.OP_MIN:
                options = options | OPT_MIN;
                break;
            case ActionDesc.OP_MAX:
                options = options | OPT_MAX;
                break;
            case ActionDesc.OP_SUM:
                options = options | OPT_SUM;
                break;
            case ActionDesc.OP_COUNT:
                options = options | OPT_COUNT;
                break;
            case ActionDesc.OP_COUNT_PC:
                options = options | OPT_COUNT_PC;
                break;
            default:
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "core.constraint.illegalop", "" + opCode)); // NOI18N
        }

        if (aggregateResultType != FieldTypeEnumeration.NOT_ENUMERATED) {
            if (this.aggregateResultType == FieldTypeEnumeration.NOT_ENUMERATED) {
                this.aggregateResultType = aggregateResultType;
            } else {
                // aggregate result type has already been set
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "sqlstore.retrievedesc.toomanyresulttypes")); // NOI18N
            }
        }
    }

    /**
     * Add a field specified by <code>name</code> to the list of fields to be prefetched.
     * @param name Name of the field to be prefetched.
     * @param foreignConstraint This parameter is null if the field is a local field.
     * If the field is a foreign field, this parameter should be not null and must refer
     * to an instance of RetrieveDesc correpsonding to the config of the foreign field.
     */
    public void addPrefetchedField(String name, RetrieveDesc foreignConstraint) {
        fields.add(new ConstraintFieldName(name, foreignConstraint, true));
    }

    /**
     * {@inheritDoc}
     */
    public void setPrefetchEnabled(boolean prefetchEnabled) {
        if (!prefetchEnabled) {
            options |= OPT_DISABLE_RELATIONSHIP_PREFETCH;
        } else {
            // options has the flag OPT_DISABLE_RELATIONSHIP_PREFETCH unset by default
        }
    }

    /**
     * <P>Adds a constraint on the foreign field specified by
     * <code>name</code>. This method is used to specify a relationship
     * navigation on field <code>name</code> to the class represented by
     * the retrieve descriptor <code>foreignConstraint</code>.
     * If <code>name</code> is null, an unrelated constraint is added.
     * A constraint is unrelated, if there is neither a foreign field
     * nor a local field connecting to the retrieve descriptor
     * <code>foreignConstraint</code>.
     */
    public void addConstraint(String name,
                              RetrieveDesc foreignConstraint) {
        if (name == null) {
            constraint.addField(null, foreignConstraint);
        } else  {
            constraint.addForeignField(name, foreignConstraint);
        }
    }

    /**
     * <P>Adds a constraint on the relationship field specified by
     * <code>name</code>.
     * This method is useful e.g. for comparisons of local fields with field of a related object:
     *   emp.addConstraint("lastName", ActionDesc.OP_EQ, mgr, lastName");
     * compares the employee's lastName field with the lastName field of the related manager.
     */
    public void addConstraint(String name, int operation,
                              RetrieveDesc foreignConstraint, String foreignName) {
        constraint.addField(foreignName, foreignConstraint);
        constraint.addField(name, null);
        constraint.addOperation(operation);
    }

    /**
     * The addConstraint method is used to limit the values of fields for
     * objects being selected.
     * addConstraint pushes the value, name, and operation onto the
     * Constraint stack.  The constraints are anded together by default.
     * Arbitrarily complex relationships on the Constraint stack are supported.
     * The Constraint stack can be manipulated directly if necessary.
     *
     * @param name
     * 	The name parameter specifies the field whose value
     * 	should be limited.
     * @param operation
     * 	The operation parameter specifies the relationship the field
     * 	should bear to the value.  Values for operation are defined in
     * 	the ActionDesc interface.
     * @param value
     * 	The value parameter usually specifies the value to which the
     * 	field should be limited, however it is sometimes used to
     * 	hold a parameter count as for the OP_IN operation.
     */
    public void addConstraint(String name,
                              int operation,
                              Object value) {
        int info = getOperationInfo(operation);

        if ((info & OPINFO_ILLEGAL) > 0) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                    "core.constraint.illegalop", "" + operation)); // NOI18N
        } else if ((info & OPINFO_FIELD_REQUIRED) > 0 && name == null) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                    "core.constraint.fieldrequired", "" + operation)); // NOI18N
        } else if ((info & OPINFO_FIELD_DISALLOWED) > 0 && name != null) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                    "core.constraint.fielddisallowed", "" + operation)); // NOI18N
        } else if ((info & OPINFO_VAL_REQUIRED) > 0 && value == null) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                    "core.constraint.valrequired", "" + operation)); // NOI18N
        } else if ((info & OPINFO_VAL_DISALLOWED) > 0 && value != null) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                    "core.constraint.valdisallowed", "" + operation)); // NOI18N
        }
        if ((info & OPINFO_VAL_IS_PCOUNT) > 0) {
            if (name != null) {
                constraint.addField(name, null);
            }
            addValueConstraint(name, value);
        } else {
            switch (operation) {
                case RetrieveDescImpl.OP_PARAMETER:
                    addParameterConstraint(value);
                    break;
                case ActionDesc.OP_IN:
                case ActionDesc.OP_NOTIN:
                    constraint.addConstraintFieldSubQuery(name,(ActionDesc) value);
                    break;
                default:
                    if (value != null) {
                        addValueConstraint(name, value);
                    }
                    if (name != null) {
                        constraint.addField(name, null);
                    }
                    break;
            }
        }
        if ((info & OPINFO_NO_OPERATION) == 0) {
            constraint.addOperation(operation);
        }
    }

    /**
     * Add Constraints corresponding to given <code>fields</code>.
     * The constraints are added as Parameter Constraints.
     * index of the parameter starts at given <code>startIndex</code>
     * @param fields fields for which constraints are to be added.
     * @param startIndex starting Index for the parameter.
     */
    public void addParameterConstraints(LocalFieldDesc[] fields, int startIndex) {
        for (int i = 0; i < fields.length; i++) {
            LocalFieldDesc field = fields[i];
            addParameterConstraint(field, i + startIndex);
        }
    }

    /**
     * Add ParameterConstraint corresponding to given <code>field</code>
     * at given  <code>index</code>.
     * @param field The field for which constraints are to be added.
     * @param index Index at which the ParameterConstraint is to be inserted.
     */
    public void addParameterConstraint(LocalFieldDesc field, int index) {
        // Generate parameter info for this parameter.
        String fieldName = field.getName();
        int type = field.getEnumType();

        addConstraint(null, ActionDesc.OP_PARAMETER,new ParameterInfo(index, type, fieldName));
        addConstraint(fieldName, ActionDesc.OP_FIELD, null);
        addConstraint(null, ActionDesc.OP_EQ, null);
    }

    /**
     * Adds information about <code>value</code> on the constraint stack.
     * @param name Name of the field to which the specified value is bound.
     * The query compiler can detect the correct value for name only in
     * case of simple expressions in the filter. A simple expression is
     * <em>fieldName op value</em>. If the compiler is not able to detect
     * correct value for name it will pass null.
     * @param value value to which the field's value is constrained.
     */
    private void addValueConstraint(String name, Object value) {
        constraint.addValue(value, getLocalFieldDesc(name));
    }

    /**
     * Adds information about parameter on the constraint stack.
     * @param value Instance of
     * <code>com.sun.jdo.spi.persistence.utility.ParameterInfo</code>.
     * Contains index, type and name of the field to which
     * this parameter is bound. The field name is used when binding
     * the parameter to the sql statement.
     * name can be null for complex expressions in a filter as described in
     * addValueConstraint.
     *  @see #addValueConstraint
     */
    private void addParameterConstraint(Object value) {
        if (value instanceof ParameterInfo) {
            ParameterInfo parameterInfo = (ParameterInfo)value;
            constraint.addParamIndex(parameterInfo.getIndex(), parameterInfo.getType(),
                    getLocalFieldDesc(parameterInfo.getAssociatedField()));
        } else {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                    "core.constraint.illegalParameterInfo")); // NOI18N
        }
    }

    /**
     * Returns the local field descriptor for the field <code>name</code>.
     * Delegates to <code>ClassDesc#getLocalFieldDesc(String)<code>.
     *
     * @param name Field name.
     * @return null if the <code>name</code> is null.
     * LocalFieldDesc for the field <code>name</code> otherwise.
     */
    private LocalFieldDesc getLocalFieldDesc(String name) {
        return name == null ? null : config.getLocalFieldDesc(name);
    }

    /**
     * 	The getOperationInfo method returns information about the operation
     * 	requested.  The following constants define different properties of
     * 	an operation and can be or'd together:
     *   OPINFO_FIELD_DISALLOWED A field parameter cannot be specified
     *                           with this operation
     *   OPINFO_FIELD_REQUIRED   A field parameter must be specified
     *                           with this operation
     *   OPINFO_VAL_DISALLOWED	 A value parameter cannot be specified
     *                           with this operation
     *   OPINFO_VAL_REQUIRED     A value parameter must be specified
     *                           with this operation
     *   OPINFO_NULL             This operation is a "null" type operation
     *                           (i.e. "is null", or "is not null")
     *   OPINFO_ILLEGAL          This operation is illegal
     *   OPINFO_NO_OPERATION     This isn't an operation at all,
     *                           it is some other specifier.
     *   OPINFO_VAL_IS_PCOUNT    This operation has a value which is the
     *                           parameter count of following parameter
     *                           nodes
     *
     * @param operation
     *   The operation parameter specifies the operation for which
     *   information is desired.
    */
    private static int getOperationInfo(int operation) {
        int info;
        switch (operation) {
            case ActionDesc.OP_ABS:
                info = OPINFO_VAL_DISALLOWED;
                break;
            case ActionDesc.OP_ADD:
                info = 0;
                break;
            case ActionDesc.OP_AND:
                info = 0;
                break;
            case ActionDesc.OP_FIELD:
                info = OPINFO_FIELD_REQUIRED | OPINFO_VAL_DISALLOWED | OPINFO_NO_OPERATION;
                break;
            case ActionDesc.OP_BETWEEN:
                info = 0;
                break;
            case ActionDesc.OP_DIV:
                info = 0;
                break;
            case ActionDesc.OP_EQ:
                info = 0;
                break;
            case ActionDesc.OP_NE:
                info = 0;
                break;
            case ActionDesc.OP_EQUIJOIN:
                info = 0;
                break;
            case ActionDesc.OP_NOT:
                info = OPINFO_FIELD_DISALLOWED | OPINFO_VAL_DISALLOWED;
                break;
            case ActionDesc.OP_GE:
                info = 0;
                break;
            case ActionDesc.OP_GT:
                info = 0;
                break;
            case ActionDesc.OP_IN:
            case ActionDesc.OP_NOTIN:
                info = OPINFO_FIELD_REQUIRED | OPINFO_VAL_REQUIRED;
                break;
            case ActionDesc.OP_LE:
                info = 0;
                break;
            case ActionDesc.OP_LEFTJOIN:
                info = 0;
                break;
            case ActionDesc.OP_LENGTH:
                info = OPINFO_VAL_DISALLOWED;
                break;
            case ActionDesc.OP_LIKE:
                info = 0;
                break;
            case ActionDesc.OP_LIKE_ESCAPE:
                info = 0;
                break;
            case ActionDesc.OP_LT:
                info = 0;
                break;
            case ActionDesc.OP_LTRIM:
                info = OPINFO_VAL_DISALLOWED;
                break;
            case ActionDesc.OP_MUL:
                info = 0;
                break;
            case ActionDesc.OP_OR:
                info = 0;
                break;
            case ActionDesc.OP_ORDERBY:
                info = 0;
                break;
            case ActionDesc.OP_ORDERBY_DESC:
                info = 0;
                break;
            case ActionDesc.OP_PARAMETER_COUNT:
                info = OPINFO_FIELD_DISALLOWED | OPINFO_VAL_REQUIRED | OPINFO_VAL_IS_PCOUNT | OPINFO_NO_OPERATION;
                break;
            case ActionDesc.OP_RIGHTJOIN:
                info = 0;
                break;
            case ActionDesc.OP_RTRIM:
                info = OPINFO_VAL_DISALLOWED;
                break;
            case ActionDesc.OP_SQRT:
                info = OPINFO_VAL_DISALLOWED;
                break;
            case ActionDesc.OP_SUB:
                info = 0;
                break;
            case ActionDesc.OP_SUBSTRING:
                info = 0;
                break;
            case ActionDesc.OP_POSITION:
                info = 0;
                break;
            case ActionDesc.OP_POSITION_START:
                info = 0;
                break;
            case ActionDesc.OP_MAYBE_NULL:
                info = OPINFO_FIELD_REQUIRED;
                break;
            case ActionDesc.OP_CONCAT:
                info = 0;
                break;
            case ActionDesc.OP_VALUE:
                info = OPINFO_FIELD_DISALLOWED | OPINFO_VAL_REQUIRED | OPINFO_NO_OPERATION;
                break;
            case ActionDesc.OP_PARAMETER:
                info = OPINFO_FIELD_DISALLOWED | OPINFO_VAL_REQUIRED | OPINFO_NO_OPERATION;
                break;
            case OP_NULL:
            case OP_NOTNULL:
                info = 0;
                break;
            case ActionDesc.OP_MOD:
                info = 0;
                break;
            default:
                info = OPINFO_ILLEGAL;
                break;
        }
        return info;
    }

    /**
     * Builds the internal query plan and initializes the select statements.
     * Projections on collection fields will not be resolved until the actual
     * retrieval in {@link SQLStoreManager#retrieve(
     * com.sun.jdo.spi.persistence.support.sqlstore.PersistenceManager,
     *       RetrieveDesc, com.sun.jdo.spi.persistence.support.sqlstore.ValueFetcher)}.
     */
    public synchronized SelectQueryPlan buildQueryPlan(SQLStoreManager store,
                                                       Concurrency concurrency) {

        if (plan == null) {

            handleProjection();

            plan = SelectQueryPlan.newInstance(this, store, concurrency);

            plan.build();

            // Generate the text for the select statements.
            ArrayList statements = plan.getStatements();

            // Sanity check.
            if (statements.size() > 1) {
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "sqlstore.retrievedesc.stmntsnotjoined")); // NOI18N
            }
        }
        return plan;
    }

    /**
     * Sets the fetch group options for all retrieve descriptors in the
     * retrieve descriptor hierarchy. This is important for projection and
     * aggregate queries, as we don't want to select additional fields
     * from the data store. This is also important for queries where relationship
     * prefetch is involved, as we want to propagate user's choice to disable
     * relationship prefetch for a finder to all the retrieve descriptors in
     * the projection tree.
     * Also sets the projection on the candidate  class in case of queries without
     * user projection.
     */
    private void handleProjection() {
        // Prepare all the query options that need to be distributed to all the
        // RetrieveDescs involved in the projection tree.
        final int queryOptions = options & (OPT_AGGREGATE | OPT_DISABLE_RELATIONSHIP_PREFETCH);
        RetrieveDescImpl projectedDesc = distributeQueryOptions(
                queryOptions, aggregateResultType);

        if (projectedDesc == null) {
            // Set the default projection on the candidate retrieve descriptor.
            options |= OPT_PROJECTION;
            // Prepare fetch group options again after default projection has been set.
            setFetchGroupOptions(queryOptions);
        }
    }

    /**
     * Marks each descriptor with the options <code>queryOptions</code> and
     * finds the projection in the retrieve descriptor hierarchy.
     * The query options have to be known for each descriptor, because we don't want to
     * generate outer joins for OPT_COUNT_PC and we want to propagate users
     * choice to disable prefetch for a finder to projected RetrieveDesc.
     * This method relies on the fact, that each query can have only
     * <b>one</b> projection.
     *
     * @param queryOptions The options that need to be set on all
     *    retrieve descriptors. This helps identify aggregate queries, see
     *    {@link SelectQueryPlan#processJoins()}.
     *    This also helps propagate user's choice to disable prefetch to the
     *    projected RetrieveDesc, see
     *    {@link SelectQueryPlan#addFetchGroup(int, java.util.ArrayList, java.util.ArrayList)}.
     * @param aggregateResultType The aggregate result type has to
     *    be set on the projected retrieve descriptor.
     * @return The projected retrieve descriptor, or null there is
     *    no projection.
     */
    private RetrieveDescImpl distributeQueryOptions(int queryOptions,
                                                    int aggregateResultType) {
        RetrieveDescImpl projectedDesc = null;

        if ((options & OPT_PROJECTION) > 0) {
            // This is a foreign field projection.
            // Set the fetch group properties.
            setFetchGroupOptions(queryOptions);
            this.aggregateResultType = aggregateResultType;
            projectedDesc = this;
        }

        // Distribute the aggregate option to all retrieve descriptors.
        options |= queryOptions;

        // Loop through all dependent retrieve descriptors in the field list.
        for (int i = 0; i < fields.size(); i++) {
            ConstraintFieldName cfn = (ConstraintFieldName) fields.get(i);

            if (cfn.isProjection()) {
                // This is a local field projection.
                // No fetch groups are added.
                this.aggregateResultType = aggregateResultType;
                projectedDesc = this;
            } else if (cfn.desc != null) {
                projectedDesc = ((RetrieveDescImpl) cfn.desc).distributeQueryOptions(
                        queryOptions, aggregateResultType);
            }
        }

        return projectedDesc;
    }

    /**
     * Sets the fetch group policy for the projected retrieve descriptor.
     * The policy is based on the following rules:
     *
     * <ul>
     * <li>Fetchgroups are added for a projection w/o aggregates.</li>
     * <li>Only keys are added for count(*) queries.</li>
     * </ul>
     *
     * @param queryOptions The quey options that needs to be set for ********
     *    <code>queryOptions</code> Aggregate queries impose
     *    special restrictions on which fields to be selected. All aggregate
     *    queries except count(*) operate on exactly one field. Count(*)
     *    queries operate on persistence capable objects.
     * @see SelectQueryPlan#processFetchGroups(ArrayList, ArrayList)
     */
    private void setFetchGroupOptions(int queryOptions) {

        if ((queryOptions & OPT_AGGREGATE_NON_COUNT_PC) == 0) {
            // Don't add fetch groups except for projections
            // w/o aggregates or counts on persistence capable objects.
            options |= OPT_ADD_FETCHGROUPS;

            if (queryOptions == OPT_COUNT_PC)  {
                options |= OPT_ADD_KEYS_ONLY;
            }
        }
    }

    /**
     * Sets a navigational id on the retrieve descriptor. This id
     * will be used to discriminate different retrieve descriptors which
     * use the same navigational field. If not set, the field name is used.
     *
     * @param navigationalId Tag to discriminate different retrieve
     * descriptors that use the same navigational field.
     */
    public void setNavigationalId(Object navigationalId) {
        this.navigationalId = navigationalId;
    }

    /**
     * Returns the navigational id of this retrieve descriptor. This id
     * will be used to discriminate different retrieve descriptors which
     * use the same navigational field. If not set, the field name is used.
     *
     * @return Tag to discriminate different retrieve descriptors that
     * use the same navigational field.
     */
    public Object getNavigationalId() {
        return navigationalId;
    }

    /**
     * Sets option <code>option</code>. Only used to mark this
     * retrieve descriptor as internal. All valid options are defined
     * in this class.
     *
     * @param option Option being set.
     */
    public void setOption(int option) {
        this.options |= option;
    }

    /**
     * Returns the result type for aggregate queries.
     */
    public int getAggregateResultType() {
        return aggregateResultType;
    }

    /**
     * Returns the options of this retrieve descriptor.
     *
     * @return The options of this retrieve descriptor.
     */
    public int getOptions() {
        return options;
    }

    public SelectQueryPlan getPlan() {
        return plan;
    }

    public ClassDesc getConfig() {
        return config;
    }

    public void setPlan(SelectQueryPlan plan) {
        this.plan = plan;
    }

    public Class getPersistenceCapableClass() {
        return pcClass;
    }

    public Constraint getConstraint() {
        return constraint;
    }

    public Iterator getFields() {
        return fields.iterator();
    }

    public Object clone() {
        try {
            RetrieveDescImpl clone = (RetrieveDescImpl) super.clone();
            clone.fields = new ArrayList();
            clone.constraint = new Constraint();

            return clone;
        } catch (CloneNotSupportedException e) {
            //
            // shouldn't happen.
            //
            return null;
        }
    }

}
