diff options
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<Entity> 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"); - * @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"); + * @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<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 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); |