/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, 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.ejb.plugins.cmp.jdbc.bridge;

import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import javax.sql.DataSource;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.Arrays;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.Principal;
import java.rmi.RemoteException;
import javax.ejb.EJBException;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBLocalHome;
import javax.ejb.RemoveException;
import javax.ejb.NoSuchObjectLocalException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.RollbackException;

import org.jboss.deployment.DeploymentException;
import org.jboss.ejb.EntityCache;
import org.jboss.ejb.EntityContainer;
import org.jboss.ejb.EntityEnterpriseContext;
import org.jboss.ejb.LocalProxyFactory;
import org.jboss.ejb.plugins.cmp.bridge.EntityBridge;
import org.jboss.ejb.plugins.cmp.bridge.FieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCContext;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCStoreManager;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCType;
import org.jboss.ejb.plugins.cmp.jdbc.SQLUtil;
import org.jboss.ejb.plugins.cmp.jdbc.CascadeDeleteStrategy;
import org.jboss.ejb.plugins.cmp.jdbc.RelationData;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCEntityPersistenceStore;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCParameterSetter;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCResultSetReader;
import org.jboss.tm.TransactionLocal;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCReadAheadMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCRelationMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCRelationshipRoleMetaData;
import org.jboss.ejb.plugins.cmp.ejbql.Catalog;
import org.jboss.ejb.plugins.lock.Entrancy;
import org.jboss.invocation.InvocationType;
import org.jboss.logging.Logger;
import org.jboss.security.SecurityAssociation;

/**
 * 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: 57209 $
 */
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 acess the relation table if relevant.
    */
   private DataSource dataSource;
   /**
    * The relation table name if relevent.
    */
   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 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 final TransactionLocal relatedPKValuesWaitingForMyPK = new TransactionLocal()
   {
      protected Object initialValue()
      {
         return new HashMap();
      }
   };

   /**
    * 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)
      throws DeploymentException
   {
      this.entity = entity;
      this.manager = manager;
      this.metadata = metadata;
      this.jdbcContextIndex = ((JDBCEntityBridge) manager.getEntityBridge()).getNextJDBCContextIndex();

      //  Creat 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() throws DeploymentException
   {
      //
      // 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 = (Catalog) manager.getApplicationData("CATALOG");
      relatedEntity = (JDBCEntityBridge) catalog.getEntityByEJBName(relatedEntityName);
      if(relatedEntity == null)
      {
         throw new DeploymentException("Related entity not found: " +
            "entity=" +
            entity.getEntityName() +
            ", " +
            "cmrField=" +
            getFieldName() +
            ", " +
            "relatedEntity=" + 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)
      {
         String message = "Related CMR field not found in " +
            relatedEntity.getEntityName() + " for relationship from";

         message += entity.getEntityName() + ".";
         if(getFieldName() != null)
         {
            message += getFieldName();
         }
         else
         {
            message += "<no-field>";
         }

         message += " to ";
         message += relatedEntityName + ".";
         if(metadata.getRelatedRole().getCMRFieldName() != null)
         {
            message += metadata.getRelatedRole().getCMRFieldName();
         }
         else
         {
            message += "<no-field>";
         }

         throw new DeploymentException(message);
      }

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

      // Related Container
      EntityContainer relatedContainer = relatedManager.getContainer();
      this.relatedContainerRef = new WeakReference(relatedContainer);

      // related findByPrimaryKey
      Class homeClass = (relatedContainer.getLocalHomeClass() != null ?
         relatedContainer.getLocalHomeClass() : relatedContainer.getHomeClass());
      try
      {
         relatedFindByPrimaryKey =
            homeClass.getMethod("findByPrimaryKey", new Class[]{relatedEntity.getPrimaryKeyClass()});
      }
      catch(Exception e)
      {
         throw new DeploymentException("findByPrimaryKey(" +
            relatedEntity.getPrimaryKeyClass().getName()
            + " pk) was not found in " + 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 new DeploymentException("Primary key not found for key-field " + 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 new DeploymentException("Primary key " + pkFields[i].getFieldName() + " is not mapped.");
            }
            keyFieldsList.add(fkField);
         }
         tableKeyFields = (JDBCCMP2xFieldBridge[]) keyFieldsList.toArray(
            new JDBCCMP2xFieldBridge[keyFieldsList.size()]);

         dataSource = metadata.getRelationMetaData().getDataSource();
      }
      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.
    *
    * @throws DeploymentException
    */
   public void start() throws DeploymentException
   {
      cascadeDeleteStrategy = CascadeDeleteStrategy.getCascadeDeleteStrategy(this);
   }

   public boolean removeFromRelations(EntityEnterpriseContext 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(EntityEnterpriseContext 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 relevent.
    */
   public String getQualifiedTableName()
   {
      return qualifiedTableName;
   }

   public String getTableName()
   {
      return tableName;
   }

   /**
    * Gets the datasource of the relation table if relevent.
    */
   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
    */
   private final EntityContainer getRelatedContainer()
   {
      return (EntityContainer) relatedContainerRef.get();
   }

   /**
    * The related entity's local home interface.
    */
   public final Class getRelatedLocalInterface()
   {
      return getRelatedContainer().getLocalClass();
   }

   /**
    * The related entity's local container invoker.
    */
   public final LocalProxyFactory getRelatedInvoker()
   {
      return getRelatedContainer().getLocalProxyFactory();
   }

   /**
    * @param ctx - entity's context
    * @return true if entity is loaded, false - otherwise.
    */
   public boolean isLoaded(EntityEnterpriseContext 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(EntityEnterpriseContext ctx)
   {
      final Map relatedPKsMap = getRelatedPKsWaitingForMyPK();
      synchronized(relatedPKsMap)
      {
         List relatedPKsWaitingForMe = (List) relatedPKsMap.get(ctx.getId());
         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(EntityEnterpriseContext 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(EntityEnterpriseContext 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(EntityEnterpriseContext ctx, Object value)
   {
      if(isReadOnly())
      {
         throw new EJBException("Field is read-only: fieldName=" + getFieldName());
      }

      if(!JDBCEntityBridge.isEjbCreateDone(ctx))
      {
         throw new IllegalStateException("A CMR field cannot be set " +
            "in ejbCreate; this should be done in the ejbPostCreate " +
            "method instead [EJB 2.0 Spec. 10.5.2].");
      }

      if(isCollectionValued() && value == null)
      {
         throw new IllegalArgumentException("null cannot be assigned to a " +
            "collection-valued cmr-field [EJB 2.0 Spec. 10.3.8].");
      }
      /*
      if(allFKFieldsMappedToPKFields)
      {
         throw new IllegalStateException(
            "Can't modify relationship: CMR field "
            + entity.getEntityName() + "." + getFieldName()
            + " has foreign key fields mapped to the primary key columns."
            + " Primary key may only be set once in ejbCreate [EJB 2.0 Spec. 10.3.5].");
      }
      */

      setInstanceValue(ctx, value);
   }

   /**
    * Gets the value of the cmr field for the instance associated with
    * the context.
    */
   public Object getInstanceValue(EntityEnterpriseContext 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 new EJBException(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 EntityContainer relatedContainer = getRelatedContainer();

      if(hasFKFieldsMappedToCMPFields
         && relatedManager.getReadAheadCache().getPreloadDataMap(fk, false) == null // not in preload cache
      )
      {
         EJBLocalHome relatedHome = relatedContainer.getLocalProxyFactory().getEJBLocalHome();
         try
         {
            relatedLocalObject = (EJBLocalObject) relatedFindByPrimaryKey.invoke(relatedHome, new Object[]{fk});
         }
         catch(Exception ignore)
         {
            // no such entity. it is ok to ignore
         }
      }
      else
      {
         relatedLocalObject = relatedContainer.getLocalProxyFactory().getEntityEJBLocalObject(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
      {
         EJBLocalHome relatedHome = getRelatedContainer().getLocalProxyFactory().getEJBLocalHome();
         try
         {
            relatedFindByPrimaryKey.invoke(relatedHome, new Object[]{fk});
            valid = true;
         }
         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(EntityEnterpriseContext 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 = getRelatedPrimaryKey(localObject);

                  // 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)
         {
            newPks = Collections.singletonList(getRelatedPrimaryKey(newValue));
         }
         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 EJBException(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(EntityEnterpriseContext 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 new IllegalStateException("Can't create relationship: CMR field "
                  +
                  entity.getEntityName() +
                  "." +
                  getFieldName()
                  +
                  " has foreign key fields mapped to the primary key columns."
                  +
                  " Primary key may only be set once in ejbCreate [EJB 2.0 Spec. 10.3.5]."
                  +
                  " primary key value is " +
                  currentValue
                  + " overriding value is " + 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(EntityEnterpriseContext myCtx, Object relatedId)
   {
      createRelationLinks(myCtx, relatedId, true);
   }

   public void createRelationLinks(EntityEnterpriseContext myCtx, Object relatedId, boolean updateForeignKey)
   {
      if(isReadOnly())
      {
         throw new EJBException("Field is read-only: " + getFieldName());
      }

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

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

   /**
    * 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(EntityEnterpriseContext 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(EntityEnterpriseContext myCtx,
                                    Object relatedId,
                                    boolean updateValueCollection)
   {
      destroyRelationLinks(myCtx, relatedId, updateValueCollection, true);
   }

   public void destroyRelationLinks(EntityEnterpriseContext myCtx,
                                    Object relatedId,
                                    boolean updateValueCollection,
                                    boolean updateForeignKey)
   {
      if(isReadOnly())
      {
         throw new EJBException("Field is read-only: " + getFieldName());
      }

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

   /**
    * Schedules children for cascade delete.
    */
   public void scheduleChildrenForCascadeDelete(EntityEnterpriseContext 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(tx, value.get(i));
         }
      }
   }

   /**
    * Schedules children for batch cascade delete.
    */
   public void scheduleChildrenForBatchCascadeDelete(EntityEnterpriseContext 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(tx, value.get(i));
         }
      }
   }

   /**
    * Schedules the instance with myId for cascade delete.
    */
   private Object invokeScheduleForCascadeDelete(Transaction tx, Object myId)
   {
      try
      {
         EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache();
         SecurityActions actions = SecurityActions.UTIL.getSecurityActions();

         CMRInvocation invocation = new CMRInvocation();
         invocation.setCmrMessage(CMRMessage.SCHEDULE_FOR_CASCADE_DELETE);
         invocation.setEntrancy(Entrancy.NON_ENTRANT);
         invocation.setId(instanceCache.createCacheKey(myId));
         invocation.setArguments(new Object[]{this});
         invocation.setTransaction(tx);
         invocation.setPrincipal(actions.getPrincipal());
         invocation.setCredential(actions.getCredential());
         invocation.setType(InvocationType.LOCAL);
         return manager.getContainer().invoke(invocation);
      }
      catch(EJBException e)
      {
         throw e;
      }
      catch(Exception e)
      {
         throw new EJBException("Error in scheduleForCascadeDelete()", e);
      }
   }

   /**
    * Schedules the instance with myId for batch cascade delete.
    */
   private Object invokeScheduleForBatchCascadeDelete(Transaction tx, Object myId)
   {
      try
      {
         EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache();
         SecurityActions actions = SecurityActions.UTIL.getSecurityActions();

         CMRInvocation invocation = new CMRInvocation();
         invocation.setCmrMessage(CMRMessage.SCHEDULE_FOR_BATCH_CASCADE_DELETE);
         invocation.setEntrancy(Entrancy.NON_ENTRANT);
         invocation.setId(instanceCache.createCacheKey(myId));
         invocation.setArguments(new Object[]{this});
         invocation.setTransaction(tx);
         invocation.setPrincipal(actions.getPrincipal());
         invocation.setCredential(actions.getCredential());
         invocation.setType(InvocationType.LOCAL);
         return manager.getContainer().invoke(invocation);
      }
      catch(EJBException e)
      {
         throw e;
      }
      catch(Exception e)
      {
         throw new EJBException("Error in scheduleForBatchCascadeDelete()", e);
      }
   }

   /**
    * Invokes the getRelatedId on the related CMR field via the container
    * invocation interceptor chain.
    */
   private Object invokeGetRelatedId(Transaction tx, Object myId)
   {
      try
      {
         EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache();
         SecurityActions actions = SecurityActions.UTIL.getSecurityActions();

         CMRInvocation invocation = new CMRInvocation();
         invocation.setCmrMessage(CMRMessage.GET_RELATED_ID);
         invocation.setEntrancy(Entrancy.NON_ENTRANT);
         invocation.setId(instanceCache.createCacheKey(myId));
         invocation.setArguments(new Object[]{this});
         invocation.setTransaction(tx);
         invocation.setPrincipal(actions.getPrincipal());
         invocation.setCredential(actions.getCredential());
         invocation.setType(InvocationType.LOCAL);
         return manager.getContainer().invoke(invocation);
      }
      catch(EJBException e)
      {
         throw e;
      }
      catch(Exception e)
      {
         throw new EJBException("Error in getRelatedId", e);
      }
   }

   /**
    * Invokes the addRelation on the related CMR field via the container
    * invocation interceptor chain.
    */
   private void invokeAddRelation(Transaction tx, Object myId, Object relatedId)
   {
      try
      {
         EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache();
         SecurityActions actions = SecurityActions.UTIL.getSecurityActions();

         CMRInvocation invocation = new CMRInvocation();
         invocation.setCmrMessage(CMRMessage.ADD_RELATION);
         invocation.setEntrancy(Entrancy.NON_ENTRANT);
         invocation.setId(instanceCache.createCacheKey(myId));
         invocation.setArguments(new Object[]{this, relatedId});
         invocation.setTransaction(tx);
         invocation.setPrincipal(actions.getPrincipal());
         invocation.setCredential(actions.getCredential());
         invocation.setType(InvocationType.LOCAL);
         manager.getContainer().invoke(invocation);
      }
      catch(EJBException e)
      {
         throw e;
      }
      catch(Exception e)
      {
         throw new EJBException("Error in addRelation", e);
      }
   }

   /**
    * Invokes the removeRelation on the related CMR field via the container
    * invocation interceptor chain.
    */
   private void invokeRemoveRelation(Transaction tx, Object myId, Object relatedId)
   {
      try
      {
         EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache();
         SecurityActions actions = SecurityActions.UTIL.getSecurityActions();

         CMRInvocation invocation = new CMRInvocation();
         invocation.setCmrMessage(CMRMessage.REMOVE_RELATION);
         invocation.setEntrancy(Entrancy.NON_ENTRANT);
         invocation.setId(instanceCache.createCacheKey(myId));
         invocation.setArguments(new Object[]{this, relatedId});
         invocation.setTransaction(tx);
         invocation.setPrincipal(actions.getPrincipal());
         invocation.setCredential(actions.getCredential());
         invocation.setType(InvocationType.LOCAL);
         manager.getContainer().invoke(invocation);
      }
      catch(EJBException e)
      {
         throw e;
      }
      catch(Exception e)
      {
         throw new EJBException("Error in removeRelation", e);
      }
   }

   /**
    * Get the related entity's id.  This only works on single valued cmr fields.
    */
   public Object getRelatedId(EntityEnterpriseContext myCtx)
   {
      if(isCollectionValued())
      {
         throw new EJBException("getRelatedId may only be called on a cmr-field with a multiplicity of one.");
      }

      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(EntityEnterpriseContext ctx)
   {
      Object relatedId = null;
      Object fkFieldValue;
      for(int i = 0; i < foreignKeyFields.length; ++i)
      {
         JDBCCMP2xFieldBridge fkField = foreignKeyFields[i];
         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(EntityEnterpriseContext myCtx, Object fk)
   {
      addRelation(myCtx, fk, true);
      relationManager.addRelation(this, myCtx.getId(), relatedCMRField, fk);
   }

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

      if(isReadOnly())
      {
         throw new EJBException("Field is read-only: " + getFieldName());
      }

      if(!JDBCEntityBridge.isEjbCreateDone(myCtx))
      {
         throw new IllegalStateException("A CMR field cannot be set or added " +
            "to a relationship in ejbCreate; this should be done in the " +
            "ejbPostCreate method instead [EJB 2.0 Spec. 10.5.2].");
      }

      // 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(EntityEnterpriseContext myCtx, Object fk)
   {
      removeRelation(myCtx, fk, true, true);
      relationManager.removeRelation(this, myCtx.getId(), relatedCMRField, fk);
   }

   private void removeRelation(EntityEnterpriseContext myCtx,
                               Object fk,
                               boolean updateValueCollection,
                               boolean updateForeignKey)
   {
      if(isReadOnly())
      {
         throw new EJBException("Field is read-only: " + 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(EntityEnterpriseContext 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 cahce load: cmrField=" + getFieldName() + " pk=" + myCtx.getId());
      }

      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.getId());
         }
         else
         {
            values = (fk == null ? Collections.EMPTY_LIST : Collections.singletonList(fk));
         }
      }
      else
      {
         values = manager.loadRelation(this, myCtx.getId());
      }
      load(myCtx, values);
   }

   public void load(EntityEnterpriseContext myCtx, Collection values)
   {
      // did we get more then one value for a single valued field
      if(isSingleValued() && values.size() > 1)
      {
         throw new EJBException("Data contains multiple values, but this cmr field is single valued: " + 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(EntityEnterpriseContext myCtx, Object fk)
   {
      if(!hasForeignKey())
      {
         throw new EJBException(getFieldName() + " CMR field does not have a foreign key to set.");
      }

      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(EntityEnterpriseContext 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(EntityEnterpriseContext 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,
                                    EntityEnterpriseContext 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,
                                  EntityEnterpriseContext 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 new EJBException("Internal error setting foreign-key field " + 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(EntityEnterpriseContext ctx)
   {
      return foreignKeyFields == null ? relationManager.isDirty() : false;
   }

   public boolean invalidateCache(EntityEnterpriseContext 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(EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   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(EntityEnterpriseContext 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
    *
    * @throws DeploymentException
    */
   private void initializeForeignKeyFields()
      throws DeploymentException
   {
      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
      {
         EntityContainer container = getJDBCStoreManager().getContainer();
         TransactionManager tm = container.getTransactionManager();
         return tm.getTransaction();
      }
      catch(SystemException e)
      {
         throw new EJBException("Error getting transaction from the transaction manager", 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 new IllegalArgumentException(e.getMessage());
         }

         /*
         if(relatedManager.wasCascadeDeleted(relatedId))
         {
            throw new IllegalArgumentException("The instance was cascade-deleted: pk=" + relatedId);
         }
         */
      }
      else
      {
         throw new IllegalArgumentException("The values of this field must be of type " +
            relatedEntity.getLocalInterface().getName());
      }
      return relatedId;
   }

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

   private final class FieldState
   {
      private final EntityEnterpriseContext 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(EntityEnterpriseContext ctx)
      {
         this.ctx = ctx;
         setHandle[0] = new ArrayList();
      }

      /**
       * Get the current value (list of primary keys).
       */
      public List getValue()
      {
         if(!isLoaded)
         {
            throw new EJBException("CMR field value not loaded yet");
         }
         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 aleready loaded
         if(isLoaded)
         {
            throw new EJBException("CMR field value is already loaded");
         }

         // 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 new EJBException("CMR field value not loaded yet");
         }

         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 relationshet
         try
         {
            // get the curent transaction
            EntityContainer container = getJDBCStoreManager().getContainer();
            TransactionManager tm = container.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);
            }
            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 new EJBException("Error while creating RelationSet", e);
         }
         catch(RollbackException e)
         {
            throw new EJBException("Error while creating RelationSet", 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 final static 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 new UnsupportedOperationException();
      }

      public Object setColumnValue(int index, Object value, Object columnValue)
      {
         throw new UnsupportedOperationException();
      }

      public boolean hasMapper()
      {
         throw new UnsupportedOperationException("hasMapper is not implemented.");
      }

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

      public JDBCParameterSetter[] getParameterSetter()
      {
         throw new UnsupportedOperationException();
      }
   }

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

      private TxSynchronization(FieldState fieldState)
      {
         if(fieldState == null)
         {
            throw new IllegalArgumentException("fieldState is null");
         }
         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 static 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 new UnsupportedOperationException();
      }
   };

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

      private final TransactionLocal relationData = new TransactionLocal()
      {
         protected Object initialValue()
         {
            return new RelationData(leftField, rightField);
         }
      };

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

      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();
   }
}
