blob: 958f2ef2ae24f36ede18f5dc1fa1ef01007454ec [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* 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 com.google.gwt.view.client;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
/**
* A concrete subclass of {@link AbstractDataProvider} that is backed by an
* in-memory list.
*
* <p>
* Modifications (inserts, removes, sets, etc.) to the list returned by
* {@link #getList()} will be reflected in the model. However, mutations to the
* items contained within the list will NOT be reflected in the model. You must
* call {@link List#set(int, Object)} to update the item within the list and
* push the change to the display, or call {@link #refresh()} to push all rows
* to the displays. {@link List#set(int, Object)} performs better because it
* allows the data provider to push only those rows which have changed, and
* usually allows the display to re-render only a subset of the rows.
* </p>
*
* <p>
* <h3>Example</h3> {@example
* com.google.gwt.examples.view.ListDataProviderExample}
* </p>
*
* @param <T> the data type of the list
*/
public class ListDataProvider<T> extends AbstractDataProvider<T> {
/**
* A wrapper around a list that updates the model on any change.
*/
private class ListWrapper implements List<T> {
/**
* A wrapped ListIterator.
*/
private final class WrappedListIterator implements ListIterator<T> {
/**
* The error message when {@link #add(Object)} or {@link #remove()} is
* called more than once per call to {@link #next()} or
* {@link #previous()}.
*/
private static final String IMPERMEABLE_EXCEPTION =
"Cannot call add/remove more than once per call to next/previous.";
/**
* The index of the object that will be returned by {@link #next()}.
*/
private int i = 0;
/**
* The index of the last object accessed through {@link #next()} or
* {@link #previous()}.
*/
private int last = -1;
private WrappedListIterator() {
}
private WrappedListIterator(int start) {
int size = ListWrapper.this.size();
if (start < 0 || start > size) {
throw new IndexOutOfBoundsException(
"Index: " + start + ", Size: " + size);
}
i = start;
}
@Override
public void add(T o) {
if (last < 0) {
throw new IllegalStateException(IMPERMEABLE_EXCEPTION);
}
ListWrapper.this.add(i++, o);
last = -1;
}
@Override
public boolean hasNext() {
return i < ListWrapper.this.size();
}
@Override
public boolean hasPrevious() {
return i > 0;
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return ListWrapper.this.get(last = i++);
}
@Override
public int nextIndex() {
return i;
}
@Override
public T previous() {
if (!hasPrevious()) {
throw new NoSuchElementException();
}
return ListWrapper.this.get(last = --i);
}
@Override
public int previousIndex() {
return i - 1;
}
@Override
public void remove() {
if (last < 0) {
throw new IllegalStateException(IMPERMEABLE_EXCEPTION);
}
ListWrapper.this.remove(last);
i = last;
last = -1;
}
@Override
public void set(T o) {
if (last == -1) {
throw new IllegalStateException();
}
ListWrapper.this.set(last, o);
}
}
/**
* The current size of the list.
*/
private int curSize = 0;
/**
* The delegate wrapper.
*/
private final ListWrapper delegate;
/**
* Set to true if the pending flush has been canceled.
*/
private boolean flushCancelled;
/**
* We wait until the end of the current event loop before flushing changes
* so that we don't spam the displays. This also allows users to clear and
* replace all of the data without forcing the display back to page 0.
*/
private ScheduledCommand flushCommand = new ScheduledCommand() {
@Override
public void execute() {
flushPending = false;
if (flushCancelled) {
flushCancelled = false;
return;
}
flushNow();
}
};
/**
* Set to true if a flush is pending.
*/
private boolean flushPending;
/**
* The list that backs the wrapper.
*/
private List<T> list;
/**
* If this is a sublist, the offset it the index relative to the main list.
*/
private final int offset;
/**
* If modified is true, the smallest modified index.
*/
private int maxModified = Integer.MIN_VALUE;
/**
* If modified is true, one past the largest modified index.
*/
private int minModified = Integer.MAX_VALUE;
/**
* True if the list data has been modified.
*/
private boolean modified;
public ListWrapper(List<T> list) {
this(list, null, 0);
// Initialize the data size based on the size of the input list.
updateRowCount(list.size(), true);
}
/**
* Construct a new {@link ListWrapper} that delegates flush calls to the
* specified delegate.
*
* @param list the list to wrap
* @param delegate the delegate
* @param offset the offset of this list
*/
private ListWrapper(List<T> list, ListWrapper delegate, int offset) {
this.list = list;
this.delegate = delegate;
this.offset = offset;
}
@Override
public void add(int index, T element) {
try {
list.add(index, element);
minModified = Math.min(minModified, index);
maxModified = size();
modified = true;
flush();
} catch (IndexOutOfBoundsException e) {
throw new IndexOutOfBoundsException(e.getMessage());
}
}
@Override
public boolean add(T e) {
boolean toRet = list.add(e);
minModified = Math.min(minModified, size() - 1);
maxModified = size();
modified = true;
flush();
return toRet;
}
@Override
public boolean addAll(Collection<? extends T> c) {
minModified = Math.min(minModified, size());
boolean toRet = list.addAll(c);
maxModified = size();
modified = true;
flush();
return toRet;
}
@Override
public boolean addAll(int index, Collection<? extends T> c) {
try {
boolean toRet = list.addAll(index, c);
minModified = Math.min(minModified, index);
maxModified = size();
modified = true;
flush();
return toRet;
} catch (IndexOutOfBoundsException e) {
throw new IndexOutOfBoundsException(e.getMessage());
}
}
@Override
public void clear() {
list.clear();
minModified = maxModified = 0;
modified = true;
flush();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean equals(Object o) {
return list.equals(o);
}
@Override
public T get(int index) {
return list.get(index);
}
@Override
public int hashCode() {
return list.hashCode();
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public Iterator<T> iterator() {
return listIterator();
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<T> listIterator() {
return new WrappedListIterator();
}
@Override
public ListIterator<T> listIterator(int index) {
return new WrappedListIterator(index);
}
@Override
public T remove(int index) {
try {
T toRet = list.remove(index);
minModified = Math.min(minModified, index);
maxModified = size();
modified = true;
flush();
return toRet;
} catch (IndexOutOfBoundsException e) {
throw new IndexOutOfBoundsException(e.getMessage());
}
}
@Override
public boolean remove(Object o) {
int index = indexOf(o);
if (index == -1) {
return false;
}
remove(index);
return true;
}
@Override
public boolean removeAll(Collection<?> c) {
boolean toRet = list.removeAll(c);
minModified = 0;
maxModified = size();
modified = true;
flush();
return toRet;
}
@Override
public boolean retainAll(Collection<?> c) {
boolean toRet = list.retainAll(c);
minModified = 0;
maxModified = size();
modified = true;
flush();
return toRet;
}
@Override
public T set(int index, T element) {
T toRet = list.set(index, element);
minModified = Math.min(minModified, index);
maxModified = Math.max(maxModified, index + 1);
modified = true;
flush();
return toRet;
}
@Override
public int size() {
return list.size();
}
@Override
public List<T> subList(int fromIndex, int toIndex) {
return new ListWrapper(list.subList(fromIndex, toIndex), this, fromIndex);
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <C> C[] toArray(C[] a) {
return list.toArray(a);
}
/**
* Flush the data to the model.
*/
private void flush() {
// Defer to the delegate.
if (delegate != null) {
delegate.minModified = Math.min(
minModified + offset, delegate.minModified);
delegate.maxModified = Math.max(
maxModified + offset, delegate.maxModified);
delegate.modified = modified || delegate.modified;
delegate.flush();
return;
}
flushCancelled = false;
if (!flushPending) {
flushPending = true;
Scheduler.get().scheduleFinally(flushCommand);
}
}
/**
* Flush pending list changes to the displays. By default,
*/
private void flushNow() {
// Cancel any pending flush command.
if (flushPending) {
flushCancelled = true;
}
// Early exit if this list has been replaced in the data provider.
if (listWrapper != this) {
return;
}
int newSize = list.size();
if (curSize != newSize) {
curSize = newSize;
updateRowCount(curSize, true);
}
if (modified) {
updateRowData(minModified, list.subList(minModified, maxModified));
modified = false;
}
minModified = Integer.MAX_VALUE;
maxModified = Integer.MIN_VALUE;
}
}
/**
* The wrapper around the actual list.
*/
private ListWrapper listWrapper;
/**
* Creates an empty model.
*/
public ListDataProvider() {
this(new ArrayList<T>(), null);
}
/**
* Creates a list model that wraps the given list.
*
* <p>
* The wrapped list should no longer be modified as the data provider cannot
* detect changes to the wrapped list. Instead, call {@link #getList()} to
* retrieve a wrapper that can be modified and will correctly forward changes
* to displays.
*
* @param listToWrap the List to be wrapped
*/
public ListDataProvider(List<T> listToWrap) {
this(listToWrap, null);
}
/**
* Creates an empty list model that wraps the given collection.
*
* @param keyProvider an instance of ProvidesKey<T>, or null if the record
* object should act as its own key
*/
public ListDataProvider(ProvidesKey<T> keyProvider) {
this(new ArrayList<T>(), keyProvider);
}
/**
* Creates a list model that wraps the given list.
*
* <p>
* The wrapped list should no longer be modified as the data provider cannot
* detect changes to the wrapped list. Instead, call {@link #getList()} to
* retrieve a wrapper that can be modified and will correctly forward changes
* to displays.
*
* @param listToWrap the List to be wrapped
* @param keyProvider an instance of ProvidesKey<T>, or null if the record
* object should act as its own key
*/
public ListDataProvider(List<T> listToWrap, ProvidesKey<T> keyProvider) {
super(keyProvider);
listWrapper = new ListWrapper(listToWrap);
}
/**
* Flush pending list changes to the displays. By default, displays are
* informed of modifications to the underlying list at the end of the current
* event loop, which makes it possible to perform multiple operations
* synchronously without repeatedly refreshing the displays. This method can
* be called to flush the changes immediately instead of waiting until the end
* of the current event loop.
*/
public void flush() {
listWrapper.flushNow();
}
/**
* Get the list that backs this model. Changes to the list will be reflected
* in the model.
*
* <p>
* NOTE: Mutations to the items contained within the list will NOT be
* reflected in the model. You must call {@link List#set(int, Object)} to
* update the item within the list and push the change to the display, or call
* {@link #refresh()} to push all rows to the displays.
* {@link List#set(int, Object)} performs better because it allows the data
* provider to push only those rows which have changed, and usually allows the
* display to re-render only a subset of the rows.
*
* @return the list
*
* @see #setList(List)
*/
public List<T> getList() {
return listWrapper;
}
/**
* Refresh all of the displays listening to this adapter.
*
* <p>
* Use {@link #refresh()} to push mutations to the underlying data items
* contained within the list. The data provider cannot detect changes to data
* objects within the list, so you must call this method if you modify items.
*
* <p>
* This is a shortcut for calling {@link List#set(int, Object)} on every item
* that you modify, but note that calling {@link List#set(int, Object)}
* performs better because the data provider knows which rows were modified
* and can push only the modified rows the displays.
*/
public void refresh() {
updateRowData(0, listWrapper);
}
/**
* Replace this model's list with the specified list.
*
* <p>
* The wrapped list should no longer be modified as the data provider cannot
* detect changes to the wrapped list. Instead, call {@link #getList()} to
* retrieve a wrapper that can be modified and will correctly forward changes
* to displays.
*
* @param listToWrap the model's new list
*
* @see #getList()
*/
public void setList(List<T> listToWrap) {
listWrapper = new ListWrapper(listToWrap);
listWrapper.minModified = 0;
listWrapper.maxModified = listWrapper.size();
listWrapper.modified = true;
flush();
}
@Override
protected void onRangeChanged(HasData<T> display) {
int size = listWrapper.size();
if (size > 0) {
// Do not push data if the data set is empty.
updateRowData(display, 0, listWrapper);
}
}
}