/* * 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.io.IOException; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import com.vaadin.data.util.BeanUtil; import com.vaadin.server.Setter; import com.vaadin.shared.util.SharedUtil; import com.vaadin.util.ReflectTools; /** * A {@link PropertySet} that uses reflection to find bean properties. * * @author Vaadin Ltd * * @since 8.0 * * @param <T> * the type of the bean */ public class BeanPropertySet<T> implements PropertySet<T> { /** * Serialized form of a property set. When deserialized, the property set * for the corresponding bean type is requested, which either returns the * existing cached instance or creates a new one. * * @see #readResolve() * @see BeanPropertyDefinition#writeReplace() */ private static class SerializedPropertySet implements Serializable { private final Class<?> beanType; private SerializedPropertySet(Class<?> beanType) { this.beanType = beanType; } private Object readResolve() { /* * When this instance is deserialized, it will be replaced with a * property set for the corresponding bean type and property name. */ return get(beanType); } } /** * Serialized form of a property definition. When deserialized, the property * set for the corresponding bean type is requested, which either returns * the existing cached instance or creates a new one. The right property * definition is then fetched from the property set. * * @see #readResolve() * @see BeanPropertySet#writeReplace() */ private static class SerializedPropertyDefinition implements Serializable { private final Class<?> beanType; private final String propertyName; private SerializedPropertyDefinition(Class<?> beanType, String propertyName) { this.beanType = beanType; this.propertyName = propertyName; } private Object readResolve() throws IOException { /* * When this instance is deserialized, it will be replaced with a * property definition for the corresponding bean type and property * name. */ return get(beanType).getProperty(propertyName) .orElseThrow(() -> new IOException( beanType + " no longer has a property named " + propertyName)); } } private abstract static class AbstractBeanPropertyDefinition<T, V> implements PropertyDefinition<T, V> { private final PropertyDescriptor descriptor; private final BeanPropertySet<T> propertySet; private final Class<?> propertyHolderType; public AbstractBeanPropertyDefinition(BeanPropertySet<T> propertySet, Class<?> propertyHolderType, PropertyDescriptor descriptor) { this.propertySet = propertySet; this.propertyHolderType = propertyHolderType; this.descriptor = descriptor; if (descriptor.getReadMethod() == null) { throw new IllegalArgumentException( "Bean property has no accessible getter: " + propertySet.beanType + "." + descriptor.getName()); } } @SuppressWarnings("unchecked") @Override public Class<V> getType() { return (Class<V>) ReflectTools .convertPrimitiveType(descriptor.getPropertyType()); } @Override public String getName() { return descriptor.getName(); } @Override public String getCaption() { return SharedUtil.propertyIdToHumanFriendly(getName()); } @Override public BeanPropertySet<T> getPropertySet() { return propertySet; } protected PropertyDescriptor getDescriptor() { return descriptor; } @Override public Class<?> getPropertyHolderType() { return propertyHolderType; } } private static class BeanPropertyDefinition<T, V> extends AbstractBeanPropertyDefinition<T, V> { public BeanPropertyDefinition(BeanPropertySet<T> propertySet, Class<T> propertyHolderType, PropertyDescriptor descriptor) { super(propertySet, propertyHolderType, descriptor); } @Override public ValueProvider<T, V> getGetter() { return bean -> { Method readMethod = getDescriptor().getReadMethod(); Object value = invokeWrapExceptions(readMethod, bean); return getType().cast(value); }; } @Override public Optional<Setter<T, V>> getSetter() { if (getDescriptor().getWriteMethod() == null) { return Optional.empty(); } Setter<T, V> setter = (bean, value) -> { // Do not "optimize" this getter call, // if its done outside the code block, that will produce // NotSerializableException because of some lambda compilation // magic Method innerSetter = getDescriptor().getWriteMethod(); invokeWrapExceptions(innerSetter, bean, value); }; return Optional.of(setter); } private Object writeReplace() { /* * Instead of serializing this actual property definition, only * serialize a DTO that when deserialized will get the corresponding * property definition from the cache. */ return new SerializedPropertyDefinition(getPropertySet().beanType, getName()); } } /** * Contains properties for a bean type which is nested in another * definition. * * @since 8.1 * @param <T> * the bean type * @param <V> * the value type returned by the getter and set by the setter */ public static class NestedBeanPropertyDefinition<T, V> extends AbstractBeanPropertyDefinition<T, V> { private final PropertyDefinition<T, ?> parent; public NestedBeanPropertyDefinition(BeanPropertySet<T> propertySet, PropertyDefinition<T, ?> parent, PropertyDescriptor descriptor) { super(propertySet, parent.getType(), descriptor); this.parent = parent; } @Override public ValueProvider<T, V> getGetter() { return bean -> { Method readMethod = getDescriptor().getReadMethod(); Object value = invokeWrapExceptions(readMethod, parent.getGetter().apply(bean)); return getType().cast(value); }; } @Override public Optional<Setter<T, V>> getSetter() { if (getDescriptor().getWriteMethod() == null) { return Optional.empty(); } Setter<T, V> setter = (bean, value) -> { // Do not "optimize" this getter call, // if its done outside the code block, that will produce // NotSerializableException because of some lambda compilation // magic Method innerSetter = getDescriptor().getWriteMethod(); invokeWrapExceptions(innerSetter, parent.getGetter().apply(bean), value); }; return Optional.of(setter); } private Object writeReplace() { /* * Instead of serializing this actual property definition, only * serialize a DTO that when deserialized will get the corresponding * property definition from the cache. */ return new SerializedPropertyDefinition(getPropertySet().beanType, parent.getName() + "." + getName()); } /** * Gets the parent property definition. * * @return the property definition for the parent */ public PropertyDefinition<T, ?> getParent() { return parent; } } private static final ConcurrentMap<Class<?>, BeanPropertySet<?>> instances = new ConcurrentHashMap<>(); private final Class<T> beanType; private final Map<String, PropertyDefinition<T, ?>> definitions; private BeanPropertySet(Class<T> beanType) { this.beanType = beanType; try { definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream() .filter(BeanPropertySet::hasNonObjectReadMethod) .map(descriptor -> new BeanPropertyDefinition<>(this, beanType, descriptor)) .collect(Collectors.toMap(PropertyDefinition::getName, Function.identity())); } catch (IntrospectionException e) { throw new IllegalArgumentException( "Cannot find property descriptors for " + beanType.getName(), e); } } /** * Gets a {@link BeanPropertySet} for the given bean type. * * @param beanType * the bean type to get a property set for, not <code>null</code> * @return the bean property set, not <code>null</code> */ @SuppressWarnings("unchecked") public static <T> PropertySet<T> get(Class<? extends T> beanType) { Objects.requireNonNull(beanType, "Bean type cannot be null"); // Cache the reflection results return (PropertySet<T>) instances.computeIfAbsent(beanType, BeanPropertySet::new); } @Override public Stream<PropertyDefinition<T, ?>> getProperties() { return definitions.values().stream(); } @Override public Optional<PropertyDefinition<T, ?>> getProperty(String name) throws IllegalArgumentException { Optional<PropertyDefinition<T, ?>> definition = Optional .ofNullable(definitions.get(name)); if (!definition.isPresent() && name.contains(".")) { try { String parentName = name.substring(0, name.lastIndexOf('.')); Optional<PropertyDefinition<T, ?>> parent = getProperty( parentName); if (!parent.isPresent()) { throw new IllegalArgumentException( "Cannot find property descriptor [" + parentName + "] for " + beanType.getName()); } Optional<PropertyDescriptor> descriptor = Optional.ofNullable( BeanUtil.getPropertyDescriptor(beanType, name)); if (descriptor.isPresent()) { NestedBeanPropertyDefinition<T, ?> nestedDefinition = new NestedBeanPropertyDefinition<>( this, parent.get(), descriptor.get()); definitions.put(name, nestedDefinition); return Optional.of(nestedDefinition); } else { throw new IllegalArgumentException( "Cannot find property descriptor [" + name + "] for " + beanType.getName()); } } catch (IntrospectionException e) { throw new IllegalArgumentException( "Cannot find property descriptors for " + beanType.getName(), e); } } return definition; } private static boolean hasNonObjectReadMethod( PropertyDescriptor descriptor) { Method readMethod = descriptor.getReadMethod(); return readMethod != null && readMethod.getDeclaringClass() != Object.class; } private static Object invokeWrapExceptions(Method method, Object target, Object... parameters) { try { return method.invoke(target, parameters); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } @Override public String toString() { return "Property set for bean " + beanType.getName(); } private Object writeReplace() { /* * Instead of serializing this actual property set, only serialize a DTO * that when deserialized will get the corresponding property set from * the cache. */ return new SerializedPropertySet(beanType); } }