Browse Source

#8093 Support JSR-303 Bean Validation

tags/7.0.0.alpha1
Henri Sara 12 years ago
parent
commit
41e6f404c4

+ 3
- 0
build/ivy/ivy.xml View File

@@ -38,6 +38,9 @@
<dependency org="emma" name="emma_ant" rev="2.0.5312" conf="ss.test.runtime,taskdefs ->master"/>
<dependency org="emma" name="emma" rev="2.0.5312-patched" conf="ss.test.runtime,taskdefs ->*"/>

<!-- Bean Validation implementation -->
<dependency org="org.slf4j" name="slf4j-log4j12" rev="1.6.1" conf="ss.test.runtime -> default"/>
<dependency org="org.hibernate" name="hibernate-validator" rev="4.2.0.Final" conf="ss.test.runtime -> default"/>
</dependencies>

</ivy-module>

+ 120
- 0
src/com/vaadin/data/validator/BeanValidationForm.java View File

@@ -0,0 +1,120 @@
package com.vaadin.data.validator;
import java.util.Collection;
import java.util.Locale;
import javax.validation.constraints.NotNull;
import com.vaadin.data.Item;
import com.vaadin.data.util.BeanItem;
import com.vaadin.ui.Field;
import com.vaadin.ui.Form;
/**
* Vaadin {@link Form} using the JSR-303 (javax.validation) declarative bean
* validation on its fields.
*
* Most validation is performed using {@link BeanValidationValidator}. In
* addition, fields are automatically marked as required when necessary based on
* the {@link NotNull} annotations.
*
* If the item is a {@link BeanItem}, the exact type of the bean is used in
* setting up validators. Otherwise, validation will only be performed based on
* beanClass, and behavior is undefined for subclasses that have fields not
* present in the superclass.
*
* Note that a JSR-303 implementation (e.g. Hibernate Validator or agimatec
* validation) must be present on the project classpath when using bean
* validation.
*
* @since 7.0
*
* @author Petri Hakala
* @author Henri Sara
*/
public class BeanValidationForm<T> extends Form {
private static final long serialVersionUID = 1L;
private Class<T> beanClass;
private Locale locale;
/**
* Creates a form that performs validation on beans of the type given by
* beanClass.
*
* Full validation of sub-types of beanClass is performed if the item used
* is a {@link BeanItem}. Otherwise, the class given to this constructor
* determines which properties are validated.
*
* @param beanClass
* base class of beans for the form
*/
public BeanValidationForm(Class<T> beanClass) {
if (beanClass == null) {
throw new IllegalArgumentException("Bean class cannot be null");
}
this.beanClass = beanClass;
}
@Override
public void addField(Object propertyId, Field field) {
Item item = getItemDataSource();
Class<? extends T> beanClass = this.beanClass;
if (item instanceof BeanItem) {
beanClass = (Class<? extends T>) ((BeanItem<T>) item).getBean()
.getClass();
}
BeanValidationValidator validator = BeanValidationValidator
.addValidator(field, propertyId, beanClass);
if (locale != null) {
validator.setLocale(locale);
}
super.addField(propertyId, field);
}
/**
* Sets the item data source for the form. If the new data source is a
* {@link BeanItem}, its bean must be assignable to the bean class of the
* form.
*
* {@inheritDoc}
*/
@Override
public void setItemDataSource(Item newDataSource) {
if ((newDataSource instanceof BeanItem)
&& !beanClass.isAssignableFrom(((BeanItem) newDataSource)
.getBean().getClass())) {
throw new IllegalArgumentException("Bean must be of type "
+ beanClass.getName());
}
super.setItemDataSource(newDataSource);
}
/**
* Sets the item data source for the form. If the new data source is a
* {@link BeanItem}, its bean must be assignable to the bean class of the
* form.
*
* {@inheritDoc}
*/
@Override
public void setItemDataSource(Item newDataSource, Collection propertyIds) {
if ((newDataSource instanceof BeanItem)
&& !beanClass.isAssignableFrom(((BeanItem) newDataSource)
.getBean().getClass())) {
throw new IllegalArgumentException("Bean must be of type "
+ beanClass.getName());
}
super.setItemDataSource(newDataSource, propertyIds);
}
/**
* Returns the base class of beans supported by this form.
*
* @return bean class
*/
public Class<T> getBeanClass() {
return beanClass;
}
}

+ 318
- 0
src/com/vaadin/data/validator/BeanValidationValidator.java View File

@@ -0,0 +1,318 @@
package com.vaadin.data.validator;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator.Context;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.PropertyDescriptor;
import com.vaadin.data.Property;
import com.vaadin.data.Validator;
import com.vaadin.data.util.MethodProperty;
import com.vaadin.ui.Field;
/**
* Vaadin {@link Validator} using the JSR-303 (javax.validation)
* annotation-based bean validation.
*
* The annotations of the fields of the beans are used to determine the
* validation to perform.
*
* Note that a JSR-303 implementation (e.g. Hibernate Validator or agimatec
* validation) must be present on the project classpath when using bean
* validation.
*
* @since 7.0
*
* @author Petri Hakala
* @author Henri Sara
*/
public class BeanValidationValidator implements Validator {
private static final long serialVersionUID = 1L;
private static ValidatorFactory factory = Validation
.buildDefaultValidatorFactory();
private transient javax.validation.Validator validator;
private String propertyName;
private Class<?> beanClass;
private MethodProperty method;
private Locale locale;
/**
* Creates a Vaadin {@link Validator} utilizing JSR-303 bean validation.
*
* @param beanClass
* bean class based on which the validation should be performed
* @param propertyName
* property to validate
*/
public BeanValidationValidator(Class<?> beanClass, String propertyName) {
this.beanClass = beanClass;
this.propertyName = propertyName;
validator = factory.getValidator();
try {
method = new MethodProperty(beanClass.newInstance(), propertyName);
} catch (Exception e) {
throw new IllegalArgumentException("Class '" + beanClass
+ "' must contain default constructor");
}
locale = Locale.getDefault();
}
/**
* Apply a bean validation validator to a field based on a bean class and
* the identifier of the property the field displays. The field is also
* marked as required if the bean field has the {@link NotNull} annotation.
* <p>
* No actual Vaadin validator is added in case no or only {@link NotNull}
* validation is used (required is practically same as NotNull validation).
*
* @param field
* the {@link Field} component to which to add a validator
* @param propertyId
* the property ID of the field of the bean that this field
* displays
* @param beanClass
* the class of the bean with the bean validation annotations
* @return the created validator
*/
public static BeanValidationValidator addValidator(Field field,
Object propertyId, Class<?> beanClass) {
BeanValidationValidator validator = new BeanValidationValidator(
beanClass, String.valueOf(propertyId));
PropertyDescriptor constraintsForProperty = validator.validator
.getConstraintsForClass(beanClass).getConstraintsForProperty(
propertyId.toString());
if (constraintsForProperty != null) {
int nonNotNullValidators = constraintsForProperty
.getConstraintDescriptors().size();
if (validator.isRequired()) {
field.setRequired(true);
field.setRequiredError(validator.getRequiredMessage());
nonNotNullValidators--;
}
if (nonNotNullValidators > 0) {
field.addValidator(validator);
}
}
return validator;
}
public boolean isValid(Object value) {
try {
validate(value);
return true;
} catch (Exception e) {
return false;
}
}
/**
* Checks if the property has been marked as required (has the
* {@link NotNull} annotation.
*
* @return true if the field is marked as not null
*/
public boolean isRequired() {
PropertyDescriptor desc = validator.getConstraintsForClass(beanClass)
.getConstraintsForProperty(propertyName);
if (desc != null) {
Iterator<ConstraintDescriptor<?>> it = desc
.getConstraintDescriptors().iterator();
while (it.hasNext()) {
final ConstraintDescriptor<?> d = it.next();
Annotation a = d.getAnnotation();
if (a instanceof NotNull) {
return true;
}
}
}
return false;
}
@SuppressWarnings("unchecked")
public String getRequiredMessage() {
return getErrorMessage(null, NotNull.class);
}
public void validate(final Object value) throws InvalidValueException {
Object convertedValue = value;
try {
convertedValue = convertValue(value);
} catch (Exception e) {
String msg = getErrorMessage(value);
if (msg != null) {
throw new InvalidValueException(msg);
} else {
// there should be always some constraints if conversion is
// needed
// for example if String -> Integer then Digits annotation
throw new InvalidValueException("Conversion exception");
}
}
Set<?> violations = validator.validateValue(beanClass, propertyName,
convertedValue);
if (violations.size() > 0) {
final Object finalValue = convertedValue;
List<String> exceptions = new ArrayList<String>();
for (Object v : violations) {
final ConstraintViolation<?> violation = (ConstraintViolation<?>) v;
String msg = factory.getMessageInterpolator().interpolate(
violation.getMessageTemplate(), new Context() {
public ConstraintDescriptor<?> getConstraintDescriptor() {
return violation.getConstraintDescriptor();
}
public Object getValidatedValue() {
return finalValue;
}
}, locale);
exceptions.add(msg);
}
StringBuilder b = new StringBuilder();
for (int i = 0; i < exceptions.size(); i++) {
if (i != 0) {
b.append("<br/>");
}
b.append(exceptions.get(i));
}
throw new InvalidValueException(b.toString());
}
}
/**
* Convert the value the way {@link MethodProperty} does: if the bean field
* is assignable from the value, return the value directly. Otherwise, try
* to find a constructor for bean field type that takes a String and call it
* with value.toString() .
*
* @param value
* the value to convert
* @return converted value, assignable to the field of the bean
* @throws {@link ConversionException} if no suitable conversion found or
* the target type constructor from string threw an exception
*/
private Object convertValue(Object value)
throws Property.ConversionException {
// Try to assign the compatible value directly
if (value == null
|| method.getType().isAssignableFrom(value.getClass())) {
return value;
} else {
try {
// Gets the string constructor
final Constructor constr = method.getType().getConstructor(
new Class[] { String.class });
return constr.newInstance(new Object[] { value.toString() });
} catch (final java.lang.Exception e) {
throw new Property.ConversionException(e);
}
}
}
private String getErrorMessage(final Object value,
Class<? extends Annotation>... an) {
BeanDescriptor beanDesc = validator.getConstraintsForClass(beanClass);
PropertyDescriptor desc = beanDesc
.getConstraintsForProperty(propertyName);
if (desc == null) {
// validate() reports a conversion error in this case
return null;
}
Iterator<ConstraintDescriptor<?>> it = desc.getConstraintDescriptors()
.iterator();
List<String> exceptions = new ArrayList<String>();
while (it.hasNext()) {
final ConstraintDescriptor<?> d = it.next();
Annotation a = d.getAnnotation();
boolean skip = false;
if (an != null && an.length > 0) {
skip = true;
for (Class<? extends Annotation> t : an) {
if (t == a.annotationType()) {
skip = false;
break;
}
}
}
if (!skip) {
String messageTemplate = null;
try {
Method m = a.getClass().getMethod("message");
messageTemplate = (String) m.invoke(a);
} catch (Exception ex) {
throw new InvalidValueException(
"Annotation must have message attribute");
}
String msg = factory.getMessageInterpolator().interpolate(
messageTemplate, new Context() {
public Object getValidatedValue() {
return value;
}
public ConstraintDescriptor<?> getConstraintDescriptor() {
return d;
}
}, locale);
exceptions.add(msg);
}
}
if (exceptions.size() > 0) {
StringBuilder b = new StringBuilder();
for (int i = 0; i < exceptions.size(); i++) {
if (i != 0) {
b.append("<br/>");
}
b.append(exceptions.get(i));
}
return b.toString();
}
return null;
}
/**
* Sets the locale used for validation error messages.
*
* Revalidation is not automatically triggered by setting the locale.
*
* @param locale
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
/**
* Gets the locale used for validation error messages.
*
* @return
*/
public Locale getLocale() {
return locale;
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
validator = factory.getValidator();
}
}

+ 56
- 0
tests/server-side/com/vaadin/tests/data/bean/BeanToValidate.java View File

@@ -0,0 +1,56 @@
package com.vaadin.tests.data.bean;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class BeanToValidate {
@NotNull
@Size(min = 3, max = 16)
private String firstname;
@NotNull(message = "Last name must not be empty")
private String lastname;
@Min(value = 18, message = "Must be 18 or above")
@Max(150)
private int age;
@Digits(integer = 3, fraction = 2)
private String decimals;
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getDecimals() {
return decimals;
}
public void setDecimals(String decimals) {
this.decimals = decimals;
}
}

+ 59
- 0
tests/server-side/com/vaadin/tests/server/validation/TestBeanValidation.java View File

@@ -0,0 +1,59 @@
package com.vaadin.tests.server.validation;

import org.junit.Test;

import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.validator.BeanValidationValidator;
import com.vaadin.tests.data.bean.BeanToValidate;

public class TestBeanValidation {
@Test(expected = InvalidValueException.class)
public void testBeanValidationNull() {
BeanValidationValidator validator = new BeanValidationValidator(
BeanToValidate.class, "firstname");
validator.validate(null);
}

@Test(expected = InvalidValueException.class)
public void testBeanValidationStringTooShort() {
BeanValidationValidator validator = new BeanValidationValidator(
BeanToValidate.class, "firstname");
validator.validate("aa");
}

@Test
public void testBeanValidationStringOk() {
BeanValidationValidator validator = new BeanValidationValidator(
BeanToValidate.class, "firstname");
validator.validate("aaa");
}

@Test(expected = InvalidValueException.class)
public void testBeanValidationIntegerTooSmall() {
BeanValidationValidator validator = new BeanValidationValidator(
BeanToValidate.class, "age");
validator.validate(17);
}

@Test
public void testBeanValidationIntegerOk() {
BeanValidationValidator validator = new BeanValidationValidator(
BeanToValidate.class, "age");
validator.validate(18);
}

@Test(expected = InvalidValueException.class)
public void testBeanValidationTooManyDigits() {
BeanValidationValidator validator = new BeanValidationValidator(
BeanToValidate.class, "decimals");
validator.validate("1234.567");
}

@Test
public void testBeanValidationDigitsOk() {
BeanValidationValidator validator = new BeanValidationValidator(
BeanToValidate.class, "decimals");
validator.validate("123.45");
}

}

Loading…
Cancel
Save