/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.cmp.jdbc.bridge;

import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.ejb.EJBException;
import javax.ejb.EJBLocalObject;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.RemoveException;
import javax.ejb.TransactionRolledbackLocalException;
import javax.sql.DataSource;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import org.jboss.as.cmp.CmpMessages;
import org.jboss.as.cmp.bridge.EntityBridge;
import org.jboss.as.cmp.bridge.FieldBridge;
import org.jboss.as.cmp.component.CmpEntityBeanComponent;
import org.jboss.as.cmp.context.CmpEntityBeanContext;
import org.jboss.as.cmp.ejbql.Catalog;
import org.jboss.as.cmp.jdbc.CascadeDeleteStrategy;
import org.jboss.as.cmp.jdbc.JDBCContext;
import org.jboss.as.cmp.jdbc.JDBCEntityPersistenceStore;
import org.jboss.as.cmp.jdbc.JDBCParameterSetter;
import org.jboss.as.cmp.jdbc.JDBCResultSetReader;
import org.jboss.as.cmp.jdbc.JDBCStoreManager;
import org.jboss.as.cmp.jdbc.JDBCType;
import org.jboss.as.cmp.jdbc.RelationData;
import org.jboss.as.cmp.jdbc.SQLUtil;
import org.jboss.as.cmp.jdbc.metadata.JDBCCMPFieldMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCReadAheadMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCRelationMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCRelationshipRoleMetaData;
import org.jboss.logging.Logger;
import org.jboss.tm.TransactionLocal;

import static org.jboss.as.cmp.CmpMessages.MESSAGES;

/**
 * JDBCCMRFieldBridge a bean relationship. This class only supports
 * relationships between entities managed by a JDBCStoreManager in the same
 * application.
 * <p/>
 * Life-cycle:
 * Tied to the EntityBridge.
 * <p/>
 * Multiplicity:
 * One for each role that entity has.
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
 * @version $Revision: 81030 $
 */
public final class JDBCCMRFieldBridge extends JDBCAbstractCMRFieldBridge {
    /**
     * The entity bridge to which this cmr field belongs.
     */
    private final JDBCEntityBridge entity;
    /**
     * The manager of this entity.
     */
    private final JDBCStoreManager manager;
    /**
     * Metadata of the relationship role that this field represents.
     */
    private final JDBCRelationshipRoleMetaData metadata;
    /**
     * The data source used to access the relation table if relevant.
     */
    private DataSource dataSource;
    /**
     * The relation table name if relevant.
     */
    private String qualifiedTableName;
    private String tableName;
    /**
     * The key fields that this entity maintains in the relation table.
     */
    private JDBCCMP2xFieldBridge[] tableKeyFields;
    /**
     * JDBCType for the foreign key fields. Basically, this is an ordered
     * merge of the JDBCType of the foreign key field.
     */
    private JDBCType jdbcType;
    /**
     * The related entity's container.
     */
    private WeakReference<CmpEntityBeanComponent> relatedContainerRef;
    /**
     * The related entity's jdbc store manager
     */
    private JDBCStoreManager relatedManager;
    /**
     * The related entity.
     */
    private JDBCEntityBridge relatedEntity;
    /**
     * The related entity's cmr field for this relationship.
     */
    private JDBCCMRFieldBridge relatedCMRField;
    /**
     * da log.
     */
    private final Logger log;

    /**
     * Foreign key fields of this entity (i.e., related entities pk fields)
     */
    private JDBCCMP2xFieldBridge[] foreignKeyFields;
    /**
     * Indicates whether all FK fields are mapped to PK fields
     */
    private boolean allFKFieldsMappedToPKFields;
    /**
     * This map contains related PK fields that are mapped through FK fields to this entity's PK fields
     */
    private final Map relatedPKFieldsByMyPKFields = new HashMap();
    /**
     * This map contains related PK fields keyed by FK fields
     */
    private final Map relatedPKFieldsByMyFKFields = new HashMap();
    /**
     * Indicates whether there are foreign key fields mapped to CMP fields
     */
    private boolean hasFKFieldsMappedToCMPFields;

    // Map for lists of related PK values keyed by this side's PK values.
    // The values are put/removed by related entities when its fields representing
    // foreign key are changed. When entity with this CMR is created, this map is checked
    // for waiting for it entities. Relationship with waiting entities is established,
    // removing waiting entities' primary keys from the map.
    // NOTE: this map is used only for foreign key fields mapped to CMP fields.
    private TransactionLocal relatedPKValuesWaitingForMyPK;

    /**
     * FindByPrimaryKey method used to find related instances in case when FK fields mapped to PK fields
     */
    private Method relatedFindByPrimaryKey;

    /**
     * index of the field in the JDBCContext
     */
    private final int jdbcContextIndex;

    /**
     * cascade-delete strategy
     */
    private CascadeDeleteStrategy cascadeDeleteStrategy;

    /**
     * This CMR field and its related CMR field share the same RelationDataManager
     */
    private RelationDataManager relationManager;

    /**
     * Creates a cmr field for the entity based on the metadata.
     */
    public JDBCCMRFieldBridge(JDBCEntityBridge entity,
                              JDBCStoreManager manager,
                              JDBCRelationshipRoleMetaData metadata) {
        this.entity = entity;
        this.manager = manager;
        this.metadata = metadata;
        this.jdbcContextIndex = ((JDBCEntityBridge) manager.getEntityBridge()).getNextJDBCContextIndex();

        //  Create the log
        String categoryName = this.getClass().getName() +
                "." + manager.getMetaData().getName() + ".";
        if (metadata.getCMRFieldName() != null) {
            categoryName += metadata.getCMRFieldName();
        } else {
            categoryName += metadata.getRelatedRole().getEntity().getName() +
                    "-" + metadata.getRelatedRole().getCMRFieldName();
        }
        this.log = Logger.getLogger(categoryName);
    }

    public RelationDataManager getRelationDataManager() {
        return relationManager;
    }

    public void resolveRelationship() {
        //
        // Set handles to the related entity's container, cache,
        // manager, and invoker
        //

        // Related Entity Name
        String relatedEntityName = metadata.getRelatedRole().getEntity().getName();

        // Related Entity
        Catalog catalog = manager.getCatalog();
        relatedEntity = (JDBCEntityBridge) catalog.getEntityByEJBName(relatedEntityName);
        if (relatedEntity == null) {
            throw CmpMessages.MESSAGES.relatedEntityNotFound(getEntity().getEntityName(), getFieldName(), relatedEntityName);
        }

        // Related CMR Field
        JDBCCMRFieldBridge[] cmrFields = (JDBCCMRFieldBridge[]) relatedEntity.getCMRFields();
        for (int i = 0; i < cmrFields.length; ++i) {
            JDBCCMRFieldBridge cmrField = cmrFields[i];
            if (metadata.getRelatedRole() == cmrField.getMetaData()) {
                relatedCMRField = cmrField;
                break;
            }
        }

        // if we didn't find the related CMR field throw an exception
        // with a detailed message
    if (relatedCMRField == null) {
            throw MESSAGES.relatedCmrFieldNotFound(relatedEntityName, entity.getEntityName(), getFieldName() != null ? getFieldName() : "<no-field>",
                    relatedEntityName, metadata.getRelatedRole().getCMRFieldName() != null ? metadata.getRelatedRole().getCMRFieldName() : "<no-field>");
        }

        // Related Manager
        relatedManager = (JDBCStoreManager) relatedEntity.getManager();

        // Related Container
        CmpEntityBeanComponent relatedComponent = relatedManager.getComponent();
        this.relatedContainerRef = new WeakReference<CmpEntityBeanComponent>(relatedComponent);

        // related findByPrimaryKey
        Class homeClass = (relatedComponent.getLocalHomeClass() != null ?
                relatedComponent.getLocalHomeClass() : relatedComponent.getHomeClass());
        try {
            relatedFindByPrimaryKey =
                    homeClass.getMethod("findByPrimaryKey", new Class[]{relatedEntity.getPrimaryKeyClass()});
        } catch (Exception e) {
            throw CmpMessages.MESSAGES.findByPrimaryKeyNotFound(relatedEntity.getPrimaryKeyClass().getName(), homeClass.getName());

        }

        //
        // Initialize the key fields
        //
        if (metadata.getRelationMetaData().isTableMappingStyle()) {
            // initialize relation table key fields
            Collection tableKeys = metadata.getKeyFields();
            List keyFieldsList = new ArrayList(tableKeys.size());

            // first phase is to create fk fields
            Map pkFieldsToFKFields = new HashMap(tableKeys.size());
            for (Iterator i = tableKeys.iterator(); i.hasNext(); ) {
                JDBCCMPFieldMetaData cmpFieldMetaData = (JDBCCMPFieldMetaData) i.next();
                FieldBridge pkField = entity.getFieldByName(cmpFieldMetaData.getFieldName());
                if (pkField == null) {
                    throw MESSAGES.pkNotFoundForKeyField(cmpFieldMetaData.getFieldName());
                }
                pkFieldsToFKFields.put(pkField, new JDBCCMP2xFieldBridge(manager, cmpFieldMetaData));
            }
            // second step is to order fk fields to match the order of pk fields
            JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
            for (int i = 0; i < pkFields.length; ++i) {
                Object fkField = pkFieldsToFKFields.get(pkFields[i]);
                if (fkField == null) {
                    throw MESSAGES.primaryKeyNotMapped(pkFields[i].getFieldName());
                }
                keyFieldsList.add(fkField);
            }
            tableKeyFields = (JDBCCMP2xFieldBridge[]) keyFieldsList.toArray(
                    new JDBCCMP2xFieldBridge[keyFieldsList.size()]);

            dataSource = manager.getDataSource(metadata.getRelationMetaData().getDataSourceName());
        } else {
            initializeForeignKeyFields();
            dataSource = hasForeignKey() ? entity.getDataSource() : relatedEntity.getDataSource();
        }

        // Fix table name
        //
        // This code doesn't work here...  The problem each side will generate
        // the table name and this will only work for simple generation.
        qualifiedTableName = SQLUtil.fixTableName(metadata.getRelationMetaData().getDefaultTableName(), dataSource);
        tableName = SQLUtil.getTableNameWithoutSchema(qualifiedTableName);

        relationManager = relatedCMRField.initRelationManager(this);
    }

    /**
     * The third phase of deployment. The method is called when relationships are already resolved.
     *
     * @
     */
    public void start() {
        cascadeDeleteStrategy = CascadeDeleteStrategy.getCascadeDeleteStrategy(this);
        relatedPKValuesWaitingForMyPK = new TransactionLocal(this.manager.getComponent().getTransactionManager()) {
            protected Object initialValue() {
                return new HashMap();
            }

            public Transaction getTransaction() {
                try {
                    return transactionManager.getTransaction();
                } catch (SystemException e) {
                    throw CmpMessages.MESSAGES.errorGettingCurrentTransaction(e);
                }
            }
        };
    }

    public boolean removeFromRelations(CmpEntityBeanContext ctx, Object[] oldRelationsRef) {
        load(ctx);

        FieldState fieldState = getFieldState(ctx);
        List value = fieldState.getValue();

        boolean removed = false;
        if (!value.isEmpty()) {
            if (hasFKFieldsMappedToCMPFields) {
                if (isForeignKeyValid(value.get(0))) {
                    cascadeDeleteStrategy.removedIds(ctx, oldRelationsRef, value);
                    removed = true;
                }
            } else {
                cascadeDeleteStrategy.removedIds(ctx, oldRelationsRef, value);
                removed = true;
            }
        }
        return removed;
    }

    public void cascadeDelete(CmpEntityBeanContext ctx, List oldValues) throws RemoveException, RemoteException {
        cascadeDeleteStrategy.cascadeDelete(ctx, oldValues);
    }

    public boolean isBatchCascadeDelete() {
        return (cascadeDeleteStrategy instanceof CascadeDeleteStrategy.BatchCascadeDeleteStrategy);
    }

    /**
     * Gets the manager of this entity.
     */
    public JDBCStoreManager getJDBCStoreManager() {
        return manager;
    }

    /**
     * Gets bridge for this entity.
     */
    public JDBCAbstractEntityBridge getEntity() {
        return entity;
    }

    /**
     * Gets the metadata of the relationship role that this field represents.
     */
    public JDBCRelationshipRoleMetaData getMetaData() {
        return metadata;
    }

    /**
     * Gets the relation metadata.
     */
    public JDBCRelationMetaData getRelationMetaData() {
        return metadata.getRelationMetaData();
    }

    /**
     * Gets the name of this field.
     */
    public String getFieldName() {
        return metadata.getCMRFieldName();
    }

    /**
     * Gets the name of the relation table if relevant.
     */
    public String getQualifiedTableName() {
        return qualifiedTableName;
    }

    public String getTableName() {
        return tableName;
    }

    /**
     * Gets the datasource of the relation table if relevant.
     */
    public DataSource getDataSource() {
        return dataSource;
    }

    /**
     * Gets the read ahead meta data.
     */
    public JDBCReadAheadMetaData getReadAhead() {
        return metadata.getReadAhead();
    }

    public JDBCType getJDBCType() {
        return jdbcType;
    }

    public boolean isPrimaryKeyMember() {
        return false;
    }

    /**
     * Does this cmr field have foreign keys.
     */
    public boolean hasForeignKey() {
        return foreignKeyFields != null;
    }

    /**
     * Returns true if all FK fields are mapped to PK fields
     */
    public boolean allFkFieldsMappedToPkFields() {
        return allFKFieldsMappedToPKFields;
    }

    /**
     * Is this a collection valued field.
     */
    public boolean isCollectionValued() {
        return metadata.getRelatedRole().isMultiplicityMany();
    }

    /**
     * Is this a single valued field.
     */
    public boolean isSingleValued() {
        return metadata.getRelatedRole().isMultiplicityOne();
    }

    /**
     * Gets the key fields that this entity maintains in the relation table.
     */
    public JDBCFieldBridge[] getTableKeyFields() {
        return tableKeyFields;
    }

    /**
     * Gets the foreign key fields of this entity (i.e., related entities pk fields)
     */
    public JDBCFieldBridge[] getForeignKeyFields() {
        return foreignKeyFields;
    }

    /**
     * The related entity's cmr field for this relationship.
     */
    public JDBCAbstractCMRFieldBridge getRelatedCMRField() {
        return relatedCMRField;
    }

    /**
     * The related manger.
     */
    public JDBCStoreManager getRelatedManager() {
        return relatedManager;
    }

    /**
     * The related entity.
     */
    public EntityBridge getRelatedEntity() {
        return relatedEntity;
    }

    /**
     * The related entity.
     */
    public JDBCEntityBridge getRelatedJDBCEntity() {
        return relatedEntity;
    }

    /**
     * The related container
     */
    public CmpEntityBeanComponent getRelatedComponent() {
        return relatedContainerRef.get();
    }

    /**
     * The related entity's local home interface.
     */
    public Class getRelatedLocalInterface() {
        return getRelatedComponent().getStoreManager().getEntityBridge().getLocalInterface();
    }

    /**
     * @param ctx - entity's context
     * @return true if entity is loaded, false - otherwise.
     */
    public boolean isLoaded(CmpEntityBeanContext ctx) {
        return getFieldState(ctx).isLoaded;
    }

    /**
     * Establishes relationships with related entities waited for passed in context
     * to be created.
     *
     * @param ctx - entity's context.
     */
    public void addRelatedPKsWaitedForMe(CmpEntityBeanContext ctx) {
        final Map relatedPKsMap = getRelatedPKsWaitingForMyPK();
        synchronized (relatedPKsMap) {
            List relatedPKsWaitingForMe = (List) relatedPKsMap.get(ctx.getPrimaryKeyUnchecked());
            if (relatedPKsWaitingForMe != null) {
                for (Iterator waitingPKsIter = relatedPKsWaitingForMe.iterator(); waitingPKsIter.hasNext(); ) {
                    Object waitingPK = waitingPKsIter.next();
                    waitingPKsIter.remove();
                    if (isForeignKeyValid(waitingPK)) {
                        createRelationLinks(ctx, waitingPK);
                    }
                }
            }
        }
    }

    /**
     * Is this field readonly?
     */
    public boolean isReadOnly() {
        return getRelationMetaData().isReadOnly();
    }

    /**
     * Had the read time expired?
     */
    public boolean isReadTimedOut(CmpEntityBeanContext ctx) {
        // if we are read/write then we are always timed out
        if (!isReadOnly()) {
            return true;
        }

        // if read-time-out is -1 then we never time out.
        if (getRelationMetaData().getReadTimeOut() == -1) {
            return false;
        }

        long readInterval = System.currentTimeMillis() - getFieldState(ctx).getLastRead();
        return readInterval > getRelationMetaData().getReadTimeOut();
    }

    /**
     * @param ctx - entity's context.
     * @return the value of this field.
     */
    public Object getValue(CmpEntityBeanContext ctx) {
        // no user checks yet, but this is where they would go
        return getInstanceValue(ctx);
    }

    /**
     * Sets new value.
     *
     * @param ctx   - entity's context;
     * @param value - new value.
     */
    public void setValue(CmpEntityBeanContext ctx, Object value) {
        if (isReadOnly()) {
            throw MESSAGES.fieldIsReadOnly(getFieldName());
        }

        if (!JDBCEntityBridge.isEjbCreateDone(ctx)) {
            throw MESSAGES.cmrFieldCannotBeSetInEjbCreate();
        }

        if (isCollectionValued() && value == null) {
            throw MESSAGES.cmrCollectionFieldsCanNotBeSetToNull();
        }
        setInstanceValue(ctx, value);
    }

    /**
     * Gets the value of the cmr field for the instance associated with
     * the context.
     */
    public Object getInstanceValue(CmpEntityBeanContext myCtx) {
        load(myCtx);
        FieldState fieldState = getFieldState(myCtx);
        if (isCollectionValued()) {
            return fieldState.getRelationSet();
        }

        // only return one
        try {
            List value = fieldState.getValue();
            if (!value.isEmpty()) {
                Object fk = value.get(0);
                return getRelatedEntityByFK(fk);
            } else if (foreignKeyFields != null) {
                // for those completely mapped to CMP fields and created in this current tx !!!
                Object relatedId = getRelatedIdFromContext(myCtx);
                if (relatedId != null) {
                    return getRelatedEntityByFK(relatedId);
                }
            }
            return null;
        } catch (EJBException e) {
            throw e;
        } catch (Exception e) {
            throw MESSAGES.errorGettingInstanceValue(e);
        }
    }

    /**
     * Returns related entity's local interface.
     * If there are foreign key fields mapped to CMP fields, existence of related entity is checked
     * with findByPrimaryKey and if, in this case, related instance is not found, null is returned.
     * If foreign key fields mapped to its own columns then existence of related entity is not checked
     * and just its local object is returned.
     *
     * @param fk - foreign key value.
     * @return related local object instance.
     */
    public EJBLocalObject getRelatedEntityByFK(Object fk) {
        EJBLocalObject relatedLocalObject = null;
        final CmpEntityBeanComponent relatedContainer = getRelatedComponent();

        if (hasFKFieldsMappedToCMPFields && relatedManager.getReadAheadCache().getPreloadDataMap(fk, false) == null) {  // not in preload cache
            try {
                relatedLocalObject = relatedContainer.getEJBLocalObject(fk);
            } catch (Exception ignore) {
                // no such entity. it is ok to ignore
            }
        } else {
            relatedLocalObject = relatedContainer.getEJBLocalObject(fk);
        }

        return relatedLocalObject;
    }

    /**
     * This method is called only for CMR fields with foreign key fields mapped to CMP fields
     * to check the validity of the foreign key value.
     *
     * @param fk the foreign key to check
     * @return true if there is related entity with the equal primary key
     */
    public boolean isForeignKeyValid(Object fk) {
        boolean valid;
        if (relatedManager.getReadAheadCache().getPreloadDataMap(fk, false) != null) {
            valid = true;
        } else {
            try {
                Object result = getRelatedComponent().getEJBLocalObject(fk);
                valid = result != null;
            } catch (Exception ignore) {
                // no such entity. it is ok to ignore
                valid = false;
            }
        }
        return valid;
    }

    /**
     * Sets the value of the cmr field for the instance associated with
     * the context.
     */
    public void setInstanceValue(CmpEntityBeanContext myCtx, Object newValue) {
        // validate new value first
        List newPks;
        if (newValue instanceof Collection) {
            Collection col = (Collection) newValue;
            if (!col.isEmpty()) {
                newPks = new ArrayList(col.size());
                for (Iterator iter = col.iterator(); iter.hasNext(); ) {
                    Object localObject = iter.next();
                    if (localObject != null) {
                        Object relatedId;
                        try {
                            relatedId = getRelatedPrimaryKey(localObject);
                        } catch (TransactionRolledbackLocalException e) {
                            //if the instance has been removed we need to throw IllegalArgumentException
                            throw MESSAGES.instanceAlreadyRemoved(e);
                        }
                        // check whether new value modifies the primary key if there are FK fields mapped to PK fields
                        if (relatedPKFieldsByMyPKFields.size() > 0) {
                            checkSetForeignKey(myCtx, relatedId);
                        }

                        newPks.add(relatedId);

                    }
                }
            } else {
                newPks = Collections.EMPTY_LIST;
            }
        } else {
            if (newValue != null) {
                try {
                    //if the instance has been removed we need to throw IllegalArgumentException
                    newPks = Collections.singletonList(getRelatedPrimaryKey(newValue));
                } catch (TransactionRolledbackLocalException e) {
                    throw MESSAGES.instanceAlreadyRemoved(e);
                }
            } else {
                newPks = Collections.EMPTY_LIST;
            }
        }

        // load the current value
        load(myCtx);
        FieldState fieldState = getFieldState(myCtx);

        // is this just setting our own relation set back
        if (newValue == fieldState.getRelationSet()) {
            return;
        }

        try {
            // Remove old value(s)
            List value = fieldState.getValue();
            if (!value.isEmpty()) {
                Object[] curPks = value.toArray(new Object[value.size()]);
                for (int i = 0; i < curPks.length; ++i) {
                    destroyRelationLinks(myCtx, curPks[i]);
                }
            }

            // Add new value(s)
            for (int i = 0; i < newPks.size(); ++i) {
                createRelationLinks(myCtx, newPks.get(i));
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Checks whether new foreign key value conflicts with primary key value
     * in case of foreign key to primary key mapping.
     *
     * @param myCtx    - entity's context;
     * @param newValue - new foreign key value.
     * @throws IllegalStateException - if new foreign key value changes
     *                               primary key value, otherwise returns silently.
     */
    private void checkSetForeignKey(CmpEntityBeanContext myCtx, Object newValue)
            throws IllegalStateException {
        JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
        for (int i = 0; i < pkFields.length; ++i) {
            JDBCCMP2xFieldBridge pkField = (JDBCCMP2xFieldBridge) pkFields[i];
            JDBCCMP2xFieldBridge relatedPkField = (JDBCCMP2xFieldBridge) relatedPKFieldsByMyPKFields.get(pkField);
            if (relatedPkField != null) {
                Object comingValue = relatedPkField.getPrimaryKeyValue(newValue);
                Object currentValue = pkField.getInstanceValue(myCtx);

                // they shouldn't be null
                if (!comingValue.equals(currentValue)) {
                    throw MESSAGES.cannotCreateRelationship(entity.getEntityName(), getFieldName(), currentValue, comingValue);
                }
            }
        }
    }

    /**
     * Creates the relation links between the instance associated with the
     * context and the related instance (just the id is passed in).
     * <p/>
     * This method calls a.addRelation(b) and b.addRelation(a)
     */
    public void createRelationLinks(CmpEntityBeanContext myCtx, Object relatedId) {
        createRelationLinks(myCtx, relatedId, true);
    }

    public void createRelationLinks(CmpEntityBeanContext myCtx, Object relatedId, boolean updateForeignKey) {
        if (isReadOnly()) {
            throw MESSAGES.fieldIsReadOnly(getFieldName());
        }

        // If my multiplicity is one, then we need to free the new related context
        // from its old relationship.
        if (metadata.isMultiplicityOne()) {
            Object oldRelatedId = relatedCMRField.invokeGetRelatedId(relatedId);
            if (oldRelatedId != null) {
                invokeRemoveRelation(oldRelatedId, relatedId);
                relatedCMRField.invokeRemoveRelation(relatedId, oldRelatedId);
            }
        }

        addRelation(myCtx, relatedId, updateForeignKey);
        relatedCMRField.invokeAddRelation(relatedId, myCtx.getPrimaryKeyUnchecked());
    }

    /**
     * Destroys the relation links between the instance associated with the
     * context and the related instance (just the id is passed in).
     * <p/>
     * This method calls a.removeRelation(b) and b.removeRelation(a)
     */
    public void destroyRelationLinks(CmpEntityBeanContext myCtx, Object relatedId) {
        destroyRelationLinks(myCtx, relatedId, true);
    }

    /**
     * Destroys the relation links between the instance associated with the
     * context and the related instance (just the id is passed in).
     * <p/>
     * This method calls a.removeRelation(b) and b.removeRelation(a)
     * <p/>
     * If updateValueCollection is false, the related id collection is not
     * updated. This form is only used by the RelationSet iterator.
     */
    public void destroyRelationLinks(CmpEntityBeanContext myCtx,
                                     Object relatedId,
                                     boolean updateValueCollection) {
        destroyRelationLinks(myCtx, relatedId, updateValueCollection, true);
    }

    public void destroyRelationLinks(CmpEntityBeanContext myCtx,
                                     Object relatedId,
                                     boolean updateValueCollection,
                                     boolean updateForeignKey) {
        if (isReadOnly()) {
            throw MESSAGES.fieldIsReadOnly(getFieldName());
        }

        removeRelation(myCtx, relatedId, updateValueCollection, updateForeignKey);
        relatedCMRField.invokeRemoveRelation(relatedId, myCtx.getPrimaryKeyUnchecked());
    }

    /**
     * Schedules children for cascade delete.
     */
    public void scheduleChildrenForCascadeDelete(CmpEntityBeanContext ctx) {
        load(ctx);
        FieldState fieldState = getFieldState(ctx);
        List value = fieldState.getValue();
        if (!value.isEmpty()) {
            Transaction tx = getTransaction();
            for (int i = 0; i < value.size(); ++i) {
                relatedCMRField.invokeScheduleForCascadeDelete(value.get(i));
            }
        }
    }

    /**
     * Schedules children for batch cascade delete.
     */
    public void scheduleChildrenForBatchCascadeDelete(CmpEntityBeanContext ctx) {
        load(ctx);
        FieldState fieldState = getFieldState(ctx);
        List value = fieldState.getValue();
        if (!value.isEmpty()) {
            Transaction tx = getTransaction();
            for (int i = 0; i < value.size(); ++i) {
                relatedCMRField.invokeScheduleForBatchCascadeDelete(value.get(i));
            }
        }
    }

    /**
     * Schedules the instance with myId for cascade delete.
     */
    private void invokeScheduleForCascadeDelete(Object myId) {
        try {
            getManager().getComponent().invoke(CMRMessage.SCHEDULE_FOR_CASCADE_DELETE, myId, this);
        } catch (Exception e) {
            throw MESSAGES.errorInScheduleCascadeDelete(e);
        }
    }

    /**
     * Schedules the instance with myId for batch cascade delete.
     */
    private void invokeScheduleForBatchCascadeDelete(Object myId) {
        try {
            getManager().getComponent().invoke(CMRMessage.SCHEDULE_FOR_BATCH_CASCADE_DELETE, myId, this);
        } catch (Exception e) {
            throw MESSAGES.errorInScheduleBatchCascadeDelete(e);
        }
    }

    /**
     * Invokes the getRelatedId on the related CMR field via the container
     * invocation interceptor chain.
     */
    private Object invokeGetRelatedId(Object myId) {
        try {
            return getManager().getComponent().invoke(CMRMessage.GET_RELATED_ID, myId, this);
        } catch (Exception e) {
            if(getManager().getComponent().getCache().contains(myId)) {
                getManager().getComponent().getCache().discard(getManager().getComponent().getCache().get(myId));
            }
            throw MESSAGES.errorGetRelatedId(e);
        }
    }

    private void invokeAddRelation(Object myId, Object relatedId) {
        try {
            getManager().getComponent().invoke(CMRMessage.ADD_RELATION, myId, this, relatedId);
        } catch (Exception e) {
            throw MESSAGES.errorAddRelation(e);
        }
    }

    private void invokeRemoveRelation(Object myId, Object relatedId) {
        try {
            getManager().getComponent().invoke(CMRMessage.REMOVE_RELATION, myId, this, relatedId);
        } catch (Exception e) {
            throw MESSAGES.errorRemoveRelation(e);
        }
    }

    /**
     * Get the related entity's id.  This only works on single valued cmr fields.
     */
    public Object getRelatedId(CmpEntityBeanContext myCtx) {
        if (isCollectionValued()) {
            throw MESSAGES.getRelatedOnlyForMuliOfOne();
        }

        load(myCtx);
        List value = getFieldState(myCtx).getValue();
        return value.isEmpty() ? null : value.get(0);
    }

    /**
     * Creates a new instance of related id based on foreign key value in the context.
     *
     * @param ctx - entity's context.
     * @return related entity's id.
     */
    public Object getRelatedIdFromContext(CmpEntityBeanContext ctx) {
        Object relatedId = null;
        Object fkFieldValue;
        for (JDBCCMP2xFieldBridge fkField : foreignKeyFields) {
            fkFieldValue = fkField.getInstanceValue(ctx);
            if (fkFieldValue == null) {
                return null;
            }
            JDBCCMP2xFieldBridge relatedPKField = (JDBCCMP2xFieldBridge) relatedPKFieldsByMyFKFields.get(fkField);
            relatedId = relatedPKField.setPrimaryKeyValue(relatedId, fkFieldValue);
        }
        return relatedId;
    }

    /**
     * Adds the foreign key to the set of related ids, and updates any foreign key fields.
     */
    public void addRelation(CmpEntityBeanContext myCtx, Object fk) {
        addRelation(myCtx, fk, true);
        relationManager.addRelation(this, myCtx.getPrimaryKeyUnchecked(), relatedCMRField, fk);
    }

    private void addRelation(CmpEntityBeanContext myCtx, Object fk, boolean updateForeignKey) {
        checkSetForeignKey(myCtx, fk);

        if (isReadOnly()) {
            throw MESSAGES.fieldIsReadOnly(getFieldName());
        }

        if (!JDBCEntityBridge.isEjbCreateDone(myCtx)) {
            throw MESSAGES.cmrFieldCannotBeSetOrAddedInEjbCreate();
        }
        // add to current related set
        FieldState myState = getFieldState(myCtx);
        myState.addRelation(fk);

        // set the foreign key, if we have one.
        if (hasForeignKey() && updateForeignKey) {
            setForeignKey(myCtx, fk);
        }
    }

    /**
     * Removes the foreign key to the set of related ids, and updates any foreign key fields.
     */
    public void removeRelation(CmpEntityBeanContext myCtx, Object fk) {
        removeRelation(myCtx, fk, true, true);
        relationManager.removeRelation(this, myCtx.getPrimaryKeyUnchecked(), relatedCMRField, fk);
    }

    private void removeRelation(CmpEntityBeanContext myCtx,
                                Object fk,
                                boolean updateValueCollection,
                                boolean updateForeignKey) {
        if (isReadOnly()) {
            throw MESSAGES.fieldIsReadOnly(getFieldName());
        }

        // remove from current related set
        if (updateValueCollection) {
            FieldState myState = getFieldState(myCtx);
            myState.removeRelation(fk);
        }

        // set the foreign key to null, if we have one.
        if (hasForeignKey() && updateForeignKey) {
            setForeignKey(myCtx, null);
        }
    }

    /**
     * loads the collection of related ids
     * NOTE: after loading, the field might not be in a clean state as we support adding and removing
     * relations while the field is not loaded. The actual value of the field will be the value loaded
     * plus added relations and minus removed relations while the field was not loaded.
     */
    private void load(CmpEntityBeanContext myCtx) {
        // if we are already loaded we're done
        FieldState fieldState = getFieldState(myCtx);
        if (fieldState.isLoaded()) {
            return;
        }

        // check the preload cache
        if (log.isTraceEnabled()) {
            log.trace("Read ahead cache load: cmrField=" + getFieldName() + " pk=" + myCtx.getPrimaryKeyUnchecked());
        }

        manager.getReadAheadCache().load(myCtx);
        if (fieldState.isLoaded()) {
            return;
        }

        // load the value from the database
        Collection values;
        if (hasForeignKey()) {
            // WARN: this method will load foreign keys if they are not yet loaded and
            // changes relationship lazy loading in advanced training labs.
            // i.e. it will load lazy cmp fields first of this entity and then will lazy load the related entity
            // instead of loading this entity JOIN related entity in one query.
            //Object fk = getRelatedIdFromContext(myCtx);

            boolean loadWithManager = false;
            Object fk = null;
            for (int i = 0; i < foreignKeyFields.length; ++i) {
                JDBCCMP2xFieldBridge fkField = foreignKeyFields[i];
                // if the field is not loaded then load relationship with manager
                if (!fkField.isLoaded(myCtx)) {
                    loadWithManager = true;
                    break;
                }

                Object fkFieldValue = fkField.getInstanceValue(myCtx);
                // if one of the fk is null, the whole fk is considered to be null
                if (fkFieldValue == null) {
                    fk = null;
                    break;
                }
                JDBCCMP2xFieldBridge relatedPKField = (JDBCCMP2xFieldBridge) relatedPKFieldsByMyFKFields.get(fkField);
                fk = relatedPKField.setPrimaryKeyValue(fk, fkFieldValue);
            }

            if (loadWithManager) {
                values = manager.loadRelation(this, myCtx.getPrimaryKeyUnchecked());
            } else {
                values = (fk == null ? Collections.EMPTY_LIST : Collections.singletonList(fk));
            }
        } else {
            values = manager.loadRelation(this, myCtx.getPrimaryKeyUnchecked());
        }
        load(myCtx, values);
    }

    public void load(CmpEntityBeanContext myCtx, Collection values) {
        // did we get more then one value for a single valued field
        if (isSingleValued() && values.size() > 1) {
            throw MESSAGES.singleValuedFieldHasMultipleValues(values);
        }

        // add the new values
        FieldState fieldState = getFieldState(myCtx);
        fieldState.loadRelations(values);

        // set the foreign key, if we have one.
        if (hasForeignKey()) {
            // update the states and locked values of FK fields
            if (!values.isEmpty()) {
                Object loadedValue = values.iterator().next();
                for (int i = 0; i < foreignKeyFields.length; ++i) {
                    JDBCCMP2xFieldBridge fkField = foreignKeyFields[i];
                    Object fieldValue = fkField.getPrimaryKeyValue(loadedValue);
                    fkField.updateState(myCtx, fieldValue);
                }
            }

            // set the real FK value
            List realValue = fieldState.getValue();
            Object fk = realValue.isEmpty() ? null : realValue.get(0);
            setForeignKey(myCtx, fk);
        }

        JDBCEntityBridge.setCreated(myCtx);
    }

    /**
     * Sets the foreign key field value.
     */
    public void setForeignKey(CmpEntityBeanContext myCtx, Object fk) {
        if (!hasForeignKey()) {
            throw MESSAGES.cmrFieldNoForeignKeySet(getFieldName());
        }

        for (int i = 0; i < foreignKeyFields.length; ++i) {
            JDBCCMP2xFieldBridge fkField = foreignKeyFields[i];
            Object fieldValue = fkField.getPrimaryKeyValue(fk);
            fkField.setInstanceValue(myCtx, fieldValue);
        }
    }

    /**
     * Initialized the foreign key fields.
     */
    public void initInstance(CmpEntityBeanContext ctx) {
        // mark this field as loaded
        getFieldState(ctx).loadRelations(Collections.EMPTY_SET);

        if (foreignKeyFields == null) {
            return;
        }

        for (int i = 0; i < foreignKeyFields.length; ++i) {
            JDBCCMP2xFieldBridge foreignKeyField = foreignKeyFields[i];
            if (!foreignKeyField.isFKFieldMappedToCMPField()) {
                foreignKeyField.setInstanceValue(ctx, null);
            }
        }
    }

    /**
     * resets the persistence context of the foreign key fields
     */
    public void resetPersistenceContext(CmpEntityBeanContext ctx) {
        // only resetStats if the read has timed out
        if (!isReadTimedOut(ctx)) {
            return;
        }

        // clear the field state
        JDBCContext jdbcCtx = (JDBCContext) ctx.getPersistenceContext();
        // invalidate current field state
        /*
     FieldState currentFieldState = (FieldState) jdbcCtx.getFieldState(jdbcContextIndex);
     if(currentFieldState != null)
        currentFieldState.invalidate();
        */
        jdbcCtx.setFieldState(jdbcContextIndex, null);

        if (foreignKeyFields == null) {
            return;
        }

        for (int i = 0; i < foreignKeyFields.length; ++i) {
            JDBCCMP2xFieldBridge foreignKeyField = foreignKeyFields[i];
            if (!foreignKeyField.isFKFieldMappedToCMPField()) {
                foreignKeyField.resetPersistenceContext(ctx);
            }
        }
    }

    public int setInstanceParameters(PreparedStatement ps,
                                     int parameterIndex,
                                     CmpEntityBeanContext ctx) {
        if (foreignKeyFields == null) {
            return parameterIndex;
        }

        List value = getFieldState(ctx).getValue();
        Object fk = (value.isEmpty() ? null : value.get(0));

        for (int i = 0; i < foreignKeyFields.length; ++i) {
            parameterIndex = foreignKeyFields[i].setPrimaryKeyParameters(ps, parameterIndex, fk);
        }

        return parameterIndex;
    }

    public int loadInstanceResults(ResultSet rs,
                                   int parameterIndex,
                                   CmpEntityBeanContext ctx) {
        if (!hasForeignKey()) {
            return parameterIndex;
        }

        // load the value from the database
        Object[] ref = new Object[1];
        parameterIndex = loadArgumentResults(rs, parameterIndex, ref);

        // only actually set the value if the state is not already loaded
        FieldState fieldState = getFieldState(ctx);
        if (!fieldState.isLoaded()) {
            if (ref[0] != null) {
                load(ctx, Collections.singleton(ref[0]));
            } else {
                load(ctx, Collections.EMPTY_SET);
            }
        }
        return parameterIndex;
    }

    public int loadArgumentResults(ResultSet rs, int parameterIndex, Object[] fkRef) {
        if (foreignKeyFields == null) {
            return parameterIndex;
        }

        boolean fkIsNull = false;

        // value of this field,  will be filled in below
        Object[] argumentRef = new Object[1];
        for (int i = 0; i < foreignKeyFields.length; ++i) {
            JDBCCMPFieldBridge field = foreignKeyFields[i];
            parameterIndex = field.loadArgumentResults(rs, parameterIndex, argumentRef);

            if (fkIsNull) {
                continue;
            }
            if (field.getPrimaryKeyField() != null) {
                // if there is a null field among FK fields, the whole FK field is considered null.
                // NOTE: don't throw exception in this case, it's ok if FK is partly mapped to a PK
                // NOTE2: we still need to iterate through foreign key fields and 'load' them to
                // return correct parameterIndex.
                if (argumentRef[0] == null) {
                    fkRef[0] = null;
                    fkIsNull = true;
                } else {
                    // if we don't have a pk object yet create one
                    if (fkRef[0] == null) {
                        fkRef[0] = relatedEntity.createPrimaryKeyInstance();
                    }
                    try {
                        // Set this field's value into the primary key object.
                        field.getPrimaryKeyField().set(fkRef[0], argumentRef[0]);
                    } catch (Exception e) {
                        // Non recoverable internal exception
                        throw MESSAGES.errorSettingFk(getFieldName(), e);
                    }
                }
            } else {
                // This field is the primary key, so no extraction is necessary.
                fkRef[0] = argumentRef[0];
            }
        }
        return parameterIndex;
    }

    /**
     * This method is never called.
     * In case of a CMR with foreign key fields, only the foreign key fields are asked for the dirty state.
     */
    public boolean isDirty(CmpEntityBeanContext ctx) {
        return foreignKeyFields == null ? relationManager.isDirty() : false;
    }

    public boolean invalidateCache(CmpEntityBeanContext ctx) {
        JDBCContext jdbcCtx = (JDBCContext) ctx.getPersistenceContext();
        FieldState fieldState = (FieldState) jdbcCtx.getFieldState(jdbcContextIndex);
        return fieldState == null ? false : fieldState.isChanged();
    }

    /**
     * This method is never called.
     * In case of a CMR
     * - with foreign key fields, the foreign key fields are cleaned when necessary according to CMP fields'
     * behaviour.
     * - from m:m relationship, added/removed key pairs are cleared in application tx data map on sync.
     */
    public void setClean(CmpEntityBeanContext ctx) {
        throw MESSAGES.setCleanNotSupported();
    }

    public boolean isCMPField() {
        return false;
    }

    public JDBCEntityPersistenceStore getManager() {
        return manager;
    }

    public boolean hasFKFieldsMappedToCMPFields() {
        return hasFKFieldsMappedToCMPFields;
    }

    public void addRelatedPKWaitingForMyPK(Object myPK, Object relatedPK) {
        Map relatedPKsWaitingForMyPK = getRelatedPKsWaitingForMyPK();
        synchronized (relatedPKsWaitingForMyPK) {
            List relatedPKs = (List) relatedPKsWaitingForMyPK.get(myPK);
            if (relatedPKs == null) {
                relatedPKs = new ArrayList(1);
                relatedPKsWaitingForMyPK.put(myPK, relatedPKs);
            }
            relatedPKs.add(relatedPK);
        }
    }

    public void removeRelatedPKWaitingForMyPK(Object myPK, Object relatedPK) {
        final Map relatedPKMap = getRelatedPKsWaitingForMyPK();
        synchronized (relatedPKMap) {
            List relatedPKs = (List) relatedPKMap.get(myPK);
            if (relatedPKs != null) {
                relatedPKs.remove(relatedPK);
            }
        }
    }

    /**
     * Gets the field state object from the persistence context.
     */
    private FieldState getFieldState(CmpEntityBeanContext ctx) {
        JDBCContext jdbcCtx = (JDBCContext) ctx.getPersistenceContext();
        FieldState fieldState = (FieldState) jdbcCtx.getFieldState(jdbcContextIndex);
        if (fieldState == null) {
            fieldState = new FieldState(ctx);
            jdbcCtx.setFieldState(jdbcContextIndex, fieldState);
        }
        return fieldState;
    }

    /**
     * Initializes foreign key fields
     *
     * @
     */
    private void initializeForeignKeyFields() {
        Collection foreignKeys = metadata.getRelatedRole().getKeyFields();

        // temporary map used later to write fk fields in special order
        Map fkFieldsByRelatedPKFields = new HashMap();
        for (Iterator i = foreignKeys.iterator(); i.hasNext(); ) {
            JDBCCMPFieldMetaData fkFieldMetaData = (JDBCCMPFieldMetaData) i.next();
            JDBCCMP2xFieldBridge relatedPKField =
                    (JDBCCMP2xFieldBridge) relatedEntity.getFieldByName(fkFieldMetaData.getFieldName());

            // now determine whether the fk is mapped to a pk column
            String fkColumnName = fkFieldMetaData.getColumnName();
            JDBCCMP2xFieldBridge fkField = null;

            // look among the CMP fields for the field with the same column name
            JDBCFieldBridge[] tableFields = entity.getTableFields();
            for (int tableInd = 0; tableInd < tableFields.length && fkField == null; ++tableInd) {
                JDBCCMP2xFieldBridge cmpField = (JDBCCMP2xFieldBridge) tableFields[tableInd];
                if (fkColumnName.equals(cmpField.getColumnName())) {
                    hasFKFieldsMappedToCMPFields = true;

                    // construct the foreign key field
                    fkField = new JDBCCMP2xFieldBridge((JDBCStoreManager) cmpField.getManager(), // this cmpField's manager
                            relatedPKField.getFieldName(),
                            relatedPKField.getFieldType(),
                            cmpField.getJDBCType(), // this cmpField's jdbc type
                            relatedPKField.isReadOnly(),
                            relatedPKField.getReadTimeOut(),
                            relatedPKField.getPrimaryKeyClass(),
                            relatedPKField.getPrimaryKeyField(),
                            cmpField, // CMP field I am mapped to
                            this,
                            fkColumnName);

                    if (cmpField.isPrimaryKeyMember()) {
                        relatedPKFieldsByMyPKFields.put(cmpField, relatedPKField);
                    }
                }
            }

            // if the fk is not a part of pk then create a new field
            if (fkField == null) {
                fkField = new JDBCCMP2xFieldBridge(manager,
                        fkFieldMetaData,
                        manager.getJDBCTypeFactory().getJDBCType(fkFieldMetaData));
            }

            fkFieldsByRelatedPKFields.put(relatedPKField, fkField); // temporary map
            relatedPKFieldsByMyFKFields.put(fkField, relatedPKField);
        }

        // Note: this important to order the foreign key fields so that their order matches
        // the order of related entity's pk fields in case of complex primary keys.
        // The order is important in fk-constraint generation and in SELECT when loading
        if (fkFieldsByRelatedPKFields.size() > 0) {
            JDBCFieldBridge[] relatedPKFields = relatedEntity.getPrimaryKeyFields();
            List fkList = new ArrayList(relatedPKFields.length);
            for (int i = 0; i < relatedPKFields.length; ++i) {
                JDBCCMPFieldBridge fkField = (JDBCCMPFieldBridge) fkFieldsByRelatedPKFields.remove(relatedPKFields[i]);
                fkList.add(fkField);
            }
            foreignKeyFields = (JDBCCMP2xFieldBridge[]) fkList.toArray(new JDBCCMP2xFieldBridge[fkList.size()]);
        } else {
            foreignKeyFields = null;
        }

        // are all FK fields mapped to PK fields?
        allFKFieldsMappedToPKFields = relatedPKFieldsByMyPKFields.size() > 0
                && relatedPKFieldsByMyPKFields.size() == foreignKeyFields.length;

        if (foreignKeyFields != null) {
            jdbcType = new CMRJDBCType(Arrays.asList(foreignKeyFields));
        }
    }

    private Transaction getTransaction() {
        try {
            CmpEntityBeanComponent component = getJDBCStoreManager().getComponent();
            TransactionManager tm = component.getTransactionManager();
            return tm.getTransaction();
        } catch (SystemException e) {
            throw MESSAGES.errorGettingTxFromManager(e);
        }
    }

    /**
     * @return Map of lists of waiting related PK values keyed by not yet created this side's PK value.
     */
    private Map getRelatedPKsWaitingForMyPK() {
        return (Map) relatedPKValuesWaitingForMyPK.get();
    }

    private RelationDataManager initRelationManager(JDBCCMRFieldBridge relatedField) {
        if (relationManager == null) {
            if (metadata.getRelationMetaData().isTableMappingStyle()) {
                relationManager = new M2MRelationManager(this, relatedField);
            } else {
                relationManager = EMPTY_RELATION_MANAGER;
            }
        }
        return relationManager;
    }

    private Object getRelatedPrimaryKey(Object localObject) {
        Object relatedId;
        if (relatedEntity.getLocalInterface().isAssignableFrom(localObject.getClass())) {
            EJBLocalObject local = (EJBLocalObject) localObject;
            try {
                relatedId = local.getPrimaryKey();
            } catch (NoSuchObjectLocalException e) {
                throw MESSAGES.noSuchRelatedObject(e);
            }
        } else {
            throw MESSAGES.invalidCmrFieldValue(relatedEntity.getLocalInterface().getName());
        }
        return relatedId;
    }

    public String toString() {
        return entity.getEntityName() + '.' + getFieldName();
    }

    private final class FieldState {
        private final CmpEntityBeanContext ctx;
        private List[] setHandle = new List[1];
        private Set addedRelations;
        private Set removedRelations;
        private Set relationSet;
        private boolean isLoaded = false;
        private final long lastRead = -1;

        private boolean changed;

        public FieldState(CmpEntityBeanContext ctx) {
            this.ctx = ctx;
            setHandle[0] = new ArrayList();
        }

        /**
         * Get the current value (list of primary keys).
         */
        public List getValue() {
            if (!isLoaded) {
                throw MESSAGES.cmrFieldNotLoaded();
            }
            return Collections.unmodifiableList(setHandle[0]);
        }

        /**
         * Has this relation been loaded.
         */
        public boolean isLoaded() {
            return isLoaded;
        }

        /**
         * When was this value last read from the datastore.
         */
        public long getLastRead() {
            return lastRead;
        }

        /**
         * Add this foreign to the relationship.
         */
        public void addRelation(Object fk) {
            if (isLoaded) {
                setHandle[0].add(fk);
            } else {
                if (removedRelations == null) {
                    removedRelations = new HashSet();
                    addedRelations = new HashSet();
                }
                removedRelations.remove(fk);
                addedRelations.add(fk);
            }
            changed = true;
        }

        /**
         * Remove this foreign to the relationship.
         */
        public void removeRelation(Object fk) {
            if (isLoaded) {
                setHandle[0].remove(fk);
            } else {
                if (removedRelations == null) {
                    removedRelations = new HashSet();
                    addedRelations = new HashSet();
                }
                addedRelations.remove(fk);
                removedRelations.add(fk);
            }

            changed = true;
        }

        /**
         * loads the collection of related ids
         */
        public void loadRelations(Collection values) {
            // check if we are already loaded
            if (isLoaded) {
                throw MESSAGES.cmrFieldAlreadyLoaded();
            }

            // just in the case where there are lingering values
            setHandle[0].clear();

            // add the new values
            setHandle[0].addAll(values);

            if (removedRelations != null) {
                // remove the already removed values
                setHandle[0].removeAll(removedRelations);
                removedRelations = null;
            }

            if (addedRelations != null) {
                // add the already added values
                // but remove FKs we are going to add to avoid duplication
                setHandle[0].removeAll(addedRelations);
                setHandle[0].addAll(addedRelations);
                addedRelations = null;
            }

            // mark the field loaded
            isLoaded = true;
        }

        /**
         * Get the current relation set or create a new one.
         */
        public Set getRelationSet() {
            if (!isLoaded) {
                throw MESSAGES.cmrFieldNotLoaded();
            }

            if (ctx.isReadOnly()) {
                // we are in a read-only invocation, so return a snapshot set
                return new RelationSet(JDBCCMRFieldBridge.this,
                        ctx,
                        new List[]{new ArrayList(setHandle[0])},
                        true);
            }

            // if we already have a relationSet use it
            if (relationSet != null) {
                return relationSet;
            }

            // construct a new relationSet
            try {
                // get the current transaction
                CmpEntityBeanComponent component = getJDBCStoreManager().getComponent();
                TransactionManager tm = component.getTransactionManager();
                Transaction tx = tm.getTransaction();

                // if whe have a valid transaction...
                if (tx != null && (tx.getStatus() == Status.STATUS_ACTIVE || tx.getStatus() == Status.STATUS_PREPARING)) {
                    // crete the relation set and register for a tx callback
                    relationSet = new RelationSet(JDBCCMRFieldBridge.this, ctx, setHandle, false);
                    TxSynchronization sync = new TxSynchronization(FieldState.this);
                    //tx.registerSynchronization(sync);
                    ctx.getComponent().getTransactionSynchronizationRegistry().registerInterposedSynchronization(sync);
                } else {
                    // if there is no transaction create a pre-failed list
                    relationSet = new RelationSet(JDBCCMRFieldBridge.this, ctx, new List[1], false);
                }

                return relationSet;
            } catch (SystemException e) {
                throw MESSAGES.errorCreatingRelationSet(e);
            } //catch (RollbackException e) {
                //throw MESSAGES.errorCreatingRelationSet(e);
            //}
        }

        /**
         * Invalidate the current relationship set.
         */
        public void invalidate() {
            // make a new set handle and copy the currentList to the new handle
            // this will cause old references to the relationSet to throw an
            // IllegalStateException if accesses, but will not cause a reload
            // in Commit Option A
            List currentList = null;
            if (setHandle != null && setHandle.length > 0) {
                currentList = setHandle[0];
                setHandle[0] = null;
            }
            setHandle = new List[1];
            setHandle[0] = currentList;

            relationSet = null;
            changed = false;
        }

        public boolean isChanged() {
            return changed;
        }
    }

    private static final class CMRJDBCType implements JDBCType {
        private final String[] columnNames;
        private final Class[] javaTypes;
        private final int[] jdbcTypes;
        private final String[] sqlTypes;
        private final boolean[] notNull;

        private CMRJDBCType(List fields) {
            List columnNamesList = new ArrayList();
            List javaTypesList = new ArrayList();
            List jdbcTypesList = new ArrayList();
            List sqlTypesList = new ArrayList();
            List notNullList = new ArrayList();

            for (Iterator iter = fields.iterator(); iter.hasNext(); ) {
                JDBCCMPFieldBridge field = (JDBCCMPFieldBridge) iter.next();
                JDBCType type = field.getJDBCType();
                for (int i = 0; i < type.getColumnNames().length; i++) {
                    columnNamesList.add(type.getColumnNames()[i]);
                    javaTypesList.add(type.getJavaTypes()[i]);
                    jdbcTypesList.add(new Integer(type.getJDBCTypes()[i]));
                    sqlTypesList.add(type.getSQLTypes()[i]);
                    notNullList.add(new Boolean(type.getNotNull()[i]));
                }
            }
            columnNames = (String[]) columnNamesList.toArray(new String[columnNamesList.size()]);
            javaTypes = (Class[]) javaTypesList.toArray(new Class[javaTypesList.size()]);
            sqlTypes = (String[]) sqlTypesList.toArray(new String[sqlTypesList.size()]);

            jdbcTypes = new int[jdbcTypesList.size()];
            for (int i = 0; i < jdbcTypes.length; i++) {
                jdbcTypes[i] = ((Integer) jdbcTypesList.get(i)).intValue();
            }

            notNull = new boolean[notNullList.size()];
            for (int i = 0; i < notNull.length; i++) {
                notNull[i] = ((Boolean) notNullList.get(i)).booleanValue();
            }
        }

        public String[] getColumnNames() {
            return columnNames;
        }

        public Class[] getJavaTypes() {
            return javaTypes;
        }

        public int[] getJDBCTypes() {
            return jdbcTypes;
        }

        public String[] getSQLTypes() {
            return sqlTypes;
        }

        public boolean[] getNotNull() {
            return notNull;
        }

        public boolean[] getAutoIncrement() {
            return new boolean[]{false};
        }

        public Object getColumnValue(int index, Object value) {
            throw MESSAGES.getColumnValueNotSupported();
        }

        public Object setColumnValue(int index, Object value, Object columnValue) {
            throw MESSAGES.setColumnValueNotSupported();
        }

        public boolean hasMapper() {
            throw MESSAGES.hasMapperNotSupported();
        }

        public boolean isSearchable() {
            throw MESSAGES.isSearchableNotSupported();
        }

        public JDBCResultSetReader[] getResultSetReaders() {
            // foreign key fields has their result set readers
            throw MESSAGES.getResultSetReadersNotSupported();
        }

        public JDBCParameterSetter[] getParameterSetter() {
            throw MESSAGES.getParamSetterNotSupported();
        }
    }

    private static final class TxSynchronization implements Synchronization {
        private final WeakReference fieldStateRef;

        private TxSynchronization(FieldState fieldState) {
            if (fieldState == null) {
                throw MESSAGES.fieldStateIsNull();
            }
            this.fieldStateRef = new WeakReference(fieldState);
        }

        public void beforeCompletion() {
            // REVIEW: THIS WILL NOT BE INVOKED ON A ROLLBACK
            // Be Careful where you put this invalidate
            // If you put it in afterCompletion, the beanlock will probably
            // be released before the invalidate and you will have a race
            FieldState fieldState = (FieldState) fieldStateRef.get();
            if (fieldState != null) {
                fieldState.invalidate();
            }
        }

        public void afterCompletion(int status) {
        }
    }

    public interface RelationDataManager {
        void addRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId);

        void removeRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId);

        boolean isDirty();

        RelationData getRelationData();
    }

    private static final RelationDataManager EMPTY_RELATION_MANAGER = new RelationDataManager() {
        public void addRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId) {
        }

        public void removeRelation(JDBCCMRFieldBridge field,
                                   Object id,
                                   JDBCCMRFieldBridge relatedField,
                                   Object relatedId) {
        }

        public boolean isDirty() {
            return false;
        }

        public RelationData getRelationData() {
            throw MESSAGES.getRelationDataNotSupported();
        }
    };

    public static class M2MRelationManager implements RelationDataManager {
        private final JDBCCMRFieldBridge leftField;
        private final JDBCCMRFieldBridge rightField;

        private final TransactionLocal relationData;

        public M2MRelationManager(final JDBCCMRFieldBridge leftField, final JDBCCMRFieldBridge rightField) {
            this.leftField = leftField;
            this.rightField = rightField;

            relationData = new TransactionLocal(leftField.manager.getComponent().getTransactionManager()) {
                protected Object initialValue() {
                    return new RelationData(leftField, rightField);
                }

                public Transaction getTransaction() {
                    try {
                        return transactionManager.getTransaction();
                    } catch (SystemException e) {
                        throw MESSAGES.errorGettingCurrentTransaction(e);
                    }
                }
            };
        }

        public void addRelation(JDBCCMRFieldBridge field,
                                Object id,
                                JDBCCMRFieldBridge relatedField,
                                Object relatedId) {
            final RelationData local = getRelationData();
            local.addRelation(field, id, relatedField, relatedId);
        }

        public void removeRelation(JDBCCMRFieldBridge field,
                                   Object id,
                                   JDBCCMRFieldBridge relatedField,
                                   Object relatedId) {
            RelationData local = getRelationData();
            local.removeRelation(field, id, relatedField, relatedId);
        }

        public boolean isDirty() {
            RelationData local = getRelationData();
            return local.isDirty();
        }

        public RelationData getRelationData() {
            final RelationData local = (RelationData) relationData.get();
            return local;
        }
    }

    /*interface SecurityActions
    {
       class UTIL
       {
          static SecurityActions getSecurityActions()
          {
             return System.getSecurityManager() == null ? NON_PRIVILEGED : PRIVILEGED;
          }
       }

       SecurityActions NON_PRIVILEGED = new SecurityActions()
       {
          public Principal getPrincipal()
          {
             return SecurityAssociation.getPrincipal();
          }

          public Object getCredential()
          {
             return SecurityAssociation.getCredential();
          }
       };

       SecurityActions PRIVILEGED = new SecurityActions()
       {
          private final PrivilegedAction getPrincipalAction = new PrivilegedAction()
          {
             public Object run()
             {
                return SecurityAssociation.getPrincipal();
             }
          };

          private final PrivilegedAction getCredentialAction = new PrivilegedAction()
          {
             public Object run()
             {
                return SecurityAssociation.getCredential();
             }
          };

          public Principal getPrincipal()
          {
             return (Principal) AccessController.doPrivileged(getPrincipalAction);
          }

          public Object getCredential()
          {
             return AccessController.doPrivileged(getCredentialAction);
          }
       };

       Principal getPrincipal();

       Object getCredential();
    }*/
}
