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

import java.lang.reflect.Array;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.ejb.EJBLocalObject;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.RemoveException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import org.jboss.as.cmp.CmpMessages;
import org.jboss.as.cmp.bridge.EntityBridge;
import org.jboss.as.cmp.bridge.FieldBridge;
import org.jboss.as.cmp.component.CmpEntityBeanComponent;
import org.jboss.as.cmp.context.CmpEntityBeanContext;
import org.jboss.as.cmp.ejbql.Catalog;
import org.jboss.as.cmp.jdbc.JDBCEntityPersistenceStore;
import org.jboss.as.cmp.jdbc.JDBCType;
import org.jboss.as.cmp.jdbc.JDBCUtil;
import org.jboss.as.cmp.jdbc.bridge.JDBCAbstractCMRFieldBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCAbstractEntityBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.as.cmp.jdbc.metadata.JDBCCMPFieldMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCRelationshipRoleMetaData;
import org.jboss.as.cmp.jdbc2.JDBCStoreManager2;
import org.jboss.as.cmp.jdbc2.PersistentContext;
import org.jboss.as.cmp.jdbc2.schema.Cache;
import org.jboss.as.cmp.jdbc2.schema.EntityTable;
import org.jboss.as.cmp.jdbc2.schema.RelationTable;
import org.jboss.logging.Logger;
import org.jboss.security.SecurityContext;
import org.jboss.security.SecurityContextAssociation;


/**
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision: 81030 $</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 CmpEntityBeanComponent relatedComponent;

    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.getComponent().getTransactionManager();

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

    // Public

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

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

        // Related Entity
        Catalog catalog = manager.getCatalog();
        relatedEntity = (JDBCEntityBridge2) catalog.getEntityByEJBName(relatedEntityName);
        if (relatedEntity == null) {
            throw CmpMessages.MESSAGES.relatedEntityNotFound(entity.getEntityName(), getFieldName(), 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) {
            throw CmpMessages.MESSAGES.relatedCmrFieldNotFound(relatedEntityName, entity.getEntityName(), getFieldName() != null ? getFieldName() : "<no-field>",
                    relatedEntityName, metadata.getRelatedRole().getCMRFieldName() != null ? metadata.getRelatedRole().getCMRFieldName() : "<no-field>");
        }

        // Related Container
        relatedComponent = relatedEntity.getComponent();

        //
        // 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 CmpMessages.MESSAGES.pkNotFoundForKeyField(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 CmpMessages.MESSAGES.primaryKeyNotMapped(pkFields[i].getFieldName());
                }
                keyFieldsList.add(fkField);
            }
            tableKeyFields = (JDBCCMPFieldBridge2[]) keyFieldsList.toArray(new JDBCCMPFieldBridge2[keyFieldsList.size()]);
        } else {
            initializeForeignKeyFields();
        }
    }

    public void initLoader() {
        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(CmpEntityBeanContext ctx, Object relatedId) {
        FieldState state = getFieldState(ctx);
        return state.removeRelatedId(ctx, relatedId);
    }

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

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

    public void destroyExistingRelationships(CmpEntityBeanContext 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 CmpMessages.MESSAGES.methodNotSupported();
    }

    // JDBCFieldBridge implementation

    public JDBCType getJDBCType() {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public boolean isPrimaryKeyMember() {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public boolean isReadOnly() {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public boolean isReadTimedOut(CmpEntityBeanContext ctx) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public boolean isLoaded(CmpEntityBeanContext ctx) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

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

    public void resetPersistenceContext(CmpEntityBeanContext ctx) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public int setInstanceParameters(PreparedStatement ps, int parameterIndex, CmpEntityBeanContext ctx) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public Object getInstanceValue(CmpEntityBeanContext ctx) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public void setInstanceValue(CmpEntityBeanContext ctx, Object value) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public int loadInstanceResults(ResultSet rs, int parameterIndex, CmpEntityBeanContext ctx) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public int loadArgumentResults(ResultSet rs, int parameterIndex, Object[] argumentRef) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

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

    public void setClean(CmpEntityBeanContext ctx) {
        throw CmpMessages.MESSAGES.methodNotSupported();
    }

    public boolean isCMPField() {
        return false;
    }

    // CMRFieldBridge implementation

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

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

    public void setValue(CmpEntityBeanContext 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() {
        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(CmpEntityBeanContext 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(CmpEntityBeanContext ctx, Object myId, Object relatedId) {
        if (log.isTraceEnabled()) {
            log.trace("Remove relation: field=" + getFieldName() +
                    " id=" + myId +
                    " relatedId=" + relatedId);
        }
        removeRelatedId(ctx, relatedId);
    }

    private void invokeAddRelatedId(CmpEntityBeanContext ctx, Object myId, Object relatedId) {
        if (log.isTraceEnabled()) {
            log.trace("Add relation: field=" + getFieldName() +
                    " id=" + myId +
                    " relatedId=" + relatedId);
        }
        addRelatedId(ctx, relatedId);
    }

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

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

    private Object getPrimaryKey(Object o) {
        if (o == null) {
            throw CmpMessages.MESSAGES.nullNumbersNotSupported();
        }

        if (!relatedEntity.getLocalInterface().isInstance(o)) {
            throw CmpMessages.MESSAGES.invalidArgumentType(entity.getLocalInterface().getName());
        }

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

    // 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(CmpEntityBeanContext ctx) {
            Object value = getLoadedValue(ctx);
            if (value == null) {
                localObject = null;
            } else if (localObject == null) {
                localObject = relatedComponent.getEJBLocalObject(value);
            }
            return localObject;
        }

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

        public void cascadeDelete(CmpEntityBeanContext ctx) throws RemoveException {
            if (manager.registerCascadeDelete(ctx.getPrimaryKeyUnchecked(), ctx.getPrimaryKeyUnchecked())) {
                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.getPrimaryKeyUnchecked());
            }
        }

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

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

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

            cacheValue(ctx);

            modified = true;

            return true;
        }

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

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

            cacheValue(ctx);

            modified = true;

            return true;
        }

        public void addLoadedPk(Object pk) {
            if (loaded) {
                throw CmpMessages.MESSAGES.singleValuedCmrFieldAlreadyLoaded(entity.getEntityName(), getFieldName(), 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(CmpEntityBeanContext 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(CmpEntityBeanContext 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(CmpEntityBeanContext ctx) {
            if (cmrSet == null) {
                cmrSet = new CMRSet(ctx, this);
            }
            return cmrSet;
        }

        public void setValue(CmpEntityBeanContext ctx, Object value) {
            if (value == null) {
                throw CmpMessages.MESSAGES.cannotSetCmrCollectionToNull(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(ctx, relatedId, ctx.getPrimaryKeyUnchecked());
                    loader.addRelatedId(ctx, relatedId);
                }
            }
        }

        public void cascadeDelete(CmpEntityBeanContext ctx) throws RemoveException {
            Collection<EJBLocalObject> cmrs = (Collection<EJBLocalObject>) getValue(ctx);
            for (EJBLocalObject localEjb : cmrs) {
                localEjb.remove();
            }
        }

        public void destroyExistingRelationships(CmpEntityBeanContext 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(ctx, relatedId, ctx.getPrimaryKeyUnchecked());
                    loader.removeRelatedId(ctx, relatedId);
                }
            }
        }

        public boolean removeRelatedId(CmpEntityBeanContext 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(CmpEntityBeanContext 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 CmpMessages.MESSAGES.collectionValuedCmrFieldAlreadyLoaded(entity.getEntityName(), getFieldName(), 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(CmpEntityBeanContext ctx) {
            PersistentContext pctx = (PersistentContext) ctx.getPersistenceContext();
            pctx.cacheRelations(cmrIndex, this);
        }

        public boolean isModified() {
            return modified;
        }

        // Private

        private Set getLoadedValue(CmpEntityBeanContext 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(CmpEntityBeanContext ctx) {
            PersistentContext pctx = (PersistentContext) ctx.getPersistenceContext();
            if (pctx == null) {
                throw CmpMessages.MESSAGES.persistenceContextNotAvailable();
            }
            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(CmpEntityBeanContext ctx);

        void cascadeDelete(CmpEntityBeanContext ctx) throws RemoveException;

        void destroyExistingRelationships(CmpEntityBeanContext ctx);

        void setValue(CmpEntityBeanContext ctx, Object value);

        boolean removeRelatedId(CmpEntityBeanContext ctx, Object relatedId);

        boolean addRelatedId(CmpEntityBeanContext ctx, Object value);

        void addLoadedPk(Object pk);

        void cacheValue(CmpEntityBeanContext 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(CmpEntityBeanContext 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.getPrimaryKeyUnchecked();
                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, false);
                    state.addLoadedPk(value);
                }
            } catch (SQLException e) {
                throw CmpMessages.MESSAGES.failedToLoadRelatedRole(entity.getEntityName(), getFieldName(), e);
            } finally {
                JDBCUtil.safeClose(rs);
                JDBCUtil.safeClose(ps);
                JDBCUtil.safeClose(con);
            }
        }

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

        public void addRelatedId(CmpEntityBeanContext ctx, Object relatedId) {
            relationTable.addRelation(JDBCCMRFieldBridge2.this, ctx.getPrimaryKeyUnchecked(), 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(CmpEntityBeanContext 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.getPrimaryKeyUnchecked();
                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, false);
                    state.addLoadedPk(value);
                }
            } catch (SQLException e) {
                throw CmpMessages.MESSAGES.failedToLoadRelatedRole(entity.getEntityName(), getFieldName(), e);
            } finally {
                JDBCUtil.safeClose(rs);
                JDBCUtil.safeClose(ps);
                JDBCUtil.safeClose(con);
            }
        }

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

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

    private class ContextForeignKeyLoader
            implements CMRFieldLoader {
        public void load(CmpEntityBeanContext 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(CmpEntityBeanContext 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(CmpEntityBeanContext 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(CmpEntityBeanContext ctx, FieldState state);

        void removeRelatedId(CmpEntityBeanContext ctx, Object relatedId);

        void addRelatedId(CmpEntityBeanContext ctx, Object relatedId);
    }

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

        public CMRSet(CmpEntityBeanContext 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(ctx, relatedId, ctx.getPrimaryKeyUnchecked());
                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);
                    }

                    removeById(curId);
                }

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

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

                    return relatedComponent.getEJBLocalObject(curId);
                }
            };
        }

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

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

            int i = 0;
            for (Iterator iter = value.iterator(); iter.hasNext(); ) {
                Object id = iter.next();
                result[i++] = relatedComponent.getEJBLocalObject(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());
            }

            int i = 0;
            for (Iterator iter = value.iterator(); iter.hasNext(); ) {
                Object id = iter.next();
                a[i++] = relatedComponent.getEJBLocalObject(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(ctx, relatedId, ctx.getPrimaryKeyUnchecked());
                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();
                Principal p = null;
                SecurityContext sc = getSecurityContext();
                if (sc != null) {
                    p = sc.getUtil().getUserPrincipal();
                }
                return p;
            }

            public Object getCredential() {
                //return SecurityAssociation.getCredential();
                Object credential = null;
                SecurityContext sc = getSecurityContext();
                if (sc != null) {
                    credential = sc.getUtil().getCredential();
                }
                return credential;
            }

            public SecurityContext getSecurityContext() {
                return SecurityContextAssociation.getSecurityContext();
            }
        };

        SecurityActions PRIVILEGED = new SecurityActions() {
            private final PrivilegedAction getPrincipalAction = new PrivilegedAction() {
                public Object run() {
                    //return SecurityAssociation.getPrincipal();
                    Principal p = null;
                    SecurityContext sc = getSecurityContext();
                    if (sc != null) {
                        p = sc.getUtil().getUserPrincipal();
                    }
                    return p;
                }
            };

            private final PrivilegedAction getCredentialAction = new PrivilegedAction() {
                public Object run() {
                    //return SecurityAssociation.getCredential();
                    Object credential = null;
                    SecurityContext sc = getSecurityContext();
                    if (sc != null) {
                        credential = sc.getUtil().getCredential();
                    }
                    return credential;
                }
            };

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

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

            public SecurityContext getSecurityContext() {
                return (SecurityContext) AccessController.doPrivileged(new PrivilegedAction() {

                    public Object run() {
                        return SecurityContextAssociation.getSecurityContext();
                    }
                });
            }
        };

        Principal getPrincipal();

        Object getCredential();

        SecurityContext getSecurityContext();
    }
}
