Java tutorial
/* * Copyright (c) 2011 Brown Bag Consulting. * This file is part of the PureCRUD project. * Author: Juan Osuna * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License Version 3 * as published by the Free Software Foundation with the addition of the * following permission added to Section 15 as permitted in Section 7(a): * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY * Brown Bag Consulting, Brown Bag Consulting DISCLAIMS THE WARRANTY OF * NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License. * * You can be released from the requirements of the license by purchasing * a commercial license. Buying such a license is mandatory as soon as you * develop commercial activities involving the PureCRUD software without * disclosing the source code of your own applications. These activities * include: offering paid services to customers as an ASP, providing * services from a web application, shipping PureCRUD with a closed * source product. * * For more information, please contact Brown Bag Consulting at this * address: juan@brownbagconsulting.com. */ package com.purebred.core.view; import com.purebred.core.dao.EntityDao; import com.purebred.core.entity.WritableEntity; import com.purebred.core.entity.security.AbstractUser; import com.purebred.core.security.SecurityService; import com.purebred.core.util.MethodDelegate; import com.purebred.core.util.SpringApplicationContext; import com.purebred.core.validation.AssertTrueForProperties; import com.purebred.core.validation.Validation; import com.purebred.core.view.field.FormField; import com.purebred.core.view.field.SelectField; import com.purebred.core.view.tomanyrelationship.ToManyRelationship; import com.vaadin.data.Item; import com.vaadin.data.Validator; import com.vaadin.data.util.BeanItem; import com.vaadin.terminal.ErrorMessage; import com.vaadin.terminal.ThemeResource; import com.vaadin.terminal.UserError; import com.vaadin.ui.*; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.validation.ConstraintViolation; import javax.validation.metadata.ConstraintDescriptor; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; /** * A form bound to a JPA entity, providing save, refresh and cancel actions. Underneath the form * are tabs representing to-many relationships, allowing related entities to be added and removed. * * @param <T> type of entity */ public abstract class EntityForm<T> extends FormComponent<T> { @Resource private Validation validation; @Resource private SecurityService securityService; private boolean isViewMode; private TabSheet toManyRelationshipTabs; private Button refreshButton; private Button saveAndCloseButton; private Button saveAndStayOpenButton; private boolean isValidationEnabled = true; private List<MethodDelegate> persistListeners = new ArrayList<MethodDelegate>(); private List<MethodDelegate> closeListeners = new ArrayList<MethodDelegate>(); private List<MethodDelegate> cancelListeners = new ArrayList<MethodDelegate>(); private List<MethodDelegate> saveListeners = new ArrayList<MethodDelegate>(); /** * Get all to-many relationships. * * @return list of to-many relationships */ public List<ToManyRelationship> getToManyRelationships() { return new ArrayList<ToManyRelationship>(); } /** * Get all viewable to-many relationships, based on security permissions. * * @return all viewable to-many relationships */ public List<ToManyRelationship> getViewableToManyRelationships() { List<ToManyRelationship> viewableToManyRelationships = new ArrayList<ToManyRelationship>(); List<ToManyRelationship> toManyRelationships = getToManyRelationships(); for (ToManyRelationship toManyRelationship : toManyRelationships) { AbstractUser user = securityService.getCurrentUser(); if (user.isViewAllowed(toManyRelationship.getResults().getEntityType().getName()) && !toManyRelationship.getResults().getDisplayFields().getViewablePropertyIds().isEmpty()) { viewableToManyRelationships.add(toManyRelationship); } } return viewableToManyRelationships; } @PostConstruct @Override public void postConstruct() { super.postConstruct(); addStyleName("p-entity-form"); List<ToManyRelationship> toManyRelationships = getViewableToManyRelationships(); if (toManyRelationships.size() > 0) { toManyRelationshipTabs = new TabSheet(); toManyRelationshipTabs.setSizeUndefined(); for (ToManyRelationship toManyRelationship : toManyRelationships) { toManyRelationshipTabs.addTab(toManyRelationship); labelDepot.putFieldLabel(getEntityType().getName(), toManyRelationship.getResults().getChildPropertyId(), "Relationship", toManyRelationship.getResults().getEntityCaption()); } Layout layout = new HorizontalLayout(); layout.setSizeUndefined(); Label label = new Label(" ", Label.CONTENT_XHTML); layout.addComponent(label); layout.addComponent(toManyRelationshipTabs); addComponent(layout); } } @Override public void postWire() { super.postWire(); List<ToManyRelationship> toManyRelationships = getViewableToManyRelationships(); for (ToManyRelationship toManyRelationship : toManyRelationships) { toManyRelationship.postWire(); } Collection<FormField> formFields = getFormFields().getFormFields(); for (FormField formField : formFields) { Field field = formField.getField(); if (field instanceof SelectField) { ((SelectField) field).getEntitySelect().postWire(); } } } /** * Animate the component if and only if there are viewable to-many tabs * * @param component component to show/hide * @return the newly created layout that contains the toggle button and animated component */ @Override protected Component animate(Component component) { if (getViewableToManyRelationships().size() > 0) { return super.animate(component); } else { return component; } } /** * Created the footer buttons: cancel, refresh, save * * @param footerLayout horizontal layout containing buttons */ @Override protected void createFooterButtons(HorizontalLayout footerLayout) { footerLayout.setSpacing(true); footerLayout.setMargin(true); Button cancelButton = new Button(uiMessageSource.getMessage("entityForm.cancel"), this, "cancel"); cancelButton.setDescription(uiMessageSource.getMessage("entityForm.cancel.description")); cancelButton.setIcon(new ThemeResource("icons/16/cancel.png")); cancelButton.addStyleName("small default"); footerLayout.addComponent(cancelButton); refreshButton = new Button(uiMessageSource.getMessage("entityForm.refresh"), this, "refresh"); refreshButton.setDescription(uiMessageSource.getMessage("entityForm.refresh.description")); refreshButton.setIcon(new ThemeResource("icons/16/refresh.png")); refreshButton.addStyleName("small default"); footerLayout.addComponent(refreshButton); saveAndStayOpenButton = new Button(uiMessageSource.getMessage("entityForm.saveAndStayOpen"), this, "saveAndStayOpen"); saveAndStayOpenButton.setDescription(uiMessageSource.getMessage("entityForm.save.description")); saveAndStayOpenButton.setIcon(new ThemeResource("icons/16/save.png")); saveAndStayOpenButton.addStyleName("small default"); footerLayout.addComponent(saveAndStayOpenButton); saveAndCloseButton = new Button(uiMessageSource.getMessage("entityForm.saveAndClose"), this, "saveAndClose"); saveAndCloseButton.setDescription(uiMessageSource.getMessage("entityForm.save.description")); saveAndCloseButton.setIcon(new ThemeResource("icons/16/save.png")); saveAndCloseButton.addStyleName("small default"); footerLayout.addComponent(saveAndCloseButton); } /** * Ask if this form is in read/view-only mode. * * @return true if in view-only mode */ public boolean isViewMode() { return isViewMode; } /** * Set if this form is in read/view-only mode. Note that this action does not immediately change * fields to read-only or restore them to writable. It just sets the mode for the next time * an entity is loaded. * * @param viewMode true if in view-only mode */ public void setViewMode(boolean viewMode) { isViewMode = viewMode; } /** * Set all entire form to read-only or writable, including fields, to-many relationships and action buttons * * @param isReadOnly true to set to read-only, otherwise make writable */ @Override public void setReadOnly(boolean isReadOnly) { getFormFields().setReadOnly(isReadOnly); saveAndCloseButton.setVisible(!isReadOnly); saveAndStayOpenButton.setVisible(!isReadOnly); refreshButton.setVisible(!isReadOnly); List<ToManyRelationship> toManyRelationships = getToManyRelationships(); for (ToManyRelationship toManyRelationship : toManyRelationships) { toManyRelationship.getResults().setReadOnly(isReadOnly); } } /** * Restore the read-only settings of the form fields to how they were originally configured. */ public void restoreIsReadOnly() { getFormFields().restoreIsReadOnly(); saveAndCloseButton.setVisible(true); saveAndStayOpenButton.setVisible(true); refreshButton.setVisible(true); List<ToManyRelationship> toManyRelationships = getToManyRelationships(); for (ToManyRelationship toManyRelationship : toManyRelationships) { toManyRelationship.getResults().setReadOnly(false); } } /** * Apply security permission logic to fields for controlling if each field is editable */ public void applySecurityIsEditable() { saveAndCloseButton.setVisible(true); saveAndStayOpenButton.setVisible(true); refreshButton.setVisible(true); getFormFields().applySecurityIsEditable(); List<ToManyRelationship> toManyRelationships = getToManyRelationships(); for (ToManyRelationship toManyRelationship : toManyRelationships) { toManyRelationship.getResults().applySecurityIsEditable(); } } /** * Ask if the current entity bound to this form is persistent, i.e. has a primary key assigned * * @return true if entity has primary key */ public boolean isEntityPersistent() { return getEntityDao().isPersistent(getEntity()); } /** * Ask if validation is currently enabled for this form. Sometimes it is useful to temporarily turn off * validation, e.g. when setting a new data-source * * @return true if validation is currently enabled */ public boolean isValidationEnabled() { return isValidationEnabled; } private void setItemDataSource(Item newDataSource, Collection<?> propertyIds) { isValidationEnabled = false; getForm().setItemDataSource(newDataSource, propertyIds); isValidationEnabled = true; } /** * Load and bind a new entity to the form. Automatically selects the first tab (if tabs exist), whenever * a new entity is loaded. * * @param entity entity to load */ public void load(WritableEntity entity) { load(entity, true); } /** * Load and bind a new entity to the form. * * @param entity entity to load * @param selectFirstTab true to select the first tab, once the entity is loaded */ public void load(WritableEntity entity, boolean selectFirstTab) { WritableEntity loadedEntity = (WritableEntity) getEntityDao().find(entity.getId()); BeanItem beanItem = createBeanItem(loadedEntity); setItemDataSource(beanItem, getFormFields().getPropertyIds()); getFormFields().autoAdjustWidths(); validate(true); loadToManyRelationships(); resetTabs(selectFirstTab); } @Override protected BeanItem createBeanItem(Object entity) { isValidationEnabled = false; BeanItem beanItem = super.createBeanItem(entity); isValidationEnabled = true; return beanItem; } private EntityDao getEntityDao() { return SpringApplicationContext.getBeanByTypeAndGenericArgumentType(EntityDao.class, getEntityType()); } private void loadToManyRelationships() { List<ToManyRelationship> toManyRelationships = getViewableToManyRelationships(); if (toManyRelationships.size() > 0) { for (ToManyRelationship toManyRelationship : toManyRelationships) { Object parent = getEntity(); toManyRelationship.getResults().getEntityQuery().clear(); toManyRelationship.getResults().getEntityQuery().setParent(parent); toManyRelationship.getResults().search(); } toManyRelationshipTabs.setVisible(true); setFormAnimatorToggleButtonVisible(true); } } /** * Clear the form and all errors. */ public void clear() { clearAllErrors(true); setItemDataSource(null, getFormFields().getPropertyIds()); } /** * Create an empty form with an empty entity. Makes to-many relationship tabs invisible. User must * save an entity and reload to add to-many relationships. */ public void create() { createImpl(); if (toManyRelationshipTabs != null) { toManyRelationshipTabs.setVisible(false); setFormAnimatorToggleButtonVisible(false); } } private void createImpl() { Object newEntity = createEntity(); BeanItem beanItem = createBeanItem(newEntity); setItemDataSource(beanItem, getFormFields().getPropertyIds()); validate(true); resetTabs(); } private T createEntity() { try { return (T) getEntityType().newInstance(); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } private void resetTabs() { resetTabs(true); } private void resetTabs(boolean selectFirstTab) { if (selectFirstTab) { selectFirstToManyTab(); } if (!hasTabs()) return; Set<String> viewableTabNames = getFormFields().getViewableTabNames(); Set<String> tabNames = getFormFields().getTabNames(); for (String tabName : tabNames) { TabSheet.Tab tab = getTabByName(tabName); Set<FormField> fields = getFormFields().getFormFields(tabName); if (getFormFields().isTabOptional(tabName)) { boolean isTabEmpty = true; for (FormField field : fields) { if (field.getField().getValue() != null) { isTabEmpty = false; break; } } setIsRequiredEnable(tabName, !isTabEmpty); tab.setClosable(!isViewMode()); tab.setVisible(!isTabEmpty && viewableTabNames.contains(tabName)); } else { tab.setVisible(viewableTabNames.contains(tabName)); } } resetContextMenu(); if (selectFirstTab || !getTabByName(getCurrentlySelectedTabName()).isVisible()) { selectFirstTab(); } syncTabAndSaveButtonErrors(); } private void selectFirstToManyTab() { if (toManyRelationshipTabs != null) { toManyRelationshipTabs.setSelectedTab(toManyRelationshipTabs.getTab(0).getComponent()); } } @Override protected void resetContextMenu() { if (getFormFields().hasOptionalTabs()) { Set<String> tabNames = getFormFields().getViewableTabNames(); for (String tabName : tabNames) { TabSheet.Tab tab = getTabByName(tabName); String caption = uiMessageSource.getMessage("formComponent.add") + " " + tabName; if (menu.containsItem(caption)) { menu.getContextMenuItem(caption).setVisible(!tab.isVisible() && !isViewMode()); } caption = uiMessageSource.getMessage("formComponent.remove") + " " + tabName; if (menu.containsItem(caption)) { menu.getContextMenuItem(caption).setVisible(tab.isVisible() && !isViewMode()); } } } } /** * Add cancel listener. * * @param target target object * @param methodName name of method to invoke */ public void addCancelListener(Object target, String methodName) { cancelListeners.add(new MethodDelegate(target, methodName)); } /** * Add close listener. Listener is invoked when form is closed. * * @param target target object * @param methodName name of method to invoke */ public void addCloseListener(Object target, String methodName) { closeListeners.add(new MethodDelegate(target, methodName)); } /** * Add save listener. Listener is invoked when form is saved. * * @param target target object * @param methodName name of method to invoke */ public void addSaveListener(Object target, String methodName) { saveListeners.add(new MethodDelegate(target, methodName)); } /** * Add persist listener. Listener is invoked only when saving a new entity. * * @param target target object * @param methodName name of method to invoke */ public void addPersistListener(Object target, String methodName) { persistListeners.add(new MethodDelegate(target, methodName)); } /** * Cancel and close the form, disgarding any changes. */ public void cancel() { clearAllErrors(true); getForm().discard(); BeanItem beanItem = (BeanItem) getForm().getItemDataSource(); if (beanItem == null) { clear(); } else { WritableEntity entity = (WritableEntity) beanItem.getBean(); if (entity.getId() == null) { clear(); } else { load(entity); } } for (MethodDelegate cancelListener : cancelListeners) { cancelListener.execute(); } } /** * Save changes to the entity, either persisting a transient entity or updating existing one. */ public void saveAndClose() { saveImpl(true); } public void saveAndStayOpen() { saveImpl(false); } private void saveImpl(boolean executeCloseListeners) { boolean isValid = validate(false); if (getForm().isValid() && isValid) { getForm().commit(); WritableEntity entity = (WritableEntity) getEntity(); if (entity.getId() != null) { entity.updateLastModified(); WritableEntity mergedEntity = (WritableEntity) getEntityDao().merge(entity); load(mergedEntity, false); } else { getEntityDao().persist(entity); load(entity, false); if (!executeCloseListeners) { loadToManyRelationships(); } for (MethodDelegate persistListener : persistListeners) { persistListener.execute(); } } for (MethodDelegate saveListener : saveListeners) { saveListener.execute(); } if (executeCloseListeners) { for (MethodDelegate closeListener : closeListeners) { closeListener.execute(); } } } } /** * Reload entity from the database, disgarding any changes and clearing any errors. */ public void refresh() { clearAllErrors(true); BeanItem beanItem = (BeanItem) getForm().getItemDataSource(); if (beanItem == null) { createImpl(); } else { WritableEntity entity = (WritableEntity) beanItem.getBean(); if (entity.getId() == null) { createImpl(); } else { getForm().discard(); load(entity, false); } } } @Override void executeContextAction(String name) { super.executeContextAction(name); validate(false); } /** * Validate this form by validating the bound JPA entity, annotated with JSR 303 annotations. * * @param clearConversionErrors true if any existing type-conversion errors should be cleared * @return true if no validation errors were found */ public boolean validate(boolean clearConversionErrors) { WritableEntity entity = (WritableEntity) getEntity(); clearAllErrors(clearConversionErrors); Set<ConstraintViolation<WritableEntity>> constraintViolations = validation.validate(entity); for (ConstraintViolation constraintViolation : constraintViolations) { String propertyPath = constraintViolation.getPropertyPath().toString(); ConstraintDescriptor descriptor = constraintViolation.getConstraintDescriptor(); Annotation annotation = descriptor.getAnnotation(); if (propertyPath.isEmpty()) { Validator.InvalidValueException error = new Validator.InvalidValueException( constraintViolation.getMessage()); getForm().setComponentError(error); } else { FormField field; if (annotation instanceof AssertTrueForProperties) { if (propertyPath.lastIndexOf(".") > 0) { propertyPath = propertyPath.substring(0, propertyPath.lastIndexOf(".") + 1); } else { propertyPath = ""; } AssertTrueForProperties assertTrueForProperties = (AssertTrueForProperties) annotation; propertyPath += assertTrueForProperties.errorProperty(); } field = getFormFields().getFormField(propertyPath); if (!field.hasIsRequiredError()) { Validator.InvalidValueException error = new Validator.InvalidValueException( constraintViolation.getMessage()); field.addError(error); } } } syncTabAndSaveButtonErrors(); return constraintViolations.isEmpty(); } private void clearAllErrors(boolean clearConversionErrors) { getFormFields().clearErrors(clearConversionErrors); getForm().setComponentError(null); saveAndCloseButton.setComponentError(null); saveAndStayOpenButton.setComponentError(null); Set<String> tabNames = getFormFields().getViewableTabNames(); for (String tabName : tabNames) { setTabError(tabName, null); } } private void setTabError(String tabName, ErrorMessage error) { TabSheet.Tab tab = getTabByName(tabName); if (tab != null) { tab.setComponentError(error); } } /** * Reset validation error indicators on tabs and the save buttons, according to whether * any validation errors exist in any fields. */ public void syncTabAndSaveButtonErrors() { Set<String> tabNames = getFormFields().getViewableTabNames(); boolean formHasErrors = false; for (String tabName : tabNames) { if (getFormFields().hasError(tabName)) { setTabError(tabName, new UserError("Tab contains invalid values")); formHasErrors = true; } else { setTabError(tabName, null); } } if (getForm().getComponentError() != null) { formHasErrors = true; } if (formHasErrors) { saveAndCloseButton.setComponentError(new UserError("Form contains invalid values")); saveAndStayOpenButton.setComponentError(new UserError("Form contains invalid values")); } else { saveAndCloseButton.setComponentError(null); saveAndStayOpenButton.setComponentError(null); } } }