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

org.xwiki.properties.internal.DefaultBeanDescriptor Maven / Gradle / Ivy

There is a newer version: 16.8.0-rc-1
Show newest version
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.properties.internal;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.component.util.DefaultParameterizedType;
import org.xwiki.properties.BeanDescriptor;
import org.xwiki.properties.PropertyDescriptor;
import org.xwiki.properties.PropertyGroupDescriptor;
import org.xwiki.properties.annotation.PropertyAdvanced;
import org.xwiki.properties.annotation.PropertyDescription;
import org.xwiki.properties.annotation.PropertyDisplayHidden;
import org.xwiki.properties.annotation.PropertyDisplayType;
import org.xwiki.properties.annotation.PropertyFeature;
import org.xwiki.properties.annotation.PropertyGroup;
import org.xwiki.properties.annotation.PropertyHidden;
import org.xwiki.properties.annotation.PropertyId;
import org.xwiki.properties.annotation.PropertyMandatory;
import org.xwiki.properties.annotation.PropertyName;

/**
 * Default implementation for BeanDescriptor.
 *
 * @version $Id: f4908afac9aeb46fc84fae9e3a2aead33c05d1cb $
 * @since 2.0M2
 */
public class DefaultBeanDescriptor implements BeanDescriptor
{
    /**
     * The logger to use to log.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultBeanDescriptor.class);

    private static final List> COMMON_ANNOTATION_CLASSES = Arrays.asList(
        PropertyMandatory.class, Deprecated.class, PropertyAdvanced.class, PropertyGroup.class,
        PropertyFeature.class, PropertyDisplayType.class, PropertyDisplayHidden.class);

    /**
     * @see #getBeanClass()
     */
    private Class beanClass;

    /**
     * The properties of the bean.
     */
    private Map parameterDescriptorMap = new LinkedHashMap<>();

    private Map groups = new HashMap<>();

    /**
     * @param beanClass the class of the JAVA bean.
     */
    public DefaultBeanDescriptor(Class beanClass)
    {
        this.beanClass = beanClass;

        extractBeanDescriptor();
    }

    /**
     * Extract informations form the bean.
     */
    protected void extractBeanDescriptor()
    {
        Object defaultInstance = null;

        // Try to get an instance of the bean class to get default values of the bean properties in the property
        // descriptor. Note that a java bean is supposed to always have an empty public constructor but we want to
        // support the use cases where it's not the case and still make it work. In this case, we simply don't
        // set default values.
        Constructor constructor = ConstructorUtils.getAccessibleConstructor(getBeanClass());
        if (constructor != null) {
            try {
                defaultInstance = constructor.newInstance();
            } catch (Exception e) {
                LOGGER.debug("Failed to create a new default instance for class [{}]. The BeanDescriptor will not "
                    + "contains any default value information.", getBeanClass().getName(), e);
            }
        }

        try {
            // Get public fields
            for (Class currentClass = getBeanClass(); currentClass != null; currentClass =
                    currentClass.getSuperclass()) {
                Field[] fields = currentClass.getFields();
                for (Field field : fields) {
                    if (!Modifier.isStatic(field.getModifiers())) {
                        extractPropertyDescriptor(field, defaultInstance);
                    }
                }
            }

            // Get getter/setter based properties
            BeanInfo beanInfo = Introspector.getBeanInfo(getBeanClass());
            java.beans.PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
            if (propertyDescriptors != null) {
                for (java.beans.PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                    if (propertyDescriptor != null) {
                        extractPropertyDescriptor(propertyDescriptor, defaultInstance);
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.warn("Failed to load bean descriptor for class [{}]. Ignoring it. Root cause: [{}]",
                getBeanClass().getName(), ExceptionUtils.getRootCauseMessage(e));
        }
    }

    /**
     * Extract provided properties information and insert it in {@link #parameterDescriptorMap}.
     *
     * @param propertyDescriptor the JAVA bean property descriptor.
     * @param defaultInstance the default instance of bean class.
     */
    protected void extractPropertyDescriptor(java.beans.PropertyDescriptor propertyDescriptor, Object defaultInstance)
    {
        DefaultPropertyDescriptor desc = new DefaultPropertyDescriptor();

        Method writeMethod = propertyDescriptor.getWriteMethod();

        if (writeMethod != null) {
            Method readMethod = propertyDescriptor.getReadMethod();

            // is parameter hidden
            PropertyHidden parameterHidden = extractPropertyAnnotation(writeMethod, readMethod, PropertyHidden.class);

            if (parameterHidden == null) {
                // get parameter id
                PropertyId propertyId = extractPropertyAnnotation(writeMethod, readMethod, PropertyId.class);
                desc.setId(propertyId != null ? propertyId.value() : propertyDescriptor.getName());

                // set parameter type
                Type propertyType;
                if (readMethod != null) {
                    propertyType = readMethod.getGenericReturnType();
                } else {
                    propertyType = writeMethod.getGenericParameterTypes()[0];
                }
                desc.setPropertyType(propertyType);

                // get parameter display name
                PropertyName parameterName = extractPropertyAnnotation(writeMethod, readMethod, PropertyName.class);

                desc.setName(parameterName != null ? parameterName.value() : desc.getId());

                // get parameter description
                PropertyDescription parameterDescription =
                        extractPropertyAnnotation(writeMethod, readMethod, PropertyDescription.class);

                desc.setDescription(parameterDescription != null ? parameterDescription.value() : propertyDescriptor
                        .getShortDescription());

                Map annotations = new HashMap<>();
                COMMON_ANNOTATION_CLASSES.forEach(aClass ->
                        annotations.put(aClass, extractPropertyAnnotation(writeMethod, readMethod, aClass))
                );

                setCommonProperties(desc, annotations);

                if (defaultInstance != null && readMethod != null) {
                    // get default value
                    try {
                        desc.setDefaultValue(readMethod.invoke(defaultInstance));
                    } catch (Exception e) {
                        LOGGER.warn("Failed to get default property value from getter [{}] in class [{}]. Ignoring it. "
                            + "Root cause [{}]", readMethod.getName(), getBeanClass(),
                            ExceptionUtils.getRootCauseMessage(e));
                    }
                }

                desc.setWriteMethod(writeMethod);

                desc.setReadMethod(readMethod);

                this.parameterDescriptorMap.put(desc.getId(), desc);
            }
        }
    }

    /**
     * Extract provided properties informations and insert it in {@link #parameterDescriptorMap}.
     *
     * @param field the JAVA bean property descriptor.
     * @param defaultInstance the default instance of bean class.
     */
    protected void extractPropertyDescriptor(Field field, Object defaultInstance)
    {
        DefaultPropertyDescriptor desc = new DefaultPropertyDescriptor();

        // is parameter hidden
        PropertyHidden parameterHidden = field.getAnnotation(PropertyHidden.class);

        if (parameterHidden == null) {
            // get parameter id
            PropertyId propertyId = field.getAnnotation(PropertyId.class);
            desc.setId(propertyId != null ? propertyId.value() : field.getName());

            // set parameter type
            desc.setPropertyType(field.getGenericType());

            // get parameter name
            PropertyName parameterName = field.getAnnotation(PropertyName.class);

            desc.setName(parameterName != null ? parameterName.value() : desc.getId());

            // get parameter description
            PropertyDescription parameterDescription = field.getAnnotation(PropertyDescription.class);

            desc.setDescription(parameterDescription != null ? parameterDescription.value() : desc.getId());

            Map annotations = new HashMap<>();
            COMMON_ANNOTATION_CLASSES.forEach(aClass ->
                    annotations.put(aClass, field.getAnnotation(aClass))
            );

            setCommonProperties(desc, annotations);

            if (defaultInstance != null) {
                // get default value
                try {
                    desc.setDefaultValue(field.get(defaultInstance));
                } catch (Exception e) {
                    LOGGER.warn("Failed to get default property value from field [{}] in class [{}]. Ignoring it. "
                        + "Root cause: [{}]", field.getName(), getBeanClass(), ExceptionUtils.getRootCauseMessage(e));
                }
            }

            desc.setField(field);

            this.parameterDescriptorMap.put(desc.getId(), desc);
        }
    }

    private void setCommonProperties(DefaultPropertyDescriptor desc, Map annotations)
    {
        desc.setMandatory(annotations.get(PropertyMandatory.class) != null);
        desc.setDeprecated(annotations.get(Deprecated.class) != null);
        desc.setAdvanced(annotations.get(PropertyAdvanced.class) != null);
        handlePropertyFeatureAndGroupAnnotations(desc, annotations);
        handlePropertyDisplayTypeAnnotation(desc, annotations);
        desc.setDisplayHidden(annotations.get(PropertyDisplayHidden.class) != null);
    }

    private void handlePropertyFeatureAndGroupAnnotations(DefaultPropertyDescriptor desc, Map annotations)
    {
        PropertyGroup parameterGroup = (PropertyGroup) annotations.get(PropertyGroup.class);
        PropertyGroupDescriptor group = this.groups.get(parameterGroup);
        if (group == null && parameterGroup != null) {
            group = new PropertyGroupDescriptor(Arrays.asList(parameterGroup.value()));
        } else if (group == null) {
            group = new PropertyGroupDescriptor(null);
        }
        desc.setGroupDescriptor(group);
        if (parameterGroup != null) {
            this.groups.put(parameterGroup, group);
        }

        PropertyFeature parameterFeature = (PropertyFeature) annotations.get(PropertyFeature.class);
        if (parameterFeature != null) {
            if (group.getFeature() != null) {
                throw new RuntimeException(String.format("Property [%s] has overridden a feature "
                    + "(previous: [%s], new: [%s])", desc.getId(), group.getFeature(), parameterFeature.value()));
            }
            group.setFeature(parameterFeature.value());
        }
    }

    private void handlePropertyDisplayTypeAnnotation(DefaultPropertyDescriptor desc, Map annotations)
    {
        PropertyDisplayType displayTypeAnnotation = (PropertyDisplayType) annotations.get(PropertyDisplayType.class);
        Type displayType;
        if (displayTypeAnnotation != null && displayTypeAnnotation.value().length > 0) {
            Class[] types = displayTypeAnnotation.value();
            if (types.length > 1) {
                displayType = new DefaultParameterizedType(null, types[0], ArrayUtils.remove(types, 0));
            } else {
                displayType = types[0];
            }
        } else {
            displayType = desc.getPropertyType();
        }
        desc.setDisplayType(displayType);
    }

    /**
     * Get the parameter annotation. Try first on the setter then on the getter if no annotation has been found.
     *
     * @param  the Class object corresponding to the annotation type.
     * @param writeMethod the method that should be used to write the property value.
     * @param readMethod the method that should be used to read the property value.
     * @param annotationClass the Class object corresponding to the annotation type.
     * @return this element's annotation for the specified annotation type if present on this element, else null.
     */
    protected  T extractPropertyAnnotation(Method writeMethod, Method readMethod,
            Class annotationClass)
    {
        T parameterDescription = writeMethod.getAnnotation(annotationClass);

        if (parameterDescription == null && readMethod != null) {
            parameterDescription = readMethod.getAnnotation(annotationClass);
        }

        return parameterDescription;
    }

    @Override
    public Class getBeanClass()
    {
        return this.beanClass;
    }

    @Override
    public Collection getProperties()
    {
        return this.parameterDescriptorMap.values();
    }

    @Override
    public PropertyDescriptor getProperty(String propertyName)
    {
        return this.parameterDescriptorMap.get(propertyName);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy