summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--all/src/main/templates/release-notes.html2
-rw-r--r--documentation/components/components-grid.asciidoc1
-rw-r--r--documentation/datamodel/datamodel-forms.asciidoc21
-rw-r--r--server/src/main/java/com/vaadin/annotations/PropertyId.java18
-rw-r--r--server/src/main/java/com/vaadin/data/BeanBinder.java679
-rw-r--r--server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java217
-rw-r--r--server/src/main/java/com/vaadin/data/Binder.java488
-rw-r--r--server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java85
-rw-r--r--server/src/main/java/com/vaadin/data/BinderPropertySet.java50
-rw-r--r--server/src/main/java/com/vaadin/data/BinderValidationStatus.java11
-rw-r--r--server/src/test/java/com/vaadin/data/BeanBinderPropertySetTest.java52
-rw-r--r--server/src/test/java/com/vaadin/data/BeanBinderTest.java39
-rw-r--r--server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java4
-rw-r--r--server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java144
-rw-r--r--server/src/test/java/com/vaadin/data/BinderInstanceFieldTest.java (renamed from server/src/test/java/com/vaadin/data/BeanBinderInstanceFieldTest.java)45
-rw-r--r--server/src/test/java/com/vaadin/data/BinderTest.java9
-rw-r--r--server/src/test/java/com/vaadin/data/Jsr303Test.java3
17 files changed, 1088 insertions, 780 deletions
diff --git a/all/src/main/templates/release-notes.html b/all/src/main/templates/release-notes.html
index 181e5794fd..9b58510f2c 100644
--- a/all/src/main/templates/release-notes.html
+++ b/all/src/main/templates/release-notes.html
@@ -120,7 +120,7 @@
<ul><h4>New Data Binding API related changes</h4>
<li>Components using <tt>Property</tt>, <tt>Item</tt> or <tt>Container</tt> API have been reimplemented for the new API, except <tt>Tree</tt>, <tt>Table</tt>, <tt>TreeTable</tt> and <tt>Calendar</tt></li>
<li>Framework 7 versions of the components available in the v7 compatibility package for easier migration, <a href="#legacycomponents">see list of legacy components</a></li>
- <li><tt>Binder</tt> is the replacement of <tt>FieldGroup</tt>, and similarly <tt>BeanBinder</tt> is the replacement of <tt>BeanFieldGroup</tt></li>
+ <li><tt>Binder</tt> is the replacement of <tt>FieldGroup</tt> and <tt>BeanFieldGroup</tt></li>
<li>Converters and Validators have been moved from Components to <tt>Binder</tt></li>
<li><tt>DataProvider</tt> is the replacement of <tt>Container</tt></li>
<ul>
diff --git a/documentation/components/components-grid.asciidoc b/documentation/components/components-grid.asciidoc
index abad9636de..085a7191f3 100644
--- a/documentation/components/components-grid.asciidoc
+++ b/documentation/components/components-grid.asciidoc
@@ -765,7 +765,6 @@ grid.getEditor().setSaveCaption("Tallenna");
grid.getEditor().setCancelCaption("Peruuta"));
----
-
[[components.grid.editing.validation]]
=== Handling Validation Errors
diff --git a/documentation/datamodel/datamodel-forms.asciidoc b/documentation/datamodel/datamodel-forms.asciidoc
index 7d3301ae4c..21ace02c80 100644
--- a/documentation/datamodel/datamodel-forms.asciidoc
+++ b/documentation/datamodel/datamodel-forms.asciidoc
@@ -417,12 +417,13 @@ If some other part of the application is also using the same instance, then that
== Binding Beans to Forms
The business objects used in an application are in most cases implemented as Java beans or POJOs.
-There is special support for that kind of business object in [classname]#BeanBinder#.
-It can use reflection based on bean property names to bind values. This reduces the amount of code you have to write when binding to fields in the bean.
+There is special support for that kind of business object in [classname]#Binder#.
+It can use reflection based on bean property names to bind values.
+This reduces the amount of code you have to write when binding to fields in the bean.
[source, java]
----
-BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+Binder<Person> binder = new Binder<>(Person.class);
// Bind based on property name
binder.bind(nameField, "name");
@@ -436,9 +437,9 @@ binder.forField(yearOfBirthField)
----
[NOTE]
-[classname]#BeanBinder# uses strings to identify the properties so it is not refactor safe.
+Code using strings to identify properties will cause exceptions during runtime if the string contains a typo or if the name of the setter and getter methods have been changed without also updating the string.
-[classname]#BeanBinder# will automatically use JSR 303 Bean Validation annotations from the bean class if a Bean Validation implementation is available.
+Bindings created based on a property name will automatically use JSR 303 Bean Validation annotations from the bean class if a Bean Validation implementation is available.
Constraints defined for properties in the bean will work in the same way as if configured when the binding is created.
[source, java]
@@ -461,7 +462,7 @@ Constraint annotations can also be defined on the bean level instead of being de
[NOTE]
Bean level validation can only be performed once the bean has been updated. This means that this functionality can only be used together with `setBean`. You need to trigger validation manually if using `readBean` and `writeBean`.
-Validation errors caused by that bean level validation might not be directly associated with any field component shown in the user interface, so [classname]#BeanBinder# cannot know where such messages should be displayed.
+Validation errors caused by that bean level validation might not be directly associated with any field component shown in the user interface, so [classname]#Binder# cannot know where such messages should be displayed.
Similarly to how the [methodname]#withStatusLabel# method can be used for defining where messages for a specific binding should be showed, we can also define a [classname]#Label# that is used for showing status messages that are not related to any specific field.
@@ -469,7 +470,7 @@ Similarly to how the [methodname]#withStatusLabel# method can be used for defini
----
Label formStatusLabel = new Label();
-BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+Binder<Person> binder = new Binder<>(Person.class);
binder.setStatusLabel(formStatusLabel);
@@ -541,14 +542,14 @@ public class PersonFormDesign extends FormLayout {
}
----
-Based on those files, we can create a subclass of the design that uses a [classname]#BeanBinder# to automatically connect bean properties to field instances.
+Based on those files, we can create a subclass of the design that uses a [classname]#Binder# to automatically connect bean properties to field instances.
This will look at all instance fields that are of a Field type in the class and try to find a bean property with the same name.
[source, java]
----
public class PersonForm extends PersonFormDesign {
- private BeanBinder<Person> binder
- = new BeanBinder<>(Person.class);
+ private Binder<Person> binder
+ = new Binder<>(Person.class);
public PersonForm(Person person) {
binder.bindInstanceFields(this);
diff --git a/server/src/main/java/com/vaadin/annotations/PropertyId.java b/server/src/main/java/com/vaadin/annotations/PropertyId.java
index b914699edf..62ac330c17 100644
--- a/server/src/main/java/com/vaadin/annotations/PropertyId.java
+++ b/server/src/main/java/com/vaadin/annotations/PropertyId.java
@@ -20,20 +20,18 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import com.vaadin.data.BeanBinder;
import com.vaadin.data.Binder;
import com.vaadin.data.HasValue;
/**
- * Defines the custom property name to be bound to a {@link Field} using
- * {@link Binder} or {@link BeanBinder}.
+ * Defines the custom property name to be bound to a {@link HasValue field
+ * component} using {@link Binder}.
* <p>
- * The automatic data binding in Binder and BeanBinder relies on a naming
- * convention by default: properties of an item are bound to similarly named
- * field components in given a editor object. If you want to map a property with
- * a different name (ID) to a {@link HasValue}, you can use this annotation for
- * the member fields, with the name (ID) of the desired property as the
- * parameter.
+ * The automatic data binding in Binder relies on a naming convention by
+ * default: properties of an item are bound to similarly named field components
+ * in given a editor object. If you want to map a property with a different name
+ * (ID) to a {@link HasValue}, you can use this annotation for the member
+ * fields, with the name (ID) of the desired property as the parameter.
* <p>
* In following usage example, the text field would be bound to property "foo"
* in the Entity class. <code>
@@ -49,7 +47,7 @@ import com.vaadin.data.HasValue;
{
Editor editor = new Editor();
- BeanBinder<Entity> binder = new BeanBinder(Entity.class);
+ Binder&lt;Entity&gt; binder = new Binder(Entity.class);
binder.bindInstanceFields(editor);
}
</pre>
diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java
deleted file mode 100644
index 898f2cd37d..0000000000
--- a/server/src/main/java/com/vaadin/data/BeanBinder.java
+++ /dev/null
@@ -1,679 +0,0 @@
-/*
- * 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.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.BiConsumer;
-
-import com.googlecode.gentyref.GenericTypeReflector;
-import com.vaadin.annotations.PropertyId;
-import com.vaadin.data.util.BeanUtil;
-import com.vaadin.data.validator.BeanValidator;
-import com.vaadin.server.SerializableFunction;
-import com.vaadin.server.SerializablePredicate;
-import com.vaadin.server.Setter;
-import com.vaadin.ui.Label;
-import com.vaadin.util.ReflectTools;
-
-/**
- * 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 8.0
- */
-public class BeanBinder<BEAN> extends Binder<BEAN> {
-
- /**
- * Represents the binding between a single field and a bean property.
- *
- * @param <BEAN>
- * the bean type
- * @param <TARGET>
- * the target property type
- */
- public interface BeanBindingBuilder<BEAN, TARGET>
- extends BindingBuilder<BEAN, TARGET> {
-
- @Override
- public BeanBindingBuilder<BEAN, TARGET> withValidator(
- Validator<? super TARGET> validator);
-
- @Override
- public default BeanBindingBuilder<BEAN, TARGET> withValidator(
- SerializablePredicate<? super TARGET> predicate,
- String message) {
- return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withValidator(
- predicate, message);
- }
-
- @Override
- default BeanBindingBuilder<BEAN, TARGET> withValidator(
- SerializablePredicate<? super TARGET> predicate,
- ErrorMessageProvider errorMessageProvider) {
- return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withValidator(
- predicate, errorMessageProvider);
- }
-
- @Override
- default BeanBindingBuilder<BEAN, TARGET> withNullRepresentation(
- TARGET nullRepresentation) {
- return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withNullRepresentation(
- nullRepresentation);
- }
-
- @Override
- public BeanBindingBuilder<BEAN, TARGET> asRequired(
- ErrorMessageProvider errorMessageProvider);
-
- @Override
- public default BeanBindingBuilder<BEAN, TARGET> asRequired(
- String errorMessage) {
- return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.asRequired(
- errorMessage);
- }
-
- @Override
- public <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter(
- Converter<TARGET, NEWTARGET> converter);
-
- @Override
- public default <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter(
- SerializableFunction<TARGET, NEWTARGET> toModel,
- SerializableFunction<NEWTARGET, TARGET> toPresentation) {
- return (BeanBindingBuilder<BEAN, NEWTARGET>) BindingBuilder.super.withConverter(
- toModel, toPresentation);
- }
-
- @Override
- public default <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter(
- SerializableFunction<TARGET, NEWTARGET> toModel,
- SerializableFunction<NEWTARGET, TARGET> toPresentation,
- String errorMessage) {
- return (BeanBindingBuilder<BEAN, NEWTARGET>) BindingBuilder.super.withConverter(
- toModel, toPresentation, errorMessage);
- }
-
- @Override
- public BeanBindingBuilder<BEAN, TARGET> withValidationStatusHandler(
- BindingValidationStatusHandler handler);
-
- @Override
- public default BeanBindingBuilder<BEAN, TARGET> withStatusLabel(
- Label label) {
- return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withStatusLabel(
- label);
- }
-
- /**
- * 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
- * @return the newly created binding
- *
- * @throws IllegalArgumentException
- * if the property name is invalid
- * @throws IllegalArgumentException
- * if the property has no accessible getter
- *
- * @see BindingBuilder#bind(ValueProvider, Setter)
- */
- public Binding<BEAN, TARGET> bind(String propertyName);
- }
-
- /**
- * An internal implementation of {@link BeanBindingBuilder}.
- *
- * @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 BindingBuilderImpl<BEAN, FIELDVALUE, TARGET>
- implements BeanBindingBuilder<BEAN, TARGET> {
-
- /**
- * 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 statusHandler
- * the handler to notify of status changes, not null
- */
- protected BeanBindingImpl(BeanBinder<BEAN> binder,
- HasValue<FIELDVALUE> field,
- Converter<FIELDVALUE, TARGET> converter,
- BindingValidationStatusHandler statusHandler) {
- super(binder, field, converter, statusHandler);
- }
-
- @Override
- public BeanBindingBuilder<BEAN, TARGET> withValidator(
- Validator<? super TARGET> validator) {
- return (BeanBindingBuilder<BEAN, TARGET>) super.withValidator(
- validator);
- }
-
- @Override
- public <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter(
- Converter<TARGET, NEWTARGET> converter) {
- return (BeanBindingBuilder<BEAN, NEWTARGET>) super.withConverter(
- converter);
- }
-
- @Override
- public BeanBindingBuilder<BEAN, TARGET> withValidationStatusHandler(
- BindingValidationStatusHandler handler) {
- return (BeanBindingBuilder<BEAN, TARGET>) super.withValidationStatusHandler(
- handler);
- }
-
- @Override
- public BeanBindingBuilder<BEAN, TARGET> asRequired(
- ErrorMessageProvider errorMessageProvider) {
- return (BeanBindingBuilder<BEAN, TARGET>) super.asRequired(
- errorMessageProvider);
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public Binding<BEAN, TARGET> bind(String propertyName) {
- checkUnbound();
-
- BindingBuilder<BEAN, Object> finalBinding;
-
- PropertyDescriptor descriptor = getDescriptor(propertyName);
-
- Method getter = descriptor.getReadMethod();
- Method setter = descriptor.getWriteMethod();
-
- finalBinding = withConverter(
- createConverter(getter.getReturnType()), false);
-
- if (BeanUtil.checkBeanValidationAvailable()) {
- finalBinding = finalBinding.withValidator(
- new BeanValidator(getBinder().beanType, propertyName));
- }
-
- try {
- return (Binding<BEAN, TARGET>) finalBinding.bind(
- bean -> invokeWrapExceptions(getter, bean),
- (bean, value) -> invokeWrapExceptions(setter, bean,
- value));
- } finally {
- getBinder().boundProperties.add(propertyName);
- getBinder().incompleteMemberFieldBindings.remove(getField());
- }
- }
-
- @Override
- protected BeanBinder<BEAN> getBinder() {
- return (BeanBinder<BEAN>) super.getBinder();
- }
-
- private static Object invokeWrapExceptions(Method method, Object target,
- Object... parameters) {
- if (method == null) {
- return null;
- }
- try {
- return method.invoke(target, parameters);
- } 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(Class<?> getterType) {
- return Converter.from(fieldValue -> cast(fieldValue, getterType),
- propertyValue -> (TARGET) propertyValue, exception -> {
- throw new RuntimeException(exception);
- });
- }
-
- private <T> T cast(TARGET value, Class<T> clazz) {
- if (clazz.isPrimitive()) {
- return (T) ReflectTools.convertPrimitiveType(clazz).cast(value);
- } else {
- return clazz.cast(value);
- }
- }
- }
-
- private final Class<? extends BEAN> beanType;
- private final Set<String> boundProperties = new HashSet<>();
- private final Map<HasValue<?>, BeanBindingImpl<BEAN, ?, ?>> incompleteMemberFieldBindings = new IdentityHashMap<>();
-
- /**
- * 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) {
- BeanUtil.checkBeanValidationAvailable();
- this.beanType = beanType;
- }
-
- @Override
- public <FIELDVALUE> BeanBindingBuilder<BEAN, FIELDVALUE> forField(
- HasValue<FIELDVALUE> field) {
- return (BeanBindingBuilder<BEAN, FIELDVALUE>) super.forField(field);
- }
-
- /**
- * Creates a new binding for the given field. The returned builder may be
- * further configured before invoking {@link #bindInstanceFields(Object)}.
- * Unlike with the {@link #forField(HasValue)} method, no explicit call to
- * {@link BeanBindingBuilder#bind(String)} is needed to complete this
- * binding in the case that the name of the field matches a field name found
- * in the bean.
- *
- * @param <FIELDVALUE>
- * the value type of the field
- * @param field
- * the field to be bound, not null
- * @return the new binding builder
- *
- * @see #forField(HasValue)
- * @see #bindInstanceFields(Object)
- */
- public <FIELDVALUE> BeanBindingBuilder<BEAN, FIELDVALUE> forMemberField(
- HasValue<FIELDVALUE> field) {
- incompleteMemberFieldBindings.put(field, null);
- return forField(field);
- }
-
- /**
- * 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
- * @return the newly created binding
- *
- * @throws IllegalArgumentException
- * if the property name is invalid
- * @throws IllegalArgumentException
- * if the property has no accessible getter
- *
- * @see #bind(HasValue, ValueProvider, Setter)
- */
- public <FIELDVALUE> Binding<BEAN, FIELDVALUE> bind(
- HasValue<FIELDVALUE> field, String propertyName) {
- return forField(field).bind(propertyName);
- }
-
- @Override
- public BeanBinder<BEAN> withValidator(Validator<? super BEAN> validator) {
- return (BeanBinder<BEAN>) super.withValidator(validator);
- }
-
- @SuppressWarnings("unchecked")
- @Override
- protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> createBinding(
- HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter,
- BindingValidationStatusHandler handler) {
- Objects.requireNonNull(field, "field cannot be null");
- Objects.requireNonNull(converter, "converter cannot be null");
- if (incompleteMemberFieldBindings.containsKey(field)) {
- BeanBindingImpl<BEAN, FIELDVALUE, TARGET> newBinding = doCreateBinding(
- field, converter, handler);
- incompleteMemberFieldBindings.put(field, newBinding);
- return newBinding;
- } else {
- return (BeanBindingImpl<BEAN, FIELDVALUE, TARGET>) super.createBinding(
- field, converter, handler);
- }
- }
-
- @Override
- protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> doCreateBinding(
- HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter,
- BindingValidationStatusHandler handler) {
- return new BeanBindingImpl<>(this, field, converter, handler);
- }
-
- /**
- * Binds member fields found in the given object.
- * <p>
- * This method processes all (Java) member fields whose type extends
- * {@link HasValue} and that can be mapped to a property id. Property id
- * mapping is done based on the field name or on a @{@link PropertyId}
- * annotation on the field. All non-null unbound fields for which a property
- * id can be determined are bound to the property id.
- * </p>
- * <p>
- * For example:
- *
- * <pre>
- * public class MyForm extends VerticalLayout {
- * private TextField firstName = new TextField("First name");
- * &#64;PropertyId("last")
- * private TextField lastName = new TextField("Last name");
- *
- * MyForm myForm = new MyForm();
- * ...
- * binder.bindMemberFields(myForm);
- * </pre>
- *
- * </p>
- * This binds the firstName TextField to a "firstName" property in the item,
- * lastName TextField to a "last" property.
- * <p>
- * It's not always possible to bind a field to a property because their
- * types are incompatible. E.g. custom converter is required to bind
- * {@code HasValue<String>} and {@code Integer} property (that would be a
- * case of "age" property). In such case {@link IllegalStateException} will
- * be thrown unless the field has been configured manually before calling
- * the {@link #bindInstanceFields(Object)} method.
- * <p>
- * It's always possible to do custom binding for any field: the
- * {@link #bindInstanceFields(Object)} method doesn't override existing
- * bindings.
- *
- * @param objectWithMemberFields
- * The object that contains (Java) member fields to bind
- * @throws IllegalStateException
- * if there are incompatible HasValue<T> and property types
- */
- public void bindInstanceFields(Object objectWithMemberFields) {
- Class<?> objectClass = objectWithMemberFields.getClass();
-
- getFieldsInDeclareOrder(objectClass).stream()
- .filter(memberField -> HasValue.class
- .isAssignableFrom(memberField.getType()))
- .forEach(memberField -> handleProperty(memberField,
- objectWithMemberFields,
- (property, type) -> bindProperty(objectWithMemberFields,
- memberField, property, type)));
- }
-
- @SuppressWarnings("unchecked")
- private BeanBindingImpl<BEAN, ?, ?> getIncompleteMemberFieldBinding(
- Field memberField, Object objectWithMemberFields) {
- memberField.setAccessible(true);
- try {
- return incompleteMemberFieldBindings
- .get(memberField.get(objectWithMemberFields));
- } catch (IllegalArgumentException | IllegalAccessException e) {
- throw new RuntimeException(e);
- } finally {
- memberField.setAccessible(false);
- }
- }
-
- /**
- * Binds {@code property} with {@code propertyType} to the field in the
- * {@code objectWithMemberFields} instance using {@code memberField} as a
- * reference to a member.
- *
- * @param objectWithMemberFields
- * the object that contains (Java) member fields to build and
- * bind
- * @param memberField
- * reference to a member field to bind
- * @param property
- * property name to bind
- * @param propertyType
- * type of the property
- */
- protected void bindProperty(Object objectWithMemberFields,
- Field memberField, String property, Class<?> propertyType) {
- Type valueType = GenericTypeReflector.getTypeParameter(
- memberField.getGenericType(),
- HasValue.class.getTypeParameters()[0]);
- if (valueType == null) {
- throw new IllegalStateException(String.format(
- "Unable to detect value type for the member '%s' in the "
- + "class '%s'.",
- memberField.getName(),
- objectWithMemberFields.getClass().getName()));
- }
- if (propertyType.equals(GenericTypeReflector.erase(valueType))) {
- HasValue<?> field;
- // Get the field from the object
- try {
- field = (HasValue<?>) ReflectTools.getJavaFieldValue(
- objectWithMemberFields, memberField, HasValue.class);
- } catch (IllegalArgumentException | IllegalAccessException
- | InvocationTargetException e) {
- // If we cannot determine the value, just skip the field
- return;
- }
- if (field == null) {
- field = makeFieldInstance(
- (Class<? extends HasValue<?>>) memberField.getType());
- initializeField(objectWithMemberFields, memberField, field);
- }
- forField(field).bind(property);
- } else {
- throw new IllegalStateException(String.format(
- "Property type '%s' doesn't "
- + "match the field type '%s'. "
- + "Binding should be configured manually using converter.",
- propertyType.getName(), valueType.getTypeName()));
- }
- }
-
- /**
- * Makes an instance of the field type {@code fieldClass}.
- * <p>
- * The resulting field instance is used to bind a property to it using the
- * {@link #bindInstanceFields(Object)} method.
- * <p>
- * The default implementation relies on the default constructor of the
- * class. If there is no suitable default constructor or you want to
- * configure the instantiated class then override this method and provide
- * your own implementation.
- *
- * @see #bindInstanceFields(Object)
- * @param fieldClass
- * type of the field
- * @return a {@code fieldClass} instance object
- */
- protected HasValue<?> makeFieldInstance(
- Class<? extends HasValue<?>> fieldClass) {
- try {
- return fieldClass.newInstance();
- } catch (InstantiationException | IllegalAccessException e) {
- throw new IllegalStateException(
- String.format("Couldn't create an '%s' type instance",
- fieldClass.getName()),
- e);
- }
- }
-
- /**
- * Returns an array containing {@link Field} objects reflecting all the
- * fields of the class or interface represented by this Class object. The
- * elements in the array returned are sorted in declare order from sub class
- * to super class.
- *
- * @param searchClass
- * class to introspect
- * @return list of all fields in the class considering hierarchy
- */
- protected List<Field> getFieldsInDeclareOrder(Class<?> searchClass) {
- ArrayList<Field> memberFieldInOrder = new ArrayList<>();
-
- while (searchClass != null) {
- memberFieldInOrder
- .addAll(Arrays.asList(searchClass.getDeclaredFields()));
- searchClass = searchClass.getSuperclass();
- }
- return memberFieldInOrder;
- }
-
- @Override
- protected void checkBindingsCompleted(String methodName) {
- if (!incompleteMemberFieldBindings.isEmpty()) {
- throw new IllegalStateException(
- "All bindings created with forMemberField must "
- + "be completed with bindInstanceFields before calling "
- + methodName);
- }
- super.checkBindingsCompleted(methodName);
- }
-
- private void initializeField(Object objectWithMemberFields,
- Field memberField, HasValue<?> value) {
- try {
- ReflectTools.setJavaFieldValue(objectWithMemberFields, memberField,
- value);
- } catch (IllegalArgumentException | IllegalAccessException
- | InvocationTargetException e) {
- throw new IllegalStateException(
- String.format("Could not assign value to field '%s'",
- memberField.getName()),
- e);
- }
- }
-
- private void handleProperty(Field field, Object objectWithMemberFields,
- BiConsumer<String, Class<?>> propertyHandler) {
-
- Optional<PropertyDescriptor> descriptor = getPropertyDescriptor(field);
-
- if (!descriptor.isPresent()) {
- return;
- }
-
- String propertyName = descriptor.get().getName();
- if (boundProperties.contains(propertyName)) {
- return;
- }
-
- BeanBindingImpl<BEAN, ?, ?> tentativeBinding = getIncompleteMemberFieldBinding(
- field, objectWithMemberFields);
- if (tentativeBinding != null) {
- tentativeBinding.bind(propertyName);
- return;
- }
-
- propertyHandler.accept(propertyName,
- descriptor.get().getPropertyType());
- boundProperties.add(propertyName);
- }
-
- private Optional<PropertyDescriptor> getPropertyDescriptor(Field field) {
- PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class);
-
- String propertyId;
- if (propertyIdAnnotation != null) {
- // @PropertyId(propertyId) always overrides property id
- propertyId = propertyIdAnnotation.value();
- } else {
- propertyId = field.getName();
- }
-
- List<PropertyDescriptor> descriptors;
- try {
- descriptors = BeanUtil.getBeanPropertyDescriptors(beanType);
- } catch (IntrospectionException e) {
- throw new IllegalArgumentException(String.format(
- "Could not resolve bean '%s' properties (see the cause):",
- beanType.getName()), e);
- }
- Optional<PropertyDescriptor> propertyDescitpor = descriptors.stream()
- .filter(descriptor -> minifyFieldName(descriptor.getName())
- .equals(minifyFieldName(propertyId)))
- .findFirst();
- return propertyDescitpor;
- }
-
- private String minifyFieldName(String fieldName) {
- return fieldName.toLowerCase(Locale.ENGLISH).replace("_", "");
- }
-
-}
diff --git a/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java b/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java
new file mode 100644
index 0000000000..3fcc40f6df
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java
@@ -0,0 +1,217 @@
+/*
+ * 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.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.vaadin.data.Binder.BindingBuilder;
+import com.vaadin.data.util.BeanUtil;
+import com.vaadin.data.validator.BeanValidator;
+import com.vaadin.server.Setter;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * A {@link BinderPropertySet} that uses reflection to find bean properties.
+ *
+ * @author Vaadin Ltd
+ *
+ * @since
+ *
+ * @param <T>
+ * the type of the bean
+ */
+public class BeanBinderPropertySet<T> implements BinderPropertySet<T> {
+
+ private static class BeanBinderPropertyDefinition<T, V>
+ implements BinderPropertyDefinition<T, V> {
+
+ private transient PropertyDescriptor descriptor;
+ private final BeanBinderPropertySet<T> propertySet;
+
+ public BeanBinderPropertyDefinition(
+ BeanBinderPropertySet<T> propertySet,
+ PropertyDescriptor descriptor) {
+ this.propertySet = propertySet;
+ this.descriptor = descriptor;
+
+ if (descriptor.getReadMethod() == null) {
+ throw new IllegalArgumentException(
+ "Bean property has no accessible getter: "
+ + propertySet.beanType + "."
+ + descriptor.getName());
+ }
+
+ }
+
+ @Override
+ public ValueProvider<T, V> getGetter() {
+ return bean -> {
+ Method readMethod = descriptor.getReadMethod();
+ Object value = invokeWrapExceptions(readMethod, bean);
+ return getType().cast(value);
+ };
+ }
+
+ @Override
+ public Optional<Setter<T, V>> getSetter() {
+ Method setter = descriptor.getWriteMethod();
+ if (setter == null) {
+ return Optional.empty();
+ }
+
+ return Optional.of(
+ (bean, value) -> invokeWrapExceptions(setter, bean, value));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Class<V> getType() {
+ return (Class<V>) ReflectTools
+ .convertPrimitiveType(descriptor.getPropertyType());
+ }
+
+ @Override
+ public BindingBuilder<T, V> beforeBind(
+ BindingBuilder<T, V> originalBuilder) {
+ if (BeanUtil.checkBeanValidationAvailable()) {
+ return originalBuilder.withValidator(new BeanValidator(
+ getPropertySet().beanType, descriptor.getName()));
+ } else {
+ return originalBuilder;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return descriptor.getName();
+ }
+
+ @Override
+ public BeanBinderPropertySet<T> getPropertySet() {
+ return propertySet;
+ }
+
+ private void writeObject(ObjectOutputStream stream) throws IOException {
+ stream.defaultWriteObject();
+ stream.writeObject(descriptor.getName());
+ stream.writeObject(propertySet.beanType);
+ }
+
+ private void readObject(ObjectInputStream stream)
+ throws IOException, ClassNotFoundException {
+ stream.defaultReadObject();
+
+ String propertyName = (String) stream.readObject();
+ @SuppressWarnings("unchecked")
+ Class<T> beanType = (Class<T>) stream.readObject();
+
+ try {
+ descriptor = BeanUtil.getBeanPropertyDescriptors(beanType)
+ .stream()
+ .filter(descriptor -> descriptor.getName()
+ .equals(propertyName))
+ .findAny()
+ .orElseThrow(() -> new IOException(
+ "Property " + propertyName + " not found for "
+ + beanType.getName()));
+ } catch (IntrospectionException e) {
+ throw new IOException(e);
+ }
+ }
+ }
+
+ private static final ConcurrentMap<Class<?>, BeanBinderPropertySet<?>> instances = new ConcurrentHashMap<>();
+
+ private final Class<T> beanType;
+
+ private final Map<String, BinderPropertyDefinition<T, ?>> definitions;
+
+ private BeanBinderPropertySet(Class<T> beanType) {
+ this.beanType = beanType;
+
+ try {
+ definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream()
+ .filter(BeanBinderPropertySet::hasReadMethod)
+ .map(descriptor -> new BeanBinderPropertyDefinition<>(this,
+ descriptor))
+ .collect(Collectors.toMap(BinderPropertyDefinition::getName,
+ Function.identity()));
+ } catch (IntrospectionException e) {
+ throw new IllegalArgumentException(
+ "Cannot find property descriptors for "
+ + beanType.getName(),
+ e);
+ }
+ }
+
+ /**
+ * Gets a {@link BeanBinderPropertySet} for the given bean type.
+ *
+ * @param beanType
+ * the bean type to get a property set for, not <code>null</code>
+ * @return the bean binder property set, not <code>null</code>
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> BinderPropertySet<T> get(Class<? extends T> beanType) {
+ Objects.requireNonNull(beanType, "Bean type cannot be null");
+
+ // Cache the reflection results
+ return (BinderPropertySet<T>) instances.computeIfAbsent(beanType,
+ BeanBinderPropertySet::new);
+ }
+
+ @Override
+ public Stream<BinderPropertyDefinition<T, ?>> getProperties() {
+ return definitions.values().stream();
+ }
+
+ @Override
+ public Optional<BinderPropertyDefinition<T, ?>> getProperty(String name) {
+ return Optional.ofNullable(definitions.get(name));
+ }
+
+ private static boolean hasReadMethod(PropertyDescriptor descriptor) {
+ return descriptor.getReadMethod() != null;
+ }
+
+ private static Object invokeWrapExceptions(Method method, Object target,
+ Object... parameters) {
+ try {
+ return method.invoke(target, parameters);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Property set for bean " + beanType.getName();
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java
index e818894f15..4695e372c1 100644
--- a/server/src/main/java/com/vaadin/data/Binder.java
+++ b/server/src/main/java/com/vaadin/data/Binder.java
@@ -16,10 +16,14 @@
package com.vaadin.data;
import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -28,10 +32,15 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.BiConsumer;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import com.googlecode.gentyref.GenericTypeReflector;
+import com.vaadin.annotations.PropertyId;
import com.vaadin.data.HasValue.ValueChangeEvent;
import com.vaadin.data.converter.StringToIntegerConverter;
+import com.vaadin.data.validator.BeanValidator;
import com.vaadin.event.EventRouter;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.SerializableFunction;
@@ -43,6 +52,7 @@ import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Component;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;
+import com.vaadin.util.ReflectTools;
/**
* Connects one or more {@code Field} components to properties of a backing data
@@ -182,6 +192,37 @@ public class Binder<BEAN> implements Serializable {
Setter<BEAN, TARGET> setter);
/**
+ * Completes this binding by connecting the field to the property with
+ * the given name. The getter and setter of the property are looked up
+ * using a {@link BinderPropertySet}.
+ * <p>
+ * For a <code>Binder</code> created using the
+ * {@link Binder#Binder(Class)} constructor, introspection will be used
+ * to find a Java Bean property. If a JSR-303 bean validation
+ * implementation is present on the classpath, a {@link BeanValidator}
+ * is also added to the 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
+ * @return the newly created binding
+ *
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ * @throws IllegalArgumentException
+ * if the property has no accessible getter
+ * @throws IllegalStateException
+ * if the binder is not configured with an appropriate
+ * {@link BinderPropertySet}
+ *
+ * @see Binder.BindingBuilder#bind(ValueProvider, Setter)
+ */
+ public Binding<BEAN, TARGET> bind(String propertyName);
+
+ /**
* Adds a validator to this binding. Validators are applied, in
* registration order, when the field value is written to the backing
* property. If any validator returns a failure, the property value is
@@ -560,6 +601,46 @@ public class Binder<BEAN> implements Serializable {
}
@Override
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public Binding<BEAN, TARGET> bind(String propertyName) {
+ Objects.requireNonNull(propertyName,
+ "Property name cannot be null");
+ checkUnbound();
+
+ BinderPropertyDefinition<BEAN, ?> definition = getBinder().propertySet
+ .getProperty(propertyName)
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Could not resolve property name " + propertyName
+ + " from " + getBinder().propertySet));
+
+ ValueProvider<BEAN, ?> getter = definition.getGetter();
+ Setter<BEAN, ?> setter = definition.getSetter()
+ .orElse((bean, value) -> {
+ // Setter ignores value
+ });
+
+ BindingBuilder finalBinding = withConverter(
+ createConverter(definition.getType()), false);
+
+ finalBinding = definition.beforeBind(finalBinding);
+
+ try {
+ return finalBinding.bind(getter, setter);
+ } finally {
+ getBinder().boundProperties.add(propertyName);
+ getBinder().incompleteMemberFieldBindings.remove(getField());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Converter<TARGET, Object> createConverter(Class<?> getterType) {
+ return Converter.from(fieldValue -> getterType.cast(fieldValue),
+ propertyValue -> (TARGET) propertyValue, exception -> {
+ throw new RuntimeException(exception);
+ });
+ }
+
+ @Override
public BindingBuilder<BEAN, TARGET> withValidator(
Validator<? super TARGET> validator) {
checkUnbound();
@@ -960,6 +1041,15 @@ public class Binder<BEAN> implements Serializable {
}
}
+ private final BinderPropertySet<BEAN> propertySet;
+
+ /**
+ * Property names that have been used for creating a binding.
+ */
+ private final Set<String> boundProperties = new HashSet<>();
+
+ private final Map<HasValue<?>, BindingBuilder<BEAN, ?>> incompleteMemberFieldBindings = new IdentityHashMap<>();
+
private BEAN bean;
private final Set<BindingImpl<BEAN, ?, ?>> bindings = new LinkedHashSet<>();
@@ -979,6 +1069,84 @@ public class Binder<BEAN> implements Serializable {
private boolean hasChanges = false;
/**
+ * Creates a binder using a custom {@link BinderPropertySet} implementation
+ * for finding and resolving property names for
+ * {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and
+ * {@link BindingBuilder#bind(String)}.
+ *
+ * @param propertySet
+ * the binder property set implementation to use, not
+ * <code>null</code>.
+ */
+ protected Binder(BinderPropertySet<BEAN> propertySet) {
+ Objects.requireNonNull(propertySet, "propertySet cannot be null");
+ this.propertySet = propertySet;
+ }
+
+ /**
+ * Creates a new binder that uses reflection based on the provided bean type
+ * to resolve bean properties. If a JSR-303 bean validation implementation
+ * is present on the classpath, a {@link BeanValidator} is added to each
+ * binding that is defined using a property name.
+ *
+ * @param beanType
+ * the bean type to use, not <code>null</code>
+ */
+ public Binder(Class<BEAN> beanType) {
+ this(BeanBinderPropertySet.get(beanType));
+ }
+
+ /**
+ * Creates a new binder without support for creating bindings based on
+ * property names. Use an alternative constructor, such as
+ * {@link Binder#Binder(Class)}, to create a binder that support creating
+ * bindings based on instance fields through
+ * {@link #bindInstanceFields(Object)}, or based on a property name through
+ * {@link #bind(HasValue, String)} or {@link BindingBuilder#bind(String)}.
+ */
+ public Binder() {
+ this(new BinderPropertySet<BEAN>() {
+ @Override
+ public Stream<BinderPropertyDefinition<BEAN, ?>> getProperties() {
+ throw new IllegalStateException(
+ "A Binder created with the default constructor doesn't support listing properties.");
+ }
+
+ @Override
+ public Optional<BinderPropertyDefinition<BEAN, ?>> getProperty(
+ String name) {
+ throw new IllegalStateException(
+ "A Binder created with the default constructor doesn't support finding properties by name.");
+ }
+ });
+ }
+
+ /**
+ * Creates a binder using a custom {@link BinderPropertySet} implementation
+ * for finding and resolving property names for
+ * {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and
+ * {@link BindingBuilder#bind(String)}.
+ * <p>
+ * This functionality is provided as static method instead of as a public
+ * constructor in order to make it possible to use a custom property set
+ * without creating a subclass while still leaving the public constructors
+ * focused on the common use cases.
+ *
+ * @see Binder#Binder()
+ * @see Binder#Binder(Class)
+ *
+ * @param propertySet
+ * the binder property set implementation to use, not
+ * <code>null</code>.
+ * @return a new binder using the provided property set, not
+ * <code>null</code>
+ */
+ public static <BEAN> Binder<BEAN> withPropertySet(
+ BinderPropertySet<BEAN> propertySet) {
+ return new Binder<>(propertySet);
+ }
+
+ /**
* Returns the bean that has been bound with {@link #bind}, or null if a
* bean is not currently bound.
*
@@ -1021,6 +1189,29 @@ public class Binder<BEAN> implements Serializable {
}
/**
+ * Creates a new binding for the given field. The returned builder may be
+ * further configured before invoking {@link #bindInstanceFields(Object)}.
+ * Unlike with the {@link #forField(HasValue)} method, no explicit call to
+ * {@link BindingBuilder#bind(String)} is needed to complete this binding in
+ * the case that the name of the field matches a field name found in the
+ * bean.
+ *
+ * @param <FIELDVALUE>
+ * the value type of the field
+ * @param field
+ * the field to be bound, not null
+ * @return the new binding builder
+ *
+ * @see #forField(HasValue)
+ * @see #bindInstanceFields(Object)
+ */
+ public <FIELDVALUE> BindingBuilder<BEAN, FIELDVALUE> forMemberField(
+ HasValue<FIELDVALUE> field) {
+ incompleteMemberFieldBindings.put(field, null);
+ return forField(field);
+ }
+
+ /**
* Binds a field to a bean property represented by the given getter and
* setter pair. The functions are used to update the field value from the
* property and to store the field value to the property, respectively.
@@ -1034,7 +1225,7 @@ public class Binder<BEAN> implements Serializable {
* {@link HasValue#getEmptyValue()}. This conversion is one-way only, if you
* want to have a two-way mapping back to {@code null}, use
* {@link #forField(HasValue)} and
- * {@link BindingBuilder#withNullRepresentation(Object))}.
+ * {@link BindingBuilder#withNullRepresentation(Object)}.
* <p>
* When a bean is bound with {@link Binder#setBean(BEAN)}, the field value
* is set to the return value of the given getter. The property value is
@@ -1079,6 +1270,42 @@ public class Binder<BEAN> implements Serializable {
}
/**
+ * Binds the given field to the property with the given name. The getter and
+ * setter of the property are looked up using a {@link BinderPropertySet}.
+ * <p>
+ * For a <code>Binder</code> created using the {@link Binder#Binder(Class)}
+ * constructor, introspection will be used to find a Java Bean property. If
+ * a JSR-303 bean validation implementation is present on the classpath, a
+ * {@link BeanValidator} is also added to the 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
+ * @return the newly created binding
+ *
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ * @throws IllegalArgumentException
+ * if the property has no accessible getter
+ * @throws IllegalStateException
+ * if the binder is not configured with an appropriate
+ * {@link BinderPropertySet}
+ *
+ * @see #bind(HasValue, ValueProvider, Setter)
+ */
+ public <FIELDVALUE> Binding<BEAN, FIELDVALUE> bind(
+ HasValue<FIELDVALUE> field, String propertyName) {
+ return forField(field).bind(propertyName);
+ }
+
+ /**
* Binds the given bean to all the fields added to this Binder. A
* {@code null} value removes a currently bound bean.
* <p>
@@ -1400,9 +1627,9 @@ public class Binder<BEAN> implements Serializable {
* Only the one validation error message is shown in this label at a time.
* <p>
* This is a convenience method for
- * {@link #setValidationStatusHandler(BinderValidationStatusHandler)}, which means
- * that this method cannot be used after the handler has been set. Also the
- * handler cannot be set after this label has been set.
+ * {@link #setValidationStatusHandler(BinderValidationStatusHandler)}, which
+ * means that this method cannot be used after the handler has been set.
+ * Also the handler cannot be set after this label has been set.
*
* @param statusLabel
* the status label to set
@@ -1463,8 +1690,8 @@ public class Binder<BEAN> implements Serializable {
* Gets the status handler of this form.
* <p>
* If none has been set with
- * {@link #setValidationStatusHandler(BinderValidationStatusHandler)}, the default
- * implementation is returned.
+ * {@link #setValidationStatusHandler(BinderValidationStatusHandler)}, the
+ * default implementation is returned.
*
* @return the status handler used, never <code>null</code>
* @see #setValidationStatusHandler(BinderValidationStatusHandler)
@@ -1529,6 +1756,9 @@ public class Binder<BEAN> implements Serializable {
BindingValidationStatusHandler handler) {
BindingBuilder<BEAN, TARGET> newBinding = doCreateBinding(field,
converter, handler);
+ if (incompleteMemberFieldBindings.containsKey(field)) {
+ incompleteMemberFieldBindings.put(field, newBinding);
+ }
incompleteBindings.put(field, newBinding);
return newBinding;
}
@@ -1722,17 +1952,259 @@ public class Binder<BEAN> implements Serializable {
/**
* Throws if this binder has incomplete bindings.
- *
+ *
* @param methodName
* name of the method where this call is originated from
* @throws IllegalStateException
* if this binder has incomplete bindings
*/
- protected void checkBindingsCompleted(String methodName) {
+ private void checkBindingsCompleted(String methodName) {
+ if (!incompleteMemberFieldBindings.isEmpty()) {
+ throw new IllegalStateException(
+ "All bindings created with forMemberField must "
+ + "be completed with bindInstanceFields before calling "
+ + methodName);
+ }
+
if (!incompleteBindings.isEmpty()) {
throw new IllegalStateException(
"All bindings created with forField must be completed before calling "
+ methodName);
}
}
+
+ /**
+ * Binds member fields found in the given object.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link HasValue} and that can be mapped to a property id. Property name
+ * mapping is done based on the field name or on a @{@link PropertyId}
+ * annotation on the field. All non-null unbound fields for which a property
+ * name can be determined are bound to the property name using
+ * {@link BindingBuilder#bind(String)}.
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * &#64;PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * binder.bindMemberFields(myForm);
+ * </pre>
+ *
+ * This binds the firstName TextField to a "firstName" property in the item,
+ * lastName TextField to a "last" property.
+ * <p>
+ * It's not always possible to bind a field to a property because their
+ * types are incompatible. E.g. custom converter is required to bind
+ * {@code HasValue<String>} and {@code Integer} property (that would be a
+ * case of "age" property). In such case {@link IllegalStateException} will
+ * be thrown unless the field has been configured manually before calling
+ * the {@link #bindInstanceFields(Object)} method.
+ * <p>
+ * It's always possible to do custom binding for any field: the
+ * {@link #bindInstanceFields(Object)} method doesn't override existing
+ * bindings.
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to bind
+ * @throws IllegalStateException
+ * if there are incompatible HasValue&lt;T&gt; and property
+ * types
+ */
+ public void bindInstanceFields(Object objectWithMemberFields) {
+ Class<?> objectClass = objectWithMemberFields.getClass();
+
+ getFieldsInDeclareOrder(objectClass).stream()
+ .filter(memberField -> HasValue.class
+ .isAssignableFrom(memberField.getType()))
+ .forEach(memberField -> handleProperty(memberField,
+ objectWithMemberFields,
+ (property, type) -> bindProperty(objectWithMemberFields,
+ memberField, property, type)));
+ }
+
+ @SuppressWarnings("unchecked")
+ private BindingBuilder<BEAN, ?> getIncompleteMemberFieldBinding(
+ Field memberField, Object objectWithMemberFields) {
+ memberField.setAccessible(true);
+ try {
+ return incompleteMemberFieldBindings
+ .get(memberField.get(objectWithMemberFields));
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } finally {
+ memberField.setAccessible(false);
+ }
+ }
+
+ /**
+ * Binds {@code property} with {@code propertyType} to the field in the
+ * {@code objectWithMemberFields} instance using {@code memberField} as a
+ * reference to a member.
+ *
+ * @param objectWithMemberFields
+ * the object that contains (Java) member fields to build and
+ * bind
+ * @param memberField
+ * reference to a member field to bind
+ * @param property
+ * property name to bind
+ * @param propertyType
+ * type of the property
+ */
+ private void bindProperty(Object objectWithMemberFields, Field memberField,
+ String property, Class<?> propertyType) {
+ Type valueType = GenericTypeReflector.getTypeParameter(
+ memberField.getGenericType(),
+ HasValue.class.getTypeParameters()[0]);
+ if (valueType == null) {
+ throw new IllegalStateException(String.format(
+ "Unable to detect value type for the member '%s' in the "
+ + "class '%s'.",
+ memberField.getName(),
+ objectWithMemberFields.getClass().getName()));
+ }
+ if (propertyType.equals(GenericTypeReflector.erase(valueType))) {
+ HasValue<?> field;
+ // Get the field from the object
+ try {
+ field = (HasValue<?>) ReflectTools.getJavaFieldValue(
+ objectWithMemberFields, memberField, HasValue.class);
+ } catch (IllegalArgumentException | IllegalAccessException
+ | InvocationTargetException e) {
+ // If we cannot determine the value, just skip the field
+ return;
+ }
+ if (field == null) {
+ field = makeFieldInstance(
+ (Class<? extends HasValue<?>>) memberField.getType());
+ initializeField(objectWithMemberFields, memberField, field);
+ }
+ forField(field).bind(property);
+ } else {
+ throw new IllegalStateException(String.format(
+ "Property type '%s' doesn't "
+ + "match the field type '%s'. "
+ + "Binding should be configured manually using converter.",
+ propertyType.getName(), valueType.getTypeName()));
+ }
+ }
+
+ /**
+ * Makes an instance of the field type {@code fieldClass}.
+ * <p>
+ * The resulting field instance is used to bind a property to it using the
+ * {@link #bindInstanceFields(Object)} method.
+ * <p>
+ * The default implementation relies on the default constructor of the
+ * class. If there is no suitable default constructor or you want to
+ * configure the instantiated class then override this method and provide
+ * your own implementation.
+ *
+ * @see #bindInstanceFields(Object)
+ * @param fieldClass
+ * type of the field
+ * @return a {@code fieldClass} instance object
+ */
+ private HasValue<?> makeFieldInstance(
+ Class<? extends HasValue<?>> fieldClass) {
+ try {
+ return fieldClass.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new IllegalStateException(
+ String.format("Couldn't create an '%s' type instance",
+ fieldClass.getName()),
+ e);
+ }
+ }
+
+ /**
+ * Returns an array containing {@link Field} objects reflecting all the
+ * fields of the class or interface represented by this Class object. The
+ * elements in the array returned are sorted in declare order from sub class
+ * to super class.
+ *
+ * @param searchClass
+ * class to introspect
+ * @return list of all fields in the class considering hierarchy
+ */
+ private List<Field> getFieldsInDeclareOrder(Class<?> searchClass) {
+ ArrayList<Field> memberFieldInOrder = new ArrayList<>();
+
+ while (searchClass != null) {
+ memberFieldInOrder
+ .addAll(Arrays.asList(searchClass.getDeclaredFields()));
+ searchClass = searchClass.getSuperclass();
+ }
+ return memberFieldInOrder;
+ }
+
+ private void initializeField(Object objectWithMemberFields,
+ Field memberField, HasValue<?> value) {
+ try {
+ ReflectTools.setJavaFieldValue(objectWithMemberFields, memberField,
+ value);
+ } catch (IllegalArgumentException | IllegalAccessException
+ | InvocationTargetException e) {
+ throw new IllegalStateException(
+ String.format("Could not assign value to field '%s'",
+ memberField.getName()),
+ e);
+ }
+ }
+
+ private void handleProperty(Field field, Object objectWithMemberFields,
+ BiConsumer<String, Class<?>> propertyHandler) {
+ Optional<BinderPropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor(
+ field);
+
+ if (!descriptor.isPresent()) {
+ return;
+ }
+
+ String propertyName = descriptor.get().getName();
+ if (boundProperties.contains(propertyName)) {
+ return;
+ }
+
+ BindingBuilder<BEAN, ?> tentativeBinding = getIncompleteMemberFieldBinding(
+ field, objectWithMemberFields);
+ if (tentativeBinding != null) {
+ tentativeBinding.bind(propertyName);
+ return;
+ }
+
+ propertyHandler.accept(propertyName, descriptor.get().getType());
+ boundProperties.add(propertyName);
+ }
+
+ private Optional<BinderPropertyDefinition<BEAN, ?>> getPropertyDescriptor(
+ Field field) {
+ PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class);
+
+ String propertyId;
+ if (propertyIdAnnotation != null) {
+ // @PropertyId(propertyId) always overrides property id
+ propertyId = propertyIdAnnotation.value();
+ } else {
+ propertyId = field.getName();
+ }
+
+ String minifiedFieldName = minifyFieldName(propertyId);
+
+ return propertySet.getProperties()
+ .map(BinderPropertyDefinition::getName)
+ .filter(name -> minifyFieldName(name).equals(minifiedFieldName))
+ .findFirst().flatMap(propertySet::getProperty);
+ }
+
+ private String minifyFieldName(String fieldName) {
+ return fieldName.toLowerCase(Locale.ENGLISH).replace("_", "");
+ }
+
}
diff --git a/server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java b/server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java
new file mode 100644
index 0000000000..7ab7e1ccec
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java
@@ -0,0 +1,85 @@
+/*
+ * 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.Serializable;
+import java.util.Optional;
+
+import com.vaadin.data.Binder.BindingBuilder;
+import com.vaadin.server.Setter;
+
+/**
+ * A property from a {@link BinderPropertySet}.
+ *
+ * @author Vaadin Ltd
+ * @since
+ *
+ * @param <T>
+ * the type of the binder property set
+ * @param <V>
+ * the property type
+ */
+public interface BinderPropertyDefinition<T, V> extends Serializable {
+ /**
+ * Gets the value provider that is used for finding the value of this
+ * property for a bean.
+ *
+ * @return the getter, not <code>null</code>
+ */
+ public ValueProvider<T, V> getGetter();
+
+ /**
+ * Gets an optional setter for storing a property value in a bean.
+ *
+ * @return the setter, or an empty optional if this property is read-only
+ */
+ public Optional<Setter<T, V>> getSetter();
+
+ /**
+ * Gets the type of this property.
+ *
+ * @return the property type. not <code>null</code>
+ */
+ public Class<V> getType();
+
+ /**
+ * Hook for modifying a binding before it is being bound to this property.
+ * This method can return the provided {@link BindingBuilder} as-is if no
+ * modifications are necessary.
+ *
+ * @param originalBuilder
+ * the original binding builder that is being bound, not
+ * <code>null</code>
+ * @return the binding builder to use for creating the binding, not
+ * <code>null</code>
+ */
+ public BindingBuilder<T, V> beforeBind(
+ BindingBuilder<T, V> originalBuilder);
+
+ /**
+ * Gets the name of this property.
+ *
+ * @return the property name, not <code>null</code>
+ */
+ public String getName();
+
+ /**
+ * Gets the {@link BinderPropertySet} that this property belongs to.
+ *
+ * @return the binder property set, not <code>null</code>
+ */
+ public BinderPropertySet<T> getPropertySet();
+}
diff --git a/server/src/main/java/com/vaadin/data/BinderPropertySet.java b/server/src/main/java/com/vaadin/data/BinderPropertySet.java
new file mode 100644
index 0000000000..6252a228aa
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/BinderPropertySet.java
@@ -0,0 +1,50 @@
+/*
+ * 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.Serializable;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Describes a set of properties that can be used with a {@link Binder}.
+ *
+ * @author Vaadin Ltd
+ *
+ * @since
+ *
+ * @param <T>
+ * the type for which the properties are defined
+ */
+public interface BinderPropertySet<T> extends Serializable {
+ /**
+ * Gets all known properties as a stream.
+ *
+ * @return a stream of property names, not <code>null</code>
+ */
+ public Stream<BinderPropertyDefinition<T, ?>> getProperties();
+
+ /**
+ * Gets the definition for the named property, or an empty optional if there
+ * is no property with the given name.
+ *
+ * @param name
+ * the property name to look for, not <code>null</code>
+ * @return the property definition, or empty optional if property doesn't
+ * exist
+ */
+ public Optional<BinderPropertyDefinition<T, ?>> getProperty(String name);
+}
diff --git a/server/src/main/java/com/vaadin/data/BinderValidationStatus.java b/server/src/main/java/com/vaadin/data/BinderValidationStatus.java
index 663d1635df..c8d021ad53 100644
--- a/server/src/main/java/com/vaadin/data/BinderValidationStatus.java
+++ b/server/src/main/java/com/vaadin/data/BinderValidationStatus.java
@@ -23,7 +23,6 @@ import java.util.Objects;
import java.util.stream.Collectors;
import com.vaadin.data.Binder.BindingBuilder;
-import com.vaadin.data.validator.BeanValidator;
/**
* Binder validation status change. Represents the outcome of binder level
@@ -160,11 +159,6 @@ public class BinderValidationStatus<BEAN> implements Serializable {
/**
* Gets the bean level validation results.
- * <p>
- * The bean level validators have been added with
- * {@link Binder#withValidator(Validator)} or in case of a
- * {@link BeanBinder} they might be automatically added {@link BeanValidator
- * BeanValidators}.
*
* @return the bean level validation results
*/
@@ -187,11 +181,6 @@ public class BinderValidationStatus<BEAN> implements Serializable {
/**
* Gets the failed bean level validation results.
- * <p>
- * The bean level validators have been added with
- * {@link Binder#withValidator(Validator)} or in case of a
- * {@link BeanBinder} they might be automatically added {@link BeanValidator
- * BeanValidators}.
*
* @return a list of failed bean level validation results
*/
diff --git a/server/src/test/java/com/vaadin/data/BeanBinderPropertySetTest.java b/server/src/test/java/com/vaadin/data/BeanBinderPropertySetTest.java
new file mode 100644
index 0000000000..d11541fb0c
--- /dev/null
+++ b/server/src/test/java/com/vaadin/data/BeanBinderPropertySetTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.provider.bov.Person;
+
+public class BeanBinderPropertySetTest {
+ @Test
+ public void testSerializeDeserialize() throws Exception {
+ BinderPropertyDefinition<Person, ?> definition = BeanBinderPropertySet
+ .get(Person.class).getProperty("born")
+ .orElseThrow(RuntimeException::new);
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(bos);
+ out.writeObject(definition);
+ out.flush();
+
+ ObjectInputStream inputStream = new ObjectInputStream(
+ new ByteArrayInputStream(bos.toByteArray()));
+
+ BinderPropertyDefinition<Person, ?> deserializedDefinition = (BinderPropertyDefinition<Person, ?>) inputStream
+ .readObject();
+
+ ValueProvider<Person, ?> getter = deserializedDefinition.getGetter();
+ Person person = new Person("Milennial", 2000);
+ Integer age = (Integer) getter.apply(person);
+
+ Assert.assertEquals(Integer.valueOf(2000), age);
+ }
+}
diff --git a/server/src/test/java/com/vaadin/data/BeanBinderTest.java b/server/src/test/java/com/vaadin/data/BeanBinderTest.java
index 3433aee1a8..6bd53044b7 100644
--- a/server/src/test/java/com/vaadin/data/BeanBinderTest.java
+++ b/server/src/test/java/com/vaadin/data/BeanBinderTest.java
@@ -3,23 +3,19 @@ package com.vaadin.data;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
-import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
-import com.vaadin.data.BeanBinder.BeanBindingBuilder;
-import com.vaadin.data.Binder.BindingBuilder;
import com.vaadin.data.converter.StringToIntegerConverter;
import com.vaadin.tests.data.bean.BeanToValidate;
import com.vaadin.ui.CheckBoxGroup;
import com.vaadin.ui.TextField;
public class BeanBinderTest
- extends BinderTestBase<BeanBinder<BeanToValidate>, BeanToValidate> {
+ extends BinderTestBase<Binder<BeanToValidate>, BeanToValidate> {
private enum TestEnum {
}
@@ -52,7 +48,7 @@ public class BeanBinderTest
@Before
public void setUp() {
- binder = new BeanBinder<>(BeanToValidate.class);
+ binder = new Binder<>(BeanToValidate.class);
item = new BeanToValidate();
item.setFirstname("Johannes");
item.setAge(32);
@@ -60,11 +56,10 @@ public class BeanBinderTest
@Test
public void bindInstanceFields_parameters_type_erased() {
- BeanBinder<TestBean> otherBinder = new BeanBinder<>(TestBean.class);
+ Binder<TestBean> otherBinder = new Binder<>(TestBean.class);
TestClass testClass = new TestClass();
otherBinder.forField(testClass.number)
- .withConverter(new StringToIntegerConverter(""))
- .bind("number");
+ .withConverter(new StringToIntegerConverter("")).bind("number");
// Should correctly bind the enum field without throwing
otherBinder.bindInstanceFields(testClass);
@@ -72,7 +67,7 @@ public class BeanBinderTest
@Test
public void bindInstanceFields_automatically_binds_incomplete_forMemberField_bindings() {
- BeanBinder<TestBean> otherBinder = new BeanBinder<>(TestBean.class);
+ Binder<TestBean> otherBinder = new Binder<>(TestBean.class);
TestClass testClass = new TestClass();
otherBinder.forMemberField(testClass.number)
@@ -87,7 +82,7 @@ public class BeanBinderTest
@Test(expected = IllegalStateException.class)
public void bindInstanceFields_does_not_automatically_bind_incomplete_forField_bindings() {
- BeanBinder<TestBean> otherBinder = new BeanBinder<>(TestBean.class);
+ Binder<TestBean> otherBinder = new Binder<>(TestBean.class);
TestClass testClass = new TestClass();
otherBinder.forField(testClass.number)
@@ -100,7 +95,7 @@ public class BeanBinderTest
@Test(expected = IllegalStateException.class)
public void incomplete_forMemberField_bindings() {
- BeanBinder<TestBean> otherBinder = new BeanBinder<>(TestBean.class);
+ Binder<TestBean> otherBinder = new Binder<>(TestBean.class);
TestClass testClass = new TestClass();
otherBinder.forMemberField(testClass.number)
@@ -262,24 +257,4 @@ public class BeanBinderTest
assertSame(field, errors.get(0).getField());
assertEquals(message, errors.get(0).getMessage().get());
}
-
- @Test
- public void beanBindingChainingMethods() {
- Method[] methods = BeanBindingBuilder.class.getMethods();
- for (int i = 0; i < methods.length; i++) {
- Method method = methods[i];
- try {
- Method actualMethod = BeanBindingBuilder.class.getMethod(
- method.getName(), method.getParameterTypes());
-
- Assert.assertNotSame(
- actualMethod + " should be overridden in "
- + BeanBindingBuilder.class
- + " with more specific return type ",
- BindingBuilder.class, actualMethod.getReturnType());
- } catch (NoSuchMethodException | SecurityException e) {
- throw new RuntimeException(e);
- }
- }
- }
}
diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
index f1d226bf20..7d2e6bd304 100644
--- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
@@ -461,7 +461,7 @@ public class BinderBookOfVaadinTest {
@Test
public void binder_saveIfValid() {
- BeanBinder<BookPerson> binder = new BeanBinder<>(BookPerson.class);
+ Binder<BookPerson> binder = new Binder<>(BookPerson.class);
// Phone or email has to be specified for the bean
Validator<BookPerson> phoneOrEmail = Validator.from(
@@ -585,7 +585,7 @@ public class BinderBookOfVaadinTest {
public void withBinderStatusLabelExample() {
Label formStatusLabel = new Label();
- BeanBinder<BookPerson> binder = new BeanBinder<>(BookPerson.class);
+ Binder<BookPerson> binder = new Binder<>(BookPerson.class);
binder.setStatusLabel(formStatusLabel);
diff --git a/server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java b/server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java
new file mode 100644
index 0000000000..3d85084bc6
--- /dev/null
+++ b/server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Binder.BindingBuilder;
+import com.vaadin.server.Setter;
+import com.vaadin.ui.TextField;
+
+public class BinderCustomPropertySetTest {
+ public static class MapPropertyDefinition
+ implements BinderPropertyDefinition<Map<String, String>, String> {
+
+ private MapPropertySet propertySet;
+ private String name;
+
+ public MapPropertyDefinition(MapPropertySet propertySet, String name) {
+ this.propertySet = propertySet;
+ this.name = name;
+ }
+
+ @Override
+ public ValueProvider<Map<String, String>, String> getGetter() {
+ return map -> map.get(name);
+ }
+
+ @Override
+ public Optional<Setter<Map<String, String>, String>> getSetter() {
+ return Optional.of((map, value) -> {
+ if (value == null) {
+ map.remove(name);
+ } else {
+ map.put(name, value);
+ }
+ });
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ @Override
+ public BindingBuilder<Map<String, String>, String> beforeBind(
+ BindingBuilder<Map<String, String>, String> originalBuilder) {
+ return originalBuilder;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public BinderPropertySet<Map<String, String>> getPropertySet() {
+ return propertySet;
+ }
+
+ }
+
+ public static class MapPropertySet
+ implements BinderPropertySet<Map<String, String>> {
+ @Override
+ public Stream<BinderPropertyDefinition<Map<String, String>, ?>> getProperties() {
+ return Stream.of("one", "two", "three").map(this::createProperty);
+ }
+
+ @Override
+ public Optional<BinderPropertyDefinition<Map<String, String>, ?>> getProperty(
+ String name) {
+ return Optional.of(createProperty(name));
+ }
+
+ private BinderPropertyDefinition<Map<String, String>, ?> createProperty(
+ String name) {
+ return new MapPropertyDefinition(this, name);
+ }
+ }
+
+ public static class InstanceFields {
+ private TextField one;
+ private TextField another;
+ }
+
+ @Test
+ public void testBindByString() {
+ TextField field = new TextField();
+ Map<String, String> map = new HashMap<>();
+ Binder<Map<String, String>> binder = Binder
+ .withPropertySet(new MapPropertySet());
+
+ binder.bind(field, "key");
+ binder.setBean(map);
+
+ field.setValue("value");
+ Assert.assertEquals(
+ "Field value should propagate to the corresponding key in the map",
+ "value", map.get("key"));
+ }
+
+ @Test
+ public void testBindInstanceFields() {
+ Map<String, String> map = new HashMap<>();
+ Binder<Map<String, String>> binder = Binder
+ .withPropertySet(new MapPropertySet());
+ InstanceFields instanceFields = new InstanceFields();
+
+ binder.bindInstanceFields(instanceFields);
+
+ Assert.assertNotNull(
+ "Field corresponding to supported property name should be bound",
+ instanceFields.one);
+ Assert.assertNull(
+ "Field corresponding to unsupported property name should be ignored",
+ instanceFields.another);
+
+ binder.setBean(map);
+
+ instanceFields.one.setValue("value");
+ Assert.assertEquals(
+ "Field value should propagate to the corresponding key in the map",
+ "value", map.get("one"));
+ }
+}
diff --git a/server/src/test/java/com/vaadin/data/BeanBinderInstanceFieldTest.java b/server/src/test/java/com/vaadin/data/BinderInstanceFieldTest.java
index 9ed2e8edda..de58be5e42 100644
--- a/server/src/test/java/com/vaadin/data/BeanBinderInstanceFieldTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderInstanceFieldTest.java
@@ -30,13 +30,7 @@ import com.vaadin.ui.DateField;
import com.vaadin.ui.FormLayout;
import com.vaadin.ui.TextField;
-/**
- * Unit tests for {@link BeanBinder#bindInstanceFields(Object)} method.
- *
- * @author Vaadin Ltd
- *
- */
-public class BeanBinderInstanceFieldTest {
+public class BinderInstanceFieldTest {
public static class BindAllFields extends FormLayout {
private TextField firstName;
@@ -132,7 +126,7 @@ public class BeanBinderInstanceFieldTest {
@Test
public void bindInstanceFields_bindAllFields() {
BindAllFields form = new BindAllFields();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
Person person = new Person();
@@ -151,10 +145,17 @@ public class BeanBinderInstanceFieldTest {
Assert.assertEquals(form.birthDate.getValue(), person.getBirthDate());
}
+ @Test(expected = IllegalStateException.class)
+ public void bind_instanceFields_noArgsConstructor() {
+ BindAllFields form = new BindAllFields();
+ Binder<Person> binder = new Binder<>();
+ binder.bindInstanceFields(form);
+ }
+
@Test
public void bindInstanceFields_bindOnlyOneFields() {
BindOnlyOneField form = new BindOnlyOneField();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
Person person = new Person();
@@ -174,7 +175,7 @@ public class BeanBinderInstanceFieldTest {
@Test
public void bindInstanceFields_bindNotHasValueField_fieldIsNull() {
BindNoHasValueField form = new BindNoHasValueField();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
Person person = new Person();
@@ -188,7 +189,7 @@ public class BeanBinderInstanceFieldTest {
@Test
public void bindInstanceFields_genericField() {
BindGenericField form = new BindGenericField();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
Person person = new Person();
@@ -206,49 +207,49 @@ public class BeanBinderInstanceFieldTest {
@Test(expected = IllegalStateException.class)
public void bindInstanceFields_genericFieldWithWrongTypeParameter() {
BindGenericWrongTypeParameterField form = new BindGenericWrongTypeParameterField();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
}
@Test(expected = IllegalStateException.class)
public void bindInstanceFields_generic() {
BindGeneric<String> form = new BindGeneric<>();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
}
@Test(expected = IllegalStateException.class)
public void bindInstanceFields_rawFieldType() {
BindRaw form = new BindRaw();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
}
@Test(expected = IllegalStateException.class)
public void bindInstanceFields_abstractFieldType() {
BindAbstract form = new BindAbstract();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
}
@Test(expected = IllegalStateException.class)
public void bindInstanceFields_noInstantiatableFieldType() {
BindNonInstantiatableType form = new BindNonInstantiatableType();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
}
@Test(expected = IllegalStateException.class)
public void bindInstanceFields_wrongFieldType() {
BindWrongTypeParameterField form = new BindWrongTypeParameterField();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
}
@Test
public void bindInstanceFields_complexGenericHierarchy() {
BindComplextHierarchyGenericType form = new BindComplextHierarchyGenericType();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
Person person = new Person();
@@ -266,7 +267,7 @@ public class BeanBinderInstanceFieldTest {
@Test
public void bindInstanceFields_bindNotHasValueField_fieldIsNotReplaced() {
BindNoHasValueField form = new BindNoHasValueField();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
String name = "foo";
form.firstName = name;
@@ -284,7 +285,7 @@ public class BeanBinderInstanceFieldTest {
@Test
public void bindInstanceFields_bindAllFieldsUsingAnnotations() {
BindFieldsUsingAnnotation form = new BindFieldsUsingAnnotation();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.bindInstanceFields(form);
Person person = new Person();
@@ -308,7 +309,7 @@ public class BeanBinderInstanceFieldTest {
@Test
public void bindInstanceFields_bindNotBoundFieldsOnly_customBindingIsNotReplaced() {
BindAllFields form = new BindAllFields();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
TextField name = new TextField();
form.firstName = name;
@@ -343,7 +344,7 @@ public class BeanBinderInstanceFieldTest {
@Test
public void bindInstanceFields_fieldsAreConfigured_customBindingIsNotReplaced() {
BindOnlyOneField form = new BindOnlyOneField();
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
TextField name = new TextField();
form.firstName = name;
diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java
index 6fe139b7ad..1994821db0 100644
--- a/server/src/test/java/com/vaadin/data/BinderTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderTest.java
@@ -285,7 +285,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> {
@Test
public void beanBinder_nullRepresentationIsNotDisabled() {
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.forField(nameField).bind("firstName");
Person person = new Person();
@@ -297,7 +297,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> {
@Test
public void beanBinder_withConverter_nullRepresentationIsNotDisabled() {
String customNullPointerRepresentation = "foo";
- BeanBinder<Person> binder = new BeanBinder<>(Person.class);
+ Binder<Person> binder = new Binder<>(Person.class);
binder.forField(nameField)
.withConverter(value -> value, value -> value == null
? customNullPointerRepresentation : value)
@@ -426,4 +426,9 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> {
firstNameField.setValue("");
Assert.assertEquals(6, invokes.get());
}
+
+ @Test(expected = IllegalStateException.class)
+ public void noArgsConstructor_stringBind_throws() {
+ binder.bind(new TextField(), "firstName");
+ }
} \ No newline at end of file
diff --git a/server/src/test/java/com/vaadin/data/Jsr303Test.java b/server/src/test/java/com/vaadin/data/Jsr303Test.java
index 016018a6b3..43b03feb97 100644
--- a/server/src/test/java/com/vaadin/data/Jsr303Test.java
+++ b/server/src/test/java/com/vaadin/data/Jsr303Test.java
@@ -84,8 +84,7 @@ public class Jsr303Test {
public void execute() {
Assert.assertFalse(BeanUtil.checkBeanValidationAvailable());
- BeanBinder<BeanToValidate> binder = new BeanBinder<>(
- BeanToValidate.class);
+ Binder<BeanToValidate> binder = new Binder<>(BeanToValidate.class);
BeanToValidate item = new BeanToValidate();
String name = "Johannes";
item.setFirstname(name);