/**
* 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.valkyriercp.application.config.ApplicationConfig;
import org.valkyriercp.binding.form.FormModel;
import org.valkyriercp.binding.form.support.FormModelPropertyAccessStrategy;
import org.valkyriercp.binding.validation.RichValidator;
import org.valkyriercp.binding.validation.ValidationMessage;
import org.valkyriercp.core.Severity;
import org.valkyriercp.rules.PropertyConstraintProvider;
import org.valkyriercp.rules.Rules;
import org.valkyriercp.rules.RulesSource;
import org.valkyriercp.rules.constraint.property.PropertyConstraint;
import org.valkyriercp.rules.reporting.BeanValidationResultsCollector;
import org.valkyriercp.rules.reporting.MessageTranslator;
import org.valkyriercp.rules.reporting.ObjectNameResolver;
import org.valkyriercp.rules.reporting.PropertyResults;
import org.valkyriercp.util.ValkyrieRepository;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* <p>
* Implementation of a {@link RichValidator} which will check the formObject
* against rules found in a {@link RulesSource}. This {@link RulesSource} can
* be specifically supplied, which allows multiple rulesSources, or can be
* globally defined in the Application Context. In the latter case the
* {@link RulesValidator} will look for the specific {@link RulesSource} type in
* the context.
* </p>
*
* <p>
* When validating an object, all results are cached. Any following validation
* of a specific property will validate that property, update the cached results
* accordingly and return <em>all</em> validation results of the object.
* </p>
*
* @author Keith Donald
* @author Jan Hoskens
*/
public class RulesValidator implements RichValidator, ObjectNameResolver {
private static final Log logger = LogFactory.getLog(RulesValidator.class);
private final DefaultValidationResults results = new DefaultValidationResults();
private MessageTranslator messageTranslator;
private final Map validationErrors = new HashMap();
private final FormModel formModel;
private BeanValidationResultsCollector validationResultsCollector;
private RulesSource rulesSource;
private String rulesContextId = null;
private Class objectClass;
/**
* Creates a RulesValidator for the given formModel. When no RulesSource is
* given, a default/global RulesSource is retrieved by the
* ApplicationServices class.
*/
public RulesValidator(FormModel formModel) {
this(formModel, null);
}
/**
* Create a RulesValidator which uses the supplied RulesSource on the
* FormModel.
*/
public RulesValidator(FormModel formModel, RulesSource rulesSource) {
this.formModel = formModel;
this.rulesSource = rulesSource;
validationResultsCollector = new BeanValidationResultsCollector(new FormModelPropertyAccessStrategy(formModel));
messageTranslator = getApplicationConfig().messageTranslatorFactory().createTranslator(this);
}
/**
* {@inheritDoc}
*/
public org.valkyriercp.binding.validation.ValidationResults validate(Object object) {
return validate(object, null);
}
/**
* {@inheritDoc}
*/
public org.valkyriercp.binding.validation.ValidationResults validate(Object object, String propertyName) {
// Forms can have different types of objects, so when type of object
// changes, messages that are already listed on the previous type must
// be removed. If evaluating the whole object (propertyName == null)
// also clear results.
if ((propertyName == null) || ((objectClass != null) && objectClass != object.getClass())) {
clearMessages();
}
objectClass = object.getClass();
Rules rules = null;
if (object instanceof PropertyConstraintProvider) {
PropertyConstraintProvider propertyConstraintProvider = (PropertyConstraintProvider) object;
if (propertyName != null) {
PropertyConstraint validationRule = propertyConstraintProvider.getPropertyConstraint(propertyName);
checkRule(validationRule);
}
else {
for (Iterator fieldNamesIter = formModel.getFieldNames().iterator(); fieldNamesIter.hasNext();) {
PropertyConstraint validationRule = propertyConstraintProvider
.getPropertyConstraint((String) fieldNamesIter.next());
checkRule(validationRule);
}
}
}
else {
if (getRulesSource() != null) {
rules = getRulesSource().getRules(objectClass, getRulesContextId());
if (rules != null) {
for (Iterator i = rules.iterator(); i.hasNext();) {
PropertyConstraint validationRule = (PropertyConstraint) i.next();
if (propertyName == null) {
if (formModel.hasValueModel(validationRule.getPropertyName())) {
checkRule(validationRule);
}
}
else if (validationRule.isDependentOn(propertyName)) {
checkRule(validationRule);
}
}
}
}
else {
logger.debug("No rules source has been configured; "
+ "please set a valid reference to enable rules-based validation.");
}
}
return results;
}
private void checkRule(PropertyConstraint validationRule) {
if (validationRule == null)
return;
BeanValidationResultsCollector resultsCollector = takeResultsCollector();
PropertyResults results = resultsCollector.collectPropertyResults(validationRule);
returnResultsCollector(resultsCollector);
if (results == null) {
constraintSatisfied(validationRule);
}
else {
constraintViolated(validationRule, results);
}
}
protected void constraintSatisfied(PropertyConstraint exp) {
ValidationMessage message = (ValidationMessage) validationErrors.remove(exp);
if (message != null) {
results.removeMessage(message);
}
}
protected void constraintViolated(PropertyConstraint exp, PropertyResults propertyResults) {
ValidationMessage message = new DefaultValidationMessage(exp.getPropertyName(), Severity.ERROR,
messageTranslator.getMessage(propertyResults));
ValidationMessage oldMessage = (ValidationMessage) validationErrors.get(exp);
if (!message.equals(oldMessage)) {
results.removeMessage(oldMessage);
validationErrors.put(exp, message);
results.addMessage(message);
}
}
private RulesSource getRulesSource() {
if (rulesSource == null) {
rulesSource = getApplicationConfig().rulesSource();
}
return rulesSource;
}
private BeanValidationResultsCollector takeResultsCollector() {
BeanValidationResultsCollector resultsCollector = validationResultsCollector;
if (resultsCollector != null) {
validationResultsCollector = null;
}
else {
resultsCollector = new BeanValidationResultsCollector(new FormModelPropertyAccessStrategy(formModel));
}
return resultsCollector;
}
private void returnResultsCollector(BeanValidationResultsCollector resultsCollector) {
validationResultsCollector = resultsCollector;
}
/**
* Returns the rules context id set on this validator.
*/
public String getRulesContextId() {
return rulesContextId;
}
/**
* Set the rules context id. This is passed in the call to
* {@link RulesSource#getRules(Class, String)} to allow for context specific
* rules.
* @param rulesContextId
*/
public void setRulesContextId(String rulesContextId) {
this.rulesContextId = rulesContextId;
}
/**
* {@inheritDoc}
*/
public String resolveObjectName(String objectName) {
return formModel.getFieldFace(objectName).getDisplayName();
}
/**
* Clear the current validationMessages and the errors.
*
* @see #validate(Object, String)
*/
public void clearMessages() {
this.results.clearMessages();
this.validationErrors.clear();
}
public ApplicationConfig getApplicationConfig() {
return ValkyrieRepository.getInstance().getApplicationConfig();
}
}