package enterpriseapp.hibernate; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; 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 java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.hibernate.Criteria; import org.hibernate.EntityMode; import org.hibernate.HibernateException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.metadata.ClassMetadata; import org.hibernate.type.ComponentType; import org.hibernate.type.Type; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.vaadin.data.Container; import com.vaadin.data.Item; import com.vaadin.data.Property; import com.vaadin.data.hbnutil.ApplicationLogger; import com.vaadin.data.hbnutil.ContainerFilter; import com.vaadin.data.util.MethodProperty; import com.vaadin.data.util.converter.Converter.ConversionException; import com.vaadin.data.util.filter.SimpleStringFilter; import com.vaadin.data.util.filter.UnsupportedFilterException; import enterpriseapp.hibernate.CustomHbnContainer.EntityItem.EntityProperty; public class CustomHbnContainer<T> implements Container, Container.Indexed, Container.Sortable, Container.Filterable, Container.Hierarchical, Container.ItemSetChangeNotifier, Container.Ordered { protected static final long serialVersionUID = -6410337120924382057L; protected ApplicationLogger logger = new ApplicationLogger(CustomHbnContainer.class); protected SessionFactory sessionFactory; protected ClassMetadata classMetadata; protected Class<T> entityType; protected String parentPropertyName = null; protected static final int ROW_BUF_SIZE = 100; protected static final int ID_TO_INDEX_MAX_SIZE = 300; protected boolean normalOrder = true; protected List<T> ascRowBuffer; protected List<T> descRowBuffer; protected Object lastId; protected Object firstId; protected List<T> indexRowBuffer; protected int indexRowBufferFirstIndex; protected final Map<Object, Integer> idToIndex = new LinkedHashMap<Object, Integer>(); protected boolean[] orderAscendings; protected Object[] orderPropertyIds; protected Integer size; protected LinkedList<ItemSetChangeListener> itemSetChangeListeners; protected HashSet<ContainerFilter> filters; protected final Map<String, Class<?>> addedProperties = new HashMap<String, Class<?>>(); protected final LoadingCache<Object, EntityItem<T>> cache; protected final HashMap<Object, Boolean> embeddedPropertiesCache = new HashMap<Object, Boolean>(); public class StringContainerFilter extends ContainerFilter { public final String filterString; public final String filterString2; public final boolean onlyMatchPrefix; public final boolean ignoreCase; public StringContainerFilter(Object propertyId, String filterString, String filterString2, boolean ignoreCase, boolean onlyMatchPrefix) { super(propertyId); this.ignoreCase = ignoreCase; this.filterString = ignoreCase ? filterString.toLowerCase() : filterString; this.filterString2 = ignoreCase ? filterString2.toLowerCase() : filterString2; this.onlyMatchPrefix = onlyMatchPrefix; } public Criterion getFieldCriterion(String fullPropertyName) { return (ignoreCase) ? Restrictions.ilike(fullPropertyName, filterString, onlyMatchPrefix ? MatchMode.START : MatchMode.ANYWHERE) : Restrictions.like(fullPropertyName, filterString, onlyMatchPrefix ? MatchMode.START : MatchMode.ANYWHERE); } } /** * Item wrappping a Hibernate mapped entity object. EntityItems are generally instantiated automatically by * HbnContainer. */ @SuppressWarnings("hiding") public class EntityItem<T> implements Item { protected static final long serialVersionUID = -2847179724504965599L; /** * Reference to hibernate mapped entity that this Item wraps. */ protected T pojo; /** * Instantiated properties of this EntityItem. May be either EntityItemProperty (hibernate field) or manually * added container property (MethodProperty). */ protected Map<Object, Property<?>> properties = new HashMap<Object, Property<?>>(); @SuppressWarnings("unchecked") public EntityItem(Serializable id) { logger.executionTrace(); pojo = (T) sessionFactory.getCurrentSession().get(entityType, id); // add non-hibernate mapped container properties for (String propertyId : addedProperties.keySet()) { addItemProperty(propertyId, new MethodProperty<Object>(pojo, propertyId)); } } /** * @return the wrapped entity object. */ public T getPojo() { logger.executionTrace(); return pojo; } @SuppressWarnings("rawtypes") public boolean addItemProperty(Object id, Property property) throws UnsupportedOperationException { logger.executionTrace(); properties.put(id, property); return true; } public Property<?> getItemProperty(Object id) { logger.executionTrace(); Property<?> p = properties.get(id); if (p == null) { p = new EntityProperty(id.toString()); properties.put(id, p); } return p; } public Collection<?> getItemPropertyIds() { logger.executionTrace(); return getContainerPropertyIds(); } public boolean removeItemProperty(Object id) throws UnsupportedOperationException { logger.executionTrace(); Property<?> removed = properties.remove(id); return removed != null; } /** * EntityItemProperty wraps one Hibernate controlled field of the pojo used by EntityItem. For common fields the * field value is the same as Property value. For relation fields it is the identifier of related object or a * collection of identifiers. * * The Property is a simple data object that contains one typed value. This interface contains methods to * inspect and modify the stored value and its type, and the object's read-only state. * * The Property also defines the events ReadOnlyStatusChangeEvent and ValueChangeEvent, and the associated * listener and notifier interfaces. * * The Property.Viewer interface should be used to attach the Property to an external data source. This way the * value in the data source can be inspected using the Property interface. * * The Property.editor interface should be implemented if the value needs to be changed through the implementing * class. */ @SuppressWarnings("rawtypes") public class EntityProperty implements Property, Property.ValueChangeNotifier { protected static final long serialVersionUID = -4086774943938055297L; protected List<ValueChangeListener> valueChangeListeners; protected String propertyName; /** * Default Constructor. */ public EntityProperty(String propertyName) { logger.executionTrace(); this.propertyName = propertyName; } /** * This method gets the value that is stored by the property. The returned object is compatible with the * class returned by getType(). */ @SuppressWarnings("unchecked") @Override public Object getValue() { logger.executionTrace(); final Session session = sessionFactory.getCurrentSession(); final SessionImplementor sessionImplementor = (SessionImplementor) session; if (!sessionFactory.getCurrentSession().contains(pojo)) pojo = (T) session.get(entityType, (Serializable) getIdForPojo(pojo)); if (propertyInEmbeddedKey(propertyName)) { final ComponentType identifierType = (ComponentType) classMetadata.getIdentifierType(); final String[] propertyNames = identifierType.getPropertyNames(); for (int i = 0; i < propertyNames.length; i++) { String name = propertyNames[i]; if (name.equals(propertyName)) { final Object id = classMetadata.getIdentifier(pojo, sessionImplementor); return identifierType.getPropertyValue(id, i, EntityMode.POJO); } } } final Type propertyType = getPropertyType(); final Object propertyValue = classMetadata.getPropertyValue(pojo, propertyName); if (!propertyType.isAssociationType()) return propertyValue; if (propertyType.isCollectionType()) { if (propertyValue == null) return null; final HashSet<Serializable> identifiers = new HashSet<Serializable>(); final Collection<?> pojos = (Collection<?>) propertyValue; for (Object object : pojos) { if (!session.contains(object)) object = session.merge(object); identifiers.add(session.getIdentifier(object)); } return identifiers; } if (propertyValue == null) return null; final Class<?> propertyTypeClass = propertyType.getReturnedClass(); final ClassMetadata metadata = sessionFactory.getClassMetadata(propertyTypeClass); final Serializable identifier = metadata.getIdentifier(propertyValue, sessionImplementor); return identifier; } /** * This method tests if the Property is in read-only mode. In read-only mode calls to the method setValue * will throw ReadOnlyException and will not modify the value of the Property. */ @Override public boolean isReadOnly() { logger.executionTrace(); return false; } /** * This method sets the property's read-only mode to the specified status. This functionality is optional, * but all properties must implement the isReadOnly mode query correctly. * * HbnContainer does not implement this functionality and will throw an UnsupportedOperationException of * this method is called. */ @Override public void setReadOnly(boolean newStatus) { throw new UnsupportedOperationException(); } /** * This method sets the value of the property. * * Implementing this functionality is optional. If the functionality is missing, one should declare the * Property to be in read-only mode and throw Property.ReadOnlyException in this function. * * Note : Since Vaadin 7.0, setting the value of a non-String property as a String is no longer supported. */ @Override public void setValue(Object newValue) throws ReadOnlyException, ConversionException { logger.executionTrace(); try { final Session session = sessionFactory.getCurrentSession(); final SessionImplementor sessionImplementor = (SessionImplementor) sessionFactory .getCurrentSession(); Object value; try { if (newValue == null || getType().isAssignableFrom(newValue.getClass())) { value = newValue; } else { final Constructor<?> constr = getType().getConstructor(new Class[] { String.class }); value = constr.newInstance(new Object[] { newValue.toString() }); } if (propertyInEmbeddedKey(propertyName)) { final ComponentType identifierType = (ComponentType) classMetadata.getIdentifierType(); final String[] propertyNames = identifierType.getPropertyNames(); for (int i = 0; i < propertyNames.length; i++) { String name = propertyNames[i]; if (name.equals(propertyName)) { final Object identifier = classMetadata.getIdentifier(pojo, sessionImplementor); final Object[] values = identifierType.getPropertyValues(identifier, EntityMode.POJO); values[i] = value; identifierType.setPropertyValues(identifier, values, EntityMode.POJO); } } } else { final Type propertyType = classMetadata.getPropertyType(propertyName); if (propertyType.isCollectionType()) { final Field declaredField = entityType.getDeclaredField(propertyName); final java.lang.reflect.Type genericType = declaredField.getGenericType(); final java.lang.reflect.Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments(); final java.lang.reflect.Type assosiatedType = actualTypeArguments[0]; final String typestring = assosiatedType.toString().substring(6); // Reuse existing persistent collection if possible so Hibernate may optimize queries // properly. @SuppressWarnings("unchecked") Collection<Object> pojoCollection = (Collection<Object>) classMetadata .getPropertyValue(pojo, propertyName); if (pojoCollection == null) { pojoCollection = new HashSet<Object>(); classMetadata.setPropertyValue(pojo, propertyName, pojoCollection); } final Collection<Object> orphans = new HashSet<Object>(pojoCollection); final Collection<?> identifiers = (Collection<?>) value; for (Object id : identifiers) { final Object object = session.get(typestring, (Serializable) id); if (!pojoCollection.contains(object)) { pojoCollection.add(object); } else { orphans.remove(object); } } pojoCollection.removeAll(orphans); } else if (propertyType.isAssociationType()) { final Class<?> referencedType = classMetadata .getPropertyType(propertyName) .getReturnedClass(); final Object object = sessionFactory .getCurrentSession() .get(referencedType, (Serializable) value); classMetadata.setPropertyValue(pojo, propertyName, object); sessionFactory.getCurrentSession().merge(object); sessionFactory.getCurrentSession().saveOrUpdate(pojo); } else { classMetadata.setPropertyValue(pojo, propertyName, value); } } @SuppressWarnings("unchecked") T newPojo = (T) session.merge(pojo); pojo = newPojo; fireValueChange(); } catch (Exception e) { logger.error(e); throw new ConversionException(e); } } catch (HibernateException e) { logger.error(e); } } /** * This method registers a new value change listener for this property. */ @Override public void addListener(ValueChangeListener listener) { logger.executionTrace(); if (valueChangeListeners == null) valueChangeListeners = new LinkedList<ValueChangeListener>(); if (!valueChangeListeners.contains(listener)) valueChangeListeners.add(listener); } /** * This method removes a previously registered value change listener. */ @Override public void removeListener(ValueChangeListener listener) { logger.executionTrace(); if (valueChangeListeners != null) valueChangeListeners.remove(listener); } /** * This method registers a new value change listener for this property. */ @Override public void addValueChangeListener(ValueChangeListener listener) { logger.executionTrace(); addListener(listener); } /** * This method removes a previously registered value change listener. */ @Override public void removeValueChangeListener(ValueChangeListener listener) { logger.executionTrace(); removeListener(listener); } /** * This method returns a string representation of the object. In general, the toString method returns a * string that "textually represents" this object. The result should be a concise but informative * representation that is easy for a person to read. It is recommended that all subclasses override this * method. * * The toString method for class Object returns a string consisting of the name of the class of which the * object is an instance, the at-sign character `@', and the unsigned hexadecimal representation of the hash * code of the object. In other words, this method returns a string equal to the value of: * * getClass().getName() + '@' + Integer.toHexString(hashCode()) */ @Override public String toString() { logger.executionTrace(); final Object value = getValue(); return (value != null) ? value.toString() : null; } /** * This method returns a reference to the containing EntityItem. */ public EntityItem<T> getEntityItem() { logger.executionTrace(); return EntityItem.this; } /** * This method returns a reference to the associated pojo. */ public T getPojo() { logger.executionTrace(); return pojo; } /** * This method returns the raw type of this property. */ protected Type getPropertyType() { logger.executionTrace(); return classMetadata.getPropertyType(propertyName); } /** * Returns the type of the Property. The methods getValue and setValue must be compatible with this type: * one must be able to safely cast the value returned from getValue to the given type and pass any variable * assignable to this type as an argument to setValue. */ public Class<?> getType() { logger.executionTrace(); if (propertyInEmbeddedKey(propertyName)) { final ComponentType idType = (ComponentType) classMetadata.getIdentifierType(); final String[] propertyNames = idType.getPropertyNames(); for (String name : propertyNames) { if (name.equals(propertyName)) { try { final String identifierName = classMetadata.getIdentifierPropertyName(); final Field identifierField = entityType.getDeclaredField(identifierName); final Field propertyField = identifierField.getType().getDeclaredField(propertyName); return propertyField.getType(); } catch (NoSuchFieldException e) { logger.error(e); throw new RuntimeException("Failed to find the type of the container property.", e); } } } } final Type propertyType = getPropertyType(); if (propertyType.isCollectionType()) { final Class<?> returnedClass = propertyType.getReturnedClass(); return returnedClass; } if (propertyType.isAssociationType()) { // For association the the property value type is the type of referenced types identifier. final ClassMetadata metadata = sessionFactory.getClassMetadata( classMetadata.getPropertyType(propertyName).getReturnedClass()); return metadata.getIdentifierType().getReturnedClass(); } return classMetadata.getPropertyType(propertyName).getReturnedClass(); } /** * Implements a value change event. */ protected class HbnPropertyValueChangeEvent implements Property.ValueChangeEvent { protected static final long serialVersionUID = 166764621324404579L; public Property<?> getProperty() { return EntityProperty.this; } } /** * This method is used to fire a value change event. */ protected void fireValueChange() { logger.executionTrace(); if (valueChangeListeners != null) { final HbnPropertyValueChangeEvent event = new HbnPropertyValueChangeEvent(); final Object[] array = valueChangeListeners.toArray(); for (int i = 0; i < array.length; i++) { final ValueChangeListener listener = (ValueChangeListener) array[i]; listener.valueChange(event); } } } } } /** * Constructor creates a new instance of HbnContainer. */ public CustomHbnContainer(Class<T> entityType, SessionFactory sessionFactory) { logger.executionTrace(); this.entityType = entityType; this.sessionFactory = sessionFactory; this.classMetadata = sessionFactory.getClassMetadata(entityType); this.cache = CacheBuilder.newBuilder() .expireAfterAccess(2, TimeUnit.MINUTES) .maximumSize(10000) .recordStats() .weakValues() .build(new CacheLoader<Object, EntityItem<T>>() { @Override public EntityItem<T> load(Object entityId) throws Exception { try { return loadEntity((Serializable) entityId); } catch (Exception e) { logger.error(e); throw e; } } }); } /** * This method is used to load an entity from the database. This method is called automatically by the cache loader * when it needs to load an entity into the cache but it can be called manually if necessary. */ protected EntityItem<T> loadEntity(Serializable entityId) { logger.executionTrace(); EntityItem<T> entity = null; if (entityId != null) entity = new EntityItem<T>(entityId); return entity; } /** * This method is used to save an entity to the database and in the process it will fire an item set change event. */ public Serializable saveEntity(T entity) { logger.executionTrace(); final Session session = sessionFactory.getCurrentSession(); final Object entityId = session.save(entity); clearInternalCache(); fireItemSetChange(); return (Serializable) entityId; } /** * This method is used to update an entity in the database, update the cache and fire value change events when * necessary. */ public Serializable updateEntity(T entity) { logger.executionTrace(); final Session session = sessionFactory.getCurrentSession(); session.update(entity); final Object entityId = getIdForPojo(entity); final EntityItem<T> cachedEntity = cache.getIfPresent(entityId); cache.refresh(entityId); if (cachedEntity != null) { for (Object propertyId : cachedEntity.getItemPropertyIds()) { Property<?> cachedProperty = cachedEntity.getItemProperty(propertyId); if (cachedProperty instanceof EntityItem.EntityProperty) { @SuppressWarnings("rawtypes") EntityProperty entityProperty = (EntityProperty) cachedProperty; entityProperty.fireValueChange(); } } } return (Serializable) entityId; } /** * This method adds a new property to all items in the container. The property id, data type and default value of * the new Property are given as parameters. HbnContainer automatically adds all fields that are mapped by Hibernate * to the database. With this method we can add a property to the container that is contained in the pojo but not * Hibernate mapped. */ @Override public boolean addContainerProperty(Object propertyId, Class<?> classType, Object defaultValue) throws UnsupportedOperationException { logger.executionTrace(); boolean propertyExists = true; try { new MethodProperty<Object>(this.entityType.newInstance(), propertyId.toString()); } catch (Exception e) { logger.debug("Note: this is not an error: " + e); propertyExists = false; } addedProperties.put(propertyId.toString(), classType); return propertyExists; } /** * Creates a new Item into the Container and assigns it an automatic ID. The new ID is returned, or null if the * operation fails. After a successful call you can use the getItemmethod to fetch the Item. This functionality is * optional. */ @Override public Object addItem() throws UnsupportedOperationException { logger.executionTrace(); try { final T entity = entityType.newInstance(); return saveEntity(entity); } catch (Exception e) { logger.error(e); return null; } } /** * Creates a new Item with the given ID in the Container. The new Item is returned, and it is ready to have its * Properties modified. Returns null if the operation fails or the Container already contains a Item with the given * ID. This functionality is optional. * * Note that in this implementation we are expecting auto-generated identifiers so this method is not implemented. */ @Override public Item addItem(Object entityId) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Tests if the Container contains the specified Item. Filtering can hide items so that they will not be visible * through the container API, and this method should respect visibility of items (i.e. only indicate visible items * as being in the container) if feasible for the container. */ @Override public boolean containsId(Object entityId) { logger.executionTrace(); try { final EntityItem<T> entity = cache.get(entityId); return (entity != null); } catch (Exception e) { logger.error(e); return false; } } /** * Gets the Property identified by the given entityId and propertyId from the Container. If the Container does not * contain the item or it is filtered out, or the Container does not have the Property, null is returned. */ @Override public Property<?> getContainerProperty(Object entityId, Object propertyId) { logger.executionTrace(); try { EntityItem<?> entity = cache.get(entityId); Property<?> property = entity.getItemProperty(propertyId); return property; } catch (Exception e) { logger.error(e); return null; } } /** * Gets the ID's of all Properties stored in the Container. The ID's cannot be modified through the returned * collection. */ @Override public Collection<String> getContainerPropertyIds() { logger.executionTrace(); Collection<String> propertyIds = getSortableContainerPropertyIds(); propertyIds.addAll(addedProperties.keySet()); return propertyIds; } /** * This is an HbnContainer specific utility method that is used to retrieve the list of embedded property key * identifiers. */ protected Collection<String> getEmbeddedKeyPropertyIds() { logger.executionTrace(); final ArrayList<String> embeddedKeyPropertyIds = new ArrayList<String>(); final Type identifierType = classMetadata.getIdentifierType(); if (identifierType.isComponentType()) { final ComponentType idComponent = (ComponentType) identifierType; final String[] propertyNameArray = idComponent.getPropertyNames(); if (propertyNameArray != null) { final List<String> propertyNames = Arrays.asList(propertyNameArray); embeddedKeyPropertyIds.addAll(propertyNames); } } return embeddedKeyPropertyIds; } /** * Gets the Item with the given Item ID from the Container. If the Container does not contain the requested Item, * null is returned. Containers should not return Items that are filtered out. */ @Override public EntityItem<T> getItem(Object entityId) { logger.executionTrace(); try { return cache.get(entityId); } catch (ExecutionException e) { logger.error(e); return null; } } /** * Gets the ID's of all visible (after filtering and sorting) Items stored in the Container. The ID's cannot be * modified through the returned collection. If the container is Container.Ordered, the collection returned by this * method should follow that order. If the container is Container.Sortable, the items should be in the sorted order. * Calling this method for large lazy containers can be an expensive operation and should be avoided when practical. * * Create an optimized query to return only identifiers. Note that this method does not scale well for large * database. At least Table is optimized so that it does not call this method. */ @Override public Collection<?> getItemIds() { logger.executionTrace(); // TODO: BUG: does not preserve sort order! final Criteria criteria = getCriteria(); criteria.setProjection(Projections.id()); return criteria.list(); } /** * Get numberOfItems consecutive item ids from the container, starting with the item id at startIndex. * * Implementations should return at most numberOfItems item ids, but can contain less if the container has less * items than required to fulfill the request. The returned list must hence contain all of the item ids from the * range: * * startIndex to max(startIndex + (numberOfItems-1), container.size()-1). */ @Override public List<?> getItemIds(int startIndex, int count) { logger.executionTrace(); final List<?> entityIds = (List<?>) getItemIds(); return entityIds.subList(startIndex, startIndex + count); } /** * Gets the data type of all Properties identified by the given Property ID. This method does pretty much the same * thing as EntityItemProperty#getType() */ public Class<?> getType(Object propertyId) { logger.executionTrace(); // TODO: refactor to use same code as EntityItemProperty#getType() // This will also fix incomplete implementation of this method (for association types). Not critical as // Components don't really rely on this methods. if (addedProperties.keySet().contains(propertyId)) return addedProperties.get(propertyId); if (propertyInEmbeddedKey(propertyId)) { final ComponentType idType = (ComponentType) classMetadata.getIdentifierType(); final String[] propertyNames = idType.getPropertyNames(); for (int i = 0; i < propertyNames.length; i++) { String name = propertyNames[i]; if (name.equals(propertyId)) { String idName = classMetadata.getIdentifierPropertyName(); try { Field idField = entityType.getDeclaredField(idName); Field propertyField = idField.getType().getDeclaredField((String) propertyId); return propertyField.getType(); } catch (NoSuchFieldException ex) { throw new RuntimeException("Could not find the type of specified container property.", ex); } } } } Type propertyType = classMetadata.getPropertyType(propertyId.toString()); return propertyType.getReturnedClass(); } /** * Removes all Items from the Container. Note that Property ID and type information is preserved. This functionality * is optional. */ @Override public boolean removeAllItems() throws UnsupportedOperationException { logger.executionTrace(); try { final Session session = sessionFactory.getCurrentSession(); final Query query = session.createQuery("DELETE FROM " + entityType.getSimpleName()); final int deleted = query.executeUpdate(); cache.invalidateAll(); if (deleted > 0) { clearInternalCache(); fireItemSetChange(); } return (size() == 0); } catch (Exception e) { logger.error(e); return false; } } /** * Removes a Property specified by the given Property ID from the Container. Note that the Property will be removed * from all Items in the Container. This functionality is optional. */ @Override public boolean removeContainerProperty(Object propertyId) throws UnsupportedOperationException { logger.executionTrace(); final Class<?> removed = addedProperties.remove(propertyId); return (removed != null); } /** * Removes the Item identified by entityId from the Container. Containers that support filtering should also allow * removing an item that is currently filtered out. This functionality is optional. * * Note that this method recursively removes all children of this entity before removing this entity. */ @Override public boolean removeItem(Object entityId) throws UnsupportedOperationException { logger.executionTrace(); for (Object id : getChildren(entityId)) removeItem(id); final Session session = sessionFactory.getCurrentSession(); final Object entity = session.load(entityType, (Serializable) entityId); session.delete(entity); cache.invalidate(entityId); clearInternalCache(); fireItemSetChange(); return true; } /** * Gets the number of visible Items in the Container. Filtering can hide items so that they will not be visible * through the container API. */ @Override public int size() { logger.executionTrace(); size = ((Number) getBaseCriteria() .setProjection(Projections.rowCount()) .uniqueResult()) .intValue(); return size.intValue(); } /** * Adds a new item after the given item. Adding an item after null item adds the item as first item of the ordered * container. Note that we can't implement properly for database backed container like this so it is unsupported. */ @Override public Object addItemAfter(Object previousEntityId) throws UnsupportedOperationException { logger.executionTrace(); throw new UnsupportedOperationException(); } /** * Adds a new item after the given item. Adding an item after null item adds the item as first item of the ordered * container. Note that we can't implement properly for database backed container like this so it is unsupported. */ @Override public Item addItemAfter(Object previousEntityId, Object newEntityId) throws UnsupportedOperationException { logger.executionTrace(); throw new UnsupportedOperationException(); } /** * Gets the ID of the first Item in the Container. */ @Override public Object firstItemId() { logger.executionTrace(); final Object firstPojo = getCriteria() .setMaxResults(1) .setCacheable(true) .uniqueResult(); firstId = getIdForPojo(firstPojo); idToIndex.put(firstId, normalOrder ? 0 : size() - 1); return firstId; } /** * Tests if the Item corresponding to the given Item ID is the first Item in the Container. */ @Override public boolean isFirstId(Object entityId) { logger.executionTrace(); return entityId.equals(firstItemId()); } /** * Tests if the Item corresponding to the given Item ID is the last Item in the Container. */ @Override public boolean isLastId(Object entityId) { logger.executionTrace(); return entityId.equals(lastItemId()); } /** * Gets the ID of the last Item in the Container. */ @Override public Object lastItemId() { logger.executionTrace(); if (lastId == null) { normalOrder = !normalOrder; lastId = firstItemId(); normalOrder = !normalOrder; } return lastId; } /** * Gets the ID of the Item following the Item that corresponds to entityId. If the given Item is the last or not * found in the Container, null is returned. * * This is a simple method but it contains a lot of code. The complicated logic is needed to avoid: * * - a large number of database queries - scrolling through a large query result * * This way this container can be used with large data sets. */ @Override public Object nextItemId(Object entityId) { logger.executionTrace(); EntityItem<T> entity = null; List<T> rowBuffer = null; try { entity = cache.get(entityId); rowBuffer = getRowBuffer(); } catch (Exception e) { logger.error(e); return null; } try { int index; if ((index = rowBuffer.indexOf(entity.getPojo())) != -1) { final T nextEntity = rowBuffer.get(index + 1); return getIdForPojo(nextEntity); } } catch (Exception e) // entityId is not in rowBuffer, suppress the exception { } int currentIndex = indexOfId(entityId); int size = size(); int firstIndex = (normalOrder) ? currentIndex + 1 : size - currentIndex; if (firstIndex < 0 || firstIndex >= size) return null; final Criteria criteria = getCriteria() .setFirstResult(firstIndex) .setMaxResults(ROW_BUF_SIZE); @SuppressWarnings("unchecked") final List<T> newRowBuffer = criteria.list(); if (newRowBuffer.size() > 0) { setRowBuffer(newRowBuffer, firstIndex); final T nextPojo = newRowBuffer.get(0); return getIdForPojo(nextPojo); } return null; } /** * Gets the ID of the Item preceding the Item that corresponds to entityId. If the given Item is the first or not * found in the Container, null is returned. */ @Override public Object prevItemId(Object entityId) { logger.executionTrace(); normalOrder = !normalOrder; Object previous = nextItemId(entityId); normalOrder = !normalOrder; return previous; } /** * Not supported in HbnContainer. Indexing/order is controlled by underlying database. */ @Override public Object addItemAt(int index) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Not supported in HbnContainer. Indexing/order is controlled by underlying database. */ @Override public Item addItemAt(int index, Object newEntityId) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Get the item id for the item at the position given by index. */ @Override public Object getIdByIndex(int index) { logger.executionTrace(); if (indexRowBuffer == null) resetIndexRowBuffer(index); int indexInCache = index - indexRowBufferFirstIndex; if (!(indexInCache >= 0 && indexInCache < indexRowBuffer.size())) { resetIndexRowBuffer(index); indexInCache = 0; } final T pojo = indexRowBuffer.get(indexInCache); final Object id = getIdForPojo(pojo); idToIndex.put(id, new Integer(index)); if (idToIndex.size() > ID_TO_INDEX_MAX_SIZE) idToIndex.remove(idToIndex.keySet().iterator().next()); return id; } /** * Gets the index of the Item corresponding to the entityId. The following is true for the returned index: 0 <= * index < size(), or index = -1 if there is no visible item with that id in the container. * * Note! Expects that getIdByIndex is called for this entityId. Otherwise it will be potentially rather slow * operation with large tables. When used with Table, this shouldn't be a problem. */ @Override public int indexOfId(Object entityId) { logger.executionTrace(); final Integer index = idToIndex.get(entityId); return (index == null) ? slowIndexOfId(entityId) : index; } /** * Gets the container property IDs which can be used to sort the items. */ @Override public Collection<String> getSortableContainerPropertyIds() { logger.executionTrace(); final String[] propertyNames = classMetadata.getPropertyNames(); final LinkedList<String> propertyIds = new LinkedList<String>(); propertyIds.addAll(Arrays.asList(propertyNames)); propertyIds.addAll(getEmbeddedKeyPropertyIds()); return propertyIds; } /** * Sort method. Sorts the container items. Sorting a container can irreversibly change the order of its items or * only change the order temporarily, depending on the container. * * HbnContainer does not actually sort anything here, just clearing cache will do the thing lazily. */ @Override public void sort(Object[] propertyId, boolean[] ascending) { logger.executionTrace(); clearInternalCache(); orderPropertyIds = propertyId; orderAscendings = ascending; } /** * Remove all active filters from the container. */ @Override public void removeAllContainerFilters() { logger.executionTrace(); if (filters != null) { filters = null; clearInternalCache(); fireItemSetChange(); } } /** * HbnContainer only supports old style addContainerFilter(Object, String, boolean booblean) API and * {@link SimpleStringFilter}. Support for this newer API maybe in upcoming versions. * * Also note that for complex filtering it is possible to override {@link #getBaseCriteria()} method and add filter * so the query directly. */ // TODO support new filtering api properly @Override public void addContainerFilter(Filter filter) throws UnsupportedFilterException { logger.executionTrace(); if (!(filter instanceof SimpleStringFilter)) { final String message = "HbnContainer only supports old style addContainerFilter(Object, String, boolean booblean) API"; throw new UnsupportedFilterException(message); } final SimpleStringFilter sf = (SimpleStringFilter) filter; final String filterString = sf.getFilterString(); final Object propertyId = sf.getPropertyId(); final boolean ignoreCase = sf.isIgnoreCase(); final boolean onlyMatchPrefix = sf.isOnlyMatchPrefix(); addContainerFilter(propertyId, filterString, "", ignoreCase, onlyMatchPrefix); // TODO: empty string? } /** * Finds the identifiers for the children of the given item. The returned collection is unmodifiable. */ @Override public Collection<?> getChildren(Object entityId) { logger.executionTrace(); final ArrayList<Object> children = new ArrayList<Object>(); try { parentPropertyName = getParentPropertyName(); if (parentPropertyName == null) return children; for (Object id : getItemIds()) { EntityItem<T> entity = cache.get(id); Property<?> property = entity.getItemProperty(parentPropertyName); Object value = property.getValue(); if (entityId.equals(value)) children.add(id); } } catch (Exception e) { logger.error(e); } return children; } /** * Gets the identifier of the given item's parent. If there is no parent or we are unable to infer the name of the * parent property this method will return null. */ @Override public Object getParent(Object entityId) { logger.executionTrace(); try { parentPropertyName = getParentPropertyName(); if (parentPropertyName == null) { logger.warn("failed to find a parent property name; hierarchy may be incomplete."); return null; } final EntityItem<T> entity = cache.get(entityId); final Property<?> property = entity.getItemProperty(parentPropertyName); final Object value = property.getValue(); return value; } catch (Exception e) { logger.error(e); return null; } } /** * Gets the IDs of all Items in the container that don't have a parent. Such items are called root Items. The * returned collection is unmodifiable. */ @Override public Collection<?> rootItemIds() { logger.executionTrace(); final ArrayList<Object> rootItems = new ArrayList<Object>(); try { parentPropertyName = getParentPropertyName(); if (parentPropertyName == null) { logger.warn("failed to find a parent property name; hierarchy may be incomplete."); return rootItems; } final Collection<?> allItemIds = getItemIds(); for (Object id : allItemIds) { EntityItem<T> entity = cache.get(id); Property<?> property = entity.getItemProperty(parentPropertyName); Object value = property.getValue(); if (value == null) rootItems.add(id); } } catch (Exception e) { logger.error(e); } return rootItems; } /** * Sets the parent of an Item. The new parent item must exist and be able to have children. ( * areChildrenAllowed(Object) == true ). It is also possible to detach a node from the hierarchy (and thus make it * root) by setting the parent null. This operation is optional. */ @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public boolean setParent(Object entityId, Object newParentId) { logger.executionTrace(); try { parentPropertyName = getParentPropertyName(); if (parentPropertyName == null) { logger.warn("failed to find a parent property name; unable to set the parent."); return false; } final EntityItem<T> item = cache.get(entityId); final Property property = item.getItemProperty(parentPropertyName); property.setValue(newParentId); final Object value = property.getValue(); return (value.equals(newParentId)); } catch (Exception e) { logger.error(e); return false; } } /** * Tests if the Item with given ID can have children. */ @Override public boolean areChildrenAllowed(Object entityId) { logger.executionTrace(); if ((parentPropertyName = getParentPropertyName()) != null) return containsId(entityId); return false; } /** * Sets the given Item's capability to have children. If the Item identified with entityId already has children and * areChildrenAllowed(Object) is false this method fails and false is returned. * * The children must be first explicitly removed with setParent(Object entityId, Object newParentId)or * com.vaadin.data.Container.removeItem(Object entityId). * * This operation is optional. If it is not implemented, the method always returns false. */ @Override public boolean setChildrenAllowed(Object entityId, boolean areChildrenAllowed) { logger.executionTrace(); return false; } /** * Tests if the Item specified with entityId is a root Item. The hierarchical container can have more than one root * and must have at least one unless it is empty. The getParent(Object entityId) method always returns null for root * Items. */ @Override public boolean isRoot(Object entityId) { logger.executionTrace(); try { parentPropertyName = getParentPropertyName(); if (parentPropertyName == null) { logger.warn("failed to find a parent property name; hierarchy may be incomplete."); return false; } final EntityItem<T> item = cache.get(entityId); final Property<?> property = item.getItemProperty(parentPropertyName); final Object value = property.getValue(); return (value == null); } catch (Exception e) { logger.error(e); return false; } } /** * Tests if the Item specified with entityId has child Items or if it is a leaf. The getChildren(Object entityId) * method always returns null for leaf Items. * * Note that being a leaf does not imply whether or not an Item is allowed to have children. */ @Override public boolean hasChildren(Object entityId) { logger.executionTrace(); try { parentPropertyName = getParentPropertyName(); if (parentPropertyName == null) { logger.warn("failed to find a parent property name; hierarchy may be incomplete."); return false; } for (Object id : getItemIds()) { EntityItem<T> item = cache.get(id); Property<?> property = item.getItemProperty(parentPropertyName); Object value = property.getValue(); if (entityId.equals(value)) return true; } return false; } catch (Exception e) { logger.error(e); return false; } } /** * Adds an Item set change listener for the object. */ @Override public void addItemSetChangeListener(ItemSetChangeListener listener) { logger.executionTrace(); if (itemSetChangeListeners == null) itemSetChangeListeners = new LinkedList<ItemSetChangeListener>(); itemSetChangeListeners.add(listener); } /** * Removes the Item set change listener from the object. */ @Override public void removeItemSetChangeListener(ItemSetChangeListener listener) { logger.executionTrace(); if (itemSetChangeListeners != null) itemSetChangeListeners.remove(listener); } /** * Adds an Item set change listener for the object. This method is deprecated. You should use * addItemSetChangeListener() instead. */ @Override @Deprecated public void addListener(ItemSetChangeListener listener) { logger.executionTrace(); addItemSetChangeListener(listener); } /** * Removes the Item set change listener from the object. This method is deprecated. You should use * addItemSetChangeListener() instead. */ @Override @Deprecated public void removeListener(ItemSetChangeListener listener) { logger.executionTrace(); removeItemSetChangeListener(listener); } // // UTILITY METHODS // /** * This method was added mainly to allow unit tests to be written to cover the filter add/remove methods. */ public Set<Filter> getContainerFilters() { return null; // return filters! } /** * This method was added mainly to allow unit tests to be written to cover the listener add/remove methods. */ public List<ItemSetChangeListener> getItemSetChangeListeners() { return itemSetChangeListeners; } /** * This is an internal HbnContainer utility method. Determines if a property is contained within an embedded key. */ protected boolean propertyInEmbeddedKey(Object propertyId) { logger.executionTrace(); if (embeddedPropertiesCache.containsKey(propertyId)) return embeddedPropertiesCache.get(propertyId); final Type identifierType = classMetadata.getIdentifierType(); if (identifierType.isComponentType()) { final ComponentType componentType = (ComponentType) identifierType; final String[] idPropertyNames = componentType.getPropertyNames(); final List<String> idPropertyNameList = Arrays.asList(idPropertyNames); return idPropertyNameList.contains(propertyId); } return false; } /** * This is an internal HbnContainer utility method. This method triggers events associated with the * ItemSetChangeListener. */ protected void fireItemSetChange() { logger.executionTrace(); if (itemSetChangeListeners != null) { final Object[] changeListeners = itemSetChangeListeners.toArray(); final Container.ItemSetChangeEvent changeEvent = new Container.ItemSetChangeEvent() { protected static final long serialVersionUID = -3002746333251784195L; public Container getContainer() { return CustomHbnContainer.this; } }; for (int i = 0; i < changeListeners.length; i++) { ItemSetChangeListener changeListener = (ItemSetChangeListener) changeListeners[i]; changeListener.containerItemSetChange(changeEvent); } } } /** * This is an internal HbnContainer utility method. Gets a base listing using current ordering criteria. */ protected Criteria getCriteria() { logger.executionTrace(); final Criteria criteria = getBaseCriteria(); final List<Order> orders = getOrder(!normalOrder); for (Order order : orders) { criteria.addOrder(order); } return criteria; } /** * This is an internal HbnContainer utility method. Return the ordering criteria in the order in which they should * be applied. The composed order must be stable and must include {@link #getNaturalOrder(boolean)} at the end. */ protected final List<Order> getOrder(boolean flipOrder) { logger.executionTrace(); final List<Order> orders = new ArrayList<Order>(); orders.addAll(getDefaultOrder(flipOrder)); orders.add(getNaturalOrder(flipOrder)); return orders; } /** * This is an internal HbnContainer utility method. Returns the ordering to use for the container contents. The * default implementation provides the {@link Container.Sortable} functionality. Can be overridden to customize item * sort order. */ protected List<Order> getDefaultOrder(boolean flipOrder) { logger.executionTrace(); final List<Order> orders = new ArrayList<Order>(); if (orderPropertyIds != null) { for (int i = 0; i < orderPropertyIds.length; i++) { String propertyId = orderPropertyIds[i].toString(); if (propertyInEmbeddedKey(propertyId)) propertyId = classMetadata.getIdentifierPropertyName() + "." + propertyId; boolean ascending = (flipOrder) ? !orderAscendings[i] : orderAscendings[i]; Order order = (ascending) ? Order.asc(propertyId) : Order.desc(propertyId); orders.add(order); } } return orders; } /** * This is an internal HbnContainer utility method. Creates the base criteria for entity class and add possible * restrictions to query. This method is protected so developers can add their own custom criteria. */ protected Criteria getBaseCriteria() { logger.executionTrace(); final Session session = sessionFactory.getCurrentSession(); Criteria criteria = session.createCriteria(entityType); if (filters != null) { for (ContainerFilter filter : filters) { String idName = null; if (propertyInEmbeddedKey(filter.getPropertyId())) idName = classMetadata.getIdentifierPropertyName(); criteria = criteria.add(filter.getCriterion(idName)); } } return criteria; } /** * This is an internal HbnContainer utility method. Natural order is the order in which the database is sorted if * container has no other ordering set. Natural order is always added as least significant order to queries. This is * needed to keep items stable order across queries. The default implementation sorts entities by identifier column. */ protected Order getNaturalOrder(boolean flipOrder) { logger.executionTrace(); final String propertyName = getIdPropertyName(); return (flipOrder) ? Order.desc(propertyName) : Order.asc(propertyName); } /** * This is an internal HbnContainer utility method to detect identifier of given entity object. */ protected Object getIdForPojo(Object pojo) { logger.executionTrace(); final Session session = sessionFactory.getCurrentSession(); return classMetadata.getIdentifier(pojo, (SessionImplementor) session); } /** * This is an internal HbnContainer utility method. RowBuffer stores a list of entity items to avoid excessive * number of DB queries. */ protected List<T> getRowBuffer() { logger.executionTrace(); return (normalOrder) ? ascRowBuffer : descRowBuffer; } /** * This is an internal HbnContainer utility method. RowBuffer stores some pojos to avoid excessive number of DB * queries. Also updates the idToIndex map. */ protected void setRowBuffer(List<T> list, int firstIndex) { logger.executionTrace(); if (normalOrder) { ascRowBuffer = list; for (int i = 0; i < list.size(); ++i) { idToIndex.put(getIdForPojo(list.get(i)), firstIndex + i); } } else { descRowBuffer = list; final int lastIndex = size() - 1; for (int i = 0; i < list.size(); ++i) { idToIndex.put(getIdForPojo(list.get(i)), lastIndex - firstIndex - i); } } } /** * This is an internal HbnContainer utility method that gets the property name of the identifier. */ protected String getIdPropertyName() { logger.executionTrace(); return classMetadata.getIdentifierPropertyName(); } /** * This is an internal HbnContainer utility method to query new set of entity items to cache from given index. */ @SuppressWarnings("unchecked") protected void resetIndexRowBuffer(int index) { logger.executionTrace(); indexRowBufferFirstIndex = index; indexRowBuffer = getCriteria().setFirstResult(index).setMaxResults(ROW_BUF_SIZE).list(); } /** * This is an internal HbnContainer utility method that gets the index of the given identifier. */ protected int slowIndexOfId(Object entityId) { logger.executionTrace(); final Criteria criteria = getCriteria().setProjection(Projections.id()); final List<?> list = criteria.list(); return list.indexOf(entityId); } /** * This is an internal HbnContainer utility method. Adds container filter for hibernate mapped property. For * property not mapped by Hibernate. */ public void addContainerFilter(Object propertyId, String filterString, String filterString2, boolean ignoreCase, boolean onlyMatchPrefix) { logger.executionTrace(); addContainerFilter(new StringContainerFilter(propertyId, filterString, filterString2, ignoreCase, onlyMatchPrefix)); } /** * This is an internal HbnContainer utility method that adds a container filter. */ public void addContainerFilter(ContainerFilter containerFilter) { logger.executionTrace(); if (addedProperties.containsKey(containerFilter.getPropertyId())) { final String message = "HbnContainer does not support filtering properties not mapped by Hibernate"; throw new UnsupportedOperationException(message); } if (filters == null) filters = new HashSet<ContainerFilter>(); filters.add(containerFilter); clearInternalCache(); fireItemSetChange(); } /** * This is an internal HbnContainer utility method that removes container filters for the given property identifier. */ public void removeContainerFilters(Object propertyId) { logger.executionTrace(); if (filters != null) { for (Iterator<ContainerFilter> iterator = filters.iterator(); iterator.hasNext();) { ContainerFilter containerFilter = iterator.next(); if (containerFilter.getPropertyId().equals(propertyId)) iterator.remove(); } clearInternalCache(); fireItemSetChange(); } } /** * This is an internal HbnContainer utility method that removes the given container filter. */ @Override public void removeContainerFilter(Filter filter) { logger.executionTrace(); // TODO support new filtering api properly // TODO the workaround for SimpleStringFilter works wrong, but hopefully will be good enough for now if (filter instanceof SimpleStringFilter) { final SimpleStringFilter sf = (SimpleStringFilter) filter; final Object propertyId = sf.getPropertyId(); removeContainerFilters(propertyId); } } /** * This is an internal HbnContainer utility method that infers the name of the parent field belonging to the current * property based on type. */ protected String getParentPropertyName() { logger.executionTrace(); // TODO: make this a little more robust, there are a number of cases where this will fail. if (parentPropertyName == null) { String[] propertyNames = classMetadata.getPropertyNames(); for (int i = 0; i < propertyNames.length; ++i) { String entityTypeName = entityType.getName(); String propertyTypeName = classMetadata.getPropertyType(propertyNames[i]).getName(); if (entityTypeName.equals(propertyTypeName)) { parentPropertyName = propertyNames[i]; break; } } } return parentPropertyName; } /** * This is an internal HbnContainer utility method to clear all cache fields. */ protected void clearInternalCache() { logger.executionTrace(); idToIndex.clear(); indexRowBuffer = null; ascRowBuffer = null; descRowBuffer = null; firstId = null; lastId = null; size = null; embeddedPropertiesCache.clear(); } }