blob: 74806ebc95c854e53e75a8ee335a8daaa8824165 [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 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
package org.eclipse.persistence.sdo.helper;
import commonj.sdo.DataObject;
import commonj.sdo.Property;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
import java.util.Collection;
import java.util.ListIterator;
import org.eclipse.persistence.sdo.SDOChangeSummary;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.sdo.SDOProperty;
/**
* INTERNAL:
* <p>
* <b>Purpose:</b>
* <ul><li>This class wraps the ArrayList of currentElements that implement the List interface.</li>
* </ul>
* <p>
* <b>Responsibilities:</b>
* <ul>
* <li>Provide access many properties on {@link DataObject dataObject}s</li>
* </ul>
*
* @see org.eclipse.persistence.sdo.SDODataObject
* @since Oracle TopLink 11.1.1.0.0
*/
public class ListWrapper implements List, Serializable, Cloneable {
/*
05/23/06 - Update addAll(int,Collection), retainAll(Collection), set(int, Object),
add(int, Object), remove(int)
05/30/06 - SDO-55: JUnit test case adjustments
07/27/06 - Add pluggable support for BC4J - make fields protected for *SDOList implementations
08/21/06 - Remove null check on overloaded addAll()
08/31/06 - Return instance variables to private state - SDOList has been removed
01/03/07 - add(*) no longer running updateContainment on itself
01/08/07 - add(object) - reverse order of and extract updateContainment call
extract out common removeContainment from remove() - coverage from 97.5-99.4
01/31/07 - add undo support by maintaining 2 internal lists
02/04/07 - add undoChanges() to reset original elements
02/08/07 - removed equals and hashCode methods as they seem to cause unexplained issues when we started using Listwrapper as a key in a map
02/21/07 - #5895047: pass the recursive removeContainment flag fromDelete into remove() when called from detach()
This will invoke creation of an intact list copy before removing its containment and clearing its changeSummary
04/16/07 - implement sequences
04/25/07 - adding duplicate containment true dataObjects are ignored - only a single instance can exist
*/
protected SDODataObject dataObject;
protected SDOProperty property;
/**
* We are maintaining two pointers to potentially two ArrayList objects.
* To implement ChangeSummary undo we require a copy of the original state of our model
* - with special handling for ListWrapper to maintain object identity of the list
* The List (originalElements) on ChangeSummary will maintain the current state of our model after logged changes.
* The List (currentElements) will be a progressively deeper distinct shallow copy of the current list as it changes
*/
protected List currentElements;
/**
* INTERNAL:
* @return
*/
private List getEmptyList() {
// ArrayList will be our implementation of List for now - consolidate new calls here
return new ArrayList();
}
public ListWrapper() {
currentElements = getEmptyList();
}
public ListWrapper(SDODataObject theDataObject, Property theProperty) {
this();
dataObject = theDataObject;
property = (SDOProperty) theProperty;
}
/**
* Constructor for non-default Pluggable ValueStore implementations<br>
* Prerequisites: Containment is already set on theList parameter.
* Do not use this constructor for default implementations as containment is not updated.
* The SDO Objects inside this ListWrapper are special case wrappers with no previous containment
* We do not call updateContainment on the SDO Wrapper objects surrounding the POJO's
* otherwise the containment of this list will be removed in the embedded detach() call
* TestCase: the first get on a list.
*/
public ListWrapper(SDODataObject theDataObject, Property theProperty, List theList) {
this(theDataObject, theProperty);
currentElements = theList;
}
/**
*
*/
@Override
public boolean add(Object item) {
return add(item, true);
}
/**
* INTERNAL:
* @param item
* @param updateSequence
* @return
*/
public boolean add(Object item, boolean updateSequence) {
// Not allowed to add null if the property is non-nullable
if (item == null && (property != null && !property.isNullable())) {
throw new UnsupportedOperationException("Property [" + property.getName() + "] is non-nullable");
}
// update element arrays before we modify original object
copyElements();
boolean result = currentElements.add(item);
// update containment
updateContainment(item, updateSequence);
// update opposite property
if (property != null && item != null) {
Property oppositeProp = property.getOpposite();
if (oppositeProp != null) {
((DataObject) item).set(oppositeProp, dataObject);
}
}
return result;
}
/**
* Inserts the specified element at the index position in this list.<br>
* @param index (start at 0 = prepend, length = append)
* @param item
*/
@Override
public void add(int index, Object item) {
add(index, item, true);
}
/**
* INTERNAL:
* @param index
* @param item
* @param updateSequence
*/
public void add(int index, Object item, boolean updateSequence) {
// Not allowed to add null if the property is non-nullable
if (item == null && (property != null && !property.isNullable())) {
throw new UnsupportedOperationException("Property [" + property.getName() + "] is non-nullable");
}
// see testLitWrapperAddMaintainsContainment()
// fail-fast range checking
if ((index < 0) || (index > size())) {
return;
}
// update element arrays before we modify original object
copyElements();
// delegate to superclass
currentElements.add(index, item);
// update containment
updateContainment(item, updateSequence);
// update opposite property
if (property != null && item != null) {
Property oppositeProp = property.getOpposite();
if (oppositeProp != null) {
((DataObject) item).set(oppositeProp, dataObject);
dataObject.set(oppositeProp, null);
}
}
}
/**
* INTERNAL:
*/
protected boolean isLogging() {
return ((dataObject != null) && (dataObject.getChangeSummary() != null) && dataObject.getChangeSummary().isLogging());
}
/**
* INTERNAL:
* Shallow copy elements
*
*/
protected void copyElements() {
// update element arrays before we modify original object
if (isLogging() && (!dataObject.getChangeSummary().isDirty(this))) {
// original will maintain object identity - swap before copying
dataObject.getChangeSummary().getOriginalElements().put(this, currentElements);
// current list will now be a shallow copy of original(itself) - we will not use ArrayList.clone()
currentElements = new ArrayList(currentElements);
}
}
/**
* INTERNAL:
* Undo any changes and return the original List
*/
public void undoChanges(SDOChangeSummary cs) {
// ignore logging state
if (null == cs) {
return;
}
if (cs.isDirty(this)) {
// swap elements, discard current state
currentElements = (List)cs.getOriginalElements().get(this);
cs.getOriginalElements().remove(this);
}
}
/**
* INTERNAL:
* Iterate the collection and add settings where appropriate.
* @param aProperty
* @param items
* @param updateSequence
*/
protected void updateSequence(Property aProperty, Collection items, boolean updateSequence) {
if (updateSequence) {
Iterator valuesIter = items.iterator();
ArrayList duplicatesList = new ArrayList();// <DataObject>
while (valuesIter.hasNext()) {
Object next = valuesIter.next();
// do not add duplicate settings for containment dataObjects
if ((null != aProperty) && !((SDOProperty)aProperty).getType().isDataType() && aProperty.isContainment()) {// dataType and containment are mutually exclusive
if (!duplicatesList.contains(next)) {
updateSequenceSettingInternal(property, next);
duplicatesList.add(next);
}
} else {
updateSequenceSettingInternal(property, next);
}
}
}
}
/**
* INTERNAL:
* Update the sequence by appending an new setting containing this item object
* @param item
*/
private void updateSequenceSettingInternal(Property aProperty, Object item) {
// create a new setting
// Note: A non spec isSequenced=true after type define will throw a NPE
if (dataObject.getType().isSequenced()) {
dataObject.getSequence().addSettingWithoutModifyingDataObject(property, item);
}
}
/**
* INTERNAL:
* Update the sequence by removing the setting containing this item object
* @param item
*/
private void removeSequenceSettingInternal(int occurrence, Property aProperty, Object item) {
// get index corresponding to the property:value pair
// Note: A non spec isSequenced=true after type define will throw a NPE
if (dataObject.getType().isSequenced()) {
dataObject.getSequence().removeSettingWithoutModifyingDataObject(property, item);
}
}
/**
* INTERNAL:
* @param item
* @param updateSequence
*/
protected void updateContainment(Object item, boolean updateSequence) {
if ((property != null) && property.isContainment() && item instanceof SDODataObject) {
dataObject.updateContainment(property, (SDODataObject)item);
} else {
if(dataObject != null) {
dataObject._setModified(true);
}
}
// update sequence for containment and non-containment objects
if ((property != null) && updateSequence) {
updateSequenceSettingInternal(property, item);
}
}
protected void updateContainment(Collection items, boolean updateSequence) {
if ((property != null) && property.isContainment()) {
dataObject.updateContainment(property, items, updateSequence);
} else {
if(dataObject != null) {
dataObject._setModified(true);
}
}
}
/**
* INTERNAL:
* @param item
* @param fromDelete
* @param updateSequence
*/
protected void removeContainment(Object item, boolean fromDelete, boolean updateSequence) {
/**
* Case multiple occurrences of a single item - which to remove?
* [0] ItemImpl (id=58) <- this one
* [1] ItemImpl (id=35)
* [2] ItemImpl (id=35)
*/
// default to removing the first occurrence of item (as in the List interface)
removeContainment(0, item, fromDelete, updateSequence);
}
/**
* INTERNAL:
* @param item
* @param fromDelete
* @param updateSequence
*/
protected void removeContainment(int occurrence, Object item, boolean fromDelete, boolean updateSequence) {
if ((property != null) && property.isContainment() && (item != null)) {
// passing a false fromDelete flag will not remove containment
((SDODataObject)item).detachOrDelete(fromDelete);
} else {
dataObject._setModified(true);
}
if ((property != null) && dataObject.getType().isSequenced() && updateSequence) {
removeSequenceSettingInternal(occurrence, property, item);
}
}
/**
* INTERNAL:
* Remove the item or first occurrence of the item.
* @param item
* @param fromDelete
* @param updateSequence
* @return
*/
public boolean remove(Object item, boolean fromDelete, boolean updateSequence) {
// update element arrays before we modify original object
copyElements();
// pass the remove containment (fromDelete) flag back to the recursive delete/detach call to dataObject
// fromDelete will always be false when called within ListWrapper
removeContainment(item, fromDelete, updateSequence);
// remove the first occurrence of any duplicates
return currentElements.remove(item);
}
/**
* @param item
* @return
*/
@Override
public boolean remove(Object item) {
// remove item without removing containment
return remove(item, false, true);
}
/**
* Appends all of the currentElements in the specified Collection to the end of this list,
* in the order that they are returned by the specified Collection's Iterator.
* The behavior of this operation is undefined if the specified Collection is modified
* while the operation is in progress. (This implies that the behavior of this call is undefined
* if the specified Collection is this list, and this list is nonempty.)<br>
* This operation is a special case of the general addAll(int, Collection).<br>
*
* From the SDO Specification: p18
* The getList(property) accessor is especially convenient for many-valued properties.
* If property.many is true then set(property, value) and setList(property, value)
* require that [value] be a java.util.Collection and List respectively.
* These methods are equivalent to getList(property).clear() followed by getList(property).addAll(value).
*
* @param items
* @return boolean
*/
@Override
public boolean addAll(Collection items) {
// Not allowed to add null if the property is non-nullable
if (items.contains(null) && (property != null && !property.isNullable())) {
throw new UnsupportedOperationException("Property [" + property.getName() + "] is non-nullable");
}
return addAll(items, true);
}
/**
* INTERNAL:
* Appends all of the currentElements in the specified Collection to the end of this list,
* in the order that they are returned by the specified Collection's Iterator.
* This function calls the public addAll(Collection) with a sequence state flag.
* @param items
* @param updateSequence
* @return
*/
public boolean addAll(Collection items, boolean updateSequence) {
// update element arrays before we modify original object
copyElements();
/**
* The order of operations for add is
* 1 - add elements
* 2 - update containment
*
* This order is the reverse of the order in remove
* 1 - remove containment
* 2 - remove elements
*/
// add new elements before we have updated containment on the items - duplicates will be removed
boolean modified = currentElements.addAll(items);
// non-default Pluggable implementations do not require updateContainment
dataObject._getCurrentValueStore().setManyProperty(property, this);
/**
* Corner case: Duplicate DataObjects
* The effect of updateContainment on duplicates will be the removal of all but one of the duplicates
* in the items collection that was added.
* For sequences we must remove duplicates from items to match updateContainment for containment dataObjects
*/
updateContainment(items, updateSequence);
// update opposite property
if (property != null) {
Property oppositeProp = property.getOpposite();
if (oppositeProp != null) {
Iterator itemsIterator = items.iterator();
while(itemsIterator.hasNext()) {
Object item = itemsIterator.next();
if (item != null) {
((DataObject) item).set(oppositeProp, dataObject);
}
}
}
}
// create new settings outside of updateContainment as we do earlier in currentElements.add
updateSequence(property, items, updateSequence);
return modified;
}
/**
* Inserts all of the currentElements in the specified Collection into this list, starting at the
* specified position. Shifts the element currently at that position (if any)
* and any subsequent currentElements to the right (increases their indices).
* The new currentElements will appear in the list in the order that they are returned by the
* specified Collection's iterator.<br>
* @param position (start at 0 = prepend, length = append)
* @param items
* @return boolean
*/
@Override
public boolean addAll(int position, Collection items) {
return addAll(position, items, true);
}
public boolean addAll(int position, Collection items, boolean updateSequence) {
// fail-fast range checking
if ((position < 0) || (position > size())) {
return false;
}
if ((items == null) || (items.size() == 0)) {
return false;
}
// Not allowed to add null if the property is non-nullable
if (items.contains(null) && (property != null && !property.isNullable())) {
throw new UnsupportedOperationException("Property [" + property.getName() + "] is non-nullable");
}
// delegate to superclass
// update element arrays before we modify original object
copyElements();
/**
* The order of operations for add is
* 1 - add elements
* 2 - update containment
*
* This order is the reverse of the order in remove
* 1 - remove containment
* 2 - remove elements
*/
boolean modified = currentElements.addAll(position, items);
/**
* Corner case: Duplicate DataObjects
* The effect of updateContainment on duplicates will be the removal of all but one of the duplicates
* in the items collection that was added.
* For sequences we must remove duplicates from items to match updateContainment for containment dataObjects
*/
// update containment
updateContainment(items, updateSequence);
// update opposite property
if (property != null) {
Property oppositeProp = property.getOpposite();
if (oppositeProp != null) {
Iterator itemsIterator = items.iterator();
while(itemsIterator.hasNext()) {
Object item = itemsIterator.next();
if (item != null) {
((DataObject) item).set(oppositeProp, dataObject);
dataObject.set(oppositeProp, null);
}
}
}
}
// create new settings outside of updateContainment as we do earlier in currentElements.add
updateSequence(property, items, updateSequence);
return modified;
}
/**
* Removes from this collection all of its currentElements that are contained in the specified collection.<br>
* @param items
* @return boolean
*/
@Override
public boolean removeAll(Collection items) {
return removeAll(items, true);
}
/**
* INTERNAL:
* Removes from this collection all of its currentElements that are contained in the specified collection.<br>
* @param items
* @param updateSequence
* @return
*/
public boolean removeAll(Collection items, boolean updateSequence) {
Iterator iter = items.iterator();
boolean modified = false;
while (iter.hasNext()) {
Object next = iter.next();
remove(next, false, updateSequence);
modified = true;
}
return modified;
}
/**
* Retains only the currentElements in this collection that are contained in the specified collection
* (optional operation).<br>
* In other words, removes from this collection all of its currentElements that
* are not contained in the specified collection.<br>
* @param itemsToKeep
* @return boolean
*/
@Override
public boolean retainAll(Collection itemsToKeep) {
// fail-fast range checking
if (itemsToKeep == null) {
return false;
}
if (itemsToKeep.size() == 0) {
clear();
return true;
}
boolean modified = false;
// iterate across the full collection and remove only non-(itemsToKeep)
// don't use an Iterator when modifying the list or we will
// get a ConcurrentModificationException on the Iterator.
int originalSize = size();// store: as size will reduce as we iterate
int index = 0;
// iterate all currentElements and update position only on skipped currentElements
for (int original = 0; original < originalSize; original++) {
// get object at current index
Object anObject = get(index);
// is this element in the keep list?
if (!itemsToKeep.contains(anObject)) {
remove(anObject);
modified = true;
} else {
index++;
}
}
// no delegation to superclass required
return modified;
}
/**
* Removes all of the currentElements from this list. The list will be empty after this call returns.
*/
@Override
public void clear() {
clear(true);
}
/**
* INTERNAL:
* @param updateSequence
*/
public void clear(boolean updateSequence) {
// update element arrays before we modify original object
int size = this.size();
for (int i = size - 1; i >= 0; i--) {
remove(i, updateSequence);
}
}
/**
* Replaces the element at the specified index in this list with the specified element.<br>
* @param index
* @param item
* @return Object (the element previously at the specified position)
*/
@Override
public Object set(int index, Object item) {
// fail-fast range checking
if ((index < 0) || (index > size())) {
throw new IndexOutOfBoundsException("index " + index + " is out of bounds.");
}
// delegate removal
Object aPreviousObject = remove(index);
// delegate insertion
add(index, item);
return aPreviousObject;
}
/**
* INTERNAL:
* Removes the element at the specified position in this list.<br>
* Position index starts at 0.
* @param index
* @param updateSequence
* @return Object (the element previously at the specified position)
*/
public Object remove(int index, boolean updateSequence) {
// fail-fast range checking
if ((index < 0) || (index >= size())) {
return null;
}
// update element arrays before we modify original object
copyElements();
/**
* The order of operations for remove is
* 1 - remove containment
* 2 - remove elements
*
* This order is the reverse of the order in addAll
* 1 - add elements
* 2 - update containment
*/
// Update containment of object and container, do not recursively remove containment
int occurrence = getOccurrenceIndex(index);
removeContainment(occurrence, currentElements.get(index), false, updateSequence);
// delegate to superclass
return currentElements.remove(index);
}
/**
* INTERNAL:
* Return the occurrence number for the current index in this list.
* The return value will be non-zero when there are duplicates of an object instance.
* <p>Example: occurrence number of index 1 = 0, of index 2 = 1</p>
* <p>[index] occurrence</p>
* <p>[0] ItemImpl (id=58) 0</p>
* <p>[1] ItemImpl (id=35) 0</p>
* <p>[2] ItemImpl (id=35) 1</p>
*
* @param index
* @return
*/
private int getOccurrenceIndex(int index) {
// Assumption: no occurrences: return 0
// Assumption: only 1 occurrence: return 0
int occurrence = 0;
boolean skipFirstOccurrence = true;
Object targetObjectAtIndex = currentElements.get(index);
for (int position = 0, size = size(); (position < size) && (position < (index + 1));
position++) {
Object searchIndexObject = currentElements.get(position);
// match only objects that are duplicates of this current one
if (targetObjectAtIndex == searchIndexObject) {
// skip counting the first occurrence
if (skipFirstOccurrence) {
skipFirstOccurrence = false;
} else {
occurrence++;
}
}
}
return occurrence;
}
/**
* Removes the element at the specified position in this list.<br>
* Position index starts at 0.
* @param index
* @return Object (the element previously at the specified position)
*/
@Override
public Object remove(int index) {
return remove(index, true);
}
@Override
public ListIterator listIterator() {
return currentElements.listIterator();
}
@Override
public ListIterator listIterator(int position) {
return currentElements.listIterator(position);
}
/**
* Return a view of the specified portion of the list
* @param start - low endpoint (inclusive) of the subList.
* @param end - high endpoint (exclusive) of the subList.
* @return
*/
@Override
public List subList(int start, int end) {
return currentElements.subList(start, end);
}
@Override
public Object[] toArray() {
return currentElements.toArray();
}
/**
* Returns an array containing all of the currentElements in this list in proper sequence;
* the runtime type of the returned array is that of the specified array.
* Obeys the general contract of the Collection.toArray(Object[]) method.<br>
* Specified by: toArray in interface {@literal Collection<E>}<br>
*
* Throws:<br>
* ArrayStoreException - if the runtime type of the specified array is not a supertype of the
* runtime type of every element in this list.<br>
* NullPointerException - if the specified array is null.<br>
*
* @param items -the array into which the currentElements of this list are to be stored, if it is big enough;
* otherwise, a new array of the same runtime type is allocated for this purpose.<br>
* @return Object[] - an array containing the currentElements of this list.
*/
@Override
public Object[] toArray(Object[] items) {
return currentElements.toArray(items);
}
@Override
public int size() {
return currentElements.size();
}
@Override
public boolean isEmpty() {
return currentElements.isEmpty();
}
@Override
public boolean contains(Object item) {
return currentElements.contains(item);
}
@Override
public boolean containsAll(Collection items) {
return currentElements.containsAll(items);
}
@Override
public Iterator iterator() {
return currentElements.iterator();
}
@Override
public int indexOf(Object item) {
return currentElements.indexOf(item);
}
@Override
public int lastIndexOf(Object item) {
return currentElements.lastIndexOf(item);
}
@Override
public Object get(int position) {
try {
return currentElements.get(position);
} catch (Exception e) {
// Return null in case of exception, as per SDO 2.1 Spec
return null;
}
}
/**
* INTERNAL:
* Defined in SDO 2.01 spec on page 65 Externalizable function is called by
* ObjectStream.writeObject() A replacement object for serialization can be
* called here.
* <p>Security Note:
* This public function exposes a data replacement vulnerability where an outside client
* can gain access and modify their non-final constants.
* We may need to wrap the GZIP streams in some sort of encryption when we are not
* using HTTPS or SSL/TLS on the wire.
*
* @see org.eclipse.persistence.sdo.SDOResolvable
*/
public Object writeReplace() {
return currentElements;
}
/**
* INTERNAL:
* @return
*/
public List getCurrentElements() {
return currentElements;
}
/**
* INTERNAL:
* bypass containment and changesummary copy of element list on modifications
*
*/
public void setCurrentElements(List currentElementsList) {
currentElements = currentElementsList;
}
/**
* Clone the ListWrapper.
* This creates a new ListWrapper with the same contents as the original (shallow clone)
* Minimal clone operation implemented to support usage in JPA
*/
@Override
public Object clone() {
ListWrapper listWrapperClone = new ListWrapper(dataObject, property);
listWrapperClone.setCurrentElements(new ArrayList(currentElements));
return listWrapperClone;
}
}