/* * 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.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import com.vaadin.data.util.BeanUtil; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.validator.BeanValidator; import com.vaadin.ui.AbstractMultiSelect; import com.vaadin.ui.AbstractSingleSelect; /** * A {@code Binder} subclass specialized for binding beans: 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 * the bean type * * @since 8.0 */ public class BeanBinder extends Binder { /** * Represents the binding between a single field and a bean property. * * @param * the bean type * @param * the field value type * @param * the target property type */ public interface BeanBinding extends Binding { @Override public BeanBinding withValidator( Validator validator); @Override public default BeanBinding withValidator( Predicate predicate, String message) { return (BeanBinding) Binding.super.withValidator( predicate, message); } @Override public BeanBinding withConverter( Converter converter); @Override public default BeanBinding withConverter( Function toModel, Function toPresentation) { return (BeanBinding) Binding.super.withConverter( toModel, toPresentation); } @Override public default BeanBinding withConverter( Function toModel, Function toPresentation, String errorMessage) { return (BeanBinding) Binding.super.withConverter( toModel, toPresentation, errorMessage); } /** * 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 * * @throws IllegalArgumentException * if the property name is invalid * @throws IllegalArgumentException * if the property has no accessible getter * * @see Binding#bind(Function, java.util.function.BiConsumer) */ public void bind(String propertyName); } /** * An internal implementation of {@link BeanBinding}. * * @param * the bean type * @param * the field value type * @param * the target property type */ protected static class BeanBindingImpl extends BindingImpl implements BeanBinding { private Method getter; private Method setter; /** * 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 BeanBinding withValidator( Validator validator) { return (BeanBinding) super.withValidator( validator); } @Override public BeanBinding withConverter( Converter converter) { return (BeanBinding) super.withConverter( converter); } @Override public void bind(String propertyName) { checkUnbound(); Binding finalBinding; finalBinding = withConverter(createConverter()); if (BeanValidator.checkBeanValidationAvailable()) { finalBinding = finalBinding.withValidator(new BeanValidator( getBinder().beanType, propertyName, findLocale())); } PropertyDescriptor descriptor = getDescriptor(propertyName); getter = descriptor.getReadMethod(); setter = descriptor.getWriteMethod(); finalBinding.bind(this::getValue, this::setValue); } @Override protected BeanBinder getBinder() { return (BeanBinder) super.getBinder(); } private void setValue(BEAN bean, Object value) { try { if (setter != null) { setter.invoke(bean, value); } } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } private Object getValue(BEAN bean) { try { return getter.invoke(bean); } 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() { return Converter.from( fieldValue -> getter.getReturnType().cast(fieldValue), propertyValue -> (TARGET) propertyValue, exception -> { throw new RuntimeException(exception); }); } } private final Class beanType; /** * Creates a new {@code BeanBinder} supporting beans of the given type. * * @param beanType * the bean {@code Class} instance, not null */ public BeanBinder(Class beanType) { BeanValidator.checkBeanValidationAvailable(); this.beanType = beanType; } @Override public BeanBinding forField( HasValue field) { return createBinding(field, Converter.identity(), this::handleValidationStatus); } @Override public BeanBinding forSelect( AbstractSingleSelect select) { return (BeanBinding) super.forSelect( select); } @Override public BeanBinding, Set> forSelect( AbstractMultiSelect select) { return (BeanBinding, Set>) super.forSelect( select); } /** * 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 * * @throws IllegalArgumentException * if the property name is invalid * @throws IllegalArgumentException * if the property has no accessible getter * * @see #bind(HasValue, java.util.function.Function, * java.util.function.BiConsumer) */ public void bind(HasValue field, String propertyName) { forField(field).bind(propertyName); } @Override public BeanBinder withValidator(Validator 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); } }