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 &amp; 
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 &amp; 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    }