summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHenri Sara <hesara@vaadin.com>2011-12-13 09:36:13 +0200
committerHenri Sara <hesara@vaadin.com>2011-12-16 11:34:29 +0200
commit41e6f404c4bc5b197e06689a181c9f673d5bfffe (patch)
treecdf295263e6f7145ae94e47faff3f2dbbcf77c01
parent624ffd391b69c5bf4ab9dbd2f9247b127bc129d7 (diff)
downloadvaadin-framework-41e6f404c4bc5b197e06689a181c9f673d5bfffe.tar.gz
vaadin-framework-41e6f404c4bc5b197e06689a181c9f673d5bfffe.zip
#8093 Support JSR-303 Bean Validation
-rw-r--r--build/ivy/ivy.xml3
-rw-r--r--src/com/vaadin/data/validator/BeanValidationForm.java120
-rw-r--r--src/com/vaadin/data/validator/BeanValidationValidator.java318
-rw-r--r--tests/server-side/com/vaadin/tests/data/bean/BeanToValidate.java56
-rw-r--r--tests/server-side/com/vaadin/tests/server/validation/TestBeanValidation.java59
5 files changed, 556 insertions, 0 deletions
diff --git a/build/ivy/ivy.xml b/build/ivy/ivy.xml
index 9dd69e1abe..4fcb44887b 100644
--- a/build/ivy/ivy.xml
+++ b/build/ivy/ivy.xml
@@ -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> \ No newline at end of file
diff --git a/src/com/vaadin/data/validator/BeanValidationForm.java b/src/com/vaadin/data/validator/BeanValidationForm.java
new file mode 100644
index 0000000000..ca94fd33f6
--- /dev/null
+++ b/src/com/vaadin/data/validator/BeanValidationForm.java
@@ -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;
+ }
+} \ No newline at end of file
diff --git a/src/com/vaadin/data/validator/BeanValidationValidator.java b/src/com/vaadin/data/validator/BeanValidationValidator.java
new file mode 100644
index 0000000000..379dcf547c
--- /dev/null
+++ b/src/com/vaadin/data/validator/BeanValidationValidator.java
@@ -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();
+ }
+} \ No newline at end of file
diff --git a/tests/server-side/com/vaadin/tests/data/bean/BeanToValidate.java b/tests/server-side/com/vaadin/tests/data/bean/BeanToValidate.java
new file mode 100644
index 0000000000..be8e40a118
--- /dev/null
+++ b/tests/server-side/com/vaadin/tests/data/bean/BeanToValidate.java
@@ -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;
+ }
+
+}
diff --git a/tests/server-side/com/vaadin/tests/server/validation/TestBeanValidation.java b/tests/server-side/com/vaadin/tests/server/validation/TestBeanValidation.java
new file mode 100644
index 0000000000..9111ab9a4b
--- /dev/null
+++ b/tests/server-side/com/vaadin/tests/server/validation/TestBeanValidation.java
@@ -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");
+ }
+
+}