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 }