/**
* 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.binding.form.support;
import org.springframework.beans.PropertyAccessException;
import org.springframework.binding.convert.ConversionException;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.valkyriercp.binding.MutablePropertyAccessStrategy;
import org.valkyriercp.binding.form.BindingErrorMessageProvider;
import org.valkyriercp.binding.form.HierarchicalFormModel;
import org.valkyriercp.binding.form.ValidatingFormModel;
import org.valkyriercp.binding.validation.RichValidator;
import org.valkyriercp.binding.validation.ValidationMessage;
import org.valkyriercp.binding.validation.ValidationResultsModel;
import org.valkyriercp.binding.validation.Validator;
import org.valkyriercp.binding.validation.support.DefaultValidationResults;
import org.valkyriercp.binding.validation.support.DefaultValidationResultsModel;
import org.valkyriercp.binding.validation.support.RulesValidator;
import org.valkyriercp.binding.value.ValueModel;
import org.valkyriercp.binding.value.support.AbstractValueModelWrapper;
import org.valkyriercp.util.ValkyrieRepository;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Map;
/**
* Default form model implementation. Is configurable, hierarchical and
* validating.
* <p>
* If you need this form model to use validation rules that are specific to a
* given context (such as a specific form), then you will need to call
* {@link #setValidator(Validator)} with a validator configured with the
* required context id. Like this: <code>
* RulesValidator validator = myValidatingFormModel.getValidator();
* validator.setRulesContextId( "mySpecialFormId" );
* </code>
* Along with this you will need to register your rules using the context id.
*
* @author Keith Donald
* @author Oliver Hutchison
*/
public class DefaultFormModel extends AbstractFormModel implements ValidatingFormModel {
private final DefaultValidationResultsModel validationResultsModel = new DefaultValidationResultsModel();
private final DefaultValidationResults additionalValidationResults = new DefaultValidationResults();
private final Map bindingErrorMessages = new HashMap();
private boolean validating = true;
private boolean oldValidating = true;
private boolean oldHasErrors = false;
private Validator validator;
private BindingErrorMessageProvider bindingErrorMessageProvider;
public DefaultFormModel() {
init();
}
public DefaultFormModel(Object domainObject) {
super(domainObject);
init();
}
public DefaultFormModel(Object domainObject, boolean buffered) {
super(domainObject, buffered);
init();
}
public DefaultFormModel(ValueModel domainObjectHolder) {
super(domainObjectHolder, true);
init();
}
public DefaultFormModel(ValueModel domainObjectHolder, boolean buffered) {
super(domainObjectHolder, buffered);
init();
}
public DefaultFormModel(MutablePropertyAccessStrategy domainObjectAccessStrategy) {
super(domainObjectAccessStrategy, true);
init();
}
public DefaultFormModel(MutablePropertyAccessStrategy domainObjectAccessStrategy, boolean bufferChanges) {
super(domainObjectAccessStrategy, bufferChanges);
init();
}
/**
* Initialization of DefaultFormModel. Adds a listener on the Enabled
* property in order to switch validating state on or off. When disabling a
* formModel, no validation will happen.
*/
protected void init() {
addPropertyChangeListener(ENABLED_PROPERTY, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
validatingUpdated();
}
});
validationResultsModel.addPropertyChangeListener(ValidationResultsModel.HAS_ERRORS_PROPERTY,
childStateChangeHandler);
}
/**
* {@inheritDoc}
*/
public boolean isValidating() {
if (validating && isEnabled()) {
if (getParent() instanceof ValidatingFormModel)
{
return ((ValidatingFormModel)getParent()).isValidating();
}
return true;
}
return false;
}
public void setValidating(boolean validating) {
this.validating = validating;
validatingUpdated();
}
protected void validatingUpdated() {
boolean validating = isValidating();
if (hasChanged(oldValidating, validating)) {
if (validating) {
validate();
}
else {
validationResultsModel.clearAllValidationResults();
}
oldValidating = validating;
firePropertyChange(VALIDATING_PROPERTY, !validating, validating);
}
}
public void addChild(HierarchicalFormModel child) {
if (child.getParent() == this)
return;
super.addChild(child);
if (child instanceof ValidatingFormModel) {
getValidationResults().add(((ValidatingFormModel) child).getValidationResults());
child.addPropertyChangeListener(ValidationResultsModel.HAS_ERRORS_PROPERTY, childStateChangeHandler);
}
}
public void removeChild(HierarchicalFormModel child) {
if (child instanceof ValidatingFormModel) {
getValidationResults().remove(((ValidatingFormModel) child).getValidationResults());
child.removePropertyChangeListener(ValidationResultsModel.HAS_ERRORS_PROPERTY, childStateChangeHandler);
}
super.removeChild(child);
}
/**
* {@inheritDoc}
*
* Additionally the {@link DefaultFormModel} adds the event:
*
* <ul>
* <li><em>Has errors event:</em> if the validation results model
* contains errors, the form model error state should be revised as well as
* the committable state.</li>
* </ul>
*
* Note that we see the {@link ValidationResultsModel} as a child model of
* the {@link DefaultFormModel} as the result model is bundled together with
* the value models.
*/
protected void childStateChanged(PropertyChangeEvent evt) {
super.childStateChanged(evt);
if (ValidationResultsModel.HAS_ERRORS_PROPERTY.equals(evt.getPropertyName())) {
hasErrorsUpdated();
}
}
public void setParent(HierarchicalFormModel parent) {
super.setParent(parent);
if (parent instanceof ValidatingFormModel) {
parent.addPropertyChangeListener(ValidatingFormModel.VALIDATING_PROPERTY, parentStateChangeHandler);
}
}
/**
* {@inheritDoc}
*
* Additionally the {@link DefaultFormModel} adds the event:
*
* <ul>
* <li><em>Validating state:</em> if validating is disabled on parent,
* child should not validate as well. If parent is set to validating the
* child's former validating state should apply.</li>
* </ul>
*/
protected void parentStateChanged(PropertyChangeEvent evt) {
super.parentStateChanged(evt);
if (ValidatingFormModel.VALIDATING_PROPERTY.equals(evt.getPropertyName())) {
validatingUpdated();
}
}
public void removeParent() {
if (getParent() instanceof ValidatingFormModel) {
getParent().removePropertyChangeListener(ValidatingFormModel.VALIDATING_PROPERTY, parentStateChangeHandler);
}
super.removeParent();
}
public ValidationResultsModel getValidationResults() {
return validationResultsModel;
}
public boolean getHasErrors() {
return validationResultsModel.getHasErrors();
}
protected void hasErrorsUpdated() {
boolean hasErrors = getHasErrors();
if (hasChanged(oldHasErrors, hasErrors)) {
oldHasErrors = hasErrors;
firePropertyChange(ValidationResultsModel.HAS_ERRORS_PROPERTY, !hasErrors, hasErrors);
committableUpdated();
}
}
public void validate() {
if (isValidating()) {
validateAfterPropertyChanged(null);
}
}
public Validator getValidator() {
if (validator == null) {
setValidator(new RulesValidator(this));
}
return validator;
}
/**
* {@inheritDoc}
*
* <p>
* Setting a validator will trigger a validate of the current object.
* </p>
*/
public void setValidator(Validator validator) {
Assert.notNull(validator, "validator");
this.validator = validator;
validate();
}
public boolean isCommittable() {
final boolean superIsCommittable = super.isCommittable();
final boolean hasNoErrors = !getValidationResults().getHasErrors();
return superIsCommittable && hasNoErrors;
}
protected ValueModel preProcessNewValueModel(String formProperty, ValueModel formValueModel) {
if (!(formValueModel instanceof ValidatingFormValueModel)) {
return new ValidatingFormValueModel(formProperty, formValueModel, true);
}
return formValueModel;
}
protected void postProcessNewValueModel(String formProperty, ValueModel valueModel) {
validateAfterPropertyChanged(formProperty);
}
protected ValueModel preProcessNewConvertingValueModel(String formProperty, Class targetClass,
ValueModel formValueModel) {
return new ValidatingFormValueModel(formProperty, formValueModel, false);
}
protected void postProcessNewConvertingValueModel(String formProperty, Class targetClass, ValueModel valueModel) {
}
protected void formPropertyValueChanged(String formProperty) {
validateAfterPropertyChanged(formProperty);
}
/**
*
* @param formProperty the name of the only property that has changed since
* the last call to validateAfterPropertyChange or <code>null</code> if
* this is not known/available.
*/
protected void validateAfterPropertyChanged(String formProperty) {
if (isValidating()) {
Validator validator = getValidator();
if (validator != null) {
DefaultValidationResults validationResults = new DefaultValidationResults(bindingErrorMessages.values());
if (formProperty != null && validator instanceof RichValidator) {
validationResults.addAllMessages(((RichValidator) validator)
.validate(getFormObject(), formProperty));
}
else {
validationResults.addAllMessages(validator.validate(getFormObject()));
}
validationResults.addAllMessages(additionalValidationResults);
validationResultsModel.updateValidationResults(validationResults);
}
}
}
protected void raiseBindingError(ValidatingFormValueModel valueModel, Object valueBeingSet, Exception e) {
ValidationMessage oldValidationMessage = (ValidationMessage) bindingErrorMessages.get(valueModel);
ValidationMessage newValidationMessage = getBindingErrorMessage(valueModel.getFormProperty(), valueBeingSet, e);
bindingErrorMessages.put(valueModel, newValidationMessage);
if (isValidating()) {
validationResultsModel.replaceMessage(oldValidationMessage, newValidationMessage);
}
}
protected void clearBindingError(ValidatingFormValueModel valueModel) {
ValidationMessage validationMessage = (ValidationMessage) bindingErrorMessages.remove(valueModel);
if (validationMessage != null) {
validationResultsModel.removeMessage(validationMessage);
}
}
public void raiseValidationMessage(ValidationMessage validationMessage) {
additionalValidationResults.addMessage(validationMessage);
if (isValidating()) {
validationResultsModel.addMessage(validationMessage);
}
}
public void clearValidationMessage(ValidationMessage validationMessage) {
additionalValidationResults.removeMessage(validationMessage);
if (isValidating()) {
validationResultsModel.removeMessage(validationMessage);
}
}
public BindingErrorMessageProvider getBindingErrorMessageProvider() {
if(bindingErrorMessageProvider == null)
return ValkyrieRepository.getInstance().getApplicationConfig().bindingErrorMessageProvider();
return bindingErrorMessageProvider;
}
protected ValidationMessage getBindingErrorMessage(String propertyName, Object valueBeingSet, Exception e) {
return getBindingErrorMessageProvider().getErrorMessage(this, propertyName, valueBeingSet, e);
}
public String toString() {
return new ToStringCreator(this).append("id", getId()).append("buffered", isBuffered()).append("enabled",
isEnabled()).append("dirty", isDirty()).append("validating", isValidating()).append(
"validationResults", getValidationResults()).toString();
}
public void setBindingErrorMessageProvider(BindingErrorMessageProvider bindingErrorMessageProvider) {
this.bindingErrorMessageProvider = bindingErrorMessageProvider;
}
protected class ValidatingFormValueModel extends AbstractValueModelWrapper {
private final String formProperty;
private final ValueChangeHandler valueChangeHander;
public ValidatingFormValueModel(String formProperty, ValueModel model, boolean validateOnChange) {
super(model);
this.formProperty = formProperty;
if (validateOnChange) {
this.valueChangeHander = new ValueChangeHandler();
addValueChangeListener(valueChangeHander);
}
else {
this.valueChangeHander = null;
}
}
public String getFormProperty() {
return formProperty;
}
public void setValueSilently(Object value, PropertyChangeListener listenerToSkip) {
try {
if (logger.isDebugEnabled()) {
Class valueClass = (value != null ? value.getClass() : null);
logger.debug("Setting '" + formProperty + "' value to convert/validate '"
+ (UserMetadata.isFieldProtected(DefaultFormModel.this, formProperty) ? "***" : value)
+ "', class=" + valueClass);
}
super.setValueSilently(value, listenerToSkip);
clearBindingError(this);
}
catch (ConversionException ce) {
logger.warn("Conversion exception occurred setting value", ce);
raiseBindingError(this, value, ce);
}
catch (PropertyAccessException pae) {
logger.warn("Type Mismatch Exception occurred setting value", pae);
raiseBindingError(this, value, pae);
}
}
public class ValueChangeHandler implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
formPropertyValueChanged(formProperty);
}
}
}
}