aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohannes Dahlström <johannesd@vaadin.com>2016-07-28 23:39:26 +0300
committerIlia Motornyi <elmot@vaadin.com>2016-08-03 13:09:13 +0000
commita91aaaf881857afe65d297c1d9a14088d45c9101 (patch)
tree96f2e1b5c07df442a335d905ee50573888b5fff2
parentc674a979101001831391baa12dfebfdfef8e4278 (diff)
downloadvaadin-framework-a91aaaf881857afe65d297c1d9a14088d45c9101.tar.gz
vaadin-framework-a91aaaf881857afe65d297c1d9a14088d45c9101.zip
Implement new BeanValidator
Change-Id: I90c0ec638227e63b435ab8f21391de53f78962e4
-rw-r--r--server/src/main/java/com/vaadin/tokka/data/Validator.java2
-rw-r--r--server/src/main/java/com/vaadin/tokka/data/util/Result.java9
-rw-r--r--server/src/main/java/com/vaadin/tokka/data/validators/BeanValidator.java192
-rw-r--r--server/src/test/java/com/vaadin/tests/data/bean/Address.java13
-rw-r--r--server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java39
-rw-r--r--server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java21
-rw-r--r--server/src/test/java/com/vaadin/tokka/data/validators/BeanValidatorTest.java89
7 files changed, 356 insertions, 9 deletions
diff --git a/server/src/main/java/com/vaadin/tokka/data/Validator.java b/server/src/main/java/com/vaadin/tokka/data/Validator.java
index 1317b97ddd..3c6fc522dd 100644
--- a/server/src/main/java/com/vaadin/tokka/data/Validator.java
+++ b/server/src/main/java/com/vaadin/tokka/data/Validator.java
@@ -48,6 +48,8 @@ import com.vaadin.tokka.data.util.Result;
* the type of the value to validate
*
* @see Result
+ *
+ * @since
*/
@FunctionalInterface
public interface Validator<T> extends Function<T, Result<T>>, Serializable {
diff --git a/server/src/main/java/com/vaadin/tokka/data/util/Result.java b/server/src/main/java/com/vaadin/tokka/data/util/Result.java
index 2943ada786..f372483984 100644
--- a/server/src/main/java/com/vaadin/tokka/data/util/Result.java
+++ b/server/src/main/java/com/vaadin/tokka/data/util/Result.java
@@ -92,6 +92,15 @@ public interface Result<R> extends Serializable {
}
/**
+ *
+ * @param next
+ * @return
+ */
+ public default <S> Result<S> append(Result<S> next) {
+ return flatMap(x -> next);
+ }
+
+ /**
* If this Result has a value, returns a Result of applying the given
* function to the value. Otherwise, returns a Result bearing the same error
* as this one. Note that any exceptions thrown by the mapping function are
diff --git a/server/src/main/java/com/vaadin/tokka/data/validators/BeanValidator.java b/server/src/main/java/com/vaadin/tokka/data/validators/BeanValidator.java
new file mode 100644
index 0000000000..876640a0c8
--- /dev/null
+++ b/server/src/main/java/com/vaadin/tokka/data/validators/BeanValidator.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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 com.vaadin.tokka.data.validators;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.MessageInterpolator.Context;
+import javax.validation.Validation;
+import javax.validation.ValidatorFactory;
+import javax.validation.metadata.ConstraintDescriptor;
+
+import com.vaadin.tokka.data.Validator;
+import com.vaadin.tokka.data.util.Result;
+
+/**
+ * A {@code Validator} using the JSR-303 (javax.validation) annotation-based
+ * bean validation mechanism. Values passed to this validator are compared
+ * against the constraints, if any, specified by annotations on the
+ * corresponding bean property.
+ * <p>
+ * Note that a JSR-303 implementation (e.g. Hibernate Validator or Apache Bean
+ * Validation - formerly agimatec validation) must be present on the project
+ * classpath when using bean validation.
+ *
+ * @author Petri Hakala
+ * @author Vaadin Ltd.
+ *
+ * @since
+ */
+public class BeanValidator implements Validator<Object> {
+
+ private static final long serialVersionUID = 1L;
+ private static ValidatorFactory factory;
+
+ private String propertyName;
+ private Class<?> beanType;
+ private Locale locale;
+
+ /**
+ * Creates a new JSR-303 {@code BeanValidator} that validates values of the
+ * specified property. Localizes validation messages using the
+ * {@linkplain Locale#getDefault() default locale}.
+ *
+ * @param beanType
+ * the bean type declaring the property, not null
+ * @param propertyName
+ * the property to validate, not null
+ */
+ public BeanValidator(Class<?> beanType, String propertyName) {
+ this(beanType, propertyName, Locale.getDefault());
+ }
+
+ /**
+ * Creates a new JSR-303 {@code BeanValidator} that validates values of the
+ * specified property. Localizes validation messages using the given locale.
+ *
+ * @param beanType
+ * the bean class declaring the property, not null
+ * @param propertyName
+ * the property to validate, not null
+ * @param locale
+ * the locale to use, not null
+ */
+ public BeanValidator(Class<?> beanType, String propertyName,
+ Locale locale) {
+ Objects.requireNonNull(beanType, "bean class cannot be null");
+ Objects.requireNonNull(propertyName, "property name cannot be null");
+ this.beanType = beanType;
+ this.propertyName = propertyName;
+ setLocale(locale);
+ }
+
+ /**
+ * Validates the given value as if it were the value of the bean property
+ * configured for this validator. Returns {@code Result.ok} if there are no
+ * JSR-303 constraint violations, a {@code Result.error} of chained
+ * constraint violation messages otherwise.
+ * <p>
+ * Null values are accepted unless the property has an {@code @NotNull}
+ * annotation or equivalent.
+ */
+ @Override
+ public Result<Object> apply(final Object value) {
+ Set<? extends ConstraintViolation<?>> violations = getJavaxBeanValidator()
+ .validateValue(beanType, propertyName, value);
+
+ return violations.stream()
+ .map(v -> Result.error(getMessage(v)))
+ .reduce(Result.ok(value), Result::append);
+ }
+
+ /**
+ * Sets the locale used for validation error messages. Revalidation is not
+ * automatically triggered by setting the locale.
+ *
+ * @param locale
+ * the locale to use for error messages, not null
+ */
+ public void setLocale(Locale locale) {
+ Objects.requireNonNull(locale, "locale cannot be null");
+ this.locale = locale;
+ }
+
+ /**
+ * Returns the locale used for validation error messages.
+ *
+ * @return the locale used for error messages
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s[%s.%s]", getClass().getSimpleName(),
+ beanType.getSimpleName(), propertyName);
+ }
+
+ /**
+ * Returns the underlying JSR-303 bean validator factory used. A factory is
+ * created using {@link Validation} if necessary.
+ *
+ * @return the validator factory to use
+ */
+ protected static ValidatorFactory getJavaxBeanValidatorFactory() {
+ if (factory == null) {
+ factory = Validation.buildDefaultValidatorFactory();
+ }
+ return factory;
+ }
+
+ /**
+ * Returns a shared JSR-303 validator instance to use.
+ *
+ * @return the validator to use
+ */
+ protected javax.validation.Validator getJavaxBeanValidator() {
+ return getJavaxBeanValidatorFactory().getValidator();
+ }
+
+ /**
+ * Returns the interpolated error message for the given constraint violation
+ * using the locale specified for this validator.
+ *
+ * @param v
+ * the constraint violation
+ * @return the localized error message
+ */
+ protected String getMessage(ConstraintViolation<?> v) {
+ return getJavaxBeanValidatorFactory().getMessageInterpolator()
+ .interpolate(v.getMessageTemplate(), createContext(v), locale);
+ }
+
+ /**
+ * Creates a simple message interpolation context based on the given
+ * constraint violation.
+ *
+ * @param v
+ * the constraint violation
+ * @return the message interpolation context
+ */
+ protected Context createContext(ConstraintViolation<?> v) {
+ return new Context() {
+ @Override
+ public ConstraintDescriptor<?> getConstraintDescriptor() {
+ return v.getConstraintDescriptor();
+ }
+
+ @Override
+ public Object getValidatedValue() {
+ return v.getInvalidValue();
+ }
+ };
+ }
+}
diff --git a/server/src/test/java/com/vaadin/tests/data/bean/Address.java b/server/src/test/java/com/vaadin/tests/data/bean/Address.java
index 15cdf34ae5..7c641f6fb5 100644
--- a/server/src/test/java/com/vaadin/tests/data/bean/Address.java
+++ b/server/src/test/java/com/vaadin/tests/data/bean/Address.java
@@ -2,12 +2,25 @@ package com.vaadin.tests.data.bean;
import java.io.Serializable;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
@SuppressWarnings("serial")
public class Address implements Serializable {
+ @NotNull
private String streetAddress = "";
+
+ @NotNull
+ @Min(0)
+ @Max(99999)
private Integer postalCode = null;
+
+ @NotNull
private String city = "";
+
+ @NotNull
private Country country = null;
public Address() {
diff --git a/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java b/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java
index 034609764f..fa8c438ee9 100644
--- a/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java
+++ b/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java
@@ -1,13 +1,18 @@
package com.vaadin.tests.data.bean;
+import java.util.Calendar;
+
+import javax.validation.Valid;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
public class BeanToValidate {
+
@NotNull
@Size(min = 3, max = 16)
private String firstname;
@@ -26,6 +31,17 @@ public class BeanToValidate {
@Size(min = 3, max = 6, message = "Must contain 3 - 6 letters")
private String nickname;
+ @Past
+ private Calendar dateOfBirth;
+
+ @NotNull
+ @Valid
+ private Address[] addresses;
+
+ @NotNull
+ @Valid
+ private Address address;
+
public String getFirstname() {
return firstname;
}
@@ -66,4 +82,27 @@ public class BeanToValidate {
this.nickname = nickname;
}
+ public Calendar getDateOfBirth() {
+ return dateOfBirth;
+ }
+
+ public void setDateOfBirth(Calendar dateOfBirth) {
+ this.dateOfBirth = dateOfBirth;
+ }
+
+ public Address[] getAddresses() {
+ return addresses;
+ }
+
+ public void setAddresses(Address[] address) {
+ this.addresses = address;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
}
diff --git a/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java b/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java
index 2aef09fa2a..f284dba398 100644
--- a/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java
+++ b/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java
@@ -65,6 +65,8 @@ public class ClassesSerializableTest {
"com\\.vaadin\\.data\\.util\\.sqlcontainer\\.connection\\.MockInitialContextFactory",
"com\\.vaadin\\.data\\.util\\.sqlcontainer\\.DataGenerator",
"com\\.vaadin\\.data\\.util\\.sqlcontainer\\.FreeformQueryUtil",
+ // the JSR-303 constraint interpolation context
+ "com\\.vaadin\\.tokka\\.data\\.validators\\.BeanValidator\\$1", //
"com\\.vaadin\\.sass.*", //
"com\\.vaadin\\.testbench.*", //
"com\\.vaadin\\.util\\.CurrentInstance\\$1", //
@@ -92,12 +94,12 @@ public class ClassesSerializableTest {
public void testClassesSerializable() throws Exception {
List<String> rawClasspathEntries = getRawClasspathEntries();
- List<String> classes = new ArrayList<String>();
+ List<String> classes = new ArrayList<>();
for (String location : rawClasspathEntries) {
classes.addAll(findServerClasses(location));
}
- ArrayList<Class<?>> nonSerializableClasses = new ArrayList<Class<?>>();
+ ArrayList<Class<?>> nonSerializableClasses = new ArrayList<>();
for (String className : classes) {
Class<?> cls = Class.forName(className);
// skip annotations and synthetic classes
@@ -149,8 +151,9 @@ public class ClassesSerializableTest {
nonSerializableString += ")";
}
}
- Assert.fail("Serializable not implemented by the following classes and interfaces: "
- + nonSerializableString);
+ Assert.fail(
+ "Serializable not implemented by the following classes and interfaces: "
+ + nonSerializableString);
}
}
@@ -181,7 +184,7 @@ public class ClassesSerializableTest {
//
private final static List<String> getRawClasspathEntries() {
// try to keep the order of the classpath
- List<String> locations = new ArrayList<String>();
+ List<String> locations = new ArrayList<>();
String pathSep = System.getProperty("path.separator");
String classpath = System.getProperty("java.class.path");
@@ -215,7 +218,7 @@ public class ClassesSerializableTest {
*/
private List<String> findServerClasses(String classpathEntry)
throws IOException {
- Collection<String> classes = new ArrayList<String>();
+ Collection<String> classes = new ArrayList<>();
File file = new File(classpathEntry);
if (file.isDirectory()) {
@@ -227,7 +230,7 @@ public class ClassesSerializableTest {
return Collections.emptyList();
}
- List<String> filteredClasses = new ArrayList<String>();
+ List<String> filteredClasses = new ArrayList<>();
for (String className : classes) {
boolean ok = false;
for (String basePackage : BASE_PACKAGES) {
@@ -265,7 +268,7 @@ public class ClassesSerializableTest {
* @throws IOException
*/
private Collection<String> findClassesInJar(File file) throws IOException {
- Collection<String> classes = new ArrayList<String>();
+ Collection<String> classes = new ArrayList<>();
JarFile jar = new JarFile(file);
Enumeration<JarEntry> e = jar.entries();
@@ -305,7 +308,7 @@ public class ClassesSerializableTest {
parentPackage += ".";
}
- Collection<String> classNames = new ArrayList<String>();
+ Collection<String> classNames = new ArrayList<>();
// add all directories recursively
File[] files = parent.listFiles();
diff --git a/server/src/test/java/com/vaadin/tokka/data/validators/BeanValidatorTest.java b/server/src/test/java/com/vaadin/tokka/data/validators/BeanValidatorTest.java
new file mode 100644
index 0000000000..0e144102b9
--- /dev/null
+++ b/server/src/test/java/com/vaadin/tokka/data/validators/BeanValidatorTest.java
@@ -0,0 +1,89 @@
+package com.vaadin.tokka.data.validators;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.Calendar;
+import java.util.Locale;
+
+import org.junit.Test;
+
+import com.vaadin.tests.data.bean.Address;
+import com.vaadin.tests.data.bean.BeanToValidate;
+
+public class BeanValidatorTest {
+
+ @Test
+ public void testFirstNameNullFails() {
+ assertFails(null, "may not be null", validator("firstname"));
+ }
+
+ @Test
+ public void testFirstNameTooShortFails() {
+ assertFails("x", "size must be between 3 and 16",
+ validator("firstname"));
+ }
+
+ @Test
+ public void testFirstNameLongEnoughPasses() {
+ assertPasses("Magi", validator("firstname"));
+ }
+
+ @Test
+ public void testAgeTooYoungFails() {
+ assertFails(14, "Must be 18 or above", validator("age"));
+ }
+
+ @Test
+ public void testDateOfBirthNullPasses() {
+ assertPasses(null, validator("dateOfBirth"));
+ }
+
+ @Test
+ public void testDateOfBirthInTheFutureFails() {
+ Calendar year3k = Calendar.getInstance();
+ year3k.set(3000, 0, 1);
+ assertFails(year3k, "must be in the past", validator("dateOfBirth"));
+ }
+
+ @Test
+ public void testAddressesEmptyArrayPasses() {
+ Address[] noAddresses = {};
+ assertPasses(noAddresses, validator("addresses"));
+ }
+
+ @Test
+ public void testAddressesNullFails() {
+ assertFails(null, "may not be null", validator("addresses"));
+ }
+
+ @Test
+ public void testInvalidDecimalsFailsInFrench() {
+ BeanValidator v = validator("decimals");
+ v.setLocale(Locale.FRENCH);
+ assertFails("1234.567", "Valeur numérique hors limite "
+ + "(<3 chiffres>.<2 chiffres> attendus)", v);
+ }
+
+ @Test
+ public void testAddressNestedPropertyInvalidPostalCodeFails() {
+ assertFails(100_000, "must be less than or equal to 99999",
+ validator("address.postalCode"));
+ }
+
+ private BeanValidator validator(String propertyName) {
+ return new BeanValidator(BeanToValidate.class, propertyName);
+ }
+
+ private <T> void assertPasses(T value, BeanValidator v) {
+ v.apply(value).handle(
+ val -> assertEquals(value, val),
+ err -> fail(value + " should pass " + v + " but got " + err));
+ }
+
+ private <T> void assertFails(T value, String error, BeanValidator v) {
+ v.apply(value).handle(
+ val -> fail(value + " should fail " + v),
+ err -> assertEquals(error, err));
+ }
+}