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->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->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 }