/*
 * 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:
//     Oracle - initial API and implementation from Oracle TopLink
//     08/15/2008-1.0.1 Chris Delahunt
//       - 237545: List attribute types on OneToMany using @OrderBy does not work with attribute change tracking
package org.eclipse.persistence.internal.queries;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.ListIterator;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.expressions.SQLSelectStatement;
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.helper.IndexedObject;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.ChangeRecord;
import org.eclipse.persistence.internal.sessions.MergeManager;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.internal.sessions.OrderedChangeObject;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.internal.sessions.CollectionChangeRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.annotations.OrderCorrectionType;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.changetracking.CollectionChangeEvent;
import org.eclipse.persistence.indirection.IndirectCollection;
import org.eclipse.persistence.indirection.IndirectList;
import org.eclipse.persistence.mappings.CollectionMapping;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;
import org.eclipse.persistence.queries.DataReadQuery;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadQuery;

/**
 * <p><b>Purpose</b>: A OrderedListContainerPolicy is ContainerPolicy whose
 * container class implements the List interface and is ordered by an @OrderBy.
 * <p>
 * <p><b>Responsibilities</b>:
 * Provide the functionality to operate on an instance of a List.
 *
 * @see ContainerPolicy
 * @see CollectionContainerPolicy
 * @see ListContainerPolicy
 */
public class OrderedListContainerPolicy extends ListContainerPolicy {
    protected static final String NOT_SET = "NOT_SET";
    protected DatabaseField listOrderField;
    protected OrderCorrectionType orderCorrectionType;

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

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

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

    /**
     * INTERNAL:
     * Add a list of elements to container.
     * This is used to add to a collection independent of JDK 1.1 and 1.2.
     * The session may be required to wrap for the wrapper policy.
     * The row may be required by subclasses
     * Return whether the container changed
     */
    @Override
    public boolean addAll(List elements, Object container, AbstractSession session, List<AbstractRecord> dbRows, ObjectBuildingQuery query, CacheKey parentCacheKey, boolean isTargetProtected) {
        if(this.listOrderField == null) {
            return super.addAll(elements, container, session, dbRows, query, parentCacheKey, isTargetProtected);
        } else {
            return addAll(elements, container, session, dbRows, query, parentCacheKey);
        }
    }

    /**
     * INTERNAL:
     * Add a list of elements to container.
     * This is used to add to a collection independent of JDK 1.1 and 1.2.
     * The session may be required to wrap for the wrapper policy.
     * The row may be required by subclasses
     * Return whether the container changed
     */
    @Override
    public boolean addAll(List elements, Object container, AbstractSession session, List<AbstractRecord> dbRows, DataReadQuery query, CacheKey parentCacheKey, boolean isTargetProtected) {
        if(this.listOrderField == null) {
            return super.addAll(elements, container, session, dbRows, query, parentCacheKey, isTargetProtected);
        } else {
            return addAll(elements, container, session, dbRows, query, parentCacheKey);
        }
    }

    protected boolean addAll(List elements, Object container, AbstractSession session, List<AbstractRecord> dbRows, ReadQuery query, CacheKey parentCacheKey) {
        int size = dbRows.size();

        if(this.elementDescriptor != null && this.elementDescriptor.getObjectBuilder().hasWrapperPolicy()) {
            ObjectBuilder objectBuilder = this.elementDescriptor.getObjectBuilder();
            List wrappedElements = new ArrayList(size);
            for(int i=0; i < size; i++) {
                wrappedElements.add(objectBuilder.wrapObject(elements.get(i), session));
            }
            elements = wrappedElements;
        }

        ConversionManager conversionManager = session.getDatasourcePlatform().getConversionManager();

        // populate container with a dummy so the container.set(i, obj) could be used later.
        for(int i=0; i < size; i++) {
            ((List)container).add(NOT_SET);
        }
        // insert the elements into container
        boolean failed = false;
        for(int i=0; i < size; i++) {
            AbstractRecord row = dbRows.get(i);
            Object orderValue = row.get(this.listOrderField);
            // order value is null
            if(orderValue == null) {
                failed = true;
                break;
            }
            int intOrderValue = ((Integer)conversionManager.convertObject(orderValue, Integer.class)).intValue();
            try {
                // one or more elements have the same order value
                if(NOT_SET != ((List)container).set(intOrderValue, elements.get(i))) {
                    failed = true;
                    break;
                }
            } catch(IndexOutOfBoundsException indexException) {
                // order value negative or greater/equal to size
                failed = true;
                break;
            }
        }

        if(failed) {
            ((List)container).clear();

            // extract order list - it will be set into exception or used by order correction.
            List<Integer> orderList = new ArrayList(size);
            for(int i=0; i < size; i++) {
                AbstractRecord row = dbRows.get(i);
                Object orderValue = row.get(this.listOrderField);
                if(orderValue == null) {
                    orderList.add(null);
                } else {
                    orderList.add((Integer)conversionManager.convertObject(orderValue, Integer.class));
                }
            }

            if(this.orderCorrectionType == OrderCorrectionType.READ || this.orderCorrectionType == OrderCorrectionType.READ_WRITE) {
                // pair each element with its order index
                List<IndexedObject> indexedElements = new ArrayList(size);
                for(int i=0; i < size; i++) {
                    indexedElements.add(new IndexedObject(orderList.get(i), elements.get(i)));
                }

                // put elements in order and add to container
                ((List)container).addAll(correctOrderList(indexedElements));

                if(this.orderCorrectionType == OrderCorrectionType.READ_WRITE) {
                    // mark IndirectList to have broken order
                    ((IndirectList)container).setIsListOrderBrokenInDb(true);
                }
            } else {
                // this.orderCorrectionType == OrderCorrectionType.EXCEPTION
                throw QueryException.listOrderFieldWrongValue(query, this.listOrderField, orderList);
            }
        }
        return size > 0;
    }

    /**
     * INTERNAL:
     * Add element into a container which implements the List interface.
     * Add at a particular index.
     */
    protected void addIntoAtIndex(Integer index, Object object, Object container, AbstractSession session) {
        if (hasElementDescriptor()) {
            object = getElementDescriptor().getObjectBuilder().wrapObject(object, session);
        }

        try {
            if (index == null || (index.intValue() > sizeFor(container))) {
                // The index can be larger than the size on a merge,
                // so should be added to the end, it may also be null if the
                // index was unknown, such as an event using the add API.
                ((List)container).add(object);
            } else {
                ((List)container).add(index.intValue(), object);
            }
        } catch (ClassCastException ex1) {
            throw QueryException.cannotAddElement(object, container, ex1);
        } catch (IllegalArgumentException ex2) {
            throw QueryException.cannotAddElement(object, container, ex2);
        } catch (UnsupportedOperationException ex3) {
            throw QueryException.cannotAddElement(object, container, ex3);
        }
    }

    /**
     * INTERNAL:
     * This method is used to calculate the differences between two collections.
     * This algorithm is a work in progress. It works great and all, but like
     * anything, you can always make it better.
     */
    @Override
    public void compareCollectionsForChange(Object oldList, Object newList, CollectionChangeRecord changeRecord, AbstractSession session, ClassDescriptor referenceDescriptor) {
        List orderedObjectsToAdd = new ArrayList();
        Map indicesToRemove = new HashMap();
        Map oldListIndexValue = new HashMap();
        IdentityHashMap oldListValueIndex = new IdentityHashMap();
        IdentityHashMap objectsToAdd = new IdentityHashMap();
        Map<Object, Integer> newListValueIndex = new IdentityHashMap();

        // Step 1 - Go through the old list.
        if (oldList != null) {
            ListIterator iterator = (ListIterator)iteratorFor(oldList);

            while (iterator.hasNext()) {
                Integer index = Integer.valueOf(iterator.nextIndex());
                Object value = iterator.next();
                oldListValueIndex.put(value, index);
                oldListIndexValue.put(index, value);
                indicesToRemove.put(index, index);
            }
        }

        // Step 2 - Go though the new list.
        if (newList != null) {
            // Step i - Gather the list info.
            ListIterator iterator = (ListIterator)iteratorFor(newList);
            while (iterator.hasNext()) {
                newListValueIndex.put(iterator.next(), Integer.valueOf(iterator.previousIndex()));
            }

            // Step ii - Go through the new list again.
            int index = 0;
            int offset = 0;
            iterator = (ListIterator)iteratorFor(newList);
            while (iterator.hasNext()) {
                index = iterator.nextIndex();
                Object currentObject = iterator.next();

                // If value is null then nothing can be done with it.
                if (currentObject != null) {
                    if (oldListValueIndex.containsKey(currentObject)) {
                        int oldIndex = ((Integer) oldListValueIndex.get(currentObject)).intValue();
                        oldListValueIndex.remove(currentObject);

                        if (index == oldIndex) {
                            indicesToRemove.remove(Integer.valueOf(oldIndex));
                            offset = 0; // Reset the offset, assume we're back on track.
                        } else if (index == (oldIndex + offset)) {
                            // We're in the right spot according to the offset.
                            indicesToRemove.remove(Integer.valueOf(oldIndex));
                        } else {
                            // Time to be clever and figure out why we're not in the right spot!
                            int movedObjects = 0;
                            int deletedObjects = 0;
                            boolean moved = true;

                            if (oldIndex < index) {
                                ++offset;
                            } else {
                                for (int i = oldIndex - 1; i >= index; i--) {
                                    Object oldObject = oldListIndexValue.get(Integer.valueOf(i));
                                    if (newListValueIndex.containsKey(oldObject)) {
                                        ++movedObjects;
                                    } else {
                                        ++deletedObjects;
                                    }
                                }

                                if (index == ((oldIndex + offset) - deletedObjects)) {
                                    // We fell into place because of deleted objects.
                                    offset = offset - deletedObjects;
                                    moved = false;
                                } else if (movedObjects > 1) {
                                    // Assume we moved down, bumping everyone by one.
                                    ++offset;
                                } else {
                                    // Assume we moved down unless the object that was
                                    // here before is directly beside us.
                                    Object oldObject = oldListIndexValue.get(Integer.valueOf(index));

                                    if (newListValueIndex.containsKey(oldObject)) {
                                        if (((newListValueIndex.get(oldObject)).intValue() - index) > 1) {
                                            moved = false; // Assume the old object moved up.
                                            --offset;
                                        }
                                    }
                                }
                            }

                            if (moved) {
                                // Add ourselves to the ordered add list.
                                orderedObjectsToAdd.add(currentObject);
                            } else {
                                // Take us off the removed list.
                                indicesToRemove.remove(Integer.valueOf(oldIndex));
                            }
                        }
                    } else {
                        ++offset;
                        objectsToAdd.put(currentObject, currentObject);
                        orderedObjectsToAdd.add(currentObject);
                    }
                } else {
                    // If we find nulls we need decrease our offset.
                    offset--;
                }
            }
        }

        // Sort the remove indices that are left and set the data on the collection change
        // record to be processed on the merge.
        List orderedIndicesToRemove = new ArrayList(indicesToRemove.values());
        Collections.sort(orderedIndicesToRemove);
        changeRecord.addAdditionChange(objectsToAdd, this, (UnitOfWorkChangeSet) changeRecord.getOwner().getUOWChangeSet(), session);
        changeRecord.addRemoveChange(oldListValueIndex, this, (UnitOfWorkChangeSet) changeRecord.getOwner().getUOWChangeSet(), session);
        changeRecord.addOrderedAdditionChange(orderedObjectsToAdd, newListValueIndex, (UnitOfWorkChangeSet) changeRecord.getOwner().getUOWChangeSet(), session);
        changeRecord.addOrderedRemoveChange(orderedIndicesToRemove, oldListIndexValue, (UnitOfWorkChangeSet) changeRecord.getOwner().getUOWChangeSet(), session);
    }

    /**
     * PUBLIC:
     * Correct object's order in the list.
     * The method is called only in one case - when the list of order indexes read from db is invalid
     * (contains nulls, duplicates, negative values, values greater/equal list size).
     * Each element of the indexedObjects is a pair of the order index and the corresponding object.
     * The goal of the method is to return back the list of objects (not indexed objects!) in the correct order.
     * The objects should not be altered.
     *
     * The default implementation of the method sorts indexedObjects according to their indexes (null less than any non-null).
     * For example:
     *   indexedObjects = {{2, objectA}, {5, ObjectB}}  returns {objectA, objectB};
     *   indexedObjects = {{2, objectA}, {-1, ObjectB}}  returns {objectB, objectA};
     *   indexedObjects = {{2, objectA}, {null, ObjectB}}  returns {objectB, objectA};
     *
     * This method could be overridden by the user.
     */
    public List correctOrderList(List<IndexedObject> indexedObjects) {
        Collections.sort(indexedObjects);
        int size = indexedObjects.size();
        List objects = new ArrayList(size);
        for(int i=0; i < size; i++) {
            objects.add(indexedObjects.get(i).getObject());
        }
        return objects;
    }

    /**
     * INTERNAL:
     * Used to create an iterator on a the Map object passed to CollectionChangeRecord.addRemoveChange()
     * to access the values to be removed.  In the case of some container policies the values will actually
     * be the keys.
     */
    @Override
    public Iterator getChangeValuesFrom(Map map) {
        return map.keySet().iterator();
    }

    public DatabaseField getListOrderField() {
        return this.listOrderField;
    }

    public void setListOrderField(DatabaseField field) {
        this.listOrderField = field;
    }

    public OrderCorrectionType getOrderCorrectionType() {
        return this.orderCorrectionType;
    }

    public void setOrderCorrectionType(OrderCorrectionType orderCorrectionType) {
        if(this.orderCorrectionType == orderCorrectionType) {
            return;
        }
        if(orderCorrectionType == OrderCorrectionType.READ_WRITE) {
            if(getContainerClass() == null || !IndirectList.class.isAssignableFrom(getContainerClass())) {
                setContainerClass(IndirectList.class);
            }
        }
        this.orderCorrectionType = orderCorrectionType;
    }

    /**
     * INTERNAL:
     * Return an list iterator for the given container.
     */
    @Override
    public Object iteratorFor(Object container) {
        return ((List)container).listIterator();
    }

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

    /**
     * INTERNAL:
     * Merge changes from the source to the target object. Because this is a
     * collection mapping, values are added to or removed from the collection
     * based on the change set.
     * Synchronize if system property is specified. If not, default to clone the
     * target collection. No need to synchronize if the collection is new.
     */
    @Override
    public void mergeChanges(CollectionChangeRecord changeRecord, Object valueOfTarget, boolean shouldMergeCascadeParts, MergeManager mergeManager, AbstractSession targetSession, boolean isSynchronizeOnMerge) {
        if (isSynchronizeOnMerge && !changeRecord.getOwner().isNew()) {
            // Ensure the collection is synchronized while changes are being made,
            // clone also synchronizes on collection (does not have cache key read-lock for indirection).
            // Must synchronize of the real collection as the clone does so.
            Object synchronizedValueOfTarget = valueOfTarget;
            if (valueOfTarget instanceof IndirectCollection) {
                synchronizedValueOfTarget = ((IndirectCollection)valueOfTarget).getDelegateObject();
            }
            synchronized(synchronizedValueOfTarget) {
                mergeChanges(changeRecord, valueOfTarget, shouldMergeCascadeParts, mergeManager, targetSession);
            }
        } else {
            mergeChanges(changeRecord, valueOfTarget, shouldMergeCascadeParts, mergeManager, targetSession);
        }
    }

    /**
     * INTERNAL:
     * Merge changes from the source to the target object. Because this is a
     * collection mapping, values are added to or removed from the collection
     * based on the change set.
     */
    @Override
    protected void mergeChanges(CollectionChangeRecord changeRecord, Object valueOfTarget, boolean shouldMergeCascadeParts, MergeManager mergeManager, AbstractSession targetSession) {
        ObjectChangeSet objectChanges;
        if(changeRecord.orderHasBeenRepaired() && valueOfTarget instanceof IndirectList) {
            ((IndirectList)valueOfTarget).setIsListOrderBrokenInDb(false);
        }
        if (!changeRecord.getOrderedChangeObjectList().isEmpty()) {
            Iterator objects =changeRecord.getOrderedChangeObjectList().iterator();
            while (objects.hasNext()){
                OrderedChangeObject changeObject = (OrderedChangeObject)objects.next();
                objectChanges = changeObject.getChangeSet();
                if (changeObject.getChangeType() == CollectionChangeEvent.REMOVE){
                    boolean objectRemoved = changeRecord.getRemoveObjectList().containsKey(objectChanges);
                    Object objectToRemove = objectChanges.getTargetVersionOfSourceObject(mergeManager, targetSession);

                    //if objectToRemove is null, we can't look it up in the collection.
                    // This should not happen unless identity is lost.
                    if (objectToRemove != null) {
                        Integer index = changeObject.getIndex();
                        if (index!=null){
                            if (objectToRemove.equals(get(index, valueOfTarget, mergeManager.getSession()))) {
                                removeFromAtIndex(index, valueOfTarget);
                            } else {
                                // Object is in the cache, but the collection doesn't have it at the location we expect
                                // Collection is invalid with respect to these changes, so invalidate the parent and abort
                                Object key = changeRecord.getOwner().getId();
                                targetSession.getIdentityMapAccessor().invalidateObject(key, changeRecord.getOwner().getClassType(targetSession));
                                return;
                            }
                        } else {
                            removeFrom(objectToRemove, valueOfTarget, targetSession);
                        }

                        if ((! mergeManager.shouldMergeChangesIntoDistributedCache()) && changeRecord.getMapping().isPrivateOwned()) {
                            // Check that the object was actually removed and not moved.
                            if (objectRemoved) {
                                mergeManager.registerRemovedNewObjectIfRequired(objectChanges.getUnitOfWorkClone());
                            }
                        }
                    }


                } else { //getChangeType == add
                    boolean objectAdded = changeRecord.getAddObjectList().containsKey(objectChanges);
                    Object object = null;
                    // The object was actually added and not moved.
                    if (objectAdded && shouldMergeCascadeParts) {
                        object = mergeCascadeParts(objectChanges, mergeManager, targetSession);
                    }

                    if (object == null) {
                        // Retrieve the object to be added to the collection.
                        object = objectChanges.getTargetVersionOfSourceObject(mergeManager, targetSession);
                    }

                    // Assume at this point the above merge will have created a new
                    // object if required and that the object was actually added and
                    // not moved.
                    if (objectAdded && mergeManager.shouldMergeChangesIntoDistributedCache()) {
                        // Bugs 4458089 & 4454532 - check if collection contains new item before adding
                        // during merge into distributed cache
                        if (! contains(object, valueOfTarget, mergeManager.getSession())) {
                            addIntoAtIndex(changeObject.getIndex(), object, valueOfTarget, mergeManager.getSession());
                        }
                    } else {
                        addIntoAtIndex(changeObject.getIndex(), object, valueOfTarget, targetSession);
                    }
                }
            }
        } else {
            //Deferred change tracking merge behavior
            // Step 1 - iterate over the removed changes and remove them from the container.
            List<Integer> removedIndices = changeRecord.getOrderedRemoveObjectIndices();

            if (removedIndices.isEmpty()) {
                // Check if we have removed objects via a
                // simpleRemoveFromCollectionChangeRecord API call.
                Iterator removedObjects = changeRecord.getRemoveObjectList().keySet().iterator();

                while (removedObjects.hasNext()) {
                    objectChanges = (ObjectChangeSet) removedObjects.next();
                    removeFrom(objectChanges.getOldKey(), objectChanges.getTargetVersionOfSourceObject(mergeManager, targetSession), valueOfTarget, targetSession);
                    registerRemoveNewObjectIfRequired(objectChanges, mergeManager);
                }
            } else {
                for (int i = removedIndices.size() - 1; i >= 0; i--) {
                    Integer index = removedIndices.get(i);
                    objectChanges = (ObjectChangeSet) changeRecord.getOrderedRemoveObject(index);
                    Object objectToRemove = objectChanges.getTargetVersionOfSourceObject(mergeManager, targetSession);
                    if ( (objectToRemove!=null) &&
                                (objectToRemove.equals(get(index, valueOfTarget, mergeManager.getSession()) )) ) {
                        removeFromAtIndex(index, valueOfTarget);
                        // The object was actually removed and not moved.
                        if (changeRecord.getRemoveObjectList().containsKey(objectChanges)) {
                            registerRemoveNewObjectIfRequired(objectChanges, mergeManager);
                        }
                    } else {

                        //Object is either not in the cache, or not at the location we expect
                        // Collection is invalid with respect to these changes, so invalidate the parent and abort
                        Object key = changeRecord.getOwner().getId();
                        targetSession.getIdentityMapAccessor().invalidateObject(key, changeRecord.getOwner().getClassType(targetSession));
                        return;
                    }
                }
            }

            // Step 2 - iterate over the added changes and add them to the container.
            for (ObjectChangeSet addChangeSet : changeRecord.getOrderedAddObjects()) {
                objectChanges =  addChangeSet;
                boolean objectAdded = changeRecord.getAddObjectList().containsKey(objectChanges);
                Object object = null;

                // The object was actually added and not moved.
                if (objectAdded && shouldMergeCascadeParts) {
                    object = mergeCascadeParts(objectChanges, mergeManager, targetSession);
                }

                if (object == null) {
                    // Retrieve the object to be added to the collection.
                    object = objectChanges.getTargetVersionOfSourceObject(mergeManager, targetSession);
                }

                // Assume at this point the above merge will have created a new
                // object if required and that the object was actually added and
                // not moved.
                if (objectAdded && mergeManager.shouldMergeChangesIntoDistributedCache()) {
                    // Bugs 4458089 & 4454532 - check if collection contains new item before adding
                    // during merge into distributed cache
                    if (! contains(object, valueOfTarget, mergeManager.getSession())) {
                        addIntoAtIndex(changeRecord.getOrderedAddObjectIndex(objectChanges), object, valueOfTarget, mergeManager.getSession());
                    }
                } else {
                    addIntoAtIndex(changeRecord.getOrderedAddObjectIndex(objectChanges), object, valueOfTarget, targetSession);
                }
            }
        }
    }

    /**
     * INTERNAL:
     */
    protected void registerRemoveNewObjectIfRequired(ObjectChangeSet objectChanges, MergeManager mergeManager) {
        if (! mergeManager.shouldMergeChangesIntoDistributedCache()) {
            mergeManager.registerRemovedNewObjectIfRequired(objectChanges.getUnitOfWorkClone());
        }
    }

    /**
     * INTERNAL:
     * Remove the element at the specified index.
     */
    protected void removeFromAtIndex(int index, Object container) {
        try {
            ((List) container).remove(index);
        } catch (ClassCastException ex1) {
            throw QueryException.cannotRemoveFromContainer(Integer.valueOf(index), container, this);
        } catch (IllegalArgumentException ex2) {
            throw QueryException.cannotRemoveFromContainer(Integer.valueOf(index), container, this);
        } catch (UnsupportedOperationException ex3) {
            throw QueryException.cannotRemoveFromContainer(Integer.valueOf(index), container, this);
        }
    }

    /**
     * This method is used to bridge the behavior between Attribute Change Tracking and
     * deferred change tracking with respect to adding the same instance multiple times.
     * Each ContainerPolicy type will implement specific behavior for the collection
     * type it is wrapping.  These methods are only valid for collections containing object references
     */
    @Override
    public void recordAddToCollectionInChangeRecord(ObjectChangeSet changeSetToAdd, CollectionChangeRecord collectionChangeRecord){
        OrderedChangeObject orderedChangeObject = new OrderedChangeObject(CollectionChangeEvent.ADD, null, changeSetToAdd);;
        collectionChangeRecord.getOrderedChangeObjectList().add(orderedChangeObject);
    }

    @Override
    public void recordRemoveFromCollectionInChangeRecord(ObjectChangeSet changeSetToRemove, CollectionChangeRecord collectionChangeRecord){
        OrderedChangeObject orderedChangeObject = new OrderedChangeObject(CollectionChangeEvent.REMOVE, null, changeSetToRemove);;
        collectionChangeRecord.getOrderedChangeObjectList().add(orderedChangeObject);
    }

    @Override
    public void recordUpdateToCollectionInChangeRecord(CollectionChangeEvent event, ObjectChangeSet changeSet, CollectionChangeRecord collectionChangeRecord){
        int changeType = event.getChangeType();
        if (changeType == CollectionChangeEvent.ADD) {
            super.recordAddToCollectionInChangeRecord(changeSet, collectionChangeRecord);
        } else if (changeType == CollectionChangeEvent.REMOVE) {
            super.recordRemoveFromCollectionInChangeRecord(changeSet, collectionChangeRecord);
        } else {
            throw ValidationException.wrongCollectionChangeEventType(changeType);
        }

        OrderedChangeObject orderedChangeObject = new OrderedChangeObject(changeType, event.getIndex(), changeSet, event.getNewValue());
        collectionChangeRecord.getOrderedChangeObjectList().add(orderedChangeObject);
    }

    /**
     * INTERNAL:
     * Indicates whether addAll method should be called to add entire collection,
     * or it's possible to call addInto multiple times instead.
     */
    @Override
    public boolean shouldAddAll() {
        return this.listOrderField != null;
    }

    /**
     * INTERNAL:
     * Return any additional fields required by the policy for a fetch join.
     */
    @Override
    public List<DatabaseField> getAdditionalFieldsForJoin(CollectionMapping baseMapping) {
        if (this.listOrderField != null) {
            List<DatabaseField> fields = new ArrayList<DatabaseField>(1);
            fields.add(this.listOrderField);
            return fields;
        }
        return null;
    }

    /**
     * INTERNAL:
     * Add the index field count.
     */
    @Override
    public int updateJoinedMappingIndexesForMapKey(Map<DatabaseMapping, Object> indexList, int index) {
        if (this.listOrderField != null) {
            return 1;
        }
        return 0;
    }

    /**
     * INTERNAL:
     * Update a ChangeRecord to replace the ChangeSet for the old entity with the changeSet for the new Entity.  This is
     * used when an Entity is merged into itself and the Entity reference new or detached entities.
     */
    @Override
    public void updateChangeRecordForSelfMerge(ChangeRecord changeRecord, Object source, Object target, ForeignReferenceMapping mapping, UnitOfWorkChangeSet parentUOWChangeSet, UnitOfWorkImpl unitOfWork){
        ObjectChangeSet sourceSet = parentUOWChangeSet.getCloneToObjectChangeSet().get(source);
        for (OrderedChangeObject changeObject : ((CollectionChangeRecord)changeRecord).getOrderedChangeObjectList()){
            if (changeObject.getChangeSet() == sourceSet){
                changeObject.setChangeSet(((UnitOfWorkChangeSet)unitOfWork.getUnitOfWorkChangeSet()).findOrCreateLocalObjectChangeSet(target, mapping.getReferenceDescriptor(), unitOfWork.isCloneNewObject(target)));
            }
            return;
        }
    }

    /**
     * INTERNAL:
     * Return any tables that will be required when this mapping is used as part of a join query.
     */
    @Override
    public List<DatabaseTable> getAdditionalTablesForJoinQuery() {
        if (this.listOrderField != null) {
            List tables = new ArrayList(1);
            tables.add(this.listOrderField.getTable());
            return tables;
        }
        return null;
    }

    /**
     * INTERNAL:
     * Add the index field to the query.
     */
    @Override
    public void addAdditionalFieldsToQuery(ReadQuery selectionQuery, Expression baseExpression) {
        if (this.listOrderField != null) {
            if (selectionQuery.isObjectLevelReadQuery()) {
                ((ObjectLevelReadQuery)selectionQuery).addAdditionalField(baseExpression.getField(this.listOrderField));
            } else {
                ((SQLSelectStatement)((DataReadQuery)selectionQuery).getSQLStatement()).addField(baseExpression.getField(this.listOrderField));
            }
        }
    }
}
