/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     tware - initial implementation
//     tware - implemenation of basic CRUD functionality
//     11/10/2011-2.4 Guy Pelletier
//       - 357474: Address primaryKey option from tenant discriminator column
//     14/05/2012-2.4 Guy Pelletier
//       - 376603: Provide for table per tenant support for multitenant applications
package org.eclipse.persistence.internal.queries;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.persistence.annotations.CacheKeyType;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.core.queries.CoreMappedKeyMapContainerPolicy;
import org.eclipse.persistence.internal.descriptors.DescriptorIterator;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.CollectionChangeRecord;
import org.eclipse.persistence.internal.sessions.MergeManager;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.AggregateObjectMapping;
import org.eclipse.persistence.mappings.Association;
import org.eclipse.persistence.mappings.CollectionMapping;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.DirectMapMapping;
import org.eclipse.persistence.mappings.DirectToFieldMapping;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;
import org.eclipse.persistence.mappings.converters.Converter;
import org.eclipse.persistence.mappings.foundation.AbstractDirectMapping;
import org.eclipse.persistence.mappings.foundation.MapKeyMapping;
import org.eclipse.persistence.mappings.foundation.MapComponentMapping;
import org.eclipse.persistence.mappings.querykeys.QueryKey;
import org.eclipse.persistence.queries.DataReadQuery;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.DeleteObjectQuery;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReadQuery;
import org.eclipse.persistence.queries.WriteObjectQuery;
import org.eclipse.persistence.sessions.DatabaseRecord;

/**
 * A MappedKeyMapContainerPolicy should be used for mappings to implementers of Map.
 * It differs from MapContainerPolicy by allowing the MapKey to be an otherwise unmapped
 * column in a table rather than a mapped element of the value in the map.
 *
 * This container policy holds a reference to a KeyMapping that will be used to construct the key
 * from the database and a reference to its owner which creates the value for the map.
 *
 * The key of the map can be any implementer of MapKeyMapping and the data representing the
 * key can either be stored in the target table of the value mapping, or in a collection table that
 * associates the source to the target.   The data can either be everything necessary to compose the
 * key, or foreign keys that allow the key to be retrieved
 *
 * @see MapContainerPolicy
 * @see MapKeyMapping
 * @see MapComponentMapping
 *
 * @author tware
 *
 */
public class MappedKeyMapContainerPolicy extends MapContainerPolicy implements CoreMappedKeyMapContainerPolicy<AbstractSession> {

    protected MapKeyMapping keyMapping;

    protected MapComponentMapping valueMapping;

    public DatabaseQuery keyQuery;

    /**
     * INTERNAL:
     * Construct a new policy.
     */
    public MappedKeyMapContainerPolicy() {
        super();
    }

    /**
     * INTERNAL:
     * Construct a new policy for the specified class.
     */
    public MappedKeyMapContainerPolicy(Class containerClass) {
        super(containerClass);
    }

    /**
     * INTERNAL:
     * Construct a new policy for the specified class name.
     */
    public MappedKeyMapContainerPolicy(String containerClassName) {
        super(containerClassName);
    }

    /**
     * INTERNAL:
     * Called when the selection query is being initialize to add the fields for the key to the query
     */
    @Override
    public void addAdditionalFieldsToQuery(ReadQuery selectionQuery, Expression baseExpression) {
        keyMapping.addAdditionalFieldsToQuery(selectionQuery, baseExpression);
    }

    /**
     * INTERNAL:
     * Add any non-Foreign-key data from an Object describe by a MapKeyMapping to a database row
     * This is typically used in write queries to ensure all the data stored in the collection table is included
     * in the query.
     */
    @Override
    public Map getKeyMappingDataForWriteQuery(Object object, AbstractSession session) {
        if (((DatabaseMapping)keyMapping).isReadOnly()) {
            return null;
        }
        Object keyValue = ((Map.Entry)object).getKey();
        return keyMapping.extractIdentityFieldsForQuery(keyValue, session);
    }

    /**
     * INTERNAL:
     * Return the type of the map key, this will be overridden by container policies that allow maps.
     */
    @Override
    public Object getKeyType() {
        return keyMapping.getMapKeyTargetType();
    }

    /**
     * INTERNAL:
     * Called when the insert query is being initialized to ensure the fields for the key are in the insert query
     *
     * @see MappedKeyMapContainerPolicy
     */
    @Override
    public void addFieldsForMapKey(AbstractRecord joinRow) {
        if (((DatabaseMapping)keyMapping).isReadOnly()) {
            return;
        }
        keyMapping.addFieldsForMapKey(joinRow);
    }

    /**
     * INTERNAL:
     * Add element into container which implements the Map interface.
     * The may be used by merging/cloning passing a Map.Entry.
     */
    @Override
    public boolean addInto(Object element, Object container, AbstractSession session) {
        if (element instanceof Map.Entry) {
            Map.Entry record = (Map.Entry)element;
            Object key = record.getKey();
            Object value = record.getValue();
            return addInto(key, value, container, session);
        }
        throw QueryException.cannotAddToContainer(element, container, this);
    }

    /**
     * INTERNAL:
     * This is used for ordered List containers to add all of the elements
     * to the collection in the order of the index field in the row.
     * This is currently only used by OrderListContainerPolicy, so this is just a stub.
     * The passing of the query is to allow future compatibility with Maps (ordered Map).
     */
    @Override
    public boolean addInto(Object element, Object container, AbstractSession session, AbstractRecord row, DataReadQuery query, CacheKey parentCacheKey, boolean isTargetProtected) {
        Object key = this.keyMapping.createMapComponentFromRow(row, null, parentCacheKey, session, isTargetProtected);
        Object value = this.valueMapping.createMapComponentFromRow(row, null, parentCacheKey, session, isTargetProtected);
        return addInto(key, value, container, session);
    }

    /**
     * INTERNAL:
     * Add element to that implements the Map interface
     * use the row to compute the key
     */
    @Override
    public boolean addInto(Object element, Object container, AbstractSession session, AbstractRecord dbRow, ObjectBuildingQuery query, CacheKey parentCacheKey, boolean isTargetProtected) {
        Object key = null;
        Object value = null;

        // we are a direct collection mapping.  This means the key will be element and the value will come
        // from dbRow
        if ((valueMapping != null) && (((DatabaseMapping)valueMapping).isDirectCollectionMapping()) && (session.getDescriptor(element.getClass()) != null)) {
            key = element;
            value = valueMapping.createMapComponentFromRow(dbRow, null, parentCacheKey, session, isTargetProtected);
        } else if (keyMapping != null) {
            value = element;
            try{
                key = keyMapping.createMapComponentFromRow(dbRow, query, parentCacheKey, session, isTargetProtected);
            } catch (Exception e) {
                throw QueryException.exceptionWhileReadingMapKey(element, e);
            }
        }
        return addInto(key, value, container, session);
    }

    /**
     * INTERNAL:
     * Used for joining.  Add any queries necessary for joining to the join manager
     */
    @Override
    public void addNestedJoinsQueriesForMapKey(JoinedAttributeManager joinManager, ObjectLevelReadQuery query, AbstractSession session){
        ObjectLevelReadQuery nestedQuery = keyMapping.getNestedJoinQuery(joinManager, query, session);
        if (nestedQuery != null){
            joinManager.getJoinedMappingQueries_().put((DatabaseMapping)keyMapping, nestedQuery);
        }
    }

    /**
     * Build a clone for the key of a Map represented by this container policy.
     */
    @Override
    public Object buildCloneForKey(Object key, Object parent, CacheKey parentCacheKey, Integer refreshCascade, AbstractSession cloningSession, boolean isExisting, boolean isCacheCheckComplete){
        return keyMapping.buildElementClone(key, parent, parentCacheKey, refreshCascade, cloningSession, isExisting, isCacheCheckComplete);

    }

    /**
     * INTERNAL:
     * Certain key mappings favor different types of selection query.  Return the appropriate
     * type of selectionQuery.
     */
    @Override
    public ReadQuery buildSelectionQueryForDirectCollectionMapping(){
        ReadQuery query = keyMapping.buildSelectionQueryForDirectCollectionKeyMapping(this);
        return query;
    }

    /**
     * Extract the key for the map from the provided row.
     */
    @Override
    public Object buildKey(AbstractRecord row, ObjectBuildingQuery query, CacheKey parentCacheKey, AbstractSession session, boolean isTargetProtected){
        return keyMapping.createMapComponentFromRow(row, query, parentCacheKey, session, isTargetProtected);
    }

    /**
     * Extract the key for the map from the provided row.
     */
    @Override
    public Object buildKeyFromJoinedRow(AbstractRecord row, JoinedAttributeManager joinManager, ObjectBuildingQuery query, CacheKey parentCacheKey, AbstractSession session, boolean isTargetProtected){
        return keyMapping.createMapComponentFromJoinedRow(row, joinManager, query, parentCacheKey, session, isTargetProtected);
    }

    /**
     * INTERNAL:
     * This method will access the target relationship and create a list of information to rebuild the collection.
     * For the MapContainerPolicy this return will consist of an array with serial Map entry key and value elements.
     * @see ObjectReferenceMapping.buildReferencesPKList
     * @see ContainerPolicy.buildReferencesPKList
     */
    @Override
    public Object[] buildReferencesPKList(Object container, AbstractSession session){
        Object[] result = new Object[this.sizeFor(container)*2];
        Iterator iterator = (Iterator)this.iteratorFor(container);
        boolean isElementCollection = ((DatabaseMapping)valueMapping).isElementCollectionMapping();
        int index = 0;
        while(iterator.hasNext()){
            Map.Entry entry = (Entry) iterator.next();
            result[index] = keyMapping.createSerializableMapKeyInfo(entry.getKey(), session);
            ++index;
            if (isElementCollection) {
                result[index] = entry.getValue();
            } else {
                result[index] = elementDescriptor.getObjectBuilder().extractPrimaryKeyFromObject(entry.getValue(), session);
            }
            ++index;
        }
        return result;

    }

    /**
     * INTERNAL:
     * Cascade discover and persist new objects during commit to the map key
     */
    @Override
    public void cascadeDiscoverAndPersistUnregisteredNewObjects(Object object, Map newObjects, Map unregisteredExistingObjects, Map visitedObjects, UnitOfWorkImpl uow, Set cascadeErrors) {
        keyMapping.cascadeDiscoverAndPersistUnregisteredNewObjects(((Map.Entry)object).getKey(), newObjects, unregisteredExistingObjects, visitedObjects, uow, false, cascadeErrors);
    }

    /**
     * INTERNAL:
     * Cascade registerNew to any mappings managed by the container policy. This will cascade the register to the key mapping.
     */
    @Override
    public void cascadePerformRemoveIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
        keyMapping.cascadePerformRemoveIfRequired(((Map.Entry)object).getKey(), uow, visitedObjects, false);
    }

    /**
     * INTERNAL:
     * Cascade registerNew to any mappings managed by the container policy. This will cascade the register to the key mapping.
     */
    @Override
    public void cascadeRegisterNewIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
        keyMapping.cascadeRegisterNewIfRequired(((Map.Entry)object).getKey(), uow, visitedObjects, false);
    }

    /**
     * INTERNAL:
     * The mapping clones itself to create deep copy.
     */
    @Override
    public Object clone() {
        MappedKeyMapContainerPolicy clone = (MappedKeyMapContainerPolicy) super.clone();

        clone.keyMapping = (MapKeyMapping) this.keyMapping.clone();

        if (this.keyQuery != null) {
            clone.keyQuery = (DatabaseQuery) this.keyQuery.clone();
        }

        return clone;
    }

    /**
     * INTERNAL:
     * Return true if keys are the same.  False otherwise
     */
    public boolean compareContainers(Object firstObjectMap, Object secondObjectMap) {
        if (sizeFor(firstObjectMap) != sizeFor(secondObjectMap)) {
            return false;
        }

        for (Object firstIterator = iteratorFor(firstObjectMap); hasNext(firstIterator);) {
            Map.Entry entry = (Map.Entry)nextEntry(firstIterator);
            Object key = entry.getKey();
            if (!((Map)firstObjectMap).get(key).equals(((Map)secondObjectMap).get(key))) {
                return false;
            }
        }
        return true;
    }

    /**
     * INTERNAL:
     * Return true if keys are the same in the source as the backup.  False otherwise
     * in the case of read-only compare against the original.
     */
    @Override
    public boolean compareKeys(Object sourceValue, AbstractSession session) {
        // Key is not stored in the object, only in the Map and the DB
        // As a result, a change in the object will not change how this object is hashed
        if (keyMapping != null){
            return true;
        }
        return super.compareKeys(sourceValue, session);
    }

    /**
     * INTERNAL:
     * Create change sets that contain map keys.
     */
    @Override
    protected void createChangeSetForKeys(Map originalKeyValues, CollectionChangeRecord changeRecord, AbstractSession session, ClassDescriptor referenceDescriptor){
        Iterator originalKeyValuesIterator = originalKeyValues.values().iterator();
        while (originalKeyValuesIterator.hasNext()){
            Association association = (Association)originalKeyValuesIterator.next();
            Object object = association.getValue();
            ObjectChangeSet changeSet = referenceDescriptor.getObjectBuilder().createObjectChangeSet(object, (UnitOfWorkChangeSet) changeRecord.getOwner().getUOWChangeSet(), session);
            changeSet.setOldKey(association.getKey());
        }
    }

    /**
     * INTERNAL:
     * Create a query key that links to the map key.
     */
    @Override
    public QueryKey createQueryKeyForMapKey() {
        return keyMapping.createQueryKeyForMapKey();
    }

    /**
     * INTERNAL:
     * This method will actually potentially wrap an object in two ways.  It will first wrap the object
     * based on the referenceDescriptor's wrapper policy.  It will also potentially do some wrapping based
     * on what is required by the container policy.
     */
    @Override
    public Object createWrappedObjectFromExistingWrappedObject(Object wrappedObject, Object parent, ClassDescriptor referenceDescriptor, MergeManager mergeManager, AbstractSession targetSession){
        Object key = ((Map.Entry)wrappedObject).getKey();
        key = keyMapping.getTargetVersionOfSourceObject(key, parent, mergeManager, targetSession);
        key = keyMapping.wrapKey(key, mergeManager.getSession());
        Object value = referenceDescriptor.getObjectBuilder().wrapObject(mergeManager.getTargetVersionOfSourceObject(unwrapIteratorResult(wrappedObject), referenceDescriptor, targetSession), mergeManager.getSession());
        return new Association(key, value);
    }

    /**
     * INTERNAL:
     * Convert all the class-name-based settings in this ContainerPolicy to actual class-based
     * settings
     * @param classLoader
     */
    @Override
    public void convertClassNamesToClasses(ClassLoader classLoader) {
        ((DatabaseMapping)keyMapping).convertClassNamesToClasses(classLoader);
    }


    /**
     * INTERNAL:
     * Delete the key and value of the passed association passed object.
     */
    @Override
    public void deleteWrappedObject(Object objectDeleted, AbstractSession session) {
        if (((DatabaseMapping)keyMapping).isPrivateOwned()){
            keyMapping.deleteMapKey(((Map.Entry)objectDeleted).getKey(), session);
        }
        session.deleteObject(unwrapIteratorResult(objectDeleted));
    }

    /**
     * INTERNAL:
     * Return any tables that will be required when this mapping is used as part of a join query.
     */
    @Override
    public List<DatabaseTable> getAdditionalTablesForJoinQuery() {
        return keyMapping.getAdditionalTablesForJoinQuery();
    }

    /**
     * INTERNAL:
     * Return any additional fields required by the policy for a fetch join.
     */
    @Override
    public List<DatabaseField> getAdditionalFieldsForJoin(CollectionMapping baseMapping) {
        return keyMapping.getAllFieldsForMapKey();
    }

    /**
     * INTERNAL:
     * Return a Map of any foreign keys defined within the the MapKey.
     */
    public Map<DatabaseField, DatabaseField> getForeignKeyFieldsForMapKey() {
        return keyMapping.getForeignKeyFieldsForMapKey();
    }

    /**
     * INTERNAL:
     * Return the reference descriptor for the map key if it exists.
     */
    @Override
    public ClassDescriptor getDescriptorForMapKey() {
        return keyMapping.getReferenceDescriptor();
    }

    /**
     * INTERNAL:
     * Used when objects are added or removed during an update.
     * This method returns either the clone from the ChangeSet or a packaged
     * version of it that contains things like map keys.
     */
    @Override
    public Object getCloneDataFromChangeSet(ObjectChangeSet changeSet) {
        Object key = changeSet.getNewKey();
        if (key == null) {
            key = changeSet.getOldKey();
        }
        return new Association(key ,changeSet.getUnitOfWorkClone());
    }


    /**
     * INTERNAL:
     * Return the DatabaseField that represents the key in a DirectMapMapping.  If the
     * keyMapping is not a DirectMapping, this will return null.
     */
    @Override
    public DatabaseField getDirectKeyField(CollectionMapping baseMapping) {
        if ((keyMapping != null) && ((DatabaseMapping)keyMapping).isDirectToFieldMapping()) {
            return ((AbstractDirectMapping)keyMapping).getField();
        }
        return null;
    }

    /**
     * INTERNAL:
     * Return the fields that make up the identity of the mapped object.  For mappings with
     * a primary key, it will be the set of fields in the primary key.  For mappings without
     * a primary key it will likely be all the fields.
     */
    @Override
    public List<DatabaseField> getIdentityFieldsForMapKey() {
        return keyMapping.getIdentityFieldsForMapKey();
    }

    /**
     * INTERNAL:
     * Get the Converter for the key of this mapping if one exists.
     */
    public Converter getKeyConverter() {
        if ((keyMapping != null) && ((DatabaseMapping)keyMapping).isDirectToFieldMapping()) {
            return ((AbstractDirectMapping)keyMapping).getConverter();
        }
        return null;
    }

    public MapKeyMapping getKeyMapping() {
        return keyMapping;
    }

    /**
     * INTERNAL:
     * Some map keys must be obtained from the database.  This query is used to obtain the key.
     */
    public DatabaseQuery getKeyQuery() {
        return keyQuery;
    }

    /**
     * INTERNAL:
     * Get the selection criteria for the map key.
     */
    @Override
    public Expression getKeySelectionCriteria() {
        return keyMapping.getAdditionalSelectionCriteriaForMapKey();
    }

    public MapComponentMapping getValueMapping(){
        return valueMapping;
    }

    /**
     * INTERNAL:
     * Initialize the key mapping
     */
    @Override
    public void initialize(AbstractSession session, DatabaseTable keyTable) {
        getKeyMapping().preinitializeMapKey(keyTable);
        ((DatabaseMapping)keyMapping).initialize(session);
    }

    /**
     * CollectionTableMapContainerPolicy is for mappings where the key is stored in a table separately from the map
     * element.
     */
    @Override
    protected boolean isKeyAvailableFromElement() {
        return false;
    }

    @Override
    public boolean isMappedKeyMapPolicy() {
        return true;
    }

    /**
     * INTERNAL:
     * Return whether a map key this container policy represents is an attribute
     * By default this method will return false since only subclasses actually represent maps.
     */
    @Override
    public boolean isMapKeyAttribute() {
        return ((DatabaseMapping)keyMapping).isAbstractDirectMapping();
    }

    /**
     * INTERNAL:
     * Return if the map key this container policy represents is a OneToOne.
     */
    @Override
    public boolean isMapKeyObject() {
        return ((DatabaseMapping)keyMapping).isOneToOneMapping();
    }

    /**
     * INTERNAL:
     * Used in Descriptor Iteration to iterate on map keys.
     */
    @Override
    public void iterateOnMapKey(DescriptorIterator iterator, Object element) {
        Object key = ((Map.Entry)element).getKey();
        keyMapping.iterateOnMapKey(iterator, key);
    }

    /**
     * INTERNAL:
     * Add the provided object to the deleted objects list on the commit manager.
     * This may be overridden by subclasses to process a composite object.
     */
    @Override
    public void postCalculateChanges(ObjectChangeSet ocs, ClassDescriptor referenceDescriptor, DatabaseMapping mapping, UnitOfWorkImpl uow) {
        if (((DatabaseMapping)getKeyMapping()).isForeignReferenceMapping() && ((DatabaseMapping)getKeyMapping()).isPrivateOwned()) {
            Object key = ocs.getOldKey();
            uow.addDeletedPrivateOwnedObjects((DatabaseMapping)getKeyMapping(), key);
        }
        super.postCalculateChanges(ocs, referenceDescriptor, mapping, uow);
    }

    /**
     * INTERNAL:
     * Add the provided object to the deleted objects list on the commit manager.
     * This may be overridden by subclasses to process a composite object.
     */
    @Override
    public void postCalculateChanges(Object key, Object value, ClassDescriptor referenceDescriptor, DatabaseMapping mapping, UnitOfWorkImpl uow) {
        if (((DatabaseMapping)getKeyMapping()).isForeignReferenceMapping() && ((DatabaseMapping)getKeyMapping()).isPrivateOwned()) {
            uow.addDeletedPrivateOwnedObjects((DatabaseMapping)getKeyMapping(), key);
        }
        super.postCalculateChanges(key, value, referenceDescriptor, mapping, uow);
    }

    /**
     * INTERNAL:
     * This method is used to check the key mapping to ensure that it does not write to
     * a field that is written by another mapping.  There are two possibilities:
     *
     * 1. The conflicting mapping has already been processed.  In that case, we add MultipleWritableMappings
     * exception to the integrity checker right away
     * 2. There are no conflicting mappings.  In that case, we store the list of fields that this mapping
     * has processed on the descriptor for the target so they can be checked as the descriptor initializes.
     */
    @Override
    public void processAdditionalWritableMapKeyFields(AbstractSession session) {
        if (!((DatabaseMapping)getKeyMapping()).isReadOnly() && (this.valueMapping instanceof CollectionMapping)) {
            CollectionMapping mapping = (CollectionMapping)valueMapping;
            Iterator<DatabaseField> i = getIdentityFieldsForMapKey().iterator();
            while (i.hasNext()){
                DatabaseField field = i.next();
                if (mapping.getReferenceDescriptor().getObjectBuilder().getMappingsByField().containsKey(field) || mapping.getReferenceDescriptor().getAdditionalWritableMapKeyFields().contains(field)) {
                    session.getIntegrityChecker().handleError(DescriptorException.multipleWriteMappingsForField(field.toString(), mapping));
                } else {
                    mapping.getReferenceDescriptor().getAdditionalWritableMapKeyFields().add(field);
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Add the key and value from provided association to the deleted objects list on the commit manager.
     */
    @Override
    public void recordPrivateOwnedRemovals(Object object,ClassDescriptor referenceDescriptor, UnitOfWorkImpl uow){
        if (((DatabaseMapping)keyMapping).isPrivateOwned()){
            Object key = ((Map.Entry)object).getKey();
            ((DatabaseMapping)keyMapping).getReferenceDescriptor().getObjectBuilder().recordPrivateOwnedRemovals(key, uow, false);
        }
        super.recordPrivateOwnedRemovals(((Map.Entry)object).getValue(), referenceDescriptor, uow);
    }

    /**
     * INTERNAL:
     * Returns whether this ContainerPolicy requires data modification events when
     * objects are added or deleted during update.
     */
    @Override
    public boolean requiresDataModificationEvents(){
        return keyMapping.requiresDataModificationEventsForMapKey();
    }

    /**
     * INTERNAL:
     * Return the key for the specified element.
     */
    @Override
    public Object keyFrom(Object element, AbstractSession session) {
        // key is mapped to the database table and not the object and therefore cannot be extracted from the object
        if (keyMapping != null){
            return null;
        }
        return super.keyFrom(element, session);
    }

    /**
     * INTERNAL:
     * Some subclasses need to post initialize mappings associated with them.
     */
    @Override
    public void postInitialize(AbstractSession session) {
        ((DatabaseMapping)this.keyMapping).postInitialize(session);
        this.keyMapping.postInitializeMapKey(this);
    }

    /**
     * INTERNAL:
     * Propagate the postDeleteEvent to any additional objects the query is aware of
     */
    @Override
    public void propogatePostDelete(DeleteObjectQuery query, Object object) {
        if (propagatesEventsToCollection()){
            ((AggregateObjectMapping)keyMapping).postDeleteAttributeValue(query, ((Map.Entry)object).getKey());
        }
    }

    /**
     * INTERNAL:
     * Propagate the postDeleteEvent to any additional objects the query is aware of
     */
    @Override
    public void propogatePostInsert(WriteObjectQuery query, Object object) {
        if (propagatesEventsToCollection()){
            ((AggregateObjectMapping)keyMapping).postInsertAttributeValue(query, ((Map.Entry)object).getKey());
        }
    }

    /**
     * INTERNAL:
     * Propagate the postDeleteEvent to any additional objects the query is aware of
     */
    @Override
    public void propogatePostUpdate(WriteObjectQuery query, Object object) {
        if (propagatesEventsToCollection()){
            Object key = object;
            if (object instanceof Map.Entry){
                key = ((Map.Entry)object).getKey();
            }
            ((AggregateObjectMapping)keyMapping).postUpdateAttributeValue(query, key);
        }
    }

    /**
     * INTERNAL:
     * Propagate the postDeleteEvent to any additional objects the query is aware of
     */
    @Override
    public void propogatePreDelete(DeleteObjectQuery query, Object object) {
        if (propagatesEventsToCollection()){
            ((AggregateObjectMapping)keyMapping).preDeleteAttributeValue(query, ((Map.Entry)object).getKey());
        }
    }

    /**
     * INTERNAL:
     * Propagate the postDeleteEvent to any additional objects the query is aware of
     */
    public void propogatePreInsert(WriteObjectQuery query, Object object) {
        if (propagatesEventsToCollection()){
            ((AggregateObjectMapping)keyMapping).preInsertAttributeValue(query, ((Map.Entry)object).getKey());
        }
    }

    /**
     * INTERNAL:
     * Propagate the postDeleteEvent to any additional objects the query is aware of
     */
    @Override
    public void propogatePreUpdate(WriteObjectQuery query, Object object) {
        if (propagatesEventsToCollection()) {
            ((AggregateObjectMapping)keyMapping).preUpdateAttributeValue(query, ((Map.Entry)object).getKey());
        }
    }

    /**
     * INTERNAL:
     * Returns true if the key mapping is an AggregateObjectMapping.
     * Aggregates need events propagated to them because they are not explicitly
     * deleted, updated or inserted
     */
    public boolean propagatesEventsToCollection() {
        return ((DatabaseMapping)keyMapping).isAggregateObjectMapping();
    }

    /**
     * INTERNAL:
     * Set the DatabaseField that will represent the key in a DirectMapMapping.
     */
    public void setKeyField(DatabaseField keyField, ClassDescriptor descriptor) {
        if (keyMapping == null) {
            AbstractDirectMapping newKeyMapping = new DirectToFieldMapping();
            newKeyMapping.setField(keyField);
            newKeyMapping.setDescriptor(descriptor);
            setKeyMapping(newKeyMapping);
        }
        if (((DatabaseMapping)keyMapping).isDirectToFieldMapping()) {
            ((AbstractDirectMapping)keyMapping).setField(keyField);;
        }
    }

    /**
     * INTERNAL:
     * Used during initialization of DirectMapMapping.  Sets the descriptor associated with
     * the key.
     */
    public void setDescriptorForKeyMapping(ClassDescriptor descriptor){
        ((DatabaseMapping)keyMapping).setDescriptor(descriptor);
    }

    /**
     * INTERNAL:
     * Set a converter on the KeyField of a DirectCollectionMapping.
     */
    public void setKeyConverter(Converter keyConverter, DirectMapMapping mapping){
        if (((DatabaseMapping)keyMapping).isDirectToFieldMapping()){
            ((AbstractDirectMapping)keyMapping).setConverter(keyConverter);
        } else {
            throw DescriptorException.cannotSetConverterForNonDirectMapping(mapping.getDescriptor(), mapping, keyConverter.getClass().getName());
        }
    }

    /**
     * INTERNAL:
     * Set the name of the class to be used as a converter for the key of a DirectMapMaping.
     */
    public void setKeyConverterClassName(String keyConverterClassName, DirectMapMapping mapping){
        if (((DatabaseMapping)keyMapping).isDirectToFieldMapping()){
            ((AbstractDirectMapping)keyMapping).setConverterClassName(keyConverterClassName);
        } else {
            throw DescriptorException.cannotSetConverterForNonDirectMapping(mapping.getDescriptor(), mapping, keyConverterClassName);
        }

    }

    public void setKeyMapping(MapKeyMapping mapping){
        if (((DatabaseMapping)mapping).isForeignReferenceMapping() && ((ForeignReferenceMapping)mapping).getIndirectionPolicy().usesIndirection()){
            throw ValidationException.mapKeyCannotUseIndirection((DatabaseMapping)mapping);
        }
        this.keyMapping = mapping;
        ((DatabaseMapping)mapping).setIsMapKeyMapping(true);
    }

    /**
     * INTERNAL:
     * Some map keys must be obtained from the database.  This query is used to obtain the key
     * @param keyQuery
     */
    public void setKeyQuery(DatabaseQuery keyQuery) {
        this.keyQuery = keyQuery;
    }

    public void setValueMapping(MapComponentMapping mapping) {
        this.valueMapping = mapping;
    }

    /**
     * INTERNAL:
     * Return whether data for a map key must be included on a Delete datamodification event
     * If the keyMapping is privateOwned, that data should be.
     */
    @Override
    public boolean shouldIncludeKeyInDeleteEvent() {
        return ((DatabaseMapping)keyMapping).isPrivateOwned();
    }


    /**
     * INTERNAL:
     * Certain types of container policies require an extra update statement after a relationship
     * is inserted.  Return whether this update statement is required.
     */
    @Override
    public boolean shouldUpdateForeignKeysPostInsert() {
        return !((DatabaseMapping)keyMapping).isReadOnly();
    }

    /**
     * INTERNAL:
     * Update the joined mapping indices
     * Adds the key mapping and it's index to the list of joined mappings.
     */
    @Override
    public int updateJoinedMappingIndexesForMapKey(Map<DatabaseMapping, Object> indexList, int index){
        indexList.put((DatabaseMapping)keyMapping, index);
        return getAdditionalFieldsForJoin(null).size();
    }

    /**
     * INTERNAL:
     * Allow the key to be unwrapped.  This will be overridden by container policies that
     * allow keys that are entities.
     */
    @Override
    public Object unwrapKey(Object key, AbstractSession session){
        return keyMapping.unwrapKey(key, session);
    }

    /**
     * INTERNAL:
     * This method is used to load a relationship from a list of PKs. This list
     * may be available if the relationship has been cached.
     */
    @Override
    public Object valueFromPKList(Object[] pks, AbstractRecord foreignKeys, ForeignReferenceMapping mapping, AbstractSession session){
        int mapSize = pks.length/2;
        Object result = containerInstance(mapSize);
        Object[] keys = new Object[mapSize];
        Object[] values = new Object[mapSize];
        for (int index = 0; index < pks.length; ++index){
            keys[index/2] = pks[index];
            ++index;
            values[index/2] = pks[index];
        }

        List<Object> keyObjects = keyMapping.createMapComponentsFromSerializableKeyInfo(keys, session);
        if (((DatabaseMapping)valueMapping).isElementCollectionMapping()) {
            for(int i = 0; i < mapSize; i++){
                addInto(keyObjects.get(i), values[i], result, session);
            }
        } else {
            Map<Object, Object> fromCache = session.getIdentityMapAccessorInstance().getAllFromIdentityMapWithEntityPK(values, elementDescriptor);
            List foreignKeyValues = new ArrayList(pks.length - fromCache.size());

            CacheKeyType cacheKeyType = this.elementDescriptor.getCachePolicy().getCacheKeyType();
            for (int index = 0; index < mapSize; ++index){
                Object pk = values[index];
                if (!fromCache.containsKey(pk)){
                    if (cacheKeyType == CacheKeyType.CACHE_ID){
                        foreignKeyValues.add(Arrays.asList(((CacheId)pk).getPrimaryKey()));
                    }else{
                        foreignKeyValues.add(pk);
                    }
                }
            }
            if (!foreignKeyValues.isEmpty()){
                if (foreignKeyValues.size() == pks.length){
                    //need to find all of the entities so just perform a FK search
                    return session.executeQuery(mapping.getSelectionQuery(), foreignKeys);
                }
                ReadAllQuery query = new ReadAllQuery(elementDescriptor.getJavaClass());
                query.setIsExecutionClone(true);
                query.addArgument(ForeignReferenceMapping.QUERY_BATCH_PARAMETER);
                query.setSession(session);
                query.setSelectionCriteria(elementDescriptor.buildBatchCriteriaByPK(query.getExpressionBuilder(), query));
                int pkCount = foreignKeyValues.size();
                Collection<Object> temp = new ArrayList<Object>();
                List arguments = new ArrayList();
                arguments.add(foreignKeyValues);
                if (pkCount > 1000){
                    int index = 0;

                    while ( index+1000 < pkCount ) { // some databases only support ins < 1000 entries
                        List pkList = new ArrayList();
                        pkList.addAll(foreignKeyValues.subList(index, index+1000));
                        arguments.set(0, pkList);
                        query.setArgumentValues(arguments);
                        temp.addAll((Collection<Object>) session.executeQuery(query));
                        index += 1000;
                    }
                    foreignKeyValues = foreignKeyValues.subList(index, pkCount);
                }
                arguments.set(0, foreignKeyValues);
                query.setArgumentValues(arguments);
                //need to put the translation row here or it will be replaced later.
                temp.addAll((Collection<Object>) session.executeQuery(query));
                if (temp.size() < pkCount){
                    //Not enough results have been found, this must be a stale collection with a removed
                    //element.  Execute a reload based on FK.
                    return session.executeQuery(mapping.getSelectionQuery(), foreignKeys);
                }
                for (Object element: temp){
                    Object pk = elementDescriptor.getObjectBuilder().extractPrimaryKeyFromObject(element, session);
                    fromCache.put(pk, element);
                }
            }

            Iterator keyIterator = keyObjects.iterator();
            for(Object key : values){
                addInto(keyIterator.next(), fromCache.get(key), result, session);
            }
        }
        return result;
    }

}
