/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt 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.jdbc2.bridge;

import org.jboss.ejb.plugins.cmp.bridge.EntityBridge;
import org.jboss.ejb.plugins.cmp.bridge.FieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc2.JDBCStoreManager2;
import org.jboss.ejb.plugins.cmp.jdbc2.schema.EntityTable;
import org.jboss.ejb.plugins.cmp.jdbc2.schema.RelationTable;
import org.jboss.ejb.plugins.cmp.jdbc2.schema.Cache;
import org.jboss.ejb.plugins.cmp.jdbc2.PersistentContext;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCRelationshipRoleMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCUtil;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCType;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCEntityPersistenceStore;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCAbstractCMRFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCAbstractEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.CMRInvocation;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.CMRMessage;
import org.jboss.ejb.plugins.cmp.ejbql.Catalog;
import org.jboss.ejb.plugins.lock.Entrancy;
import org.jboss.ejb.EntityEnterpriseContext;
import org.jboss.ejb.EntityContainer;
import org.jboss.ejb.EntityCache;
import org.jboss.ejb.LocalProxyFactory;
import org.jboss.deployment.DeploymentException;
import org.jboss.logging.Logger;
import org.jboss.security.SecurityAssociation;
import org.jboss.invocation.InvocationType;

import javax.ejb.EJBException;
import javax.ejb.EJBLocalObject;
import javax.ejb.RemoveException;
import javax.ejb.NoSuchObjectLocalException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.SystemException;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.Collections;
import java.util.HashSet;
import java.util.ConcurrentModificationException;
import java.sql.PreparedStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.lang.reflect.Array;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.Principal;


/**
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision$</tt>
 */
public class JDBCCMRFieldBridge2
   extends JDBCAbstractCMRFieldBridge
{
   private final JDBCRelationshipRoleMetaData metadata;
   private final JDBCStoreManager2 manager;
   private final JDBCEntityBridge2 entity;
   private final int cmrIndex;
   private final Logger log;

   private JDBCEntityBridge2 relatedEntity;
   private JDBCCMRFieldBridge2 relatedCMRField;
   private EntityContainer relatedContainer;

   private JDBCCMPFieldBridge2[] tableKeyFields;
   private JDBCCMPFieldBridge2[] foreignKeyFields;
   private JDBCCMPFieldBridge2[] relatedPKFields;

   private CMRFieldLoader loader;
   private RelationTable relationTable;

   private EntityTable.ForeignKeyConstraint fkConstraint;

   private final TransactionManager tm;

   public JDBCCMRFieldBridge2(JDBCEntityBridge2 entityBridge,
                              JDBCStoreManager2 manager,
                              JDBCRelationshipRoleMetaData metadata)
   {
      this.manager = manager;
      this.entity = entityBridge;
      this.metadata = metadata;
      cmrIndex = entity.getNextCMRIndex();
      tm = manager.getContainer().getTransactionManager();

      log = Logger.getLogger(getClass().getName() + "." + entity.getEntityName() + "#" + getFieldName());
   }

   // Public

   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 = (JDBCEntityBridge2)catalog.getEntityByEJBName(relatedEntityName);
      if(relatedEntity == null)
      {
         throw new DeploymentException("Related entity not found: "
            +
            "entity="
            +
            entity.getEntityName()
            +
            ", "
            +
            "cmrField="
            +
            getFieldName()
            +
            ", " +
            "relatedEntity=" + relatedEntityName
         );
      }

      // Related CMR Field
      JDBCCMRFieldBridge2[] cmrFields = (JDBCCMRFieldBridge2[])relatedEntity.getCMRFields();
      for(int i = 0; i < cmrFields.length; ++i)
      {
         JDBCCMRFieldBridge2 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 Container
      relatedContainer = relatedEntity.getContainer();

      //
      // 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 JDBCCMPFieldBridge2(manager, entity, cmpFieldMetaData, -1));
         }

         // 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 = (JDBCCMPFieldBridge2[])keyFieldsList.toArray(new JDBCCMPFieldBridge2[keyFieldsList.size()]);
      }
      else
      {
         initializeForeignKeyFields();
      }
   }

   public void initLoader() throws DeploymentException
   {
      if(metadata.getRelationMetaData().isTableMappingStyle())
      {
         relationTable = relatedCMRField.getRelationTable();
         loader = new RelationTableLoader();
      }
      else
      {
         if(foreignKeyFields != null)
         {
            loader = new ContextForeignKeyLoader();
         }
         else
         {
            loader = new ForeignKeyLoader();
         }
      }
   }

   public JDBCRelationshipRoleMetaData getMetaData()
   {
      return metadata;
   }

   public boolean removeRelatedId(EntityEnterpriseContext ctx, Object relatedId)
   {
      FieldState state = getFieldState(ctx);
      return state.removeRelatedId(ctx, relatedId);
   }

   public boolean addRelatedId(EntityEnterpriseContext ctx, Object relatedId)
   {
      FieldState state = getFieldState(ctx);
      return state.addRelatedId(ctx, relatedId);
   }

   public void remove(EntityEnterpriseContext ctx) throws RemoveException
   {
      if(metadata.getRelatedRole().isCascadeDelete())
      {
         FieldState state = getFieldState(ctx);
         state.cascadeDelete(ctx);
      }
      else
      {
         destroyExistingRelationships(ctx);
      }
   }

   public void destroyExistingRelationships(EntityEnterpriseContext ctx)
   {
      FieldState state = getFieldState(ctx);
      state.destroyExistingRelationships(ctx);
   }

   public JDBCFieldBridge[] getTableKeyFields()
   {
      return tableKeyFields;
   }

   public JDBCEntityPersistenceStore getManager()
   {
      return manager;
   }

   public boolean hasForeignKey()
   {
      return foreignKeyFields != null;
   }

   public JDBCAbstractCMRFieldBridge getRelatedCMRField()
   {
      return this.relatedCMRField;
   }

   public JDBCFieldBridge[] getForeignKeyFields()
   {
      return foreignKeyFields;
   }

   public JDBCCMRFieldBridge2 getRelatedField()
   {
      return relatedCMRField;
   }

   public JDBCAbstractEntityBridge getEntity()
   {
      return entity;
   }

   public String getQualifiedTableName()
   {
      return relationTable.getTableName();
   }

   public String getTableName()
   {
      throw new UnsupportedOperationException();
   }

   // JDBCFieldBridge implementation

   public JDBCType getJDBCType()
   {
      throw new UnsupportedOperationException();
   }

   public boolean isPrimaryKeyMember()
   {
      throw new UnsupportedOperationException();
   }

   public boolean isReadOnly()
   {
      throw new UnsupportedOperationException();
   }

   public boolean isReadTimedOut(EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   public boolean isLoaded(EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   public void initInstance(EntityEnterpriseContext ctx)
   {
      getFieldState(ctx).init();
   }

   public void resetPersistenceContext(EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   public int setInstanceParameters(PreparedStatement ps, int parameterIndex, EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   public Object getInstanceValue(EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   public void setInstanceValue(EntityEnterpriseContext ctx, Object value)
   {
      throw new UnsupportedOperationException();
   }

   public int loadInstanceResults(ResultSet rs, int parameterIndex, EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   public int loadArgumentResults(ResultSet rs, int parameterIndex, Object[] argumentRef)
   {
      throw new UnsupportedOperationException();
   }

   public boolean isDirty(EntityEnterpriseContext ctx)
   {
      return getFieldState(ctx).isModified();
   }

   public void setClean(EntityEnterpriseContext ctx)
   {
      throw new UnsupportedOperationException();
   }

   public boolean isCMPField()
   {
      return false;
   }

   // CMRFieldBridge implementation

   public String getFieldName()
   {
      return metadata.getCMRFieldName();
   }

   public Object getValue(EntityEnterpriseContext ctx)
   {
      FieldState state = getFieldState(ctx);
      return state.getValue(ctx);
   }

   public void setValue(EntityEnterpriseContext ctx, Object value)
   {
      FieldState state = getFieldState(ctx);
      state.setValue(ctx, value);
      state.cacheValue(ctx);
   }

   public boolean isSingleValued()
   {
      return metadata.getRelatedRole().isMultiplicityOne();
   }

   public EntityBridge getRelatedEntity()
   {
      return relatedEntity;
   }

   // Private

   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();
         JDBCCMPFieldBridge2 relatedPKField =
            (JDBCCMPFieldBridge2)relatedEntity.getFieldByName(fkFieldMetaData.getFieldName());

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

         // look among the CMP fields for the field with the same column name
         JDBCCMPFieldBridge2[] tableFields = (JDBCCMPFieldBridge2[])entity.getTableFields();
         for(int tableInd = 0; tableInd < tableFields.length && fkField == null; ++tableInd)
         {
            JDBCCMPFieldBridge2 cmpField = tableFields[tableInd];
            if(fkColumnName.equals(cmpField.getColumnName()))
            {
               // construct the foreign key field
               fkField = new JDBCCMPFieldBridge2(cmpField, relatedPKField);
               /*
                  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 the fk is not a part of pk then create a new field
         if(fkField == null)
         {
            fkField = entity.addTableField(fkFieldMetaData);
         }
         fkFieldsByRelatedPKFields.put(relatedPKField, fkField); // temporary map
      }

      // 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[] pkFields = relatedEntity.getPrimaryKeyFields();
         List fkList = new ArrayList(pkFields.length);
         List relatedPKList = new ArrayList(pkFields.length);
         for(int i = 0; i < pkFields.length; ++i)
         {
            JDBCFieldBridge relatedPKField = pkFields[i];
            JDBCFieldBridge fkField = (JDBCCMPFieldBridge2)fkFieldsByRelatedPKFields.remove(relatedPKField);
            fkList.add(fkField);
            relatedPKList.add(relatedPKField);
         }
         foreignKeyFields = (JDBCCMPFieldBridge2[])fkList.toArray(new JDBCCMPFieldBridge2[fkList.size()]);
         relatedPKFields =
            (JDBCCMPFieldBridge2[])relatedPKList.toArray(new JDBCCMPFieldBridge2[relatedPKList.size()]);

         if(metadata.hasForeignKeyConstraint())
         {
            fkConstraint = entity.getTable().addFkConstraint(foreignKeyFields, relatedEntity.getTable());
         }
      }
      else
      {
         foreignKeyFields = null;
         relatedPKFields = null;
      }
   }

   private FieldState getFieldState(EntityEnterpriseContext ctx)
   {
      PersistentContext pctx = (PersistentContext)ctx.getPersistenceContext();
      FieldState state = pctx.getCMRState(cmrIndex);
      if(state == null)
      {
         if(isSingleValued())
         {
            state = new SingleValuedFieldState();
         }
         else
         {
            state = new CollectionValuedFieldState();
         }
         pctx.setCMRState(cmrIndex, state);
      }
      return state;
   }

   private void invokeRemoveRelatedId(Object myId, Object relatedId)
   {
      try
      {
         Transaction tx = getTransaction();
         EntityCache instanceCache = (EntityCache)manager.getContainer().getInstanceCache();

         /*
         RelationInterceptor.RelationInvocation invocation =
            new RelationInterceptor.RelationInvocation(RelationInterceptor.CMRMessage.REMOVE_RELATED_ID);
         invocation.setId(instanceCache.createCacheKey(myId));
         invocation.setArguments(new Object[]{this, relatedId});
         invocation.setTransaction(tx);
         invocation.setPrincipal(SecurityAssociation.getPrincipal());
         invocation.setCredential(SecurityAssociation.getCredential());
         invocation.setType(InvocationType.LOCAL);
         */

         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 invokeRemoveRelatedId()", e);
      }
   }

   private void invokeAddRelatedId(Object myId, Object relatedId)
   {
      try
      {
         Transaction tx = getTransaction();
         EntityCache instanceCache = (EntityCache)manager.getContainer().getInstanceCache();
         /*
         RelationInterceptor.RelationInvocation invocation =
            new RelationInterceptor.RelationInvocation(RelationInterceptor.CMRMessage.ADD_RELATED_ID);
         invocation.setId(instanceCache.createCacheKey(myId));
         invocation.setArguments(new Object[]{this, relatedId});
         invocation.setTransaction(tx);
         invocation.setPrincipal(SecurityAssociation.getPrincipal());
         invocation.setCredential(SecurityAssociation.getCredential());
         invocation.setType(InvocationType.LOCAL);
         */
         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 invokeAddRelatedId()", e);
      }
   }

   private Transaction getTransaction() throws SystemException
   {
      return tm.getTransaction();
   }

   private RelationTable getRelationTable() throws DeploymentException
   {
      if(relationTable == null)
      {
         relationTable = manager.getSchema().createRelationTable(this, relatedCMRField);
      }
      return relationTable;
   }

   private Object getPrimaryKey(Object o)
   {
      if(o == null)
      {
         throw new IllegalArgumentException("This implementation does not support null members.");
      }

      if(!relatedEntity.getLocalInterface().isInstance(o))
      {
         throw new IllegalArgumentException("Argument must be of type " + entity.getLocalInterface().getName());
      }

      EJBLocalObject local = (EJBLocalObject)o;
      try
      {
         return local.getPrimaryKey();
      }
      catch(NoSuchObjectLocalException e)
      {
         throw new IllegalArgumentException(e.getMessage());
      }
   }

   // Inner

   public class SingleValuedFieldState
      implements FieldState
   {
      private boolean loaded;
      private Object value;
      private EJBLocalObject localObject;
      private boolean modified;

      public void init()
      {
         loaded = true;
      }

      public Object getValue(EntityEnterpriseContext ctx)
      {
         Object value = getLoadedValue(ctx);
         if(value == null)
         {
            localObject = null;
         }
         else if(localObject == null)
         {
            localObject = relatedContainer.getLocalProxyFactory().getEntityEJBLocalObject(value);
         }
         return localObject;
      }

      public void setValue(EntityEnterpriseContext ctx, Object value)
      {
         if(value != null)
         {
            Object relatedId = getPrimaryKey(value);
            addRelatedId(ctx, relatedId);
            relatedCMRField.invokeAddRelatedId(relatedId, ctx.getId());
            localObject = (EJBLocalObject)value;
         }
         else
         {
            destroyExistingRelationships(ctx);
         }
      }

      public void cascadeDelete(EntityEnterpriseContext ctx) throws RemoveException
      {
         if(manager.registerCascadeDelete(ctx.getId(), ctx.getId()))
         {
            EJBLocalObject value = (EJBLocalObject)getValue(ctx);
            if(value != null)
            {
               changeValue(null);

               final Object relatedId = value.getPrimaryKey();
               final JDBCStoreManager2 relatedManager = (JDBCStoreManager2)relatedEntity.getManager();

               if(!relatedManager.isCascadeDeleted(relatedId))
               {
                  value.remove();
               }
            }

            manager.unregisterCascadeDelete(ctx.getId());
         }
      }

      public void destroyExistingRelationships(EntityEnterpriseContext ctx)
      {
         Object value = getLoadedValue(ctx);
         if(value != null)
         {
            removeRelatedId(ctx, value);
            relatedCMRField.invokeRemoveRelatedId(value, ctx.getId());
         }
      }

      public boolean removeRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         if(hasForeignKey())
         {
            getLoadedValue(ctx);
         }

         changeValue(null);
         loader.removeRelatedId(ctx, relatedId);

         cacheValue(ctx);

         modified = true;

         return true;
      }

      public boolean addRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         Object value = getLoadedValue(ctx);
         if(value != null)
         {
            relatedCMRField.invokeRemoveRelatedId(value, ctx.getId());
         }

         changeValue(relatedId);
         loader.addRelatedId(ctx, relatedId);

         cacheValue(ctx);

         modified = true;

         return true;
      }

      public void addLoadedPk(Object pk)
      {
         if(loaded)
         {
            throw new IllegalStateException(entity.getEntityName()
               +
               "."
               +
               getFieldName()
               +
               " single-valued CMR field is already loaded. Check the database for consistancy. "
               + " current value=" + value + ", loaded value=" + pk
            );
         }

         changeValue(pk);
      }

      public Object loadFromCache(Object value)
      {
         if(value != null)
         {
            changeValue(NULL_VALUE == value ? null : value);
         }
         return value;
      }

      public Object getCachedValue()
      {
         return value == null ? NULL_VALUE : value;
      }

      public void cacheValue(EntityEnterpriseContext ctx)
      {
         PersistentContext pctx = (PersistentContext)ctx.getPersistenceContext();
         pctx.cacheRelations(cmrIndex, this);
      }

      public boolean isModified()
      {
         return modified;
      }

      // Private

      private void changeValue(Object newValue)
      {
         this.value = newValue;
         this.localObject = null;
         loaded = true;
      }

      private Object getLoadedValue(EntityEnterpriseContext ctx)
      {
         if(!loaded)
         {
            PersistentContext pctx = (PersistentContext)ctx.getPersistenceContext();
            pctx.loadCachedRelations(cmrIndex, this);
            if(!loaded)
            {
               loader.load(ctx, this);
               loaded = true;
               cacheValue(ctx);
            }
         }
         return value;
      }
   }

   public class CollectionValuedFieldState
      implements FieldState
   {
      private boolean loaded;
      private Set value;
      private CMRSet cmrSet;

      private Set removedWhileNotLoaded;
      private Set addedWhileNotLoaded;

      private boolean modified;

      public void init()
      {
         loaded = true;
         value = new HashSet();
      }

      public Object getValue(EntityEnterpriseContext ctx)
      {
         if(cmrSet == null)
         {
            cmrSet = new CMRSet(ctx, this);
         }
         return cmrSet;
      }

      public void setValue(EntityEnterpriseContext ctx, Object value)
      {
         if(value == null)
         {
            throw new IllegalArgumentException("Can't set collection-valued CMR field to null: " +
               entity.getEntityName() + "." + getFieldName()
            );
         }

         destroyExistingRelationships(ctx);

         Collection newValue = (Collection)value;
         if(!newValue.isEmpty())
         {
            Set copy = new HashSet(newValue);
            for(Iterator iter = copy.iterator(); iter.hasNext();)
            {
               Object relatedId = getPrimaryKey(iter.next());
               addRelatedId(ctx, relatedId);
               relatedCMRField.invokeAddRelatedId(relatedId, ctx.getId());
               loader.addRelatedId(ctx, relatedId);
            }
         }
      }

      public void cascadeDelete(EntityEnterpriseContext ctx) throws RemoveException
      {
         Collection value = (Collection)getValue(ctx);
         if(!value.isEmpty())
         {
            EJBLocalObject[] locals = (EJBLocalObject[])value.toArray();
            for(int i = 0; i < locals.length; ++i)
            {
               locals[i].remove();
            }
         }
      }

      public void destroyExistingRelationships(EntityEnterpriseContext ctx)
      {
         Set value = getLoadedValue(ctx);
         if(!value.isEmpty())
         {
            Object[] copy = value.toArray();
            for(int i = 0; i < copy.length; ++i)
            {
               Object relatedId = copy[i];
               removeRelatedId(ctx, relatedId);
               relatedCMRField.invokeRemoveRelatedId(relatedId, ctx.getId());
               loader.removeRelatedId(ctx, relatedId);
            }
         }
      }

      public boolean removeRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         boolean removed = false;
         if(loaded)
         {
            Set value = getLoadedValue(ctx);
            if(!value.isEmpty())
            {
               removed = value.remove(relatedId);
            }
         }
         else
         {
            loadOnlyFromCache(ctx);
            if(loaded)
            {
               Set value = getLoadedValue(ctx);
               if(!value.isEmpty())
               {
                  removed = value.remove(relatedId);
               }
            }
            else
            {
               removed = removeWhileNotLoaded(relatedId);
            }
         }

         modified = true;

         if(removed)
         {
            ((PersistentContext)ctx.getPersistenceContext()).setDirtyRelations();
         }

         return removed;
      }

      public boolean addRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         boolean added;
         if(loaded)
         {
            Set value = getLoadedValue(ctx);
            added = value.add(relatedId);
         }
         else
         {
            loadOnlyFromCache(ctx);
            if(loaded)
            {
               Set value = getLoadedValue(ctx);
               added = value.add(relatedId);
            }
            else
            {
               added = addWhileNotLoaded(relatedId);
            }
         }

         modified = true;

         if(added)
         {
            ((PersistentContext)ctx.getPersistenceContext()).setDirtyRelations();
         }

         return added;
      }

      public void addLoadedPk(Object pk)
      {
         if(loaded)
         {
            throw new IllegalStateException(entity.getEntityName()
               +
               "."
               +
               getFieldName()
               +
               " collection-valued CMR field is already loaded. Check the database for consistancy. "
               + " current value=" + value + ", loaded value=" + pk
            );
         }

         if(pk != null)
         {
            value.add(pk);
         }
      }

      public Object loadFromCache(Object value)
      {
         if(value != null)
         {
            value = this.value = new HashSet((Set)value);
            loaded = true;
         }
         return value;
      }

      public Object getCachedValue()
      {
         return value;
      }

      public void cacheValue(EntityEnterpriseContext ctx)
      {
         PersistentContext pctx = (PersistentContext)ctx.getPersistenceContext();
         pctx.cacheRelations(cmrIndex, this);
      }

      public boolean isModified()
      {
         return modified;
      }

      // Private

      private Set getLoadedValue(EntityEnterpriseContext ctx)
      {
         if(!loaded)
         {
            loadOnlyFromCache(ctx);

            if(!loaded)
            {
               if(value == null || value == Collections.EMPTY_SET)
               {
                  value = new HashSet();
               }

               loader.load(ctx, this);
               cacheValue(ctx);

               loaded = true;
            }

            if(addedWhileNotLoaded != null)
            {
               value.addAll(addedWhileNotLoaded);
               addedWhileNotLoaded = null;
            }

            if(removedWhileNotLoaded != null)
            {
               value.removeAll(removedWhileNotLoaded);
               removedWhileNotLoaded = null;
            }
         }
         return value;
      }

      private void loadOnlyFromCache(EntityEnterpriseContext ctx)
      {
         PersistentContext pctx = (PersistentContext)ctx.getPersistenceContext();
         pctx.loadCachedRelations(cmrIndex, this);
      }

      private boolean removeWhileNotLoaded(Object relatedId)
      {
         boolean removed = false;
         if(addedWhileNotLoaded != null)
         {
            removed = addedWhileNotLoaded.remove(relatedId);
         }

         if(!removed)
         {
            if(removedWhileNotLoaded == null)
            {
               removedWhileNotLoaded = new HashSet();
            }
            removed = removedWhileNotLoaded.add(relatedId);
         }

         if(log.isTraceEnabled() && removed)
         {
            log.trace("removed while not loaded: relatedId=" + relatedId);
         }

         return removed;
      }

      private boolean addWhileNotLoaded(Object relatedId)
      {
         boolean added = false;
         if(removedWhileNotLoaded != null)
         {
            added = removedWhileNotLoaded.remove(relatedId);
         }

         if(!added)
         {
            if(addedWhileNotLoaded == null)
            {
               addedWhileNotLoaded = new HashSet();
            }
            added = addedWhileNotLoaded.add(relatedId);
         }

         if(log.isTraceEnabled() && added)
         {
            log.trace("added while not loaded: relatedId=" + relatedId);
         }

         return added;
      }
   }

   public interface FieldState
      extends Cache.CacheLoader
   {
      Object NULL_VALUE = new Object();

      void init();

      Object getValue(EntityEnterpriseContext ctx);

      void cascadeDelete(EntityEnterpriseContext ctx) throws RemoveException;

      void destroyExistingRelationships(EntityEnterpriseContext ctx);

      void setValue(EntityEnterpriseContext ctx, Object value);

      boolean removeRelatedId(EntityEnterpriseContext ctx, Object relatedId);

      boolean addRelatedId(EntityEnterpriseContext ctx, Object value);

      void addLoadedPk(Object pk);

      void cacheValue(EntityEnterpriseContext ctx);

      boolean isModified();
   }

   private class RelationTableLoader
      implements CMRFieldLoader
   {
      private final String loadSql;

      public RelationTableLoader()
      {
         StringBuffer sql = new StringBuffer();
         sql.append("select ");

         String relatedTable = relatedEntity.getQualifiedTableName();
         String relationTable = metadata.getRelationMetaData().getDefaultTableName();

         relatedEntity.getTable().appendColumnNames((JDBCCMPFieldBridge2[])relatedEntity.getTableFields(),
            relatedTable,
            sql
         );
         sql.append(" from ")
            .append(relatedTable)
            .append(" inner join ")
            .append(relationTable)
            .append(" on ");

         JDBCCMPFieldBridge2[] pkFields = (JDBCCMPFieldBridge2[])relatedEntity.getPrimaryKeyFields();
         for(int i = 0; i < pkFields.length; ++i)
         {
            if(i > 0)
            {
               sql.append(" and ");
            }

            sql.append(relatedTable).append('.').append(pkFields[i].getColumnName())
               .append('=')
               .append(relationTable).append('.').append(relatedCMRField.tableKeyFields[i].getColumnName());
         }

         /*
         sql.append(" inner join ")
            .append(myTable)
            .append(" on ");

         String myTable = entity.getQualifiedTableName();
         pkFields = entity.getPrimaryKeyFields();
         for(int i = 0; i < pkFields.length; ++i)
         {
            if(i > 0)
            {
               sql.append(", ");
            }

            sql.append(myTable).append('.').append(pkFields[i].getColumnName())
               .append('=')
               .append(relationTable).append('.').append(tableKeyFields[i].getColumnName());
         }
         */

         sql.append(" where ");
         for(int i = 0; i < tableKeyFields.length; ++i)
         {
            if(i > 0)
            {
               sql.append(" and ");
            }

            sql.append(relationTable).append('.').append(tableKeyFields[i].getColumnName()).append("=?");
         }

         loadSql = sql.toString();

         if(log.isTraceEnabled())
         {
            log.trace("load sql: " + loadSql);
         }
      }

      public void load(EntityEnterpriseContext ctx, FieldState state)
      {
         Object value;
         EntityTable relatedTable = relatedEntity.getTable();

         Connection con = null;
         PreparedStatement ps = null;
         ResultSet rs = null;
         try
         {
            if(log.isDebugEnabled())
            {
               log.debug("executing: " + loadSql);
            }

            con = relatedTable.getDataSource().getConnection();
            ps = con.prepareStatement(loadSql);

            JDBCCMPFieldBridge2[] pkFields = (JDBCCMPFieldBridge2[])entity.getPrimaryKeyFields();

            Object myPk = ctx.getId();
            int paramInd = 1;
            for(int i = 0; i < pkFields.length; ++i)
            {
               JDBCCMPFieldBridge2 pkField = pkFields[i];
               Object fieldValue = pkField.getPrimaryKeyValue(myPk);

               JDBCCMPFieldBridge2 relatedFkField = tableKeyFields[i];
               relatedFkField.setArgumentParameters(ps, paramInd++, fieldValue);
            }

            rs = ps.executeQuery();

            while(rs.next())
            {
               value = relatedTable.loadRow(rs);
               state.addLoadedPk(value);
            }
         }
         catch(SQLException e)
         {
            log.error("Failed to load related role: ejb-name="
               +
               entity.getEntityName() +
               ", cmr-field=" + getFieldName() + ": " + e.getMessage(), e
            );
            throw new EJBException("Failed to load related role: ejb-name="
               +
               entity.getEntityName() +
               ", cmr-field=" + getFieldName() + ": " + e.getMessage(), e
            );
         }
         finally
         {
            JDBCUtil.safeClose(rs);
            JDBCUtil.safeClose(ps);
            JDBCUtil.safeClose(con);
         }
      }

      public void removeRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         relationTable.removeRelation(JDBCCMRFieldBridge2.this, ctx.getId(), relatedCMRField, relatedId);
      }

      public void addRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         relationTable.addRelation(JDBCCMRFieldBridge2.this, ctx.getId(), relatedCMRField, relatedId);
      }
   }

   private class ForeignKeyLoader
      implements CMRFieldLoader
   {
      private final String loadSql;

      public ForeignKeyLoader()
      {
         StringBuffer sql = new StringBuffer();
         sql.append("select ");
         relatedEntity.getTable().appendColumnNames((JDBCCMPFieldBridge2[])relatedEntity.getTableFields(), null, sql);
         sql.append(" from ").append(relatedEntity.getQualifiedTableName()).append(" where ");

         JDBCCMPFieldBridge2[] relatedFkFields = relatedCMRField.foreignKeyFields;
         sql.append(relatedFkFields[0].getColumnName()).append("=?");
         for(int i = 1; i < relatedFkFields.length; ++i)
         {
            JDBCCMPFieldBridge2 relatedFkField = relatedFkFields[i];
            sql.append(" and ").append(relatedFkField.getColumnName()).append("=?");
         }

         loadSql = sql.toString();

         if(log.isTraceEnabled())
         {
            log.trace("load sql: " + loadSql);
         }
      }

      public void load(EntityEnterpriseContext ctx, FieldState state)
      {
         Object value;
         EntityTable relatedTable = relatedEntity.getTable();

         Connection con = null;
         PreparedStatement ps = null;
         ResultSet rs = null;
         try
         {
            if(log.isDebugEnabled())
            {
               log.debug("executing: " + loadSql);
            }

            con = relatedTable.getDataSource().getConnection();
            ps = con.prepareStatement(loadSql);

            JDBCCMPFieldBridge2[] relatedFkFields = relatedCMRField.foreignKeyFields;
            JDBCCMPFieldBridge2[] myPkFields = relatedCMRField.relatedPKFields;

            Object myPk = ctx.getId();
            int paramInd = 1;
            for(int i = 0; i < relatedFkFields.length; ++i)
            {
               JDBCCMPFieldBridge2 myPkField = myPkFields[i];
               Object fieldValue = myPkField.getPrimaryKeyValue(myPk);

               JDBCCMPFieldBridge2 relatedFkField = relatedFkFields[i];
               relatedFkField.setArgumentParameters(ps, paramInd++, fieldValue);
            }

            rs = ps.executeQuery();

            while(rs.next())
            {
               value = relatedTable.loadRow(rs);
               state.addLoadedPk(value);
            }
         }
         catch(SQLException e)
         {
            log.error("Failed to load related role: ejb-name="
               +
               entity.getEntityName() +
               ", cmr-field=" + getFieldName() + ": " + e.getMessage(), e
            );
            throw new EJBException("Failed to load related role: ejb-name="
               +
               entity.getEntityName() +
               ", cmr-field=" + getFieldName() + ": " + e.getMessage(), e
            );
         }
         finally
         {
            JDBCUtil.safeClose(rs);
            JDBCUtil.safeClose(ps);
            JDBCUtil.safeClose(con);
         }
      }

      public void removeRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
      }

      public void addRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
      }
   }

   private class ContextForeignKeyLoader
      implements CMRFieldLoader
   {
      public void load(EntityEnterpriseContext ctx, FieldState state)
      {
         Object relatedId = null;
         for(int i = 0; i < foreignKeyFields.length; ++i)
         {
            JDBCCMPFieldBridge2 fkField = foreignKeyFields[i];
            Object fkFieldValue = fkField.getValue(ctx);
            if(fkFieldValue == null)
            {
               break;
            }

            JDBCCMPFieldBridge2 relatedPKField = relatedPKFields[i];
            relatedId = relatedPKField.setPrimaryKeyValue(relatedId, fkFieldValue);
         }

         state.addLoadedPk(relatedId);
      }

      public void removeRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         for(int i = 0; i < foreignKeyFields.length; ++i)
         {
            foreignKeyFields[i].setValueInternal(ctx, null, fkConstraint == null);
         }

         if(fkConstraint != null)
         {
            PersistentContext pctx = (PersistentContext)ctx.getPersistenceContext();
            pctx.nullForeignKey(fkConstraint);
         }
      }

      public void addRelatedId(EntityEnterpriseContext ctx, Object relatedId)
      {
         final boolean markDirty = relatedId != null || fkConstraint == null;
         for(int i = 0; i < foreignKeyFields.length; ++i)
         {
            JDBCCMPFieldBridge2 relatedPKField = relatedPKFields[i];
            Object fieldValue = relatedPKField.getPrimaryKeyValue(relatedId);
            foreignKeyFields[i].setValueInternal(ctx, fieldValue, markDirty);
         }

         if(fkConstraint != null)
         {
            PersistentContext pctx = (PersistentContext)ctx.getPersistenceContext();
            if(relatedId == null)
            {
               pctx.nullForeignKey(fkConstraint);
            }
            else
            {
               pctx.nonNullForeignKey(fkConstraint);
            }
         }
      }
   }

   private interface CMRFieldLoader
   {
      void load(EntityEnterpriseContext ctx, FieldState state);

      void removeRelatedId(EntityEnterpriseContext ctx, Object relatedId);

      void addRelatedId(EntityEnterpriseContext ctx, Object relatedId);
   }

   private class CMRSet
      implements Set
   {
      private final EntityEnterpriseContext ctx;
      private final CollectionValuedFieldState state;

      public CMRSet(EntityEnterpriseContext ctx, CollectionValuedFieldState state)
      {
         this.ctx = ctx;
         this.state = state;
      }

      public int size()
      {
         return state.getLoadedValue(ctx).size();
      }

      public void clear()
      {
         destroyExistingRelationships(ctx);
      }

      public boolean isEmpty()
      {
         return size() == 0;
      }

      public boolean add(Object o)
      {
         Object relatedId = getPrimaryKey(o);
         boolean modified = addRelatedId(ctx, relatedId);

         if(modified)
         {
            relatedCMRField.invokeAddRelatedId(relatedId, ctx.getId());
            loader.addRelatedId(ctx, relatedId);
         }

         return modified;
      }

      public boolean contains(Object o)
      {
         Object pk = getPrimaryKey(o);
         return state.getLoadedValue(ctx).contains(pk);
      }

      public boolean remove(Object o)
      {
         Object relatedId = getPrimaryKey(o);
         return removeById(relatedId);
      }

      public boolean addAll(Collection c)
      {
         if(c == null || c.isEmpty())
         {
            return false;
         }

         boolean modified = false;
         Object[] copy = c.toArray();
         for(int i = 0; i < copy.length; ++i)
         {
            // not modified || add()
            modified = add(copy[i]) || modified;
         }

         return modified;
      }

      public boolean containsAll(Collection c)
      {
         if(c == null || c.isEmpty())
         {
            return true;
         }

         Set ids = argumentToIdSet(c);
         return state.getLoadedValue(ctx).containsAll(ids);
      }

      public boolean removeAll(Collection c)
      {
         if(c == null || c.isEmpty())
         {
            return false;
         }

         boolean modified = false;
         Object[] copy = c.toArray();
         for(int i = 0; i < copy.length; ++i)
         {
            modified = remove(copy[i]) || modified;
         }

         return modified;
      }

      public boolean retainAll(Collection c)
      {
         Set value = state.getLoadedValue(ctx);
         if(c == null || c.isEmpty())
         {
            if(value.isEmpty())
            {
               return false;
            }
            else
            {
               clear();
            }
         }

         boolean modified = false;
         Set idSet = argumentToIdSet(c);
         Object[] valueCopy = value.toArray();
         for(int i = 0; i < valueCopy.length; ++i)
         {
            Object id = valueCopy[i];
            if(!idSet.contains(id))
            {
               removeById(id);
               modified = true;
            }
         }

         return modified;
      }

      public Iterator iterator()
      {
         return new Iterator()
         {
            // todo get rid of copying
            private final Iterator idIter = new HashSet(state.getLoadedValue(ctx)).iterator();
            private Object curId;

            public void remove()
            {
               try
               {
                  idIter.remove();
               }
               catch(ConcurrentModificationException e)
               {
                  throw new IllegalStateException(e.getMessage());
               }

               removeById(curId);
            }

            public boolean hasNext()
            {
               try
               {
                  return idIter.hasNext();
               }
               catch(ConcurrentModificationException e)
               {
                  throw new IllegalStateException(e.getMessage());
               }
            }

            public Object next()
            {
               try
               {
                  curId = idIter.next();
               }
               catch(ConcurrentModificationException e)
               {
                  throw new IllegalStateException(e.getMessage());
               }

               return relatedContainer.getLocalProxyFactory().getEntityEJBLocalObject(curId);
            }
         };
      }

      public Object[] toArray()
      {
         Set value = state.getLoadedValue(ctx);

         Object[] result = (Object[])Array.newInstance(relatedEntity.getLocalInterface(), value.size());

         LocalProxyFactory relatedPF = relatedContainer.getLocalProxyFactory();
         int i = 0;
         for(Iterator iter = value.iterator(); iter.hasNext();)
         {
            Object id = iter.next();
            result[i++] = relatedPF.getEntityEJBLocalObject(id);
         }

         return result;
      }

      public Object[] toArray(Object a[])
      {
         Set value = state.getLoadedValue(ctx);
         if(a == null || a.length < value.size())
         {
            a = (Object[])Array.newInstance(entity.getLocalInterface(), value.size());
         }

         LocalProxyFactory relatedPF = relatedContainer.getLocalProxyFactory();
         int i = 0;
         for(Iterator iter = value.iterator(); iter.hasNext();)
         {
            Object id = iter.next();
            a[i++] = relatedPF.getEntityEJBLocalObject(id);
         }

         return a;
      }

      public String toString()
      {
         return state.getLoadedValue(ctx).toString();
      }

      // Private

      private boolean removeById(Object relatedId)
      {
         boolean modified = removeRelatedId(ctx, relatedId);
         if(modified)
         {
            relatedCMRField.invokeRemoveRelatedId(relatedId, ctx.getId());
            loader.removeRelatedId(ctx, relatedId);
         }
         return modified;
      }

      private Set argumentToIdSet(Collection c)
      {
         Set ids = new HashSet();
         for(Iterator iter = c.iterator(); iter.hasNext();)
         {
            Object pk = getPrimaryKey(iter.next());
            ids.add(pk);
         }
         return ids;
      }
   }

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