/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.utils.validation;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import nl.strohalm.cyclos.utils.PropertyHelper;
import nl.strohalm.cyclos.utils.RangeConstraint;
import org.apache.commons.lang.StringUtils;
/**
* This class has the information needed to validate an object. Common validation rules, like required and length constraints are handled out of the
* box. Other validations should be done with an external implementation of the Validation interface. When the constructor with a baseName is called,
* properties will have a default key "baseName.propertyName"
* @author luis
*/
public class Validator implements Serializable {
/**
* Bean style property retrieving strategy
* @author luis
*/
public static class BeanPropertyRetrieveStrategy implements PropertyRetrieveStrategy {
private static final long serialVersionUID = -5276405909672992884L;
private final String name;
public BeanPropertyRetrieveStrategy(final String name) {
this.name = name;
}
@Override
public Object description(final Object object, final String name) {
return name;
}
@Override
public Object get(final Object object) {
return PropertyHelper.get(object, name);
}
}
/**
* Describes a property validation
* @author luis
*/
public class Property implements Serializable {
private static final long serialVersionUID = -6974004392652790404L;
private final String name;
private String displayName;
private String key;
private final List<PropertyValidation> validations = new ArrayList<PropertyValidation>();
private final PropertyRetrieveStrategy retrieveStrategy;
private Property(final String name) {
this(name, new BeanPropertyRetrieveStrategy(name));
}
private Property(final String name, final PropertyRetrieveStrategy retrieveStrategy) {
this.name = name;
this.retrieveStrategy = retrieveStrategy;
if (StringUtils.isNotEmpty(baseName)) {
key(baseName + "." + name);
}
}
public Property add(final PropertyValidation... validations) {
if (validations != null) {
for (final PropertyValidation validation : validations) {
this.validations.add(validation);
}
}
return this;
}
public Property anyOf(final Collection<?> values) {
return add(new AnyOfValidation(values));
}
public Property anyOf(final Object... values) {
return add(new AnyOfValidation(values));
}
public Property between(final Number from, final Number to) {
return between(from, to, true, true);
}
public Property between(final Number from, final Number to, final boolean includeLowerBound, final boolean includeUpperBound) {
if (includeLowerBound) {
greaterEquals(from);
} else {
greaterThan(from);
}
if (includeUpperBound) {
return lessEquals(to);
} else {
return lessThan(to);
}
}
public Property comparable(final Comparable<?> comparable, final String operation) {
return comparable(comparable, operation, null);
}
public Property comparable(final Comparable<?> comparable, final String operation, final ValidationError error) {
final boolean acceptNegative = Arrays.asList("<", "<=").contains(operation);
final boolean acceptZero = Arrays.asList("<=", "=", ">=").contains(operation);
final boolean acceptPositive = Arrays.asList(">", ">=").contains(operation);
return add(new CompareToValidation(comparable, acceptNegative, acceptZero, acceptPositive, error));
}
public Property displayName(final String displayName) {
this.displayName = displayName;
key = null;
return this;
}
public Property fixedLength(final int length) {
return length(length, length);
}
public Property future() {
return add(TodayValidation.future());
}
public Property futureOrToday() {
return add(TodayValidation.futureOrToday());
}
public String getDisplayName() {
return displayName;
}
public String getKey() {
return key;
}
public String getName() {
return name;
}
public PropertyRetrieveStrategy getRetrieveStrategy() {
return retrieveStrategy;
}
public List<PropertyValidation> getValidations() {
return validations;
}
public Property greaterEquals(final Number number) {
return add(CompareToValidation.greaterEquals((Comparable<?>) number));
}
public Property greaterThan(final Number number) {
return add(CompareToValidation.greaterThan((Comparable<?>) number));
}
public Property inetAddr() {
return add(InetAddrValidation.instance());
}
public Property instanceOf(final Class<?> expectedType) {
return add(new InstanceOfValidation(expectedType));
}
public Property key(final String key) {
this.key = key;
displayName = null;
return this;
}
public Property length(final int from, final int to) {
return add(new LengthValidation(RangeConstraint.between(from, to)));
}
public Property lessEquals(final Number number) {
return add(CompareToValidation.lessEquals((Comparable<?>) number));
}
public Property lessThan(final Number number) {
return add(CompareToValidation.lessThan((Comparable<?>) number));
}
public Property maxLength(final int maxLength) {
return add(new LengthValidation(RangeConstraint.to(maxLength)));
}
public Property minLength(final int minLength) {
return add(new LengthValidation(RangeConstraint.from(minLength)));
}
public Property noneOf(final Collection<?> values) {
return add(new NoneOfValidation(values));
}
public Property noneOf(final Object... values) {
return add(new NoneOfValidation(values));
}
public Property numeric() {
return add(NumericValidation.instance());
}
public Property past() {
return add(TodayValidation.past());
}
public Property pastOrToday() {
return add(TodayValidation.pastOrToday());
}
public Property positive() {
return add(PositiveValidation.instance());
}
public Property positiveNonZero() {
return add(PositiveNonZeroValidation.instance());
}
public Property regex(final String regex) {
return add(new RegexValidation(regex));
}
public Property required() {
return add(RequiredValidation.instance());
}
public Property url() {
return url(false);
}
public Property url(final boolean requireDotOnHostname) {
return add(URLValidation.instance(requireDotOnHostname));
}
}
/**
* Strategy for retrieving a property
* @author luis
*/
public static interface PropertyRetrieveStrategy extends Serializable {
/**
* Returns a property description
*/
Object description(Object object, String name);
/**
* Returns the property value for a given object
*/
Object get(Object object);
}
private static final long serialVersionUID = -2281234585616311623L;
private String baseName;
private String nestedProperty;
private final List<GeneralValidation> generalValidations = new LinkedList<GeneralValidation>();
private final Map<String, Property> properties = new LinkedHashMap<String, Property>();
private final List<Validator> chainedValidators = new LinkedList<Validator>();
public Validator() {
}
public Validator(final String baseName) {
this.baseName = baseName;
}
public Validator(final String baseName, final String nestedProperty) {
this(baseName);
this.nestedProperty = nestedProperty;
}
public Validator chained(final Validator... validators) {
if (validators != null && validators.length > 0) {
chainedValidators.addAll(Arrays.asList(validators));
}
return this;
}
/**
* Register general validations
*/
public Validator general(final GeneralValidation... validations) {
if (validations != null && validations.length > 0) {
generalValidations.addAll(Arrays.asList(validations));
}
return this;
}
public String getBaseName() {
return baseName;
}
public String getNestedProperty() {
return nestedProperty;
}
/**
* Return a property validation descriptor
*/
public Property property(final String name) {
return property(name, null);
}
/**
* Return a property validation descriptor
*/
public Property property(final String name, final PropertyRetrieveStrategy retrieveStrategy) {
Property property = properties.get(name);
if (property == null) {
if (retrieveStrategy == null) {
property = new Property(name);
} else {
property = new Property(name, retrieveStrategy);
}
properties.put(name, property);
}
return property;
}
/**
* Validates the given object, throwing a ValidationException if the entity is invalid
*/
public void validate(final Object object) throws ValidationException {
final ValidationException vex = new ValidationException();
vex.setBaseName(baseName);
appendValidationErrors(vex, object);
for (final Validator chained : chainedValidators) {
final String chainedProperty = chained.getNestedProperty();
if (chainedProperty == null) {
chained.appendValidationErrors(vex, object);
} else {
final Object toValidate = PropertyHelper.get(object, chainedProperty);
if (toValidate != null) {
chained.appendValidationErrors(vex, toValidate);
}
}
}
vex.throwIfHasErrors();
}
protected void appendValidationErrors(final ValidationException vex, final Object object) {
// Add general validations
for (final GeneralValidation val : generalValidations) {
final ValidationError error = val.validate(object);
if (error != null) {
vex.addGeneralError(error);
}
}
// Add property validations
for (final Map.Entry<String, Property> entry : properties.entrySet()) {
final String name = entry.getKey();
final Property property = entry.getValue();
if (property.getKey() != null) {
vex.setPropertyKey(name, property.getKey());
} else if (property.getDisplayName() != null) {
vex.setPropertyDisplayName(name, property.getDisplayName());
}
final Object value = property.getRetrieveStrategy().get(object);
final Object propertyData = property.getRetrieveStrategy().description(object, name);
for (final PropertyValidation val : property.getValidations()) {
final ValidationError error = val.validate(object, propertyData, value);
if (error != null) {
vex.addPropertyError(name, error);
}
}
}
}
}