/** * Copyright (C) 2015 Valkyrie RCP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.valkyriercp.form; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.valkyriercp.application.config.ApplicationConfig; import org.valkyriercp.binding.form.CommitListener; import org.valkyriercp.binding.form.FormModel; import org.valkyriercp.binding.form.HierarchicalFormModel; import org.valkyriercp.binding.form.ValidatingFormModel; import org.valkyriercp.binding.validation.ValidationListener; import org.valkyriercp.binding.value.IndexAdapter; import org.valkyriercp.binding.value.ObservableList; import org.valkyriercp.binding.value.ValueModel; import org.valkyriercp.command.support.ActionCommand; import org.valkyriercp.core.Guarded; import org.valkyriercp.core.Messagable; import org.valkyriercp.core.Secured; import org.valkyriercp.factory.AbstractControlFactory; import org.valkyriercp.form.binding.BindingFactory; import org.valkyriercp.util.ValkyrieRepository; import javax.annotation.PostConstruct; import javax.swing.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Base implementation of a Form. * * Commands provided: * <ul> * <li><em>CommitCommand</em>: wraps the {@link FormModel#commit()} method. * Writes data to backing bean. Guarded mask {@link FormGuard#ON_NOERRORS}, * {@link FormGuard#ON_ISDIRTY} and {@link FormGuard#ON_ENABLED}.</li> * <li><em>RevertCommand</em>: wraps the {@link FormModel#revert()} method. * Fall back to the values of the backing bean. Guarded mask * {@link FormGuard#ON_ISDIRTY} and {@link FormGuard#ON_ENABLED}.</li> * <li><em>NewFormObjectCommand</em>: set a fresh instance on the * {@link FormModel}. Guarded mask {@link FormGuard#ON_ENABLED}</li> * </ul> * * All commands provide a securityControllerId. * * @author Keith Donald */ public abstract class AbstractForm extends AbstractControlFactory implements Form, CommitListener, Secured { private Logger logger = LoggerFactory.getLogger(getClass()); private final FormObjectChangeHandler formObjectChangeHandler = new FormObjectChangeHandler(); private String formId; private ValidatingFormModel formModel; private HierarchicalFormModel parentFormModel; private FormGuard formGuard; private JButton lastDefaultButton; private PropertyChangeListener formEnabledChangeHandler; private ActionCommand newFormObjectCommand; private ActionCommand commitCommand; private ActionCommand revertCommand; private boolean editingNewFormObject; private boolean clearFormOnCommit = false; private ObservableList editableFormObjects; private ValueModel editingFormObjectIndexHolder; private PropertyChangeListener editingFormObjectSetter; private BindingFactory bindingFactory; private Map childForms = new HashMap(); private List validationResultsReporters = new ArrayList(); public abstract FormModel createFormModel(); protected AbstractForm(String id) { this.formId = id; init(); } protected AbstractForm() { init(); } /** * Hook called when constructing the Form. */ protected void init() { FormModel model = createFormModel(); if(getId() == null) formId = model.getId(); if (model instanceof ValidatingFormModel) { ValidatingFormModel validatingFormModel = (ValidatingFormModel) model; setFormModel(validatingFormModel); } else { throw new IllegalArgumentException("Unsupported form model implementation " + formModel); } getApplicationConfig().applicationObjectConfigurer().configure(this, getId()); } public String getId() { return formId; } public ValidatingFormModel getFormModel() { return formModel; } /** * Returns a {@link BindingFactory} bound to the inner {@link FormModel} to * provide binding support. */ public BindingFactory getBindingFactory() { if (bindingFactory == null) { bindingFactory = getApplicationConfig().bindingFactoryProvider().getBindingFactory(formModel); } return bindingFactory; } /** * Set the {@link FormModel} for this {@link Form}. Normally a Form won't * change it's FormModel as this may lead to an inconsistent state. Only use * this when the formModel isn't set yet. * * TODO check why we do allow setting when no control is created. * ValueModels might exist already leading to an inconsistent state. * * @param formModel */ protected void setFormModel(ValidatingFormModel formModel) { Assert.notNull(formModel); if (this.formModel != null && isControlCreated()) { throw new UnsupportedOperationException("Cannot reset form model once form control has been created"); } if (this.formModel != null) { this.formModel.removeCommitListener(this); } this.formModel = formModel; this.formGuard = new FormGuard(formModel); this.formModel.addCommitListener(this); setFormModelDefaultEnabledState(); } /** * Returns the parent of this Form's FormModel or <code>null</code>. */ protected HierarchicalFormModel getParent() { return this.parentFormModel; } /** * Add a child (or sub) form to this form. Child forms will be tied in to * the same validation results reporter as this form and they will be * configured to control the same guarded object as this form. * <p> * Validation listeners are unique to a form, so calling * {@link #addValidationListener(ValidationListener)} will only add a * listener to this form. If you want to listen to the child forms, you will * need to add a validation listener on each child form of interest. * <p> * <em>Note:</em> It is very important that the child form provided be * created using a form model that is a child model of this form's form * model. If this is not done, then commit and revert operations will not be * properly delegated to the child form models. * * @param childForm to add */ public void addChildForm(Form childForm) { childForms.put(childForm.getId(), childForm); getFormModel().addChild(childForm.getFormModel()); } /** * @inheritDoc */ public List getValidationResultsReporters() { return validationResultsReporters; } /** * @inheritDoc */ public void addValidationResultsReporter(ValidationResultsReporter reporter) { this.validationResultsReporters.add(reporter); } /** * @inheritDoc */ public void removeValidationResultsReporter(ValidationResultsReporter reporter) { this.validationResultsReporters.remove(reporter); } /** * @inheritDoc */ public void removeChildForm(Form childForm) { getFormModel().removeChild(childForm.getFormModel()); childForms.remove(childForm.getId()); } /** * Return a child form of this form with the given form id. * @param id of child form * @return child form, null if no child form with the given id has been * registered */ protected Form getChildForm(String id) { return (Form) childForms.get(id); } protected void setEditableFormObjects(ObservableList editableFormObjects) { this.editableFormObjects = editableFormObjects; } protected void setEditingFormObjectIndexHolder(ValueModel valueModel) { this.editingFormObjectIndexHolder = valueModel; this.editingFormObjectSetter = new EditingFormObjectSetter(); this.editingFormObjectIndexHolder.addValueChangeListener(editingFormObjectSetter); } public boolean isEditingNewFormObject() { return editingNewFormObject; } /** * Set the "editing new form object" state as indicated. * @param editingNewFormOject */ protected void setEditingNewFormObject(boolean editingNewFormOject) { this.editingNewFormObject = editingNewFormOject; } private class EditingFormObjectSetter implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { int selectionIndex = getEditingFormObjectIndex(); if (selectionIndex == -1) { // FIXME: why do we need this // getFormModel().reset(); setEnabled(false); } else { if (selectionIndex < editableFormObjects.size()) { // If we were editing a "new" object, we need to clear // that flag since a new object has been selected setEditingNewFormObject(false); setFormObject(getEditableFormObject(selectionIndex)); setEnabled(true); } } } } protected int getEditingFormObjectIndex() { return ((Integer) editingFormObjectIndexHolder.getValue()).intValue(); } protected Object getEditableFormObject(int selectionIndex) { return editableFormObjects.get(selectionIndex); } public void setClearFormOnCommit(boolean clearFormOnCommit) { this.clearFormOnCommit = clearFormOnCommit; } protected JButton getDefaultButton() { if (isControlCreated()) { JRootPane rootPane = SwingUtilities.getRootPane(getControl()); return rootPane == null ? null : rootPane.getDefaultButton(); } return null; } protected void setDefaultButton(JButton button) { JRootPane rootPane = SwingUtilities.getRootPane(getControl()); if (rootPane != null) { rootPane.setDefaultButton(button); } } protected final JComponent createControl() { Assert .state(getFormModel() != null, "This form's FormModel cannot be null once control creation is triggered!"); initStandardLocalFormCommands(); JComponent formControl = createFormControl(); this.formEnabledChangeHandler = new FormEnabledPropertyChangeHandler(); getFormModel().addPropertyChangeListener(FormModel.ENABLED_PROPERTY, formEnabledChangeHandler); addFormObjectChangeListener(formObjectChangeHandler); if (getCommitCommand() != null) { getFormModel().addCommitListener(this); } return formControl; } private void initStandardLocalFormCommands() { getNewFormObjectCommand(); getCommitCommand(); getRevertCommand(); } /** * Set the form's enabled state based on a default policy--specifically, * disable if the form object is null or the form object is guarded and is * marked as disabled. */ protected void setFormModelDefaultEnabledState() { if (getFormObject() == null) { getFormModel().setEnabled(false); } else { if (getFormObject() instanceof Guarded) { setEnabled(((Guarded) getFormObject()).isEnabled()); } } } protected abstract JComponent createFormControl(); private class FormObjectChangeHandler implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { setFormModelDefaultEnabledState(); } } private class FormEnabledPropertyChangeHandler implements PropertyChangeListener { public FormEnabledPropertyChangeHandler() { handleEnabledChange(getFormModel().isEnabled()); } public void propertyChange(PropertyChangeEvent evt) { handleEnabledChange(getFormModel().isEnabled()); } } protected void handleEnabledChange(boolean enabled) { if (enabled) { if (getCommitCommand() != null) { if (lastDefaultButton == null) { lastDefaultButton = getDefaultButton(); } getCommitCommand().setDefaultButton(); } } else { if (getCommitCommand() != null) { getCommitCommand().setEnabled(false); } // set previous default button if (lastDefaultButton != null) { setDefaultButton(lastDefaultButton); } } } public ActionCommand getNewFormObjectCommand() { if (this.newFormObjectCommand == null) { this.newFormObjectCommand = createNewFormObjectCommand(); } return newFormObjectCommand; } public ActionCommand getCommitCommand() { if (this.commitCommand == null) { this.commitCommand = createCommitCommand(); } return commitCommand; } public ActionCommand getRevertCommand() { if (this.revertCommand == null) { this.revertCommand = createRevertCommand(); } return revertCommand; } private ActionCommand createNewFormObjectCommand() { String commandId = getNewFormObjectCommandId(); if (!StringUtils.hasText(commandId)) { return null; } ActionCommand newFormObjectCmd = new ActionCommand(commandId) { protected void doExecuteCommand() { getFormModel().setFormObject(createNewObject()); getFormModel().setEnabled(true); editingNewFormObject = true; if (isEditingFormObjectSelected()) { setEditingFormObjectIndexSilently(-1); } } }; newFormObjectCmd.setSecurityControllerId(getNewFormObjectSecurityControllerId()); attachFormGuard(newFormObjectCmd, FormGuard.LIKE_NEWFORMOBJCOMMAND); return (ActionCommand) getApplicationConfig().commandConfigurer().configure(newFormObjectCmd); } /** * Create a new object to install into the form. By default, this simply * returns null. This will cause the form model to instantiate a new copy of * the model object class. Subclasses should override this method if they * need more control over how new objects are constructed. * * @return new object for editing */ protected Object createNewObject() { return null; } private boolean isEditingFormObjectSelected() { if (editingFormObjectIndexHolder == null) { return false; } int value = ((Integer) editingFormObjectIndexHolder.getValue()).intValue(); return value != -1; } protected void setEditingFormObjectIndexSilently(int index) { editingFormObjectIndexHolder.removeValueChangeListener(editingFormObjectSetter); editingFormObjectIndexHolder.setValue(new Integer(index)); editingFormObjectIndexHolder.addValueChangeListener(editingFormObjectSetter); } /** * Returns a command wrapping the commit behavior of the {@link FormModel}. * This command has the guarded and security aspects. */ private final ActionCommand createCommitCommand() { String commandId = getCommitCommandFaceDescriptorId(); if (!StringUtils.hasText(commandId)) { return null; } ActionCommand commitCmd = new ActionCommand(commandId) { protected void doExecuteCommand() { commit(); } }; commitCmd.setSecurityControllerId(getCommitSecurityControllerId()); attachFormGuard(commitCmd, FormGuard.LIKE_COMMITCOMMAND); return (ActionCommand) getApplicationConfig().commandConfigurer().configure(commitCmd); } public void preCommit(FormModel formModel) { } public void postCommit(FormModel formModel) { if (editableFormObjects != null) { if (editingNewFormObject) { editableFormObjects.add(formModel.getFormObject()); setEditingFormObjectIndexSilently(editableFormObjects.size() - 1); } else { int index = getEditingFormObjectIndex(); // Avoid updating unless we have actually selected an object for // edit if (index >= 0) { IndexAdapter adapter = editableFormObjects.getIndexAdapter(index); adapter.setValue(formModel.getFormObject()); adapter.fireIndexedObjectChanged(); } } } if (clearFormOnCommit) { setFormObject(null); } editingNewFormObject = false; } private final ActionCommand createRevertCommand() { String commandId = getRevertCommandFaceDescriptorId(); if (!StringUtils.hasText(commandId)) { return null; } ActionCommand revertCmd = new ActionCommand(commandId) { protected void doExecuteCommand() { revert(); } }; attachFormGuard(revertCmd, FormGuard.LIKE_REVERTCOMMAND); return (ActionCommand) getApplicationConfig().commandConfigurer().configure(revertCmd); } protected final JButton createNewFormObjectButton() { Assert.state(newFormObjectCommand != null, "New form object command has not been created!"); return (JButton) newFormObjectCommand.createButton(); } protected final JButton createCommitButton() { Assert.state(commitCommand != null, "Commit command has not been created!"); return (JButton) commitCommand.createButton(); } protected String getNewFormObjectCommandId() { return "new" + StringUtils .capitalize(ClassUtils.getShortName(getFormModel().getFormObject().getClass() + "Command")); } protected String getCommitCommandFaceDescriptorId() { return null; } protected String getRevertCommandFaceDescriptorId() { return null; } /** * Subclasses may override to return a security controller id to be attached * to the newFormObject command. The default is * <code>[formModel.id] + "." + [getNewFormObjectCommandId()]</code>. * <p> * This id can be mapped to a specific security controller using the * SecurityControllerManager service. * * @return security controller id, may be null if the face id is null */ protected String getNewFormObjectSecurityControllerId() { return constructSecurityControllerId(getNewFormObjectCommandId()); } /** * Subclasses may override to return a security controller id to be attached * to the commit command. The default is The default is * <code>[formModel.id] + "." + [getCommitCommandFaceDescriptorId()]</code>. * <p> * This id can be mapped to a specific security controller using the * SecurityControllerManager service. * * @return security controller id, may be null if the face id is null */ protected String getCommitSecurityControllerId() { return constructSecurityControllerId(getCommitCommandFaceDescriptorId()); } /** * Construct a default security controller Id for a given command face id. * The id will be a combination of the form model id, if any, and the face * id. * <p> * <code>[formModel.id] + "." + [commandFaceId]</code> if the form model * id is not null. * <p> * <code>[commandFaceId]</code> if the form model is null. * <p> * <code>null</code> if the commandFaceId is null. * @param commandFaceId * @return default security controller id */ protected String constructSecurityControllerId(String commandFaceId) { String id = null; String formModelId = getFormModel().getId(); if (commandFaceId != null) { id = (formModelId != null) ? formModelId + "." + commandFaceId : commandFaceId; } return id; } protected void attachFormErrorGuard(Guarded guarded) { attachFormGuard(guarded, FormGuard.FORMERROR_GUARDED); } protected void attachFormGuard(Guarded guarded, int mask) { this.formGuard.addGuarded(guarded, mask); } protected void detachFormGuard(Guarded guarded) { this.formGuard.removeGuarded(guarded); } public Object getFormObject() { return formModel.getFormObject(); } public void setFormObject(Object formObject) { formModel.setFormObject(formObject); } public Object getValue(String formProperty) { return formModel.getValueModel(formProperty).getValue(); } public ValueModel getValueModel(String formProperty) { ValueModel valueModel = formModel.getValueModel(formProperty); if (valueModel == null) { logger.warn("A value model for property '" + formProperty + "' could not be found. Typo?"); } return valueModel; } public <T> ValueModel<T> getValueModel(String formProperty, Class<T> expectedType) { ValueModel<T> valueModel = formModel.getValueModel(formProperty, expectedType); if (valueModel == null) { logger.warn("A value model for property '" + formProperty + "' could not be found. Typo?"); } return valueModel; } public boolean isEnabled() { return this.formModel.isEnabled(); } public void setEnabled(boolean enabled) { this.formModel.setEnabled(enabled); } public void addValidationListener(ValidationListener listener) { formModel.getValidationResults().addValidationListener(listener); } public void removeValidationListener(ValidationListener listener) { formModel.getValidationResults().removeValidationListener(listener); } /** * Construct the validation results reporter for this form and attach it to * the provided Guarded object. An instance of * {@link SimpleValidationResultsReporter} will be constructed and returned. * All registered child forms will be attached to the same * <code>guarded</code> and <code>messageReceiver</code> as this form. */ public ValidationResultsReporter newSingleLineResultsReporter(Messagable messageReceiver) { SimpleValidationResultsReporter reporter = new SimpleValidationResultsReporter( formModel.getValidationResults(), messageReceiver); return reporter; } public void addFormObjectChangeListener(PropertyChangeListener listener) { formModel.getFormObjectHolder().addValueChangeListener(listener); } public void removeFormObjectChangeListener(PropertyChangeListener listener) { formModel.getFormObjectHolder().removeValueChangeListener(listener); } public void addFormValueChangeListener(String formPropertyPath, PropertyChangeListener listener) { getFormModel().getValueModel(formPropertyPath).addValueChangeListener(listener); } public void removeFormValueChangeListener(String formPropertyPath, PropertyChangeListener listener) { getFormModel().getValueModel(formPropertyPath).removeValueChangeListener(listener); } public boolean isDirty() { return formModel.isDirty(); } public boolean hasErrors() { return formModel.getValidationResults().getHasErrors(); } public void commit() { formModel.commit(); } public void revert() { formModel.revert(); } public void reset() { getFormModel().reset(); } public void addGuarded(Guarded guarded) { formGuard.addGuarded(guarded, FormGuard.FORMERROR_GUARDED); } public void addGuarded(Guarded guarded, int mask) { formGuard.addGuarded(guarded, mask); } public void removeGuarded(Guarded guarded) { formGuard.removeGuarded(guarded); } protected ApplicationConfig getApplicationConfig() { return ValkyrieRepository.getInstance().getApplicationConfig(); } protected String getMessage(String key, Object... args) { return getApplicationConfig().messageResolver().getMessage(key, args); } private String securityControllerId; private String[] authorities; public String[] getAuthorities() { return authorities; } public void setAuthorities(String... authorities) { this.authorities = authorities; } public String getSecurityControllerId() { if(securityControllerId == null) return getId(); return securityControllerId; } public void setSecurityControllerId(String securityControllerId) { this.securityControllerId = securityControllerId; } private boolean authorized; public boolean isAuthorized() { return authorized; } public void setAuthorized(boolean authorized) { this.authorized = authorized; getFormModel().setReadOnly(getFormModel().isReadOnly() || !authorized); } }