diff options
author | Denis <denis@vaadin.com> | 2017-02-08 14:32:09 +0200 |
---|---|---|
committer | Leif Åstrand <legioth@gmail.com> | 2017-02-08 14:32:09 +0200 |
commit | 1309be6c20316e50ccc959f7f17f64b7a4585c07 (patch) | |
tree | 9b3c32b8454512f1ac62dd9782a7ce55c848929b | |
parent | e4b2e8d2688dfea521df4c15fe000bae01f246cb (diff) | |
download | vaadin-framework-1309be6c20316e50ccc959f7f17f64b7a4585c07.tar.gz vaadin-framework-1309be6c20316e50ccc959f7f17f64b7a4585c07.zip |
Provide a way to configure bean binder to auto set required fields (#8460)
@NotNull, @Size(min>1), @NotEmpty annotations are handled via default
configuration which marks fields as required.
Fixes #8382
5 files changed, 335 insertions, 3 deletions
diff --git a/server/src/main/java/com/vaadin/data/BeanValidationBinder.java b/server/src/main/java/com/vaadin/data/BeanValidationBinder.java index 34af4156a4..101da5b1f7 100644 --- a/server/src/main/java/com/vaadin/data/BeanValidationBinder.java +++ b/server/src/main/java/com/vaadin/data/BeanValidationBinder.java @@ -15,6 +15,10 @@ */ package com.vaadin.data; +import javax.validation.metadata.BeanDescriptor; +import javax.validation.metadata.ConstraintDescriptor; +import javax.validation.metadata.PropertyDescriptor; + import com.vaadin.data.util.BeanUtil; import com.vaadin.data.validator.BeanValidator; @@ -29,6 +33,8 @@ public class BeanValidationBinder<BEAN> extends Binder<BEAN> { private final Class<BEAN> beanType; + private RequiredFieldConfigurator requiredConfigurator = RequiredFieldConfigurator.DEFAULT; + /** * Creates a new binder that uses reflection based on the provided bean type * to resolve bean properties. It assumes that JSR-303 bean validation @@ -53,11 +59,63 @@ public class BeanValidationBinder<BEAN> extends Binder<BEAN> { this.beanType = beanType; } + /** + * Sets a logic which allows to configure require indicator via + * {@link HasValue#setRequiredIndicatorVisible(boolean)} based on property + * descriptor. + * <p> + * Required indicator configuration will not be used at all if + * {@code configurator} is null. + * <p> + * By default the {@link RequiredFieldConfigurator#DEFAULT} configurator is + * used. + * + * @param configurator + * required indicator configurator, may be {@code null} + */ + public void setRequiredConfigurator( + RequiredFieldConfigurator configurator) { + requiredConfigurator = configurator; + } + + /** + * Gets field required indicator configuration logic. + * + * @see #setRequiredConfigurator(RequiredFieldConfigurator) + * + * @return required indicator configurator, may be {@code null} + */ + public RequiredFieldConfigurator getRequiredConfigurator() { + return requiredConfigurator; + } + @Override protected BindingBuilder<BEAN, ?> configureBinding( BindingBuilder<BEAN, ?> binding, PropertyDefinition<BEAN, ?> definition) { - return binding.withValidator( - new BeanValidator(beanType, definition.getName())); + BeanValidator validator = new BeanValidator(beanType, + definition.getName()); + if (requiredConfigurator != null) { + configureRequired(binding, definition, validator); + } + return binding.withValidator(validator); + } + + private void configureRequired(BindingBuilder<BEAN, ?> binding, + PropertyDefinition<BEAN, ?> definition, BeanValidator validator) { + assert requiredConfigurator != null; + BeanDescriptor descriptor = validator.getJavaxBeanValidator() + .getConstraintsForClass(beanType); + PropertyDescriptor propertyDescriptor = descriptor + .getConstraintsForProperty(definition.getName()); + if (propertyDescriptor == null) { + return; + } + if (propertyDescriptor.getConstraintDescriptors().stream() + .map(ConstraintDescriptor::getAnnotation) + .anyMatch(requiredConfigurator)) { + binding.getField().setRequiredIndicatorVisible(true); + } } + } diff --git a/server/src/main/java/com/vaadin/data/RequiredFieldConfigurator.java b/server/src/main/java/com/vaadin/data/RequiredFieldConfigurator.java new file mode 100644 index 0000000000..4bc1ffdbc5 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/RequiredFieldConfigurator.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2016 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.data; + +import java.lang.annotation.Annotation; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import com.vaadin.server.SerializablePredicate; + +/** + * This interface represents a predicate which returns {@code true} if bound + * field should be configured to have required indicator via + * {@link HasValue#setRequiredIndicatorVisible(boolean)}. + * + * @see BeanValidationBinder + * @see BeanValidationBinder#setRequiredConfigurator(RequiredFieldConfigurator) + * + * @author Vaadin Ltd + * @since 8.0 + * + */ +public interface RequiredFieldConfigurator + extends SerializablePredicate<Annotation> { + + /** + * Configurator which is aware of {@literal @NotNull} annotation presence + * for a property. + */ + public RequiredFieldConfigurator NOT_NULL = annotation -> annotation + .annotationType().equals(NotNull.class); + + /** + * Configurator which is aware of {@literal @NotEmpty} annotation presence + * for a property. + */ + public RequiredFieldConfigurator NOT_EMPTY = annotation -> annotation + .annotationType().getName() + .equals("org.hibernate.validator.constraints.NotEmpty"); + + /** + * Configurator which is aware of {@literal Size} annotation with + * {@code min()> 0} presence for a property. + */ + public RequiredFieldConfigurator SIZE = annotation -> annotation + .annotationType().equals(Size.class) + && ((Size) annotation).min() > 0; + + /** + * Default configurator which is combination of {@link #NOT_NULL}, + * {@link #NOT_EMPTY} and {@link #SIZE} configurators. + */ + public RequiredFieldConfigurator DEFAULT = NOT_NULL.chain(NOT_EMPTY) + .chain(SIZE); + + public default RequiredFieldConfigurator chain( + RequiredFieldConfigurator configurator) { + return descriptor -> test(descriptor) || configurator.test(descriptor); + } +} diff --git a/server/src/main/java/com/vaadin/data/validator/BeanValidator.java b/server/src/main/java/com/vaadin/data/validator/BeanValidator.java index 097b47cb0f..e2b1f0ed92 100644 --- a/server/src/main/java/com/vaadin/data/validator/BeanValidator.java +++ b/server/src/main/java/com/vaadin/data/validator/BeanValidator.java @@ -150,7 +150,7 @@ public class BeanValidator implements Validator<Object> { * * @return the validator to use */ - protected javax.validation.Validator getJavaxBeanValidator() { + public javax.validation.Validator getJavaxBeanValidator() { return getJavaxBeanValidatorFactory().getValidator(); } diff --git a/server/src/test/java/com/vaadin/data/BeanBinderTest.java b/server/src/test/java/com/vaadin/data/BeanBinderTest.java index 2103c2b132..652c406073 100644 --- a/server/src/test/java/com/vaadin/data/BeanBinderTest.java +++ b/server/src/test/java/com/vaadin/data/BeanBinderTest.java @@ -6,6 +6,13 @@ import static org.junit.Assert.assertSame; import java.util.List; import java.util.Set; +import javax.validation.constraints.Digits; +import javax.validation.constraints.Max; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import org.hibernate.validator.constraints.NotEmpty; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -46,6 +53,43 @@ public class BeanBinderTest } } + public class RequiredConstraints { + @NotNull + @Max(10) + private String firstname; + + @Size(min = 3, max = 16) + @Digits(integer = 3, fraction = 2) + private String age; + + @NotEmpty + private String lastname; + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getAge() { + return age; + } + + public void setAge(String age) { + this.age = age; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + } + @Before public void setUp() { binder = new BeanValidationBinder<>(BeanToValidate.class); @@ -249,6 +293,45 @@ public class BeanBinderTest assertEquals(20, item.getAge()); } + @Test + public void firstName_isNotNullConstraint_fieldIsRequired() { + BeanValidationBinder<RequiredConstraints> binder = new BeanValidationBinder<>( + RequiredConstraints.class); + RequiredConstraints bean = new RequiredConstraints(); + + TextField field = new TextField(); + binder.bind(field, "firstname"); + binder.setBean(bean); + + Assert.assertTrue(field.isRequiredIndicatorVisible()); + } + + @Test + public void age_minSizeConstraint_fieldIsRequired() { + BeanValidationBinder<RequiredConstraints> binder = new BeanValidationBinder<>( + RequiredConstraints.class); + RequiredConstraints bean = new RequiredConstraints(); + + TextField field = new TextField(); + binder.bind(field, "age"); + binder.setBean(bean); + + Assert.assertTrue(field.isRequiredIndicatorVisible()); + } + + @Test + public void lastName_minSizeConstraint_fieldIsRequired() { + BeanValidationBinder<RequiredConstraints> binder = new BeanValidationBinder<>( + RequiredConstraints.class); + RequiredConstraints bean = new RequiredConstraints(); + + TextField field = new TextField(); + binder.bind(field, "lastname"); + binder.setBean(bean); + + Assert.assertTrue(field.isRequiredIndicatorVisible()); + } + private void assertInvalid(HasValue<?> field, String message) { BinderValidationStatus<?> status = binder.validate(); List<BindingValidationStatus<?>> errors = status diff --git a/server/src/test/java/com/vaadin/data/NotEmptyTest.java b/server/src/test/java/com/vaadin/data/NotEmptyTest.java new file mode 100644 index 0000000000..6b13825455 --- /dev/null +++ b/server/src/test/java/com/vaadin/data/NotEmptyTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2000-2016 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.data; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; + +import org.apache.commons.io.IOUtils; +import org.hibernate.validator.constraints.NotEmpty; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.tests.data.bean.BeanToValidate; +import com.vaadin.ui.TextField; + +/** + * @author Vaadin Ltd + * + */ +public class NotEmptyTest { + + private static String NOT_EMPTY = "org.hibernate.validator.constraints.NotEmpty"; + + private static class TestClassLoader extends URLClassLoader { + + public TestClassLoader() { + super(new URL[0], Thread.currentThread().getContextClassLoader()); + } + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + String vaadinPackagePrefix = getClass().getPackage().getName(); + vaadinPackagePrefix = vaadinPackagePrefix.substring(0, + vaadinPackagePrefix.lastIndexOf('.')); + if (name.equals(UnitTest.class.getName())) { + super.loadClass(name); + } else if (name.startsWith(NotEmpty.class.getPackage().getName())) { + throw new ClassNotFoundException(); + } else if (name.startsWith(vaadinPackagePrefix)) { + String path = name.replace('.', '/').concat(".class"); + URL resource = Thread.currentThread().getContextClassLoader() + .getResource(path); + InputStream stream; + try { + stream = resource.openStream(); + byte[] bytes = IOUtils.toByteArray(stream); + return defineClass(name, bytes, 0, bytes.length); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return super.loadClass(name); + } + } + + public interface UnitTest { + void execute(); + } + + public static class NotEmptyUnitTest implements UnitTest { + + private final TextField nameField = new TextField(); + + @Override + public void execute() { + try { + Class.forName(NOT_EMPTY); + // The NotEmpty class must not be in the classpath + Assert.fail(); + } catch (ClassNotFoundException e) { + } + BeanValidationBinder<BeanToValidate> binder = new BeanValidationBinder<>( + BeanToValidate.class); + + BeanToValidate item = new BeanToValidate(); + String name = "Johannes"; + item.setFirstname(name); + item.setAge(32); + + binder.bind(nameField, "firstname"); + binder.setBean(item); + + Assert.assertTrue(nameField.isRequiredIndicatorVisible()); + } + + } + + @Test + public void notEmptyAnnotationIsNotInClasspath() + throws ClassNotFoundException, NoSuchMethodException, + SecurityException, InstantiationException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException, IOException, + InterruptedException { + try (URLClassLoader loader = new TestClassLoader()) { + Class<?> clazz = loader.loadClass(NotEmptyUnitTest.class.getName()); + UnitTest test = (UnitTest) clazz.newInstance(); + test.execute(); + } + } + +} |