package au.com.vaadinutils.crud; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.persistence.metamodel.SingularAttribute; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.vaadin.dialogs.ConfirmDialog; 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.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.Property.ValueChangeEvent; import com.vaadin.data.Property.ValueChangeListener; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.fieldgroup.FieldGroup.CommitException; import com.vaadin.event.FieldEvents.TextChangeEvent; import com.vaadin.event.FieldEvents.TextChangeListener; import com.vaadin.event.dd.DragAndDropEvent; import com.vaadin.event.dd.DropHandler; import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; import com.vaadin.event.dd.acceptcriteria.SourceIsTarget; import com.vaadin.shared.ui.MarginInfo; import com.vaadin.shared.ui.dd.VerticalDropLocation; import com.vaadin.shared.ui.label.ContentMode; import com.vaadin.ui.AbstractLayout; import com.vaadin.ui.AbstractSelect.AbstractSelectTargetDetails; import com.vaadin.ui.AbstractTextField.TextChangeEventMode; import com.vaadin.ui.Alignment; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.Button.ClickListener; import com.vaadin.ui.ComboBox; import com.vaadin.ui.Component; import com.vaadin.ui.CssLayout; import com.vaadin.ui.Field; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; import com.vaadin.ui.Notification; import com.vaadin.ui.Notification.Type; import com.vaadin.ui.Panel; import com.vaadin.ui.TabSheet; import com.vaadin.ui.Table; import com.vaadin.ui.Table.ColumnGenerator; import com.vaadin.ui.Table.TableDragMode; import com.vaadin.ui.TextField; import com.vaadin.ui.UI; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.themes.ValoTheme; import au.com.vaadinutils.crud.events.CrudEventDistributer; import au.com.vaadinutils.crud.events.CrudEventType; import au.com.vaadinutils.crud.security.SecurityManagerFactoryProxy; import au.com.vaadinutils.dao.EntityManagerProvider; import au.com.vaadinutils.dao.EntityManagerRunnable; import au.com.vaadinutils.dao.RunnableUI; import au.com.vaadinutils.errorHandling.ErrorWindow; import au.com.vaadinutils.listener.ClickEventLogged; import au.com.vaadinutils.menu.Menu; import au.com.vaadinutils.menu.Menus; public abstract class BaseCrudView<E extends CrudEntity> extends VerticalLayout implements RowChangeListener<E>, Selected<E>, DirtyListener, ButtonListener, ParentCrud<E> { private static transient Logger logger = LogManager.getLogger(BaseCrudView.class); private static final long serialVersionUID = 1L; protected EntityItem<E> newEntity = null; /** * When we enter inNew mode we need to hide the delete button. When we exit * inNew mode thsi var is used to determine if we need to restore the delete * button. i.e. if it wasn't visible before 'new' we shouldn't make it * visible now. */ protected boolean restoreDelete; protected TextField searchField = new TextField(); protected HorizontalLayout actionLayout; protected CssLayout actionGroupLayout = new CssLayout(); protected ComboBox actionCombo; protected Label actionLabel; protected Label actionMessage; protected Button actionNewButton = new Button("New"); protected Button actionApplyButton = new Button("Apply"); protected Button searchButton = new Button("Search"); protected CrudAction<E> exportAction; private boolean dynamicSearch = true; protected Class<E> entityClass; protected ValidatingFieldGroup<E> fieldGroup; private VerticalLayout mainEditPanel = new VerticalLayout(); // private E currentEntity; protected JPAContainer<E> container; protected EntityList<E> entityTable; protected VerticalLayout rightLayout; // Layout for the tablesaveOnRowChange protected VerticalLayout leftLayout; protected Component editor; protected CrudPanelPair splitPanel; protected BaseCrudSaveCancelButtonTray buttonLayout; protected AbstractLayout advancedSearchLayout; private VerticalLayout searchLayout; protected Set<ChildCrudListener<E>> childCrudListeners = new HashSet<>(); private CrudDisplayMode displayMode = CrudDisplayMode.HORIZONTAL; private boolean disallowEditing = false; private boolean disallowNew = false; private boolean noEditor; public boolean advancedSearchOn = false; private boolean triggerFilterOnClear = true; private Button advancedSearchButton; private Set<RowChangedListener<E>> rowChangedListeners = new CopyOnWriteArraySet<>(); private int minSearchTextLength = 0; protected HeadingPropertySet headings; private boolean dragAndDropOrderingEnabled = false; private SingularAttribute<E, Long> ordinalField; private boolean isMainView = true; protected BaseCrudView() { } BaseCrudView(CrudDisplayMode mode) { this.displayMode = mode; } protected void init(Class<E> entityClass, JPAContainer<E> container, HeadingPropertySet headings) { this.entityClass = entityClass; this.container = container; try { container.setBuffered(true); container.setFireContainerItemSetChangeEvents(true); // container.setAutoCommit(true); } catch (Exception e) { logger.error( " ******* when constructing a jpaContainer for use with the BaseCrudView use JPAContainerFactory.makeBatchable ****** "); logger.error(e, e); throw new RuntimeException(e); } fieldGroup = new ValidatingFieldGroup<>(container, entityClass); fieldGroup.setBuffered(true); // disable this, as the disabling of the save/cancel button is buggy // fieldGroup.setDirtyListener(this); this.headings = headings; entityTable = getTable(container, headings); entityTable.setId("CrudTable-" + this.getClass().getSimpleName()); entityTable.setRowChangeListener(this); entityTable.setSortEnabled(true); entityTable.setColumnCollapsingAllowed(true); // calling resetFilters here so the filters are in place when the page // first loads resetFiltersWithoutChangeEvents(); // call createExportAction before calling initLayout since crudActions // will be needed in initLayout-->buildActionLayout exportAction = createExportAction(); initializeEntityTable(); initLayout(); initSearch(); initButtons(); this.setVisible(true); showNoSelectionMessage(); entityTable.select(entityTable.firstItemId()); // do the security check after all the other setup, so extending classes // don't throw npe's due to // uninitialised components if (!getSecurityManager().canUserView()) { this.setSizeFull(); Label sorryMessage = new Label("Sorry, you do not have permission to access " + getTitleText()); sorryMessage.setStyleName(ValoTheme.LABEL_H1); this.removeAllComponents(); this.addComponent(sorryMessage); return; } if (!getSecurityManager().canUserDelete()) { // disable delete as the user doesn't have permission to delete disallowDelete(true); } if (!getSecurityManager().canUserCreate()) { disallowNew(true); } buttonLayout = new BaseCrudSaveCancelButtonTray(!getSecurityManager().canUserEdit() || disallowEditing, !getSecurityManager().canUserCreate() || disallowNew, this); rightLayout.addComponent(buttonLayout); } protected void initializeEntityTable() { try { entityTable.init(this.getClass().getSimpleName()); } catch (Exception e) { ErrorWindow.showErrorWindow(e); } } /** * allows the user to sort the items in the list via drag and drop * * @param ordinalField */ public void enableDragAndDropOrdering(final SingularAttribute<E, Long> ordinalField) { dragAndDropOrderingEnabled = true; this.ordinalField = ordinalField; container.sort(new Object[] { ordinalField.getName() }, new boolean[] { true }); this.entityTable.setDragMode(TableDragMode.ROW); this.entityTable.setDropHandler(new DropHandler() { private static final long serialVersionUID = -6024948983201516170L; @Override public AcceptCriterion getAcceptCriterion() { return SourceIsTarget.get(); } @SuppressWarnings("unchecked") @Override public void drop(DragAndDropEvent event) { if (isDirty()) { Notification.show("You must save first", Type.WARNING_MESSAGE); return; } Object draggedItemId = event.getTransferable().getData("itemId"); AbstractSelectTargetDetails td = (AbstractSelectTargetDetails) event.getTargetDetails(); VerticalDropLocation dl = td.getDropLocation(); Object targetId = ((AbstractSelectTargetDetails) event.getTargetDetails()).getItemIdOver(); int idx = container.indexOfId(targetId); if (dl == VerticalDropLocation.BOTTOM) { // drop below so move the idx down one idx++; } if (idx > -1) { targetId = container.getIdByIndex(idx); } EntityItem<E> dragged = container.getItem(draggedItemId); EntityItemProperty draggedOrdinalProp = dragged.getItemProperty(ordinalField.getName()); boolean added = false; Long ctr = 1l; for (Object id : container.getItemIds()) { if (id.equals(targetId)) { draggedOrdinalProp.setValue(ctr++); added = true; } if (!id.equals(draggedItemId)) { container.getItem(id).getItemProperty(ordinalField.getName()).setValue(ctr++); } } if (!added) { draggedOrdinalProp.setValue(ctr++); } container.commit(); container.refresh(); container.sort(new Object[] { ordinalField.getName() }, new boolean[] { true }); // cause this crud to save, or if its a child cause the parent // to save. try { invokeTopLevelCrudSave(); } catch (Exception e) { ErrorWindow.showErrorWindow(e); } } }); } /** * the child crud variant of this method calls parent.save(); */ protected void invokeTopLevelCrudSave() { save(); } /** * if you need to provide a security manager, call * SecurityManagerFactoryProxy.setFactory(...) at application initialisation * time * * @return * @throws ExecutionException */ @Override public CrudSecurityManager getSecurityManager() { return SecurityManagerFactoryProxy.getSecurityManager(this.getClass()); } public void addGeneratedColumn(final Object id, ColumnGenerator generator) { Preconditions.checkState(entityTable != null, "call BaseCrudView.init() first"); Object idName = id; if (id instanceof SingularAttribute) { idName = ((SingularAttribute<?, ?>) id).getName(); } entityTable.addGeneratedColumn(idName, generator); } protected EntityList<E> getTable(JPAContainer<E> container, HeadingPropertySet headings) { return new EntityTable<>(container, headings); } /* * build the button layout and editor panel */ protected abstract Component buildEditor(ValidatingFieldGroup<E> fieldGroup2); private void initLayout() { this.setSizeFull(); splitPanel = displayMode.getContainer(); this.addComponent(splitPanel.getPanel()); this.setExpandRatio(splitPanel.getPanel(), 1); this.setSizeFull(); leftLayout = new VerticalLayout(); // Start by defining the LHS which contains the table splitPanel.setFirstComponent(leftLayout); searchLayout = new VerticalLayout(); searchLayout.setWidth("100%"); searchField.setWidth("100%"); // expandratio and use of setSizeFull are incompatible // searchLayout.setSizeFull(); Component title = getTitle(); if (title != null) { leftLayout.addComponent(getTitle()); } leftLayout.addComponent(searchLayout); buildSearchBar(); leftLayout.addComponent(entityTable); leftLayout.setSizeFull(); /* * On the left side, expand the size of the entity List so that it uses * all the space left after from bottomLeftLayout */ leftLayout.setExpandRatio(entityTable, 1); entityTable.setSizeFull(); // Now define the edit area rightLayout = new VerticalLayout(); splitPanel.setSecondComponent(rightLayout); /* Put a little margin around the fields in the right side editor */ Panel scroll = new Panel(); // mainEditPanel.setDescription("BaseCrud:MainEditPanel"); if (!noEditor) { mainEditPanel.setVisible(true); mainEditPanel.setSizeFull(); mainEditPanel.setId("EditPanel-" + this.getClass().getSimpleName()); scroll.setSizeFull(); scroll.setContent(mainEditPanel); rightLayout.addComponent(scroll); rightLayout.setExpandRatio(scroll, 1.0f); rightLayout.setSizeFull(); rightLayout.setId("rightLayout"); editor = buildEditor(fieldGroup); Preconditions.checkNotNull(editor, "Your editor implementation returned null!, you better create an editor. " + entityClass.getSimpleName()); mainEditPanel.addComponent(editor); } else { this.setSplitPosition(100); splitPanel.setLocked(true); } buildActionLayout(); leftLayout.addComponent(actionLayout); rightLayout.setVisible(false); } /** * call this method before init if you intend not to provide an editor */ public void noEditor() { noEditor = true; } /** * get the title for the page from the menu annotation, override this menu * to provide a custom page title * * @return */ protected String getTitleText() { Annotation annotation = this.getClass().getAnnotation(Menu.class); if (annotation instanceof Menu) { return ((Menu) annotation).display(); } annotation = this.getClass().getAnnotation(Menus.class); if (annotation instanceof Menus) { return ((Menus) annotation).menus()[0].display(); } Exception e = new Exception( "Override getTitleText() to set a custom title in " + this.getClass().getCanonicalName()); logger.error(e, e); return "Override getTitleText() to set a custom title. " + this.getClass().getCanonicalName(); } protected Component getTitle() { HorizontalLayout holder = new HorizontalLayout(); Label titleLabel = new Label(getTitleText()); titleLabel.addStyleName(ValoTheme.LABEL_H2); titleLabel.addStyleName(ValoTheme.LABEL_BOLD); holder.addComponent(titleLabel); holder.setComponentAlignment(titleLabel, Alignment.MIDDLE_RIGHT); return holder; } private void buildActionLayout() { actionLayout = new HorizontalLayout(); actionLayout.setWidth("100%"); actionLayout.setMargin(new MarginInfo(false, true, false, false)); actionLabel = new Label(" Action"); actionLabel.setContentMode(ContentMode.HTML); actionLabel.setWidth("50"); actionGroupLayout.addStyleName("v-component-group"); actionLayout.addComponent(actionGroupLayout); actionGroupLayout.addComponent(actionLabel); actionCombo = new ComboBox(null); actionCombo.setWidth("160"); actionCombo.setNullSelectionAllowed(false); actionCombo.setTextInputAllowed(false); actionGroupLayout.addComponent(actionCombo); addCrudActions(); actionGroupLayout.addComponent(actionApplyButton); actionApplyButton.setId("applyButton"); actionMessage = new Label("", ContentMode.HTML); actionGroupLayout.addComponent(actionMessage); String newButtonLabel = getNewButtonLabel(); if (newButtonLabel == null) { newButtonLabel = ""; } actionNewButton.setCaption(newButtonLabel); actionNewButton.setId("CrudNewButton-" + newButtonLabel.replace(" ", "")); actionLayout.addComponent(actionNewButton); actionLayout.setComponentAlignment(actionGroupLayout, Alignment.MIDDLE_LEFT); actionLayout.setComponentAlignment(actionNewButton, Alignment.MIDDLE_RIGHT); actionLayout.setExpandRatio(actionGroupLayout, 1.0f); actionLayout.setHeight("35"); } protected void setActionMessage(final String message) { actionMessage.setValue("   " + message); } protected String getNewButtonLabel() { return "New"; } private void addCrudActions() { final List<CrudAction<E>> actions = getCrudActions(); // If there are no actions... if (actions == null || actions.isEmpty()) { // ...and the new button is disable, remove the bottom panel... if (disallowNew) { actionLayout.setVisible(false); } // ...otherwise just remove the combo box and button else { actionGroupLayout.setVisible(false); } return; } CrudAction<E> defaultAction = null; for (CrudAction<E> action : actions) { if (action.isDefault()) { if (defaultAction != null) { String message = "Only one action may be marked as default: " + defaultAction.toString() + " was already the default when " + action.toString() + " was found to also be default."; throw new IllegalStateException(message); } defaultAction = action; } actionCombo.addItem(action); } // Select the default action actionCombo.setValue(defaultAction); } /** * overload this method to add custom actions, in your overloaded version * you should call super.getCrudActions() to get a list with the * DeleteAction pre-populated if you need a specialized form of delete that * does things like delete files, there is a callback on the * CrudActionDelete class for that */ protected List<CrudAction<E>> getCrudActions() { List<CrudAction<E>> actions = new LinkedList<>(); CrudAction<E> crudAction = new CrudActionDelete<>(); actions.add(crudAction); actions.add(exportAction); return actions; } protected CrudAction<E> createExportAction() { return new CrudAction<E>() { private static final long serialVersionUID = -7703959823800614876L; @Override public boolean isDefault() { return false; } @Override public boolean showPreparingDialog() { return true; } @Override public void exec(BaseCrudView<E> crud, EntityItem<E> entity) { new ContainerCSVExport<E>(getTitleText(), (Table) entityTable, headings); } @Override public String toString() { return "Export CSV Data"; } }; } private void buildSearchBar() { AbstractLayout group = new HorizontalLayout(); if (UI.getCurrent().getTheme().equals(ValoTheme.class.getSimpleName())) { group = new CssLayout(); group.addStyleName("v-component-group"); } group.setSizeFull(); searchLayout.addComponent(group); AbstractLayout advancedSearch = buildAdvancedSearch(); if (advancedSearch != null) { group.addComponent(advancedSearchButton); } Button clear = createClearButton(); group.addComponent(clear); // searchField.setWidth("80%"); searchField.setId("CrudSearchField"); group.addComponent(searchField); if (group instanceof HorizontalLayout) { ((HorizontalLayout) group).setExpandRatio(searchField, 1); } group.addComponent(searchButton); searchButton.addClickListener(new ClickListener() { private static final long serialVersionUID = 1L; @Override public void buttonClick(ClickEvent event) { triggerFilter(); } }); searchButton.setVisible(!dynamicSearch); final OnEnterKeyHandler onEnterKeyHandler = new OnEnterKeyHandler() { @Override public void enterKeyPressed() { if (!dynamicSearch) { searchButton.click(); } } }; onEnterKeyHandler.attachTo(searchField); } private Button createClearButton() { Button clear = new Button("X"); clear.setImmediate(true); clear.addClickListener(new ClickEventLogged.ClickListener() { private static final long serialVersionUID = 1L; @Override public void clicked(ClickEvent event) { searchField.setValue(""); clearAdvancedFilters(); if (triggerFilterOnClear) { triggerFilter(); } } }); return clear; } private AbstractLayout buildAdvancedSearch() { advancedSearchLayout = getAdvancedSearchLayout(); if (advancedSearchLayout != null) { advancedSearchButton = new Button(getAdvancedCaption()); advancedSearchOn = false; advancedSearchButton.setImmediate(true); advancedSearchButton.addClickListener(new ClickListener() { private static final long serialVersionUID = 7777043506655571664L; @Override public void buttonClick(ClickEvent event) { clearAdvancedFilters(); advancedSearchOn = !advancedSearchOn; advancedSearchLayout.setVisible(advancedSearchOn); if (!advancedSearchOn && dynamicSearch) { triggerFilter(); } if (!advancedSearchOn) { advancedSearchButton.setCaption(getAdvancedCaption()); advancedSearchButton.removeStyleName(ValoTheme.BUTTON_FRIENDLY); } else { advancedSearchButton.setCaption(getBasicCaption()); advancedSearchButton.setStyleName(ValoTheme.BUTTON_FRIENDLY); } } }); searchLayout.addComponent(advancedSearchLayout); advancedSearchLayout.setVisible(false); } return advancedSearchLayout; } /** * Show advanced search and, if lockAdvancedSearch is true, lock it into * place * * @param lockAdvancedSearch * lock advanced search into place */ public void showAdvancedSearch(boolean lockAdvancedSearch) { advancedSearchOn = true; advancedSearchLayout.setVisible(advancedSearchOn); advancedSearchButton.setCaption(getBasicCaption()); advancedSearchButton.setStyleName(ValoTheme.BUTTON_FRIENDLY); if (lockAdvancedSearch) { advancedSearchButton.setVisible(false); } } public void closeAdvancedSearch() { clearAdvancedFilters(); advancedSearchOn = false; advancedSearchLayout.setVisible(advancedSearchOn); advancedSearchButton.setCaption(getAdvancedCaption()); advancedSearchButton.removeStyleName(ValoTheme.BUTTON_FRIENDLY); } protected AbstractLayout getAdvancedSearchLayout() { return null; } /** * Used when creating a 'new' record to disable actions such as 'new' and * delete until the record is saved. * * @param show */ protected void activateEditMode(boolean activate) { actionCombo.setEnabled(!activate); actionApplyButton.setEnabled(!activate); boolean showNew = !activate; if (disallowNew) { showNew = false; } actionNewButton.setEnabled(showNew); } /** * A child class can call this method to stop a user from being able to edit * a record. When called the Save/Cancel buttons are disabled from the * screen. If you also set hideSaveCancelLayout to true then the save/cancel * buttons will be completely removed from the layout. By default editing is * allowed. * * @param disallow */ protected void disallowEdit(boolean disallow) { if (buttonLayout != null) { throw new IllegalStateException("You must call disallowEdit before'init()'"); } this.disallowEditing = disallow; } /** * A child class can call this method to stop a user from being able to add * new records. When called the 'New' button is removed from the UI. By * default adding new records is allowed. * * @param disallow */ protected void disallowNew(boolean disallow) { if (buttonLayout != null) { throw new IllegalStateException("You must call disallowNew before'init()'"); } this.disallowNew = disallow; showNew(!disallow); } public boolean isDisallowNew() { return this.disallowNew; } /** * A child class can call this method to stop a user from being able to * delete a record. When called the delete action is removed from the action * combo. If the delete is the only action then the action combo and apply * button will also be removed. By default deleting is allowed. * * @param disallow */ protected void disallowDelete(boolean disallow) { if (this.actionCombo == null) { throw new IllegalStateException("You must call disallowDelete after 'init()'"); } if (disallow || !getSecurityManager().canUserDelete()) { // find and remove the delete action for (Object id : this.actionCombo.getItemIds()) { if (id instanceof CrudActionDelete) { this.actionCombo.removeItem(id); break; } } if (this.actionCombo.size() == 0) { this.actionCombo.setVisible(false); this.actionApplyButton.setVisible(false); this.actionLabel.setVisible(false); } } else { this.actionCombo.removeAllItems(); addCrudActions(); this.actionCombo.setVisible(true); this.actionApplyButton.setVisible(true); this.actionLabel.setVisible(true); } } /** * Internal method to show hide the new button when editing. If the user has * called disallowNew then the new button will never be displayed. */ private void showNew(boolean show) { if (disallowNew) { show = false; } actionNewButton.setVisible(show); } /** * Sets the visibility of the Action layout which contains the 'New' button * and 'Action' combo. Hiding the action layout effectively stops the user * from creating new records or applying any action such as deleting a * record. Hiding the action layout provides more room for the list of * records. * * @param showAction * visibility of action layout */ protected void showActionLayout(final boolean showAction) { actionLayout.setEnabled(showAction); } @Override public void setSplitPosition(float pos) { splitPanel.setSplitPosition(pos); } public void setSplitPosition(float pos, Unit units) { splitPanel.setSplitPosition(pos, units); } public void setLocked(boolean locked) { splitPanel.setLocked(locked); } private void initButtons() { actionNewButton.addClickListener(new ClickEventLogged.ClickListener() { private static final long serialVersionUID = 1L; @Override public void clicked(ClickEvent event) { newClicked(); } }); actionApplyButton.addClickListener(new ClickEventLogged.ClickListener() { private static final long serialVersionUID = 1L; @Override public void clicked(ClickEvent event) { final Object entityId = entityTable.getValue(); if (entityId != null) { @SuppressWarnings("unchecked") CrudAction<E> action = (CrudAction<E>) actionCombo.getValue(); if (action != null) { if (action.showPreparingDialog()) { performActionWithWaitDialog(entityId, action); } else { performAction(entityId, action); } } else { Notification.show("Please select an Action first."); } } else { Notification.show("Please select record first."); } } private void performAction(final Object entityId, final CrudAction<E> action) { EntityItem<E> entity = container.getItem(entityId); if (interceptAction(action, entity)) { action.exec(BaseCrudView.this, entity); } container.commit(); container.refreshItem(entity.getItemId()); // actionCombo.select(actionCombo.getNullSelectionItemId()); } private void performActionWithWaitDialog(final Object entityId, final CrudAction<E> action) { final ConfirmDialog pleaseWaitMessage = createPleaseWaitDialog(); if (action != null) { // we have to delay, because if we try to close the window // before it's created - that won't work. final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); final EntityManagerRunnable runner = invokeAction(entityId, pleaseWaitMessage, action); exec.schedule(runner, 1, TimeUnit.SECONDS); exec.shutdown(); UI.getCurrent().setPollInterval(500); } } private EntityManagerRunnable invokeAction(final Object entityId, final ConfirmDialog pleaseWaitMessage, final CrudAction<E> action) { final EntityManagerRunnable runner = new EntityManagerRunnable(new RunnableUI(UI.getCurrent()) { @Override protected void run(final UI ui) { ui.access(new Runnable() { @Override public void run() { try { EntityItem<E> entity = container.getItem(entityId); if (interceptAction(action, entity)) { action.exec(BaseCrudView.this, entity); } container.commit(); container.refreshItem(entity.getItemId()); // actionCombo.select(actionCombo.getNullSelectionItemId()); } finally { pleaseWaitMessage.close(); ui.setPollInterval(-1); } } }); } }); return runner; } private ConfirmDialog createPleaseWaitDialog() { final ConfirmDialog pleaseWaitMessage = ConfirmDialog.show(UI.getCurrent(), "Please wait...", new ConfirmDialog.Listener() { private static final long serialVersionUID = 1L; @Override public void onClose(ConfirmDialog dialog) { } }); pleaseWaitMessage.setClosable(false); pleaseWaitMessage.getCancelButton().setVisible(false); pleaseWaitMessage.getOkButton().setVisible(false); pleaseWaitMessage.setModal(true); pleaseWaitMessage.setCaption("Preparing Action"); return pleaseWaitMessage; } }); } @Override public void cancelClicked() { fieldGroup.discard(); for (ChildCrudListener<E> child : childCrudListeners) { child.discard(); } if (isMainView) { if (newEntity != null) { if (restoreDelete) { activateEditMode(false); restoreDelete = false; } newEntity = null; // set the selection to the first item on the page. // We need to set it to null first as if the first item was // already selected // then we won't get a row change which is need to update // the rhs. // CONSIDER: On the other hand I'm concerned that we might // confuse people as they // get two row changes events. BaseCrudView.this.entityTable.select(null); BaseCrudView.this.entityTable.select(entityTable.getCurrentPageFirstItemId()); } else { // Force the row to be reselected so that derived // classes get a rowChange when we cancel. // CONSIDER: is there a better way of doing this? // Could we not just fire an 'onCancel' event or similar? Long id = null; if (entityTable.getCurrent() != null) { id = entityTable.getCurrent().getEntity().getId(); } BaseCrudView.this.entityTable.select(null); BaseCrudView.this.entityTable.select(id); } splitPanel.showFirstComponent(); if (entityTable.getCurrent() == null) { showNoSelectionMessage(); } Notification.show("Changes discarded.", "Any changes you have made to this record been discarded.", Type.TRAY_NOTIFICATION); buttonLayout.setDefaultState(); } } /** * Override this method to intercept activation of an action. Return true if * you are happy for the action to proceed otherwise return false if you * want to suppress the action. When suppressing the action you should * display a notification as to why you suppressed it. * * @param action * @param entity * @return */ protected boolean interceptAction(CrudAction<E> action, EntityItem<E> entity) { return true; } public void delete() { E deltedEntity = entityTable.getCurrent().getEntity(); Object entityId = entityTable.getValue(); Object previousItemId = null; // using prevVisableItemId instead of prevItemId because with some // sorting options JPAContainer causes eclipse link ti throw an error // and rolls the transaction back previousItemId = entityTable.prevVisibleItemId(entityId); newEntity = null; preDelete(deltedEntity); // set the selection to the first item // on the page. // We need to set it to null first as if // the first item was already selected // then we won't get a row change which // is need to update the rhs. // CONSIDER: On the other hand I'm // concerned that we might confuse // developers as they // get two row changes events. BaseCrudView.this.entityTable.select(null); if (previousItemId != null) { BaseCrudView.this.entityTable.select(previousItemId); } else { entityTable.select(entityTable.firstItemId()); } try { container.removeItem(entityId); container.commit(); EntityManagerProvider.getEntityManager().flush(); postDelete(deltedEntity); CrudEventDistributer.publishEvent(this, CrudEventType.DELETE, deltedEntity); } catch (Exception e) { logger.error("Exception trying to delete {} from {}", entityId, entityClass.getCanonicalName()); logger.error(e, e); reloadDataFromDB(); Notification.show("An error occurred deleting the record, you may have to delete it's children first", Type.ERROR_MESSAGE); } } /** * hook for implementations that need to do some additional cleanup before a * delete. */ protected void preDelete(E entity) { } /** * hook for implementations that need to do some additional cleanup after a * delete. */ protected void postDelete(E entity) { } @Override public void save() { boolean selected = false; try { commitFieldGroup(); validateChildren(); CrudEventType eventType = CrudEventType.EDIT; if (newEntity != null) { if (!okToSave(newEntity)) { return; } eventType = CrudEventType.CREATE; interceptSaveValues(newEntity); Object id = container.addEntity(newEntity.getEntity()); EntityItem<E> item = container.getItem(id); fieldGroup.setItemDataSource(item); selected = true; if (restoreDelete) { activateEditMode(false); restoreDelete = false; } } else { EntityItem<E> current = entityTable.getCurrent(); if (current != null) { if (!okToSave(current)) { return; } interceptSaveValues(current); } } // commit the row to the database, and retrieve the possibly new // entity E newEntity = commitContainerAndGetEntityFromDB(); if (newEntity == null) { throw new RuntimeException( "An error occurred, unable to retrieve updated record. Failed to save changes"); } newEntity = EntityManagerProvider.getEntityManager().merge(newEntity); for (ChildCrudListener<E> commitListener : childCrudListeners) { // only commit dirty children, saves time for a crud with lots // of children if (commitListener.isDirty()) { commitListener.committed(newEntity); } } EntityManagerProvider.getEntityManager().flush(); // children may have been added to the parent, evict the parent from // the JPA cache so it will get updated EntityManagerProvider.getEntityManager().getEntityManagerFactory().getCache().evict(entityClass, newEntity.getId()); newEntity = EntityManagerProvider.merge(newEntity); postSaveAction(newEntity); EntityManagerProvider.getEntityManager().flush(); reloadDataFromDB(newEntity.getId()); CrudEventDistributer.publishEvent(this, eventType, newEntity); if (eventType == CrudEventType.CREATE) { addFilterToShowNewRow(newEntity); container.discard(); } this.newEntity = null; // select has been moved to here because when it happens earlier, // child cruds are caused to discard their data before saving it for // a new record entityTable.select(newEntity.getId()); splitPanel.showFirstComponent(); Notification.show("Changes Saved", "Any changes you have made have been saved.", Type.TRAY_NOTIFICATION); // return save/edit buttons to default settings buttonLayout.setDefaultState(); } catch (Exception e) { if (e instanceof InvalidValueException) { handleInvalidValueException((InvalidValueException) e); } else if (e.getCause() instanceof InvalidValueException) { handleInvalidValueException((InvalidValueException) e.getCause()); } else { ErrorWindow.showErrorWindow(e); } } finally { if (newEntity != null) { if (selected && entityTable.getCurrent() != null) { container.removeItem(entityTable.getCurrent()); } } buttonLayout.setDefaultState(); } } /** * Override this method if you need to modify filters to allow a newly * created record to be shown */ protected void addFilterToShowNewRow(E id) { } protected void handleInvalidValueException(InvalidValueException m) { String causeMessage = ""; for (InvalidValueException cause : m.getCauses()) { causeMessage += cause.getMessage() + ". "; } if (m.getMessage() != null && m.getMessage().length() > 0) { causeMessage += m.getMessage() + ". "; } Notification.show("Please fix the form errors and then try again.\n\n " + causeMessage, Type.ERROR_MESSAGE); } /** * @throws Exception */ protected boolean okToSave(EntityItem<E> entity) throws Exception { return true; } /** * 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 E commitContainerAndGetEntityFromDB() { // don't really need an AtomicReference, just using it as a mutable // final variable to be used in the callback final AtomicReference<E> entityReference = new AtomicReference<>(); // call back to collect the id of the new record when the container // fires the ItemSetChangeEvent ItemSetChangeListener tempListener = new ItemSetChangeListener() { private static final long serialVersionUID = 8086546233136795406L; @Override public void containerItemSetChange(ItemSetChangeEvent event) { if (event instanceof ProviderChangedEvent) { @SuppressWarnings("rawtypes") ProviderChangedEvent pce = (ProviderChangedEvent) event; @SuppressWarnings("unchecked") Collection<E> affectedEntities = pce.getChangeEvent().getAffectedEntities(); if (affectedEntities.size() > 0) { @SuppressWarnings("unchecked") E entity = (E) affectedEntities.toArray()[0]; entityReference.set(entity); } } } }; // Only remove the listeners if we are editing an existing entity. If it // is a new entity then we need the listeners to update components with // the new entity. final LinkedList<ItemSetChangeListener> listeners = new LinkedList<>(); if (newEntity == null) { listeners.addAll(container.getItemSetChangeListeners()); } try { // get existing listeners and remove them for (ItemSetChangeListener listener : listeners) { container.removeItemSetChangeListener(listener); } // add the temp listener container.addItemSetChangeListener(tempListener); // call commit container.commit(); entityReference.set(EntityManagerProvider.getEntityManager().merge(entityReference.get())); } catch (Exception e) { ErrorWindow.showErrorWindow(e); } finally { // detach the temp listener container.removeItemSetChangeListener(tempListener); // restore the existing listeners for (ItemSetChangeListener listener : listeners) { container.addItemSetChangeListener(listener); } } // return the entity return entityReference.get(); } /** * called after a record has been committed to the database */ protected void postSaveAction(E entityItem) { } /** * opportunity for implementing classes to modify or add data to the entity * being saved. NOTE: modify the item properties not the entity as accessing * the entity is unreliable * * @param item * @throws Exception */ protected void interceptSaveValues(EntityItem<E> entityItem) throws Exception { } private String searchFieldText = ""; protected String getSearchFieldText() { return searchFieldText; } private void initSearch() { searchField.setInputPrompt("Search"); searchField.setTextChangeEventMode(TextChangeEventMode.LAZY); searchField.setImmediate(true); searchField.focus(); searchField.addValueChangeListener(new ValueChangeListener() { private static final long serialVersionUID = 1L; @Override public void valueChange(ValueChangeEvent event) { searchFieldText = (String) event.getProperty().getValue(); } }); searchField.addTextChangeListener(new TextChangeListener() { private static final long serialVersionUID = 1L; @Override public void textChange(final TextChangeEvent event) { try { // If advanced search is active then it should be // responsible // for triggering the filter. if (triggerSearchOnTextChange()) { searchFieldText = event.getText(); if (dynamicSearch && searchFieldText.length() >= minSearchTextLength) { triggerFilter(searchFieldText); } } } catch (Exception e) { logger.error(e, e); Notification.show(e.getClass().getSimpleName() + " " + e.getMessage(), Type.ERROR_MESSAGE); } } }); searchField.focus(); } /** * override this method if you have searches to trigger when the user types * in the search field even if advanced search is on * * @return */ protected Boolean triggerSearchOnTextChange() { return !advancedSearchOn; } /** * the minimum number of characters entered in to the search field required * to trigger a search, default is 0 * * @param minLength */ public void setMinimumSearchTextLength(int minLength) { minSearchTextLength = minLength; } /** * call this method to cause filters to be applied */ public void triggerFilter() { triggerFilter(searchField.getValue()); } private int emptyFilterWarningCount = 3; protected void triggerFilter(String searchText) { boolean advancedSearchActive = advancedSearchOn; Filter filter = getContainerFilter(searchText.trim(), advancedSearchActive); if (filter == null && emptyFilterWarningCount-- > 0) { logger.warn("({}.java:1) getContainerFilter() returned NULL", this.getClass().getCanonicalName()); } applyFilter(filter); } public void applyFilter(final Filter filter) { try { // If there are filters to be applied then don't fire item set // change listeners until we have applied the correct set of filters if (filter != null) { container.setFireContainerItemSetChangeEvents(false); resetFilters(); container.setFireContainerItemSetChangeEvents(true); container.addContainerFilter(filter); } else { resetFilters(); } container.discard(); entityTable.select(entityTable.firstItemId()); } catch (Exception e) { ErrorWindow.showErrorWindow(e); } } /** * create a filter for the text supplied, the text is as entered in the text * search bar. * * @param string * @return */ abstract protected Filter getContainerFilter(String filterString, boolean advancedSearchActive); /** * called when the advancedFilters layout should clear it's values */ protected void clearAdvancedFilters() { } @Override /** * Called when the currently selected row in the table part of this view has * changed. We use this to update the editor's current item. */ public void allowRowChange(final RowChangeCallback callback) { boolean dirty = false; for (ChildCrudListener<E> commitListener : childCrudListeners) { dirty |= commitListener.isDirty(); } if (fieldGroup.isModified() || newEntity != null || dirty) { ConfirmDialog.show(UI.getCurrent(), "Discard changes?", "You have unsaved changes for this record. Continuing will result in those changes being discarded. ", "Continue", "Cancel", new ConfirmDialog.Listener() { private static final long serialVersionUID = 1L; @Override public void onClose(ConfirmDialog dialog) { if (dialog.isConfirmed()) { /* * When an entity is selected from the list, we * want to show that in our editor on the right. * This is nicely done by the FieldGroup that * binds all the fields to the corresponding * Properties in our entity at once. */ fieldGroup.discard(); for (ChildCrudListener<E> child : childCrudListeners) { child.discard(); } if (restoreDelete) { activateEditMode(false); restoreDelete = false; } newEntity = null; callback.allowRowChange(); } else { // User did not confirm so don't allow // the change. } } }); } else { try { callback.allowRowChange(); } catch (Exception e) { ErrorWindow.showErrorWindow(e); } } } @Override /** * Called when the currently selected row in the table part of this view has * changed. We use this to update the editor's current item. * * @item the item that is now selected. This may be null if selection has * been lost. */ public void rowChanged(EntityItem<E> item) { splitPanel.showSecondComponent(); fieldGroup.setItemDataSource(item); Map<String, Long> times = new HashMap<>(); // notifiy ChildCrudView's that we've changed row. for (ChildCrudListener<E> commitListener : childCrudListeners) { Stopwatch timer = Stopwatch.createUnstarted(); timer.start(); commitListener.selectedParentRowChanged(item); times.put(commitListener.getClass().getSimpleName() + ":" + commitListener.hashCode(), timer.elapsed(TimeUnit.MILLISECONDS)); } if (item != null || newEntity != null) { splitPanel.setSecondComponent(rightLayout); } else { showNoSelectionMessage(); } rightLayout.setVisible(item != null || newEntity != null); if (item == null) { notifyRowChangedListeners(null); } else { notifyRowChangedListeners(item.getEntity()); } for (Entry<String, Long> time : times.entrySet()) { logger.debug("{}: {}ms", time.getKey(), time.getValue()); } if (allowCurrentRowEdit(item)) { if (this.getButtonLayout() != null) { if (this.getButtonLayout().getCancelButton() != null) { this.getButtonLayout().getCancelButton().setEnabled(true); } if (this.getButtonLayout().getSaveButton() != null) { this.getButtonLayout().getSaveButton().setEnabled(true); } } if (this.actionApplyButton != null) { this.actionApplyButton.setEnabled(true); } } else { if (this.getButtonLayout() != null) { if (this.getButtonLayout().getCancelButton() != null) { this.getButtonLayout().getCancelButton().setEnabled(false); } if (this.getButtonLayout().getSaveButton() != null) { this.getButtonLayout().getSaveButton().setEnabled(false); } } // TODO: we shouldn't be removing the apply button but rather just // the Delete action. if (this.actionApplyButton != null) { this.actionApplyButton.setEnabled(false); } } } /** * Overload this method to control if a specific row is editable. By default * all rows are editable. This method is called each time the row changes. * * @param item * @return true if the row should be editable by the user. */ protected boolean allowCurrentRowEdit(EntityItem<E> item) { return true; } protected void showNoSelectionMessage() { String message = ""; if (actionNewButton.isVisible()) { message = "Click New to create a new record."; if (entityTable.firstItemId() != null) { message = "Click New to create a new record or click an existing " + "record to view and or edit the records details."; } } else { if (entityTable.firstItemId() != null) { message = "click an existing record to view and or edit the records details."; } else { message = "No records were found."; } } VerticalLayout pane = new VerticalLayout(); pane.setSizeFull(); Label label = new Label(message); label.setWidth("300"); label.setContentMode(ContentMode.HTML); pane.addComponent(label); pane.setComponentAlignment(label, Alignment.MIDDLE_CENTER); splitPanel.setSecondComponent(pane); } protected void commitFieldGroup() throws CommitException { formValidate(); String fieldName = selectFirstErrorFieldAndShowTab(this.fieldGroup); if (!fieldGroup.isValid()) { throw new InvalidValueException("Invalid Field: " + fieldName); } fieldGroup.commit(); } protected void validateChildren() { for (ChildCrudListener<E> child : childCrudListeners) { // Only validate dirty children // Note: If there is existing bad data in the database then // this will pass validation if nothing has been edited on the child if (child.isDirty()) { child.validateFieldz(); } } } /** * Overload this method to provide cross-field (form level) validation. * * @return */ protected void formValidate() throws InvalidValueException { } VerticalLayout getEmptyPanel() { VerticalLayout layout = new VerticalLayout(); Label pleaseAdd = new Label( "Click the 'New' button to add a new Record or click an existing record in the adjacent table to edit it."); layout.addComponent(pleaseAdd); layout.setComponentAlignment(pleaseAdd, Alignment.MIDDLE_CENTER); layout.setSizeFull(); return layout; } @Override public E getCurrent() { E entity = null; if (newEntity != null) { entity = newEntity.getEntity(); } if (entity == null) { EntityItem<E> entityItem = entityTable.getCurrent(); if (entityItem != null) { entity = entityItem.getEntity(); } } return entity; } /** * update the container and editor with any changes from the db. */ public void updateEditorFromDb() { Preconditions.checkState(!isDirty(), "The editor is dirty, save or cancel first."); E entity = entityTable.getCurrent().getEntity(); container.refresh(); entityTable.select(null); entityTable.select(entity.getId()); } /** * check if the editor has changes * * @return */ @Override public boolean isDirty() { return fieldGroup.isModified() || newEntity != null; } /** * a ChildCrudView adds it's self here so it will be notified when the * parent saves * * @param listener */ @Override public void addChildCrudListener(ChildCrudListener<E> listener) { childCrudListeners.add(listener); } public void newClicked() { /* * Rows in the Container data model are called Item. Here we add a new * row in the beginning of the list. */ allowRowChange(new RowChangeCallback() { @Override public void allowRowChange() { try { if (advancedSearchLayout != null) { closeAdvancedSearch(); } final E previousEntity = getCurrent(); createNewEntity(previousEntity); rowChanged(newEntity); // Can't delete when you are adding a new record. // Use cancel instead. if (actionApplyButton.isVisible()) { restoreDelete = true; activateEditMode(true); } rightLayout.setVisible(true); selectFirstFieldAndShowTab(); postNew(newEntity); buttonLayout.startNewPhase(); } catch (Exception e) { ErrorWindow.showErrorWindow(e); } } }); } protected void selectFirstFieldAndShowTab() { for (Field<?> field : fieldGroup.getFields()) { Component childField = field; for (int i = 0; i < 10; i++) { Component parentField = childField.getParent(); if (parentField instanceof TabSheet) { ((TabSheet) parentField).setSelectedTab(childField); break; } childField = parentField; } field.focus(); break; } } protected String selectFirstErrorFieldAndShowTab(ValidatingFieldGroup<? extends CrudEntity> fieldGroup) { String ret = ""; int ctr = 0; for (Field<?> field : fieldGroup.getFields()) { try { ctr++; field.validate(); } catch (Exception e) { String message = ""; if (e instanceof InvalidValueException) { message = ((InvalidValueException) e).getMessage(); if (message == null) { for (InvalidValueException cause : ((InvalidValueException) e).getCauses()) { message = cause.getMessage(); if (message != null) { break; } } } } ret = field.getCaption() + "\n\n" + message; logger.warn( "Invalid Field...\n caption:'{}'\n type:{}\n fieldNumber: {}\n value: '{}'\n crud: {} ({})\n {}\n", field.getCaption(), field.getClass().getSimpleName(), ctr, field.getValue(), this.getClass().getCanonicalName(), this.getClass().getSimpleName() + ".java:1", message); Component childField = field; for (int i = 0; i < 10; i++) { Component parentField = childField.getParent(); if (parentField instanceof TabSheet) { ((TabSheet) parentField).setSelectedTab(childField); break; } if (parentField == null) { // couldn't find a tab in the hierarchy break; } childField = parentField; } break; } } return ret; } /** * you might want to implement this method in a child crud that needs to * load some sort of list when a new entity is created based on the parent * * @throws InstantiationException * @throws IllegalAccessException */ protected void createNewEntity(E previousEntity) throws InstantiationException, IllegalAccessException { newEntity = container.createEntityItem(preNew(previousEntity)); } /** * override this method if you have child entities, you can use this * opportunity to do some dirty hacking to populate fields * * @param newEntity */ @SuppressWarnings("unchecked") protected void postNew(EntityItem<E> newEntity) { if (dragAndDropOrderingEnabled) { newEntity.getItemProperty(ordinalField.getName()).setValue(container.size() + 1); } } /** * Override this method if you need to initialise the entity when a new * record is created. * * @param newEntity * @return * @throws IllegalAccessException * @throws InstantiationException */ protected E preNew(E previousEntity) throws InstantiationException, IllegalAccessException { return entityClass.newInstance(); } /** * after making changes via JPA, to get the crud to see the changes call * this method if needed. beware it is a costly operation. commit the * transaction, then refresh the container */ @Override public void reloadDataFromDB() { reloadDataFromDB(null); } public void reloadDataFromDB(Long itemId) { Object selectedId = null; if (entityTable.getCurrent() != null) { selectedId = entityTable.getCurrent().getItemId(); } EntityManagerProvider.getEntityManager().getTransaction().commit(); EntityManagerProvider.getEntityManager().getTransaction().begin(); if (itemId != null) { container.refreshItem(itemId); } else { container.refresh(); } if (selectedId == null) { entityTable.select(null); entityTable.select(entityTable.firstItemId()); } else { entityTable.select(selectedId); } } protected void resetFiltersWithoutChangeEvents() { container.setFireContainerItemSetChangeEvents(false); resetFilters(); container.setFireContainerItemSetChangeEvents(true); } /** * for child cruds, they overload this to ensure that the minimum necessary * filters are always applied. You must ensure that you call * container.removeAllContainerFilters() otherwise the set of filters will * continue to grow. CONSIDER: should we just be calling * container.removeAllContainerFilters() before this method is called to * ensure it happens regardless? */ protected void resetFilters() { try { container.removeAllContainerFilters(); } catch (Exception e) { ErrorWindow.showErrorWindow(e); } } @Override public boolean isNew() { return this.newEntity != null; } @Override public JPAContainer<E> getContainer() { return container; } // disabled as the save/cancel enable/disable is buggy @Override public void fieldGroupIsDirty(boolean b) { // saveButton.setEnabled(b); // cancelButton.setEnabled(b); } Set<ChildCrudListener<E>> getChildCrudListeners() { return Collections.unmodifiableSet(childCrudListeners); } public DeleteVetoResponseData canDelete(E entity) { return new DeleteVetoResponseData(true); } private void notifyRowChangedListeners(E entity) { for (RowChangedListener<E> listener : rowChangedListeners) { listener.rowChanged(entity); } } public void addRowChangedListener(RowChangedListener<E> listener) { rowChangedListeners.add(listener); } public void removeRowChangedListener(RowChangedListener<E> listener) { rowChangedListeners.remove(listener); } @Override public void saveClicked() { // If interceptSaveClicked returns false then abort saving if (!interceptSaveClicked()) { // return save/edit buttons to default settings buttonLayout.setDefaultState(); return; } for (ChildCrudListener<E> child : getChildCrudListeners()) { if (!child.interceptSaveClicked()) { // return save/edit buttons to default settings buttonLayout.setDefaultState(); return; } } save(); } /** * Override this method to intercept the save process after clicking the * save button. Return true if you would like the save action to proceed * otherwise return false if you want to halt the save process. When * suppressing the action you should display a notification as to why you * suppressed it. * * @return whether to continue saving */ public boolean interceptSaveClicked() { return true; } @Override public void setSearchFilterText(String string) { if (!searchField.getValue().equals(string)) { entityTable.select(null); searchField.setValue(string); triggerFilter(); } } public boolean isNewAllowed() { return !disallowNew; } public boolean hasDirtyChildren() { boolean dirty = false; for (ChildCrudListener<E> child : childCrudListeners) { dirty |= child.isDirty(); } return dirty; } @Override public BaseCrudSaveCancelButtonTray getButtonLayout() { return buttonLayout; } public EntityList<E> getEntityTable() { return entityTable; } @Override public ValidatingFieldGroup<E> getFieldGroup() { return fieldGroup; } public void setDynamicSearch(boolean dynamicSearch) { this.dynamicSearch = dynamicSearch; searchButton.setVisible(!dynamicSearch); } @Override public EntityItem<E> getNewEntity() { return newEntity; } public void setTriggerFilterOnClear(boolean triggerFilterOnClear) { this.triggerFilterOnClear = triggerFilterOnClear; } public boolean isDisallowEditing() { return disallowEditing; } /** * Set this to false if you are using the BaseCrudView's buttonLayout in a * different page (e.g. pop-up window) and wants to limit the * cancelClicked() action * * @param isMainView */ @Override public void setMainView(boolean isMainView) { this.isMainView = isMainView; } /** * Use this to remove temporaryChildCrudListener * * @param listener * ChildCrudListener that you do not intend to keep */ @Override public void removeChildCrudListener(ChildCrudListener<E> listener) { if (childCrudListeners != null && childCrudListeners.contains(listener)) { childCrudListeners.remove(listener); } } @Override public EntityItem<E> getContainerItem(Long id) { return container.getItem(id); } protected String getAdvancedCaption() { return "Advanced"; } protected String getBasicCaption() { return "Basic"; } public VerticalLayout getLeftLayout() { return leftLayout; } public VerticalLayout getRightLayout() { return rightLayout; } }