001    /*
002     * Copyright 2002-2004 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.ArrayList;
021    import java.util.Collections;
022    import java.util.HashMap;
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Set;
028    
029    import org.springframework.beans.BeanUtils;
030    import org.springframework.binding.MutablePropertyAccessStrategy;
031    import org.springframework.binding.PropertyAccessStrategy;
032    import org.springframework.binding.PropertyMetadataAccessStrategy;
033    import org.springframework.binding.convert.ConversionExecutor;
034    import org.springframework.binding.convert.ConversionService;
035    import org.springframework.binding.convert.Converter;
036    import org.springframework.binding.convert.support.DefaultConversionService;
037    import org.springframework.binding.convert.support.GenericConversionService;
038    import org.springframework.binding.form.CommitListener;
039    import org.springframework.binding.form.ConfigurableFormModel;
040    import org.springframework.binding.form.FieldFace;
041    import org.springframework.binding.form.FieldFaceSource;
042    import org.springframework.binding.form.FieldMetadata;
043    import org.springframework.binding.form.FormModel;
044    import org.springframework.binding.form.HierarchicalFormModel;
045    import org.springframework.binding.support.BeanPropertyAccessStrategy;
046    import org.springframework.binding.value.CommitTrigger;
047    import org.springframework.binding.value.ValueModel;
048    import org.springframework.binding.value.support.AbstractPropertyChangePublisher;
049    import org.springframework.binding.value.support.BufferedValueModel;
050    import org.springframework.binding.value.support.DirtyTrackingValueModel;
051    import org.springframework.binding.value.support.MethodInvokingDerivedValueModel;
052    import org.springframework.binding.value.support.TypeConverter;
053    import org.springframework.binding.value.support.ValueHolder;
054    import org.springframework.richclient.application.ApplicationServicesLocator;
055    import org.springframework.richclient.util.Assert;
056    import org.springframework.richclient.util.ClassUtils;
057    import org.springframework.richclient.util.EventListenerListHelper;
058    import org.springframework.util.CachingMapDecorator;
059    
060    /**
061     * Base implementation of HierarchicalFormModel and ConfigurableFormModel
062     * subclasses need only implement the 4 value model interception methods.
063     *
064     * @author Keith Donald
065     * @author Oliver Hutchison
066     */
067    public abstract class AbstractFormModel extends AbstractPropertyChangePublisher implements HierarchicalFormModel,
068                    ConfigurableFormModel {
069    
070            private String id;
071    
072            private final FormModelMediatingValueModel formObjectHolder;
073    
074            private final MutablePropertyAccessStrategy propertyAccessStrategy;
075    
076            private HierarchicalFormModel parent;
077    
078            private final List children = new ArrayList();
079    
080            private boolean buffered = false;
081    
082            private boolean enabled = true;
083    
084            private boolean oldEnabled = true;
085    
086            private boolean readOnly = false;
087    
088            private boolean oldReadOnly = false;
089    
090            private boolean authorized = true;
091            
092            private boolean oldDirty;
093    
094            private boolean oldCommittable = true;
095    
096            private ConversionService conversionService;
097    
098            private final CommitTrigger commitTrigger = new CommitTrigger();
099    
100            private final Map mediatingValueModels = new HashMap();
101    
102            private final Map propertyValueModels = new HashMap();
103    
104            private final Map convertingValueModels = new HashMap();
105    
106            private final Map fieldMetadata = new HashMap();
107    
108            private final Set dirtyValueAndFormModels = new HashSet();
109    
110            private final Map propertyConversionServices = new CachingMapDecorator() {
111                    public Object create(Object key) {
112                            return new DefaultConversionService() {
113                                    protected void addDefaultConverters() {
114                                    }
115                            };
116                    }
117            };
118    
119            private FieldFaceSource fieldFaceSource;
120    
121            protected final PropertyChangeListener parentStateChangeHandler = new ParentStateChangeHandler();
122    
123            protected final PropertyChangeListener childStateChangeHandler = new ChildStateChangeHandler();
124    
125            private final EventListenerListHelper commitListeners = new EventListenerListHelper(CommitListener.class);
126    
127            private Class defaultInstanceClass;
128    
129            protected AbstractFormModel() {
130                    this(new ValueHolder());
131            }
132    
133            protected AbstractFormModel(Object domainObject) {
134                    this(new ValueHolder(domainObject), true);
135            }
136    
137            public AbstractFormModel(Object domainObject, boolean buffered) {
138                    this(new ValueHolder(domainObject), buffered);
139            }
140    
141            protected AbstractFormModel(ValueModel domainObjectHolder, boolean buffered) {
142                    this(new BeanPropertyAccessStrategy(domainObjectHolder), buffered);
143            }
144    
145            protected AbstractFormModel(MutablePropertyAccessStrategy propertyAccessStrategy, boolean buffered) {
146                    ValueModel domainObjectHolder = propertyAccessStrategy.getDomainObjectHolder();
147                    prepareValueModel(domainObjectHolder);
148                    this.formObjectHolder = new FormModelMediatingValueModel(domainObjectHolder, false);
149                    this.propertyAccessStrategy = propertyAccessStrategy;
150                    this.buffered = buffered;
151                    if (domainObjectHolder.getValue() != null)
152                            this.defaultInstanceClass = domainObjectHolder.getValue().getClass();
153            }
154    
155            /**
156             * Prepare the provided value model for use in this form model.
157             * @param valueModel to prepare
158             */
159            protected void prepareValueModel(ValueModel valueModel) {
160                    if (valueModel instanceof BufferedValueModel) {
161                            ((BufferedValueModel) valueModel).setCommitTrigger(commitTrigger);
162                    }
163    
164                    // If the value model that we were built on is "dirty trackable" then we
165                    // need to monitor it for changes in its dirty state
166                    if (valueModel instanceof DirtyTrackingValueModel) {
167                            ((DirtyTrackingValueModel) valueModel).addPropertyChangeListener(DIRTY_PROPERTY, childStateChangeHandler);
168                    }
169            }
170    
171            public String getId() {
172                    return id;
173            }
174    
175            public void setId(String id) {
176                    this.id = id;
177            }
178    
179            public Object getFormObject() {
180                    return getFormObjectHolder().getValue();
181            }
182    
183            public void setFormObject(Object formObject) {
184                    setDeliverValueChangeEvents(false, false);
185                    if (formObject == null) {
186                            handleSetNullFormObject();
187                    }
188                    else {
189                            getFormObjectHolder().setValue(formObject);
190                            setEnabled(true);
191                    }
192                    // this will cause all buffered value models to revert
193                    // to the new form objects property values
194                    commitTrigger.revert();
195                    setDeliverValueChangeEvents(true, true);
196            }
197    
198            /**
199             * Disconnect view from data in MediatingValueModels, possibly clearing them
200             * afterwards.
201             *
202             * @param deliverValueChangeEvents <code>true</code> if events should be
203             * delivered.
204             * @param clearValueModels <code>true</code> if models should be cleared
205             * afterwards.
206             */
207            private void setDeliverValueChangeEvents(boolean deliverValueChangeEvents, boolean clearValueModels) {
208                    formObjectHolder.setDeliverValueChangeEvents(deliverValueChangeEvents);
209                    for (Iterator i = mediatingValueModels.values().iterator(); i.hasNext();) {
210                            FormModelMediatingValueModel valueModel = (FormModelMediatingValueModel) i.next();
211                            valueModel.setDeliverValueChangeEvents(deliverValueChangeEvents);
212                            if (clearValueModels)
213                                    valueModel.clearDirty();
214                    }
215            }
216    
217            public void setDefaultInstanceClass(Class defaultInstanceClass) {
218                    this.defaultInstanceClass = defaultInstanceClass;
219            }
220    
221            public Class getDefaultInstanceClass() {
222                    return defaultInstanceClass;
223            }
224    
225            protected void handleSetNullFormObject() {
226                    if (logger.isInfoEnabled()) {
227                            logger.info("New form object value is null; resetting to a new fresh object instance and disabling form");
228                    }
229                    if (getDefaultInstanceClass() != null) {
230                            getFormObjectHolder().setValue(BeanUtils.instantiateClass(getDefaultInstanceClass()));
231                    }
232                    else { // old behaviour
233                            getFormObjectHolder().setValue(BeanUtils.instantiateClass(getFormObject().getClass()));
234                    }
235                    setEnabled(false);
236            }
237    
238            /**
239             * Returns the value model which holds the object currently backing this
240             * form.
241             */
242            public ValueModel getFormObjectHolder() {
243                    return formObjectHolder;
244            }
245    
246            public HierarchicalFormModel getParent() {
247                    return parent;
248            }
249    
250            /**
251             * {@inheritDoc}
252             * 
253             * When the parent is set, the enabled and read-only states are bound and
254             * updated as needed.
255             */
256            public void setParent(HierarchicalFormModel parent) {
257                    Assert.required(parent, "parent");
258                    this.parent = parent;
259                    this.parent.addPropertyChangeListener(ENABLED_PROPERTY, parentStateChangeHandler);
260                    this.parent.addPropertyChangeListener(READONLY_PROPERTY, parentStateChangeHandler);
261                    enabledUpdated();
262                    readOnlyUpdated();
263            }
264    
265            public void removeParent() {
266                    this.parent.removePropertyChangeListener(READONLY_PROPERTY, parentStateChangeHandler);
267                    this.parent.removePropertyChangeListener(ENABLED_PROPERTY, parentStateChangeHandler);
268                    this.parent = null;
269                    readOnlyUpdated();
270                    enabledUpdated();
271            }
272    
273            public FormModel[] getChildren() {
274                    return (FormModel[]) children.toArray(new FormModel[children.size()]);
275            }
276    
277            /**
278             * Add child to this FormModel. Dirty and committable changes are forwarded
279             * to parent model.
280             * @param child FormModel to add as child.
281             */
282            public void addChild(HierarchicalFormModel child) {
283                    Assert.required(child, "child");
284                    if (child.getParent() == this)
285                            return;
286                    Assert.isTrue(child.getParent() == null, "Child form model '" + child + "' already has a parent");
287                    child.setParent(this);
288                    children.add(child);
289                    child.addPropertyChangeListener(DIRTY_PROPERTY, childStateChangeHandler);
290                    child.addPropertyChangeListener(COMMITTABLE_PROPERTY, childStateChangeHandler);
291                    if (child.isDirty())
292                    {
293                            dirtyValueAndFormModels.add(child);
294                            dirtyUpdated();
295                    }
296            }
297    
298            /**
299             * Remove a child FormModel. Dirty and committable listeners are removed.
300             * When child was dirty, remove the formModel from the dirty list and update
301             * the dirty state.
302             * @param child FormModel to remove from childlist.
303             */
304            public void removeChild(HierarchicalFormModel child) {
305                    Assert.required(child, "child");
306                    child.removeParent();
307                    children.remove(child);
308                    child.removePropertyChangeListener(DIRTY_PROPERTY, childStateChangeHandler);
309                    child.removePropertyChangeListener(COMMITTABLE_PROPERTY, childStateChangeHandler);
310                    // when dynamically adding/removing childModels take care of
311                    // dirtymessages:
312                    // removing child that was dirty: remove from dirty map and update dirty
313                    // state
314                    if (dirtyValueAndFormModels.remove(child))
315                            dirtyUpdated();
316            }
317    
318            public boolean hasValueModel(String formProperty) {
319                    return propertyValueModels.containsKey(formProperty);
320            }
321    
322            public ValueModel getValueModel(String formProperty) {
323                    ValueModel propertyValueModel = (ValueModel) propertyValueModels.get(formProperty);
324                    if (propertyValueModel == null) {
325                            propertyValueModel = add(formProperty);
326                    }
327                    return propertyValueModel;
328            }
329    
330            public ValueModel getValueModel(String formProperty, Class targetClass) {
331                    final ConvertingValueModelKey key = new ConvertingValueModelKey(formProperty, targetClass);
332                    ValueModel convertingValueModel = (ValueModel) convertingValueModels.get(key);
333                    if (convertingValueModel == null) {
334                            convertingValueModel = createConvertingValueModel(formProperty, targetClass);
335                            convertingValueModels.put(key, convertingValueModel);
336                    }
337                    return convertingValueModel;
338            }
339    
340            /**
341             * Creates a new value mode for the the given property. Usually delegates to
342             * the underlying property access strategy but subclasses may provide
343             * alternative value model creation strategies.
344             */
345            protected ValueModel createValueModel(String formProperty) {
346                    Assert.required(formProperty, "formProperty");
347                    if (logger.isDebugEnabled()) {
348                            logger.debug("Creating " + (buffered ? "buffered" : "") + " value model for form property '" + formProperty
349                                            + "'.");
350                    }
351                    return buffered ? new BufferedValueModel(propertyAccessStrategy.getPropertyValueModel(formProperty))
352                                    : propertyAccessStrategy.getPropertyValueModel(formProperty);
353            }
354    
355            protected ValueModel createConvertingValueModel(String formProperty, Class targetClass) {
356                    if (logger.isDebugEnabled()) {
357                            logger.debug("Creating converting value model for form property '" + formProperty
358                                            + "' converting to type '" + targetClass + "'.");
359                    }
360                    final ValueModel sourceValueModel = getValueModel(formProperty);
361                    Assert.notNull(sourceValueModel, "Form does not have a property called '" + formProperty + "'.");
362                    final Class sourceClass = ClassUtils
363                                    .convertPrimitiveToWrapper(getFieldMetadata(formProperty).getPropertyType());
364                    // sourceClass can be null when using eg Map, assume that given
365                    // targetClass is the correct one
366                    if ((sourceClass == null) || (sourceClass == targetClass)) {
367                            return sourceValueModel;
368                    }
369    
370                    final ConversionService conversionService = getConversionService();
371                    ConversionExecutor convertTo = null;
372                    ConversionExecutor convertFrom = null;
373    
374                    // Check for locally registered property converters
375                    if (propertyConversionServices.containsKey(formProperty)) {
376                            // TODO - extract ConfigurableConversionService interface...
377                            final GenericConversionService propertyConversionService = (GenericConversionService) propertyConversionServices
378                                            .get(formProperty);
379    
380                            if (propertyConversionService != null) {
381                                    convertTo = propertyConversionService.getConversionExecutor(sourceClass, targetClass);
382                                    convertFrom = propertyConversionService.getConversionExecutor(targetClass, sourceClass);
383                            }
384                    }
385    
386                    // If we have nothing from the property level, then try the conversion
387                    // service
388                    if (convertTo == null) {
389                            convertTo = conversionService.getConversionExecutor(sourceClass, targetClass);
390                    }
391                    Assert.notNull(convertTo, "conversionService returned null ConversionExecutor");
392    
393                    if (convertFrom == null) {
394                            convertFrom = conversionService.getConversionExecutor(targetClass, sourceClass);
395                    }
396                    Assert.notNull(convertFrom, "conversionService returned null ConversionExecutor");
397    
398                    ValueModel convertingValueModel = preProcessNewConvertingValueModel(formProperty, targetClass,
399                                    new TypeConverter(sourceValueModel, convertTo, convertFrom));
400                    preProcessNewConvertingValueModel(formProperty, targetClass, convertingValueModel);
401                    return convertingValueModel;
402            }
403    
404            /**
405             * Register converters for a given property name.
406             * @param propertyName name of property on which to register converters
407             * @param toConverter Convert from source to target type
408             * @param fromConverter Convert from target to source type
409             */
410            public void registerPropertyConverter(String propertyName, Converter toConverter, Converter fromConverter) {
411                    DefaultConversionService propertyConversionService = (DefaultConversionService) propertyConversionServices
412                                    .get(propertyName);
413                    propertyConversionService.addConverter(toConverter);
414                    propertyConversionService.addConverter(fromConverter);
415            }
416    
417            public ValueModel add(String propertyName) {
418                    return add(propertyName, createValueModel(propertyName));
419            }
420    
421            public ValueModel add(String formProperty, ValueModel valueModel) {
422                    // XXX: this assert should be active but it breaks the
423                    // code in SwingBindingFactory#createBoundListModel
424                    // Assert.isTrue(!hasValueModel(formProperty), "A property called '" +
425                    // formProperty + "' already exists.");
426                    if (valueModel instanceof BufferedValueModel) {
427                            ((BufferedValueModel) valueModel).setCommitTrigger(commitTrigger);
428                    }
429    
430                    PropertyMetadataAccessStrategy metadataAccessStrategy = getFormObjectPropertyAccessStrategy()
431                                    .getMetadataAccessStrategy();
432    
433                    FormModelMediatingValueModel mediatingValueModel = new FormModelMediatingValueModel(valueModel,
434                                    metadataAccessStrategy.isWriteable(formProperty));
435                    mediatingValueModels.put(formProperty, mediatingValueModel);
436    
437                    FieldMetadata metadata = new DefaultFieldMetadata(this, mediatingValueModel, metadataAccessStrategy
438                                    .getPropertyType(formProperty), !metadataAccessStrategy.isWriteable(formProperty),
439                                    metadataAccessStrategy.getAllUserMetadata(formProperty));
440                    metadata.addPropertyChangeListener(FieldMetadata.DIRTY_PROPERTY, childStateChangeHandler);
441                    return add(formProperty, mediatingValueModel, metadata);
442            }
443    
444            /**
445             * {@inheritDoc}
446             */
447            public ValueModel add(String propertyName, ValueModel valueModel, FieldMetadata metadata) {
448                    fieldMetadata.put(propertyName, metadata);
449    
450                    valueModel = preProcessNewValueModel(propertyName, valueModel);
451                    propertyValueModels.put(propertyName, valueModel);
452    
453                    if (logger.isDebugEnabled()) {
454                            logger.debug("Registering '" + propertyName + "' form property, property value model=" + valueModel);
455                    }
456                    postProcessNewValueModel(propertyName, valueModel);
457                    return valueModel;
458            }
459    
460            /**
461             * Provides a hook for subclasses to optionally decorate a new value model
462             * added to this form model.
463             */
464            protected abstract ValueModel preProcessNewValueModel(String formProperty, ValueModel formValueModel);
465    
466            /**
467             * Provides a hook for subclasses to perform some processing after a new
468             * value model has been added to this form model.
469             */
470            protected abstract void postProcessNewValueModel(String formProperty, ValueModel valueModel);
471    
472            /**
473             * Provides a hook for subclasses to optionally decorate a new converting
474             * value model added to this form model.
475             */
476            protected abstract ValueModel preProcessNewConvertingValueModel(String formProperty, Class targetClass,
477                            ValueModel formValueModel);
478    
479            /**
480             * Provides a hook for subclasses to perform some processing after a new
481             * converting value model has been added to this form model.
482             */
483            protected abstract void postProcessNewConvertingValueModel(String formProperty, Class targetClass,
484                            ValueModel valueModel);
485    
486            public FieldMetadata getFieldMetadata(String propertyName) {
487                    FieldMetadata metadata = (FieldMetadata) fieldMetadata.get(propertyName);
488                    if (metadata == null) {
489                            add(propertyName);
490                            metadata = (FieldMetadata) fieldMetadata.get(propertyName);
491                    }
492                    return metadata;
493            }
494    
495            /**
496             * {@inheritDoc}
497             */
498            public Set getFieldNames() {
499                    return Collections.unmodifiableSet(propertyValueModels.keySet());
500            }
501    
502            /**
503             * Sets the FieldFaceSource that will be used to obtain FieldFace instances.
504             * <p>
505             * If this value is <code>null</code> the default FieldFaceSource from
506             * <code>ApplicationServices</code> instance will be used.
507             */
508            public void setFieldFaceSource(FieldFaceSource fieldFaceSource) {
509                    this.fieldFaceSource = fieldFaceSource;
510            }
511    
512            /**
513             * Returns the FieldFaceSource that should be used to obtain FieldFace
514             * instances for this form model.
515             */
516            protected FieldFaceSource getFieldFaceSource() {
517                    if (fieldFaceSource == null) {
518                            fieldFaceSource = (FieldFaceSource) ApplicationServicesLocator.services().getService(FieldFaceSource.class);
519                    }
520                    return fieldFaceSource;
521            }
522    
523            public FieldFace getFieldFace(String field) {
524                    return getFieldFaceSource().getFieldFace(field, this);
525            }
526    
527            public ValueModel addMethod(String propertyMethodName, String derivedFromProperty) {
528                    return addMethod(propertyMethodName, new String[] { derivedFromProperty });
529            }
530    
531            public ValueModel addMethod(String propertyMethodName, String[] derivedFromProperties) {
532                    ValueModel[] propertyValueModels = new ValueModel[derivedFromProperties.length];
533                    for (int i = 0; i < propertyValueModels.length; i++) {
534                            propertyValueModels[i] = getValueModel(derivedFromProperties[i]);
535                    }
536                    ValueModel valueModel = new MethodInvokingDerivedValueModel(this, propertyMethodName, propertyValueModels);
537                    return add(propertyMethodName, valueModel);
538            }
539    
540            public ConversionService getConversionService() {
541                    if (conversionService == null) {
542                            conversionService = (ConversionService) ApplicationServicesLocator.services().getService(
543                                            ConversionService.class);
544                    }
545                    return conversionService;
546            }
547    
548            public void setConversionService(ConversionService conversionService) {
549                    this.conversionService = conversionService;
550            }
551    
552            public MutablePropertyAccessStrategy getFormObjectPropertyAccessStrategy() {
553                    return propertyAccessStrategy;
554            }
555    
556            public PropertyAccessStrategy getPropertyAccessStrategy() {
557                    return new FormModelPropertyAccessStrategy(this);
558            }
559    
560            public void commit() {
561                    if (logger.isDebugEnabled()) {
562                            logger.debug("Commit requested for this form model " + this);
563                    }
564                    if (getFormObject() == null) {
565                            if (logger.isDebugEnabled()) {
566                                    logger.debug("Form object is null; nothing to commit.");
567                            }
568                            return;
569                    }
570                    if (isCommittable()) {
571                            for (Iterator i = commitListeners.iterator(); i.hasNext();) {
572                                    ((CommitListener) i.next()).preCommit(this);
573                            }
574                            preCommit();
575                            if (isCommittable()) {
576                                    doCommit();
577                                    postCommit();
578                                    for (Iterator i = commitListeners.iterator(); i.hasNext();) {
579                                            ((CommitListener) i.next()).postCommit(this);
580                                    }
581                            }
582                            else {
583                                    throw new IllegalStateException("Form model '" + this
584                                                    + "' became non-committable after preCommit phase");
585                            }
586                    }
587                    else {
588                            throw new IllegalStateException("Form model '" + this + "' is not committable");
589                    }
590            }
591    
592            private void doCommit() {
593                    for (Iterator i = children.iterator(); i.hasNext();) {
594                            ((FormModel) i.next()).commit();
595                    }
596                    commitTrigger.commit();
597                    for (Iterator i = mediatingValueModels.values().iterator(); i.hasNext();) {
598                            ((DirtyTrackingValueModel) i.next()).clearDirty();
599                    }
600            }
601    
602            /**
603             * Hook for subclasses to intercept before a commit.
604             */
605            protected void preCommit() {
606            }
607    
608            /**
609             * Hook for subclasses to intercept after a successful commit has finished.
610             */
611            protected void postCommit() {
612            }
613    
614            /**
615             * Revert state. If formModel has children, these will be reverted first.
616             * CommitTrigger is used to revert bufferedValueModels while
617             * revertToOriginal() is called upon FormMediatingValueModels.
618             */
619            public void revert() {
620                    for (Iterator i = children.iterator(); i.hasNext();) {
621                            ((FormModel) i.next()).revert();
622                    }
623                    // this will cause all buffered value models to revert
624                    commitTrigger.revert();
625                    // this will then go back and revert all unbuffered value models
626                    for (Iterator i = mediatingValueModels.values().iterator(); i.hasNext();) {
627                            ((DirtyTrackingValueModel) i.next()).revertToOriginal();
628                    }
629            }
630    
631            /**
632             * Complex forms with parent-child relations can use derived formModels.
633             * Such a Hierarchical tree cannot have its children reset on its own as it
634             * would break the top-down structure. see RCP-329 and the cvs maillist.
635             *
636             * TODO add a unit test with such a complex use case
637             *
638             * @see FormModel#reset()
639             */
640            public void reset() {
641                    setFormObject(null);
642            }
643    
644            public boolean isBuffered() {
645                    return buffered;
646            }
647    
648            /**
649             * Returns <code>true</code> if this formModel or any of its children has
650             * dirty valueModels.
651             */
652            public boolean isDirty() {
653                    return dirtyValueAndFormModels.size() > 0;
654            }
655    
656            /**
657             * Fires the necessary property change event for changes to the dirty
658             * property. Must be called whenever the value of dirty is changed.
659             */
660            protected void dirtyUpdated() {
661                    boolean dirty = isDirty();
662                    if (hasChanged(oldDirty, dirty)) {
663                            oldDirty = dirty;
664                            firePropertyChange(DIRTY_PROPERTY, !dirty, dirty);
665                    }
666            }
667    
668            public void setReadOnly(boolean readOnly) {
669                    this.readOnly = readOnly;
670                    readOnlyUpdated();
671            }
672    
673            public boolean isReadOnly() {
674                    return readOnly || !authorized || (parent != null && parent.isReadOnly());
675            }
676            
677            /**
678             * Check if the form has the correct authorization and can be edited.
679             * 
680             * @return <code>true</code> if this form is authorized and may be edited.
681             */
682            public boolean isAuthorized() {
683                    return authorized;
684            }
685    
686            /**
687             * Set whether or not the form is authorized and can be edited.
688             * 
689             * @param authorized <code>true</code> if this form may be edited.
690             */
691            public void setAuthorized(boolean authorized) {
692                    this.authorized = authorized;
693                    readOnlyUpdated();
694            }
695            
696            /**
697             * Fires the necessary property change event for changes to the readOnly
698             * property. Must be called whenever the value of readOnly is changed.
699             */
700            protected void readOnlyUpdated() {
701                    boolean localReadOnly = isReadOnly();
702                    if (hasChanged(oldReadOnly, localReadOnly)) {
703                            oldReadOnly = localReadOnly;
704                            firePropertyChange(READONLY_PROPERTY, !localReadOnly, localReadOnly);
705                    }
706            }
707    
708            public void setEnabled(boolean enabled) {
709                    this.enabled = enabled;
710                    enabledUpdated();
711            }
712    
713            public boolean isEnabled() {
714                    return enabled && (parent == null || parent.isEnabled());
715            }
716    
717            /**
718             * Fires the necessary property change event for changes to the enabled
719             * property. Must be called whenever the value of enabled is changed.
720             */
721            protected void enabledUpdated() {
722                    boolean enabled = isEnabled();
723                    if (hasChanged(oldEnabled, enabled)) {
724                            oldEnabled = enabled;
725                            firePropertyChange(ENABLED_PROPERTY, !enabled, enabled);
726                    }
727            }
728    
729            public boolean isCommittable() {
730                    for (Iterator i = children.iterator(); i.hasNext();) {
731                            final FormModel childFormModel = (FormModel) i.next();
732                            if (!childFormModel.isCommittable()) {
733                                    return false;
734                            }
735                    }
736                    return true;
737            }
738    
739            /**
740             * Fires the necessary property change event for changes to the committable
741             * property. Must be called whenever the value of committable is changed.
742             */
743            protected void committableUpdated() {
744                    boolean committable = isCommittable();
745                    if (hasChanged(oldCommittable, committable)) {
746                            oldCommittable = committable;
747                            firePropertyChange(COMMITTABLE_PROPERTY, !committable, committable);
748                    }
749            }
750    
751            public void addCommitListener(CommitListener listener) {
752                    commitListeners.add(listener);
753            }
754    
755            public void removeCommitListener(CommitListener listener) {
756                    commitListeners.remove(listener);
757            }
758    
759            /**
760             * Listener to be registered on properties of the parent form model. Calls
761             * are delegated to
762             * {@link AbstractFormModel#parentStateChanged(PropertyChangeEvent)}. This
763             * way subclasses can extend the parent-&gt;child behaviour meaning state
764             * changes in the parent that influence the children.
765             */
766            protected class ParentStateChangeHandler implements PropertyChangeListener {
767    
768                    public void propertyChange(PropertyChangeEvent evt) {
769                            parentStateChanged(evt);
770                    }
771            }
772    
773            /**
774             * Events from the parent form model that have side-effects on this form
775             * model should be handled here. This includes:
776             *
777             * <ul>
778             * <li><em>Enabled state:</em> when parent gets disabled, child should be
779             * disabled as well. If parent is enabled, child should go back to its
780             * original state.</li>
781             * <li><em>Read-only state:</em> when a parent is set read-only, child
782             * should be read-only as well. If parent is set editable, child should go
783             * back to its original state.</li>
784             * </ul>
785             */
786            protected void parentStateChanged(PropertyChangeEvent evt) {
787                    if (ENABLED_PROPERTY.equals(evt.getPropertyName())) {
788                            enabledUpdated();
789                    }
790                    else if (READONLY_PROPERTY.equals(evt.getPropertyName())) {
791                            readOnlyUpdated();
792                    }
793            }
794    
795            /**
796             * Listener to be registered on properties of child form models and other
797             * valueModels. Calls are delegated to
798             * {@link AbstractFormModel#childStateChanged(PropertyChangeEvent)}. This
799             * way subclasses can extend the child-&gt;parent behaviour meaning state
800             * changes in the child that influence the parent.
801             */
802            protected class ChildStateChangeHandler implements PropertyChangeListener {
803    
804                    public void propertyChange(PropertyChangeEvent evt) {
805                            childStateChanged(evt);
806                    }
807            }
808    
809            /**
810             * Events from the child form model or value models that have side-effects
811             * on this form model should be handled here. This includes:
812             *
813             * <ul>
814             * <li><em>Dirty state:</em> when a child is dirty, the parent should be
815             * dirty.</li>
816             * <li><em>Committable state:</em> when a child is committable, the
817             * parent might be committable as well. The committable state of the parent
818             * should be taken into account and revised when a child sends this event.</li>
819             * </ul>
820             *
821             * Note that we include value models and their metadata as being children.
822             * As these are low level models, they cannot be parents and therefore don't
823             * show up in
824             * {@link AbstractFormModel#parentStateChanged(PropertyChangeEvent)}.
825             */
826            protected void childStateChanged(PropertyChangeEvent evt) {
827                    if (FormModel.DIRTY_PROPERTY.equals(evt.getPropertyName())) {
828                            Object source = evt.getSource();
829    
830                            if (source instanceof FieldMetadata) {
831                                    FieldMetadata metadata = (FieldMetadata) source;
832                                    if (metadata.isDirty()) {
833                                            dirtyValueAndFormModels.add(metadata);
834                                    }
835                                    else {
836                                            dirtyValueAndFormModels.remove(metadata);
837                                    }
838                            }
839                            else if (source instanceof FormModel) {
840                                    FormModel formModel = (FormModel) source;
841                                    if (formModel.isDirty()) {
842                                            dirtyValueAndFormModels.add(formModel);
843                                    }
844                                    else {
845                                            dirtyValueAndFormModels.remove(formModel);
846                                    }
847                            }
848                            else {
849                                    DirtyTrackingValueModel valueModel = (DirtyTrackingValueModel) source;
850                                    if (valueModel.isDirty()) {
851                                            dirtyValueAndFormModels.add(valueModel);
852                                    }
853                                    else {
854                                            dirtyValueAndFormModels.remove(valueModel);
855                                    }
856                            }
857                            dirtyUpdated();
858                    }
859                    else if (COMMITTABLE_PROPERTY.equals(evt.getPropertyName())) {
860                            committableUpdated();
861                    }
862            }
863    
864            /**
865             * Class for keys in the convertingValueModels map.
866             */
867            protected static class ConvertingValueModelKey {
868    
869                    private final String propertyName;
870    
871                    private final Class targetClass;
872    
873                    public ConvertingValueModelKey(String propertyName, Class targetClass) {
874                            this.propertyName = propertyName;
875                            this.targetClass = targetClass;
876                    }
877    
878                    public boolean equals(Object o) {
879                            if (this == o)
880                                    return true;
881                            if (!(o instanceof ConvertingValueModelKey))
882                                    return false;
883    
884                            final ConvertingValueModelKey key = (ConvertingValueModelKey) o;
885                            return propertyName.equals(key.propertyName) && (targetClass == key.targetClass);
886                    }
887    
888                    public int hashCode() {
889                            return (propertyName.hashCode() * 29) + (targetClass == null ? 7 : targetClass.hashCode());
890                    }
891            }
892    }