/* @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: * * * 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; } }