aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohannes Dahlström <johannesd@vaadin.com>2016-07-25 12:49:26 +0300
committerVaadin Code Review <review@vaadin.com>2016-08-22 12:17:13 +0000
commitec8904f6b0ab77231d567daa35c9cc7138b6fe59 (patch)
tree7985a8d935fd5f326bd8aad50e827bf5cf747cdb
parent762ca747ce33f385e65fd9d77e9f102c3b86ac05 (diff)
downloadvaadin-framework-ec8904f6b0ab77231d567daa35c9cc7138b6fe59.tar.gz
vaadin-framework-ec8904f6b0ab77231d567daa35c9cc7138b6fe59.zip
Implement BeanBinder with JSR-303 validation
Change-Id: Ieaba56e9a26381d98b139845c30d65340dac0639
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java2
-rw-r--r--server/src/main/java/com/vaadin/data/BeanBinder.java322
-rw-r--r--server/src/main/java/com/vaadin/data/Binder.java153
-rw-r--r--server/src/main/java/com/vaadin/data/util/BeanUtil.java101
-rw-r--r--server/src/main/java/com/vaadin/data/validator/BeanValidator.java50
-rw-r--r--server/src/test/java/com/vaadin/data/BeanBinderTest.java171
-rw-r--r--server/src/test/java/com/vaadin/data/BinderTest.java2
-rw-r--r--server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java12
8 files changed, 692 insertions, 121 deletions
diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java
index c0c4fdcaa6..2725606a4d 100644
--- a/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java
+++ b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java
@@ -188,7 +188,7 @@ public class BeanItem<BT> extends PropertysetItem {
// Try to introspect, if it fails, we just have an empty Item
try {
List<PropertyDescriptor> propertyDescriptors = BeanUtil
- .getBeanPropertyDescriptor(beanClass);
+ .getBeanPropertyDescriptors(beanClass);
// Add all the bean properties as MethodProperties to this Item
// later entries on the list overwrite earlier ones
diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java
new file mode 100644
index 0000000000..214cb6781f
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/BeanBinder.java
@@ -0,0 +1,322 @@
+/*
+ * 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.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import com.vaadin.data.util.BeanUtil;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.validator.BeanValidator;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.UI;
+
+/**
+ * A {@code Binder} subclass specialized for binding <em>beans</em>: classes
+ * that conform to the JavaBeans specification. Bean properties are bound by
+ * their names. If a JSR-303 bean validation implementation is present on the
+ * classpath, {@code BeanBinder} adds a {@link BeanValidator} to each binding.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @param <BEAN>
+ * the bean type
+ *
+ * @since
+ */
+public class BeanBinder<BEAN> extends Binder<BEAN> {
+
+ /**
+ * Represents the binding between a single field and a bean property.
+ *
+ * @param <BEAN>
+ * the bean type
+ * @param <FIELDVALUE>
+ * the field value type
+ * @param <TARGET>
+ * the target property type
+ */
+ public interface BeanBinding<BEAN, FIELDVALUE, TARGET> extends
+ Binding<BEAN, FIELDVALUE, TARGET> {
+
+ @Override
+ public BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator(
+ Validator<? super TARGET> validator);
+
+ @Override
+ public default BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator(
+ Predicate<? super TARGET> predicate, String message) {
+ return (BeanBinding<BEAN, FIELDVALUE, TARGET>) Binding.super.withValidator(
+ predicate, message);
+ }
+
+ @Override
+ public <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
+ Converter<TARGET, NEWTARGET> converter);
+
+ @Override
+ public default <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
+ Function<TARGET, NEWTARGET> toModel,
+ Function<NEWTARGET, TARGET> toPresentation) {
+ return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) Binding.super.withConverter(
+ toModel, toPresentation);
+ }
+
+ @Override
+ public default <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
+ Function<TARGET, NEWTARGET> toModel,
+ Function<NEWTARGET, TARGET> toPresentation,
+ String errorMessage) {
+ return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) Binding.super.withConverter(
+ toModel, toPresentation, errorMessage);
+ }
+
+ /**
+ * Completes this binding by connecting the field to the property with
+ * the given name. The getter and setter methods of the property are
+ * looked up with bean introspection and used to read and write the
+ * property value.
+ * <p>
+ * If a JSR-303 bean validation implementation is present on the
+ * classpath, adds a {@link BeanValidator} to this binding.
+ * <p>
+ * The property must have an accessible getter method. It need not have
+ * an accessible setter; in that case the property value is never
+ * updated and the binding is said to be <i>read-only</i>.
+ *
+ * @param propertyName
+ * the name of the property to bind, not null
+ *
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ * @throws IllegalArgumentException
+ * if the property has no accessible getter
+ *
+ * @see Binding#bind(Function, java.util.function.BiConsumer)
+ */
+ public void bind(String propertyName);
+ }
+
+ /**
+ * An internal implementation of {@link BeanBinding}.
+ *
+ * @param <BEAN>
+ * the bean type
+ * @param <FIELDVALUE>
+ * the field value type
+ * @param <TARGET>
+ * the target property type
+ */
+ protected static class BeanBindingImpl<BEAN, FIELDVALUE, TARGET> extends
+ BindingImpl<BEAN, FIELDVALUE, TARGET>
+ implements BeanBinding<BEAN, FIELDVALUE, TARGET> {
+
+ private Method getter;
+ private Method setter;
+
+ /**
+ * Creates a new bean binding.
+ *
+ * @param binder
+ * the binder this instance is connected to, not null
+ * @param field
+ * the field to use, not null
+ * @param converter
+ * the initial converter to use, not null
+ * @param statusChangeHandler
+ * the handler to notify of status changes, not null
+ */
+ protected BeanBindingImpl(BeanBinder<BEAN> binder,
+ HasValue<FIELDVALUE> field,
+ Converter<FIELDVALUE, TARGET> converter,
+ StatusChangeHandler statusChangeHandler) {
+ super(binder, field, converter, statusChangeHandler);
+ }
+
+ @Override
+ public BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator(
+ Validator<? super TARGET> validator) {
+ return (BeanBinding<BEAN, FIELDVALUE, TARGET>) super.withValidator(
+ validator);
+ }
+
+ @Override
+ public <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
+ Converter<TARGET, NEWTARGET> converter) {
+ return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) super.withConverter(
+ converter);
+ }
+
+ @Override
+ public void bind(String propertyName) {
+ checkUnbound();
+
+ Binding<BEAN, FIELDVALUE, Object> finalBinding;
+
+ finalBinding = withConverter(createConverter());
+
+ if (BeanValidator.checkBeanValidationAvailable()) {
+ finalBinding = finalBinding.withValidator(new BeanValidator(
+ getBinder().beanType, propertyName, getLocale()));
+ }
+
+ PropertyDescriptor descriptor = getDescriptor(propertyName);
+ getter = descriptor.getReadMethod();
+ setter = descriptor.getWriteMethod();
+ finalBinding.bind(this::getValue, this::setValue);
+ }
+
+ @Override
+ protected BeanBinder<BEAN> getBinder() {
+ return (BeanBinder<BEAN>) super.getBinder();
+ }
+
+ private void setValue(BEAN bean, Object value) {
+ try {
+ if (setter != null) {
+ setter.invoke(bean, value);
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Object getValue(BEAN bean) {
+ try {
+ return getter.invoke(bean);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private PropertyDescriptor getDescriptor(String propertyName) {
+ final Class<?> beanType = getBinder().beanType;
+ PropertyDescriptor descriptor = null;
+ try {
+ descriptor = BeanUtil.getPropertyDescriptor(beanType,
+ propertyName);
+ } catch (IntrospectionException ie) {
+ throw new IllegalArgumentException(
+ "Could not resolve bean property name (see the cause): "
+ + beanType.getName() + "." + propertyName, ie);
+ }
+ if (descriptor == null) {
+ throw new IllegalArgumentException(
+ "Could not resolve bean property name (please check spelling and getter visibility): "
+ + beanType.getName() + "." + propertyName);
+ }
+ if (descriptor.getReadMethod() == null) {
+ throw new IllegalArgumentException(
+ "Bean property has no accessible getter: "
+ + beanType.getName() + "." + propertyName);
+ }
+ return descriptor;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Converter<TARGET, Object> createConverter() {
+ return Converter.from(
+ fieldValue -> getter.getReturnType().cast(fieldValue),
+ propertyValue -> (TARGET) propertyValue,
+ exception -> {
+ throw new RuntimeException(exception);
+ });
+ }
+
+ private Locale getLocale() {
+ Locale l = null;
+ if (getField() instanceof Component) {
+ l = ((Component) getField()).getLocale();
+ }
+ if (l == null && UI.getCurrent() != null) {
+ l = UI.getCurrent().getLocale();
+ }
+ if (l == null) {
+ l = Locale.getDefault();
+ }
+ return l;
+ }
+ }
+
+ private final Class<? extends BEAN> beanType;
+
+ /**
+ * Creates a new {@code BeanBinder} supporting beans of the given type.
+ *
+ * @param beanType
+ * the bean {@code Class} instance, not null
+ */
+ public BeanBinder(Class<? extends BEAN> beanType) {
+ BeanValidator.checkBeanValidationAvailable();
+ this.beanType = beanType;
+ }
+
+ @Override
+ public <FIELDVALUE> BeanBinding<BEAN, FIELDVALUE, FIELDVALUE> forField(
+ HasValue<FIELDVALUE> field) {
+ return createBinding(field, Converter.identity(),
+ this::handleValidationStatusChange);
+ }
+
+ /**
+ * Binds the given field to the property with the given name. The getter and
+ * setter methods of the property are looked up with bean introspection and
+ * used to read and write the property value.
+ * <p>
+ * Use the {@link #forField(HasValue)} overload instead if you want to
+ * further configure the new binding.
+ * <p>
+ * The property must have an accessible getter method. It need not have an
+ * accessible setter; in that case the property value is never updated and
+ * the binding is said to be <i>read-only</i>.
+ *
+ * @param <FIELDVALUE>
+ * the value type of the field to bind
+ * @param field
+ * the field to bind, not null
+ * @param propertyName
+ * the name of the property to bind, not null
+ *
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ * @throws IllegalArgumentException
+ * if the property has no accessible getter
+ *
+ * @see #bind(HasValue, java.util.function.Function,
+ * java.util.function.BiConsumer)
+ */
+ public <FIELDVALUE> void bind(HasValue<FIELDVALUE> field,
+ String propertyName) {
+ forField(field).bind(propertyName);
+ }
+
+ @Override
+ protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> createBinding(
+ HasValue<FIELDVALUE> field,
+ Converter<FIELDVALUE, TARGET> converter,
+ StatusChangeHandler handler) {
+ Objects.requireNonNull(field, "field cannot be null");
+ Objects.requireNonNull(converter, "converter cannot be null");
+ return new BeanBindingImpl<>(this, field, converter, handler);
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java
index d4501955d6..c7c55bfe7e 100644
--- a/server/src/main/java/com/vaadin/data/Binder.java
+++ b/server/src/main/java/com/vaadin/data/Binder.java
@@ -154,8 +154,10 @@ public class Binder<BEAN> implements Serializable {
* @throws IllegalStateException
* if {@code bind} has already been called
*/
- public Binding<BEAN, FIELDVALUE, TARGET> withValidator(
- Predicate<? super TARGET> predicate, String message);
+ public default Binding<BEAN, FIELDVALUE, TARGET> withValidator(
+ Predicate<? super TARGET> predicate, String message) {
+ return withValidator(Validator.from(predicate, message));
+ }
/**
* Maps the binding to another data type using the given
@@ -210,7 +212,7 @@ public class Binder<BEAN> implements Serializable {
* @throws IllegalStateException
* if {@code bind} has already been called
*/
- default public <NEWTARGET> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
+ public default <NEWTARGET> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
Function<TARGET, NEWTARGET> toModel,
Function<NEWTARGET, TARGET> toPresentation) {
return withConverter(Converter.from(toModel, toPresentation,
@@ -381,35 +383,17 @@ public class Binder<BEAN> implements Serializable {
private Converter<FIELDVALUE, TARGET> converterValidatorChain;
/**
- * Creates a new binding associated with the given field.
- *
- * @param binder
- * the binder this instance is connected to
- * @param field
- * the field to bind
- * @param statusChangeHandler
- * handler to track validation status
- */
- @SuppressWarnings("unchecked")
- protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field,
- StatusChangeHandler statusChangeHandler) {
- this(binder, field,
- (Converter<FIELDVALUE, TARGET>) Converter.identity(),
- statusChangeHandler);
- }
-
- /**
- * Creates a new binding associated with the given field using the given
- * converter chain.
+ * Creates a new binding associated with the given field. Initializes
+ * the binding with the given converter chain and status change handler.
*
* @param binder
- * the binder this instance is connected to
+ * the binder this instance is connected to, not null
* @param field
- * the field to bind
+ * the field to bind, not null
* @param converterValidatorChain
- * the converter/validator chain to use
+ * the converter/validator chain to use, not null
* @param statusChangeHandler
- * handler to track validation status
+ * the handler to track validation status, not null
*/
protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field,
Converter<FIELDVALUE, TARGET> converterValidatorChain,
@@ -428,8 +412,8 @@ public class Binder<BEAN> implements Serializable {
this.getter = getter;
this.setter = setter;
- binder.bindings.add(this);
- binder.getBean().ifPresent(this::bind);
+ getBinder().bindings.add(this);
+ getBinder().getBean().ifPresent(this::bind);
}
@Override
@@ -438,46 +422,68 @@ public class Binder<BEAN> implements Serializable {
checkUnbound();
Objects.requireNonNull(validator, "validator cannot be null");
- Converter<TARGET, TARGET> validatorAsConverter = new ValidatorAsConverter<>(
- validator);
converterValidatorChain = converterValidatorChain
- .chain(validatorAsConverter);
+ .chain(new ValidatorAsConverter<>(validator));
return this;
}
@Override
- public Binding<BEAN, FIELDVALUE, TARGET> withValidator(
- Predicate<? super TARGET> predicate, String message) {
- return withValidator(Validator.from(predicate, message));
- }
-
- @Override
public <NEWTARGET> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
Converter<TARGET, NEWTARGET> converter) {
checkUnbound();
Objects.requireNonNull(converter, "converter cannot be null");
- return createNewBinding(converter);
+ return getBinder().createBinding(getField(), converterValidatorChain
+ .chain(converter), statusChangeHandler);
}
@Override
public Binding<BEAN, FIELDVALUE, TARGET> withStatusChangeHandler(
StatusChangeHandler handler) {
- Objects.requireNonNull(handler, "Handler may not be null");
+ checkUnbound();
+ Objects.requireNonNull(handler, "handler cannot be null");
if (isStatusHandlerChanged) {
throw new IllegalStateException(
"A StatusChangeHandler has already been set");
}
isStatusHandlerChanged = true;
- checkUnbound();
statusChangeHandler = handler;
return this;
}
+ @Override
+ public HasValue<FIELDVALUE> getField() {
+ return field;
+ }
+
+ /**
+ * Returns the {@code Binder} connected to this {@code Binding}
+ * instance.
+ *
+ * @return the binder
+ */
+ protected Binder<BEAN> getBinder() {
+ return binder;
+ }
+
+ /**
+ * Throws if this binding is already completed and cannot be modified
+ * anymore.
+ *
+ * @throws IllegalStateException
+ * if this binding is already bound
+ */
+ protected void checkUnbound() {
+ if (getter != null) {
+ throw new IllegalStateException(
+ "cannot modify binding: already bound to a property");
+ }
+ }
+
private void bind(BEAN bean) {
setFieldValue(bean);
- onValueChange = field
- .addValueChangeListener(e -> storeFieldValue(bean));
+ onValueChange = getField().addValueChangeListener(
+ e -> storeFieldValue(bean));
}
@Override
@@ -512,13 +518,13 @@ public class Binder<BEAN> implements Serializable {
*/
private void setFieldValue(BEAN bean) {
assert bean != null;
- field.setValue(convertDataToFieldType(bean));
+ getField().setValue(convertDataToFieldType(bean));
}
private FIELDVALUE convertDataToFieldType(BEAN bean) {
return converterValidatorChain.convertToPresentation(
- getter.apply(bean),
- ((AbstractComponent) field).getLocale());
+ getter.apply(bean), ((AbstractComponent) getField())
+ .getLocale());
}
/**
@@ -535,26 +541,6 @@ public class Binder<BEAN> implements Serializable {
}
}
- private void checkUnbound() {
- if (getter != null) {
- throw new IllegalStateException(
- "cannot modify binding: already bound to a property");
- }
- }
-
- @Override
- public HasValue<FIELDVALUE> getField() {
- return field;
- }
-
- private <NEWTARGET> BindingImpl<BEAN, FIELDVALUE, NEWTARGET> createNewBinding(
- Converter<TARGET, NEWTARGET> converter) {
- BindingImpl<BEAN, FIELDVALUE, NEWTARGET> newBinding = new BindingImpl<>(
- binder, field, converterValidatorChain.chain(converter),
- statusChangeHandler);
- return newBinding;
- }
-
private void fireStatusChangeEvent(Result<TARGET> result) {
ValidationStatusChangeEvent event = new ValidationStatusChangeEvent(
getField(),
@@ -634,7 +620,9 @@ public class Binder<BEAN> implements Serializable {
*/
public <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> forField(
HasValue<FIELDVALUE> field) {
- return createBinding(field);
+ Objects.requireNonNull(field, "field cannot be null");
+ return createBinding(field, Converter.identity(),
+ this::handleValidationStatusChange);
}
/**
@@ -714,11 +702,12 @@ public class Binder<BEAN> implements Serializable {
* @return the validation result.
*/
public List<ValidationError<?>> validate() {
+
List<ValidationError<?>> resultErrors = new ArrayList<>();
- for (BindingImpl<BEAN, ?, ?> binding : bindings) {
- Result<?> result = binding.validate();
- result.ifError(errorMessage -> resultErrors
- .add(new ValidationError<>(binding.field, errorMessage)));
+ for (BindingImpl<?, ?, ?> binding : bindings) {
+ binding.validate().ifError(errorMessage -> resultErrors
+ .add(new ValidationError<>(binding.getField(),
+ errorMessage)));
}
return resultErrors;
}
@@ -768,16 +757,22 @@ public class Binder<BEAN> implements Serializable {
*
* @param <FIELDVALUE>
* the value type of the field
+ * @param <TARGET>
+ * the target data type
* @param field
- * the field to bind
+ * the field to bind, not null
+ * @param converter
+ * the converter for converting between FIELDVALUE and TARGET
+ * types, not null
+ * @param handler
+ * the handler to notify of status changes, not null
* @return the new incomplete binding
*/
- protected <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> createBinding(
- HasValue<FIELDVALUE> field) {
- Objects.requireNonNull(field, "field cannot be null");
- BindingImpl<BEAN, FIELDVALUE, FIELDVALUE> b = new BindingImpl<BEAN, FIELDVALUE, FIELDVALUE>(
- this, field, this::handleValidationStatusChange);
- return b;
+ protected <FIELDVALUE, TARGET> BindingImpl<BEAN, FIELDVALUE, TARGET> createBinding(
+ HasValue<FIELDVALUE> field,
+ Converter<FIELDVALUE, TARGET> converter,
+ StatusChangeHandler handler) {
+ return new BindingImpl<>(this, field, converter, handler);
}
/**
@@ -819,7 +814,7 @@ public class Binder<BEAN> implements Serializable {
* @param event
* the validation event
*/
- private void handleValidationStatusChange(
+ protected void handleValidationStatusChange(
ValidationStatusChangeEvent event) {
HasValue<?> source = event.getSource();
clearError(source);
diff --git a/server/src/main/java/com/vaadin/data/util/BeanUtil.java b/server/src/main/java/com/vaadin/data/util/BeanUtil.java
index ca897d57c5..375c449701 100644
--- a/server/src/main/java/com/vaadin/data/util/BeanUtil.java
+++ b/server/src/main/java/com/vaadin/data/util/BeanUtil.java
@@ -44,70 +44,102 @@ public final class BeanUtil implements Serializable {
* both the setter and the getter for a property must be in the same
* interface and should not be overridden in subinterfaces for the discovery
* to work correctly.
- *
+ * <p>
* NOTE : This utility method relies on introspection (and returns
* PropertyDescriptor) which is a part of java.beans package. The latter
* package could require bigger JDK in the future (with Java 9+). So it may
* be changed in the future.
- *
+ * <p>
* For interfaces, the iteration is depth first and the properties of
* superinterfaces are returned before those of their subinterfaces.
- *
- * @param beanClass
- * @return
+ *
+ * @param beanType
+ * the type whose properties to query
+ * @return a list of property descriptors of the given type
* @throws IntrospectionException
+ * if the introspection fails
*/
- public static List<PropertyDescriptor> getBeanPropertyDescriptor(
- final Class<?> beanClass) throws IntrospectionException {
+ public static List<PropertyDescriptor> getBeanPropertyDescriptors(
+ final Class<?> beanType) throws IntrospectionException {
// Oracle bug 4275879: Introspector does not consider superinterfaces of
// an interface
- if (beanClass.isInterface()) {
- List<PropertyDescriptor> propertyDescriptors = new ArrayList<PropertyDescriptor>();
+ if (beanType.isInterface()) {
+ List<PropertyDescriptor> propertyDescriptors = new ArrayList<>();
- for (Class<?> cls : beanClass.getInterfaces()) {
- propertyDescriptors.addAll(getBeanPropertyDescriptor(cls));
+ for (Class<?> cls : beanType.getInterfaces()) {
+ propertyDescriptors.addAll(getBeanPropertyDescriptors(cls));
}
- BeanInfo info = Introspector.getBeanInfo(beanClass);
+ BeanInfo info = Introspector.getBeanInfo(beanType);
propertyDescriptors.addAll(getPropertyDescriptors(info));
return propertyDescriptors;
} else {
- BeanInfo info = Introspector.getBeanInfo(beanClass);
+ BeanInfo info = Introspector.getBeanInfo(beanType);
return getPropertyDescriptors(info);
}
}
/**
- * Returns {@code propertyId} class for property declared in {@code clazz}.
- * Property could be of form "property.subProperty[.subProperty2]" i.e.
- * refer to some nested property.
- *
- * @param clazz
- * class where property is declared
- * @param propertyId
- * property of form "property" or
- * "property.subProperty[.subProperty2]"
- * @return class of the property
+ * Returns the type of the property with the given name and declaring class.
+ * The property name may refer to a nested property, eg.
+ * "property.subProperty" or "property.subProperty1.subProperty2". The
+ * property must have a public read method (or a chain of read methods in
+ * case of a nested property).
+ *
+ * @param beanType
+ * the type declaring the property
+ * @param propertyName
+ * the name of the property
+ * @return the property type
* @throws IntrospectionException
+ * if the introspection fails
*/
- public static Class<?> getPropertyType(Class<?> clazz, String propertyId)
+ public static Class<?> getPropertyType(Class<?> beanType,
+ String propertyName)
throws IntrospectionException {
- if (propertyId.contains(".")) {
- String[] parts = propertyId.split("\\.", 2);
- // Get the type of the field in the "cls" class
- Class<?> propertyBean = getPropertyType(clazz, parts[0]);
+ PropertyDescriptor descriptor = getPropertyDescriptor(beanType,
+ propertyName);
+ if (descriptor != null) {
+ return descriptor.getPropertyType();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the property descriptor for the property of the given name and
+ * declaring class. The property name may refer to a nested property, eg.
+ * "property.subProperty" or "property.subProperty1.subProperty2". The
+ * property must have a public read method (or a chain of read methods in
+ * case of a nested property).
+ *
+ * @param beanType
+ * the type declaring the property
+ * @param propertyName
+ * the name of the property
+ * @return the corresponding descriptor
+ * @throws IntrospectionException
+ * if the introspection fails
+ */
+ public static PropertyDescriptor getPropertyDescriptor(Class<?> beanType,
+ String propertyName) throws IntrospectionException {
+ if (propertyName.contains(".")) {
+ String[] parts = propertyName.split("\\.", 2);
+ // Get the type of the field in the bean class
+ Class<?> propertyBean = getPropertyType(beanType, parts[0]);
// Find the rest from the sub type
- return getPropertyType(propertyBean, parts[1]);
+ return getPropertyDescriptor(propertyBean, parts[1]);
} else {
- List<PropertyDescriptor> descriptors = getBeanPropertyDescriptor(
- clazz);
+ List<PropertyDescriptor> descriptors = getBeanPropertyDescriptors(
+ beanType);
for (PropertyDescriptor descriptor : descriptors) {
final Method getMethod = descriptor.getReadMethod();
- if (descriptor.getName().equals(propertyId) && getMethod != null
+ if (descriptor.getName().equals(propertyName)
+ && getMethod != null
&& getMethod.getDeclaringClass() != Object.class) {
- return descriptor.getPropertyType();
+ return descriptor;
}
}
return null;
@@ -118,8 +150,7 @@ public final class BeanUtil implements Serializable {
private static List<PropertyDescriptor> getPropertyDescriptors(
BeanInfo beanInfo) {
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
- List<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>(
- descriptors.length);
+ List<PropertyDescriptor> result = new ArrayList<>(descriptors.length);
for (PropertyDescriptor descriptor : descriptors) {
try {
Method readMethod = getMethodFromBridge(
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 dd563e9f0d..3813797a63 100644
--- a/server/src/main/java/com/vaadin/data/validator/BeanValidator.java
+++ b/server/src/main/java/com/vaadin/data/validator/BeanValidator.java
@@ -20,6 +20,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.BinaryOperator;
+import java.util.logging.Logger;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator.Context;
@@ -36,10 +37,12 @@ import com.vaadin.data.Validator;
* 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.
- *
+ * Note that a JSR-303 implementation (for instance
+ * <a href="http://hibernate.org/validator/">Hibernate Validator</a> or
+ * <a href="http://bval.apache.org/">Apache BVal</a>) must be present on the
+ * project classpath when using bean validation. Specification versions 1.0 and
+ * 1.1 are supported.
+ *
* @author Petri Hakala
* @author Vaadin Ltd.
*
@@ -47,7 +50,7 @@ import com.vaadin.data.Validator;
*/
public class BeanValidator implements Validator<Object> {
- private static final long serialVersionUID = 1L;
+ private static volatile Boolean beanValidationAvailable;
private static ValidatorFactory factory;
private String propertyName;
@@ -55,6 +58,32 @@ public class BeanValidator implements Validator<Object> {
private Locale locale;
/**
+ * Returns whether an implementation of JSR-303 version 1.0 or 1.1 is
+ * present on the classpath. If this method returns false, trying to create
+ * a {@code BeanValidator} instance will throw an
+ * {@code IllegalStateException}. If an implementation is not found, logs a
+ * level {@code FINE} message the first time it is run.
+ *
+ * @return {@code true} if bean validation is available, {@code false}
+ * otherwise.
+ */
+ public static boolean checkBeanValidationAvailable() {
+ if (beanValidationAvailable == null) {
+ try {
+ Class.forName(Validation.class.getName());
+ beanValidationAvailable = true;
+ } catch (ClassNotFoundException e) {
+ Logger.getLogger(BeanValidator.class.getName()).fine(
+ "A JSR-303 bean validation implementation not found on the classpath. "
+ + BeanValidator.class.getSimpleName()
+ + " cannot be used.");
+ beanValidationAvailable = false;
+ }
+ }
+ return beanValidationAvailable;
+ }
+
+ /**
* Creates a new JSR-303 {@code BeanValidator} that validates values of the
* specified property. Localizes validation messages using the
* {@linkplain Locale#getDefault() default locale}.
@@ -63,6 +92,8 @@ public class BeanValidator implements Validator<Object> {
* the bean type declaring the property, not null
* @param propertyName
* the property to validate, not null
+ * @throws IllegalStateException
+ * if {@link #checkBeanValidationAvailable()} returns false
*/
public BeanValidator(Class<?> beanType, String propertyName) {
this(beanType, propertyName, Locale.getDefault());
@@ -78,11 +109,19 @@ public class BeanValidator implements Validator<Object> {
* the property to validate, not null
* @param locale
* the locale to use, not null
+ * @throws IllegalStateException
+ * if {@link #checkBeanValidationAvailable()} returns false
*/
public BeanValidator(Class<?> beanType, String propertyName,
Locale locale) {
+ if (!checkBeanValidationAvailable()) {
+ throw new IllegalStateException("Cannot create a "
+ + BeanValidator.class.getSimpleName()
+ + ": a JSR-303 Bean Validation implementation not found on theclasspath");
+ }
Objects.requireNonNull(beanType, "bean class cannot be null");
Objects.requireNonNull(propertyName, "property name cannot be null");
+
this.beanType = beanType;
this.propertyName = propertyName;
setLocale(locale);
@@ -132,6 +171,7 @@ public class BeanValidator implements Validator<Object> {
*/
protected static ValidatorFactory getJavaxBeanValidatorFactory() {
if (factory == null) {
+ checkBeanValidationAvailable();
factory = Validation.buildDefaultValidatorFactory();
}
return factory;
diff --git a/server/src/test/java/com/vaadin/data/BeanBinderTest.java b/server/src/test/java/com/vaadin/data/BeanBinderTest.java
new file mode 100644
index 0000000000..a3e4d4b992
--- /dev/null
+++ b/server/src/test/java/com/vaadin/data/BeanBinderTest.java
@@ -0,0 +1,171 @@
+package com.vaadin.data;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.tests.data.bean.BeanToValidate;
+import com.vaadin.ui.TextField;
+
+public class BeanBinderTest {
+
+ BeanBinder<BeanToValidate> binder;
+
+ TextField nameField;
+ TextField ageField;
+
+ BeanToValidate p = new BeanToValidate();
+
+ @Before
+ public void setUp() {
+ binder = new BeanBinder<>(BeanToValidate.class);
+ p.setFirstname("Johannes");
+ p.setAge(32);
+ nameField = new TextField();
+ ageField = new TextField();
+ }
+
+ @Test
+ public void fieldBound_bindBean_fieldValueUpdated() {
+ binder.bind(nameField, "firstname");
+ binder.bind(p);
+
+ assertEquals("Johannes", nameField.getValue());
+ }
+
+ @Test
+ public void beanBound_bindField_fieldValueUpdated() {
+ binder.bind(p);
+ binder.bind(nameField, "firstname");
+
+ assertEquals("Johannes", nameField.getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void bindInvalidPropertyName_throws() {
+ binder.bind(nameField, "firstnaem");
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void bindNullPropertyName_throws() {
+ binder.bind(nameField, null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void bindNonReadableProperty_throws() {
+ binder.bind(nameField, "writeOnlyProperty");
+ }
+
+ @Test
+ public void beanBound_setValidFieldValue_propertyValueChanged() {
+ binder.bind(p);
+ binder.bind(nameField, "firstname");
+
+ nameField.setValue("Henri");
+
+ assertEquals("Henri", p.getFirstname());
+ }
+
+ @Test
+ public void readOnlyPropertyBound_setFieldValue_ignored() {
+ binder.bind(nameField, "readOnlyProperty");
+ binder.bind(p);
+
+ String propertyValue = p.getReadOnlyProperty();
+ nameField.setValue("Foo");
+
+ assertEquals(propertyValue, p.getReadOnlyProperty());
+ }
+
+ @Test
+ public void beanBound_setInvalidFieldValue_validationError() {
+ binder.bind(p);
+ binder.bind(nameField, "firstname");
+
+ nameField.setValue("H"); // too short
+
+ assertEquals("Johannes", p.getFirstname());
+ assertInvalid(nameField, "size must be between 3 and 16");
+ }
+
+ @Test
+ public void beanNotBound_setInvalidFieldValue_validationError() {
+ binder.bind(nameField, "firstname");
+
+ nameField.setValue("H"); // too short
+
+ assertInvalid(nameField, "size must be between 3 and 16");
+ }
+
+ @Test
+ public void explicitValidatorAdded_setInvalidFieldValue_explicitValidatorRunFirst() {
+ binder.forField(nameField).withValidator(name -> name.startsWith("J"),
+ "name must start with J").bind("firstname");
+
+ nameField.setValue("A");
+
+ assertInvalid(nameField, "name must start with J");
+ }
+
+ @Test
+ public void explicitValidatorAdded_setInvalidFieldValue_beanValidatorRun() {
+ binder.forField(nameField).withValidator(name -> name.startsWith("J"),
+ "name must start with J").bind("firstname");
+
+ nameField.setValue("J");
+
+ assertInvalid(nameField, "size must be between 3 and 16");
+ }
+
+ @Test(expected = ClassCastException.class)
+ public void fieldWithIncompatibleTypeBound_bindBean_throws() {
+ binder.bind(ageField, "age");
+ binder.bind(p);
+ }
+
+ @Test(expected = ClassCastException.class)
+ public void fieldWithIncompatibleTypeBound_loadBean_throws() {
+ binder.bind(ageField, "age");
+ binder.load(p);
+ }
+
+ @Test(expected = ClassCastException.class)
+ public void fieldWithIncompatibleTypeBound_saveBean_throws()
+ throws Throwable {
+ try {
+ binder.bind(ageField, "age");
+ binder.save(p);
+ } catch (RuntimeException e) {
+ throw e.getCause();
+ }
+ }
+
+ @Test
+ public void fieldWithConverterBound_bindBean_fieldValueUpdated() {
+ binder.forField(ageField).withConverter(Integer::valueOf,
+ String::valueOf).bind("age");
+ binder.bind(p);
+
+ assertEquals("32", ageField.getValue());
+ }
+
+ @Test(expected = ClassCastException.class)
+ public void fieldWithInvalidConverterBound_bindBean_fieldValueUpdated() {
+ binder.forField(ageField).withConverter(Float::valueOf,
+ String::valueOf).bind("age");
+ binder.bind(p);
+
+ assertEquals("32", ageField.getValue());
+ }
+
+ private void assertInvalid(HasValue<?> field, String message) {
+ List<ValidationError<?>> errors = binder.validate();
+ assertEquals(1, errors.size());
+ assertSame(field, errors.get(0).getField());
+ assertEquals(message, errors.get(0).getMessage());
+ }
+}
diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java
index fddae9ac99..f2c166f6ce 100644
--- a/server/src/test/java/com/vaadin/data/BinderTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderTest.java
@@ -377,7 +377,7 @@ public class BinderTest {
TextField field = new TextField();
StatusBean bean = new StatusBean();
bean.setStatus("1");
- Binder<StatusBean> binder = new Binder<StatusBean>();
+ Binder<StatusBean> binder = new Binder<>();
Binding<StatusBean, String, String> binding = binder.forField(field)
.withConverter(presentation -> {
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 fa8c438ee9..9f0b244360 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
@@ -42,6 +42,10 @@ public class BeanToValidate {
@Valid
private Address address;
+ private String readOnlyProperty = "READONLY DATA";
+
+ private String writeOnlyProperty;
+
public String getFirstname() {
return firstname;
}
@@ -105,4 +109,12 @@ public class BeanToValidate {
public void setAddress(Address address) {
this.address = address;
}
+
+ public String getReadOnlyProperty() {
+ return readOnlyProperty;
+ }
+
+ public void setWriteOnlyProperty(String writeOnlyProperty) {
+ this.writeOnlyProperty = writeOnlyProperty;
+ }
}