/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 groovy.lang; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; /** * A wrapper for {@link List} which automatically grows the list when either {@link #get(int)} or * {@link #getAt(int)} is called with an index greater than or equal to {@code size()}. * * @author Andre Steingress * @since 1.8.7 */ public final class ListWithDefault<T> implements List<T> { private final List<T> delegate; private final boolean lazyDefaultValues; private final Closure initClosure; private ListWithDefault(List<T> items, boolean lazyDefaultValues, Closure initClosure) { this.delegate = items; this.lazyDefaultValues = lazyDefaultValues; this.initClosure = initClosure; } public List<T> getDelegate() { return delegate != null ? new ArrayList<T>(delegate) : null; } public boolean isLazyDefaultValues() { return lazyDefaultValues; } public Closure getInitClosure() { return initClosure != null ? (Closure) initClosure.clone() : null; } public static <T> ListWithDefault<T> newInstance(List<T> items, boolean lazyDefaultValues, Closure initClosure) { if (items == null) throw new IllegalArgumentException("Parameter \"items\" must not be null"); if (initClosure == null) throw new IllegalArgumentException("Parameter \"initClosure\" must not be null"); return new ListWithDefault<T>(new ArrayList<T>(items), lazyDefaultValues, (Closure) initClosure.clone()); } public int size() { return delegate.size(); } public boolean isEmpty() { return delegate.isEmpty(); } public boolean contains(Object o) { return delegate.contains(o); } public Iterator<T> iterator() { return delegate.iterator(); } public Object[] toArray() { return delegate.toArray(); } public <T> T[] toArray(T[] ts) { return delegate.toArray(ts); } public boolean add(T t) { return delegate.add(t); } public boolean remove(Object o) { return delegate.remove(o); } public boolean containsAll(Collection<?> objects) { return delegate.containsAll(objects); } public boolean addAll(Collection<? extends T> ts) { return delegate.addAll(ts); } public boolean addAll(int i, Collection<? extends T> ts) { return delegate.addAll(i, ts); } public boolean removeAll(Collection<?> objects) { return delegate.removeAll(objects); } public boolean retainAll(Collection<?> objects) { return delegate.retainAll(objects); } public void clear() { delegate.clear(); } /** * Overwrites subscript operator handling by redirecting to {@link #get(int)}. * * @param index an index (might be greater or equal to {@code size()}, or smaller than 0) * @return the value at the given {@code index} or the default value */ public T getAt(int index) { return get(index); } /** * Returns the element at the given index but grows the list if needed. If the requested {@code index} is * greater than or equal to {@code size()}, the list will grow to the new size and a default value calculated * using the <code>initClosure</code> will be used to populate the missing value and returned. * <p> * If <code>lazyDefaultValues</code> is <code>true</code> any gaps when growing the list are filled * with nulls. Subsequent attempts to retrieve items from the list from those gap index values * will, upon finding null, call the <code>initClosure</code> to populate the list for the * given list value. Hence, when in this mode, nulls cannot be stored in this list. * If <code>lazyDefaultValues</code> is <code>false</code> any gaps when growing the list are filled * eagerly by calling the <code>initClosure</code> for all gap indexes during list growth. * No calls to <code>initClosure</code> are made except during list growth and it is ok to * store null values in the list when in this mode. * <p> * This implementation breaks * the contract of {@link java.util.List#get(int)} as it a) possibly modifies the underlying list and b) does * NOT throw an {@link IndexOutOfBoundsException} when {@code index < 0 || index >= size()}. * * @param index an index (might be greater or equal to {@code size()}, or smaller than 0) * @return the value at the given {@code index} or the default value */ public T get(int index) { final int size = size(); int normalisedIndex = normaliseIndex(index, size); if (normalisedIndex < 0) { throw new IndexOutOfBoundsException("Negative index [" + normalisedIndex + "] too large for list size " + size); } // either index >= size or the normalised index is negative if (normalisedIndex >= size) { // find out the number of gaps to fill with null/the default value final int gapCount = normalisedIndex - size; // fill all gaps for (int i = 0; i < gapCount; i++) { final int idx = size(); // if we lazily create default values, use 'null' as placeholder if (lazyDefaultValues) delegate.add(idx, null); else delegate.add(idx, getDefaultValue(idx)); } // add the first/last element being always the default value final int idx = normalisedIndex; delegate.add(idx, getDefaultValue(idx)); // normalise index again to get positive index normalisedIndex = normaliseIndex(index, size()); } T item = delegate.get(normalisedIndex); if (item == null && lazyDefaultValues) { item = getDefaultValue(normalisedIndex); delegate.set(normalisedIndex, item); } return item; } @SuppressWarnings("unchecked") private T getDefaultValue(int idx) { return (T) initClosure.call(new Object[]{idx}); } private static int normaliseIndex(int index, int size) { if (index < 0) { index += size; } return index; } public T set(int i, T t) { return delegate.set(i, t); } public void add(int i, T t) { delegate.add(i, t); } public T remove(int i) { return delegate.remove(i); } public int indexOf(Object o) { return delegate.indexOf(o); } public int lastIndexOf(Object o) { return delegate.lastIndexOf(o); } public ListIterator<T> listIterator() { return delegate.listIterator(); } public ListIterator<T> listIterator(int i) { return delegate.listIterator(i); } @Override public boolean equals(Object obj) { return delegate.equals(obj); } @Override public int hashCode() { return delegate.hashCode(); } /** * Returns a view of a portion of this list. This method returns a list with the same * lazy list settings as the original list. * * @param fromIndex low endpoint of the subList (inclusive) * @param toIndex upper endpoint of the subList (exclusive) * @return a view of a specified range within this list, keeping all lazy list settings */ public ListWithDefault<T> subList(int fromIndex, int toIndex) { return new ListWithDefault<T>(delegate.subList(fromIndex, toIndex), lazyDefaultValues, (Closure) initClosure.clone()); } }