UnmodifiableCollection.java

/*
 * Copyright (C) 2018 Stein Eldar Johnsen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.morimekta.collect;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;

import static java.util.Objects.requireNonNull;
import static net.morimekta.collect.UnmodifiableList.listOf;

/**
 * Simple unmodifiable collection base class. Note that these are called
 * 'unmodifiable', not 'immutable', as whether they are truly immutable
 * relies on the containing items being immutable classes too. But in
 * that case it <b>is</b> immutable.
 *
 * @param <E> The element type of the collection.
 */
public abstract class UnmodifiableCollection<E> implements Collection<E> {
    /**
     * Get the collection as an unmodifiable collection. This will only instantiate
     * if the incoming collection is not unmodifiable. Convenience mostly to be able
     * to access methods defined here.
     *
     * @param collection The incoming collection.
     * @param <E>        The collection item type.
     * @return The unmodifiable collection.
     */
    public static <E> UnmodifiableCollection<E> unmodifiable(Collection<E> collection) {
        if (collection instanceof UnmodifiableCollection) {
            return (UnmodifiableCollection<E>) collection;
        }
        return UnmodifiableList.asList(collection);
    }

    /**
     * Get the collection as an unmodifiable random access list.
     *
     * @return The unmodifiable list.
     */
    public UnmodifiableList<E> asList() {
        if (asList == null) {
            if (length == 0) {
                asList = listOf();
            } else {
                asList = new UnmodifiableList<>(offset, length, array);
            }
        }
        return asList;
    }

    /**
     * Return a list with the local content appended with the content of the
     * provided collection.
     *
     * @param toBeAppended Collection content to be appended.
     * @return The resulting list.
     */
    public UnmodifiableList<E> append(Collection<? extends E> toBeAppended) {
        if (toBeAppended.isEmpty()) {
            return asList();
        }
        if (isEmpty()) {
            return UnmodifiableList.asList(toBeAppended);
        }
        Object[] other = toBeAppended.toArray();
        Object[] both = new Object[length + other.length];
        System.arraycopy(array, offset, both, 0, length);
        System.arraycopy(other, 0, both, length, other.length);
        return new UnmodifiableList<>(both);
    }

    /**
     * Return a list with the local content appended with the provided
     * items.
     *
     * @param toBeAppended Content items to be appended.
     * @return The resulting list.
     */
    public UnmodifiableList<E> append(E... toBeAppended) {
        checkNotNull(toBeAppended);
        if (toBeAppended.length == 0) {
            return asList();
        }
        if (isEmpty()) {
            return UnmodifiableList.asList(toBeAppended);
        }
        Object[] both = new Object[length + toBeAppended.length];
        System.arraycopy(array, offset, both, 0, length);
        System.arraycopy(toBeAppended, 0, both, length, toBeAppended.length);
        return new UnmodifiableList<>(both);
    }

    /**
     * Return a list with the local content appended to the content of the
     * provided collection.
     *
     * @param item Entry content to be appended.
     * @return The resulting list.
     */
    public UnmodifiableList<E> append(E item) {
        requireNonNull(item, "item == null");
        if (isEmpty()) {
            return listOf(item);
        }
        Object[] both = new Object[length + 1];
        System.arraycopy(array, offset, both, 0, length);
        both[length] = item;
        return new UnmodifiableList<>(both);
    }

    /**
     * Map all values in the collection to some other type.
     *
     * @param mapper The mapper function to use.
     * @param <M>    The mapped to type.
     * @return The list of mapped items.
     */
    public <M> UnmodifiableList<M> map(Function<? super E, M> mapper) {
        if (isEmpty()) {
            return listOf();
        }
        UnmodifiableList.Builder<M> builder = UnmodifiableList.newBuilder(size());
        for (E value : this) {
            builder.add(mapper.apply(value));
        }
        return builder.build();
    }

    /**
     * Filter the collection, but keep the same type and sorting / ordering.
     *
     * @param predicate The predicate to filter by.
     * @return The filtered collection.
     */
    public UnmodifiableCollection<E> filtered(Predicate<? super E> predicate) {
        requireNonNull(predicate, "predicate == null");
        if (isEmpty()) {
            return this;
        }
        Object[] out = new Object[length];
        int outLength = 0;
        for (E value : this) {
            if (predicate.test(value)) {
                out[outLength++] = value;
            }
        }
        if (outLength == length) {
            return this;
        }

        return newInstance(out, outLength);
    }

    /**
     * Make a list with the same items in the reversed order.
     *
     * @return The reversed list.
     */
    public abstract UnmodifiableCollection<E> reversed();

    /**
     * Get the content of the collection sorted using natural order.
     *
     * @return The sorted list.
     */
    @SuppressWarnings("unchecked")
    public UnmodifiableList<E> sorted() {
        if (length == 0) {
            return listOf();
        }
        if (this instanceof UnmodifiableList && length == 1) {
            return (UnmodifiableList<E>) this;
        }
        E[] tmp = (E[]) toArray();
        Arrays.sort(tmp);
        return new UnmodifiableList<>(tmp);
    }

    /**
     * Get the content of the collection sorted by the given comparator.
     *
     * @param comparator Comparator to sort by. If null will sort using natural order.
     * @return The sorted list.
     */
    @SuppressWarnings("unchecked")
    public UnmodifiableList<E> sortedBy(Comparator<? super E> comparator) {
        if (length == 0) {
            return listOf();
        }
        if (this instanceof UnmodifiableList && length == 1) {
            return (UnmodifiableList<E>) this;
        }
        E[] tmp = (E[]) toArray();
        Arrays.sort(tmp, comparator);
        return new UnmodifiableList<>(tmp);
    }

    /**
     * Get the content of the collection as a set ordered by the given comparator.
     *
     * @param comparator Comparator to sort by. If null will sort using natural order.
     * @return The ordered set.
     */
    @SuppressWarnings("unchecked")
    public UnmodifiableSortedSet<E> orderedSetBy(Comparator<? super E> comparator) {
        return UnmodifiableSortedSet.construct((Comparator<Object>) comparator, toArray());
    }

    // -------- Collection --------

    @Override
    public Object[] toArray() {
        Object[] out = new Object[length];
        if (length > 0) {
            System.arraycopy(array, offset, out, 0, length);
        }
        return out;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] ts) {
        Object[] out = (Object[]) Array.newInstance(ts.getClass().getComponentType(), length);
        if (length > 0) {
            System.arraycopy(array, offset, out, 0, length);
        }
        return (T[]) out;
    }

    @Override
    public <T> T[] toArray(IntFunction<T[]> generator) {
        T[] out = generator.apply(length);
        if (length > 0) {
            System.arraycopy(array, offset, out, 0, length);
        }
        return out;
    }

    @Override
    public int size() {
        return length;
    }

    @Override
    public boolean isEmpty() {
        return length == 0;
    }

    @Override
    public boolean contains(Object o) {
        if (o == null) {
            return false;
        }
        for (int i = offset; i < (offset + length); ++i) {
            if (array[i].equals(o)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Iterator<E> iterator() {
        return new IteratorImpl(offset);
    }

    @Override
    public boolean containsAll(Collection<?> collection) {
        if (isEmpty() && !collection.isEmpty()) {
            return false;
        }
        for (Object o : collection) {
            if (!contains(o)) {
                return false;
            }
        }
        return true;
    }

    // -------- Private --------

    static final Object[] EMPTY_ARRAY = new Object[0];

    transient final Object[] array;
    transient final int      length;
    transient final int      offset;
    transient       Integer  hashCode;

    private transient UnmodifiableList<E> asList;

    UnmodifiableCollection(int offset, int length, Object[] array) {
        if (offset < 0 || length < 0) {
            throw new IllegalArgumentException(String.format("offset: %d length: %s", offset, length));
        }
        if (offset + length > array.length) {
            throw new IllegalArgumentException(
                    String.format("offset: %d + length: %d > array: %d",
                                  offset,
                                  length,
                                  array.length));
        }
        this.offset = offset;
        this.length = length;
        this.array = array;
    }

    abstract UnmodifiableCollection<E> newInstance(Object[] array, int length);

    Object[] reversedArray() {
        if (array.length < 2 && length == array.length && offset == 0) {
            return array;
        }
        Object[] out = new Object[length];
        for (int i = 0, o = offset + length - 1; i < length; ++i, --o) {
            out[i] = this.array[o];
        }
        return out;
    }

    // -------- ITERATOR --------

    class IteratorImpl implements ListIterator<E> {
        private int next;

        IteratorImpl(int index) {
            this.next = index;
        }

        @Override
        public boolean hasNext() {
            return next < (offset + length);
        }

        @Override
        @SuppressWarnings("unchecked")
        public E next() {
            if (next >= (offset + length)) {
                throw new IndexOutOfBoundsException("Index " + (next - offset) + " out of bounds for length " + length);
            }
            ++next;
            return (E) array[next - 1];
        }

        @Override
        public boolean hasPrevious() {
            return (next - offset) > 0;
        }

        @Override
        @SuppressWarnings("unchecked")
        public E previous() {
            if ((next - offset - 1) < 0) {
                throw new IndexOutOfBoundsException("Index -1 out of bounds for length " + length);
            }
            --next;
            return (E) array[next];
        }

        @Override
        public int nextIndex() {
            return next - offset;
        }

        @Override
        public int previousIndex() {
            return next - offset - 1;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Operation not allowed");
        }

        @Override
        public void set(E e) {
            throw new UnsupportedOperationException("Operation not allowed");
        }

        @Override
        public void add(E e) {
            throw new UnsupportedOperationException("Operation not allowed");
        }
    }

    // -------- UTILITIES --------

    static void checkNotNull(Object[] array) {
        if (array == null) {
            throw new NullPointerException("null array");
        }
        for (int i = 0; i < array.length; ++i) {
            if (array[i] == null) {
                throw new NullPointerException("null item at pos " + i);
            }
        }
    }

    static void checkNotNull(Collection<?> collection) {
        if (collection == null) {
            throw new NullPointerException("null collection");
        }
        int i = 0;
        for (Object o : collection) {
            if (o == null) {
                throw new NullPointerException("null item at pos " + i);
            }
            ++i;
        }
    }

    // -------- UNSUPPORTED --------

    @Override
    public boolean add(E e) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public boolean remove(Object o) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public boolean addAll(Collection<? extends E> collection) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public boolean removeAll(Collection<?> collection) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public boolean retainAll(Collection<?> collection) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException("Operation not allowed");
    }
}