/*
* Copyright 2010 IT Mill Ltd.
*
* 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.vaadin.data.util;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.vaadin.data.Container;
import com.vaadin.data.Property;
import com.vaadin.data.Container.Filterable;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.ItemSetChangeNotifier;
import com.vaadin.data.Container.Sortable;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Property.ValueChangeNotifier;
/**
* An {@link ArrayList} backed container for {@link BeanItem}s.
* <p>
* Bean objects act as identifiers. For this reason, they should implement
* Object.equals(Object) and Object.hashCode().
* </p>
*
* @param <BT>
*
* @since 5.4
*/
@SuppressWarnings("serial")
public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable,
ItemSetChangeNotifier, ValueChangeListener {
/**
* The filteredItems variable contains the items that are visible outside
* the container. If filters are enabled this contains a subset of allItems,
* if no filters are set this contains the same items as allItems.
*/
private ListSet<BT> filteredItems = new ListSet<BT>();
/**
* The allItems variable always contains all the items in the container.
* Some or all of these are also in the filteredItems list.
*/
private ListSet<BT> allItems = new ListSet<BT>();
/**
* Maps all pojos (item ids) in the container (including filtered) to their
* corresponding BeanItem.
*/
private final Map<BT, BeanItem<BT>> beanToItem = new HashMap<BT, BeanItem<BT>>();
// internal data model to obtain property IDs etc.
private final Class<? extends BT> type;
private transient LinkedHashMap<String, PropertyDescriptor> model;
private List<ItemSetChangeListener> itemSetChangeListeners;
private Set<Filter> filters = new HashSet<Filter>();
/**
* The item sorter which is used for sorting the container.
*/
private ItemSorter itemSorter = new DefaultItemSorter();
/* Special serialization to handle method references */
private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
model = BeanItem.getPropertyDescriptors(type);
}
/**
* Constructs BeanItemContainer for beans of a given type.
*
* @param type
* the class of beans to be used with this containers.
* @throws IllegalArgumentException
* If the type is null
*/
public BeanItemContainer(Class<? extends BT> type) {
if (type == null) {
throw new IllegalArgumentException(
"The type passed to BeanItemContainer must not be null");
}
this.type = type;
model = BeanItem.getPropertyDescriptors(type);
}
/**
* Constructs BeanItemContainer with given collection of beans in it. The
* collection must not be empty or an IllegalArgument is thrown.
*
* @param collection
* non empty {@link Collection} of beans.
* @throws IllegalArgumentException
* If the collection is null or empty.
*/
@SuppressWarnings("unchecked")
public BeanItemContainer(Collection<BT> collection)
throws IllegalArgumentException {
if (collection == null || collection.isEmpty()) {
throw new IllegalArgumentException(
"The collection passed to BeanItemContainer must not be null or empty");
}
type = (Class<? extends BT>) collection.iterator().next().getClass();
model = BeanItem.getPropertyDescriptors(type);
addAll(collection);
}
private void addAll(Collection<BT> collection) {
// Pre-allocate space for the collection
allItems.ensureCapacity(allItems.size() + collection.size());
int idx = size();
for (BT bean : collection) {
if (internalAddAt(idx, bean) != null) {
idx++;
}
}
// Filter the contents when all items have been added
filterAll();
}
/**
* Unsupported operation.
*
* @see com.vaadin.data.Container.Indexed#addItemAt(int)
*/
public Object addItemAt(int index) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Adds new item at given index.
*
* The bean is used both as the item contents and as the item identifier.
*
* @see com.vaadin.data.Container.Indexed#addItemAt(int, Object)
*/
public BeanItem<BT> addItemAt(int index, Object newItemId)
throws UnsupportedOperationException {
if (index < 0 || index > size()) {
return null;
} else if (index == 0) {
// add before any item, visible or not
return addItemAtInternalIndex(0, newItemId);
} else {
// if index==size(), adds immediately after last visible item
return addItemAfter(getIdByIndex(index - 1), newItemId);
}
}
/**
* Adds new item at given index of the internal (unfiltered) list.
* <p>
* The item is also added in the visible part of the list if it passes the
* filters.
* </p>
*
* @param index
* Internal index to add the new item.
* @param newItemId
* Id of the new item to be added.
* @return Returns new item or null if the operation fails.
*/
private BeanItem<BT> addItemAtInternalIndex(int index, Object newItemId) {
BeanItem<BT> beanItem = internalAddAt(index, (BT) newItemId);
if (beanItem != null) {
filterAll();
}
return beanItem;
}
/**
* Adds the bean to all internal data structures at the given position.
* Fails if the bean is already in the container or is not assignable to the
* correct type. Returns the new BeanItem if the bean was added
* successfully.
*
* <p>
* Caller should call {@link #filterAll()} after calling this method to
* ensure the filtered list is updated.
* </p>
*
* @param position
* The position at which the bean should be inserted
* @param bean
* The bean to insert
*
* @return true if the bean was added successfully, false otherwise
*/
private BeanItem<BT> internalAddAt(int position, BT bean) {
// Make sure that the item has not been added previously
if (allItems.contains(bean)) {
return null;
}
if (!type.isAssignableFrom(bean.getClass())) {
return null;
}
// "filteredList" will be updated in filterAll() which should be invoked
// by the caller after calling this method.
allItems.add(position, bean);
BeanItem<BT> beanItem = new BeanItem<BT>(bean, model);
beanToItem.put(bean, beanItem);
// add listeners to be able to update filtering on property
// changes
for (Filter filter : filters) {
// addValueChangeListener avoids adding duplicates
addValueChangeListener(beanItem, filter.propertyId);
}
return beanItem;
}
@SuppressWarnings("unchecked")
public BT getIdByIndex(int index) {
return filteredItems.get(index);
}
public int indexOfId(Object itemId) {
return filteredItems.indexOf(itemId);
}
/**
* Unsupported operation.
*
* @see com.vaadin.data.Container.Ordered#addItemAfter(Object)
*/
public Object addItemAfter(Object previousItemId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Adds new item after the given item.
*
* The bean is used both as the item contents and as the item identifier.
*
* @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
*/
public BeanItem<BT> addItemAfter(Object previousItemId, Object newItemId)
throws UnsupportedOperationException {
// only add if the previous item is visible
if (previousItemId == null) {
return addItemAtInternalIndex(0, newItemId);
} else if (containsId(previousItemId)) {
return addItemAtInternalIndex(allItems.indexOf(previousItemId) + 1,
newItemId);
} else {
return null;
}
}
public BT firstItemId() {
if (size() > 0) {
return getIdByIndex(0);
} else {
return null;
}
}
public boolean isFirstId(Object itemId) {
return firstItemId() == itemId;
}
public boolean isLastId(Object itemId) {
return lastItemId() == itemId;
}
public BT lastItemId() {
if (size() > 0) {
return getIdByIndex(size() - 1);
} else {
return null;
}
}
public BT nextItemId(Object itemId) {
int index = indexOfId(itemId);
if (index >= 0 && index < size() - 1) {
return getIdByIndex(index + 1);
} else {
// out of bounds
return null;
}
}
public BT prevItemId(Object itemId) {
int index = indexOfId(itemId);
if (index > 0) {
return getIdByIndex(index - 1);
} else {
// out of bounds
return null;
}
}
public boolean addContainerProperty(Object propertyId, Class<?> type,
Object defaultValue) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Unsupported operation.
*
* @see com.vaadin.data.Container#addItem()
*/
public Object addItem() throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Creates a new Item with the bean into the Container.
*
* The bean is used both as the item contents and as the item identifier.
*
* @see com.vaadin.data.Container#addItem(Object)
*/
public BeanItem<BT> addBean(BT bean) {
return addItem(bean);
}
/**
* Creates a new Item with the bean into the Container.
*
* The bean is used both as the item contents and as the item identifier.
*
* @see com.vaadin.data.Container#addItem(Object)
*/
public BeanItem<BT> addItem(Object itemId)
throws UnsupportedOperationException {
if (size() > 0) {
// add immediately after last visible item
int lastIndex = allItems.indexOf(lastItemId());
return addItemAtInternalIndex(lastIndex + 1, itemId);
} else {
return addItemAtInternalIndex(0, itemId);
}
}
public boolean containsId(Object itemId) {
// only look at visible items after filtering
return filteredItems.contains(itemId);
}
public Property getContainerProperty(Object itemId, Object propertyId) {
return getItem(itemId).getItemProperty(propertyId);
}
public Collection<String> getContainerPropertyIds() {
return model.keySet();
}
public BeanItem<BT> getItem(Object itemId) {
return beanToItem.get(itemId);
}
@SuppressWarnings("unchecked")
public Collection<BT> getItemIds() {
return (Collection<BT>) filteredItems.clone();
}
public Class<?> getType(Object propertyId) {
return model.get(propertyId).getPropertyType();
}
public boolean removeAllItems() throws UnsupportedOperationException {
allItems.clear();
filteredItems.clear();
// detach listeners from all BeanItems
for (BeanItem<BT> item : beanToItem.values()) {
removeAllValueChangeListeners(item);
}
beanToItem.clear();
fireItemSetChange();
return true;
}
public boolean removeContainerProperty(Object propertyId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
public boolean removeItem(Object itemId)
throws UnsupportedOperationException {
if (!allItems.remove(itemId)) {
return false;
}
// detach listeners from Item
removeAllValueChangeListeners(getItem(itemId));
// remove item
beanToItem.remove(itemId);
filteredItems.remove(itemId);
fireItemSetChange();
return true;
}
private void addValueChangeListener(BeanItem<BT> beanItem, Object propertyId) {
Property property = beanItem.getItemProperty(propertyId);
if (property instanceof ValueChangeNotifier) {
// avoid multiple notifications for the same property if
// multiple filters are in use
ValueChangeNotifier notifier = (ValueChangeNotifier) property;
notifier.removeListener(this);
notifier.addListener(this);
}
}
private void removeValueChangeListener(BeanItem<BT> item, Object propertyId) {
Property property = item.getItemProperty(propertyId);
if (property instanceof ValueChangeNotifier) {
((ValueChangeNotifier) property).removeListener(this);
}
}
private void removeAllValueChangeListeners(BeanItem<BT> item) {
for (Object propertyId : item.getItemPropertyIds()) {
removeValueChangeListener(item, propertyId);
}
}
public int size() {
return filteredItems.size();
}
public Collection<Object> getSortableContainerPropertyIds() {
LinkedList<Object> sortables = new LinkedList<Object>();
for (Object propertyId : getContainerPropertyIds()) {
Class<?> propertyType = getType(propertyId);
if (Comparable.class.isAssignableFrom(propertyType)
|| propertyType.isPrimitive()) {
sortables.add(propertyId);
}
}
return sortables;
}
/*
* (non-Javadoc)
*
* @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
* boolean[])
*/
public void sort(Object[] propertyId, boolean[] ascending) {
itemSorter.setSortProperties(this, propertyId, ascending);
doSort();
// notifies if anything changes in the filtered list, including order
filterAll();
}
/**
* Perform the sorting of the data structures in the container. This is
* invoked when the <code>itemSorter</code> has been prepared for the sort
* operation. Typically this method calls
* <code>Collections.sort(aCollection, getItemSorter())</code> on all arrays
* (containing item ids) that need to be sorted.
*
*/
protected void doSort() {
Collections.sort(allItems, getItemSorter());
}
public void addListener(ItemSetChangeListener listener) {
if (itemSetChangeListeners == null) {
itemSetChangeListeners = new LinkedList<ItemSetChangeListener>();
}
itemSetChangeListeners.add(listener);
}
public void removeListener(ItemSetChangeListener listener) {
if (itemSetChangeListeners != null) {
itemSetChangeListeners.remove(listener);
}
}
private void fireItemSetChange() {
if (itemSetChangeListeners != null) {
final Container.ItemSetChangeEvent event = new Container.ItemSetChangeEvent() {
public Container getContainer() {
return BeanItemContainer.this;
}
};
for (ItemSetChangeListener listener : itemSetChangeListeners) {
listener.containerItemSetChange(event);
}
}
}
public void addContainerFilter(Object propertyId, String filterString,
boolean ignoreCase, boolean onlyMatchPrefix) {
if (filters.isEmpty()) {
filteredItems = (ListSet<BT>) allItems.clone();
}
// listen to change events to be able to update filtering
for (BeanItem<BT> item : beanToItem.values()) {
addValueChangeListener(item, propertyId);
}
Filter f = new Filter(propertyId, filterString, ignoreCase,
onlyMatchPrefix);
filter(f);
filters.add(f);
fireItemSetChange();
}
/**
* 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() {
// avoid notification if the filtering had no effect
List<BT> originalItems = filteredItems;
// it is somewhat inefficient to do a (shallow) clone() every time
filteredItems = (ListSet<BT>) allItems.clone();
for (Filter f : filters) {
filter(f);
}
// check if exactly the same items are there after filtering to avoid
// unnecessary notifications
// this may be slow in some cases as it uses BT.equals()
if (!originalItems.equals(filteredItems)) {
fireItemSetChange();
}
}
protected void filter(Filter f) {
Iterator<BT> iterator = filteredItems.iterator();
while (iterator.hasNext()) {
BT bean = iterator.next();
if (!f.passesFilter(getItem(bean))) {
iterator.remove();
}
}
}
public void removeAllContainerFilters() {
if (!filters.isEmpty()) {
filters = new HashSet<Filter>();
// stop listening to change events for any property
for (BeanItem<BT> item : beanToItem.values()) {
removeAllValueChangeListeners(item);
}
filterAll();
}
}
public void removeContainerFilters(Object propertyId) {
if (!filters.isEmpty()) {
for (Iterator<Filter> iterator = filters.iterator(); iterator
.hasNext();) {
Filter f = iterator.next();
if (f.propertyId.equals(propertyId)) {
iterator.remove();
}
}
// stop listening to change events for the property
for (BeanItem<BT> item : beanToItem.values()) {
removeValueChangeListener(item, propertyId);
}
filterAll();
}
}
public void valueChange(ValueChangeEvent event) {
// if a property that is used in a filter is changed, refresh filtering
filterAll();
}
public ItemSorter getItemSorter() {
return itemSorter;
}
public void setItemSorter(ItemSorter itemSorter) {
this.itemSorter = itemSorter;
}
}