) 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.
*
* If a JSR-303 bean validation implementation is present on the
* classpath, adds a {@link BeanValidator} to this binding.
*
* 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 read-only.
*
* @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(SerializableFunction,
* SerializableBiConsumer)
*/
public Binding bind(String propertyName);
}
/**
* An internal implementation of {@link BeanBindingBuilder}.
*
* @param
* the bean type
* @param
* the field value type
* @param
* the target property type
*/
protected static class BeanBindingImpl
extends BindingBuilderImpl
implements BeanBindingBuilder {
/**
* 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 binder,
HasValue field,
Converter converter,
ValidationStatusHandler statusHandler) {
super(binder, field, converter, statusHandler);
}
@Override
public BeanBindingBuilder withValidator(
Validator super TARGET> validator) {
return (BeanBindingBuilder) super.withValidator(
validator);
}
@Override
public BeanBindingBuilder withConverter(
Converter converter) {
return (BeanBindingBuilder) super.withConverter(
converter);
}
@Override
public BeanBindingBuilder withValidationStatusHandler(
ValidationStatusHandler handler) {
return (BeanBindingBuilder) super.withValidationStatusHandler(
handler);
}
@Override
public BeanBindingBuilder setRequired(
ErrorMessageProvider errorMessageProvider) {
return (BeanBindingBuilder) super.setRequired(
errorMessageProvider);
}
@Override
public Binding bind(String propertyName) {
checkUnbound();
BindingBuilder 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) finalBinding.bind(
bean -> invokeWrapExceptions(getter, bean),
(bean, value) -> invokeWrapExceptions(setter, bean,
value));
} finally {
getBinder().boundProperties.add(propertyName);
}
}
@Override
protected BeanBinder getBinder() {
return (BeanBinder) 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 createConverter(Class> getterType) {
return Converter.from(fieldValue -> cast(fieldValue, getterType),
propertyValue -> (TARGET) propertyValue, exception -> {
throw new RuntimeException(exception);
});
}
private T cast(TARGET value, Class 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 boundProperties;
/**
* 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;
boundProperties = new HashSet<>();
}
@Override
public BeanBindingBuilder forField(
HasValue field) {
return (BeanBindingBuilder) super.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.
*
* Use the {@link #forField(HasValue)} overload instead if you want to
* further configure the new binding.
*
* 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 read-only.
*
* @param
* 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, SerializableFunction, SerializableBiConsumer)
*/
public Binding bind(
HasValue field, String propertyName) {
return forField(field).bind(propertyName);
}
@Override
public BeanBinder withValidator(Validator super BEAN> validator) {
return (BeanBinder) super.withValidator(validator);
}
@Override
protected BeanBindingImpl createBinding(
HasValue field, Converter converter,
ValidationStatusHandler handler) {
Objects.requireNonNull(field, "field cannot be null");
Objects.requireNonNull(converter, "converter cannot be null");
return new BeanBindingImpl<>(this, field, converter, handler);
}
/**
* Binds member fields found in the given object.
*
* 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.
*
*
* For example:
*
*
* 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);
*
*
*
* This binds the firstName TextField to a "firstName" property in the item,
* lastName TextField to a "last" property.
*
* 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} 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.
*
* 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 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,
(property, type) -> bindProperty(objectWithMemberFields,
memberField, property, type)));
}
/**
* 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(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 manulaly using converter.",
propertyType.getName(), valueType.getTypeName()));
}
}
/**
* Makes an instance of the field type {@code fieldClass}.
*
* The resulting field instance is used to bind a property to it using the
* {@link #bindInstanceFields(Object)} method.
*
* 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 getFieldsInDeclareOrder(Class> searchClass) {
ArrayList 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,
BiConsumer> propertyHandler) {
Optional descriptor = getPropertyDescriptor(field);
if (!descriptor.isPresent()) {
return;
}
String propertyName = descriptor.get().getName();
if (boundProperties.contains(propertyName)) {
return;
}
propertyHandler.accept(propertyName,
descriptor.get().getPropertyType());
boundProperties.add(propertyName);
}
private Optional 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 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 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("_", "");
}
}