/*
* Copyright 2002-2007 the original author or authors.
*
* 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.internna.iwebmvc.core.validation;
import java.lang.reflect.Field;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.internna.iwebmvc.core.validation.annotation.BeanConstraint;
import org.internna.iwebmvc.core.validation.annotation.CollectionConstraint;
import org.internna.iwebmvc.core.validation.annotation.DateConstraint;
import org.internna.iwebmvc.core.validation.annotation.NumberConstraint;
import org.internna.iwebmvc.core.validation.annotation.StringConstraint;
import org.internna.iwebmvc.core.visitor.FieldVisitor;
import org.internna.iwebmvc.core.visitor.Visitable;
import org.internna.iwebmvc.utils.Assert;
import org.springframework.stereotype.Component;
/**
* Visitor implementation that validates an object.
*
* @author Jose Noheda
* @since 1.0
*/
@Component
public class ValidationVisitor implements FieldVisitor {
protected static final String[] errorCodes = {"error.required", "error.regexp", "error.min", "error.max"};
/**
* Visits a {@link org.internna.iwebmvc.core.visitor.Visitable} object.
* @param element a Visitable object
* @return
*/
@SuppressWarnings("unchecked")
public <T> T visit(final Object element) {
Assert.isInstanceOf(Visitable.class, element);
return (T) visit((Visitable) element);
}
/**
* Visits a Vistable object calling its accept method.
*
* @param entity any Visitable
* @return a clone
*/
protected final Collection<ValidationError> visit(final Visitable entity) {
return entity.accept(this);
}
/**
* Validates any field.
*
* @param field the field being validated
* @param fieldValue the value of that field
* @return
*/
@SuppressWarnings("unchecked")
public <T> Collection<T> visit(final Field field, final Object fieldValue) {
Assert.notNull(field);
Collection<ValidationError> errors = new HashSet<ValidationError>();
errors.addAll(validateStringConstraint(field, fieldValue));
errors.addAll(validateNumericConstraint(field, fieldValue));
errors.addAll(validateDateConstraint(field, fieldValue));
errors.addAll(validateBeanConstraint(field, fieldValue));
errors.addAll(validateCollectionConstraint(field, fieldValue));
return (Collection<T>) errors;
}
/**
* Validates a
* {@link org.internna.iwebmvc.core.validation.annotation.StringConstraint}.
*
* @param field the field to be validated
* @param object the value to be validated
* @return a collection of {@link ValidationError}
*/
protected Set<ValidationError> validateStringConstraint(final Field field, final Object object) {
final StringConstraint stringConstraint = field.getAnnotation(StringConstraint.class);
Set<ValidationError> errors = new HashSet<ValidationError>();
if (stringConstraint != null) {
if (stringConstraint.required() & (object == null)) errors.add(new ValidationError(field.getName(), errorCodes[0], stringConstraint, object));
if ((object != null) & (stringConstraint.regexp().length() > 0)) {
try {
Pattern pattern = Pattern.compile(stringConstraint.regexp());
Matcher matcher = pattern.matcher(object.toString());
if (!matcher.matches()) errors.add(new ValidationError(field.getName(), errorCodes[1], stringConstraint, object));
} catch (Exception ex) {
throw new ValidationException(ex);
}
}
if ((object != null) & (stringConstraint.minLength() > 0)) {
if (object.toString().length() < stringConstraint.minLength()) errors.add(new ValidationError(field.getName(), errorCodes[2], stringConstraint, object));
}
if ((object != null) & (stringConstraint.maxLength() > 0)) {
if (object.toString().length() > stringConstraint.maxLength()) errors.add(new ValidationError(field.getName(), errorCodes[3], stringConstraint, object));
}
}
return errors;
}
protected Set<ValidationError> validateNumericConstraint(final Field field, final Object object) {
final NumberConstraint numberConstraint = field.getAnnotation(NumberConstraint.class);
Set<ValidationError> errors = new HashSet<ValidationError>();
if (numberConstraint != null) {
if (numberConstraint.required() & (object == null)) errors.add(new ValidationError(field.getName(), errorCodes[0], numberConstraint, object));
if (object != null) {
double numericValue = ((Number) object).doubleValue();
if (numberConstraint.minValue() > numericValue) errors.add(new ValidationError(field.getName(), errorCodes[2], numberConstraint, object));
if (numberConstraint.maxValue() < numericValue) errors.add(new ValidationError(field.getName(), errorCodes[3], numberConstraint, object));
}
}
return errors;
}
/**
* Validates a
* {@link org.internna.iwebmvc.core.validation.annotation.DateConstraint}.
*
* @param field the field to be validated
* @param object the value to be validated
* @return a collection of {@link ValidationError}
*/
protected Set<ValidationError> validateDateConstraint(final Field field, final Object object) {
final DateConstraint dateConstraint = field.getAnnotation(DateConstraint.class);
Set<ValidationError> errors = new HashSet<ValidationError>();
if (dateConstraint != null) {
if (dateConstraint.required() & (object == null)) errors.add(new ValidationError(field.getName(), errorCodes[0], dateConstraint, object));
if (object != null) {
Calendar min = dateConstraint.minValue().getDate(false);
Calendar max = dateConstraint.maxValue().getDate(true);
Calendar date = Calendar.getInstance();
date.setTime((Date) object);
if (date.before(min)) errors.add(new ValidationError(field.getName(), errorCodes[2], dateConstraint, object));
if (date.after(max)) errors.add(new ValidationError(field.getName(), errorCodes[3], dateConstraint, object));
}
}
return errors;
}
protected Set<ValidationError> validateBeanConstraint(final Field field, final Object bean) {
Set<ValidationError> errors = new HashSet<ValidationError>();
BeanConstraint beanConstraint = field.getAnnotation(BeanConstraint.class);
if (beanConstraint != null) {
if (beanConstraint.required() & (bean == null)) errors.add(new ValidationError(field.getName(), errorCodes[0], beanConstraint, bean));
if ((bean != null) && (beanConstraint.traverse())) {
Collection<ValidationError> deepErrors = this.visit(bean);
for (ValidationError error : deepErrors) error.addPath(field.getName());
errors.addAll(deepErrors);
}
}
return errors;
}
@SuppressWarnings("unchecked")
protected Set<ValidationError> validateCollectionConstraint(final Field field, final Object collection) {
Set<ValidationError> errors = new HashSet<ValidationError>();
CollectionConstraint collectionConstraint = field.getAnnotation(CollectionConstraint.class);
if (collectionConstraint != null) {
if (collectionConstraint.required() & (collection == null)) errors.add(new ValidationError(field.getName(), errorCodes[0], collectionConstraint, collection));
if (collection != null) {
if (Collection.class.isInstance(collection)) {
Collection<?> col = (Collection<?>) collection;
if (collectionConstraint.traverse()) {
Collection<ValidationError> deepErrors = new HashSet<ValidationError>();
int i = 0;
for (Object element : col) {
Collection<ValidationError> inner = this.visit(element);
for (ValidationError error : inner) error.addPath(field.getName() + "[" + i + "]");
deepErrors.addAll(inner);
i++;
}
errors.addAll(deepErrors);
}
if (col.size() < collectionConstraint.minLength()) errors.add(new ValidationError(field.getName(), errorCodes[2], collectionConstraint, collection));
if (col.size() > collectionConstraint.maxLength()) errors.add(new ValidationError(field.getName(), errorCodes[3], collectionConstraint, collection));
} else {
throw new IllegalArgumentException("The annotated element is not a collection [" + collection + "]");
}
}
}
return errors;
}
}