/*
@VaadinApache2LicenseForJavaFiles@
*/
package com.vaadin.data.util;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.vaadin.data.Container;
import com.vaadin.data.Container.ItemSetChangeNotifier;
import com.vaadin.data.Item;
import com.vaadin.data.util.filter.SimpleStringFilter;
import com.vaadin.data.util.filter.UnsupportedFilterException;
/**
* Abstract {@link Container} class that handles common functionality for
* in-memory containers. Concrete in-memory container classes can either inherit
* this class, inherit {@link AbstractContainer}, or implement the
* {@link Container} interface directly.
*
* Adding and removing items (if desired) must be implemented in subclasses by
* overriding the appropriate add*Item() and remove*Item() and removeAllItems()
* methods, calling the corresponding
* {@link #internalAddItemAfter(Object, Object, Item)},
* {@link #internalAddItemAt(int, Object, Item)},
* {@link #internalAddItemAtEnd(Object, Item, boolean)},
* {@link #internalRemoveItem(Object)} and {@link #internalRemoveAllItems()}
* methods.
*
* By default, adding and removing container properties is not supported, and
* subclasses need to implement {@link #getContainerPropertyIds()}. Optionally,
* subclasses can override {@link #addContainerProperty(Object, Class, Object)}
* and {@link #removeContainerProperty(Object)} to implement them.
*
* Features:
*
* {@link Container.Ordered}
* {@link Container.Indexed}
* {@link Filterable} and {@link SimpleFilterable} (internal implementation,
* does not implement the interface directly)
* {@link Sortable} (internal implementation, does not implement the
* interface directly)
*
*
* To implement {@link Sortable}, subclasses need to implement
* {@link #getSortablePropertyIds()} and call the superclass method
* {@link #sortContainer(Object[], boolean[])} in the method
* sort(Object[], boolean[])
.
*
* To implement {@link Filterable}, subclasses need to implement the methods
* {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)}
* (calling {@link #addFilter(Filter)}),
* {@link Filterable#removeAllContainerFilters()} (calling
* {@link #removeAllFilters()}) and
* {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)}
* (calling {@link #removeFilter(com.vaadin.data.Container.Filter)}).
*
* To implement {@link SimpleFilterable}, subclasses also need to implement the
* methods
* {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)}
* and {@link SimpleFilterable#removeContainerFilters(Object)} calling
* {@link #addFilter(com.vaadin.data.Container.Filter)} and
* {@link #removeFilters(Object)} respectively.
*
* @param
* the class of item identifiers in the container, use Object if can
* be any class
* @param
* the class of property identifiers for the items in the container,
* use Object if can be any class
* @param
* the (base) class of the Item instances in the container, use
* {@link Item} if unknown
*
* @since 6.6
*/
public abstract class AbstractInMemoryContainer
extends AbstractContainer implements ItemSetChangeNotifier,
Container.Indexed {
/**
* An ordered {@link List} of all item identifiers in the container,
* including those that have been filtered out.
*
* Must not be null.
*/
private List allItemIds;
/**
* An ordered {@link List} of item identifiers in the container after
* filtering, excluding those that have been filtered out.
*
* This is what the external API of the {@link Container} interface and its
* subinterfaces shows (e.g. {@link #size()}, {@link #nextItemId(Object)}).
*
* If null, the full item id list is used instead.
*/
private List filteredItemIds;
/**
* Filters that are applied to the container to limit the items visible in
* it
*/
private Set filters = new HashSet();
/**
* The item sorter which is used for sorting the container.
*/
private ItemSorter itemSorter = new DefaultItemSorter();
// Constructors
/**
* Constructor for an abstract in-memory container.
*/
protected AbstractInMemoryContainer() {
setAllItemIds(new ListSet());
}
// Container interface methods with more specific return class
// default implementation, can be overridden
@Override
public ITEMCLASS getItem(Object itemId) {
if (containsId(itemId)) {
return getUnfilteredItem(itemId);
} else {
return null;
}
}
/**
* Get an item even if filtered out.
*
* For internal use only.
*
* @param itemId
* @return
*/
protected abstract ITEMCLASS getUnfilteredItem(Object itemId);
// cannot override getContainerPropertyIds() and getItemIds(): if subclass
// uses Object as ITEMIDCLASS or PROPERTYIDCLASS, Collection cannot
// be cast to Collection
// public abstract Collection getContainerPropertyIds();
// public abstract Collection getItemIds();
// Container interface method implementations
@Override
public int size() {
return getVisibleItemIds().size();
}
@Override
public boolean containsId(Object itemId) {
// only look at visible items after filtering
if (itemId == null) {
return false;
} else {
return getVisibleItemIds().contains(itemId);
}
}
@Override
public List> getItemIds() {
return Collections.unmodifiableList(getVisibleItemIds());
}
// Container.Ordered
@Override
public ITEMIDTYPE nextItemId(Object itemId) {
int index = indexOfId(itemId);
if (index >= 0 && index < size() - 1) {
return getIdByIndex(index + 1);
} else {
// out of bounds
return null;
}
}
@Override
public ITEMIDTYPE prevItemId(Object itemId) {
int index = indexOfId(itemId);
if (index > 0) {
return getIdByIndex(index - 1);
} else {
// out of bounds
return null;
}
}
@Override
public ITEMIDTYPE firstItemId() {
if (size() > 0) {
return getIdByIndex(0);
} else {
return null;
}
}
@Override
public ITEMIDTYPE lastItemId() {
if (size() > 0) {
return getIdByIndex(size() - 1);
} else {
return null;
}
}
@Override
public boolean isFirstId(Object itemId) {
if (itemId == null) {
return false;
}
return itemId.equals(firstItemId());
}
@Override
public boolean isLastId(Object itemId) {
if (itemId == null) {
return false;
}
return itemId.equals(lastItemId());
}
// Container.Indexed
@Override
public ITEMIDTYPE getIdByIndex(int index) {
return getVisibleItemIds().get(index);
}
@Override
public int indexOfId(Object itemId) {
return getVisibleItemIds().indexOf(itemId);
}
// methods that are unsupported by default, override to support
@Override
public Object addItemAt(int index) throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public Item addItemAt(int index, Object newItemId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public Object addItemAfter(Object previousItemId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public Item addItemAfter(Object previousItemId, Object newItemId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public Item addItem(Object itemId) throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public Object addItem() throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public boolean removeItem(Object itemId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Removing items not supported. Override the removeItem() method if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public boolean removeAllItems() throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Removing items not supported. Override the removeAllItems() method if required as specified in AbstractInMemoryContainer javadoc.");
}
@Override
public boolean addContainerProperty(Object propertyId, Class> type,
Object defaultValue) throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Adding container properties not supported. Override the addContainerProperty() method if required.");
}
@Override
public boolean removeContainerProperty(Object propertyId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Removing container properties not supported. Override the addContainerProperty() method if required.");
}
// ItemSetChangeNotifier
@Override
public void addListener(Container.ItemSetChangeListener listener) {
super.addListener(listener);
}
@Override
public void removeListener(Container.ItemSetChangeListener listener) {
super.removeListener(listener);
}
// internal methods
// Filtering support
/**
* Filter the view to recreate the visible item list from the unfiltered
* items, and send a notification if the set of visible items changed in any
* way.
*/
protected void filterAll() {
if (doFilterContainer(!getFilters().isEmpty())) {
fireItemSetChange();
}
}
/**
* Filters the data in the container and updates internal data structures.
* This method should reset any internal data structures and then repopulate
* them so {@link #getItemIds()} and other methods only return the filtered
* items.
*
* @param hasFilters
* true if filters has been set for the container, false
* otherwise
* @return true if the item set has changed as a result of the filtering
*/
protected boolean doFilterContainer(boolean hasFilters) {
if (!hasFilters) {
boolean changed = getAllItemIds().size() != getVisibleItemIds()
.size();
setFilteredItemIds(null);
return changed;
}
// Reset filtered list
List originalFilteredItemIds = getFilteredItemIds();
boolean wasUnfiltered = false;
if (originalFilteredItemIds == null) {
originalFilteredItemIds = Collections.emptyList();
wasUnfiltered = true;
}
setFilteredItemIds(new ListSet());
// Filter
boolean equal = true;
Iterator origIt = originalFilteredItemIds.iterator();
for (final Iterator i = getAllItemIds().iterator(); i
.hasNext();) {
final ITEMIDTYPE id = i.next();
if (passesFilters(id)) {
// filtered list comes from the full list, can use ==
equal = equal && origIt.hasNext() && origIt.next() == id;
getFilteredItemIds().add(id);
}
}
return (wasUnfiltered && !getAllItemIds().isEmpty()) || !equal
|| origIt.hasNext();
}
/**
* Checks if the given itemId passes the filters set for the container. The
* caller should make sure the itemId exists in the container. For
* non-existing itemIds the behavior is undefined.
*
* @param itemId
* An itemId that exists in the container.
* @return true if the itemId passes all filters or no filters are set,
* false otherwise.
*/
protected boolean passesFilters(Object itemId) {
ITEMCLASS item = getUnfilteredItem(itemId);
if (getFilters().isEmpty()) {
return true;
}
final Iterator i = getFilters().iterator();
while (i.hasNext()) {
final Filter f = i.next();
if (!f.passesFilter(itemId, item)) {
return false;
}
}
return true;
}
/**
* Adds a container filter and re-filter the view.
*
* The filter must implement Filter and its sub-filters (if any) must also
* be in-memory filterable.
*
* This can be used to implement
* {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)}
* and optionally also
* {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)}
* (with {@link SimpleStringFilter}).
*
* Note that in some cases, incompatible filters cannot be detected when
* added and an {@link UnsupportedFilterException} may occur when performing
* filtering.
*
* @throws UnsupportedFilterException
* if the filter is detected as not supported by the container
*/
protected void addFilter(Filter filter) throws UnsupportedFilterException {
getFilters().add(filter);
filterAll();
}
/**
* Remove a specific container filter and re-filter the view (if necessary).
*
* This can be used to implement
* {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)}
* .
*/
protected void removeFilter(Filter filter) {
for (Iterator iterator = getFilters().iterator(); iterator
.hasNext();) {
Filter f = iterator.next();
if (f.equals(filter)) {
iterator.remove();
filterAll();
return;
}
}
}
/**
* Remove all container filters for all properties and re-filter the view.
*
* This can be used to implement
* {@link Filterable#removeAllContainerFilters()}.
*/
protected void removeAllFilters() {
if (getFilters().isEmpty()) {
return;
}
getFilters().clear();
filterAll();
}
/**
* Checks if there is a filter that applies to a given property.
*
* @param propertyId
* @return true if there is an active filter for the property
*/
protected boolean isPropertyFiltered(Object propertyId) {
if (getFilters().isEmpty() || propertyId == null) {
return false;
}
final Iterator i = getFilters().iterator();
while (i.hasNext()) {
final Filter f = i.next();
if (f.appliesToProperty(propertyId)) {
return true;
}
}
return false;
}
/**
* Remove all container filters for a given property identifier and
* re-filter the view. This also removes filters applying to multiple
* properties including the one identified by propertyId.
*
* This can be used to implement
* {@link Filterable#removeContainerFilters(Object)}.
*
* @param propertyId
* @return Collection removed filters
*/
protected Collection removeFilters(Object propertyId) {
if (getFilters().isEmpty() || propertyId == null) {
return Collections.emptyList();
}
List removedFilters = new LinkedList();
for (Iterator iterator = getFilters().iterator(); iterator
.hasNext();) {
Filter f = iterator.next();
if (f.appliesToProperty(propertyId)) {
removedFilters.add(f);
iterator.remove();
}
}
if (!removedFilters.isEmpty()) {
filterAll();
return removedFilters;
}
return Collections.emptyList();
}
// sorting
/**
* Returns the ItemSorter used for comparing items in a sort. See
* {@link #setItemSorter(ItemSorter)} for more information.
*
* @return The ItemSorter used for comparing two items in a sort.
*/
protected ItemSorter getItemSorter() {
return itemSorter;
}
/**
* Sets the ItemSorter used for comparing items in a sort. The
* {@link ItemSorter#compare(Object, Object)} method is called with item ids
* to perform the sorting. A default ItemSorter is used if this is not
* explicitly set.
*
* @param itemSorter
* The ItemSorter used for comparing two items in a sort (not
* null).
*/
protected void setItemSorter(ItemSorter itemSorter) {
this.itemSorter = itemSorter;
}
/**
* Sort base implementation to be used to implement {@link Sortable}.
*
* Subclasses should call this from a public
* {@link #sort(Object[], boolean[])} method when implementing Sortable.
*
* @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
* boolean[])
*/
protected void sortContainer(Object[] propertyId, boolean[] ascending) {
if (!(this instanceof Sortable)) {
throw new UnsupportedOperationException(
"Cannot sort a Container that does not implement Sortable");
}
// Set up the item sorter for the sort operation
getItemSorter().setSortProperties((Sortable) this, propertyId,
ascending);
// Perform the actual sort
doSort();
// Post sort updates
if (isFiltered()) {
filterAll();
} else {
fireItemSetChange();
}
}
/**
* Perform the sorting of the data structures in the container. This is
* invoked when the itemSorter
has been prepared for the sort
* operation. Typically this method calls
* Collections.sort(aCollection, getItemSorter())
on all arrays
* (containing item ids) that need to be sorted.
*
*/
protected void doSort() {
Collections.sort(getAllItemIds(), getItemSorter());
}
/**
* Returns the sortable property identifiers for the container. Can be used
* to implement {@link Sortable#getSortableContainerPropertyIds()}.
*/
protected Collection> getSortablePropertyIds() {
LinkedList sortables = new LinkedList();
for (Object propertyId : getContainerPropertyIds()) {
Class> propertyType = getType(propertyId);
if (Comparable.class.isAssignableFrom(propertyType)
|| propertyType.isPrimitive()) {
sortables.add(propertyId);
}
}
return sortables;
}
// removing items
/**
* Removes all items from the internal data structures of this class. This
* can be used to implement {@link #removeAllItems()} in subclasses.
*
* No notification is sent, the caller has to fire a suitable item set
* change notification.
*/
protected void internalRemoveAllItems() {
// Removes all Items
getAllItemIds().clear();
if (isFiltered()) {
getFilteredItemIds().clear();
}
}
/**
* Removes a single item from the internal data structures of this class.
* This can be used to implement {@link #removeItem(Object)} in subclasses.
*
* No notification is sent, the caller has to fire a suitable item set
* change notification.
*
* @param itemId
* the identifier of the item to remove
* @return true if an item was successfully removed, false if failed to
* remove or no such item
*/
protected boolean internalRemoveItem(Object itemId) {
if (itemId == null) {
return false;
}
boolean result = getAllItemIds().remove(itemId);
if (result && isFiltered()) {
getFilteredItemIds().remove(itemId);
}
return result;
}
// adding items
/**
* Adds the bean to all internal data structures at the given position.
* Fails if an item with itemId is already in the container. Returns a the
* item if it was added successfully, null otherwise.
*
*
* Caller should initiate filtering after calling this method.
*
*
* For internal use only - subclasses should use
* {@link #internalAddItemAtEnd(Object, Item, boolean)},
* {@link #internalAddItemAt(int, Object, Item, boolean)} and
* {@link #internalAddItemAfter(Object, Object, Item, boolean)} instead.
*
* @param position
* The position at which the item should be inserted in the
* unfiltered collection of items
* @param itemId
* The item identifier for the item to insert
* @param item
* The item to insert
*
* @return ITEMCLASS if the item was added successfully, null otherwise
*/
private ITEMCLASS internalAddAt(int position, ITEMIDTYPE itemId,
ITEMCLASS item) {
if (position < 0 || position > getAllItemIds().size() || itemId == null
|| item == null) {
return null;
}
// Make sure that the item has not been added previously
if (getAllItemIds().contains(itemId)) {
return null;
}
// "filteredList" will be updated in filterAll() which should be invoked
// by the caller after calling this method.
getAllItemIds().add(position, itemId);
registerNewItem(position, itemId, item);
return item;
}
/**
* Add an item at the end of the container, and perform filtering if
* necessary. An event is fired if the filtered view changes.
*
* @param newItemId
* @param item
* new item to add
* @param filter
* true to perform filtering and send event after adding the
* item, false to skip these operations for batch inserts - if
* false, caller needs to make sure these operations are
* performed at the end of the batch
* @return item added or null if no item was added
*/
protected ITEMCLASS internalAddItemAtEnd(ITEMIDTYPE newItemId,
ITEMCLASS item, boolean filter) {
ITEMCLASS newItem = internalAddAt(getAllItemIds().size(), newItemId,
item);
if (newItem != null && filter) {
// TODO filter only this item, use fireItemAdded()
filterAll();
if (!isFiltered()) {
// TODO hack: does not detect change in filterAll() in this case
fireItemAdded(indexOfId(newItemId), newItemId, item);
}
}
return newItem;
}
/**
* Add an item after a given (visible) item, and perform filtering. An event
* is fired if the filtered view changes.
*
* The new item is added at the beginning if previousItemId is null.
*
* @param previousItemId
* item id of a visible item after which to add the new item, or
* null to add at the beginning
* @param newItemId
* @param item
* new item to add
* @param filter
* true to perform filtering and send event after adding the
* item, false to skip these operations for batch inserts - if
* false, caller needs to make sure these operations are
* performed at the end of the batch
* @return item added or null if no item was added
*/
protected ITEMCLASS internalAddItemAfter(ITEMIDTYPE previousItemId,
ITEMIDTYPE newItemId, ITEMCLASS item, boolean filter) {
// only add if the previous item is visible
ITEMCLASS newItem = null;
if (previousItemId == null) {
newItem = internalAddAt(0, newItemId, item);
} else if (containsId(previousItemId)) {
newItem = internalAddAt(
getAllItemIds().indexOf(previousItemId) + 1, newItemId,
item);
}
if (newItem != null && filter) {
// TODO filter only this item, use fireItemAdded()
filterAll();
if (!isFiltered()) {
// TODO hack: does not detect change in filterAll() in this case
fireItemAdded(indexOfId(newItemId), newItemId, item);
}
}
return newItem;
}
/**
* Add an item at a given (visible after filtering) item index, and perform
* filtering. An event is fired if the filtered view changes.
*
* @param index
* position where to add the item (visible/view index)
* @param newItemId
* @param item
* new item to add
* @param filter
* true to perform filtering and send event after adding the
* item, false to skip these operations for batch inserts - if
* false, caller needs to make sure these operations are
* performed at the end of the batch
* @return item added or null if no item was added
*/
protected ITEMCLASS internalAddItemAt(int index, ITEMIDTYPE newItemId,
ITEMCLASS item, boolean filter) {
if (index < 0 || index > size()) {
return null;
} else if (index == 0) {
// add before any item, visible or not
return internalAddItemAfter(null, newItemId, item, filter);
} else {
// if index==size(), adds immediately after last visible item
return internalAddItemAfter(getIdByIndex(index - 1), newItemId,
item, filter);
}
}
/**
* Registers a new item as having been added to the container. This can
* involve storing the item or any relevant information about it in internal
* container-specific collections if necessary, as well as registering
* listeners etc.
*
* The full identifier list in {@link AbstractInMemoryContainer} has already
* been updated to reflect the new item when this method is called.
*
* @param position
* @param itemId
* @param item
*/
protected void registerNewItem(int position, ITEMIDTYPE itemId,
ITEMCLASS item) {
}
// item set change notifications
/**
* Notify item set change listeners that an item has been added to the
* container.
*
* Unless subclasses specify otherwise, the default notification indicates a
* full refresh.
*
* @param postion
* position of the added item in the view (if visible)
* @param itemId
* id of the added item
* @param item
* the added item
*/
protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) {
fireItemSetChange();
}
/**
* Notify item set change listeners that an item has been removed from the
* container.
*
* Unless subclasses specify otherwise, the default notification indicates a
* full refresh.
*
* @param postion
* position of the removed item in the view prior to removal (if
* was visible)
* @param itemId
* id of the removed item, of type {@link Object} to satisfy
* {@link Container#removeItem(Object)} API
*/
protected void fireItemRemoved(int position, Object itemId) {
fireItemSetChange();
}
// visible and filtered item identifier lists
/**
* Returns the internal list of visible item identifiers after filtering.
*
* For internal use only.
*/
protected List getVisibleItemIds() {
if (isFiltered()) {
return getFilteredItemIds();
} else {
return getAllItemIds();
}
}
/**
* Returns true is the container has active filters.
*
* @return true if the container is currently filtered
*/
protected boolean isFiltered() {
return filteredItemIds != null;
}
/**
* Internal helper method to set the internal list of filtered item
* identifiers. Should not be used outside this class except for
* implementing clone(), may disappear from future versions.
*
* @param filteredItemIds
*/
@Deprecated
protected void setFilteredItemIds(List filteredItemIds) {
this.filteredItemIds = filteredItemIds;
}
/**
* Internal helper method to get the internal list of filtered item
* identifiers. Should not be used outside this class except for
* implementing clone(), may disappear from future versions - use
* {@link #getVisibleItemIds()} in other contexts.
*
* @return List
*/
protected List getFilteredItemIds() {
return filteredItemIds;
}
/**
* Internal helper method to set the internal list of all item identifiers.
* Should not be used outside this class except for implementing clone(),
* may disappear from future versions.
*
* @param allItemIds
*/
@Deprecated
protected void setAllItemIds(List allItemIds) {
this.allItemIds = allItemIds;
}
/**
* Internal helper method to get the internal list of all item identifiers.
* Avoid using this method outside this class, may disappear in future
* versions.
*
* @return List
*/
protected List getAllItemIds() {
return allItemIds;
}
/**
* Set the internal collection of filters without performing filtering.
*
* This method is mostly for internal use, use
* {@link #addFilter(com.vaadin.data.Container.Filter)} and
* remove*Filter*
(which also re-filter the container) instead
* when possible.
*
* @param filters
*/
protected void setFilters(Set filters) {
this.filters = filters;
}
/**
* Returns the internal collection of filters. The returned collection
* should not be modified by callers outside this class.
*
* @return Set
*/
protected Set getFilters() {
return filters;
}
}