package au.com.vaadinutils.crud;
import java.util.Collection;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import javax.persistence.PersistenceException;
import javax.persistence.metamodel.SingularAttribute;
import javax.validation.ConstraintViolationException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.vaadin.addon.jpacontainer.EntityItem;
import com.vaadin.addon.jpacontainer.EntityItemProperty;
import com.vaadin.addon.jpacontainer.EntityProviderChangeEvent;
import com.vaadin.addon.jpacontainer.EntityProviderChangeEvent.EntitiesAddedEvent;
import com.vaadin.addon.jpacontainer.EntityProviderChangeEvent.EntitiesRemovedEvent;
import com.vaadin.addon.jpacontainer.EntityProviderChangeEvent.EntitiesUpdatedEvent;
import com.vaadin.addon.jpacontainer.JPAContainer;
import com.vaadin.addon.jpacontainer.JPAContainer.ProviderChangedEvent;
import com.vaadin.data.Container.Filter;
import com.vaadin.data.Container.ItemSetChangeEvent;
import com.vaadin.data.Container.ItemSetChangeListener;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.util.filter.Compare;
import com.vaadin.data.util.filter.UnsupportedFilterException;
import com.vaadin.ui.Component;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Notification.Type;
import au.com.vaadinutils.dao.EntityManagerProvider;
import au.com.vaadinutils.dao.JpaBaseDao;
import au.com.vaadinutils.errorHandling.ErrorWindow;
/**
* child crud does not support nesting.
*
* @author rsutton
*
* @param <P>
* @param <E>
*/
public abstract class ChildCrudView<P extends CrudEntity, E extends ChildCrudEntity> extends BaseCrudView<E>
implements ChildCrudListener<P>
{
private static final long serialVersionUID = -7756584349283089830L;
private transient Logger loggerChildCrud = LogManager.getLogger(ChildCrudView.class);
private String parentKey;
protected String childKey;
public Object parentId;
protected P currentParent;
protected Filter parentFilter;
protected boolean dirty = false;
final private Class<P> parentType;
public ParentCrud<P> parentCrud;
private ChildCrudEventHandler<E> eventHandler = getNullEventHandler();
private Class<E> childType;
/**
*
* @param parentKey
* - this will usually be the primary key of the parent table
* @param childKey
* - this will be the foreign key in the child table
*/
public ChildCrudView(ParentCrud<P> parent, Class<P> parentType, Class<E> childType,
SingularAttribute<? extends CrudEntity, ? extends Object> parentKey,
SingularAttribute<? extends CrudEntity, ? extends Object> childKey)
{
super(CrudDisplayMode.VERTICAL);
this.parentKey = parentKey.getName();
this.childKey = childKey.getName();
this.parentType = parentType;
this.parentCrud = parent;
this.childType = childType;
// setMargin(true);
}
public ChildCrudView(ParentCrud<P> parent, Class<P> parentType, Class<E> childType,
SingularAttribute<? extends CrudEntity, ? extends Object> parentKey, String childKey)
{
super(CrudDisplayMode.VERTICAL);
this.parentKey = parentKey.getName();
this.childKey = childKey;
this.parentType = parentType;
this.parentCrud = parent;
this.childType = childType;
// setMargin(true);
}
// public ChildCrudView(final BaseCrudView<P> parentCrud, Class<P>
// parentType, Class<E> childType,
// SingularAttribute<? extends CrudEntity, ? extends Object> parentKey,
// SingularAttribute<? extends CrudEntity, ? extends Object> childKey)
// {
// super(CrudDisplayMode.VERTICAL);
// this.parentKey = parentKey.getName();
// this.childKey = childKey.getName();
// this.parentType = parentType;
// this.childType = childType;
// this.parentCrud = new ParentCrud<P>(){
//
// @Override
// public EntityItem<P> getContainerItem(Long id)
// {
// return parentCrud.getContainer().getItem(id);
// }
//
// @Override
// public void fieldGroupIsDirty(boolean b)
// {
// parentCrud.fieldGroupIsDirty(b);
//
// }
//
// @Override
// public P getCurrent()
// {
// return parentCrud.getCurrent();
// }
//
// @Override
// public boolean isDirty()
// {
// return parentCrud.isDirty();
// }
//
// @Override
// public void reloadDataFromDB()
// {
// parentCrud.reloadDataFromDB();
//
// }
//
// @Override
// public void save()
// {
// parentCrud.save();
//
// }
//
// @Override
// public void setSplitPosition(float pos)
// {
// parentCrud.setSplitPosition(pos);
//
// }};
//
// }
@Override
protected void init(Class<E> entityClass, JPAContainer<E> container, HeadingPropertySet headings)
{
setBogusParentFilter();
super.init(entityClass, container, headings);
// ensure auto commit is off, so that child updates don't go to the db
// until the parent saves
container.setAutoCommit(false);
// container.setBuffered(true);
// child cruds dont have save/cancel buttons
if (buttonLayout != null)
{
rightLayout.removeComponent(buttonLayout);
}
}
/**
* Load the page with a bogus parent filter to prevent possible large
* queries from being executed before a parent row is selected
*/
private void setBogusParentFilter()
{
P tmp;
try
{
tmp = parentType.newInstance();
tmp.setId(-1L);
parentFilter = new Compare.Equal(childKey, tmp);
}
catch (InstantiationException | IllegalAccessException e)
{
loggerChildCrud.warn("Failed to instance " + parentType + " to create bogus parent filter");
}
}
/**
* this method is invoked when the parent saves, signalling the children
* that they too should save.
*
* @throws Exception
*/
@SuppressWarnings("unchecked")
@Override
public void committed(P newParentId) throws Exception
{
Long selectedIdBeforeSave = null;
String currentGuid = null;
if (getCurrent() != null)
{
selectedIdBeforeSave = getCurrent().getId();
currentGuid = getCurrent().getGuid();
}
resetFiltersWithoutChangeEvents();
saveEditsToTemp();
int numberOfChildren = 0;
for (Object id : container.getItemIds())
{
numberOfChildren++;
EntityItem<E> item = container.getItem(id);
EntityItemProperty reference = item.getItemProperty(childKey);
if (reference == null)
{
loggerChildCrud.error(
"Child key " + childKey + " doesn't exist in the container " + container.getEntityClass());
}
if (reference == null || reference.getValue() == null)
{
try
{
// TODO: this looks like the spot to allow a ManyToMany
// relationship to be updated,
// need to add a hook for that here (I think, may be
// problems with the parent when it's also a new parent)
if (item.getItemProperty(childKey).getType() == newParentId.getClass())
{
item.getItemProperty(childKey).setValue(newParentId);
}
else
{
loggerChildCrud.warn(
"Child key type is not the same as the Parent type, if it's an ID thats probably ok?");
// special handling when the child key is an id(Long)
// rather than an entity.
item.getItemProperty(childKey).setValue(translateParentId(newParentId.getId()));
}
// item.getItemProperty(childKey).setValue(newParentId);
}
catch (Exception e)
{
loggerChildCrud.error(e, e);
}
}
extendedChildCommitProcessing(newParentId, item);
}
// container.commit();
loggerChildCrud.warn("Committing for " + this.getClass());
commitContainerWithHooks();
// on a new parent, the parent id changes and the container becomes
// empty. so reset the parent filter and refresh the container
createParentFilter(parentCrud.getContainerItem(newParentId.getId()));
resetFiltersWithoutChangeEvents();
// container.discard();
container.refresh();
int changeInItems = numberOfChildren - container.getItemIds().size();
if (changeInItems != 0)
{
// This is usually caused by a filter that eliminates a newly added
// child. An example of this is where a child crud is associated via
// a ManyToMany and the ManyToMany relationship is not yet updated
// and the filter therefore eliminates the new child. - see the
// above TODO.
// Another example is when resetFilter has been overridden and the
// resulting filters eliminate the new child
throw new IllegalStateException(changeInItems
+ ", The number of items in the container is not the same as it was before the refresh. "
+ this.getClass().getSimpleName());
}
associateChildren(newParentId);
dirty = false;
entityTable.select(null);
triggerFilter();
if (selectedIdBeforeSave != null && selectedIdBeforeSave > 0)
{
entityTable.select(selectedIdBeforeSave);
}
else
{
if (getGuidAttribute() != null && currentGuid != null)
{
E entity = JpaBaseDao.getGenericDao(childType).findOneByAttribute(getGuidAttribute(), currentGuid);
if (entity != null)
{
entityTable.select(entity.getId());
}
else
{
loggerChildCrud.warn(
"Unable to locate newly created child entity, will not be able to select it for the user.");
}
}
}
}
/**
* return the singluarAttribute for the guid field of the child entity
*
* @return
*/
abstract public SingularAttribute<? super E, String> getGuidAttribute();
/**
* commits the container and retrieves the new recordid
*
* we have to hook the ItemSetChangeListener to be able to get the database
* id of a new record.
*/
private void commitContainerWithHooks()
{
// call back to collect the id of the new record when the container
// fires the ItemSetChangeEvent
ItemSetChangeListener tmp = new ItemSetChangeListener()
{
private static final long serialVersionUID = -4893881323343394274L;
@Override
public void containerItemSetChange(ItemSetChangeEvent event)
{
if (event instanceof ProviderChangedEvent)
{
@SuppressWarnings("rawtypes")
ProviderChangedEvent pce = (ProviderChangedEvent) event;
@SuppressWarnings("unchecked")
EntityProviderChangeEvent<E> changeEvent = pce.getChangeEvent();
if (changeEvent instanceof EntitiesAddedEvent)
{
Collection<E> affectedEntities = changeEvent.getAffectedEntities();
eventHandler.entitiesAdded(affectedEntities);
}
if (changeEvent instanceof EntitiesUpdatedEvent)
{
Collection<E> affectedEntities = changeEvent.getAffectedEntities();
eventHandler.entitiesUpdated(affectedEntities);
}
if (changeEvent instanceof EntitiesRemovedEvent)
{
Collection<E> affectedEntitys = changeEvent.getAffectedEntities();
eventHandler.entitiesDeleted(affectedEntitys);
}
}
}
};
final LinkedList<ItemSetChangeListener> listeners = new LinkedList<>(container.getItemSetChangeListeners());
try
{
// get existing listeners and remove them
for (ItemSetChangeListener listener : listeners)
{
container.removeItemSetChangeListener(listener);
}
// add the hook listener
container.addItemSetChangeListener(tmp);
// call commit
container.commit();
}
catch (Exception e)
{
ErrorWindow.showErrorWindow(e);
}
finally
{
// detach the hook listener
container.removeItemSetChangeListener(tmp);
// restore the existing listeners
for (ItemSetChangeListener listener : listeners)
{
container.addItemSetChangeListener(listener);
}
}
}
/**
* EventHandler is the preferred integration point for classes extending
* ChildCrudView as it makes them less tightly coupled.
*
* @param eventHandler
*/
public void setEventHandler(ChildCrudEventHandler<E> eventHandler)
{
this.eventHandler = eventHandler;
}
private void associateChildren(P newParent) throws Exception
{
P mParent = EntityManagerProvider.merge(newParent);
for (Object id : container.getItemIds())
{
E child = EntityManagerProvider.merge(container.getItem(id).getEntity());
associateChild(mParent, child);
for (ChildCrudListener<E> childListener : getChildCrudListeners())
{
// allow child of child crud to commit
childListener.committed(child);
}
}
}
abstract public void associateChild(P newParent, E child);
/**
* @throws Exception
*/
protected void extendedChildCommitProcessing(P newParentId, EntityItem<E> item) throws Exception
{
}
/**
* if a row is deleted in the childCrud, then it is dirty for the purposes
* of the parent crud
*/
@Override
public void delete()
{
Object entityId = entityTable.getValue();
preChildDelete(entityId);
Object previousItemId = null;
try
{
previousItemId = entityTable.prevItemId(entityId);
}
catch (Exception e)
{
loggerChildCrud.warn(e, e);
}
entityTable.removeItem(entityId);
newEntity = null;
entityTable.select(null);
entityTable.select(previousItemId);
dirty = true;
container.removeItem(entityId);
JpaBaseDao<E, Long> dao = new JpaBaseDao<>(entityClass);
E entity = dao.findById((Long) entityId);
if (entity != null)
{
EntityManagerProvider.remove(entity);
}
parentCrud.reloadDataFromDB();
entityTable.select(null);
postChildDelete(entityId);
}
/**
* Called just before a child entity is deleted so that a derived class can
* inject some logic just before the delete occurs.
*
* @param entityId
*/
protected void preChildDelete(Object entityId)
{
}
/**
* Called just after a child entity is deleted so that a derived class can
* inject some logic just after the delete occurs.
*
* @param entityId
*/
protected void postChildDelete(Object entityId)
{
}
@Override
public void validateFieldz()
{
try
{
if (getCurrent() != null)
{
String fieldName = selectFirstErrorFieldAndShowTab(this.fieldGroup);
if (!fieldGroup.isValid())
{
throw new InvalidValueException("Invalid Field: " + fieldName);
}
}
}
catch (InvalidValueException e)
{
throw e;
}
}
/**
* Need to over-ride as the rules display of a child's new button a
* different to those of the parent.
*/
@Override
protected void activateEditMode(boolean activate)
{
actionCombo.setEnabled(!activate);
actionApplyButton.setEnabled(!activate);
// for child new is always enabled unless explicitly disallowed
boolean showNew = true;
if (isDisallowNew())
{
showNew = false;
}
actionNewButton.setEnabled(showNew);
}
/**
* used to prevent cascading saves when new is clicked
*/
boolean inNew = false;
@Override
public void rowChanged(EntityItem<E> item)
{
if (preventRowChangeCascade == false && !inNew)
{
try
{
preventRowChangeCascade = true;
saveEditsToTemp();
super.rowChanged(item);
activateEditMode(false);
}
finally
{
preventRowChangeCascade = false;
}
}
else
{
super.rowChanged(item);
}
}
@Override
public void newClicked()
{
try
{
inNew = true;
E previousEntity = getCurrent();
saveEditsToTemp();
resetFiltersWithoutChangeEvents();
triggerFilter();
createNewEntity(previousEntity);
// if we call the overridden version we loop indefinitely
ChildCrudView.super.rowChanged(newEntity);
// Can't delete when you are adding a new record.
// Use cancel instead.
if (actionApplyButton.isVisible())
{
restoreDelete = true;
activateEditMode(true);
actionLayout.setVisible(true);
}
selectFirstFieldAndShowTab();
postNew(newEntity);
rightLayout.setVisible(true);
}
catch (ConstraintViolationException e)
{
FormHelper.showConstraintViolation(e);
}
catch (InstantiationException e)
{
loggerChildCrud.error(e, e);
throw new RuntimeException(e);
}
catch (IllegalAccessException e)
{
loggerChildCrud.error(e, e);
throw new RuntimeException(e);
}
catch (Exception e)
{
loggerChildCrud.error(e, e);
throw new RuntimeException(e);
}
finally
{
inNew = false;
}
}
/**
* for child crud, save is implied when the row changes
*/
@Override
public void save()
{
}
@Override
protected void invokeTopLevelCrudSave()
{
parentCrud.save();
}
/**
* used to prevent cascading save calls
*/
boolean saving = false;
@Override
public void saveEditsToTemp()
{
if (saving == false)
{
try
{
saving = true;
// if a row is saved in the childCrud, then it is dirty for the
// purposes of the parent crud
if (fieldGroup.isModified() || areNonFieldGroupFieldsDirty())
{
dirty = true;
commitFieldGroup();
if (newEntity != null)
{
interceptSaveValues(newEntity);
container.addEntity(newEntity.getEntity());
// EntityItem<E> item = container.getItem(id);
// container.commit();
// fieldGroup.setItemDataSource(item);
// entityTable.select(item.getItemId());
// If we leave the save button active, clicking it again
// duplicates the record
// rightLayout.setVisible(false);
}
else
{
EntityItem<E> current = entityTable.getCurrent();
if (current != null)
{
interceptSaveValues(current);
// container.commit();
}
}
}
else
{
if (getCurrent() != null)
{
loggerChildCrud.info("There are no dirty fields, not saving record {} {}",
getCurrent().getClass().getSimpleName(), getCurrent());
}
}
for (ChildCrudListener<E> child : getChildCrudListeners())
{
child.saveEditsToTemp();
}
// Notification.show("Changes Saved",
// "Any changes you have made have been saved.",
// Type.TRAY_NOTIFICATION);
}
catch (PersistenceException e)
{
loggerChildCrud.error(e, e);
Notification.show(e.getMessage(), Type.ERROR_MESSAGE);
}
catch (ConstraintViolationException e)
{
loggerChildCrud.error(e, e);
FormHelper.showConstraintViolation(e);
}
catch (InvalidValueException e)
{
handleInvalidValueException(e);
}
catch (CommitException e)
{
if (e.getCause() instanceof InvalidValueException)
{
handleInvalidValueException((InvalidValueException) e.getCause());
}
else
{
loggerChildCrud.error(e, e);
Notification.show(e.getMessage(), Type.ERROR_MESSAGE);
}
}
catch (Exception e)
{
loggerChildCrud.error(e, e);
Notification.show(e.getMessage(), Type.ERROR_MESSAGE);
}
finally
{
if (newEntity != null)
{
newEntity = null;
if (restoreDelete)
{
activateEditMode(false);
restoreDelete = false;
}
}
splitPanel.showFirstComponent();
saving = false;
}
// }
// }
}
}
/**
*
*
* When adding fields to a layout that need to be committed, but are not
* part of the field group, override this method to let the crud know that a
* commit should be performed.
*
* This is required because committing on a child crud is not explicit -no
* save button is available, the need to save is determined based on if the
* fields are dirty.
*
* @return
*/
protected boolean areNonFieldGroupFieldsDirty()
{
return false;
}
/**
* used to prevent cascading rowChange events
*/
boolean preventRowChangeCascade = false;
private Collection<RowChangedListener<P>> parentRowChangeListeners = new LinkedList<>();
private boolean isInitialised = false;
@Override
protected void initializeEntityTable()
{
// do nothing, this will be done when the parent selects the first row!
}
/**
* this method is called when the parent crud changes row, so we set filters
* so that this child only displays the associated rows
*/
@Override
public void selectedParentRowChanged(EntityItem<P> item)
{
try
{
if (item != null && item.getEntity() != null)
{
loggerChildCrud.debug("Parent Row Changed {} {}", item.getEntity().getId(), item.getEntity().getName());
}
searchField.setValue("");
clearAdvancedFilters();
if (isInitialised)
{
saveEditsToTemp();
}
createParentFilter(item);
currentParent = null;
if (item != null)
{
currentParent = item.getEntity();
}
if (isInitialised)
{
fieldGroup.discard();
container.discard();
}
dirty = false;
Stopwatch timer = Stopwatch.createUnstarted();
timer.start();
resetFiltersWithoutChangeEvents();
triggerFilter();
if (!isInitialised)
{
entityTable.init(this.getClass().getSimpleName());
isInitialised = true;
// entityTable doesn't generate a row change event on this path,
// so we'll simulate one!
rowChanged(entityTable.getCurrent());
}
loggerChildCrud.debug("Child crud load {} took {}", this.getClass().getSimpleName(),
timer.elapsed(TimeUnit.MILLISECONDS));
Object id = entityTable.firstItemId();
if (id != null)
{
entityTable.select(entityTable.firstItemId());
}
else
{
try
{
entityTable.select(null);
}
catch (Exception e)
{
loggerChildCrud.warn(e, e);
// ignore this. if we don't do this the child continues
// to
// show data from the previously selected row
// TODO: come up with a better solution
}
}
searchField.setValue("");
if (item == null)
{
notifyParentRowChangeListeners(null);
}
else
{
notifyParentRowChangeListeners(item.getEntity());
}
}
catch (Exception e)
{
ErrorWindow.showErrorWindow(e);
}
}
private void notifyParentRowChangeListeners(P entity)
{
for (RowChangedListener<P> listener : parentRowChangeListeners)
{
listener.rowChanged(entity);
}
}
public void addParentRowChangedListener(RowChangedListener<P> rowChangedListener)
{
parentRowChangeListeners.add(rowChangedListener);
}
private void createParentFilter(EntityItem<P> item) throws InstantiationException, IllegalAccessException
{
parentFilter = new Compare.Equal(childKey, translateParentId(-1l));
if (item != null)
{
EntityItemProperty key = item.getItemProperty(parentKey);
Preconditions.checkNotNull(key, "parentKey " + parentKey + " doesn't exist in properties");
parentId = key.getValue();
if (parentId != null)
{
parentFilter = new Compare.Equal(childKey, translateParentId(parentId));
}
}
}
/**
* the id (Long) of the parent will need to be translated into an entity for
* the filtering. an implementing class must implement this method and
* return an instance of the Parent class (P) with it's id set (parentId2)
*
* @param parentId2
* @return
* @throws IllegalAccessException
* @throws InstantiationException
*/
protected Object translateParentId(Object parentId2) throws InstantiationException, IllegalAccessException
{
Preconditions.checkNotNull(parentId2,
"attempt to translate null parent id in " + entityClass.getCanonicalName());
P tmp = parentType.newInstance();
Preconditions.checkNotNull(tmp, "failed to create instance of " + entityClass.getCanonicalName());
tmp.setId((Long) parentId2);
Preconditions.checkArgument(tmp.getId() != null,
"setId or getId has not been implemented correctly by " + entityClass.getCanonicalName());
Preconditions.checkArgument(tmp.getId().equals(parentId2),
"setId or getId has not been implemented correctly by " + entityClass.getCanonicalName());
return tmp;
}
@Override
protected void resetFilters()
{
try
{
container.removeAllContainerFilters();
container.addContainerFilter(parentFilter);
}
catch (UnsupportedFilterException e)
{
ErrorWindow.showErrorWindow(e);
}
}
/**
* parent crud will check if the children are dirty, before allowing a row
* change
*/
@Override
public boolean isDirty()
{
boolean ret = false;
// call the super to see if the fields are dirty and then also check
// that records haven't been added or removed
ret = super.isDirty() || dirty || inNew || areNonFieldGroupFieldsDirty();
return ret;
}
public Object getParentId()
{
return parentId;
}
@Override
protected Component getTitle()
{
// no title in child cruds
return null;
}
// disabled as the save/cancel enable/disable is buggy
@Override
public void fieldGroupIsDirty(boolean b)
{
// if (b)
// {
// dirty = true;
// }
// parentCrud.fieldGroupIsDirty(dirty);
}
@Override
public void discard()
{
fieldGroup.discard();
container.discard();
for (ChildCrudListener<E> child : childCrudListeners)
{
child.discard();
}
if (newEntity != null)
{
if (restoreDelete)
{
activateEditMode(false);
restoreDelete = false;
}
newEntity = null;
entityTable.select(null);
if (entityTable.getCurrent() == null)
{
showNoSelectionMessage();
}
}
}
/**
* noop event handler
*
* @return
*/
private ChildCrudEventHandler<E> getNullEventHandler()
{
return new ChildCrudEventHandler<E>()
{
@Override
public void entitiesAdded(Collection<E> entities)
{
}
@Override
public void entitiesUpdated(Collection<E> entities)
{
}
@Override
public void entitiesDeleted(Collection<E> entities)
{
}
};
}
@Override
protected String getNewButtonLabel()
{
return getNewButtonActionLabel();
}
/**
* it gets confusing with multiple new buttons that appear when using nested
* cruds, so provide a label like "New Goal" to ease the confusion
*
* @return
*/
public abstract String getNewButtonActionLabel();
}