123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- /*
- * 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.util.converter.Converter;
- import com.vaadin.data.validator.BeanValidator;
- import com.vaadin.server.SerializableBiConsumer;
- import com.vaadin.server.SerializableFunction;
- import com.vaadin.server.SerializablePredicate;
- 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> setRequired(
- ErrorMessageProvider errorMessageProvider);
-
- @Override
- public default BeanBindingBuilder<BEAN, TARGET> setRequired(
- String errorMessage) {
- return (BeanBindingBuilder<BEAN, TARGET>) BindingBuilder.super.setRequired(
- 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(
- ValidationStatusHandler 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(SerializableFunction,
- * SerializableBiConsumer)
- */
- 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,
- ValidationStatusHandler 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(
- ValidationStatusHandler handler) {
- return (BeanBindingBuilder<BEAN, TARGET>) super.withValidationStatusHandler(
- handler);
- }
-
- @Override
- public BeanBindingBuilder<BEAN, TARGET> setRequired(
- ErrorMessageProvider errorMessageProvider) {
- return (BeanBindingBuilder<BEAN, TARGET>) super.setRequired(
- 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, SerializableFunction, SerializableBiConsumer)
- */
- 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,
- 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.
- * <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(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}.
- * <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("_", "");
- }
-
- }
|