com.vaadin.data.util.AbstractBeanContainer Maven / Gradle / Ivy
/*
* Vaadin Framework 7
*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.data.util;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.vaadin.data.Container;
import com.vaadin.data.Container.Filterable;
import com.vaadin.data.Container.PropertySetChangeNotifier;
import com.vaadin.data.Container.SimpleFilterable;
import com.vaadin.data.Container.Sortable;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Property.ValueChangeNotifier;
import com.vaadin.data.util.MethodProperty.MethodException;
import com.vaadin.data.util.filter.SimpleStringFilter;
import com.vaadin.data.util.filter.UnsupportedFilterException;
/**
* An abstract base class for in-memory containers for JavaBeans.
*
*
* The properties of the container are determined automatically by introspecting
* the used JavaBean class and explicitly adding or removing properties is not
* supported. Only beans of the same type can be added to the container.
*
*
*
* Subclasses should implement any public methods adding items to the container,
* typically calling the protected methods {@link #addItem(Object, Object)},
* {@link #addItemAfter(Object, Object, Object)} and
* {@link #addItemAt(int, Object, Object)}.
*
*
* @param
* The type of the item identifier
* @param
* The type of the Bean
*
* @since 6.5
*/
public abstract class AbstractBeanContainer
extends AbstractInMemoryContainer>
implements Filterable, SimpleFilterable, Sortable, ValueChangeListener,
PropertySetChangeNotifier {
/**
* Resolver that maps beans to their (item) identifiers, removing the need
* to explicitly specify item identifiers when there is no need to customize
* this.
*
* Note that beans can also be added with an explicit id even if a resolver
* has been set.
*
* @param
* @param
*
* @since 6.5
*/
public static interface BeanIdResolver
extends Serializable {
/**
* Return the item identifier for a bean.
*
* @param bean
* @return
*/
public IDTYPE getIdForBean(BEANTYPE bean);
}
/**
* A item identifier resolver that returns the value of a bean property.
*
* The bean must have a getter for the property, and the getter must return
* an object of type IDTYPE.
*/
protected class PropertyBasedBeanIdResolver
implements BeanIdResolver {
private final Object propertyId;
public PropertyBasedBeanIdResolver(Object propertyId) {
if (propertyId == null) {
throw new IllegalArgumentException(
"Property identifier must not be null");
}
this.propertyId = propertyId;
}
@Override
@SuppressWarnings("unchecked")
public IDTYPE getIdForBean(BEANTYPE bean)
throws IllegalArgumentException {
VaadinPropertyDescriptor pd = model.get(propertyId);
if (null == pd) {
throw new IllegalStateException(
"Property " + propertyId + " not found");
}
try {
Property property = (Property) pd
.createProperty(bean);
return property.getValue();
} catch (MethodException e) {
throw new IllegalArgumentException(e);
}
}
}
/**
* The resolver that finds the item ID for a bean, or null not to use
* automatic resolving.
*
* Methods that add a bean without specifying an ID must not be called if no
* resolver has been set.
*/
private BeanIdResolver beanIdResolver = null;
/**
* Maps all item ids in the container (including filtered) to their
* corresponding BeanItem.
*/
private final Map> itemIdToItem = new HashMap>();
/**
* The type of the beans in the container.
*/
private final Class super BEANTYPE> type;
/**
* A description of the properties found in beans of type {@link #type}.
* Determines the property ids that are present in the container.
*/
private final LinkedHashMap> model;
/**
* Constructs a {@code AbstractBeanContainer} for beans of the given type.
*
* @param type
* the type of the beans that will be added to the container.
* @throws IllegalArgumentException
* If {@code type} is null
*/
protected AbstractBeanContainer(Class super BEANTYPE> type) {
if (type == null) {
throw new IllegalArgumentException(
"The bean type passed to AbstractBeanContainer must not be null");
}
this.type = type;
model = BeanItem.getPropertyDescriptors((Class) type);
}
@Override
public Class> getType(Object propertyId) {
VaadinPropertyDescriptor descriptor = model.get(propertyId);
if (descriptor == null) {
return null;
}
return descriptor.getPropertyType();
}
/**
* Create a BeanItem for a bean using pre-parsed bean metadata (based on
* {@link #getBeanType()}).
*
* @param bean
* @return created {@link BeanItem} or null if bean is null
*/
protected BeanItem createBeanItem(BEANTYPE bean) {
return bean == null ? null : new BeanItem(bean, model);
}
/**
* Returns the type of beans this Container can contain.
*
* This comes from the bean type constructor parameter, and bean metadata
* (including container properties) is based on this.
*
* @return
*/
public Class super BEANTYPE> getBeanType() {
return type;
}
@Override
public Collection getContainerPropertyIds() {
return model.keySet();
}
@Override
public boolean removeAllItems() {
int origSize = size();
IDTYPE firstItem = getFirstVisibleItem();
internalRemoveAllItems();
// detach listeners from all Items
for (Item item : itemIdToItem.values()) {
removeAllValueChangeListeners(item);
}
itemIdToItem.clear();
// fire event only if the visible view changed, regardless of whether
// filtered out items were removed or not
if (origSize != 0) {
fireItemsRemoved(0, firstItem, origSize);
}
return true;
}
@Override
public BeanItem getItem(Object itemId) {
// TODO return only if visible?
return getUnfilteredItem(itemId);
}
@Override
protected BeanItem getUnfilteredItem(Object itemId) {
return itemIdToItem.get(itemId);
}
@Override
@SuppressWarnings("unchecked")
public List getItemIds() {
return (List) super.getItemIds();
}
@Override
public Property getContainerProperty(Object itemId, Object propertyId) {
Item item = getItem(itemId);
if (item == null) {
return null;
}
return item.getItemProperty(propertyId);
}
@Override
public boolean removeItem(Object itemId) {
// TODO should also remove items that are filtered out
int origSize = size();
Item item = getItem(itemId);
int position = indexOfId(itemId);
if (internalRemoveItem(itemId)) {
// detach listeners from Item
removeAllValueChangeListeners(item);
// remove item
itemIdToItem.remove(itemId);
// fire event only if the visible view changed, regardless of
// whether filtered out items were removed or not
if (size() != origSize) {
fireItemRemoved(position, itemId);
}
return true;
} else {
return false;
}
}
/**
* Re-filter the container when one of the monitored properties changes.
*/
@Override
public void valueChange(ValueChangeEvent event) {
// if a property that is used in a filter is changed, refresh filtering
filterAll();
}
@Override
public void addContainerFilter(Object propertyId, String filterString,
boolean ignoreCase, boolean onlyMatchPrefix) {
try {
addFilter(new SimpleStringFilter(propertyId, filterString,
ignoreCase, onlyMatchPrefix));
} catch (UnsupportedFilterException e) {
// the filter instance created here is always valid for in-memory
// containers
}
}
@Override
public void removeAllContainerFilters() {
if (!getFilters().isEmpty()) {
for (Item item : itemIdToItem.values()) {
removeAllValueChangeListeners(item);
}
removeAllFilters();
}
}
@Override
public void removeContainerFilters(Object propertyId) {
Collection removedFilters = super.removeFilters(propertyId);
if (!removedFilters.isEmpty()) {
// stop listening to change events for the property
for (Item item : itemIdToItem.values()) {
removeValueChangeListener(item, propertyId);
}
}
}
@Override
public void addContainerFilter(Filter filter)
throws UnsupportedFilterException {
addFilter(filter);
}
@Override
public void removeContainerFilter(Filter filter) {
removeFilter(filter);
}
@Override
public boolean hasContainerFilters() {
return super.hasContainerFilters();
}
@Override
public Collection getContainerFilters() {
return super.getContainerFilters();
}
/**
* Make this container listen to the given property provided it notifies
* when its value changes.
*
* @param item
* The {@link Item} that contains the property
* @param propertyId
* The id of the property
*/
private void addValueChangeListener(Item item, Object propertyId) {
Property> property = item.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);
}
}
/**
* Remove this container as a listener for the given property.
*
* @param item
* The {@link Item} that contains the property
* @param propertyId
* The id of the property
*/
private void removeValueChangeListener(Item item, Object propertyId) {
Property> property = item.getItemProperty(propertyId);
if (property instanceof ValueChangeNotifier) {
((ValueChangeNotifier) property).removeListener(this);
}
}
/**
* Remove this contains as a listener for all the properties in the given
* {@link Item}.
*
* @param item
* The {@link Item} that contains the properties
*/
private void removeAllValueChangeListeners(Item item) {
for (Object propertyId : item.getItemPropertyIds()) {
removeValueChangeListener(item, propertyId);
}
}
@Override
public Collection> getSortableContainerPropertyIds() {
return getSortablePropertyIds();
}
@Override
public void sort(Object[] propertyId, boolean[] ascending) {
sortContainer(propertyId, ascending);
}
@Override
public ItemSorter getItemSorter() {
return super.getItemSorter();
}
@Override
public void setItemSorter(ItemSorter itemSorter) {
super.setItemSorter(itemSorter);
}
@Override
protected void registerNewItem(int position, IDTYPE itemId,
BeanItem item) {
itemIdToItem.put(itemId, item);
// add listeners to be able to update filtering on property
// changes
for (Filter filter : getFilters()) {
for (String propertyId : getContainerPropertyIds()) {
if (filter.appliesToProperty(propertyId)) {
// addValueChangeListener avoids adding duplicates
addValueChangeListener(item, propertyId);
}
}
}
}
/**
* Check that a bean can be added to the container (is of the correct type
* for the container).
*
* @param bean
* @return
*/
private boolean validateBean(BEANTYPE bean) {
return bean != null && getBeanType().isAssignableFrom(bean.getClass());
}
/**
* Adds the bean to the Container.
*
* Note: the behavior of this method changed in Vaadin 6.6 - now items are
* added at the very end of the unfiltered container and not after the last
* visible item if filtering is used.
*
* @see Container#addItem(Object)
*/
protected BeanItem addItem(IDTYPE itemId, BEANTYPE bean) {
if (!validateBean(bean)) {
return null;
}
return internalAddItemAtEnd(itemId, createBeanItem(bean), true);
}
/**
* Adds the bean after the given bean.
*
* @see Container.Ordered#addItemAfter(Object, Object)
*/
protected BeanItem addItemAfter(IDTYPE previousItemId,
IDTYPE newItemId, BEANTYPE bean) {
if (!validateBean(bean)) {
return null;
}
return internalAddItemAfter(previousItemId, newItemId,
createBeanItem(bean), true);
}
/**
* Adds a new bean at the given index.
*
* The bean is used both as the item contents and as the item identifier.
*
* @param index
* Index at which the bean should be added.
* @param newItemId
* The item id for the bean to add to the container.
* @param bean
* The bean to add to the container.
*
* @return Returns the new BeanItem or null if the operation fails.
*/
protected BeanItem addItemAt(int index, IDTYPE newItemId,
BEANTYPE bean) {
if (!validateBean(bean)) {
return null;
}
return internalAddItemAt(index, newItemId, createBeanItem(bean), true);
}
/**
* Adds a bean to the container using the bean item id resolver to find its
* identifier.
*
* A bean id resolver must be set before calling this method.
*
* @see #addItem(Object, Object)
*
* @param bean
* the bean to add
* @return BeanItem item added or null
* @throws IllegalStateException
* if no bean identifier resolver has been set
* @throws IllegalArgumentException
* if an identifier cannot be resolved for the bean
*/
protected BeanItem addBean(BEANTYPE bean)
throws IllegalStateException, IllegalArgumentException {
if (bean == null) {
return null;
}
IDTYPE itemId = resolveBeanId(bean);
if (itemId == null) {
throw new IllegalArgumentException(
"Resolved identifier for a bean must not be null");
}
return addItem(itemId, bean);
}
/**
* Adds a bean to the container after a specified item identifier, using the
* bean item id resolver to find its identifier.
*
* A bean id resolver must be set before calling this method.
*
* @see #addItemAfter(Object, Object, Object)
*
* @param previousItemId
* the identifier of the bean after which this bean should be
* added, null to add to the beginning
* @param bean
* the bean to add
* @return BeanItem item added or null
* @throws IllegalStateException
* if no bean identifier resolver has been set
* @throws IllegalArgumentException
* if an identifier cannot be resolved for the bean
*/
protected BeanItem addBeanAfter(IDTYPE previousItemId,
BEANTYPE bean)
throws IllegalStateException, IllegalArgumentException {
if (bean == null) {
return null;
}
IDTYPE itemId = resolveBeanId(bean);
if (itemId == null) {
throw new IllegalArgumentException(
"Resolved identifier for a bean must not be null");
}
return addItemAfter(previousItemId, itemId, bean);
}
/**
* Adds a bean at a specified (filtered view) position in the container
* using the bean item id resolver to find its identifier.
*
* A bean id resolver must be set before calling this method.
*
* @see #addItemAfter(Object, Object, Object)
*
* @param index
* the index (in the filtered view) at which to add the item
* @param bean
* the bean to add
* @return BeanItem item added or null
* @throws IllegalStateException
* if no bean identifier resolver has been set
* @throws IllegalArgumentException
* if an identifier cannot be resolved for the bean
*/
protected BeanItem addBeanAt(int index, BEANTYPE bean)
throws IllegalStateException, IllegalArgumentException {
if (bean == null) {
return null;
}
IDTYPE itemId = resolveBeanId(bean);
if (itemId == null) {
throw new IllegalArgumentException(
"Resolved identifier for a bean must not be null");
}
return addItemAt(index, itemId, bean);
}
/**
* Adds all the beans from a {@link Collection} in one operation using the
* bean item identifier resolver. More efficient than adding them one by
* one.
*
* A bean id resolver must be set before calling this method.
*
* Note: the behavior of this method changed in Vaadin 6.6 - now items are
* added at the very end of the unfiltered container and not after the last
* visible item if filtering is used.
*
* @param collection
* The collection of beans to add. Must not be null.
* @throws IllegalStateException
* if no bean identifier resolver has been set
* @throws IllegalArgumentException
* if the resolver returns a null itemId for one of the beans in
* the collection
*/
protected void addAll(Collection extends BEANTYPE> collection)
throws IllegalStateException, IllegalArgumentException {
boolean modified = false;
int origSize = size();
for (BEANTYPE bean : collection) {
// TODO skipping invalid beans - should not allow them in javadoc?
if (bean == null
|| !getBeanType().isAssignableFrom(bean.getClass())) {
continue;
}
IDTYPE itemId = resolveBeanId(bean);
if (itemId == null) {
throw new IllegalArgumentException(
"Resolved identifier for a bean must not be null");
}
if (internalAddItemAtEnd(itemId, createBeanItem(bean),
false) != null) {
modified = true;
}
}
if (modified) {
// Filter the contents when all items have been added
if (isFiltered()) {
doFilterContainer(!getFilters().isEmpty());
}
if (visibleNewItemsWasAdded(origSize)) {
// fire event about added items
int firstPosition = origSize;
IDTYPE firstItemId = getVisibleItemIds().get(firstPosition);
int affectedItems = size() - origSize;
fireItemsAdded(firstPosition, firstItemId, affectedItems);
}
}
}
private boolean visibleNewItemsWasAdded(int origSize) {
return size() > origSize;
}
/**
* Use the bean resolver to get the identifier for a bean.
*
* @param bean
* @return resolved bean identifier, null if could not be resolved
* @throws IllegalStateException
* if no bean resolver is set
*/
protected IDTYPE resolveBeanId(BEANTYPE bean) {
if (beanIdResolver == null) {
throw new IllegalStateException(
"Bean item identifier resolver is required.");
}
return beanIdResolver.getIdForBean(bean);
}
/**
* Sets the resolver that finds the item id for a bean, or null not to use
* automatic resolving.
*
* Methods that add a bean without specifying an id must not be called if no
* resolver has been set.
*
* Note that methods taking an explicit id can be used whether a resolver
* has been defined or not.
*
* @param beanIdResolver
* to use or null to disable automatic id resolution
*/
protected void setBeanIdResolver(
BeanIdResolver beanIdResolver) {
this.beanIdResolver = beanIdResolver;
}
/**
* Returns the resolver that finds the item ID for a bean.
*
* @return resolver used or null if automatic item id resolving is disabled
*/
public BeanIdResolver getBeanIdResolver() {
return beanIdResolver;
}
/**
* Create an item identifier resolver using a named bean property.
*
* @param propertyId
* property identifier, which must map to a getter in BEANTYPE
* @return created resolver
*/
protected BeanIdResolver createBeanPropertyResolver(
Object propertyId) {
return new PropertyBasedBeanIdResolver(propertyId);
}
/**
* @deprecated As of 7.0, replaced by {@link #addPropertySetChangeListener}
**/
@Deprecated
@Override
public void addListener(Container.PropertySetChangeListener listener) {
addPropertySetChangeListener(listener);
}
@Override
public void addPropertySetChangeListener(
Container.PropertySetChangeListener listener) {
super.addPropertySetChangeListener(listener);
}
/**
* @deprecated As of 7.0, replaced by
* {@link #removePropertySetChangeListener(Container.PropertySetChangeListener)}
**/
@Deprecated
@Override
public void removeListener(Container.PropertySetChangeListener listener) {
removePropertySetChangeListener(listener);
}
@Override
public void removePropertySetChangeListener(
Container.PropertySetChangeListener listener) {
super.removePropertySetChangeListener(listener);
}
@Override
public boolean addContainerProperty(Object propertyId, Class> type,
Object defaultValue) throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Use addNestedContainerProperty(String) to add container properties to a "
+ getClass().getSimpleName());
}
/**
* Adds a property for the container and all its items.
*
* Primarily for internal use, may change in future versions.
*
* @param propertyId
* @param propertyDescriptor
* @return true if the property was added
*/
protected final boolean addContainerProperty(String propertyId,
VaadinPropertyDescriptor propertyDescriptor) {
if (null == propertyId || null == propertyDescriptor) {
return false;
}
// Fails if the Property is already present
if (model.containsKey(propertyId)) {
return false;
}
model.put(propertyId, propertyDescriptor);
for (BeanItem item : itemIdToItem.values()) {
item.addItemProperty(propertyId,
propertyDescriptor.createProperty(item.getBean()));
}
// Sends a change event
fireContainerPropertySetChange();
return true;
}
/**
* Adds a nested container property for the container, e.g.
* "manager.address.street".
*
* All intermediate getters must exist and should return non-null values
* when the property value is accessed. If an intermediate getter returns
* null, a null value will be returned.
*
* @see NestedMethodProperty
*
* @param propertyId
* @return true if the property was added
*/
public boolean addNestedContainerProperty(String propertyId) {
return addContainerProperty(propertyId,
new NestedPropertyDescriptor(propertyId, type));
}
/**
* Adds a nested container properties for all sub-properties of a named
* property to the container. The named property itself is removed from the
* model as its subproperties are added.
*
* All intermediate getters must exist and should return non-null values
* when the property value is accessed. If an intermediate getter returns
* null, a null value will be returned.
*
* @see NestedMethodProperty
* @see #addNestedContainerProperty(String)
*
* @param propertyId
*/
@SuppressWarnings("unchecked")
public void addNestedContainerBean(String propertyId) {
Class> propertyType = getType(propertyId);
LinkedHashMap> pds = BeanItem
.getPropertyDescriptors((Class