001 /* 002 * Copyright 2002-2005 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of 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, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.springframework.binding.validation.support; 017 018 import java.beans.PropertyChangeEvent; 019 import java.beans.PropertyChangeListener; 020 import java.util.ArrayList; 021 import java.util.HashSet; 022 import java.util.Iterator; 023 import java.util.List; 024 import java.util.Set; 025 026 import org.springframework.binding.validation.ValidationListener; 027 import org.springframework.binding.validation.ValidationMessage; 028 import org.springframework.binding.validation.ValidationResults; 029 import org.springframework.binding.validation.ValidationResultsModel; 030 import org.springframework.core.style.ToStringCreator; 031 import org.springframework.richclient.core.Severity; 032 import org.springframework.richclient.util.Assert; 033 import org.springframework.richclient.util.EventListenerListHelper; 034 import org.springframework.util.CachingMapDecorator; 035 import org.springframework.util.ObjectUtils; 036 037 /** 038 * Default implementation of {@link ValidationResultsModel}. Several events are 039 * fired when validationResults are set and can be tracked by registering the 040 * appropriate listener. 041 * 042 * <p> 043 * You can register listeners on: 044 * </p> 045 * <ul> 046 * <li>Changes of the ValidationResults in general. ({@link #addValidationListener(ValidationListener)})</li> 047 * <li>Changes of validationResults concerning a specific property of the 048 * FormModel. ({@link #addValidationListener(String, ValidationListener)})</li> 049 * <li>Specific events concerning errors, warnings and info. ({@link #addPropertyChangeListener(String, PropertyChangeListener) with one of: 050 * ValidationResultsModel#HAS_ERRORS_PROPERTY, 051 * ValidationResultsModel#HAS_INFO_PROPERTY or 052 * ValidationResultsModel#HAS_WARNINGS_PROPERTY)</li> 053 * </ul> 054 * 055 * <p> 056 * A child-parent relation can be used to bundle events and results. A listener 057 * set on a parent will receive events originating from the child and when 058 * polling for messages, childMessages will be available as well. This makes it 059 * possible to efficiently couple formModels and their validation aspect and 060 * provides a means to bundle validation reporting. When eg using a 061 * {@link org.springframework.richclient.form.ValidationResultsReporter}, you have the opportunity to bundle 062 * results from various unrelated formModels to report to one end point.</p> 063 * 064 * <p>Example:</p> 065 * <pre> 066 * DefaultFormModel formModelA = ... 067 * DefaultFormModel formModelChildOfA = ... 068 * formModelA.addChild(formModelChildOfA); 069 * 070 * DefaultFormModel formModelB = ... 071 * 072 * \\ At this stage, the ValidationResultsModel of formModelChildOfA will route results & 073 * \\ events to the ValidationResultsModel of formModelA 074 * 075 * DefaultValidationResultsModel container = new DefaultValidationResultsModel(); 076 * container.add(formModelA.getValidationResults()); 077 * container.add(formModelB.getValidationResults()); 078 * 079 * new SimpleValidationResultsReporter(container, messagable); 080 * 081 * \\ the reporter will now receive events & results of all formModels and can show messages of each of them 082 * 083 * </pre> 084 * 085 * @see org.springframework.binding.form.support.DefaultFormModel#addChild(org.springframework.binding.form.HierarchicalFormModel) 086 * @see org.springframework.richclient.form.SimpleValidationResultsReporter 087 * 088 * @author Oliver Hutchison 089 * @author Jan Hoskens 090 */ 091 public class DefaultValidationResultsModel implements ValidationResultsModel, ValidationListener, 092 PropertyChangeListener { 093 094 private final EventListenerListHelper validationListeners = new EventListenerListHelper(ValidationListener.class); 095 096 private final CachingMapDecorator propertyValidationListeners = new CachingMapDecorator() { 097 098 protected Object create(Object propertyName) { 099 return new EventListenerListHelper(ValidationListener.class); 100 } 101 }; 102 103 private final CachingMapDecorator propertyChangeListeners = new CachingMapDecorator() { 104 105 protected Object create(Object propertyName) { 106 return new EventListenerListHelper(PropertyChangeListener.class); 107 } 108 }; 109 110 /** Delegate or reference to this. */ 111 private final ValidationResultsModel delegateFor; 112 113 /** All children connected to this {@link DefaultValidationResultsModel}. */ 114 private List children = new ArrayList(); 115 116 /** The actual results for this instance only. */ 117 private ValidationResults validationResults = EmptyValidationResults.INSTANCE; 118 119 /** Error bookkeeping. */ 120 private boolean hasErrors = false; 121 122 /** Warning bookkeeping. */ 123 private boolean hasWarnings = false; 124 125 /** Info bookkeeping. */ 126 private boolean hasInfo = false; 127 128 /** 129 * Constructor without delegate. (Delegating for 'this'). 130 */ 131 public DefaultValidationResultsModel() { 132 delegateFor = this; 133 } 134 135 /** 136 * Constructor with delegate. 137 * 138 * @param delegateFor delegate object. 139 */ 140 public DefaultValidationResultsModel(ValidationResultsModel delegateFor) { 141 this.delegateFor = delegateFor; 142 } 143 144 public void updateValidationResults(ValidationResults newValidationResults) { 145 Assert.required(newValidationResults, "newValidationResults"); 146 ValidationResults oldValidationResults = validationResults; 147 validationResults = newValidationResults; 148 if (oldValidationResults.getMessageCount() == 0 && validationResults.getMessageCount() == 0) { 149 return; 150 } 151 fireChangedEvents(); 152 for (Iterator i = propertyValidationListeners.keySet().iterator(); i.hasNext();) { 153 String propertyName = (String) i.next(); 154 if (oldValidationResults.getMessageCount(propertyName) > 0 155 || validationResults.getMessageCount(propertyName) > 0) { 156 fireValidationResultsChanged(propertyName); 157 } 158 } 159 } 160 161 // TODO: test 162 public void addMessage(ValidationMessage validationMessage) { 163 if (!validationResults.getMessages().contains(validationMessage)) { 164 ValidationResults oldValidationResults = validationResults; 165 List newMessages = new ArrayList(oldValidationResults.getMessages()); 166 newMessages.add(validationMessage); 167 validationResults = new DefaultValidationResults(newMessages); 168 fireChangedEvents(); 169 fireValidationResultsChanged(validationMessage.getProperty()); 170 } 171 } 172 173 // TODO: test 174 public void removeMessage(ValidationMessage validationMessage) { 175 if (validationResults.getMessages().contains(validationMessage)) { 176 ValidationResults oldValidationResults = validationResults; 177 List newMessages = new ArrayList(oldValidationResults.getMessages()); 178 newMessages.remove(validationMessage); 179 validationResults = new DefaultValidationResults(newMessages); 180 fireChangedEvents(); 181 fireValidationResultsChanged(validationMessage.getProperty()); 182 } 183 } 184 185 // TODO: test 186 public void replaceMessage(ValidationMessage messageToReplace, ValidationMessage replacementMessage) { 187 ValidationResults oldValidationResults = validationResults; 188 List newMessages = new ArrayList(oldValidationResults.getMessages()); 189 final boolean containsMessageToReplace = validationResults.getMessages().contains(messageToReplace); 190 if (containsMessageToReplace) { 191 newMessages.remove(messageToReplace); 192 } 193 newMessages.add(replacementMessage); 194 validationResults = new DefaultValidationResults(newMessages); 195 fireChangedEvents(); 196 if (containsMessageToReplace 197 && !ObjectUtils.nullSafeEquals(messageToReplace.getProperty(), replacementMessage.getProperty())) { 198 fireValidationResultsChanged(messageToReplace.getProperty()); 199 } 200 fireValidationResultsChanged(replacementMessage.getProperty()); 201 } 202 203 public void clearAllValidationResults() { 204 updateValidationResults(EmptyValidationResults.INSTANCE); 205 } 206 207 /** 208 * @return <code>true</code> if this instance of one of its children has 209 * errors contained in their results. 210 */ 211 public boolean getHasErrors() { 212 return hasErrors; 213 } 214 215 /** 216 * Revaluate the hasErrors property and fire an event if things have 217 * changed. 218 */ 219 private void updateErrors() { 220 boolean oldErrors = hasErrors; 221 hasErrors = false; 222 if (validationResults.getHasErrors()) { 223 hasErrors = true; 224 } 225 else { 226 Iterator childIter = children.iterator(); 227 while (childIter.hasNext()) { 228 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 229 if (childModel.getHasErrors()) { 230 hasErrors = true; 231 break; 232 } 233 } 234 } 235 firePropertyChange(HAS_ERRORS_PROPERTY, oldErrors, hasErrors); 236 } 237 238 /** 239 * @return <code>true</code> if this instance of one of its children has 240 * info contained in their results. 241 */ 242 public boolean getHasInfo() { 243 return hasInfo; 244 } 245 246 /** 247 * Revaluate the hasInfo property and fire an event if things have changed. 248 */ 249 private void updateInfo() { 250 boolean oldInfo = hasInfo; 251 hasInfo = false; 252 if (validationResults.getHasInfo()) { 253 hasInfo = true; 254 } 255 else { 256 Iterator childIter = children.iterator(); 257 while (childIter.hasNext()) { 258 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 259 if (childModel.getHasInfo()) { 260 hasInfo = true; 261 break; 262 } 263 } 264 } 265 firePropertyChange(HAS_INFO_PROPERTY, oldInfo, hasInfo); 266 } 267 268 /** 269 * @return <code>true</code> if this instance of one of its children has 270 * warnings contained in their results. 271 */ 272 public boolean getHasWarnings() { 273 return hasWarnings; 274 } 275 276 /** 277 * Revaluate the hasWarnings property and fire an event if things have 278 * changed. 279 */ 280 private void updateWarnings() { 281 boolean oldWarnings = hasWarnings; 282 hasWarnings = false; 283 if (validationResults.getHasWarnings()) { 284 hasWarnings = true; 285 } 286 else { 287 Iterator childIter = children.iterator(); 288 while (childIter.hasNext()) { 289 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 290 if (childModel.getHasWarnings()) { 291 hasWarnings = true; 292 break; 293 } 294 } 295 } 296 firePropertyChange(HAS_WARNINGS_PROPERTY, oldWarnings, hasWarnings); 297 } 298 299 public int getMessageCount() { 300 int count = validationResults.getMessageCount(); 301 Iterator childIter = children.iterator(); 302 while (childIter.hasNext()) { 303 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 304 count += childModel.getMessageCount(); 305 } 306 return count; 307 } 308 309 public int getMessageCount(Severity severity) { 310 int count = validationResults.getMessageCount(severity); 311 Iterator childIter = children.iterator(); 312 while (childIter.hasNext()) { 313 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 314 count += childModel.getMessageCount(severity); 315 } 316 return count; 317 } 318 319 public int getMessageCount(String propertyName) { 320 int count = validationResults.getMessageCount(propertyName); 321 Iterator childIter = children.iterator(); 322 while (childIter.hasNext()) { 323 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 324 count += childModel.getMessageCount(propertyName); 325 } 326 return count; 327 } 328 329 public Set getMessages() { 330 Set messages = new HashSet(); 331 messages.addAll(validationResults.getMessages()); 332 Iterator childIter = children.iterator(); 333 while (childIter.hasNext()) { 334 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 335 messages.addAll(childModel.getMessages()); 336 } 337 return messages; 338 } 339 340 public Set getMessages(Severity severity) { 341 Set messages = new HashSet(); 342 messages.addAll(validationResults.getMessages(severity)); 343 Iterator childIter = children.iterator(); 344 while (childIter.hasNext()) { 345 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 346 messages.addAll(childModel.getMessages(severity)); 347 } 348 return messages; 349 } 350 351 public Set getMessages(String propertyName) { 352 Set messages = new HashSet(); 353 messages.addAll(validationResults.getMessages(propertyName)); 354 Iterator childIter = children.iterator(); 355 while (childIter.hasNext()) { 356 ValidationResultsModel childModel = (ValidationResultsModel) childIter.next(); 357 messages.addAll(childModel.getMessages(propertyName)); 358 } 359 return messages; 360 } 361 362 public void addValidationListener(ValidationListener listener) { 363 validationListeners.add(listener); 364 } 365 366 public void removeValidationListener(ValidationListener listener) { 367 validationListeners.remove(listener); 368 } 369 370 public void addValidationListener(String propertyName, ValidationListener listener) { 371 getValidationListeners(propertyName).add(listener); 372 } 373 374 public void removeValidationListener(String propertyName, ValidationListener listener) { 375 getValidationListeners(propertyName).remove(listener); 376 } 377 378 public void addPropertyChangeListener(PropertyChangeListener listener) { 379 throw new UnsupportedOperationException("This method is not implemented"); 380 } 381 382 public void removePropertyChangeListener(PropertyChangeListener listener) { 383 throw new UnsupportedOperationException("This method is not implemented"); 384 } 385 386 public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { 387 getPropertyChangeListeners(propertyName).add(listener); 388 } 389 390 public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { 391 getPropertyChangeListeners(propertyName).remove(listener); 392 } 393 394 protected void fireChangedEvents() { 395 updateErrors(); 396 updateWarnings(); 397 updateInfo(); 398 fireValidationResultsChanged(); 399 } 400 401 protected void fireValidationResultsChanged() { 402 validationListeners.fire("validationResultsChanged", delegateFor); 403 } 404 405 protected void fireValidationResultsChanged(String propertyName) { 406 for (Iterator i = getValidationListeners(propertyName).iterator(); i.hasNext();) { 407 ((ValidationListener) i.next()).validationResultsChanged(delegateFor); 408 } 409 } 410 411 protected EventListenerListHelper getValidationListeners(String propertyName) { 412 return ((EventListenerListHelper) propertyValidationListeners.get(propertyName)); 413 } 414 415 protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { 416 if (oldValue != newValue) { 417 EventListenerListHelper propertyChangeListeners = getPropertyChangeListeners(propertyName); 418 if (propertyChangeListeners.hasListeners()) { 419 PropertyChangeEvent event = new PropertyChangeEvent(delegateFor, propertyName, Boolean 420 .valueOf(oldValue), Boolean.valueOf(newValue)); 421 propertyChangeListeners.fire("propertyChange", event); 422 } 423 } 424 } 425 426 protected EventListenerListHelper getPropertyChangeListeners(String propertyName) { 427 return ((EventListenerListHelper) propertyChangeListeners.get(propertyName)); 428 } 429 430 public String toString() { 431 return new ToStringCreator(this).append("messages", getMessages()).toString(); 432 } 433 434 /** 435 * Add a validationResultsModel as a child to this one. Attach listeners and 436 * if it already has messages, fire events. 437 * 438 * @param validationResultsModel 439 */ 440 public void add(ValidationResultsModel validationResultsModel) { 441 if (children.add(validationResultsModel)) { 442 validationResultsModel.addValidationListener(this); 443 validationResultsModel.addPropertyChangeListener(HAS_ERRORS_PROPERTY, this); 444 validationResultsModel.addPropertyChangeListener(HAS_WARNINGS_PROPERTY, this); 445 validationResultsModel.addPropertyChangeListener(HAS_INFO_PROPERTY, this); 446 if ((validationResultsModel.getMessageCount() > 0)) 447 fireChangedEvents(); 448 } 449 } 450 451 /** 452 * Remove the given validationResultsModel from the list of children. Remove 453 * listeners and if it had messages, fire events. 454 * 455 * @param validationResultsModel 456 */ 457 public void remove(ValidationResultsModel validationResultsModel) { 458 if (children.remove(validationResultsModel)) { 459 validationResultsModel.removeValidationListener(this); 460 validationResultsModel.removePropertyChangeListener(HAS_ERRORS_PROPERTY, this); 461 validationResultsModel.removePropertyChangeListener(HAS_WARNINGS_PROPERTY, this); 462 validationResultsModel.removePropertyChangeListener(HAS_INFO_PROPERTY, this); 463 if (validationResultsModel.getMessageCount() > 0) 464 fireChangedEvents(); 465 } 466 } 467 468 /** 469 * {@link DefaultValidationResultsModel} registers itself as a 470 * validationListener on it's children to forward the event. 471 */ 472 public void validationResultsChanged(ValidationResults results) { 473 fireValidationResultsChanged(); 474 } 475 476 /** 477 * Forwarding of known property events coming from child models. Each event 478 * triggers a specific evaluation of the parent property, which will trigger 479 * events as needed. 480 */ 481 public void propertyChange(PropertyChangeEvent evt) { 482 if (evt.getPropertyName() == HAS_ERRORS_PROPERTY) 483 updateErrors(); 484 else if (evt.getPropertyName() == HAS_WARNINGS_PROPERTY) 485 updateWarnings(); 486 else if (evt.getPropertyName() == HAS_INFO_PROPERTY) 487 updateInfo(); 488 } 489 }