// Copyright 2010, 2012 The Apache Software Foundation
//
// 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.apache.tapestry5.internal.beanvalidator;
import org.apache.tapestry5.Field;
import org.apache.tapestry5.FieldValidator;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.beanvalidator.BeanValidatorGroupSource;
import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptor;
import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptorSource;
import org.apache.tapestry5.internal.BeanValidationContext;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.services.Environment;
import org.apache.tapestry5.services.FormSupport;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator;
import javax.validation.MessageInterpolator.Context;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import static java.lang.String.format;
public class BeanFieldValidator implements FieldValidator
{
private final Field field;
private final ValidatorFactory validatorFactory;
private final BeanValidatorGroupSource beanValidationGroupSource;
private final ClientConstraintDescriptorSource clientValidatorSource;
private final FormSupport formSupport;
private final Environment environment;
public BeanFieldValidator(Field field,
ValidatorFactory validatorFactory,
BeanValidatorGroupSource beanValidationGroupSource,
ClientConstraintDescriptorSource clientValidatorSource,
FormSupport formSupport,
Environment environment)
{
this.field = field;
this.validatorFactory = validatorFactory;
this.beanValidationGroupSource = beanValidationGroupSource;
this.clientValidatorSource = clientValidatorSource;
this.formSupport = formSupport;
this.environment = environment;
}
@Override
public boolean isRequired()
{
return false;
}
@Override
public void render(final MarkupWriter writer)
{
final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
if (beanValidationContext == null)
{
return;
}
final Validator validator = validatorFactory.getValidator();
final String currentProperty = beanValidationContext.getCurrentProperty();
if (currentProperty == null) return;
final ValidationInfo validationInfo = getValidationInfo(beanValidationContext, currentProperty, validator);
final PropertyDescriptor propertyDescriptor = validationInfo.getPropertyDescriptor();
if (propertyDescriptor == null) return;
for (final ConstraintDescriptor<?> descriptor : propertyDescriptor.getConstraintDescriptors())
{
Class<? extends Annotation> annotationType = descriptor.getAnnotation().annotationType();
ClientConstraintDescriptor clientConstraintDescriptor = clientValidatorSource.getConstraintDescriptor(annotationType);
if (clientConstraintDescriptor == null)
{
continue;
}
String message = format("%s %s", field.getLabel(), interpolateMessage(descriptor));
Map<String, Object> attributes = CollectionFactory.newMap();
for (String attribute : clientConstraintDescriptor.getAttributes())
{
Object object = descriptor.getAttributes().get(attribute);
if (object == null)
{
throw new NullPointerException(
String.format("Attribute '%s' of %s is null but is required to apply client-side validation.",
attribute, descriptor));
}
attributes.put(attribute, object);
}
clientConstraintDescriptor.applyClientValidation(writer, message, attributes);
}
}
@Override
@SuppressWarnings("unchecked")
public void validate(final Object value) throws ValidationException
{
final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
if (beanValidationContext == null)
{
return;
}
final Validator validator = validatorFactory.getValidator();
String currentProperty = beanValidationContext.getCurrentProperty();
if (currentProperty == null) return;
final ValidationInfo validationInfo = getValidationInfo(beanValidationContext, currentProperty, validator);
final PropertyDescriptor propertyDescriptor = validationInfo.getPropertyDescriptor();
if (propertyDescriptor == null) return;
final Set<ConstraintViolation<Object>> violations = validator.validateValue(
(Class<Object>) validationInfo.getBeanType(), validationInfo.getPropertyName(),
value, beanValidationGroupSource.get());
if (violations.isEmpty())
{
return;
}
final StringBuilder builder = new StringBuilder();
for (Iterator<ConstraintViolation<Object>> iterator = violations.iterator(); iterator.hasNext(); )
{
ConstraintViolation<?> violation = iterator.next();
builder.append(format("%s %s", field.getLabel(), violation.getMessage()));
if (iterator.hasNext())
builder.append(", ");
}
throw new ValidationException(builder.toString());
}
/**
* Returns the class of a given property, but only if it is a constrained property of the
* parent class. Otherwise, it returns null.
*/
final private static Class<?> getConstrainedPropertyClass(BeanDescriptor beanDescriptor, String propertyName)
{
Class<?> clasz = null;
for (PropertyDescriptor descriptor : beanDescriptor.getConstrainedProperties())
{
if (descriptor.getPropertyName().equals(propertyName))
{
clasz = descriptor.getElementClass();
break;
}
}
return clasz;
}
private String interpolateMessage(final ConstraintDescriptor<?> descriptor)
{
String messageTemplate = (String) descriptor.getAttributes().get("message");
MessageInterpolator messageInterpolator = validatorFactory.getMessageInterpolator();
return messageInterpolator.interpolate(messageTemplate, new Context()
{
@Override
public ConstraintDescriptor<?> getConstraintDescriptor()
{
return descriptor;
}
@Override
public Object getValidatedValue()
{
return null;
}
});
}
final private static ValidationInfo getValidationInfo(BeanValidationContext beanValidationContext, String currentProperty, Validator validator) {
Class<?> beanType = beanValidationContext.getBeanType();
String[] path = currentProperty.split("\\.");
BeanDescriptor beanDescriptor = validator.getConstraintsForClass(beanType);
for (int i = 1; i < path.length - 1; i++)
{
Class<?> constrainedPropertyClass = getConstrainedPropertyClass(beanDescriptor, path[i]);
if (constrainedPropertyClass != null) {
beanType = constrainedPropertyClass;
beanDescriptor = validator.getConstraintsForClass(beanType);
}
}
final String propertyName = path[path.length - 1];
PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(propertyName);
return new ValidationInfo(beanType, propertyName, propertyDescriptor);
}
final private static class ValidationInfo {
final private Class<?> beanType;
final private String propertyName;
final private PropertyDescriptor propertyDescriptor;
public ValidationInfo(Class<?> beanType, String propertyName,
PropertyDescriptor propertyDescriptor)
{
super();
this.beanType = beanType;
this.propertyName = propertyName;
this.propertyDescriptor = propertyDescriptor;
}
public Class<?> getBeanType()
{
return beanType;
}
public String getPropertyName()
{
return propertyName;
}
public PropertyDescriptor getPropertyDescriptor()
{
return propertyDescriptor;
}
}
}