/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.binding.validation.support;
import org.springframework.binding.collection.AbstractCachingMapDecorator;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.valkyriercp.binding.validation.ValidationListener;
import org.valkyriercp.binding.validation.ValidationMessage;
import org.valkyriercp.binding.validation.ValidationResults;
import org.valkyriercp.binding.validation.ValidationResultsModel;
import org.valkyriercp.core.Severity;
import org.valkyriercp.util.EventListenerListHelper;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
/**
* Default implementation of {@link ValidationResultsModel}. Several events are
* fired when validationResults are set and can be tracked by registering the
* appropriate listener.
*
* <p>
* You can register listeners on:
* </p>
* <ul>
* <li>Changes of the ValidationResults in general. ({@link #addValidationListener(ValidationListener)})</li>
* <li>Changes of validationResults concerning a specific property of the
* FormModel. ({@link #addValidationListener(String, ValidationListener)})</li>
* <li>Specific events concerning errors, warnings and info. ({@link #addPropertyChangeListener(String, java.beans.PropertyChangeListener) with one of:
* ValidationResultsModel#HAS_ERRORS_PROPERTY,
* ValidationResultsModel#HAS_INFO_PROPERTY or
* ValidationResultsModel#HAS_WARNINGS_PROPERTY)</li>
* </ul>
*
* <p>
* A child-parent relation can be used to bundle events and results. A listener
* set on a parent will receive events originating from the child and when
* polling for messages, childMessages will be available as well. This makes it
* possible to efficiently couple formModels and their validation aspect and
* provides a means to bundle validation reporting. When eg using a
* {@link org.springframework.richclient.form.ValidationResultsReporter}, you have the opportunity to bundle
* results from various unrelated formModels to report to one end point.</p>
*
* <p>Example:</p>
* <pre>
* DefaultFormModel formModelA = ...
* DefaultFormModel formModelChildOfA = ...
* formModelA.addChild(formModelChildOfA);
*
* DefaultFormModel formModelB = ...
*
* \\ At this stage, the ValidationResultsModel of formModelChildOfA will route results &
* \\ events to the ValidationResultsModel of formModelA
*
* DefaultValidationResultsModel container = new DefaultValidationResultsModel();
* container.add(formModelA.getValidationResults());
* container.add(formModelB.getValidationResults());
*
* new SimpleValidationResultsReporter(container, messagable);
*
* \\ the reporter will now receive events & results of all formModels and can show messages of each of them
*
* </pre>
*
* @see org.springframework.binding.form.support.DefaultFormModel#addChild(org.springframework.binding.form.HierarchicalFormModel)
* @see org.springframework.richclient.form.SimpleValidationResultsReporter
*
* @author Oliver Hutchison
* @author Jan Hoskens
*/
public class DefaultValidationResultsModel implements ValidationResultsModel, ValidationListener,
PropertyChangeListener {
private final EventListenerListHelper validationListeners = new EventListenerListHelper(ValidationListener.class);
private final AbstractCachingMapDecorator propertyValidationListeners = new AbstractCachingMapDecorator() {
protected Object create(Object propertyName) {
return new EventListenerListHelper(ValidationListener.class);
}
};
private final AbstractCachingMapDecorator propertyChangeListeners = new AbstractCachingMapDecorator() {
protected Object create(Object propertyName) {
return new EventListenerListHelper(PropertyChangeListener.class);
}
};
/** Delegate or reference to this. */
private final ValidationResultsModel delegateFor;
/** All children connected to this {@link DefaultValidationResultsModel}. */
private List children = new ArrayList();
/** The actual results for this instance only. */
private ValidationResults validationResults = EmptyValidationResults.INSTANCE;
/** Error bookkeeping. */
private boolean hasErrors = false;
/** Warning bookkeeping. */
private boolean hasWarnings = false;
/** Info bookkeeping. */
private boolean hasInfo = false;
/**
* Constructor without delegate. (Delegating for 'this').
*/
public DefaultValidationResultsModel() {
delegateFor = this;
}
/**
* Constructor with delegate.
*
* @param delegateFor delegate object.
*/
public DefaultValidationResultsModel(ValidationResultsModel delegateFor) {
this.delegateFor = delegateFor;
}
public void updateValidationResults(ValidationResults newValidationResults) {
Assert.notNull(newValidationResults, "newValidationResults");
ValidationResults oldValidationResults = validationResults;
validationResults = newValidationResults;
if (oldValidationResults.getMessageCount() == 0 && validationResults.getMessageCount() == 0) {
return;
}
fireChangedEvents();
for (Iterator i = propertyValidationListeners.keySet().iterator(); i.hasNext();) {
String propertyName = (String) i.next();
if (oldValidationResults.getMessageCount(propertyName) > 0
|| validationResults.getMessageCount(propertyName) > 0) {
fireValidationResultsChanged(propertyName);
}
}
}
// TODO: test
public void addMessage(ValidationMessage validationMessage) {
if (!validationResults.getMessages().contains(validationMessage)) {
ValidationResults oldValidationResults = validationResults;
List newMessages = new ArrayList(oldValidationResults.getMessages());
newMessages.add(validationMessage);
validationResults = new DefaultValidationResults(newMessages);
fireChangedEvents();
fireValidationResultsChanged(validationMessage.getProperty());
}
}
// TODO: test
public void removeMessage(ValidationMessage validationMessage) {
if (validationResults.getMessages().contains(validationMessage)) {
ValidationResults oldValidationResults = validationResults;
List newMessages = new ArrayList(oldValidationResults.getMessages());
newMessages.remove(validationMessage);
validationResults = new DefaultValidationResults(newMessages);
fireChangedEvents();
fireValidationResultsChanged(validationMessage.getProperty());
}
}
// TODO: test
public void replaceMessage(ValidationMessage messageToReplace, ValidationMessage replacementMessage) {
ValidationResults oldValidationResults = validationResults;
List newMessages = new ArrayList(oldValidationResults.getMessages());
final boolean containsMessageToReplace = validationResults.getMessages().contains(messageToReplace);
if (containsMessageToReplace) {
newMessages.remove(messageToReplace);
}
newMessages.add(replacementMessage);
validationResults = new DefaultValidationResults(newMessages);
fireChangedEvents();
if (containsMessageToReplace
&& !ObjectUtils.nullSafeEquals(messageToReplace.getProperty(), replacementMessage.getProperty())) {
fireValidationResultsChanged(messageToReplace.getProperty());
}
fireValidationResultsChanged(replacementMessage.getProperty());
}
public void clearAllValidationResults() {
updateValidationResults(EmptyValidationResults.INSTANCE);
}
/**
* @return <code>true</code> if this instance of one of its children has
* errors contained in their results.
*/
public boolean getHasErrors() {
return hasErrors;
}
/**
* Revaluate the hasErrors property and fire an event if things have
* changed.
*/
private void updateErrors() {
boolean oldErrors = hasErrors;
hasErrors = false;
if (validationResults.getHasErrors()) {
hasErrors = true;
}
else {
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
if (childModel.getHasErrors()) {
hasErrors = true;
break;
}
}
}
firePropertyChange(HAS_ERRORS_PROPERTY, oldErrors, hasErrors);
}
/**
* @return <code>true</code> if this instance of one of its children has
* info contained in their results.
*/
public boolean getHasInfo() {
return hasInfo;
}
/**
* Revaluate the hasInfo property and fire an event if things have changed.
*/
private void updateInfo() {
boolean oldInfo = hasInfo;
hasInfo = false;
if (validationResults.getHasInfo()) {
hasInfo = true;
}
else {
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
if (childModel.getHasInfo()) {
hasInfo = true;
break;
}
}
}
firePropertyChange(HAS_INFO_PROPERTY, oldInfo, hasInfo);
}
/**
* @return <code>true</code> if this instance of one of its children has
* warnings contained in their results.
*/
public boolean getHasWarnings() {
return hasWarnings;
}
/**
* Revaluate the hasWarnings property and fire an event if things have
* changed.
*/
private void updateWarnings() {
boolean oldWarnings = hasWarnings;
hasWarnings = false;
if (validationResults.getHasWarnings()) {
hasWarnings = true;
}
else {
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
if (childModel.getHasWarnings()) {
hasWarnings = true;
break;
}
}
}
firePropertyChange(HAS_WARNINGS_PROPERTY, oldWarnings, hasWarnings);
}
public int getMessageCount() {
int count = validationResults.getMessageCount();
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
count += childModel.getMessageCount();
}
return count;
}
public int getMessageCount(Severity severity) {
int count = validationResults.getMessageCount(severity);
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
count += childModel.getMessageCount(severity);
}
return count;
}
public int getMessageCount(String propertyName) {
int count = validationResults.getMessageCount(propertyName);
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
count += childModel.getMessageCount(propertyName);
}
return count;
}
public Set getMessages() {
Set messages = new HashSet();
messages.addAll(validationResults.getMessages());
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
messages.addAll(childModel.getMessages());
}
return messages;
}
public Set getMessages(Severity severity) {
Set messages = new HashSet();
messages.addAll(validationResults.getMessages(severity));
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
messages.addAll(childModel.getMessages(severity));
}
return messages;
}
public Set getMessages(String propertyName) {
Set messages = new HashSet();
messages.addAll(validationResults.getMessages(propertyName));
Iterator childIter = children.iterator();
while (childIter.hasNext()) {
ValidationResultsModel childModel = (ValidationResultsModel) childIter.next();
messages.addAll(childModel.getMessages(propertyName));
}
return messages;
}
public void addValidationListener(ValidationListener listener) {
validationListeners.add(listener);
}
public void removeValidationListener(ValidationListener listener) {
validationListeners.remove(listener);
}
public void addValidationListener(String propertyName, ValidationListener listener) {
getValidationListeners(propertyName).add(listener);
}
public void removeValidationListener(String propertyName, ValidationListener listener) {
getValidationListeners(propertyName).remove(listener);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
throw new UnsupportedOperationException("This method is not implemented");
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
throw new UnsupportedOperationException("This method is not implemented");
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
getPropertyChangeListeners(propertyName).add(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
getPropertyChangeListeners(propertyName).remove(listener);
}
protected void fireChangedEvents() {
updateErrors();
updateWarnings();
updateInfo();
fireValidationResultsChanged();
}
protected void fireValidationResultsChanged() {
validationListeners.fire("validationResultsChanged", delegateFor);
}
protected void fireValidationResultsChanged(String propertyName) {
for (Iterator i = getValidationListeners(propertyName).iterator(); i.hasNext();) {
((ValidationListener) i.next()).validationResultsChanged(delegateFor);
}
}
protected EventListenerListHelper getValidationListeners(String propertyName) {
return ((EventListenerListHelper) propertyValidationListeners.get(propertyName));
}
protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
if (oldValue != newValue) {
EventListenerListHelper propertyChangeListeners = getPropertyChangeListeners(propertyName);
if (propertyChangeListeners.hasListeners()) {
PropertyChangeEvent event = new PropertyChangeEvent(delegateFor, propertyName, Boolean
.valueOf(oldValue), Boolean.valueOf(newValue));
propertyChangeListeners.fire("propertyChange", event);
}
}
}
protected EventListenerListHelper getPropertyChangeListeners(String propertyName) {
return ((EventListenerListHelper) propertyChangeListeners.get(propertyName));
}
public String toString() {
return new ToStringCreator(this).append("messages", getMessages()).toString();
}
/**
* Add a validationResultsModel as a child to this one. Attach listeners and
* if it already has messages, fire events.
*
* @param validationResultsModel
*/
public void add(ValidationResultsModel validationResultsModel) {
if (children.add(validationResultsModel)) {
validationResultsModel.addValidationListener(this);
validationResultsModel.addPropertyChangeListener(HAS_ERRORS_PROPERTY, this);
validationResultsModel.addPropertyChangeListener(HAS_WARNINGS_PROPERTY, this);
validationResultsModel.addPropertyChangeListener(HAS_INFO_PROPERTY, this);
if ((validationResultsModel.getMessageCount() > 0))
fireChangedEvents();
}
}
/**
* Remove the given validationResultsModel from the list of children. Remove
* listeners and if it had messages, fire events.
*
* @param validationResultsModel
*/
public void remove(ValidationResultsModel validationResultsModel) {
if (children.remove(validationResultsModel)) {
validationResultsModel.removeValidationListener(this);
validationResultsModel.removePropertyChangeListener(HAS_ERRORS_PROPERTY, this);
validationResultsModel.removePropertyChangeListener(HAS_WARNINGS_PROPERTY, this);
validationResultsModel.removePropertyChangeListener(HAS_INFO_PROPERTY, this);
if (validationResultsModel.getMessageCount() > 0)
fireChangedEvents();
}
}
/**
* {@link DefaultValidationResultsModel} registers itself as a
* validationListener on it's children to forward the event.
*/
public void validationResultsChanged(ValidationResults results) {
fireValidationResultsChanged();
}
/**
* Forwarding of known property events coming from child models. Each event
* triggers a specific evaluation of the parent property, which will trigger
* events as needed.
*/
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName() == HAS_ERRORS_PROPERTY)
updateErrors();
else if (evt.getPropertyName() == HAS_WARNINGS_PROPERTY)
updateWarnings();
else if (evt.getPropertyName() == HAS_INFO_PROPERTY)
updateInfo();
}
}