diff options
author | Leif Åstrand <legioth@gmail.com> | 2017-01-12 09:22:50 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2017-01-12 09:22:50 +0200 |
commit | 2fd7e13c421114963762ee2e2832cbda6520efa6 (patch) | |
tree | d8aa0e0125bb7e92222eba9e22a3d16e02d3e347 /server/src/main/java/com/vaadin/data/BeanBinder.java | |
parent | c2ad28cc7e26504b2e01bc9bb0f84ceed793cdb0 (diff) | |
download | vaadin-framework-2fd7e13c421114963762ee2e2832cbda6520efa6.tar.gz vaadin-framework-2fd7e13c421114963762ee2e2832cbda6520efa6.zip |
Integrate BeanBinder functionality into Binder (#8096)
* Integrate BeanBinder functionality into Binder
Diffstat (limited to 'server/src/main/java/com/vaadin/data/BeanBinder.java')
-rw-r--r-- | server/src/main/java/com/vaadin/data/BeanBinder.java | 679 |
1 files changed, 0 insertions, 679 deletions
diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java deleted file mode 100644 index 898f2cd37d..0000000000 --- a/server/src/main/java/com/vaadin/data/BeanBinder.java +++ /dev/null @@ -1,679 +0,0 @@ -/* - * Copyright 2000-2016 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.data; - -import java.beans.IntrospectionException; -import java.beans.PropertyDescriptor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.BiConsumer; - -import com.googlecode.gentyref.GenericTypeReflector; -import com.vaadin.annotations.PropertyId; -import com.vaadin.data.util.BeanUtil; -import com.vaadin.data.validator.BeanValidator; -import com.vaadin.server.SerializableFunction; -import com.vaadin.server.SerializablePredicate; -import com.vaadin.server.Setter; -import com.vaadin.ui.Label; -import com.vaadin.util.ReflectTools; - -/** - * A {@code Binder} subclass specialized for binding <em>beans</em>: classes - * that conform to the JavaBeans specification. Bean properties are bound by - * their names. If a JSR-303 bean validation implementation is present on the - * classpath, {@code BeanBinder} adds a {@link BeanValidator} to each binding. - * - * @author Vaadin Ltd. - * - * @param <BEAN> - * the bean type - * - * @since 8.0 - */ -public class BeanBinder<BEAN> extends Binder<BEAN> { - - /** - * Represents the binding between a single field and a bean property. - * - * @param <BEAN> - * the bean type - * @param <TARGET> - * the target property type - */ - public interface BeanBindingBuilder<BEAN, TARGET> - extends BindingBuilder<BEAN, TARGET> { - - @Override - public BeanBindingBuilder<BEAN, TARGET> withValidator( - Validator<? super TARGET> validator); - - @Override - public default BeanBindingBuilder<BEAN, TARGET> withValidator( - SerializablePredicate<? super TARGET> predicate, - String message) { - return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withValidator( - predicate, message); - } - - @Override - default BeanBindingBuilder<BEAN, TARGET> withValidator( - SerializablePredicate<? super TARGET> predicate, - ErrorMessageProvider errorMessageProvider) { - return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withValidator( - predicate, errorMessageProvider); - } - - @Override - default BeanBindingBuilder<BEAN, TARGET> withNullRepresentation( - TARGET nullRepresentation) { - return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withNullRepresentation( - nullRepresentation); - } - - @Override - public BeanBindingBuilder<BEAN, TARGET> asRequired( - ErrorMessageProvider errorMessageProvider); - - @Override - public default BeanBindingBuilder<BEAN, TARGET> asRequired( - String errorMessage) { - return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.asRequired( - errorMessage); - } - - @Override - public <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter( - Converter<TARGET, NEWTARGET> converter); - - @Override - public default <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter( - SerializableFunction<TARGET, NEWTARGET> toModel, - SerializableFunction<NEWTARGET, TARGET> toPresentation) { - return (BeanBindingBuilder<BEAN, NEWTARGET>) BindingBuilder.super.withConverter( - toModel, toPresentation); - } - - @Override - public default <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter( - SerializableFunction<TARGET, NEWTARGET> toModel, - SerializableFunction<NEWTARGET, TARGET> toPresentation, - String errorMessage) { - return (BeanBindingBuilder<BEAN, NEWTARGET>) BindingBuilder.super.withConverter( - toModel, toPresentation, errorMessage); - } - - @Override - public BeanBindingBuilder<BEAN, TARGET> withValidationStatusHandler( - BindingValidationStatusHandler handler); - - @Override - public default BeanBindingBuilder<BEAN, TARGET> withStatusLabel( - Label label) { - return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.withStatusLabel( - label); - } - - /** - * Completes this binding by connecting the field to the property with - * the given name. The getter and setter methods of the property are - * looked up with bean introspection and used to read and write the - * property value. - * <p> - * If a JSR-303 bean validation implementation is present on the - * classpath, adds a {@link BeanValidator} to this binding. - * <p> - * The property must have an accessible getter method. It need not have - * an accessible setter; in that case the property value is never - * updated and the binding is said to be <i>read-only</i>. - * - * @param propertyName - * the name of the property to bind, not null - * @return the newly created binding - * - * @throws IllegalArgumentException - * if the property name is invalid - * @throws IllegalArgumentException - * if the property has no accessible getter - * - * @see BindingBuilder#bind(ValueProvider, Setter) - */ - public Binding<BEAN, TARGET> bind(String propertyName); - } - - /** - * An internal implementation of {@link BeanBindingBuilder}. - * - * @param <BEAN> - * the bean type - * @param <FIELDVALUE> - * the field value type - * @param <TARGET> - * the target property type - */ - protected static class BeanBindingImpl<BEAN, FIELDVALUE, TARGET> - extends BindingBuilderImpl<BEAN, FIELDVALUE, TARGET> - implements BeanBindingBuilder<BEAN, TARGET> { - - /** - * Creates a new bean binding. - * - * @param binder - * the binder this instance is connected to, not null - * @param field - * the field to use, not null - * @param converter - * the initial converter to use, not null - * @param statusHandler - * the handler to notify of status changes, not null - */ - protected BeanBindingImpl(BeanBinder<BEAN> binder, - HasValue<FIELDVALUE> field, - Converter<FIELDVALUE, TARGET> converter, - BindingValidationStatusHandler statusHandler) { - super(binder, field, converter, statusHandler); - } - - @Override - public BeanBindingBuilder<BEAN, TARGET> withValidator( - Validator<? super TARGET> validator) { - return (BeanBindingBuilder<BEAN, TARGET>) super.withValidator( - validator); - } - - @Override - public <NEWTARGET> BeanBindingBuilder<BEAN, NEWTARGET> withConverter( - Converter<TARGET, NEWTARGET> converter) { - return (BeanBindingBuilder<BEAN, NEWTARGET>) super.withConverter( - converter); - } - - @Override - public BeanBindingBuilder<BEAN, TARGET> withValidationStatusHandler( - BindingValidationStatusHandler handler) { - return (BeanBindingBuilder<BEAN, TARGET>) super.withValidationStatusHandler( - handler); - } - - @Override - public BeanBindingBuilder<BEAN, TARGET> asRequired( - ErrorMessageProvider errorMessageProvider) { - return (BeanBindingBuilder<BEAN, TARGET>) super.asRequired( - errorMessageProvider); - } - - @SuppressWarnings("unchecked") - @Override - public Binding<BEAN, TARGET> bind(String propertyName) { - checkUnbound(); - - BindingBuilder<BEAN, Object> finalBinding; - - PropertyDescriptor descriptor = getDescriptor(propertyName); - - Method getter = descriptor.getReadMethod(); - Method setter = descriptor.getWriteMethod(); - - finalBinding = withConverter( - createConverter(getter.getReturnType()), false); - - if (BeanUtil.checkBeanValidationAvailable()) { - finalBinding = finalBinding.withValidator( - new BeanValidator(getBinder().beanType, propertyName)); - } - - try { - return (Binding<BEAN, TARGET>) finalBinding.bind( - bean -> invokeWrapExceptions(getter, bean), - (bean, value) -> invokeWrapExceptions(setter, bean, - value)); - } finally { - getBinder().boundProperties.add(propertyName); - getBinder().incompleteMemberFieldBindings.remove(getField()); - } - } - - @Override - protected BeanBinder<BEAN> getBinder() { - return (BeanBinder<BEAN>) super.getBinder(); - } - - private static Object invokeWrapExceptions(Method method, Object target, - Object... parameters) { - if (method == null) { - return null; - } - try { - return method.invoke(target, parameters); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - private PropertyDescriptor getDescriptor(String propertyName) { - final Class<?> beanType = getBinder().beanType; - PropertyDescriptor descriptor = null; - try { - descriptor = BeanUtil.getPropertyDescriptor(beanType, - propertyName); - } catch (IntrospectionException ie) { - throw new IllegalArgumentException( - "Could not resolve bean property name (see the cause): " - + beanType.getName() + "." + propertyName, - ie); - } - if (descriptor == null) { - throw new IllegalArgumentException( - "Could not resolve bean property name (please check spelling and getter visibility): " - + beanType.getName() + "." + propertyName); - } - if (descriptor.getReadMethod() == null) { - throw new IllegalArgumentException( - "Bean property has no accessible getter: " - + beanType.getName() + "." + propertyName); - } - return descriptor; - } - - @SuppressWarnings("unchecked") - private Converter<TARGET, Object> createConverter(Class<?> getterType) { - return Converter.from(fieldValue -> cast(fieldValue, getterType), - propertyValue -> (TARGET) propertyValue, exception -> { - throw new RuntimeException(exception); - }); - } - - private <T> T cast(TARGET value, Class<T> clazz) { - if (clazz.isPrimitive()) { - return (T) ReflectTools.convertPrimitiveType(clazz).cast(value); - } else { - return clazz.cast(value); - } - } - } - - private final Class<? extends BEAN> beanType; - private final Set<String> boundProperties = new HashSet<>(); - private final Map<HasValue<?>, BeanBindingImpl<BEAN, ?, ?>> incompleteMemberFieldBindings = new IdentityHashMap<>(); - - /** - * Creates a new {@code BeanBinder} supporting beans of the given type. - * - * @param beanType - * the bean {@code Class} instance, not null - */ - public BeanBinder(Class<? extends BEAN> beanType) { - BeanUtil.checkBeanValidationAvailable(); - this.beanType = beanType; - } - - @Override - public <FIELDVALUE> BeanBindingBuilder<BEAN, FIELDVALUE> forField( - HasValue<FIELDVALUE> field) { - return (BeanBindingBuilder<BEAN, FIELDVALUE>) super.forField(field); - } - - /** - * Creates a new binding for the given field. The returned builder may be - * further configured before invoking {@link #bindInstanceFields(Object)}. - * Unlike with the {@link #forField(HasValue)} method, no explicit call to - * {@link BeanBindingBuilder#bind(String)} is needed to complete this - * binding in the case that the name of the field matches a field name found - * in the bean. - * - * @param <FIELDVALUE> - * the value type of the field - * @param field - * the field to be bound, not null - * @return the new binding builder - * - * @see #forField(HasValue) - * @see #bindInstanceFields(Object) - */ - public <FIELDVALUE> BeanBindingBuilder<BEAN, FIELDVALUE> forMemberField( - HasValue<FIELDVALUE> field) { - incompleteMemberFieldBindings.put(field, null); - return forField(field); - } - - /** - * Binds the given field to the property with the given name. The getter and - * setter methods of the property are looked up with bean introspection and - * used to read and write the property value. - * <p> - * Use the {@link #forField(HasValue)} overload instead if you want to - * further configure the new binding. - * <p> - * The property must have an accessible getter method. It need not have an - * accessible setter; in that case the property value is never updated and - * the binding is said to be <i>read-only</i>. - * - * @param <FIELDVALUE> - * the value type of the field to bind - * @param field - * the field to bind, not null - * @param propertyName - * the name of the property to bind, not null - * @return the newly created binding - * - * @throws IllegalArgumentException - * if the property name is invalid - * @throws IllegalArgumentException - * if the property has no accessible getter - * - * @see #bind(HasValue, ValueProvider, Setter) - */ - public <FIELDVALUE> Binding<BEAN, FIELDVALUE> bind( - HasValue<FIELDVALUE> field, String propertyName) { - return forField(field).bind(propertyName); - } - - @Override - public BeanBinder<BEAN> withValidator(Validator<? super BEAN> validator) { - return (BeanBinder<BEAN>) super.withValidator(validator); - } - - @SuppressWarnings("unchecked") - @Override - protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> createBinding( - HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter, - BindingValidationStatusHandler handler) { - Objects.requireNonNull(field, "field cannot be null"); - Objects.requireNonNull(converter, "converter cannot be null"); - if (incompleteMemberFieldBindings.containsKey(field)) { - BeanBindingImpl<BEAN, FIELDVALUE, TARGET> newBinding = doCreateBinding( - field, converter, handler); - incompleteMemberFieldBindings.put(field, newBinding); - return newBinding; - } else { - return (BeanBindingImpl<BEAN, FIELDVALUE, TARGET>) super.createBinding( - field, converter, handler); - } - } - - @Override - protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> doCreateBinding( - HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter, - BindingValidationStatusHandler handler) { - return new BeanBindingImpl<>(this, field, converter, handler); - } - - /** - * Binds member fields found in the given object. - * <p> - * This method processes all (Java) member fields whose type extends - * {@link HasValue} and that can be mapped to a property id. Property id - * mapping is done based on the field name or on a @{@link PropertyId} - * annotation on the field. All non-null unbound fields for which a property - * id can be determined are bound to the property id. - * </p> - * <p> - * For example: - * - * <pre> - * public class MyForm extends VerticalLayout { - * private TextField firstName = new TextField("First name"); - * @PropertyId("last") - * private TextField lastName = new TextField("Last name"); - * - * MyForm myForm = new MyForm(); - * ... - * binder.bindMemberFields(myForm); - * </pre> - * - * </p> - * This binds the firstName TextField to a "firstName" property in the item, - * lastName TextField to a "last" property. - * <p> - * It's not always possible to bind a field to a property because their - * types are incompatible. E.g. custom converter is required to bind - * {@code HasValue<String>} and {@code Integer} property (that would be a - * case of "age" property). In such case {@link IllegalStateException} will - * be thrown unless the field has been configured manually before calling - * the {@link #bindInstanceFields(Object)} method. - * <p> - * It's always possible to do custom binding for any field: the - * {@link #bindInstanceFields(Object)} method doesn't override existing - * bindings. - * - * @param objectWithMemberFields - * The object that contains (Java) member fields to bind - * @throws IllegalStateException - * if there are incompatible HasValue<T> and property types - */ - public void bindInstanceFields(Object objectWithMemberFields) { - Class<?> objectClass = objectWithMemberFields.getClass(); - - getFieldsInDeclareOrder(objectClass).stream() - .filter(memberField -> HasValue.class - .isAssignableFrom(memberField.getType())) - .forEach(memberField -> handleProperty(memberField, - objectWithMemberFields, - (property, type) -> bindProperty(objectWithMemberFields, - memberField, property, type))); - } - - @SuppressWarnings("unchecked") - private BeanBindingImpl<BEAN, ?, ?> getIncompleteMemberFieldBinding( - Field memberField, Object objectWithMemberFields) { - memberField.setAccessible(true); - try { - return incompleteMemberFieldBindings - .get(memberField.get(objectWithMemberFields)); - } catch (IllegalArgumentException | IllegalAccessException e) { - throw new RuntimeException(e); - } finally { - memberField.setAccessible(false); - } - } - - /** - * Binds {@code property} with {@code propertyType} to the field in the - * {@code objectWithMemberFields} instance using {@code memberField} as a - * reference to a member. - * - * @param objectWithMemberFields - * the object that contains (Java) member fields to build and - * bind - * @param memberField - * reference to a member field to bind - * @param property - * property name to bind - * @param propertyType - * type of the property - */ - protected void bindProperty(Object objectWithMemberFields, - Field memberField, String property, Class<?> propertyType) { - Type valueType = GenericTypeReflector.getTypeParameter( - memberField.getGenericType(), - HasValue.class.getTypeParameters()[0]); - if (valueType == null) { - throw new IllegalStateException(String.format( - "Unable to detect value type for the member '%s' in the " - + "class '%s'.", - memberField.getName(), - objectWithMemberFields.getClass().getName())); - } - if (propertyType.equals(GenericTypeReflector.erase(valueType))) { - HasValue<?> field; - // Get the field from the object - try { - field = (HasValue<?>) ReflectTools.getJavaFieldValue( - objectWithMemberFields, memberField, HasValue.class); - } catch (IllegalArgumentException | IllegalAccessException - | InvocationTargetException e) { - // If we cannot determine the value, just skip the field - return; - } - if (field == null) { - field = makeFieldInstance( - (Class<? extends HasValue<?>>) memberField.getType()); - initializeField(objectWithMemberFields, memberField, field); - } - forField(field).bind(property); - } else { - throw new IllegalStateException(String.format( - "Property type '%s' doesn't " - + "match the field type '%s'. " - + "Binding should be configured manually using converter.", - propertyType.getName(), valueType.getTypeName())); - } - } - - /** - * Makes an instance of the field type {@code fieldClass}. - * <p> - * The resulting field instance is used to bind a property to it using the - * {@link #bindInstanceFields(Object)} method. - * <p> - * The default implementation relies on the default constructor of the - * class. If there is no suitable default constructor or you want to - * configure the instantiated class then override this method and provide - * your own implementation. - * - * @see #bindInstanceFields(Object) - * @param fieldClass - * type of the field - * @return a {@code fieldClass} instance object - */ - protected HasValue<?> makeFieldInstance( - Class<? extends HasValue<?>> fieldClass) { - try { - return fieldClass.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { - throw new IllegalStateException( - String.format("Couldn't create an '%s' type instance", - fieldClass.getName()), - e); - } - } - - /** - * Returns an array containing {@link Field} objects reflecting all the - * fields of the class or interface represented by this Class object. The - * elements in the array returned are sorted in declare order from sub class - * to super class. - * - * @param searchClass - * class to introspect - * @return list of all fields in the class considering hierarchy - */ - protected List<Field> getFieldsInDeclareOrder(Class<?> searchClass) { - ArrayList<Field> memberFieldInOrder = new ArrayList<>(); - - while (searchClass != null) { - memberFieldInOrder - .addAll(Arrays.asList(searchClass.getDeclaredFields())); - searchClass = searchClass.getSuperclass(); - } - return memberFieldInOrder; - } - - @Override - protected void checkBindingsCompleted(String methodName) { - if (!incompleteMemberFieldBindings.isEmpty()) { - throw new IllegalStateException( - "All bindings created with forMemberField must " - + "be completed with bindInstanceFields before calling " - + methodName); - } - super.checkBindingsCompleted(methodName); - } - - private void initializeField(Object objectWithMemberFields, - Field memberField, HasValue<?> value) { - try { - ReflectTools.setJavaFieldValue(objectWithMemberFields, memberField, - value); - } catch (IllegalArgumentException | IllegalAccessException - | InvocationTargetException e) { - throw new IllegalStateException( - String.format("Could not assign value to field '%s'", - memberField.getName()), - e); - } - } - - private void handleProperty(Field field, Object objectWithMemberFields, - BiConsumer<String, Class<?>> propertyHandler) { - - Optional<PropertyDescriptor> descriptor = getPropertyDescriptor(field); - - if (!descriptor.isPresent()) { - return; - } - - String propertyName = descriptor.get().getName(); - if (boundProperties.contains(propertyName)) { - return; - } - - BeanBindingImpl<BEAN, ?, ?> tentativeBinding = getIncompleteMemberFieldBinding( - field, objectWithMemberFields); - if (tentativeBinding != null) { - tentativeBinding.bind(propertyName); - return; - } - - propertyHandler.accept(propertyName, - descriptor.get().getPropertyType()); - boundProperties.add(propertyName); - } - - private Optional<PropertyDescriptor> getPropertyDescriptor(Field field) { - PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class); - - String propertyId; - if (propertyIdAnnotation != null) { - // @PropertyId(propertyId) always overrides property id - propertyId = propertyIdAnnotation.value(); - } else { - propertyId = field.getName(); - } - - List<PropertyDescriptor> descriptors; - try { - descriptors = BeanUtil.getBeanPropertyDescriptors(beanType); - } catch (IntrospectionException e) { - throw new IllegalArgumentException(String.format( - "Could not resolve bean '%s' properties (see the cause):", - beanType.getName()), e); - } - Optional<PropertyDescriptor> propertyDescitpor = descriptors.stream() - .filter(descriptor -> minifyFieldName(descriptor.getName()) - .equals(minifyFieldName(propertyId))) - .findFirst(); - return propertyDescitpor; - } - - private String minifyFieldName(String fieldName) { - return fieldName.toLowerCase(Locale.ENGLISH).replace("_", ""); - } - -} |