* Integrate BeanBinder functionality into Bindertags/8.0.0.beta2
@@ -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> |
@@ -765,7 +765,6 @@ grid.getEditor().setSaveCaption("Tallenna"); | |||
grid.getEditor().setCancelCaption("Peruuta")); | |||
---- | |||
[[components.grid.editing.validation]] | |||
=== Handling Validation Errors | |||
@@ -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); |
@@ -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> |
@@ -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("_", ""); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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 | |||
@@ -181,6 +191,37 @@ public class Binder<BEAN> implements Serializable { | |||
public Binding<BEAN, TARGET> bind(ValueProvider<BEAN, TARGET> getter, | |||
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 | |||
@@ -559,6 +600,46 @@ public class Binder<BEAN> implements Serializable { | |||
return binding; | |||
} | |||
@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) { | |||
@@ -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<>(); | |||
@@ -978,6 +1068,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. | |||
@@ -1020,6 +1188,29 @@ public class Binder<BEAN> implements Serializable { | |||
this::handleValidationStatus); | |||
} | |||
/** | |||
* 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 | |||
@@ -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 | |||
@@ -1078,6 +1269,42 @@ public class Binder<BEAN> implements Serializable { | |||
return forField(field).bind(getter, setter); | |||
} | |||
/** | |||
* 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. | |||
@@ -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("_", ""); | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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); | |||
} |
@@ -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 | |||
*/ |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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); | |||
@@ -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")); | |||
} | |||
} |
@@ -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; |
@@ -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"); | |||
} | |||
} |
@@ -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); |