package com.tri.ui.model; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.apache.commons.collections.CollectionUtils; import com.tri.ui.model.observer.ChangeEventType; import com.tri.ui.model.utility.Validate; /** * <p> * List based DataController implementation. Supports: * </p> * <ul> * <li>lazy loading</li> * <li>single- and multi-selection</li> * <li>sorting</li> * </ul> * <p> * Notifies observers for selection changes. Notice: Sort order changes are not * seen as data changes, see {@link #setSorting(List)} etc. * </p> * * @author khennig@pobox.com * * @param <K> * key * @param <V> * value */ public abstract class ListDataController<K, V> extends DataController<List<V>> implements Selection<K, V>, Cache { private static final long serialVersionUID = 1L; private List<V> selection = new ArrayList<V>(); private List<V> data; int selectionIndex = -1; private List<SortProperty> sorting = new ArrayList<SortProperty>(); /** * Loads data. Loaded data will be cached internally, call * {@link #clearCache()} to clear cache. * * @param sorting * sort order * @return List, can be empty but never null. */ public abstract List<V> load(List<SortProperty> sorting); @Override public List<V> getData() { if (data == null) { data = load(sorting); } return data; } /** * Returns the number of values in this controller. * * @return number of values in this controller */ public int getSize() { return getData().size(); } @Override public void clearCache() { data = null; } /** * Returns selection values as unmodifiable list. * * @return list of selected values */ @Override public List<V> getSelection() { return Collections.unmodifiableList(selection); } @Override public V getSelectionValue(int index) { return selection.get(index); } @Override public int getSelectionSize() { return selection.size(); } @Override public void addSelection(final List<V> values) { Validate.noNullElements(values, "No null values allowed"); if (!values.isEmpty()) { selection.addAll(values); updateSelectionIndex(); notify(ChangeEventType.SELECTION); } } @Override public void addSelection(final V value) { Validate.notNull(value, "Value required"); selection.add(value); updateSelectionIndex(); notify(ChangeEventType.SELECTION); } @Override public void setSelection(final List<V> values) { Validate.noNullElements(values, "No null values allowed"); if ((!selection.isEmpty() || !values.isEmpty()) && !CollectionUtils.isEqualCollection(selection, values)) { selection.clear(); selection.addAll(values); updateSelectionIndex(); notify(ChangeEventType.SELECTION); } } @Override public void setSelection(final V value) { Validate.notNull(value, "Value required"); if (selection.size() != 1 || !selection.contains(value)) { selection.clear(); selection.add(value); updateSelectionIndex(); notify(ChangeEventType.SELECTION); } } @Override public void setSelection(final V value, int selectionIndex) { Validate.notNull(value, "Value required"); if (selection.size() != 1 || !selection.contains(value)) { Validate.isTrue(selectionIndex >= -1, "SelectionIndex %d not >= -1", selectionIndex); final int size = getSize(); Validate.isTrue(selectionIndex < size, "SelectionIndex %d not < size: %d", selectionIndex, size); selection.clear(); selection.add(value); this.selectionIndex = selectionIndex; notify(ChangeEventType.SELECTION); } } @Override public void clearSelection() { if (!selection.isEmpty()) { selection.clear(); updateSelectionIndex(); notify(ChangeEventType.SELECTION); } } /** * Updates the internal selection index to reflect current selection. */ void updateSelectionIndex() { if (selection.size() == 1) { selectionIndex = getIndexOf(selection.get(0)); } else { selectionIndex = -1; } } /** * Returns the absolute index of the current single selection, or -1 if * there is no selection or there are multiple selections. * * @return absolute index of current single selection, zero based */ public int getSelectionIndex() { return selectionIndex; } /** * Returns the index of the first occurrence of the specified value in this * controller, or -1 if this controller does not contain the value. * * @param value * @return found index, or -1 if value was not found * @throws NullPointerException * if parameter value is null * @throws IllegalStateException * if there is no data available */ int getIndexOf(final V value) { Validate.notNull(value, "Value required"); Validate.validState(getSize() > 0, "No data"); return getData().indexOf(value); } /** * <p> * Returns the value for given key. Default implementation looks for value * in caches, see {@link #findChachedValueOf(Object)}. * </p> * <p> * {@link #getKeyOf(Object)} must be implemented to use the default * implementation of this method. * </p> * * @param key * value key * @return found value or null if none was found */ @Override public V getValueOf(K key) { return findChachedValueOf(key); } /** * Find value in cached data. * * @param key * @return found value or null if non was found */ public V findChachedValueOf(K key) { // check selection for (V value : selection) { if (getKeyOf(value).equals(key)) { return value; } } // check data cache for (V value : getData()) { if (getKeyOf(value).equals(key)) { return value; } } return null; } @Override public void first() { Validate.validState(getSize() > 0, "No data"); setSelection(getData().get(0)); selectionIndex = 0; } @Override public void last() { Validate.validState(getSize() > 0, "No data"); final int newSelectionIndex = getSize() - 1; setSelection(getData().get(newSelectionIndex)); selectionIndex = newSelectionIndex; } @Override public void next() { Validate.validState(hasNext(), "Has no next"); final int newSelectionIndex = selectionIndex + 1; setSelection(getData().get(newSelectionIndex), newSelectionIndex); } @Override public void previous() { Validate.validState(hasPrevious(), "Has no previous"); final int newSelectionIndex = selectionIndex - 1; setSelection(getData().get(newSelectionIndex)); selectionIndex = newSelectionIndex; } @Override public boolean hasNext() { if (selectionIndex > -1) { return (selectionIndex < getSize() - 1) ? true : false; } else { return false; } } @Override public boolean hasPrevious() { if (selectionIndex > -1) { return (selectionIndex > 0) ? true : false; } else { return false; } } public List<SortProperty> getSorting() { return sorting; } /** * Sets sorting, does nothing if sorting will not change. Clears the cache * but does not notify observers for data changes. * * @param newSorting * List of SortProperties, replacing */ public void setSorting(List<SortProperty> newSorting) { Validate.notNull(newSorting, "Sorting required"); if (!sorting.equals(newSorting)) { sorting.clear(); sorting.addAll(newSorting); selectionIndex = -1; clearCache(); } } /** * Clears sorting, does nothing if sorting will not change. Clears the cache * but does not notify observers for data changes. */ public void clearSorting() { if (!sorting.isEmpty()) { sorting.clear(); selectionIndex = -1; clearCache(); } } }