package edu.ualberta.med.biobank.mvp.model; import java.util.ArrayList; import java.util.List; import com.google.gwt.event.logical.shared.HasValueChangeHandlers; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.pietschy.gwt.pectin.client.condition.Condition; import com.pietschy.gwt.pectin.client.condition.Conditions; import com.pietschy.gwt.pectin.client.condition.DelegatingCondition; import com.pietschy.gwt.pectin.client.condition.OrFunction; import com.pietschy.gwt.pectin.client.condition.ReducingCondition; import com.pietschy.gwt.pectin.client.form.Field; import com.pietschy.gwt.pectin.client.form.FieldModel; import com.pietschy.gwt.pectin.client.form.FormModel; import com.pietschy.gwt.pectin.client.form.FormattedFieldModel; import com.pietschy.gwt.pectin.client.form.FormattedListFieldModel; import com.pietschy.gwt.pectin.client.form.ListFieldModel; import com.pietschy.gwt.pectin.client.form.binding.FormBinder; import com.pietschy.gwt.pectin.client.form.validation.FormValidator; import com.pietschy.gwt.pectin.client.form.validation.HasValidation; import com.pietschy.gwt.pectin.client.form.validation.Severity; import com.pietschy.gwt.pectin.client.form.validation.ValidationEvent; import com.pietschy.gwt.pectin.client.form.validation.ValidationHandler; import com.pietschy.gwt.pectin.client.form.validation.ValidationPlugin; import com.pietschy.gwt.pectin.client.form.validation.ValidationResult; import com.pietschy.gwt.pectin.client.form.validation.binding.ValidationBinder; import com.pietschy.gwt.pectin.client.form.validation.component.ValidationDisplay; import com.pietschy.gwt.pectin.client.value.MutableValueModel; import com.pietschy.gwt.pectin.client.value.ValueHolder; import com.pietschy.gwt.pectin.client.value.ValueModel; import com.pietschy.gwt.pectin.reflect.ReflectionBeanModelProvider; import edu.ualberta.med.biobank.mvp.model.validation.ValidationTree; import edu.ualberta.med.biobank.mvp.util.HandlerRegistry; /** * For use by {@link edu.ualberta.med.biobank.mvp.presenter.IPresenter}-s. * {@link AbstractModel}-s should only have knowledge of other models and only * supply validation. It is the presenter's responsibility to call * {@link AbstractModel#bind()} and {@link AbstractModel#unbind()} on a model * and to bind the model's attributes to the view, and bind the validation to * the view. * <p> * However, note that when a model's {@link FieldModel}-s are bound to a widget/ * component that implements {@link ValidationDisplay}, then the widget will be * automatically notified of validation via {@link ValidationResult}. * <p> * Validation rules should be put in the constructor. * * @author jferland * * @param <T> */ public abstract class AbstractModel<T> extends FormModel { protected final ReflectionBeanModelProvider<T> provider; private final FormBinder binder = new FormBinder(); private final ValidationBinder validationBinder = new ValidationBinder(); private final ValidationTree validationTree = new ValidationTree(); private final DelegatingCondition dirty = new DelegatingCondition(false); private final ValueHolder<Boolean> valid = new ValueHolder<Boolean>(false); private final List<AbstractModel<?>> models = new ArrayList<AbstractModel<?>>(); private final HandlerRegistry handlerRegistry = new HandlerRegistry(); private final ValidationMonitor validationMonitor = new ValidationMonitor(); private boolean bound = false; @SuppressWarnings("unchecked") private final Condition validAndDirty = Conditions.and(valid, dirty); public AbstractModel(Class<T> beanModelClass) { // TODO: could read the .class from the generic parameter? // TODO: should get this from an injected provider in the future if ever // go the GWT-way since it this specific implementation won't work with // GWT provider = new ReflectionBeanModelProvider<T>(beanModelClass); // auto-commit so models can be bound to other models and are // automatically updated instantly provider.setAutoCommit(true); validationTree.add(getFormValidator()); validationTree.addValidationHandler(validationMonitor); } public T getValue() { return provider.getValue(); } public void setValue(T value) { provider.setValue(value); } /** * Creates a new checkpoint for {@link AbstractModel#revert()} to revert to, * if called, and clears the dirty state (including that of added inner * {@link AbstractModel}-s). */ public void checkpoint() { provider.checkpoint(); for (AbstractModel<?> model : models) { model.checkpoint(); } } public ValueModel<Boolean> dirty() { return dirty; } public ValueModel<Boolean> valid() { return valid; } public ValueModel<Boolean> validAndDirty() { return validAndDirty; } public void bindValidationTo(ValidationDisplay validationDisplay) { validationBinder.bindValidationOf(validationTree).to(validationDisplay); } /** * Reverts this {@link AbstractModel} to the value it was originally * provided with (via {@link AbstractModel#setValue(Object)}) or the value * when {@link AbstractModel#checkpoint()} was last called. */ public void revert() { provider.revert(); // revert inner models before checkpoint()-ing, otherwise the inner // models will revert to the checkpoint we just made (and not their // "original" value, which would have been overwritten by the // checkpoint). for (AbstractModel<?> model : models) { model.revert(); } checkpoint(); } public boolean validate() { return validationTree.validate(); } /** * Binds a field to a model. The {@link FieldModel} <em>must</em> belong to * this {@link FormModel}. * <p> * Adds a {@link AbstractModel} to this {@link AbstractModel}, so that the * former is reverted, checkpoint-ed, validated, bound, unbound, * dirty-checked, etc. whenever this {@link AbstractModel} is. * * @param field bound to the model. * @param model bound to the field. * @param binder used to bind the model and field. */ public <E> void bind(FieldModel<E> field, AbstractModel<E> model) { if (!field.getFormModel().equals(this)) { throw new IllegalArgumentException("field is not from this model"); } models.add(model); binder.bind(field).to(model.getMutableValueModel()); validationTree.add(model.validationTree); } public MutableValueModel<T> getMutableValueModel() { return provider; } public void bind() { if (!bound) { // bind inner models before binding ourself bindModels(); onBind(); // TODO: add checkpoint and dirty watchers for all values. addValidationHandlers(); updateDirtyDelegate(); validate(); bound = true; } } public void unbind() { if (bound) { bound = false; unbindModels(); onUnbind(); dirty.setDelegate(provider.dirty()); handlerRegistry.dispose(); binder.dispose(); validationBinder.dispose(); validationTree.dispose(); } } public abstract void onBind(); public abstract void onUnbind(); protected FormValidator getFormValidator() { return ValidationPlugin.getValidationManager(this).getFormValidator(); } private void addValidationHandlers() { for (Field<?> field : allFields()) { addValidationHandler(field); } } private <E> void addValidationHandler(final Field<E> field) { final HasValidation validator = getValidator(field); if (validator != null && field instanceof HasValueChangeHandlers) { @SuppressWarnings("unchecked") HandlerRegistration handlerRegistration = ((HasValueChangeHandlers<E>) field) .addValueChangeHandler(new ValueChangeHandler<E>() { @Override public void onValueChange(ValueChangeEvent<E> event) { validator.validate(); } }); handlerRegistry.add(handlerRegistration); // TODO: listen to conditions of validation, then re-validate(). // But how? } } private HasValidation getValidator(Field<?> field) { FormValidator formValidator = getFormValidator(); // unfortunately, FormValidator.getValidator() will create the validator // if it doesn't exist, but we only want to get one if it exists if (field instanceof FieldModel) { return formValidator.getFieldValidator( (FieldModel<?>) field, false); } else if (field instanceof FormattedFieldModel) { return formValidator.getFieldValidator( (FormattedFieldModel<?>) field, false); } else if (field instanceof ListFieldModel) { return formValidator.getFieldValidator( (ListFieldModel<?>) field, false); } else if (field instanceof FormattedListFieldModel) { return formValidator.getFieldValidator( (FormattedListFieldModel<?>) field, false); } return null; } /** * Make this {@link AbstractModel}'s dirty value depend on the inner * {@link AbstractModel}-s, if there are any. */ private void updateDirtyDelegate() { List<ValueModel<Boolean>> values = new ArrayList<ValueModel<Boolean>>(); values.add(provider.dirty()); for (AbstractModel<?> model : models) { values.add(model.dirty()); } dirty.setDelegate(new ReducingCondition(new OrFunction(), values)); } private void bindModels() { for (AbstractModel<?> model : models) { model.bind(); } } private void unbindModels() { for (AbstractModel<?> model : models) { model.unbind(); } } private void updateValidity(ValidationResult result) { boolean isValid = !result.contains(Severity.ERROR); valid.setValue(isValid); } private class ValidationMonitor implements ValidationHandler { @Override public void onValidate(ValidationEvent event) { updateValidity(event.getValidationResult()); } } }