package org.vaadin.viritin.v7.form;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.validation.groups.Default;
import org.vaadin.viritin.v7.BeanBinder;
import org.vaadin.viritin.v7.MBeanFieldGroup;
import org.vaadin.viritin.v7.MBeanFieldGroup.FieldGroupListener;
import org.vaadin.viritin.button.DeleteButton;
import org.vaadin.viritin.button.MButton;
import org.vaadin.viritin.button.PrimaryButton;
import org.vaadin.viritin.label.RichText;
import org.vaadin.viritin.layouts.MHorizontalLayout;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.AbstractComponentContainer;
import com.vaadin.v7.ui.AbstractField;
import com.vaadin.v7.ui.AbstractTextField;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomComponent;
import com.vaadin.v7.ui.Field;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.UI;
import com.vaadin.ui.Window;
import com.vaadin.ui.themes.ValoTheme;
import com.vaadin.util.ReflectTools;
/**
* Abstract super class for simple editor forms.
*
* See {@link #createContent()} for usage information.
*
*
* @see <a href="https://github.com/viritin/viritin/wiki/AbstractForm">The
* wiki</a>
*
* @param <T> the type of the bean edited
*/
public abstract class AbstractForm<T> extends CustomComponent implements
FieldGroupListener<T> {
private static final long serialVersionUID = -2368496151988753088L;
private String modalWindowTitle = "Edit entry";
private String saveCaption = "Save";
private String deleteCaption = "Delete";
private String cancelCaption = "Cancel";
public static class ValidityChangedEvent<T> extends Component.Event {
private static final long serialVersionUID = 7410354508832863756L;
private static final Method method = ReflectTools.findMethod(
ValidityChangedListener.class, "onValidityChanged",
ValidityChangedEvent.class);
public ValidityChangedEvent(Component source) {
super(source);
}
@Override
public AbstractForm<T> getComponent() {
return (AbstractForm<T>) super.getComponent();
}
}
public interface ValidityChangedListener<T> extends Serializable {
public void onValidityChanged(ValidityChangedEvent<T> event);
}
private Window popup;
public AbstractForm() {
addAttachListener(new AttachListener() {
private static final long serialVersionUID = 3193438171004932112L;
@Override
public void attach(AttachEvent event) {
lazyInit();
adjustResetButtonState();
}
});
}
protected void lazyInit() {
if (getCompositionRoot() == null) {
setCompositionRoot(createContent());
adjustSaveButtonState();
adjustResetButtonState();
}
}
private MBeanFieldGroup<T> fieldGroup;
/**
* The validity checked and cached on last change. Should be pretty much
* always up to date due to eager changes. At least after onFieldGroupChange
* call.
*/
boolean isValid = false;
private RichText beanLevelViolations;
@Override
public void onFieldGroupChange(MBeanFieldGroup<T> beanFieldGroup) {
boolean wasValid = isValid;
isValid = fieldGroup.isValid();
adjustSaveButtonState();
adjustResetButtonState();
if (wasValid != isValid) {
fireValidityChangedEvent();
}
updateConstraintViolationsDisplay();
}
protected void updateConstraintViolationsDisplay() {
if (beanLevelViolations != null) {
Collection<String> errorMessages = getFieldGroup().
getBeanLevelValidationErrors();
if (!errorMessages.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (String e : errorMessages) {
sb.append(e);
sb.append("<br/>");
}
beanLevelViolations.setValue(sb.toString());
beanLevelViolations.setVisible(true);
} else {
beanLevelViolations.setVisible(false);
beanLevelViolations.setValue("");
}
}
}
public Component getConstraintViolationsDisplay() {
if (beanLevelViolations == null) {
beanLevelViolations = new RichText();
beanLevelViolations.setVisible(false);
beanLevelViolations.setStyleName(ValoTheme.LABEL_FAILURE);
}
return beanLevelViolations;
}
public boolean isValid() {
return isValid;
}
protected void adjustSaveButtonState() {
if (isEagerValidation() && isBound()) {
boolean beanModified = fieldGroup.isBeanModified();
getSaveButton().setEnabled(beanModified && isValid());
}
}
protected boolean isBound() {
return fieldGroup != null;
}
protected void adjustResetButtonState() {
if (popup != null && popup.getParent() != null) {
// Assume cancel button in a form opened to a popup also closes
// it, allows closing via cancel button by default
getResetButton().setEnabled(true);
return;
}
if (isEagerValidation() && isBound()) {
boolean modified = fieldGroup.isBeanModified();
getResetButton().setEnabled(modified || popup != null);
}
}
public void addValidityChangedListener(ValidityChangedListener<T> listener) {
addListener(ValidityChangedEvent.class, listener,
ValidityChangedEvent.method);
}
public void removeValidityChangedListener(
ValidityChangedListener<T> listener) {
removeListener(ValidityChangedEvent.class, listener,
ValidityChangedEvent.method);
}
private void fireValidityChangedEvent() {
fireEvent(new ValidityChangedEvent<T>(this));
}
public interface SavedHandler<T> extends Serializable {
void onSave(T entity);
}
public interface ResetHandler<T> extends Serializable {
void onReset(T entity);
}
public interface DeleteHandler<T> extends Serializable {
void onDelete(T entity);
}
private T entity;
private SavedHandler<T> savedHandler;
private ResetHandler<T> resetHandler;
private DeleteHandler<T> deleteHandler;
private boolean eagerValidation = true;
public boolean isEagerValidation() {
return eagerValidation;
}
/**
* In case one is working with "detached entities" enabling eager validation
* will highly improve usability. The validity of the form will be updated
* on each changes and save/cancel buttons will reflect to the validity and
* possible changes.
*
* @param eagerValidation true if the form should have eager validation
*/
public void setEagerValidation(boolean eagerValidation) {
this.eagerValidation = eagerValidation;
}
/**
* Sets the object to be edited by this form. This method binds all fields
* from this form to given objects.
* <p>
* If your form needs to manually configure something based on the state of
* the edited object, you can override this method to do that either before
* the object is bound to fields or to do something after the bean binding.
*
* @param entity the object to be edited by this form
* @return the MBeanFieldGroup that is used to do the binding. Most often
* you don't need to do anything with it.
*/
public MBeanFieldGroup<T> setEntity(T entity) {
this.entity = entity;
lazyInit();
if (entity != null) {
if (isBound()) {
fieldGroup.unbind();
}
fieldGroup = bindEntity(entity);
try {
fieldGroup.setValidationGroups(getValidationGroups());
} catch (Throwable e) {
// Probably no Validation API available
}
for (Map.Entry<MBeanFieldGroup.MValidator<T>, Collection<AbstractComponent>> e : mValidators.
entrySet()) {
fieldGroup.addValidator(e.getKey(), e.getValue().toArray(
new AbstractComponent[e.getValue().size()]));
}
for (Map.Entry<Class<?>, AbstractComponent> e : validatorToErrorTarget.entrySet()) {
fieldGroup.setValidationErrorTarget(e.getKey(), e.getValue());
}
isValid = fieldGroup.isValid();
if (isEagerValidation()) {
fieldGroup.withEagerValidation(this);
adjustSaveButtonState();
adjustResetButtonState();
}
fieldGroup.hideInitialEmpyFieldValidationErrors();
setVisible(true);
return fieldGroup;
} else {
setVisible(false);
return null;
}
}
/**
* Creates a field group, configures the fields, binds the entity to those
* fields
*
* @param entity The entity to bind
* @return the fieldGroup created
*/
protected MBeanFieldGroup<T> bindEntity(T entity) {
return BeanBinder.bind(entity, this, getNestedProperties());
}
private String[] nestedProperties;
public String[] getNestedProperties() {
return nestedProperties;
}
public void setNestedProperties(String... nestedProperties) {
this.nestedProperties = nestedProperties;
}
/**
* Sets the given object to be a handler for saved,reset,deleted, based on
* what it happens to implement.
*
* @param handler the handler to be set as saved/reset/delete handler
*/
public void setHandler(Object handler) {
if (handler != null) {
if (handler instanceof SavedHandler) {
setSavedHandler((SavedHandler<T>) handler);
}
if (handler instanceof ResetHandler) {
setResetHandler((ResetHandler<T>) handler);
}
if (handler instanceof DeleteHandler) {
setDeleteHandler((DeleteHandler<T>) handler);
}
}
}
public void setSavedHandler(SavedHandler<T> savedHandler) {
this.savedHandler = savedHandler;
getSaveButton().setVisible(this.savedHandler != null);
}
public void setResetHandler(ResetHandler<T> resetHandler) {
this.resetHandler = resetHandler;
getResetButton().setVisible(this.resetHandler != null);
}
public void setDeleteHandler(DeleteHandler<T> deleteHandler) {
this.deleteHandler = deleteHandler;
getDeleteButton().setVisible(this.deleteHandler != null);
}
public ResetHandler<T> getResetHandler() {
return resetHandler;
}
public SavedHandler<T> getSavedHandler() {
return savedHandler;
}
public DeleteHandler<T> getDeleteHandler() {
return deleteHandler;
}
public Window openInModalPopup() {
popup = new Window(getModalWindowTitle(), this);
popup.setModal(true);
UI.getCurrent().addWindow(popup);
focusFirst();
return popup;
}
/**
*
* @return the last Popup into which the Form was opened with
* #openInModalPopup method or null if the form hasn't been use in window
*/
public Window getPopup() {
return popup;
}
/**
* If the form is opened into a popup window using openInModalPopup(), you
* you can use this method to close the popup.
*/
public void closePopup() {
if (popup != null) {
popup.close();
popup = null;
}
}
/**
* @return A default toolbar containing save/cancel/delete buttons
*/
public HorizontalLayout getToolbar() {
return new MHorizontalLayout(
getSaveButton(),
getResetButton(),
getDeleteButton()
);
}
protected Button createCancelButton() {
return new MButton(getCancelCaption())
.withVisible(false);
}
private Button resetButton;
public Button getResetButton() {
if (resetButton == null) {
setResetButton(createCancelButton());
}
return resetButton;
}
public void setResetButton(Button resetButton) {
this.resetButton = resetButton;
this.resetButton.addClickListener(new Button.ClickListener() {
private static final long serialVersionUID = -19755976436277487L;
@Override
public void buttonClick(Button.ClickEvent event) {
reset(event);
}
});
}
protected Button createSaveButton() {
return new PrimaryButton(getSaveCaption())
.withVisible(false);
}
private Button saveButton;
public void setSaveButton(Button saveButton) {
this.saveButton = saveButton;
saveButton.addClickListener(new Button.ClickListener() {
private static final long serialVersionUID = -2058398434893034442L;
@Override
public void buttonClick(Button.ClickEvent event) {
save(event);
}
});
}
public Button getSaveButton() {
if (saveButton == null) {
setSaveButton(createSaveButton());
}
return saveButton;
}
protected Button createDeleteButton() {
return new DeleteButton(getDeleteCaption())
.withVisible(false);
}
private Button deleteButton;
public void setDeleteButton(final Button deleteButton) {
this.deleteButton = deleteButton;
deleteButton.addClickListener(new Button.ClickListener() {
private static final long serialVersionUID = -2693734056915561664L;
@Override
public void buttonClick(Button.ClickEvent event) {
delete(event);
}
});
}
public Button getDeleteButton() {
if (deleteButton == null) {
setDeleteButton(createDeleteButton());
}
return deleteButton;
}
protected void save(Button.ClickEvent e) {
savedHandler.onSave(getEntity());
getFieldGroup().setBeanModified(false);
adjustResetButtonState();
adjustSaveButtonState();
}
protected void reset(Button.ClickEvent e) {
resetHandler.onReset(getEntity());
getFieldGroup().setBeanModified(false);
adjustResetButtonState();
adjustSaveButtonState();
}
protected void delete(Button.ClickEvent e) {
deleteHandler.onDelete(getEntity());
}
/**
* Focuses the first field found from the form. It often improves UX to call
* this method, or focus another field, when you assign a bean for editing.
*/
public void focusFirst() {
Component compositionRoot = getCompositionRoot();
findFieldAndFocus(compositionRoot);
}
private boolean findFieldAndFocus(Component compositionRoot) {
if (compositionRoot instanceof AbstractComponentContainer) {
AbstractComponentContainer cc = (AbstractComponentContainer) compositionRoot;
for (Component component : cc) {
if (component instanceof AbstractTextField) {
AbstractTextField abstractTextField = (AbstractTextField) component;
abstractTextField.selectAll();
return true;
}
if (component instanceof AbstractField) {
AbstractField abstractField = (AbstractField) component;
abstractField.focus();
return true;
}
if (component instanceof AbstractComponentContainer) {
if (findFieldAndFocus(component)) {
return true;
}
}
}
}
return false;
}
/**
* This method should return the actual content of the form, including
* possible toolbar.
*
* Use setEntity(T entity) to fill in the data. Am example implementation
* could look like this:
* <pre><code>
* public class PersonForm extends AbstractForm<Person> {
*
* private TextField firstName = new MTextField("First Name");
* private TextField lastName = new MTextField("Last Name");
*
* {@literal @}Override
* protected Component createContent() {
* return new MVerticalLayout(
* new FormLayout(
* firstName,
* lastName
* ),
* getToolbar()
* );
* }
* }
* </code></pre>
*
* @return the content of the form
*
*/
protected abstract Component createContent();
public MBeanFieldGroup<T> getFieldGroup() {
return fieldGroup;
}
public T getEntity() {
return entity;
}
private final LinkedHashMap<MBeanFieldGroup.MValidator<T>, Collection<AbstractComponent>> mValidators
= new LinkedHashMap<>();
private final Map<Class<?>, AbstractComponent> validatorToErrorTarget = new LinkedHashMap<>();
private Class<?>[] validationGroups;
/**
* @return the JSR 303 bean validation groups that should be used to
* validate the bean
*/
public Class<?>[] getValidationGroups() {
if (validationGroups == null) {
return new Class<?>[]{Default.class};
}
return validationGroups;
}
/**
* @param validationGroups the JSR 303 bean validation groups that should be
* used to validate the bean. Note, that groups currently only affect
* cross-field/bean-level validation.
*/
public void setValidationGroups(Class<?>... validationGroups) {
this.validationGroups = validationGroups;
if (getFieldGroup() != null) {
getFieldGroup().setValidationGroups(validationGroups);
}
}
public void setValidationErrorTarget(Class<?> aClass,
AbstractComponent errorTarget) {
validatorToErrorTarget.put(aClass, errorTarget);
if (getFieldGroup() != null) {
getFieldGroup().setValidationErrorTarget(aClass, errorTarget);
}
}
/**
* EXPERIMENTAL: The cross field validation support is still experimental
* and its API is likely to change.
*
* @param validator a validator that validates the whole bean making cross
* field validation much simpler
* @param fields the ui fields that this validator affects and on which a
* possible error message is shown.
* @return this FieldGroup
*/
public AbstractForm<T> addValidator(
MBeanFieldGroup.MValidator<T> validator,
AbstractComponent... fields) {
mValidators.put(validator, Arrays.asList(fields));
if (getFieldGroup() != null) {
getFieldGroup().addValidator(validator, fields);
}
return this;
}
public AbstractForm<T> removeValidator(
MBeanFieldGroup.MValidator<T> validator) {
Collection<AbstractComponent> remove = mValidators.remove(validator);
if (remove != null) {
if (getFieldGroup() != null) {
getFieldGroup().removeValidator(validator);
}
}
return this;
}
/**
* Removes all MValidators added the MFieldGroup
*
* @return the instance
*/
public AbstractForm<T> clearValidators() {
mValidators.clear();
if (getFieldGroup() != null) {
getFieldGroup().clearValidators();
}
return this;
}
public void setRequired(Field... fields) {
for (Field field : fields) {
field.setRequired(true);
}
}
public String getModalWindowTitle() {
return modalWindowTitle;
}
public void setModalWindowTitle(String modalWindowTitle) {
this.modalWindowTitle = modalWindowTitle;
}
public String getCancelCaption() {
return cancelCaption;
}
public void setCancelCaption(String cancelCaption) {
this.cancelCaption = cancelCaption;
}
public String getSaveCaption() {
return saveCaption;
}
public void setSaveCaption(String saveCaption) {
this.saveCaption = saveCaption;
}
public String getDeleteCaption() {
return deleteCaption;
}
public void setDeleteCaption(String deleteCaption) {
this.deleteCaption = deleteCaption;
}
public AbstractForm<T> withI18NCaption(String saveCaption, String deleteCaption, String cancelCaption) {
this.saveCaption = saveCaption;
this.deleteCaption = deleteCaption;
this.cancelCaption = cancelCaption;
return this;
}
public boolean isValidateOnlyBoundFields() {
return fieldGroup.isValidateOnlyBoundFields();
}
/**
* Tells that only bound fields from the bean (bound entity) should be validated.
* Useful when the form does not contain all bean properties or, on the other hand, is not valid until all properties are valid.
* By default, only bound bean properties are validated.
* If set to false, all bean properties will be validated.
*
* @param validateOnlyBoundFields true if only bound fields should be validated
*/
public void setValidateOnlyBoundFields(boolean validateOnlyBoundFields) {
fieldGroup.setValidateOnlyBoundFields(validateOnlyBoundFields);
}
}