summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenis <denis@vaadin.com>2017-02-08 14:32:09 +0200
committerLeif Åstrand <legioth@gmail.com>2017-02-08 14:32:09 +0200
commit1309be6c20316e50ccc959f7f17f64b7a4585c07 (patch)
tree9b3c32b8454512f1ac62dd9782a7ce55c848929b
parente4b2e8d2688dfea521df4c15fe000bae01f246cb (diff)
downloadvaadin-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
-rw-r--r--server/src/main/java/com/vaadin/data/BeanValidationBinder.java62
-rw-r--r--server/src/main/java/com/vaadin/data/RequiredFieldConfigurator.java74
-rw-r--r--server/src/main/java/com/vaadin/data/validator/BeanValidator.java2
-rw-r--r--server/src/test/java/com/vaadin/data/BeanBinderTest.java83
-rw-r--r--server/src/test/java/com/vaadin/data/NotEmptyTest.java117
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();
+ }
+ }
+
+}