/* * 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.List; import java.util.Locale; 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); } @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); } } @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; /** * 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 <FIELDVALUE> BeanBindingBuilder<BEAN, FIELDVALUE> forField( HasValue<FIELDVALUE> field) { return (BeanBindingBuilder<BEAN, FIELDVALUE>) 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. * <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); } @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"); 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, (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(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; } 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<String, Class<?>> propertyHandler) { Optional<PropertyDescriptor> 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<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("_", ""); } }