diff options
author | Johannes Dahlström <johannesd@vaadin.com> | 2016-07-28 23:39:26 +0300 |
---|---|---|
committer | Ilia Motornyi <elmot@vaadin.com> | 2016-08-03 13:09:13 +0000 |
commit | a91aaaf881857afe65d297c1d9a14088d45c9101 (patch) | |
tree | 96f2e1b5c07df442a335d905ee50573888b5fff2 | |
parent | c674a979101001831391baa12dfebfdfef8e4278 (diff) | |
download | vaadin-framework-a91aaaf881857afe65d297c1d9a14088d45c9101.tar.gz vaadin-framework-a91aaaf881857afe65d297c1d9a14088d45c9101.zip |
Implement new BeanValidator
Change-Id: I90c0ec638227e63b435ab8f21391de53f78962e4
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)); + } +} |