/** * Copyright 2009-2013 Oy Vaadin Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.vaadin.addon.jpacontainer; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.Collection; import java.util.EventObject; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import com.vaadin.addon.jpacontainer.util.HibernateUtil; import com.vaadin.v7.data.Container; import com.vaadin.v7.data.Container.ItemSetChangeEvent; import com.vaadin.v7.data.Property; import com.vaadin.v7.data.Property.ValueChangeListener; import com.vaadin.v7.data.Validator.InvalidValueException; import com.vaadin.v7.data.util.converter.Converter.ConversionException; /** * {@link EntityItem}-implementation that is used by {@link JPAContainer}. * Should not be used directly by clients. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ public final class JPAContainerItem<T> implements EntityItem<T> { private static final long serialVersionUID = 3835181888110236341L; private static boolean nullSafeEquals(Object o1, Object o2) { try { return o1 == o2 || o1.equals(o2); } catch (NullPointerException e) { return false; } } /** * {@link Property}-implementation that is used by {@link EntityItem}. * Should not be used directly by clients. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ final class ItemProperty implements EntityItemProperty { private static final long serialVersionUID = 2791934277775480650L; private String propertyId; private Object cachedValue; /** * Creates a new <code>ItemProperty</code>. * * @param propertyId * the property id of the new property (must not be null). */ ItemProperty(String propertyId) { assert propertyId != null : "propertyId must not be null"; this.propertyId = propertyId; // Initialize cached value if necessary if (!isWriteThrough()) { cacheRealValue(); } } public String getPropertyId() { return propertyId; } /** * Like the name suggests, this method notifies the listeners if the * cached value and real value are different. */ void notifyListenersIfCacheAndRealValueDiffer() { Object realValue = getRealValue(); if (!nullSafeEquals(realValue, cachedValue)) { fireValueChangeEvent(); } } /** * Caches the real value of the property. */ void cacheRealValue() { Object realValue = getRealValue(); cachedValue = realValue; } /** * Clears the cached value, without notifying any listeners. */ void clearCache() { cachedValue = null; } /** * <b>Note! This method assumes that write through is OFF!</b> * <p> * Sets the real value to the cached value. If read through is on, the * listeners are also notified as the value will appear to have changed * to them. * <p> * If the property is read only, nothing happens. * * @throws ConversionException * if the real value could not be set for some reason. */ void commit() throws ConversionException { if (!isReadOnly()) { try { setRealValue(cachedValue); } catch (Exception e) { throw new ConversionException(e); } } } /** * <b>Note! This method assumes that write through is OFF!</b> * <p> * Replaces the cached value with the real value. If read through is * off, the listeners are also notified as the value will appera to have * changed to them. */ void discard() { Object realValue = getRealValue(); if (!nullSafeEquals(realValue, cachedValue)) { cacheRealValue(); fireValueChangeEvent(); } else { cacheRealValue(); } } public EntityItem<?> getItem() { return JPAContainerItem.this; } public Class<?> getType() { return propertyList.getPropertyType(propertyId); } public Object getValue() { if (isReadThrough() && isWriteThrough()) { return getRealValue(); } else { return cachedValue; } } /** * Gets the real value from the backend entity. * * @return the real value. */ private Object getRealValue() { ensurePropertyLoaded(propertyId); return propertyList.getPropertyValue(entity, propertyId); } @Override public String toString() { final Object value = getValue(); if (value == null) { return null; } return value.toString(); } public boolean isReadOnly() { return !propertyList.isPropertyWritable(propertyId); } /** * <strong>This functionality is not supported by this * implementation.</strong> * <p> * {@inheritDoc } */ public void setReadOnly(boolean newStatus) { throw new UnsupportedOperationException( "The read only state cannot be changed"); } /** * Sets the real value of the property to <code>newValue</code>. The * value is expected to be of the correct type at this point (i.e. any * conversions from a String should have been done already). As this * method updates the backend entity object, it also turns on the * <code>dirty</code> flag of the item. * * @see JPAContainerItem#isDirty() * @param newValue * the new value to set. */ private void setRealValue(Object newValue) { ensurePropertyLoaded(propertyId); propertyList.setPropertyValue(entity, propertyId, newValue); dirty = true; } /** * Ensures that any lazy loaded properties are available. * * @param propertyId * the id of the property to check. */ private void ensurePropertyLoaded(String propertyId) { LazyLoadingDelegate lazyLoadingDelegate = getContainer() .getEntityProvider().getLazyLoadingDelegate(); if (lazyLoadingDelegate == null || !propertyList.isPropertyLazyLoaded(propertyId)) { // Don't need to do anything return; } boolean shouldLoadEntity = false; try { Object value = propertyList .getPropertyValue(entity, propertyId); if (value != null) { shouldLoadEntity = HibernateUtil .isUninitializedAndUnattachedProxy(value); if (Collection.class.isAssignableFrom(propertyList .getPropertyType(propertyId))) { ((Collection<?>) value).iterator().hasNext(); } } } catch (IllegalArgumentException e) { shouldLoadEntity = true; } catch (RuntimeException e) { if (HibernateUtil.isLazyInitializationException(e)) { shouldLoadEntity = true; } else { throw e; } } if (shouldLoadEntity) { entity = lazyLoadingDelegate.ensureLazyPropertyLoaded(entity, propertyId); } } public void setValue(Object newValue) throws ReadOnlyException, ConversionException { if (isReadOnly()) { throw new ReadOnlyException(); } if (newValue != null && !getType().isAssignableFrom(newValue.getClass())) { /* * The type we try to set is incompatible with the type of the * property. We therefore try to convert the value to a string * and see if there is a constructor that takes a single string * argument. If this fails, we throw an exception. */ try { // Gets the string constructor final Constructor<?> constr = getType().getConstructor( new Class[] { String.class }); newValue = constr.newInstance(new Object[] { newValue .toString() }); } catch (Exception e) { throw new ConversionException(e); } } try { if (isWriteThrough()) { setRealValue(newValue); container.containerItemPropertyModified( JPAContainerItem.this, propertyId); } else { cachedValue = newValue; modified = true; } } catch (Exception e) { throw new ConversionException(e); } fireValueChangeEvent(); } private List<ValueChangeListener> listeners; private class ValueChangeEvent extends EventObject implements Property.ValueChangeEvent { private static final long serialVersionUID = 4999596001491426923L; private ValueChangeEvent(ItemProperty source) { super(source); } public Property getProperty() { return (Property) getSource(); } } /** * Notifies all the listeners that the value of the property has * changed. */ public void fireValueChangeEvent() { if (listeners != null) { final Object[] l = listeners.toArray(); final Property.ValueChangeEvent event = new ValueChangeEvent( this); for (int i = 0; i < l.length; i++) { ((Property.ValueChangeListener) l[i]).valueChange(event); } } } public void addListener(ValueChangeListener listener) { assert listener != null : "listener must not be null"; if (listeners == null) { listeners = new LinkedList<ValueChangeListener>(); } listeners.add(listener); } public void removeListener(ValueChangeListener listener) { assert listener != null : "listener must not be null"; if (listeners != null) { listeners.remove(listener); if (listeners.isEmpty()) { listeners = null; } } } public void addValueChangeListener(ValueChangeListener listener) { addListener(listener); } public void removeValueChangeListener(ValueChangeListener listener) { removeListener(listener); } } private T entity; private JPAContainer<T> container; private PropertyList<T> propertyList; private Map<Object, ItemProperty> propertyMap; private boolean modified = false; private boolean dirty = false; private boolean persistent = true; private boolean readThrough = true; private boolean writeThrough = true; private boolean deleted = false; private Object itemId; /** * Creates a new <code>JPAContainerItem</code>. This constructor assumes * that <code>entity</code> is persistent. The item ID is the entity * identifier. * * @param container * the container that holds the item (must not be null). * @param entity * the entity for which the item should be created (must not be * null). */ JPAContainerItem(JPAContainer<T> container, T entity) { this(container, entity, container.getEntityClassMetadata() .getPropertyValue( entity, container.getEntityClassMetadata() .getIdentifierProperty().getName()), true); } /** * Creates a new <code>JPAContainerItem</code>. * * @param container * the container that created the item (must not be null). * @param entity * the entity for which the item should be created (must not be * null). * @param itemId * the item ID, or null if the item is not yet inside the * container that created it. * @param persistent * true if the entity is persistent, false otherwise. If * <code>itemId</code> is null, this parameter will be ignored. */ JPAContainerItem(JPAContainer<T> container, T entity, Object itemId, boolean persistent) { assert container != null : "container must not be null"; assert entity != null : "entity must not be null"; this.entity = entity; this.container = container; this.propertyList = new PropertyList<T>(container.getPropertyList()); this.itemId = itemId; if (itemId == null) { this.persistent = false; } else { this.persistent = persistent; } this.propertyMap = new HashMap<Object, ItemProperty>(); container.registerItem(this); } public Object getItemId() { return itemId; } public boolean addItemProperty(Object id, Property property) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } public void addNestedContainerProperty(String nestedProperty) throws UnsupportedOperationException { propertyList.addNestedProperty(nestedProperty); } public EntityItemProperty getItemProperty(Object id) { assert id != null : "id must not be null"; ItemProperty p = propertyMap.get(id); if (p == null) { if (!getItemPropertyIds().contains(id.toString())) { return null; } p = new ItemProperty(id.toString()); propertyMap.put(id, p); } return p; } public Collection<String> getItemPropertyIds() { /* * Although the container may only contain a few properties, all * properties are available for items. */ return propertyList.getAllAvailablePropertyNames(); } public boolean removeItemProperty(Object id) throws UnsupportedOperationException { assert id != null : "id must not be null"; if (id.toString().indexOf('.') > -1) { return propertyList.removeProperty(id.toString()); } else { return false; } } public boolean isModified() { return modified; } /** * Changes the <code>dirty</code> flag of this item. * * @see #isDirty() * @param dirty * true to mark the item as dirty, false to mark it as untouched. */ void setDirty(boolean dirty) { this.dirty = dirty; } public boolean isDirty() { return isPersistent() && dirty; } public boolean isPersistent() { return persistent; } /** * Changes the <code>persistent</code> flag of this item. * * @see #isPersistent() * @param persistent * true to mark the item as persistent, false to mark it as * transient. */ void setPersistent(boolean persistent) { this.persistent = persistent; } public boolean isDeleted() { return isPersistent() && !getContainer().isBuffered() && deleted; } /** * Changes the <code>deleted</code> flag of this item. * * @see #isDeleted() * @param deleted * true to mark the item as deleted, false to mark it as * undeleted. */ void setDeleted(boolean deleted) { this.deleted = true; } public EntityContainer<T> getContainer() { return container; } public T getEntity() { return entity; } public void commit() throws SourceException, InvalidValueException { if (!isWriteThrough()) { try { /* * Commit all properties. The commit() operation will check if * the property is read only and ignore it if that is the case. */ for (ItemProperty prop : propertyMap.values()) { prop.commit(); } modified = false; container.containerItemModified(this); } catch (Property.ReadOnlyException e) { throw new SourceException(this, e); } } } public void discard() throws SourceException { if (!isWriteThrough()) { for (ItemProperty prop : propertyMap.values()) { prop.discard(); } modified = false; } } public boolean isReadThrough() { return readThrough; } public boolean isWriteThrough() { return writeThrough; } public void setReadThrough(boolean readThrough) throws SourceException { if (this.readThrough != readThrough) { if (!readThrough && writeThrough) { throw new IllegalStateException( "ReadThrough can only be turned off if WriteThrough is turned off"); } this.readThrough = readThrough; } } public void setWriteThrough(boolean writeThrough) throws SourceException, InvalidValueException { if (this.writeThrough != writeThrough) { if (writeThrough) { /* * According to the Buffered interface, commit must be executed * if writeThrough is turned on. */ commit(); /* * Do some cleaning up */ for (ItemProperty prop : propertyMap.values()) { prop.clearCache(); } } else { /* * We can iterate directly over the map, as this operation only * affects existing properties. Properties that are lazily * created afterwards will work automatically. */ for (ItemProperty prop : propertyMap.values()) { prop.cacheRealValue(); } } this.writeThrough = writeThrough; } } public void addListener(ValueChangeListener listener) { /* * This operation affects ALL properties, so we have to iterate over the * list of ids instead of the map. */ for (String propertyId : getItemPropertyIds()) { ((Property.ValueChangeNotifier) getItemProperty(propertyId)) .addValueChangeListener(listener); } } public void removeListener(ValueChangeListener listener) { /* * This operation affects ALL properties, so we have to iterate over the * list of ids instead of the map. */ for (String propertyId : getItemPropertyIds()) { ((Property.ValueChangeNotifier) getItemProperty(propertyId)) .removeValueChangeListener(listener); } } public void addValueChangeListener(ValueChangeListener listener) { addListener(listener); } public void removeValueChangeListener(ValueChangeListener listener) { removeListener(listener); } @Override public String toString() { return entity.toString(); } @SuppressWarnings("serial") public void refresh() { if (isPersistent()) { T refreshedEntity = getContainer().getEntityProvider() .refreshEntity(entity); if (refreshedEntity == null) { /* * Entity has been removed, fire item set change for the * container */ setPersistent(false); container.fireContainerItemSetChange(new ItemSetChangeEvent() { public Container getContainer() { return container; } }); return; } else { entity = refreshedEntity; } if (isDirty()) { discard(); } Collection<String> itemPropertyIds = getItemPropertyIds(); for (String string : itemPropertyIds) { getItemProperty(string).fireValueChangeEvent(); } } } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); container.registerItem(this); } public void setBuffered(boolean buffered) { setWriteThrough(!buffered); setReadThrough(!buffered); } public boolean isBuffered() { return !isReadThrough() && !isWriteThrough(); } }