![JAR search and dependency download from the Maven repository](/logo.png)
com.vaadin.addon.jpacontainer.JPAContainer Maven / Gradle / Ivy
/*
JPAContainer
Copyright (C) 2009-2011 Oy Vaadin Ltd
This program is available under GNU Affero General Public License (version
3 or later at your option).
See the file licensing.txt distributed with this software for more
information about licensing.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package com.vaadin.addon.jpacontainer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import com.vaadin.addon.jpacontainer.filter.Filters;
import com.vaadin.addon.jpacontainer.filter.PropertyFilter;
import com.vaadin.addon.jpacontainer.filter.util.AdvancedFilterableSupport;
import com.vaadin.addon.jpacontainer.metadata.EntityClassMetadata;
import com.vaadin.addon.jpacontainer.metadata.MetadataFactory;
import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.util.filter.UnsupportedFilterException;
/**
* This is the main container class of JPAContainer (and the default
* implementation of {@link EntityContainer}). You can use it in your
* applications like so:
* EntityContainer<MyEntity> container = new JPAContainer<MyEntity>(
* MyEntity.class);
* container.setEntityProvider(myEntityProvider);
* ...
* myTable.setContainerDataSource(container);
*
In the example code, myEntityProvider
is an
* instance of an {@link EntityProvider} that, like the name suggests, is
* responsible for providing the entities to the container. If the container
* should be writable, the entity provider must implement the
* {@link MutableEntityProvider} interface and if buffering is desired (i.e.
* write-through turned off) the {@link BatchableEntityProvider} interface as
* well. There are ready-made implementations of all these interfaces that can
* be used out-of-the-box (check the See Also section of this Javadoc).
*
* All sorting and filtering is handled by the entity provider, which in turn
* normally delegates it to the database. Therefore, only persistent properties
* can be filtered and/or sorted by.
*
* It is possible to use JPAContainer as a hierarchical container if the
* entities in the container can be related to each other by means of a parent
* property. For example:
*
*
*
* @Entity
* public class Node {
* ...
* @ManyToOne
* private Node parent;
* ...
* }
*
*
*
* Note, however, that the implementation of {@link HierarchicalEntityContainer}
* is still experimental and has some limitations. For example, the data is
* always read directly from the entity provider regardless of whether buffering
* is used or not. Therefore, this feature should be used with care in
* production systems.
*
* Buffering
* Here follows some notes on how buffering has been implemented in this class.
* If you are not going to use buffering, you can skip this section.
*
* When using buffered mode, the following rules apply:
*
* - All operations that add, update or remove entities are recorded
* internally.
* - If an item is added and then removed later within the same transaction,
* the add operation will be removed and no update operation will be recorded.
* - If an item is added and then updated later within the same transaction,
* only the add operation will be recorded.
* - If an item is updated and then removed later within the same transaction,
* only the remove operation will be recorded.
* - In the case of an update or edit, a clone of the affected entity is
* recorded together with the operation if the entity class implements the
* {@link Cloneable} interface. Otherwise, the entity instance is recorded.
* - When the changes are committed, all recorded operations are carried out
* on the {@link BatchableEntityProvider} in the same order that they were
* recorded.
*
*
* Please note, that if an entity is not cloneable and it is modified twice,
* both update records will point to the same entity instance. Thus, the changes
* of the second modification will be present in the record of the first
* modification. This is something that implementations of
* {@link BatchableEntityProvider} need to be aware of.
*
* Also note, that it is possible to use buffered mode even if the entities
* returned from the entity provider are not explicitly detached (see
* {@link EntityProvider#isEntitiesDetached() }), but this should be avoided
* unless you know what you are doing.
*
* @see com.vaadin.addon.jpacontainer.provider.LocalEntityProvider
* @see com.vaadin.addon.jpacontainer.provider.CachingLocalEntityProvider
* @see com.vaadin.addon.jpacontainer.provider.BatchableLocalEntityProvider
* @author Petter Holmström (Vaadin Ltd)
* @since 1.0
*/
public class JPAContainer implements EntityContainer,
EntityProviderChangeListener, HierarchicalEntityContainer,
Container.Indexed {
private static final long serialVersionUID = -4031940552175752858L;
private EntityProvider entityProvider;
private AdvancedFilterableSupport filterSupport;
private LinkedList listeners;
private EntityClassMetadata entityClassMetadata;
private List sortByList;
private PropertyList propertyList;
private BufferedContainerDelegate bufferingDelegate;
private boolean readOnly = false;
private boolean writeThrough = false;
/**
* Creates a new JPAContainer
instance for entities of class
* entityClass
. An entity provider must be provided using the
* {@link #setEntityProvider(com.vaadin.addon.jpacontainer.EntityProvider) }
* before the container can be used.
*
* @param entityClass
* the class of the entities that will reside in this container
* (must not be null).
*/
public JPAContainer(Class entityClass) {
assert entityClass != null : "entityClass must not be null";
this.entityClassMetadata = MetadataFactory.getInstance()
.getEntityClassMetadata(entityClass);
this.propertyList = new PropertyList(entityClassMetadata);
this.filterSupport = new AdvancedFilterableSupport();
this.bufferingDelegate = new BufferedContainerDelegate(this);
/*
* Add a listener to filterSupport, so that we can notify all clients
* that use our container that the data has been filtered.
*/
this.filterSupport
.addListener(new AdvancedFilterableSupport.ApplyFiltersListener() {
private static final long serialVersionUID = -23196201919497112L;
public void filtersApplied(AdvancedFilterableSupport sender) {
fireContainerItemSetChange(new FiltersAppliedEvent(
JPAContainer.this));
}
});
updateFilterablePropertyIds();
}
private Collection additionalFilterablePropertyIds;
/**
* Sometimes, it may be necessary to filter by properties that do not show
* up in the container. This method can be used to add additional property
* IDs to the {@link #getFilterablePropertyIds() } collection. This method
* performs no propertyId validation, so it is up to the client to make sure
* the propertyIds are valid.
*
* @param propertyIds
* an array of additional propertyIds, may be null.
*/
public void setAdditionalFilterablePropertyIds(String... propertyIds) {
if (propertyIds == null || propertyIds.length == 0) {
additionalFilterablePropertyIds = null;
} else {
additionalFilterablePropertyIds = Arrays.asList(propertyIds);
}
updateFilterablePropertyIds();
}
protected void updateFilterablePropertyIds() {
// this.filterSupport
// .setFilterablePropertyIds((Collection>) propertyList
// .getPersistentPropertyNames());
HashSet properties = new HashSet();
properties.addAll(propertyList.getPersistentPropertyNames());
if (additionalFilterablePropertyIds != null) {
properties.addAll(additionalFilterablePropertyIds);
}
this.filterSupport.setFilterablePropertyIds(properties);
}
/**
* Gets the mapping metadata of the entity class.
*
* @see EntityClassMetadata
*
* @return the metadata (never null).
*/
protected EntityClassMetadata getEntityClassMetadata() {
return entityClassMetadata;
}
public void addListener(ItemSetChangeListener listener) {
if (listener == null) {
return;
}
if (listeners == null) {
listeners = new LinkedList();
}
listeners.add(listener);
}
public void removeListener(ItemSetChangeListener listener) {
if (listener != null && listeners != null) {
listeners.remove(listener);
}
}
/**
* Publishes event
to all registered
* ItemSetChangeListener
s.
*
* @param event
* the event to publish (must not be null).
*/
@SuppressWarnings("unchecked")
protected void fireContainerItemSetChange(final ItemSetChangeEvent event) {
assert event != null : "event must not be null";
if (listeners == null || !fireContainerItemSetChangeEvents) {
return;
}
LinkedList list = (LinkedList) listeners
.clone();
for (ItemSetChangeListener l : list) {
l.containerItemSetChange(event);
}
}
private boolean fireContainerItemSetChangeEvents = true;
/**
* Specifies whether the container should fire an item set change event when
* it detects a change in the entity provider (such as an entity being
* added, updated or deleted).
*/
public void setFireContainerItemSetChangeEvents(boolean value) {
this.fireContainerItemSetChangeEvents = value;
}
/**
* Tests whether the container should fire an item set change event when it
* detects a change in the entity provider (such as an entity being added,
* updated or deleted).
*
* @return true if item set change events should be fired (default), false
* otherwise.
*/
public boolean isFireContainerItemSetChangeEvents() {
return fireContainerItemSetChangeEvents;
}
public void addNestedContainerProperty(String nestedProperty)
throws UnsupportedOperationException {
propertyList.addNestedProperty(nestedProperty);
updateFilterablePropertyIds();
}
public Class getEntityClass() {
return getEntityClassMetadata().getMappedClass();
}
public EntityProvider getEntityProvider() {
return entityProvider;
}
/**
* Checks that the entity provider is not null and returns it.
*
* @return the entity provider (never null).
* @throws IllegalStateException
* if the entity provider was null.
*/
protected EntityProvider doGetEntityProvider()
throws IllegalStateException {
if (entityProvider == null) {
throw new IllegalStateException("No EntityProvider has been set");
}
return entityProvider;
}
@SuppressWarnings("unchecked")
public boolean isReadOnly() {
return !(doGetEntityProvider() instanceof MutableEntityProvider)
|| readOnly;
}
@SuppressWarnings("unchecked")
public void setEntityProvider(EntityProvider entityProvider) {
assert entityProvider != null : "entityProvider must not be null";
// Remove listener from old provider
if (this.entityProvider != null
&& this.entityProvider instanceof EntityProviderChangeNotifier) {
((EntityProviderChangeNotifier) this.entityProvider)
.removeListener(this);
}
this.entityProvider = entityProvider;
// Register listener with new provider
if (this.entityProvider instanceof EntityProviderChangeNotifier) {
((EntityProviderChangeNotifier) this.entityProvider)
.addListener(this);
}
}
private boolean fireItemSetChangeOnProviderChange = true;
/**
* Specifies whether the container should fire an ItemSetChangeEvent when an
* EntityProviderChangeEvent is received. This is used to prevent clients
* from receiving duplicate ItemSetChangeEvents when the container modifies
* data and wants to handle ItemSetChangeEvents itself.
*
* @param fireItemSetChangeOnProviderChange
* true fo fire an ItemSetChangeEvent when the provider changes,
* false not to.
*/
protected void setFireItemSetChangeOnProviderChange(
boolean fireItemSetChangeOnProviderChange) {
this.fireItemSetChangeOnProviderChange = fireItemSetChangeOnProviderChange;
}
/**
* @see #setFireItemSetChangeOnProviderChange(boolean)
* @return true if an ItemSetChangeEvent should be fired when the provider
* changes, false if it should not.
*/
protected boolean isFireItemSetChangeOnProviderChange() {
return fireItemSetChangeOnProviderChange;
}
public void entityProviderChange(EntityProviderChangeEvent event) {
if (isFireItemSetChangeOnProviderChange()) {
fireContainerItemSetChange(new ProviderChangedEvent(event));
}
}
@SuppressWarnings("unchecked")
public void setReadOnly(boolean readOnly)
throws UnsupportedOperationException {
if (readOnly) {
this.readOnly = readOnly;
} else {
if (doGetEntityProvider() instanceof MutableEntityProvider) {
this.readOnly = readOnly;
} else {
throw new UnsupportedOperationException(
"EntityProvider is not mutable");
}
}
}
/**
* Configures a property to be sortable based on another property, normally
* a sub-property of the main property to sort.
*
* For example, let's say there is a property named address
and
* that this property's type in turn has the property street
.
* Addresses are not directly sortable as they are not simple properties.
*
* If we want to be able to sort addresses based on the street property, we
* can set the sort property for address
to be
* address.street
using this method.
*
* Normally the sort property should be of the form
* propertyId + "." + subPropertyId
. Sort properties must be
* persistent and usable in JPQL, but need not be registered as separate
* properties in the container.
*
* Note that the sort property is not checked when this method is called. If
* it is not a valid sort property, an exception will be thrown when trying
* to sort a container.
*
* @param propertyId
* property for which sorting should be configured
* @param sortProperty
* property or other JPQL string that should be used when sorting
* by propertyId is requested, typically a sub-property
* propertyId
* @throws IllegalArgumentException
* if the property propertyId
is not in the
* container
* @since 1.2.1
*/
public void setSortProperty(String propertyId, String sortProperty)
throws IllegalArgumentException {
propertyList.setSortProperty(propertyId, sortProperty);
}
public Collection getSortableContainerPropertyIds() {
// This includes properties for which a separate sort property has been
// defined.
return propertyList.getSortablePropertyMap().keySet();
}
public void sort(Object[] propertyId, boolean[] ascending) {
assert propertyId != null : "propertyId must not be null";
assert ascending != null : "ascending must not be null";
assert propertyId.length == ascending.length : "propertyId and ascending must have the same length";
sortByList = new LinkedList();
for (int i = 0; i < propertyId.length; ++i) {
if (!getSortableContainerPropertyIds().contains(
propertyId[i].toString())) {
throw new IllegalArgumentException(
"No such sortable property ID: " + propertyId[i]);
}
// #7711 map property ID to a sortable sub-property if configured
Object sortProperty = propertyList.getSortablePropertyMap().get(
propertyId[i]);
sortByList.add(new SortBy(sortProperty, ascending[i]));
}
sortByList = Collections.unmodifiableList(sortByList);
fireContainerItemSetChange(new ContainerSortedEvent());
}
/**
* Gets all the properties that the items should be sorted by, if any.
*
* @return an unmodifiable, possible empty list of SortBy
* instances (never null).
*/
protected List getSortByList() {
if (sortByList == null) {
return Collections.emptyList();
} else {
return sortByList;
}
}
/**
* This functionality is not supported by this
* implementation.
*
* {@inheritDoc }
*/
public Object addItemAfter(Object previousItemId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* This functionality is not supported by this
* implementation.
*
* {@inheritDoc }
*/
public Item addItemAfter(Object previousItemId, Object newItemId)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
public Object firstItemId() {
if (isWriteThrough() || bufferingDelegate.getAddedItemIds().isEmpty()) {
Object itemId = doGetEntityProvider().getFirstEntityIdentifier(
getAppliedFiltersAsConjunction(), getSortByList());
return itemId;
} else {
return bufferingDelegate.getAddedItemIds().get(0);
}
}
public boolean isFirstId(Object itemId) {
assert itemId != null : "itemId must not be null";
return itemId.equals(firstItemId());
}
public boolean isLastId(Object itemId) {
assert itemId != null : "itemId must not be null";
return itemId.equals(lastItemId());
}
public Object lastItemId() {
Object itemId = doGetEntityProvider().getLastEntityIdentifier(
getAppliedFiltersAsConjunction(), getSortByList());
if (isWriteThrough() || bufferingDelegate.getAddedItemIds().isEmpty()) {
return itemId;
} else {
if (itemId == null) {
return bufferingDelegate.getAddedItemIds().get(
bufferingDelegate.getAddedItemIds().size() - 1);
} else {
return itemId;
}
}
}
public Object nextItemId(Object itemId) {
if (isWriteThrough() || bufferingDelegate.getAddedItemIds().isEmpty()
|| !bufferingDelegate.isAdded(itemId)) {
return doGetEntityProvider().getNextEntityIdentifier(itemId,
getAppliedFiltersAsConjunction(), getSortByList());
} else {
int ix = bufferingDelegate.getAddedItemIds().indexOf(itemId);
if (ix == bufferingDelegate.getAddedItemIds().size() - 1) {
return doGetEntityProvider().getFirstEntityIdentifier(
getAppliedFiltersAsConjunction(), getSortByList());
} else {
return bufferingDelegate.getAddedItemIds().get(ix + 1);
}
}
}
public Object prevItemId(Object itemId) {
if (isWriteThrough() || bufferingDelegate.getAddedItemIds().isEmpty()) {
return doGetEntityProvider().getPreviousEntityIdentifier(itemId,
getAppliedFiltersAsConjunction(), getSortByList());
} else {
if (bufferingDelegate.isAdded(itemId)) {
int ix = bufferingDelegate.getAddedItemIds().indexOf(itemId);
if (ix == 0) {
return null;
} else {
return bufferingDelegate.getAddedItemIds().get(ix - 1);
}
} else {
Object prevId = doGetEntityProvider()
.getPreviousEntityIdentifier(itemId,
getAppliedFiltersAsConjunction(),
getSortByList());
if (prevId == null) {
return bufferingDelegate.getAddedItemIds().get(
bufferingDelegate.getAddedItemIds().size() - 1);
} else {
return prevId;
}
}
}
}
/**
* This functionality is not supported by this
* implementation.
*
* {@inheritDoc }
*/
public boolean addContainerProperty(Object propertyId, Class> type,
Object defaultValue) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* This functionality is not supported by this
* implementation.
*
* {@inheritDoc }
*/
public Item addItem(Object itemId) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* This functionality is not supported by this
* implementation.
*
* {@inheritDoc }
*/
public Object addItem() throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
public boolean containsId(Object itemId) {
boolean result = doContainsId(itemId);
if (containsIdFiresItemSetChangeIfNotFound && !result) {
fireContainerItemSetChange(new ItemNotFoundEvent());
}
return result;
}
private boolean containsIdFiresItemSetChangeIfNotFound = false;
/**
* Returns whether the {@link #containsId(java.lang.Object) } method will
* fire an item set change event if it returns false. This may be necessary
* when using the container together with a {@link com.vaadin.ui.Table} and
* there are multiple users modifying the same data source.
*
* When a user selects an item in a Table, the table checks with the
* container if the item exists or not. If it does not exist, nothing
* happens. Normally, the item should always exist, but if the container has
* been changed after the initial set of items were fetched and cached by
* the table, there may be items in the Table that are not present in the
* container.
*
* By enabling this flag, the Table will repaint itself if it tries to
* select a nonexistent item, causing the item to dissapear from the table
* as well.
*/
public boolean isContainsIdFiresItemSetChangeIfNotFound() {
return containsIdFiresItemSetChangeIfNotFound;
}
/**
* See {@link #isContainsIdFiresItemSetChangeIfNotFound() }.
*
* @param value
*/
public void setContainsIdFiresItemSetChangeIfNotFound(boolean value) {
this.containsIdFiresItemSetChangeIfNotFound = value;
}
/**
* @see Container#containsId(java.lang.Object)
*/
protected boolean doContainsId(Object itemId) {
if (isWriteThrough()) {
return doGetEntityProvider().containsEntity(itemId,
getAppliedFiltersAsConjunction());
} else {
return bufferingDelegate.isAdded(itemId)
|| doGetEntityProvider().containsEntity(itemId,
getAppliedFiltersAsConjunction());
}
}
public Property getContainerProperty(Object itemId, Object propertyId) {
Item item = getItem(itemId);
return item == null ? null : item.getItemProperty(propertyId);
}
public Collection getContainerPropertyIds() {
return propertyList.getPropertyNames();
}
/**
* Method used by {@link EntityItem} to gain access to the property list.
* Not to be used by clients directly.
*
* @return the property list.
*/
PropertyList getPropertyList() {
// TODO Not sure whether this is a good idea, maybe the property list
// could be passed to EntityItem as a constructor parameter?
return propertyList;
}
/**
* {@inheritDoc }
*
* Please note, that this method will create a new instance of
* {@link EntityItem} upon every execution. That is, two subsequent calls to
* this method with the same itemId
will not return the
* same {@link EntityItem} instance. The actual entity instance may still be
* the same though, depending on the implementation of the entity provider.
*/
public EntityItem getItem(Object itemId) {
if (itemId == null) {
return null;
}
if (isWriteThrough() || !bufferingDelegate.isModified()) {
T entity = doGetEntityProvider().getEntity(itemId);
return entity != null ? new JPAContainerItem(this, entity)
: null;
} else {
if (bufferingDelegate.isAdded(itemId)) {
JPAContainerItem item = new JPAContainerItem(this,
bufferingDelegate.getAddedEntity(itemId), itemId, false);
return item;
} else if (bufferingDelegate.isUpdated(itemId)) {
JPAContainerItem item = new JPAContainerItem(this,
bufferingDelegate.getUpdatedEntity(itemId));
item.setDirty(true);
return item;
} else if (bufferingDelegate.isDeleted(itemId)) {
T entity = doGetEntityProvider().getEntity(itemId);
if (entity != null) {
JPAContainerItem item = new JPAContainerItem(this,
entity);
item.setDeleted(true);
return item;
} else {
return null;
}
} else {
T entity = doGetEntityProvider().getEntity(itemId);
return entity != null ? new JPAContainerItem(this, entity)
: null;
}
}
}
/**
* This impementation does not use lazy loading and performs bad
* when the number of items is large! Do not use unless you absolutely have
* to!
*
* {@inheritDoc }
*/
public Collection