package org.codefx.libfx.collection.transform;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Abstract superclass to {@link Collection}s which transform another collection.
* <p>
* This class allows null elements. Subclasses might override that by implementing aggressive null checks.
*
* @param <I>
* the inner type, i.e. the type of the elements contained in the wrapped/inner collection
* @param <O>
* the outer type, i.e. the type of elements appearing to be in this collection
*/
abstract class AbstractTransformingCollection<I, O> implements Collection<O> {
// #begin CONSTANTS
/**
* The largest possible (non-power of two) array size.
* <p>
* Note that some collections can contain more entries than fit into {@code int} (see e.g.
* {@link java.util.concurrent.ConcurrentHashMap#mappingCount() mappingCount()}). In that case, the return value of
* {@link #size()} is capped at {@link Integer#MAX_VALUE}, which is ok because it is greater than
* {@code MAX_ARRAY_SIZE} and will throw an error anyways.
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* The message used for {@link OutOfMemoryError}s.
*/
private static final String COLLECTION_TOO_LARGE_ERROR_MESSAGE = "Required array size too large";
// #end CONSTANTS
// #begin IMPLEMENTATION OF 'Collection<O>'
/**
* Indicates whether the specified collection is equivalent to this one. This is the case if it is also an
* {@link AbstractTransformingCollection} and wraps the same {@link #getInnerCollection() innerCollection}.
*
* @param otherCollection
* the {@link Collection} which is compared with this one
* @return true if this and the specified collection are equivalent
*/
protected final boolean isThisCollection(Collection<?> otherCollection) {
if (otherCollection == this)
return true;
if (otherCollection instanceof AbstractTransformingCollection) {
AbstractTransformingCollection<?, ?> otherTransformingCollection =
(AbstractTransformingCollection<?, ?>) otherCollection;
boolean sameInnerCollection = otherTransformingCollection.getInnerCollection() == getInnerCollection();
return sameInnerCollection;
}
return false;
}
// size
@Override
public int size() {
return getInnerCollection().size();
}
@Override
public boolean isEmpty() {
return getInnerCollection().isEmpty();
}
// contains
@Override
public boolean contains(Object object) {
if (isOuterElement(object)) {
@SuppressWarnings("unchecked")
/*
* This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a
* 'ClassCastException' will be thrown which is in accordance with the contract of 'contains'. If
* 'isOuterElement' does its job well (which can be hard due to erasure) this will not happen.
*/
O outerElement = (O) object;
I innerElement = transformToInner(outerElement);
return getInnerCollection().contains(innerElement);
} else
return false;
}
@Override
public boolean containsAll(Collection<?> otherCollection) {
Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null.");
if (isThisCollection(otherCollection))
return true;
return callContainsAllOnInner(otherCollection);
}
/**
* Wraps the specified collection into a transformation and calls {@link Collection#containsAll(Collection)
* containsAll} on the {@link #getInnerCollection() innerCollection}.
* <p>
* Subclasses may chose to use this method if they override {@link #containsAll(Collection)}.
* <p>
* Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this
* collection's outer type {@code O}. Consider using {@link #callContainsOnThis(Collection)}.
*
* @param otherCollection
* the parameter to {@code containsAll}
* @return result of the call to {@code containsAll}
*/
protected final boolean callContainsAllOnInner(Collection<?> otherCollection) {
Collection<I> asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection);
return getInnerCollection().containsAll(asInnerCollection);
}
/**
* Iterates over the specified collection and calls {@link #contains(Object)} (on this collection) for each element.
* <p>
* Subclasses may chose to use this method if they override {@link #containsAll(Collection)}.
* <p>
* Manually iterating over the specified collection and calling {@code this.}{@link #contains(Object)} individually
* might break guarantees or optimizations made by the inner collection. Consider using
* {@link #callContainsAllOnInner(Collection)}.
*
* @param otherCollection
* the collection whose elements are passed to {@code contains}
* @return false if at least one call to {@code contains} returns false; otherwise true
*/
protected final boolean callContainsOnThis(Collection<?> otherCollection) {
for (Object item : otherCollection)
if (!contains(item))
return false;
return true;
}
// add
@Override
public boolean add(O element) {
I innerElement = transformToInner(element);
return getInnerCollection().add(innerElement);
}
@Override
public boolean addAll(Collection<? extends O> otherCollection) {
Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null.");
return callAddAllOnInner(otherCollection);
}
/**
* Wraps the specified collection into a transformation and calls {@link Collection#addAll(Collection) addAll} on
* the {@link #getInnerCollection() innerCollection}.
* <p>
* Subclasses may chose to use this method if they override {@link #addAll(Collection)}.
* <p>
* Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this
* collection's outer type {@code O}. Consider using {@link #callAddOnThis(Collection)}.
*
* @param otherCollection
* the parameter to {@code addAll}
* @return result of the call to {@code addAll}
*/
protected final boolean callAddAllOnInner(Collection<? extends O> otherCollection) {
Collection<I> asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection);
return getInnerCollection().addAll(asInnerCollection);
}
/**
* Iterates over the specified collection and calls {@link #add(Object) add(O)} (on this collection) for each
* element.
* <p>
* Subclasses may chose to use this method if they override {@link #addAll(Collection)}.
* <p>
* Manually iterating over the specified collection and calling {@code this.}{@link #add(Object)} individually might
* break guarantees (e.g. regarding atomicity) or optimizations made by the inner collection. Consider using
* {@link #callAddAllOnInner(Collection)}.
*
* @param otherCollection
* the collection whose elements are passed to {@code add}
* @return true if at least one call to {@code add} returns true; otherwise false
*/
protected final boolean callAddOnThis(Collection<? extends O> otherCollection) {
boolean changed = false;
for (O entry : otherCollection)
changed |= add(entry);
return changed;
}
// remove
@Override
public boolean remove(Object object) {
if (isOuterElement(object)) {
@SuppressWarnings("unchecked")
/*
* This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a
* 'ClassCastException' will be thrown which is in accordance with the contract of 'remove'. If
* 'isOuterElement' does its job well (which can be hard due to erasure) this will not happen.
*/
O outerElement = (O) object;
I innerElement = transformToInner(outerElement);
return getInnerCollection().remove(innerElement);
} else
return false;
}
@Override
public boolean removeIf(Predicate<? super O> filter) {
Objects.requireNonNull(filter, "The argument 'filter' must not be null.");
Predicate<I> innerFilter = innerElement -> {
O outerElement = transformToOuter(innerElement);
return filter.test(outerElement);
};
return getInnerCollection().removeIf(innerFilter);
}
@Override
public boolean removeAll(Collection<?> otherCollection) {
Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null.");
if (isThisCollection(otherCollection))
return clearToRemoveAll();
return callRemoveAllOnInner(otherCollection);
}
/**
* Calls {@link #clear()} to remove all instances from the {@link #getInnerCollection() innerCollection}.
*
* @return true if the call changed the innerCollection
*/
protected final boolean clearToRemoveAll() {
if (size() == 0)
return false;
clear();
return true;
}
/**
* Wraps the specified collection into a transformation and calls {@link Collection#removeAll(Collection) removeAll}
* on the {@link #getInnerCollection() innerCollection}.
* <p>
* Subclasses may chose to use this method if they override {@link #removeAll(Collection)}.
* <p>
* Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this
* collection's outer type {@code O}. Consider using {@link #callRemoveOnThis(Collection)}.
*
* @param otherCollection
* the parameter to {@code removeAll}
* @return result of the call to {@code removeAll}
*/
protected final boolean callRemoveAllOnInner(Collection<?> otherCollection) {
Collection<I> asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection);
return getInnerCollection().removeAll(asInnerCollection);
}
/**
* Iterates over the specified collection and calls {@link #remove(Object)} (on this collection) for each element.
* <p>
* Subclasses may chose to use this method if they override {@link #removeAll(Collection)}.
* <p>
* Manually iterating over the specified collection and calling {@code this.}{@link #remove(Object)} individually
* might break guarantees (e.g. regarding atomicity) or optimizations made by the inner collection. Consider using
* {@link #callRemoveAllOnInner(Collection)}.
*
* @param otherCollection
* the collection whose elements are passed to {@code remove}
* @return true if at least one call to {@code remove} returns true; otherwise false
*/
protected final boolean callRemoveOnThis(Collection<?> otherCollection) {
boolean changed = false;
for (Object entry : otherCollection)
changed |= remove(entry);
return changed;
}
@Override
public boolean retainAll(Collection<?> otherCollection) {
Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null.");
if (isThisCollection(otherCollection))
return false;
return callRetainAllOnInner(otherCollection);
}
/**
* Wraps the specified collection into a transformation and calls {@link Collection#retainAll(Collection) retainAll}
* on the {@link #getInnerCollection() innerCollection}.
* <p>
* Subclasses may choose to use this method if they override {@link #retainAll(Collection)}.
* <p>
* Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this
* collection's outer type {@code O}. Consider using {@link #retainByCallingRemoveOnThis(Collection)}.
*
* @param otherCollection
* the parameter to {@code retainAll}
* @return result of the call to {@code retainAll}
*/
protected final boolean callRetainAllOnInner(Collection<?> otherCollection) {
Collection<I> asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection);
return getInnerCollection().retainAll(asInnerCollection);
}
/**
* Iterates over this collection (i.e. over the outer elements) and removes each element which is not contained in
* the specified collection.
* <p>
* Subclasses may choose to use this method if they override {@link #retainAll(Collection)}.
* <p>
* Manually iterating over this collection and calling {@code this.}{@link #remove(Object)} individually might break
* guarantees (e.g. regarding atomicity) or optimizations made by the inner collection. Consider using
* {@link #callRetainAllOnInner(Collection)}.
*
* @param otherCollection
* the collection whose elements are not removed from this collection
* @return true if at least one element was removed; otherwise false
*/
protected final boolean retainByCallingRemoveOnThis(Collection<?> otherCollection) {
boolean changed = false;
for (Iterator<O> iterator = iterator(); iterator.hasNext();) {
O element = iterator.next();
boolean remove = !otherCollection.contains(element);
if (remove) {
iterator.remove();
changed = true;
}
}
return changed;
}
@Override
public void clear() {
getInnerCollection().clear();
}
// iteration
@Override
public Iterator<O> iterator() {
// use an iterator which immediately forwards all transformation calls to this collection;
// this excludes the 'TransformingIterator' which does some null handling on its own
return new ForwardingTransformingIterator();
}
@Override
public Spliterator<O> spliterator() {
// use a spliterator which immediately forwards all transformation calls to this collection;
// this excludes the 'TransformingSpliterator' which does some null handling on its own
return new ForwardingTransformingSpliterator();
}
// #begin TOARRAY
@Override
public Object[] toArray() {
/*
* Because this collection view might be used on a map which allows concurrent modifications, the method must be
* able to handle the situation where the number of elements changes throughout the its execution. For this
* reason the code is inspired by 'ConcurrentHashMap.CollectionView.toArray'.
*/
Object[] array = createObjectArrayWithMapSize();
int currentElementIndex = 0;
for (O element : this) {
// the map might have grown, in which case a new array has to be allocated
array = provideArrayWithSufficientLength(array, currentElementIndex);
array[currentElementIndex] = element;
currentElementIndex++;
}
// the map might have shrunk or a larger array might have been allocated;
// in both cases the array has to be truncated to the correct length
return truncateArrayToLength(array, currentElementIndex);
}
/**
* Creates an object array with this collection's current {@link #size()}.
*
* @return an empty object array
*/
private Object[] createObjectArrayWithMapSize() {
int size = size();
if (size > MAX_ARRAY_SIZE)
throw new OutOfMemoryError(COLLECTION_TOO_LARGE_ERROR_MESSAGE);
return new Object[size];
}
/**
* Provides an array with at least the specified minimum length. If the specified array has that length, it is
* returned. Otherwise a new array with an unspecified length but sufficient is returned to which the input array's
* elements were copied.
*
* @param <T>
* the component type of the array
* @param array
* the array whose length is tested
* @param minLength
* the minimum length of the required array
* @return an array with at least length {@code minLength}
*/
private static <T> T[] provideArrayWithSufficientLength(T[] array, int minLength) {
boolean arrayHasSufficientLength = minLength < array.length;
if (arrayHasSufficientLength)
return array;
else
return copyToLargerArray(array);
}
/**
* Creates a new array with a length greater than the specified one's and copies all elements to it.
*
* @param <T>
* the component type of the array
* @param array
* the array whose elements are copied
* @return a new array
*/
private static <T> T[] copyToLargerArray(T[] array) {
if (array.length == MAX_ARRAY_SIZE)
throw new OutOfMemoryError(COLLECTION_TOO_LARGE_ERROR_MESSAGE);
int newSize = getIncreasedSize(array.length);
return Arrays.copyOf(array, newSize);
}
/**
* Returns the size for the new array, which is guaranteed to be greater than the specified size.
*
* @param size
* the current size
* @return the new size
*/
private static int getIncreasedSize(int size) {
// bit shifting is used to increase the size by ~ 50 %
boolean sizeWouldBeIncreasedAboveMaximum = size >= MAX_ARRAY_SIZE - (MAX_ARRAY_SIZE >>> 1) - 1;
if (sizeWouldBeIncreasedAboveMaximum)
return MAX_ARRAY_SIZE;
else
return size + (size >>> 1) + 1;
}
/**
* Returns an array with the specified length.If the specified array has the correct length, it is returned,
* otherwise a new array is allocated and the values are copied into it.
*
* @param <T>
* the component type of the array
* @param array
* the array to be truncated
* @param length
* the new array's length
* @return an array with the specified length
*/
private static <T> T[] truncateArrayToLength(T[] array, int length) {
if (array.length == length)
return array;
else
return Arrays.copyOf(array, length);
}
@Override
public <T> T[] toArray(T[] inputArray) {
/*
* Because this collection view might be used on a map which allows concurrent modifications, the method must be
* able to handle the situation where the number of elements changes throughout its execution. For this reason
* the code is inspired by 'ConcurrentHashMap.CollectionView.toArray'.
*/
Objects.requireNonNull(inputArray, "The argument 'inputArray' must not be null.");
T[] array = provideTypedArrayWithMapSize(inputArray);
int currentElementIndex = 0;
for (O element : this) {
// the map might have grown, in which case a new array has to be allocated
array = provideArrayWithSufficientLength(array, currentElementIndex);
@SuppressWarnings("unchecked")
// due to erasure, this cast can never fail, but writing the reference to the array can;
// this would throw a ArrayStoreException which is in accordance with the contract of
// 'Collection.toArray(T[])'
T unsafelyTypedElement = (T) element;
array[currentElementIndex] = unsafelyTypedElement;
currentElementIndex++;
}
// if the original array is still used, it must be terminated with null as per contract of this method;
// otherwise, the array created above might be too large so it has to be truncated
return markOrTruncateArray(inputArray, array, currentElementIndex);
}
/**
* Provides an array of the same type as the specified array which has at least the length of this collection's
* current {@link #size() size}. If the input array is sufficiently long, it is returned; otherwise a new array is
* created.
*
* @param <T>
* the component type of the array
* @param inputArray
* the input array
* @return an array {@code T[]} with length equal to or greater than {@link #size() size}
*/
private <T> T[] provideTypedArrayWithMapSize(T[] inputArray) {
int size = size();
boolean arrayHasSufficientLength = size <= inputArray.length;
if (arrayHasSufficientLength)
return inputArray;
else {
if (size > MAX_ARRAY_SIZE)
throw new OutOfMemoryError(COLLECTION_TOO_LARGE_ERROR_MESSAGE);
@SuppressWarnings("unchecked")
// the array created by 'Array.newInstance' is of the correct type
T[] array = (T[]) Array.newInstance(inputArray.getClass().getComponentType(), size);
return array;
}
}
/**
* The specified {@code array} is prepared to be returned by {@link #toArray(Object[])}.
*
* @param <T>
* the component type of the array
* @param inputArray
* the array which was given to {@link #toArray(Object[])}
* @param array
* the array which contains this collection's elements (might be the same as {@code inputArray})
* @param nrOfElements
* the number of elements in the {@code array}
* @return an array which fulfills the contract of {@link #toArray(Object[])}
*/
private static <T> T[] markOrTruncateArray(T[] inputArray, T[] array, int nrOfElements) {
boolean usingInputArray = array == inputArray;
if (usingInputArray)
return markEndWithNull(array, nrOfElements);
else
return truncateArrayToLength(array, nrOfElements);
}
/**
* Returns the specified array but with a null reference at the specified index if the array's length allows it.
*
* @param <T>
* the component type of the array
* @param array
* the array which might be edited
* @param nullIndex
* the index where a null reference has to inserted
* @return the specified array
*/
private static <T> T[] markEndWithNull(T[] array, int nullIndex) {
if (nullIndex < array.length)
array[nullIndex] = null;
return array;
}
// #end TOARRAY
// #end IMPLEMENTATION OF 'Collection<O>'
// #begin OBJECT
@Override
public abstract boolean equals(Object object);
@Override
public abstract int hashCode();
@Override
public String toString() {
return stream()
.map(Objects::toString)
.collect(Collectors.joining(", ", "[", "]"));
}
// #end OBJECT
// #begin ABSTRACT METHODS
/**
* @return the inner collection wrapped by this transforming collection
*/
protected abstract Collection<I> getInnerCollection();
/**
* Checks whether the specified object might be an inner element.
* <p>
* This method does not have to be exact (which might be impossible due to involved generic types) and might produce
* false positives (but no false negatives).
*
* @param object
* the object to check; may be null
* @return true if the object might be an inner element
*/
protected abstract boolean isInnerElement(Object object);
/**
* Transforms the specified element to an instance of the outer type.
* <p>
* It can not be guaranteed that the specified element is really of the inner type. If not, an exception can be
* thrown.
*
* @param innerElement
* the element to transform; may be null
* @return the transformed element
* @throws ClassCastException
* if the specified element is not of the correct type
*/
protected abstract O transformToOuter(I innerElement) throws ClassCastException;
/**
* Checks whether the specified object might be an outer element.
* <p>
* This method does not have to be exact (which might be impossible due to involved generic types) and might produce
* false positives (but no false negatives).
*
* @param object
* the object to check; may be null
* @return true if the object might be an outer element
*/
protected abstract boolean isOuterElement(Object object);
/**
* Transforms the specified element to an instance of the inner type.
* <p>
* It can not be guaranteed that the specified element is really of the outer type. If not, an exception can be
* thrown.
*
* @param outerElement
* the element to transform; may be null
* @return the transformed element
* @throws ClassCastException
* if the specified element is not of the correct type
*/
protected abstract I transformToInner(O outerElement) throws ClassCastException;
// #end ABSTRACT METHODS
// #begin INNER CLASSES
/**
* A transforming iterator which directly forwards all transformation calls to the abstract methods in this
* collection.
*/
private class ForwardingTransformingIterator extends AbstractTransformingIterator<I, O> {
private final Iterator<I> innerIterator = getInnerCollection().iterator();
@Override
protected Iterator<I> getInnerIterator() {
return innerIterator;
}
@Override
protected O transformToOuter(I innerElement) {
return AbstractTransformingCollection.this.transformToOuter(innerElement);
}
}
/**
* A transforming spliterator which directly forwards all transformation calls to the abstract methods in this
* collection.
*/
private class ForwardingTransformingSpliterator extends AbstractTransformingSpliterator<I, O> {
private final Spliterator<I> innerSpliterator = getInnerCollection().spliterator();
@Override
protected Spliterator<I> getInnerSpliterator() {
return innerSpliterator;
}
@Override
protected O transformToOuter(I innerElement) {
return AbstractTransformingCollection.this.transformToOuter(innerElement);
}
@Override
protected I transformToInner(O outerElement) {
return AbstractTransformingCollection.this.transformToInner(outerElement);
}
@Override
protected Spliterator<O> wrapNewSpliterator(Spliterator<I> newSpliterator) {
return new ForwardingTransformingSpliterator();
}
}
/**
* Wraps a collection with any element type {@code E} into a transforming collection with the inner type {@code I}.
* <p>
* This works under the assumption that {@code E = O}. Of course, it is highly unsafe and can lead to
* {@link ClassCastException}s when this is not the case and the collection is accessed.
*
* @param <E>
* the type of elements in the specified collection
*/
protected final class TransformToReadOnlyInnerCollection<E> extends AbstractReadOnlyTransformingCollection<E, I> {
private final Collection<E> transformedCollection;
/**
* Creates a new read only collection which transforms the specified one to the inner type
*
* @param transformedCollection
* the collection to transform
*/
public TransformToReadOnlyInnerCollection(Collection<E> transformedCollection) {
assert transformedCollection != null : "The argument 'innerCollection' must not be null.";
this.transformedCollection = transformedCollection;
}
@Override
protected Collection<E> getInnerCollection() {
return transformedCollection;
}
@Override
protected boolean isInnerElement(Object object) {
return AbstractTransformingCollection.this.isOuterElement(object);
}
@Override
protected I transformToOuter(E innerElement) {
@SuppressWarnings("unchecked")
/*
* This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a
* 'ClassCastException' will be thrown which is in accordance with the contract of the method which created
* this wrapper.
*/
O asClientOuterElement = (O) innerElement;
return AbstractTransformingCollection.this.transformToInner(asClientOuterElement);
}
@Override
protected boolean isOuterElement(Object object) {
return AbstractTransformingCollection.this.isInnerElement(object);
}
@Override
protected E transformToInner(I outerElement) {
O transformedToClientOuterElement = AbstractTransformingCollection.this.transformToOuter(outerElement);
@SuppressWarnings("unchecked")
/*
* This cast can not fail due to erasure but whatever happens next might. In that case a
* 'ClassCastException' will be thrown which is in accordance with the contract of the method which created
* this wrapper.
*/
E asThisInnerElement = (E) transformedToClientOuterElement;
return asThisInnerElement;
}
@Override
public boolean equals(Object object) {
if (object == this)
return true;
if (!(object instanceof Collection))
return false;
Collection<?> other = (Collection<?>) object;
if (isThisCollection(other))
return true;
return other.containsAll(this) && this.containsAll(other);
}
@Override
public int hashCode() {
int hashCode = 1;
for (I clientInnerElement : this)
hashCode = 31 * hashCode + (clientInnerElement == null ? 0 : clientInnerElement.hashCode());
return hashCode;
}
}
// #end INNER CLASSES
}