FileDocCategorySizeDatePackage
ClassDesc.javaAPI DocGlassfish v2 API56760Wed Jul 11 12:57:14 BST 2007com.sun.jdo.spi.persistence.support.sqlstore.model

ClassDesc.java

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, 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/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

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

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

import org.netbeans.modules.dbschema.ColumnElement;
import org.netbeans.modules.dbschema.ColumnPairElement;
import org.netbeans.modules.dbschema.TableElement;
import com.sun.jdo.api.persistence.model.Model;
import com.sun.jdo.api.persistence.model.jdo.ConcurrencyGroupElement;
import com.sun.jdo.api.persistence.model.jdo.PersistenceClassElement;
import com.sun.jdo.api.persistence.model.jdo.PersistenceFieldElement;
import com.sun.jdo.api.persistence.model.jdo.RelationshipElement;
import com.sun.jdo.api.persistence.model.mapping.MappingClassElement;
import com.sun.jdo.api.persistence.model.mapping.MappingFieldElement;
import com.sun.jdo.api.persistence.model.mapping.MappingRelationshipElement;
import com.sun.jdo.api.persistence.model.mapping.impl.*;
import com.sun.jdo.api.persistence.support.*;
import com.sun.jdo.spi.persistence.support.sqlstore.*;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.RetrieveDescImpl;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.UpdateObjectDescImpl;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.concurrency.*;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.generator.UpdateQueryPlan;
import com.sun.jdo.spi.persistence.utility.I18NHelper;
import com.sun.jdo.spi.persistence.utility.StringHelper;
import com.sun.jdo.spi.persistence.utility.logging.Logger;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

/**
 *
 */
public class ClassDesc
        implements com.sun.jdo.spi.persistence.support.sqlstore.PersistenceConfig {

    private ArrayList fetchGroups;

    private int maxHierarchicalGroupID;

    /**
     * Used for batched update check. Set to true if this class has at least one
     * local field that does not belong to DFG
     */
    private boolean hasLocalNonDFGField;

    /** Contains all local and foreign fields. */
    public ArrayList fields;

    /** Contains all hidden fields. */
    public ArrayList hiddenFields;

    /** Contains all relationship fields. */
    public ArrayList foreignFields;

    /** Contains the fields used for version consistency validation. */
    private LocalFieldDesc[] versionFields;

    private ArrayList tables;

    private Class pcClass;

    private Class oidClass;

    public int maxFields;

    public int maxVisibleFields;

    public int maxHiddenFields;

    private Concurrency optimisticConcurrency;

    private Concurrency checkDirtyConcurrency;

    private Concurrency databaseConcurrency;

    private Concurrency explicitConcurrency;

    private MappingClassElementImpl mdConfig;

    private ClassLoader classLoader;

    private Constructor constructor;

    private PersistenceClassElement pcElement;

    private PersistenceFieldElement[] persistentFields;

    private Field keyFields[];
    private String keyFieldNames[];
    private LocalFieldDesc[] keyFieldDescs;

    /** The logger. */
    private static Logger logger = LogHelperSQLStore.getLogger();

    /** RetrieveDescriptor cache for navigation and reloading. */
    private final Map retrieveDescCache = new HashMap();

    /**
     * RetrieveDescriptor cache for navigation queries. This cache
     * holds foreign RetrieveDescriptors constrained by the relationship
     * key values.
     */
    private final Map foreignRetrieveDescCache = new HashMap();

    /** Retrieve descriptor for version consistency verification. */
    private RetrieveDesc retrieveDescForVerification;
    private Object retrieveDescForVerificationSynchObj = new Object();

    /** UpdateQueryPlan cache. */
    private final Map updateQueryPlanCache = new HashMap();

    /** UpdateQueryPlan for insert. */
    private UpdateQueryPlan updateQueryPlanForInsert;
    private Object updateQueryPlanForInsertSynchObj = new Object();

    /** UpdateQueryPlan for delete. */
    private UpdateQueryPlan updateQueryPlanForDelete;
    private Object updateQueryPlanForDeleteSynchObj = new Object();

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

    /** PC Constructor signature. */
    private static final Class[] sigSM = new Class[]{StateManager.class};

    public ClassDesc(MappingClassElement mdConfig, Class pcClass) {

        this.mdConfig = (MappingClassElementImpl) mdConfig;
        pcElement = this.mdConfig.getPersistenceElement();
        this.pcClass = pcClass;
        classLoader = pcClass.getClassLoader();

        try {
            constructor = pcClass.getConstructor(sigSM);
        } catch (Exception e) {
            // Persistence capable classes must provide this constructor
            throw new JDOFatalUserException(I18NHelper.getMessage(messages,
                    "jdo.persistencemanagerimpl.assertpersistencecapable.error", // NOI18N
                    pcClass.getName()), e);
        }

        fields = new ArrayList();
        foreignFields = new ArrayList();
        tables = new ArrayList();
        fetchGroups = new ArrayList();
    }

    public String toString() {
        return getName();
    }

    /**
     * Creates a new instance of <code>ClassDesc</code> for the given
     * <code>pcClass</code>.
     * @param pcClass The persistence capable class.
     * @return A new instance of ClassDesc.
     */
    static ClassDesc newInstance(Class pcClass) {
        Model model = Model.RUNTIME;
        String className = pcClass.getName();
        ClassLoader classLoader = pcClass.getClassLoader();
        ClassDesc rc = null;

        try {
            MappingClassElement mdConfig =
               model.getMappingClass(className, classLoader);

            // Validate the model information for this class.
            validateModel(model, className, classLoader);

            rc = new ClassDesc(mdConfig, pcClass);

        } catch (JDOException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            throw new JDOFatalUserException(I18NHelper.getMessage(messages,
                "core.configuration.loadfailed.class", className), e); // NOI18N
        } catch (Exception e) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                "core.configuration.loadfailed.class", className), e); // NOI18N
        }

        return rc;
    }

    /**
     * Validate the mapping for the given class. If the result
     * collection returned by validate() is not empty, it means
     * it failed the test. After logging all the exceptions,
     * throw a JDOUserException and inform the user to go back
     * to the mapping tool to fix up the mapping file. The
     * JDOUserException contains the validation messages aswell.
     *
     * @param model Runtime model.
     * @param className Persistence capable class' name.
     * @param classLoader Persistence capable class' loader.
     */
    static private void validateModel(Model model,
                                      String className,
                                      ClassLoader classLoader) {
        Collection c = null;

        if (!(c = model.validate(className, classLoader, null)).isEmpty()) {
            Iterator iter = c.iterator();
            StringBuffer validationMsgs = new StringBuffer();

            while (iter.hasNext()) {
                Exception ex = (Exception) iter.next();
                String validationMsg = ex.getLocalizedMessage();

                logger.fine(I18NHelper.getMessage(messages,
                    "core.configuration.validationproblem", // NOI18N
                    className, validationMsg));
                validationMsgs.append(validationMsg).append('\n'); // NOI18N
            }
            throw new JDOFatalUserException(I18NHelper.getMessage(messages,
                "core.configuration.validationfailed", // NOI18N
                className, validationMsgs.toString()));
        }
    }

    public void initialize(ConfigCache cache) {
        boolean debug = logger.isLoggable();
        if (debug) {
            logger.fine("sqlstore.model.classdesc.persistconfiginit", mdConfig); // NOI18N
        }

        loadOidClass();
        initializeFields();
        computeTrackedPrimitiveFields();

        initializeTables();
        initializeJoinTables();
        initializeVersionFields();
        initializeConcurrency();

        initializeKeyFields();
        initializeFetchGroups();

        fixupForeignReferences(cache);
        fixupFieldProperties();
        computeTrackedRelationshipFields();
        cleanupTrackedFields();

        // All fields should be initialized at this point. Now calculate
        // the total number of hidden fields and fields.
        if (hiddenFields != null) {
            maxHiddenFields = hiddenFields.size();
        }
        maxFields = maxVisibleFields + maxHiddenFields;

        if (debug) {
            logger.fine("sqlstore.model.classdesc.persistconfiginit.exit"); // NOI18N
        }
    }

    private void loadOidClass() {

        if (oidClass != null) return;

        String keyClassName = pcElement.getKeyClass();
        String suffix = keyClassName.substring(keyClassName.length() - 4);

        // First check whether the key class name ends with ".oid".
        // If so, we need to convert it '.' to '$' because is it
        // an inner class.
        if (suffix.compareToIgnoreCase(".oid") == 0) { // NOI18N
            StringBuffer buf = new StringBuffer(keyClassName);

            buf.setCharAt(buf.length() - 4, '$');
            keyClassName = buf.toString();
        }

        try {
            oidClass = Class.forName(keyClassName, true, classLoader);
        } catch (Throwable e) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                    "core.configuration.cantloadclass", keyClassName)); // NOI18N
        }

        if (logger.isLoggable()) {
            logger.fine("sqlstore.model.classdesc.loadedclass", oidClass); // NOI18N
        }
    }

    /**
     * This method maps all the visible fields. It has the side-effect of computing
     * the value for maxVisibleFields.
     */
    private void initializeFields() {
        ArrayList concurrencyGroups = new ArrayList();
        persistentFields = pcElement.getFields();

        for (int i = 0; i < persistentFields.length; i++) {
            PersistenceFieldElement pcf = persistentFields[i];
            MappingFieldElementImpl mdf = (MappingFieldElementImpl) mdConfig.getField(pcf.getName());

            if (mdf == null) {
                throw new JDOFatalUserException(I18NHelper.getMessage(messages,
                        "core.configuration.fieldnotmapped", // NOI18N
                        pcf.getName(), pcElement.getName()));
            }

            FieldDesc f;

            if (!(mdf instanceof MappingRelationshipElement)) {
                f = createLocalField(mdf);
            } else {
                f = createForeignField((RelationshipElement) pcf, (MappingRelationshipElementImpl) mdf);
            }

            initializeFetchAndConcurrencyGroup(f, pcf, mdf, concurrencyGroups);

            if (mdf.isReadOnly()) {
                f.sqlProperties |= FieldDesc.PROP_READ_ONLY;
            }

            try {
                f.setupDesc(pcClass, pcf.getName());
            } catch (JDOException e) {
                throw e;
            } catch (Exception e) {
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                "core.configuration.loadfailed.field", // NOI18N
                pcf.getName(), pcElement.getName()), e);
            }

            f.absoluteID = pcf.getFieldNumber();

            addField(f);

            if (logger.isLoggable(Logger.FINEST)) {
                Object[] items = new Object[] {f.getName(),new Integer(f.absoluteID)};
                logger.finest("sqlstore.model.classdesc.fieldinfo", items); // NOI18N
            }
        }

        this.maxVisibleFields = fields.size();
    }

    /**
     * Creates an instance of <code>LocalFieldDesc</code>.

     * @param mdf Input <code>MappingFieldElementImpl</code>
     */
    private LocalFieldDesc createLocalField(MappingFieldElementImpl mdf) {
        ArrayList columnDesc = mdf.getColumnObjects();

        // Make sure this field is properly mapped.
        if ((columnDesc == null) || (columnDesc.size() == 0)) {
            throw new JDOFatalUserException(I18NHelper.getMessage(messages,
                    "core.configuration.fieldnotmapped", // NOI18N
                    mdf.getName(), pcElement.getName()));
        }
        return new LocalFieldDesc(this, columnDesc);
    }

    /**
     * Creates an instance of <code>LocalFieldDesc</code> for <code>column</code>
     * that corresponds to a hidden field. Adds the newly created field to
     * <code>hiddenFields</code> and <code>declaredHiddenFields</code>.
     *
     * @param column The input column.
     * @return New instance of <code>LocalFieldDesc</code>
     */
    private LocalFieldDesc createLocalHiddenField(ColumnElement column) {
        ArrayList columnDesc = new ArrayList();
        columnDesc.add(column);
        LocalFieldDesc lf = new LocalFieldDesc(this, columnDesc);

        if (hiddenFields == null) {
            hiddenFields = new ArrayList();
        }
        hiddenFields.add(lf);

        // AbsouluteID for hidden fields must be < 0.
        lf.absoluteID = -hiddenFields.size();

        if (logger.isLoggable(Logger.FINEST)) {
            Object[] items = new Object[] {pcClass,lf.getName(),column.getName().getFullName()};
            logger.finest("sqlstore.model.classdesc.getlocalfielddesc", items); // NOI18N
        }

        return lf;
    }

    private FieldDesc createForeignField(RelationshipElement fpcf,
                                         MappingRelationshipElementImpl fmdf) {

        ForeignFieldDesc ff = new ForeignFieldDesc(this);

        addForeignField(ff);

        ff.cardinalityLWB = fpcf.getLowerBound();
        ff.cardinalityUPB = fpcf.getUpperBound();
        ff.deleteAction = fpcf.getDeleteAction();

        initializeColumnLists(ff, fmdf);

        // This is a workaround to make sure that that cardinalityUPB is not <= 1
        // if the field is of type collection.
        if (Model.RUNTIME.isCollection(fpcf.getCollectionClass()) &&
                (ff.cardinalityUPB <= 1)) {
            ff.cardinalityUPB = Integer.MAX_VALUE;
        }

        if (ff.cardinalityUPB > 1) {
            try {
                ff.setComponentType(Class.forName(fpcf.getElementClass(), true, classLoader));
            } catch (Throwable e) {
              logger.log(Logger.WARNING, "sqlstore.exception.log", e);
            }
        }

        return ff;
    }

    private void initializeColumnLists(ForeignFieldDesc ff, MappingRelationshipElementImpl fmdf) {
        ArrayList assocPairs = fmdf.getAssociatedColumnObjects();
        ArrayList pairs = fmdf.getColumnObjects();
        ArrayList localColumns = new ArrayList();
        ArrayList foreignColumns = new ArrayList();

        // We need to go through each local column and extract the foreign column.
        if ((assocPairs == null) || (assocPairs.size() == 0)) {
            for (int i = 0; i < pairs.size(); i++) {
                ColumnPairElement fce = (ColumnPairElement) pairs.get(i);
                localColumns.add(fce.getLocalColumn());
                foreignColumns.add(fce.getReferencedColumn());
            }

            ff.localColumns = localColumns;
            ff.foreignColumns = foreignColumns;
        } else {
            ArrayList assocLocalColumns = new ArrayList();
            ArrayList assocForeignColumns = new ArrayList();

            for (int i = 0; i < pairs.size(); i++) {
                ColumnPairElement alc = (ColumnPairElement) pairs.get(i);
                localColumns.add(alc.getLocalColumn());
                assocLocalColumns.add(alc.getReferencedColumn());
            }

            for (int i = 0; i < assocPairs.size(); i++) {
                ColumnPairElement afc = (ColumnPairElement) assocPairs.get(i);
                assocForeignColumns.add(afc.getLocalColumn());
                foreignColumns.add(afc.getReferencedColumn());
            }

            ff.localColumns = localColumns;
            ff.assocLocalColumns = assocLocalColumns;
            ff.assocForeignColumns = assocForeignColumns;
            ff.foreignColumns = foreignColumns;
        }
    }

    private void initializeFetchAndConcurrencyGroup(FieldDesc f,
                                                    PersistenceFieldElement pcf,
                                                    MappingFieldElementImpl mdf,
                                                    ArrayList concurrencyGroups) {
        f.fetchGroup = mdf.getFetchGroup();

        // RESOLVE: For now, we can only handle one concurrency group per field
        // MBO:
        // I can call method getConcurrencyGroups w/o exception using the latest mapping files.
        // Please note the mapping files do not include any concurrency group info,
        // thus getConcurrencyGroups returns an empty array.
        ConcurrencyGroupElement cgroups[] = pcf.getConcurrencyGroups();
        //ConcurrencyGroupElement cgroups[] = null;

        if ((cgroups == null) || (cgroups.length == 0)) {
            if (f.fetchGroup == FieldDesc.GROUP_DEFAULT) {
                f.concurrencyGroup = f.fetchGroup;
            }
        } else {
            ConcurrencyGroupElement cge = cgroups[0];
            int index = 0;

            if ((index = concurrencyGroups.indexOf(cge)) == -1) {
                index = concurrencyGroups.size();
                concurrencyGroups.add(cge);
            }

            f.concurrencyGroup = index;
        }
    }

    private void createSecondaryTableKey(TableDesc table, MappingReferenceKeyElementImpl mappingSecondaryKey) {

        ColumnPairElement pairs[] = mappingSecondaryKey.getColumnPairs();
        KeyDesc referencingKey = new KeyDesc();
        KeyDesc referencedKey = new KeyDesc();
        TableDesc secondaryTable = findTableDesc(((MappingTableElementImpl) mappingSecondaryKey.getTable()).getTableObject());

        for (int i = 0; i < pairs.length; i++) {
            ColumnPairElement pair = pairs[i];

            ColumnElement lc = pair.getLocalColumn();
            ColumnElement fc = pair.getReferencedColumn();

            referencingKey.addColumn(lc);

            FieldDesc lf = getLocalFieldDesc(lc);
            referencingKey.addField(lf);

            // We need to force field for the referencing key to be in the DFG
            // so it will always be loaded. This is to facilitate updating
            // secondary tables that requires this field to be loaded
            // for constraint purposes.
            lf.fetchGroup = FieldDesc.GROUP_DEFAULT;

            referencedKey.addColumn(fc);
            referencedKey.addField(getLocalFieldDesc(fc));
        }

        table.addSecondaryTableKey(new ReferenceKeyDesc(secondaryTable, referencingKey, referencedKey));
        secondaryTable.setPrimaryTableKey(new ReferenceKeyDesc(table, referencedKey, referencingKey));
    }

    /**
     * This method maps all the tables.
     */
    private void initializeTables() {
        ArrayList mdTables = mdConfig.getTables();

        createTables(mdTables);
        processSecondaryTables(mdTables);
    }

    private void createTables(ArrayList mdTables) {
        for (int i = 0; i < mdTables.size(); i++) {
            MappingTableElementImpl mdt = (MappingTableElementImpl) mdTables.get(i);
            TableDesc t = new TableDesc(mdt.getTableObject());

            ArrayList keys = mdt.getKeyObjects();
            KeyDesc key = new KeyDesc();
            t.setKey(key);
            key.addColumns(keys);

            for (int j = 0; j < keys.size(); j++) {
                ColumnElement c = (ColumnElement) keys.get(j);

                if (c != null) {
                    key.addField(getLocalFieldDesc(c));
                }
            }

            addTableDesc(t);
        }
    }

    /**
     * Validity checks on secondary tables:
     *	1) Ensure that every secondary table has a TableDesc
     *	2) Every referencing key is the same length as the table's key
     * Build the referencing keys.
     * NOTE: This method assumes that the entries of <code>mdTables</code>
     * and <code>tables</code> are sorted in the same order.
     */
    private void processSecondaryTables(ArrayList mdTables) {

        for (int i = 0; i < tables.size(); i++) {
            MappingTableElementImpl mdt = (MappingTableElementImpl) mdTables.get(i);
            TableDesc t = (TableDesc) tables.get(i);
            ArrayList secondaryKeys = mdt.getReferencingKeys();

            for (int j = 0; j < secondaryKeys.size(); j++) {
                MappingReferenceKeyElementImpl mappingSecondaryKey = (MappingReferenceKeyElementImpl) secondaryKeys.get(j);
                createSecondaryTableKey(t, mappingSecondaryKey);
            }
        }
    }

    private void initializeJoinTables() {
        Iterator iter = foreignFields.iterator();

        while (iter.hasNext()) {
            ForeignFieldDesc ff = (ForeignFieldDesc) iter.next();

            if (ff.useJoinTable()) {
                TableElement joinTable = ((ColumnElement) ff.assocLocalColumns.get(0)).getDeclaringTable();
                TableDesc joinTableDesc = findTableDesc(joinTable);

                if (joinTableDesc == null) {
                    joinTableDesc = new TableDesc(joinTable);

                    // Mark this table as a join table
                    joinTableDesc.setJoinTable(true);
                    addTableDesc(joinTableDesc);
                }
              }
        }
    }

    /**
     * Returns a list of fields in fetchGroup <code>groupID</code>.
     *
     * @param groupID Fetch group id.
     * @return List of fields in fetchGroup <code>groupID</code>. The list
     *  for <code>FieldDesc.GROUP_NONE</code> is empty.
     * @see #initializeFetchGroups
     */
    public ArrayList getFetchGroup(int groupID) {
        int index = 0;

        if (groupID >= FieldDesc.GROUP_NONE) {
            index = groupID;
        } else if (groupID < FieldDesc.GROUP_NONE) {
            index = -groupID + maxHierarchicalGroupID;
        }

        for (int i = fetchGroups.size(); i <= index; i++) {
            fetchGroups.add(null);
        }

        ArrayList group = (ArrayList) fetchGroups.get(index);

        if (group == null) {
            group = new ArrayList();
            fetchGroups.set(index, group);
        }

        return group;
    }

    private void addField(FieldDesc f) {
        fields.add(f);
    }

    private void addForeignField(ForeignFieldDesc f) {
        foreignFields.add(f);
    }

    /**
     * Compute the fetch group lists for the declared fields.
     * A fetch group lists the fields sharing the same value for
     * <code>FieldDesc.fetchGroup</code>.
     * Note: Fields that aren't in any fetch group must not be added
     * to <code>fetchGroups[FieldDesc.GROUP_NONE]</code>.
     *
     * @see #getFetchGroup
     */
    private void initializeFetchGroups() {

        for (int i = 0; i < 2; i++) {
            ArrayList theFields = null;

            if (i == 0) {
                theFields = fields;
            } else {
                // It is possible to have hidden fields in the DFG.
                if ((theFields = hiddenFields) == null)
                    continue;
            }

            for (int j = 0; j < theFields.size(); j++) {
                FieldDesc f = (FieldDesc) theFields.get(j);

                // Do not add the field to the fetch group for GROUP_NONE.
                if (f.fetchGroup > FieldDesc.GROUP_NONE) {
                    getFetchGroup(f.fetchGroup).add(f);
                }

                // Check declared visible fields only.
                if (i == 0 && !f.isRelationshipField()
                        && f.fetchGroup != FieldDesc.GROUP_DEFAULT) {
                    hasLocalNonDFGField = true;
                }
            }
        }

        this.maxHierarchicalGroupID = fetchGroups.size() - 1;

        for (int i = 0; i < fields.size(); i++) {
            FieldDesc f = (FieldDesc) fields.get(i);

            // Do not add the field to the fetch group for GROUP_NONE.
            if (f.fetchGroup < FieldDesc.GROUP_NONE) {
                getFetchGroup(f.fetchGroup).add(f);
            }
        }
    }

    private void initializeConcurrency() {
        optimisticConcurrency = new ConcurrencyOptVerify();
        optimisticConcurrency.configPersistence(this);
        checkDirtyConcurrency = new ConcurrencyCheckDirty();
        checkDirtyConcurrency.configPersistence(this);
        databaseConcurrency = new ConcurrencyDBNative();
        databaseConcurrency.configPersistence(this);
        explicitConcurrency = new ConcurrencyDBExplicit();
        explicitConcurrency.configPersistence(this);
    }

    private void initializeKeyFields() {
        boolean debug = logger.isLoggable(Logger.FINEST);
        if (oidClass == null)
            return;

        keyFields = oidClass.getFields();
        keyFieldNames = new String[keyFields.length];
        keyFieldDescs = new LocalFieldDesc[keyFields.length];

        if (debug) {
            logger.finest("sqlstore.model.classdesc.createsqldesc", oidClass); // NOI18N
        }

        for (int i = 0; i < keyFields.length; i++) {
            Field kf = keyFields[i];
            String name = kf.getName();
            keyFieldNames[i] = name;

            if (name.equals("serialVersionUID")) { // NOI18N
                continue;
            }

            LocalFieldDesc f = getLocalFieldDesc(name);

            if (f != null) {
                if (debug) {
                    logger.finest("sqlstore.model.classdesc.pkfield", f.getName()); // NOI18N
                }

                // The fetch group for pk fields should always be DFG.
                f.fetchGroup = FieldDesc.GROUP_DEFAULT;
                f.sqlProperties &= ~(FieldDesc.PROP_REF_INTEGRITY_UPDATES);
                f.sqlProperties &= ~(FieldDesc.PROP_IN_CONCURRENCY_CHECK);
                f.sqlProperties |= FieldDesc.PROP_PRIMARY_KEY_FIELD;

                keyFieldDescs[i] = f;
            } else {
                throw new JDOFatalUserException(I18NHelper.getMessage(messages,
                        "core.configuration.noneexistentpkfield", // NOI18N
                        name, oidClass.getName(), pcClass.getName()));
            }
        }
    }

    /**
     * Initialize the list of field descriptors of version consistency fields.
     * The names of the version fields are obtained from
     * {@link MappingClassElement#getVersionFields}.
     */
    private void initializeVersionFields() {
        int size = mdConfig.getVersionFields().size();
        Iterator versionFieldIterator = mdConfig.getVersionFields().iterator();
        versionFields = new LocalFieldDesc[size];

        for (int i = 0; i < size; i++) {
            MappingFieldElement mdField = (MappingFieldElement) versionFieldIterator.next();
            LocalFieldDesc f = (LocalFieldDesc) getField(mdField.getName());

            if (f != null) {
                if (logger.isLoggable()) {
                    logger.finest("sqlstore.model.classdesc.vcfield", f.getName()); // NOI18N
                }

                versionFields[i] = f;
                registerVersionFieldWithTable(f);

                // The fetch group for version fields should always be DFG.
                f.fetchGroup = FieldDesc.GROUP_DEFAULT;
                f.sqlProperties &= ~(FieldDesc.PROP_REF_INTEGRITY_UPDATES);
                f.sqlProperties |= FieldDesc.PROP_VERSION_FIELD;
            } else {
                throw new JDOFatalUserException(I18NHelper.getMessage(messages,
                        "core.configuration.noneexistentvcfield", // NOI18N
                        mdField.getName(), pcClass.getName()));
            }
        }
    }

    /**
     * Registers the version field <cod>versionField</code> with the
     * corresponding table.
     *
     * @param versionField Field used in version consistency check.
     */
    private void registerVersionFieldWithTable(LocalFieldDesc versionField) {
        // Version field must be mapped to exactly one column.
        ColumnElement ce =  (ColumnElement) versionField.getColumnElements().next();
        Iterator iter = tables.iterator();

        while (iter.hasNext()) {
            TableDesc table = (TableDesc) iter.next();

            if (!table.isJoinTable()) {
                if (ce.getDeclaringTable() == table.getTableElement()) {
                    table.setVersionField(versionField);
                    break;
                }
            }
        }
    }

    /**
     * Computes all the primitive tracked fields.
     * Primitive fields track each other if they are mapped to same columns.
     * One of them is made the primary tracked field as per precedence rules
     * on the field types. This field is used to bind values to columns while
     * updating the database.
     */
    private void computeTrackedPrimitiveFields() {
        // Compute the list of primitive fields to track for each primitive field.
        for (int i = 0; i < fields.size(); i++) {
            FieldDesc f = (FieldDesc) fields.get(i);

            if (!f.isRelationshipField()) {
                LocalFieldDesc lf = (LocalFieldDesc) f;

                lf.computeTrackedPrimitiveFields();
                lf.computePrimaryTrackedPrimitiveField();
            }
        }
    }

    /**
     * Computes the tracked relationship fields. Relationships are tracked in
     * the following way:
     * <ul>
     * <li>A relationship field tracks a local field, if it's mappped
     * to the same columns. The relationship will be updated when the
     * local field is set.</li>
     * <li>Relationship fields track each other if they are mapped to
     * the same columns. The first field in the field order is marked
     * as primary tracked field. If the foreign key columns are
     * explicitly mapped, this field also tracks the local foreign key
     * field. The foreign key columns are explicitly mapped in
     * overlapping pk/fk situations. The other relationship fields
     * mapped to the same columns are updated when the primary tracked
     * relationship field is set.</li>
     * </ul>
     * @see FieldDesc#PROP_PRIMARY_TRACKED_FIELD
     * @see FieldDesc#PROP_SECONDARY_TRACKED_FIELD
     */
    private void computeTrackedRelationshipFields() {
        // Compute the list of fields to track for each field.
        for (int i = 0; i < 2; i++) {
            ArrayList theFields = null;

            // We first check all the visible fields and then the hidden fields.
            if (i == 0) {
                theFields = this.fields;
            } else {
                if ((theFields = this.hiddenFields) == null) {
                    continue;
                }
            }

            for (int j = 0; j < theFields.size(); j++) {
                FieldDesc f = (FieldDesc) theFields.get(j);

                f.computeTrackedRelationshipFields();
            }
        }
    }

    /**
     * Remove redundant ForeignFieldDescs from each LocalFieldDesc's tracked field list.
     */
    private void cleanupTrackedFields() {

        for (int i = 0; i < fields.size(); i++) {
            FieldDesc f = (FieldDesc) fields.get(i);

            if (f instanceof LocalFieldDesc) {
                ((LocalFieldDesc) f).cleanupTrackedFields();
            }
        }
    }

    /**
     *  The fixupForeignReferences method finds all the references to foreign
     *  classes in this configuration. It then builds the appropriate run-time
     *  information (ForeignFieldDesc) for the foreign reference from the
     *  meta-data.
     */
    private void fixupForeignReferences(ConfigCache cache) {

        for (int i = 0; i < foreignFields.size(); i++) {
            ForeignFieldDesc ff = (ForeignFieldDesc) foreignFields.get(i);
            Class classType = null;

            if ((classType = ff.getComponentType()) == null) {
                classType = ff.getType();
            }

            ClassDesc foreignConfig = (ClassDesc) cache.getPersistenceConfig(classType);

            if (foreignConfig == null) continue;

            // Look up the inverse relationship field name if there is any.
            String irName = pcElement.getRelationship(ff.getName()).getInverseRelationshipName();
            ForeignFieldDesc inverseField = null;

            if (irName != null) {
                inverseField = (ForeignFieldDesc) foreignConfig.getField(irName);
            }

            ff.fixupForeignReference(foreignConfig, inverseField);
        }
    }

    private void fixupFieldProperties() {

        for (int i = 0; i < foreignFields.size(); i++) {
            ForeignFieldDesc ff = (ForeignFieldDesc) foreignFields.get(i);
            ff.fixupFieldProperties();
        }
    }

    LocalFieldDesc getLocalFieldDesc(ColumnElement column) {
        LocalFieldDesc result;

        for (int i = 0; i < 2; i++) {
            ArrayList theFields = null;

            if (i == 0) {
                theFields = fields;
            } else {
                theFields = hiddenFields;
            }

            if (theFields != null) {
                for (int j = 0; j < theFields.size(); j++) {
                    FieldDesc f = (FieldDesc) theFields.get(j);

                    if (f instanceof LocalFieldDesc) {
                        result = (LocalFieldDesc) f;

                        for (int k = 0; k < result.columnDescs.size(); k++) {
                            ColumnElement c = (ColumnElement) result.columnDescs.get(k);

                            // if (c.equals(column))
                            if (c.getName().getFullName().compareTo(column.getName().getFullName()) == 0) {
                                // If f is a tracked field and it is not the primary, we continue
                                // searching.
                                if ((f.getTrackedFields() != null) &&
                                        ((f.sqlProperties & FieldDesc.PROP_PRIMARY_TRACKED_FIELD) == 0)) {
                                    continue;
                                }

                                return result;
                            }
                        }
                    }
                }
            }
        }

        // If we didn't find the field associated with the column, we need to
        // create a hidden field and add it to the hiddenFields list.
        result = createLocalHiddenField(column);

        return result;
    }

    /**
     * Returns the local field descriptor for the field <code>name</code>.
     *
     * @param name Field name.
     * @return Local field descriptor for the field <code>name</code>.
     * @throws JDOFatalInternalException if the field is not defined for this class.
     */
    public LocalFieldDesc getLocalFieldDesc(String name) {
        FieldDesc desc = getField(name);

        if (desc == null) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "core.generic.unknownfield", // NOI18N
                        name, getName()));
        }

        if (!(desc instanceof LocalFieldDesc)) {
            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "core.generic.notinstanceof", // NOI18N
                        desc.getClass().getName(), "LocalFieldDesc")); // NOI18N
        }

        return ((LocalFieldDesc) desc);
    }

    public TableDesc findTableDesc(TableElement mdTable) {
        for (int i = 0; i < tables.size(); i++) {
            TableDesc t = (TableDesc) tables.get(i);

            if (t.getTableElement().equals(mdTable)) {
                return t;
            }
        }

        return null;
    }

    private void addTableDesc(TableDesc t) {
        // setConsistencyLevel of this table before adding it to our list
        if (!t.isJoinTable()) {
            // JoinTables represent relationships instead of "real" objects,
            // they should never have a special consistencyLevel.
            t.setConsistencyLevel(mdConfig.getConsistencyLevel());
        }
        tables.add(t);
    }

    public int getTableIndex(TableDesc tableDesc) {
        return tables.indexOf(tableDesc);
    }

    public FieldDesc getField(String name) {
        for (int i = 0; i < fields.size(); i++) {
            FieldDesc f = (FieldDesc) fields.get(i);

            if ((f != null) && (f.getName().compareTo(name) == 0)) {
                return f;
            }
        }

        if (hiddenFields != null) {
            for (int i = 0; i < hiddenFields.size(); i++) {
                FieldDesc f = (FieldDesc) hiddenFields.get(i);

                if (f.getName().compareTo(name) == 0) {
                    return f;
                }
            }
        }

        return null;
    }

    public FieldDesc getField(int index) {
        if (index >= 0) {
            return (FieldDesc) fields.get(index);
        } else {
            return (FieldDesc) hiddenFields.get(-(index + 1));
        }
    }

    public Constructor getConstructor() {
        return constructor;
    }

    public Class getPersistenceCapableClass() {
        return pcClass;
    }

    public Class getOidClass() {
        return oidClass;
    }

    public String getName() {
        return pcClass.getName();
    }

    public Iterator getTables() {
        return tables.iterator();
    }

    public TableDesc getPrimaryTable() {
        return (TableDesc) tables.get(0);
    }

    public boolean isNavigable() {
        return mdConfig.isNavigable();
    }

    public boolean hasVersionConsistency() {
        return mdConfig.getConsistencyLevel() == MappingClassElement.VERSION_CONSISTENCY;
    }

    public LocalFieldDesc[] getVersionFields() {
        return versionFields;
    }

    public Concurrency getConcurrency(boolean optimistic) {
        // Following algo is used to determine which concurrency to return:
        // consistency level specified in model(represented by the variable consistencyLevel),
        // takes precedence over
        // concurrency specified by transaction (represented by parameter 'optimistic' to this method)

        Concurrency concurrency = null;
        int consistencyLevel = mdConfig.getConsistencyLevel();

        if (consistencyLevel == MappingClassElement.NONE_CONSISTENCY) {
            // No consistency level specified in model
            if (optimistic) {
                concurrency = (Concurrency) optimisticConcurrency.clone();
            } else {
                concurrency = (Concurrency) databaseConcurrency.clone();
            }

        } else {
            // currently, we would not try to interprete consistencyLevel as bitmap
            // When we implment consistencyLevel like 2+, 3+, we would have to interprete
            // consistencyLevel as bitmap and implement some changes in class hierarchy
            // starting  with interface Concurrency
            switch(consistencyLevel) {

                // We would never reach this code because we are in the else part
                // case MappingClassElement.NONE_CONSISTENCY :
                //	concurrency = (Concurrency) databaseConcurrency.clone();
                //	break;

            case MappingClassElement.CHECK_MODIFIED_AT_COMMIT_CONSISTENCY :
                concurrency = (Concurrency) checkDirtyConcurrency.clone();
                break;

            case MappingClassElement.LOCK_WHEN_LOADED_CONSISTENCY :
                // This consistency level is implemeted inside SelectStatement and
                // not by any object implementing Concurrency
                concurrency = (Concurrency) explicitConcurrency.clone();
                break;

            case MappingClassElement.VERSION_CONSISTENCY :
                // This consistency level is implemented inside ClassDesc and
                // UpdateQueryPlan and not by any object implementing Concurrency.
                // Selects are handled in ClassDesc, updates/deletes in
                // UpdateQueryPlan.
                concurrency = (Concurrency) databaseConcurrency.clone();
                break;

            // **Note**
            // Plese change text for the exception thrown in default below when
            // we start supporting new consistency levels. The text lists currently supported
            // consistency level
            default :
                throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                    "core.configuration.unsupportedconsistencylevel", pcClass));// NOI18N
            }
        }

        return concurrency;
    }

    /**
     * Determines whether this classDesc has the
     * CHECK_MODIFIED_AT_COMMIT_CONSISTENCY level.
     * @return <code>true</code> if this has the
     * CHECK_MODIFIED_AT_COMMIT_CONSISTENCY level;
     * <code>false</code> otherwise.
     */
    public boolean hasModifiedCheckAtCommitConsistency() {
        return(mdConfig.getConsistencyLevel() ==
                MappingClassElement.CHECK_MODIFIED_AT_COMMIT_CONSISTENCY);
    }

    public boolean isPKField(int index) {
        return persistentFields[index].isKey();
    }

    /**
     * Return the key fields as array of java.lang.reflect.Field instances.
     * @return The key fields as array of java.lang.reflect.Field instances.
     */
    public Field[] getKeyFields() {
        return keyFields;
    }

    /**
     * Returns the key field names as array of String.
     * @return The key field names as array of String.
     */
    public String[] getKeyFieldNames() {
        return keyFieldNames;
    }

    /**
     * Returns the descriptors for key fields as array of LocalFieldDesc.
     * @return The descriptors for key fields as array of LocalFieldDesc.
     */
    public LocalFieldDesc[] getKeyFieldDescs() {
        return keyFieldDescs;
    }

    /**
     * Returns a new <code>SQLStateManager</code> instance,
     * initialized with passed store manager and this instance of
     * the runtime class model.
     *
     * @param store Store manager, an instance of SQLStoreManager.
     * @return A new <code>SQLStateManager</code> instance.
     */
    public SQLStateManager newStateManagerInstance(PersistenceStore store) {
        return new SQLStateManager(store, this);
    }

    /**
     * Returns a RetrieveDescriptor which represent a SQL query selecting a pc
     * instance by pk-fields. Please note that the RDs are cached, so the method
     * first checks the cache. If there is no corresponding RetrieveDescriptor
     * in the cache, it creates a new one and stores it in the cache. If the
     * additionalField is not null, the method retrieves the field indicated by
     * it along with the query. Fetch group fields will be added when the query
     * plan is build.
     *
     * Note, the reason to introduce the RetrieveDesc cache in ClassDesc and not
     * in the store manager is, that we can have the cache per class, where
     * the store manager could only provide one big cache for all pc classes.
     *
     * @param additionalField The field to be retrieved in addition to the
     * DFG fields.
     * @param store The store manager.
     * @return A RetrieveDescriptor selecting a pc instance by pk-fields.
     * @see #getRetrieveDescForFKQuery
     */
    public RetrieveDesc getRetrieveDescForPKQuery(FieldDesc additionalField, PersistenceStore store) {
        RetrieveDescImpl rd = null;
        String cacheKey = generateRDCacheKey(additionalField);

        synchronized (retrieveDescCache) {
            // Cache lookup.
            rd = (RetrieveDescImpl) retrieveDescCache.get(cacheKey);
            // Generate a new RD if there isn't one be found in the cache.
            if (rd == null) {
                rd = (RetrieveDescImpl) store.getRetrieveDesc(pcClass);
                if (additionalField != null) {
                    RetrieveDesc frd = null;
                    String name = additionalField.getName();
                    // If the additionalField is not null, we will retrieve
                    // the field indicated by it along with the query.
                    if (additionalField instanceof ForeignFieldDesc) {
                        Class additionalClass = ((ForeignFieldDesc) additionalField).
                                foreignConfig.getPersistenceCapableClass();

                        frd = store.getRetrieveDesc(additionalClass);
                    }

                    rd.addPrefetchedField(name, frd);
                }

                addPKConstraints(rd);
                // Cache fillup.
                retrieveDescCache.put(cacheKey, rd);
            }
        }

        return rd;
    }

    /**
     * Returns a RetrieveDescriptor which represent a SQL query selecting pc
     * instances by the relationship key. The relationship key is taken from
     * the foreign field <code>foreignField</code> and used as query constraint.
     * Please note that the RDs are cached, so the method first checks the cache.
     * If there is no corresponding RetrieveDescriptor in the cache, it creates
     * a new one and stores it in the cache. FetchGroup fields will be added
     * when the query plan is build, see <code>SelectQueryPlan#processFetchGroups</code>.
     *
     * Note, the reason to introduce the RetrieveDesc cache in ClassDesc and not
     * in the store manager is, that we can have the cache per class, where
     * the store manager could only provide one big cache for all pc classes.
     *
     * @param foreignField The relationship field to be retrieved.
     *  Following is true for this field.
     * <ul>
     *  <li> It is part of an independent fetch group with only one field in the fetch group.
     *       <p>Or
     *       <P>Not part of any fetch group.
     *  </li>
     *  <li>It is not mapped to a join table. </li>
     * </ul>
     * @param store The store manager.
     * @return A RetrieveDescriptor selecting pc instance(s) corresponding to
     * the foreign field
     * @see #getRetrieveDescForPKQuery
     */
    public RetrieveDesc getRetrieveDescForFKQuery(ForeignFieldDesc foreignField, PersistenceStore store) {
        RetrieveDescImpl rd = null;
        String cacheKey = generateRDCacheKey(foreignField);

        synchronized (foreignRetrieveDescCache) {
            // Cache lookup.
            rd = (RetrieveDescImpl) foreignRetrieveDescCache.get(cacheKey);
            // Generate a new RD if there isn't one be found in the cache.
            if (rd == null) {
                rd = (RetrieveDescImpl) store.getRetrieveDesc(foreignField.foreignConfig.getPersistenceCapableClass());

                addFKConstraints(rd, foreignField);
                // Cache fillup.
                foreignRetrieveDescCache.put(cacheKey, rd);
            }
        }

        return rd;
    }

    /**
     * Gets RetrieveDescriptor(rd) for verifying a VC instance. The returned rd
     * is set up to expect constraints for pk followed by constraints for version
     * fields.
     * @param store
     * @return Instance of retrieve Descriptor for verifying a VC instance.
     */
    public RetrieveDesc getRetrieveDescForVerificationQuery(PersistenceStore store) {

        assert hasVersionConsistency();

        synchronized(retrieveDescForVerificationSynchObj) {
            if (retrieveDescForVerification == null) {
                RetrieveDescImpl rd = (RetrieveDescImpl) store.getRetrieveDesc(pcClass);

                int index = addPKConstraints(rd);
                rd.addParameterConstraints(versionFields, index);
                rd.setOption(RetrieveDescImpl.OPT_VERIFY);

                retrieveDescForVerification = rd;
            }
        }
        return retrieveDescForVerification;
    }

    /**
     * Generate for each keyField 'keyfield = ?' in the where clause
     * with InputParamValues for later binding of the actual query parameters.
     */
    private int addPKConstraints(RetrieveDescImpl rd) {
        // PK Constraints are always added first hence startIndex == 0
        rd.addParameterConstraints(keyFieldDescs, 0);

        return keyFieldDescs.length;
    }

    /**
     * Generate the condition on the relationship key in the where clause
     * with InputParamValues for later binding of the actual query parameters.
     */
    private void addFKConstraints(RetrieveDescImpl rd, ForeignFieldDesc foreignField) {
        for (int i = 0; i < foreignField.foreignFields.size(); i++) {
            LocalFieldDesc fff = (LocalFieldDesc) foreignField.foreignFields.get(i);
            rd.addParameterConstraint(fff, i);
        }
    }

    /**
     * Generates the key for a RetrieveDescriptor for the cache lookup.
     * The key has one of two forms:
     * <li>
     * Fully qualified classname of the pcclass, if additionalField is null.
     * </li>
     * <li>
     * Fully qualified classname of the pcclass + '/' + additionalFieldName +
     * '/' + additionalFieldID, otherwise.
     * </li>
     * @param additionalField The field to be retrieved in addition to the
     * DFG fields.
     * @return The generated cache key as a String.
     */
    private String generateRDCacheKey(FieldDesc additionalField)
    {
        StringBuffer key = new StringBuffer();

        key.append(pcClass.getName());

        if (additionalField != null) {
            // using '/' as separator between class and fieldname
            key.append('/');
            key.append(additionalField.getName());
            key.append('/');
            key.append(additionalField.absoluteID);
        }

        return key.toString();
    }

    /**
     * Returns true, if this class has got local fields not in the default
     * fetch group. Because UpdateQueryPlans for updates are cached depending
     * on the set of updated fields {@link #getUpdateQueryPlanForUpdate}, we
     * might need to compare the updated fields btw. two instances when
     * batching is enabled.
     *
     * @return True, if this class has got local fields not in the default
     * fetch group.
     * @see SQLStateManager#requiresImmediateFlush
     */
    public boolean hasLocalNonDFGFields() {
        // All instances with modified DFG fields can be batched with
        // the same statement. If there's a primitive field outside
        // the DFG, we need to compare modified fields to check if the
        // same statement can be used.
        return hasLocalNonDFGField;
    }

    /**
     * Retrieves the update query plan for the specified descriptor.
     * @param desc the descriptor
     * @param store the store manager
     * @return
     */
    public UpdateQueryPlan getUpdateQueryPlan(
            UpdateObjectDescImpl desc, SQLStoreManager store)
    {
        switch (desc.getUpdateAction()) {
            case ActionDesc.LOG_CREATE:
                return getUpdateQueryPlanForInsert(desc, store);
            case ActionDesc.LOG_DESTROY:
                return getUpdateQueryPlanForDelete(desc, store);
            case ActionDesc.LOG_UPDATE:
                return getUpdateQueryPlanForUpdate(desc, store);
            default:
                // TBD: error message + I18N
                // error unknown action
                return null;
        }
    }

    /** */
    private UpdateQueryPlan getUpdateQueryPlanForInsert(
            UpdateObjectDescImpl desc, SQLStoreManager store)
    {
        synchronized(updateQueryPlanForInsertSynchObj) {
            if (updateQueryPlanForInsert == null) {
                updateQueryPlanForInsert = buildQueryPlan(store, desc);
            }
        }
        return updateQueryPlanForInsert;
    }

    /** */
    private UpdateQueryPlan getUpdateQueryPlanForDelete(
            UpdateObjectDescImpl desc, SQLStoreManager store)
    {
        synchronized(updateQueryPlanForDeleteSynchObj) {
            if (updateQueryPlanForDelete == null) {
                updateQueryPlanForDelete = buildQueryPlan(store, desc);
            }
        }
        return updateQueryPlanForDelete;
    }

    /** */
    private UpdateQueryPlan getUpdateQueryPlanForUpdate(
            UpdateObjectDescImpl desc, SQLStoreManager store)
    {
        String key = getSortedFieldNumbers(desc.getUpdatedFields());
        UpdateQueryPlan plan;
        synchronized(updateQueryPlanCache) {
            plan = (UpdateQueryPlan)updateQueryPlanCache.get(key);
            if (plan == null) {
                plan = buildQueryPlan(store, desc);
                updateQueryPlanCache.put(key, plan);
            }
        }
        return plan;
    }

    /**
     * Builds and initializes a new query plan based on the information
     * passed with the <code>desc<//code> parameter. The returned plan
     * and its related data structures will be readonly.
     *
     * @param store Store manager
     * @param desc Update information, including the update action and
     * modified fields as appropriate.
     *
     * @return A new query plan. The returned plan will be readonly.
     */
    private UpdateQueryPlan buildQueryPlan(SQLStoreManager store,
                                           UpdateObjectDescImpl desc) {
        UpdateQueryPlan plan;

        plan = new UpdateQueryPlan(desc, store);
        plan.build(true);

        // Initialize the text for all statements. After this point,
        // the plan and its related data structures will be readonly.
        plan.getStatements();

        return plan;
    }

    /**
     * The methods returns the string representation of the sorted field numbers
     * of the FieldDescs.
     * The key is the string representation of the sorted field number list
     * of updated fields.
     * @param fields the list of FieldDescs
     * @return the sorted field number string
     */
    private String getSortedFieldNumbers(List fields)
    {
        // Use the array of field numbers of the updated fields as the key
        int size = fields.size();
        int [] fieldNos = new int[size];
        for (int i = 0; i < size; i++) {
            FieldDesc f = (FieldDesc)fields.get(i);
            fieldNos[i] = f.absoluteID;
        }
        Arrays.sort(fieldNos);
        return StringHelper.intArrayToSeparatedList(fieldNos, ","); //NOI18N
    }

}