001    /*
002     * Copyright 2002-2005 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.binding.form.support;
017    
018    import java.beans.PropertyChangeEvent;
019    import java.beans.PropertyChangeListener;
020    import java.util.HashMap;
021    import java.util.Map;
022    
023    import org.springframework.beans.PropertyAccessException;
024    import org.springframework.binding.MutablePropertyAccessStrategy;
025    import org.springframework.binding.convert.ConversionException;
026    import org.springframework.binding.form.BindingErrorMessageProvider;
027    import org.springframework.binding.form.HierarchicalFormModel;
028    import org.springframework.binding.form.ValidatingFormModel;
029    import org.springframework.binding.validation.RichValidator;
030    import org.springframework.binding.validation.ValidationMessage;
031    import org.springframework.binding.validation.ValidationResultsModel;
032    import org.springframework.binding.validation.Validator;
033    import org.springframework.binding.validation.support.DefaultValidationResults;
034    import org.springframework.binding.validation.support.DefaultValidationResultsModel;
035    import org.springframework.binding.validation.support.RulesValidator;
036    import org.springframework.binding.value.ValueModel;
037    import org.springframework.binding.value.support.AbstractValueModelWrapper;
038    import org.springframework.core.style.ToStringCreator;
039    import org.springframework.richclient.util.Assert;
040    import org.springframework.rules.support.DefaultRulesSource;
041    
042    /**
043     * Default form model implementation. Is configurable, hierarchical and
044     * validating.
045     * <p>
046     * If you need this form model to use validation rules that are specific to a
047     * given context (such as a specific form), then you will need to call
048     * {@link #setValidator(Validator)} with a validator configured with the
049     * required context id. Like this: <code>
050     * RulesValidator validator = myValidatingFormModel.getValidator();
051     * validator.setRulesContextId( "mySpecialFormId" );
052     * </code>
053     * Along with this you will need to register your rules using the context id.
054     * See {@link DefaultRulesSource#addRules(String, org.springframework.rules.Rules)}.
055     *
056     * @author Keith Donald
057     * @author Oliver Hutchison
058     */
059    public class DefaultFormModel extends AbstractFormModel implements ValidatingFormModel {
060    
061            private final DefaultValidationResultsModel validationResultsModel = new DefaultValidationResultsModel();
062    
063            private final DefaultValidationResults additionalValidationResults = new DefaultValidationResults();
064    
065            private final Map bindingErrorMessages = new HashMap();
066    
067            private boolean validating = true;
068    
069            private boolean oldValidating = true;
070    
071            private boolean oldHasErrors = false;
072    
073            private Validator validator;
074    
075            private BindingErrorMessageProvider bindingErrorMessageProvider = new DefaultBindingErrorMessageProvider();
076    
077            public DefaultFormModel() {
078                    init();
079            }
080    
081            public DefaultFormModel(Object domainObject) {
082                    super(domainObject);
083                    init();
084            }
085    
086            public DefaultFormModel(Object domainObject, boolean buffered) {
087                    super(domainObject, buffered);
088                    init();
089            }
090    
091            public DefaultFormModel(ValueModel domainObjectHolder) {
092                    super(domainObjectHolder, true);
093                    init();
094            }
095    
096            public DefaultFormModel(ValueModel domainObjectHolder, boolean buffered) {
097                    super(domainObjectHolder, buffered);
098                    init();
099            }
100    
101            public DefaultFormModel(MutablePropertyAccessStrategy domainObjectAccessStrategy) {
102                    super(domainObjectAccessStrategy, true);
103                    init();
104            }
105    
106            public DefaultFormModel(MutablePropertyAccessStrategy domainObjectAccessStrategy, boolean bufferChanges) {
107                    super(domainObjectAccessStrategy, bufferChanges);
108                    init();
109            }
110    
111            /**
112             * Initialization of DefaultFormModel. Adds a listener on the Enabled
113             * property in order to switch validating state on or off. When disabling a
114             * formModel, no validation will happen.
115             */
116            protected void init() {
117                    addPropertyChangeListener(ENABLED_PROPERTY, new PropertyChangeListener() {
118    
119                            public void propertyChange(PropertyChangeEvent evt) {
120                                    validatingUpdated();
121                            }
122    
123                    });
124                    validationResultsModel.addPropertyChangeListener(ValidationResultsModel.HAS_ERRORS_PROPERTY,
125                                    childStateChangeHandler);
126            }
127    
128            /**
129             * {@inheritDoc}
130             */
131            public boolean isValidating() {
132                    if (validating && isEnabled()) {
133                            if (getParent() instanceof ValidatingFormModel)
134                            {
135                                    return ((ValidatingFormModel)getParent()).isValidating();
136                            }
137                            return true;
138                }
139                    return false;
140            }
141    
142            public void setValidating(boolean validating) {
143                    this.validating = validating;
144                    validatingUpdated();
145            }
146    
147            protected void validatingUpdated() {
148                    boolean validating = isValidating();
149                    if (hasChanged(oldValidating, validating)) {
150                            if (validating) {
151                                    validate();
152                            }
153                            else {
154                                    validationResultsModel.clearAllValidationResults();
155                            }
156                            oldValidating = validating;
157                            firePropertyChange(VALIDATING_PROPERTY, !validating, validating);
158                    }
159            }
160    
161            public void addChild(HierarchicalFormModel child) {
162                    if (child.getParent() == this)
163                            return;
164    
165                    super.addChild(child);
166                    if (child instanceof ValidatingFormModel) {
167                            getValidationResults().add(((ValidatingFormModel) child).getValidationResults());
168                            child.addPropertyChangeListener(ValidationResultsModel.HAS_ERRORS_PROPERTY, childStateChangeHandler);
169                    }
170            }
171    
172            public void removeChild(HierarchicalFormModel child) {
173                    if (child instanceof ValidatingFormModel) {
174                            getValidationResults().remove(((ValidatingFormModel) child).getValidationResults());
175                            child.removePropertyChangeListener(ValidationResultsModel.HAS_ERRORS_PROPERTY, childStateChangeHandler);
176                    }
177                    super.removeChild(child);
178            }
179    
180            /**
181             * {@inheritDoc}
182             *
183             * Additionally the {@link DefaultFormModel} adds the event:
184             *
185             * <ul>
186             * <li><em>Has errors event:</em> if the validation results model
187             * contains errors, the form model error state should be revised as well as
188             * the committable state.</li>
189             * </ul>
190             *
191             * Note that we see the {@link ValidationResultsModel} as a child model of
192             * the {@link DefaultFormModel} as the result model is bundled together with
193             * the value models.
194             */
195            protected void childStateChanged(PropertyChangeEvent evt) {
196                    super.childStateChanged(evt);
197                    if (ValidationResultsModel.HAS_ERRORS_PROPERTY.equals(evt.getPropertyName())) {
198                            hasErrorsUpdated();
199                    }
200            }
201    
202            public void setParent(HierarchicalFormModel parent) {
203                    super.setParent(parent);
204                    if (parent instanceof ValidatingFormModel) {
205                            parent.addPropertyChangeListener(ValidatingFormModel.VALIDATING_PROPERTY, parentStateChangeHandler);
206                    }
207            }
208    
209            /**
210             * {@inheritDoc}
211             *
212             * Additionally the {@link DefaultFormModel} adds the event:
213             *
214             * <ul>
215             * <li><em>Validating state:</em> if validating is disabled on parent,
216             * child should not validate as well. If parent is set to validating the
217             * child's former validating state should apply.</li>
218             * </ul>
219             */
220            protected void parentStateChanged(PropertyChangeEvent evt) {
221                    super.parentStateChanged(evt);
222                    if (ValidatingFormModel.VALIDATING_PROPERTY.equals(evt.getPropertyName())) {
223                            validatingUpdated();
224                    }
225            }
226    
227            public void removeParent() {
228                    if (getParent() instanceof ValidatingFormModel) {
229                            getParent().removePropertyChangeListener(ValidatingFormModel.VALIDATING_PROPERTY, parentStateChangeHandler);
230                    }
231                    super.removeParent();
232            }
233    
234            public ValidationResultsModel getValidationResults() {
235                    return validationResultsModel;
236            }
237    
238            public boolean getHasErrors() {
239                    return validationResultsModel.getHasErrors();
240            }
241    
242            protected void hasErrorsUpdated() {
243                    boolean hasErrors = getHasErrors();
244                    if (hasChanged(oldHasErrors, hasErrors)) {
245                            oldHasErrors = hasErrors;
246                            firePropertyChange(ValidationResultsModel.HAS_ERRORS_PROPERTY, !hasErrors, hasErrors);
247                            committableUpdated();
248                    }
249            }
250    
251            public void validate() {
252                    if (isValidating()) {
253                            validateAfterPropertyChanged(null);
254                    }
255            }
256    
257            public Validator getValidator() {
258                    if (validator == null) {
259                            setValidator(new RulesValidator(this));
260                    }
261                    return validator;
262            }
263    
264            /**
265             * {@inheritDoc}
266             *
267             * <p>
268             * Setting a validator will trigger a validate of the current object.
269             * </p>
270             */
271            public void setValidator(Validator validator) {
272                    Assert.required(validator, "validator");
273                    this.validator = validator;
274                    validate();
275            }
276    
277            public boolean isCommittable() {
278                    final boolean superIsCommittable = super.isCommittable();
279                    final boolean hasNoErrors = !getValidationResults().getHasErrors();
280                    return superIsCommittable && hasNoErrors;
281            }
282    
283            protected ValueModel preProcessNewValueModel(String formProperty, ValueModel formValueModel) {
284                    if (!(formValueModel instanceof ValidatingFormValueModel)) {
285                            return new ValidatingFormValueModel(formProperty, formValueModel, true);
286                    }
287                    return formValueModel;
288            }
289    
290            protected void postProcessNewValueModel(String formProperty, ValueModel valueModel) {
291                    validateAfterPropertyChanged(formProperty);
292            }
293    
294            protected ValueModel preProcessNewConvertingValueModel(String formProperty, Class targetClass,
295                            ValueModel formValueModel) {
296                    return new ValidatingFormValueModel(formProperty, formValueModel, false);
297            }
298    
299            protected void postProcessNewConvertingValueModel(String formProperty, Class targetClass, ValueModel valueModel) {
300            }
301    
302            protected void formPropertyValueChanged(String formProperty) {
303                    validateAfterPropertyChanged(formProperty);
304            }
305    
306            /**
307             *
308             * @param formProperty the name of the only property that has changed since
309             * the last call to validateAfterPropertyChange or <code>null</code> if
310             * this is not known/available.
311             */
312            protected void validateAfterPropertyChanged(String formProperty) {
313                    if (isValidating()) {
314                            Validator validator = getValidator();
315                            if (validator != null) {
316                                    DefaultValidationResults validationResults = new DefaultValidationResults(bindingErrorMessages.values());
317                                    if (formProperty != null && validator instanceof RichValidator) {
318                                            validationResults.addAllMessages(((RichValidator) validator)
319                                                            .validate(getFormObject(), formProperty));
320                                    }
321                                    else {
322                                            validationResults.addAllMessages(validator.validate(getFormObject()));
323                                    }
324                                    validationResults.addAllMessages(additionalValidationResults);
325                                    validationResultsModel.updateValidationResults(validationResults);
326                            }
327                    }
328            }
329    
330            protected void raiseBindingError(ValidatingFormValueModel valueModel, Object valueBeingSet, Exception e) {
331                    ValidationMessage oldValidationMessage = (ValidationMessage) bindingErrorMessages.get(valueModel);
332                    ValidationMessage newValidationMessage = getBindingErrorMessage(valueModel.getFormProperty(), valueBeingSet, e);
333                    bindingErrorMessages.put(valueModel, newValidationMessage);
334                    if (isValidating()) {
335                            validationResultsModel.replaceMessage(oldValidationMessage, newValidationMessage);
336                    }
337            }
338    
339            protected void clearBindingError(ValidatingFormValueModel valueModel) {
340                    ValidationMessage validationMessage = (ValidationMessage) bindingErrorMessages.remove(valueModel);
341                    if (validationMessage != null) {
342                            validationResultsModel.removeMessage(validationMessage);
343                    }
344            }
345    
346            public void raiseValidationMessage(ValidationMessage validationMessage) {
347                    additionalValidationResults.addMessage(validationMessage);
348                    if (isValidating()) {
349                            validationResultsModel.addMessage(validationMessage);
350                    }
351            }
352    
353            public void clearValidationMessage(ValidationMessage validationMessage) {
354                    additionalValidationResults.removeMessage(validationMessage);
355                    if (isValidating()) {
356                            validationResultsModel.removeMessage(validationMessage);
357                    }
358            }
359    
360            protected ValidationMessage getBindingErrorMessage(String propertyName, Object valueBeingSet, Exception e) {
361                    return bindingErrorMessageProvider.getErrorMessage(this, propertyName, valueBeingSet, e);
362            }
363    
364            public void setBindingErrorMessageProvider(BindingErrorMessageProvider bindingErrorMessageProvider) {
365                    Assert.required(bindingErrorMessageProvider, "bindingErrorMessageProvider");
366                    this.bindingErrorMessageProvider = bindingErrorMessageProvider;
367            }
368    
369            public String toString() {
370                    return new ToStringCreator(this).append("id", getId()).append("buffered", isBuffered()).append("enabled",
371                                    isEnabled()).append("dirty", isDirty()).append("validating", isValidating()).append(
372                                    "validationResults", getValidationResults()).toString();
373            }
374    
375            protected class ValidatingFormValueModel extends AbstractValueModelWrapper {
376                    private final String formProperty;
377    
378                    private final ValueChangeHandler valueChangeHander;
379    
380                    public ValidatingFormValueModel(String formProperty, ValueModel model, boolean validateOnChange) {
381                            super(model);
382                            this.formProperty = formProperty;
383                            if (validateOnChange) {
384                                    this.valueChangeHander = new ValueChangeHandler();
385                                    addValueChangeListener(valueChangeHander);
386                            }
387                            else {
388                                    this.valueChangeHander = null;
389                            }
390                    }
391    
392                    public String getFormProperty() {
393                            return formProperty;
394                    }
395    
396                    public void setValueSilently(Object value, PropertyChangeListener listenerToSkip) {
397                            try {
398                                    if (logger.isDebugEnabled()) {
399                                            Class valueClass = (value != null ? value.getClass() : null);
400                                            logger.debug("Setting '" + formProperty + "' value to convert/validate '"
401                                                            + (UserMetadata.isFieldProtected(DefaultFormModel.this, formProperty) ? "***" : value)
402                                                            + "', class=" + valueClass);
403                                    }
404                                    super.setValueSilently(value, listenerToSkip);
405                                    clearBindingError(this);
406                            }
407                            catch (ConversionException ce) {
408                                    logger.warn("Conversion exception occurred setting value", ce);
409                                    raiseBindingError(this, value, ce);
410                            }
411                            catch (PropertyAccessException pae) {
412                                    logger.warn("Type Mismatch Exception occurred setting value", pae);
413                                    raiseBindingError(this, value, pae);
414                            }
415                    }
416    
417                    public class ValueChangeHandler implements PropertyChangeListener {
418                            public void propertyChange(PropertyChangeEvent evt) {
419                                    formPropertyValueChanged(formProperty);
420                            }
421                    }
422            }
423    }