All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.data.BeanPropertySet Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * 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.HashMap;
import java.util.List;
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;

/**
 * A {@link PropertySet} that uses reflection to find bean properties.
 *
 * @author Vaadin Ltd
 *
 * @since 8.0
 *
 * @param 
 *            the type of the bean
 */
public class BeanPropertySet implements PropertySet {

    /**
     * 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 InstanceKey instanceKey;

        private SerializedPropertySet(InstanceKey instanceKey) {
            this.instanceKey = instanceKey;
        }

        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(instanceKey.type, instanceKey.checkNestedDefinitions,
                    new PropertyFilterDefinition(instanceKey.depth,
                            instanceKey.ignorePackageNames));
        }
    }

    /**
     * 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 static class BeanPropertyDefinition
            extends AbstractBeanPropertyDefinition {

        public BeanPropertyDefinition(BeanPropertySet propertySet,
                Class propertyHolderType, PropertyDescriptor descriptor) {
            super(propertySet, propertyHolderType, descriptor);
        }

        @Override
        public ValueProvider getGetter() {
            return bean -> {
                Method readMethod = getDescriptor().getReadMethod();
                Object value = invokeWrapExceptions(readMethod, bean);
                return getType().cast(value);
            };
        }

        @Override
        public Optional> getSetter() {
            if (getDescriptor().getWriteMethod() == null) {
                return Optional.empty();
            }

            Setter 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().instanceKey.type, getName());
        }
    }

    /**
     * Contains properties for a bean type which is nested in another
     * definition.
     *
     * @since 8.1
     * @param 
     *            the bean type
     * @param 
     *            the value type returned by the getter and set by the setter
     */
    public static class NestedBeanPropertyDefinition
            extends AbstractBeanPropertyDefinition {

        /**
         * Default maximum depth for scanning nested properties.
         *
         * @since 8.2
         */
        protected static final int MAX_PROPERTY_NESTING_DEPTH = 10;

        private final PropertyDefinition parent;

        public NestedBeanPropertyDefinition(BeanPropertySet propertySet,
                PropertyDefinition parent,
                PropertyDescriptor descriptor) {
            super(propertySet, parent.getType(), descriptor);
            this.parent = parent;
        }

        @Override
        public ValueProvider getGetter() {
            return bean -> {
                Method readMethod = getDescriptor().getReadMethod();
                Object value = invokeWrapExceptions(readMethod,
                        parent.getGetter().apply(bean));
                return getType().cast(value);
            };
        }

        @Override
        public Optional> getSetter() {
            if (getDescriptor().getWriteMethod() == null) {
                return Optional.empty();
            }

            Setter 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);
        }

        @Override
        public String getName() {
            return parent.getName() + "." + super.getName();
        }

        @Override
        public String getTopLevelName() {
            return super.getName();
        }

        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().instanceKey.type, getName());
        }

        /**
         * Gets the parent property definition.
         *
         * @return the property definition for the parent
         */
        public PropertyDefinition getParent() {
            return parent;
        }
    }

    /**
     * Key for identifying cached BeanPropertySet instances.
     *
     * @since 8.2
     */
    private static class InstanceKey implements Serializable {
        private Class type;
        private boolean checkNestedDefinitions;
        private int depth;
        private List ignorePackageNames;

        public InstanceKey(Class type, boolean checkNestedDefinitions,
                int depth, List ignorePackageNames) {
            this.type = type;
            this.checkNestedDefinitions = checkNestedDefinitions;
            this.depth = depth;
            this.ignorePackageNames = ignorePackageNames;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + (checkNestedDefinitions ? 1231 : 1237);
            result = prime * result + depth;
            result = prime * result + ((ignorePackageNames == null) ? 0
                    : ignorePackageNames.hashCode());
            result = prime * result + ((type == null) ? 0 : type.hashCode());
            return result;
        }

        @Override
        @SuppressWarnings("rawtypes")
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            InstanceKey other = (InstanceKey) obj;
            if (checkNestedDefinitions != other.checkNestedDefinitions) {
                return false;
            }
            if (depth != other.depth) {
                return false;
            }
            if (ignorePackageNames == null) {
                if (other.ignorePackageNames != null) {
                    return false;
                }
            } else if (!ignorePackageNames.equals(other.ignorePackageNames)) {
                return false;
            }
            return Objects.equals(type, other.type);
        }

    }

    private static final ConcurrentMap, BeanPropertySet> INSTANCES = new ConcurrentHashMap<>();

    private final InstanceKey instanceKey;

    private final Map> definitions;

    private BeanPropertySet(InstanceKey instanceKey) {
        this.instanceKey = instanceKey;

        try {
            definitions = BeanUtil.getBeanPropertyDescriptors(instanceKey.type)
                    .stream().filter(BeanPropertySet::hasNonObjectReadMethod)
                    .map(descriptor -> new BeanPropertyDefinition<>(this,
                            instanceKey.type, descriptor))
                    .collect(Collectors.toMap(PropertyDefinition::getName,
                            Function.identity(), this::mergePropertyDefinitions));
        } catch (IntrospectionException e) {
            throw new IllegalArgumentException(
                    "Cannot find property descriptors for "
                            + instanceKey.type.getName(),
                    e);
        }
    }

    private PropertyDefinition mergePropertyDefinitions(
            PropertyDefinition def1, PropertyDefinition def2) {
        if (!def1.getType().equals(def2.getType())) {
            throw new IllegalStateException(String.format(
                    "Two property definition for property %s are discovered with different types: %s and %s",
                    def1.getName(), def1.getType(), def2.getType()));
        }
        if (!def1.getPropertyHolderType()
                .equals(def2.getPropertyHolderType())) {
            throw new IllegalStateException(String.format(
                    "Two property definition for property %s are discovered with different holder types: %s and %s",
                    def1.getName(), def1.getPropertyHolderType(),
                    def2.getPropertyHolderType()));
        }
        return def1;
    }

    private BeanPropertySet(InstanceKey instanceKey,
            Map> definitions) {
        this.instanceKey = instanceKey;
        this.definitions = new HashMap<>(definitions);
    }

    private BeanPropertySet(InstanceKey instanceKey,
            boolean checkNestedDefinitions,
            PropertyFilterDefinition propertyFilterDefinition) {
        this(instanceKey);
        if (checkNestedDefinitions) {
            Objects.requireNonNull(propertyFilterDefinition,
                    "You must define a property filter callback if using nested property scan.");
            findNestedDefinitions(definitions, 0, propertyFilterDefinition);
        }
    }

    private void findNestedDefinitions(
            Map> parentDefinitions, int depth,
            PropertyFilterDefinition filterCallback) {
        if (depth >= filterCallback.getMaxNestingDepth()) {
            return;
        }
        if (parentDefinitions == null) {
            return;
        }
        Map> moreProps = new HashMap<>();
        for (String parentPropertyKey : parentDefinitions.keySet()) {
            PropertyDefinition parentProperty = parentDefinitions
                    .get(parentPropertyKey);
            Class type = parentProperty.getType();
            if (type.getPackage() == null || type.isEnum()) {
                continue;
            }
            String packageName = type.getPackage().getName();
            if (filterCallback.getIgnorePackageNamesStartingWith().stream()
                    .anyMatch(prefix -> packageName.startsWith(prefix))) {
                continue;
            }

            try {
                List descriptors = BeanUtil
                        .getBeanPropertyDescriptors(type).stream()
                        .filter(BeanPropertySet::hasNonObjectReadMethod)
                        .collect(Collectors.toList());
                for (PropertyDescriptor descriptor : descriptors) {
                    String name = parentPropertyKey + "."
                            + descriptor.getName();
                    PropertyDescriptor subDescriptor = BeanUtil
                            .getPropertyDescriptor(instanceKey.type, name);
                    moreProps.put(name, new NestedBeanPropertyDefinition<>(this,
                            parentProperty, subDescriptor));

                }
            } catch (IntrospectionException e) {
                throw new IllegalArgumentException(
                        "Error finding nested property descriptors for "
                                + type.getName(),
                        e);
            }
        }
        if (moreProps.size() > 0) {
            definitions.putAll(moreProps);
            findNestedDefinitions(moreProps, ++depth, filterCallback);
        }

    }

    /**
     * Gets a {@link BeanPropertySet} for the given bean type.
     *
     * @param beanType
     *            the bean type to get a property set for, not null
     * @param 
     *            bean type
     * @return the bean property set, not null
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static  PropertySet get(Class beanType) {
        Objects.requireNonNull(beanType, "Bean type cannot be null");
        InstanceKey key = new InstanceKey(beanType, false, 0, null);
        // Cache the reflection results
        return (PropertySet) INSTANCES
                .computeIfAbsent(key, ignored -> new BeanPropertySet<>(key))
                .copy();
    }

    private BeanPropertySet copy() {
        return new BeanPropertySet<>(instanceKey, definitions);
    }

    /**
     * Gets a {@link BeanPropertySet} for the given bean type.
     *
     * @param beanType
     *            the bean type to get a property set for, not null
     * @param checkNestedDefinitions
     *            whether to scan for nested definitions in beanType
     * @param filterDefinition
     *            filtering conditions for nested properties
     * @param 
     *            bean type
     * @return the bean property set, not null
     * @since 8.2
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static  PropertySet get(Class beanType,
            boolean checkNestedDefinitions,
            PropertyFilterDefinition filterDefinition) {
        Objects.requireNonNull(beanType, "Bean type cannot be null");
        InstanceKey key = new InstanceKey(beanType, false,
                filterDefinition.getMaxNestingDepth(),
                filterDefinition.getIgnorePackageNamesStartingWith());
        return (PropertySet) INSTANCES
                .computeIfAbsent(key, k -> new BeanPropertySet<>(key,
                        checkNestedDefinitions, filterDefinition))
                .copy();
    }

    @Override
    public Stream> getProperties() {
        return definitions.values().stream();
    }

    @Override
    public Optional> getProperty(String name)
            throws IllegalArgumentException {
        Optional> definition = Optional
                .ofNullable(definitions.get(name));
        if (!definition.isPresent() && name.contains(".")) {
            try {
                String parentName = name.substring(0, name.lastIndexOf('.'));
                Optional> parent = getProperty(
                        parentName);
                if (!parent.isPresent()) {
                    throw new IllegalArgumentException(
                            "Cannot find property descriptor [" + parentName
                                    + "] for " + instanceKey.type.getName());
                }

                Optional descriptor = Optional.ofNullable(
                        BeanUtil.getPropertyDescriptor(instanceKey.type, name));
                if (descriptor.isPresent()) {
                    NestedBeanPropertyDefinition 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 " + instanceKey.type.getName());
                }

            } catch (IntrospectionException e) {
                throw new IllegalArgumentException(
                        "Cannot find property descriptors for "
                                + instanceKey.type.getName(),
                        e);
            }
        }
        return definition;
    }

    /**
     * Gets the bean type of this bean property set.
     *
     * @since 8.2
     * @return the bean type of this bean property set
     */
    public Class getBeanType() {
        return instanceKey.type;
    }

    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 " + instanceKey.type.getName();
    }

    @SuppressWarnings("rawtypes")
    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(instanceKey);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy