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 }