cat.albirar.framework.dynabean.impl.DynaBeanDescriptor.java Source code

Java tutorial

Introduction

Here is the source code for cat.albirar.framework.dynabean.impl.DynaBeanDescriptor.java

Source

/*
 * This file is part of "dynabean".
 * 
 * "dynabean" is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * "dynabean" 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with calendar. If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * Copyright (C) 2015 Octavi Forns <ofornes@albirar.cat>
 */

package cat.albirar.framework.dynabean.impl;

import static cat.albirar.framework.dynabean.impl.DynaBeanImplementationUtils.fromMethodToPropertyName;
import static cat.albirar.framework.dynabean.impl.DynaBeanImplementationUtils.isCorrectProperty;
import static cat.albirar.framework.dynabean.impl.DynaBeanImplementationUtils.isGetter;
import static cat.albirar.framework.dynabean.impl.DynaBeanImplementationUtils.isPropertyMethod;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import cat.albirar.framework.dynabean.annotations.DynaBean;
import cat.albirar.framework.dynabean.annotations.PropertyDefaultValue;
import cat.albirar.framework.utilities.StringUtilities;

/**
 * A descriptor for a {@link DynaBeanImpl}.
 * 
 * @author <a href="mailto:ofornes@albirar.cat">Octavi Forns ofornes@albirar.cat</a>
 * @since 2.0
 */
public class DynaBeanDescriptor<T> implements Serializable {

    private static final long serialVersionUID = 90102804695593324L;

    private static final Logger logger = LoggerFactory.getLogger(DynaBeanDescriptor.class);

    /** The implemented qualified type name. */
    private Class<T> implementedType;

    /** The list of properties descriptors. */
    private Map<String, DynaBeanPropertyDescriptor> properties;

    /** The associated dynaBean factory. */
    private transient IDynaBeanImplementationFactory factory;

    /**
     * A pattern to create messages for ignoring properties
     */
    private static final String PATTERN_IGNORING_PROPERTIES = "At model '%s', property method '%s' %s. Ignoring";
    /**
     * A pattern to create a 'toString' response. Only need to call {@link String#format(String, Object...)} with the
     * values.
     */
    private String patternForToString;

    private boolean validDescriptor;

    /**
     * Default constructor.
     */
    DynaBeanDescriptor(IDynaBeanImplementationFactory factory) {
        this.factory = factory;
        validDescriptor = false;
        properties = Collections.synchronizedMap(new TreeMap<String, DynaBeanPropertyDescriptor>());
    }

    /**
     * Constructor with the type to implement to get the information.
     * @param factory The factory to work with
     * @param typeToImplement the {@link Class} type to implement.
     * @throws IllegalArgumentException If {@code typeToImplement} is null or isn't an interface
     */
    public DynaBeanDescriptor(IDynaBeanImplementationFactory factory, Class<T> typeToImplement) {
        this(factory);

        DynaBeanPropertyDescriptor propDesc;
        StringBuilder stb;
        String s;

        if (typeToImplement == null || typeToImplement.isInterface() == false) {
            if (typeToImplement == null) {
                logger.error("The type to implement is required");
                throw new IllegalArgumentException("The type to implement is required");
            }
            // Only interfaces
            logger.error("DynaBean can only implement interfaces. '" + typeToImplement.getName()
                    + "' is not an interface");
            throw new IllegalArgumentException("DynaBean can only implement interfaces. '"
                    + typeToImplement.getName() + "' is not an interface");
        }

        implementedType = typeToImplement;
        if (logger.isDebugEnabled()) {
            logger.debug("Working for implementing the type '".concat(typeToImplement.getName()).concat("'"));
        }
        // Prepare the properties list
        for (Method method : typeToImplement.getMethods()) {
            if (isPropertyMethod(method.getName()) && isCorrectProperty(method)) {
                if ((propDesc = getPropertyByMethodName(method.getName())) == null) {
                    // Put them!
                    propDesc = new DynaBeanPropertyDescriptor();
                    propDesc.propertyName = fromMethodToPropertyName(method.getName());
                    propDesc.propertyPath = implementedType.getName().concat(".").concat(propDesc.propertyName);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Working on property '".concat(implementedType.getName()).concat(".")
                                .concat(propDesc.getPropertyName()));
                    }
                    if (isGetter(method.getName())) {
                        propDesc.getterMethod = method;
                    } else {
                        propDesc.setterMethod = method;
                    }
                    resolvePropertyComponentType(propDesc);
                    resolvePropertyEditorForProperty(propDesc);
                    resolvePropertyCloneMethod(propDesc);
                    properties.put(propDesc.getPropertyName(), propDesc);
                } else {
                    if (isGetter(method.getName())) {
                        propDesc.getterMethod = method;
                    } else {
                        propDesc.setterMethod = method;
                    }
                }
            } else {
                if (!isPropertyMethod(method.getName())) {
                    if (logger.isInfoEnabled()) {
                        logger.info(String.format(PATTERN_IGNORING_PROPERTIES, implementedType.getName(),
                                method.getName(), " is not a property method"));
                    }
                }
                if (!isCorrectProperty(method)) {
                    if (logger.isWarnEnabled()) {
                        logger.warn(String.format(PATTERN_IGNORING_PROPERTIES, implementedType.getName(),
                                method.getName(), " is an INVALID PROPERTY METHOD"));
                    }
                }
            }
        }
        // Check that at least one property is found
        Assert.isTrue(!properties.isEmpty(), "The model '" + implementedType.getName()
                + "' doesn't defines a valid model, doesn't have any valid property");

        // Check value coherence
        for (DynaBeanPropertyDescriptor property : properties.values()) {
            // Check type return and set equals
            if (property.isRW()) {
                // All two methods should to have the same type
                Assert.isTrue(
                        property.getterMethod.getReturnType().equals(property.setterMethod.getParameterTypes()[0]),
                        String.format(
                                "The set and get values of the property '%s' at '%s' model ARE DIFFERENTS. This model is invalid",
                                property.propertyName, implementedType.getName()));
            }
        }
        // The descriptor have all the properties
        // Prepare the string pattern and test annotations
        stb = new StringBuilder();
        stb.append(implementedType.getSimpleName()).append(" [");
        s = "";
        for (String name : getPropertyNames()) {
            // Test annotations
            processAnnotations(properties.get(name));
            // Add the property to the 'toString' pattern
            stb.append(s).append(name).append("=%s");
            s = ", ";
        }
        // Change the last element (",") by "]"
        stb.append("]");
        patternForToString = stb.toString();
        if (logger.isDebugEnabled()) {
            logger.debug("Pattern string for '".concat(implementedType.getClass().getName()).concat("': ")
                    .concat(patternForToString));
        }
        validDescriptor = true;
    }

    /**
     * Resolve the item type for an array or collection property. 
     * @param propDesc The property descriptor
     */
    private void resolvePropertyComponentType(DynaBeanPropertyDescriptor propDesc) {
        Type[] t;
        Class<?> propType;

        if (propDesc.isCollection()) {
            if (propDesc.getterMethod != null) {
                t = ((ParameterizedType) propDesc.getterMethod.getGenericReturnType()).getActualTypeArguments();
            } else {
                t = ((ParameterizedType) propDesc.setterMethod.getGenericParameterTypes()[0])
                        .getActualTypeArguments();
            }
            propType = (Class<?>) t[0];
        } else {
            if (propDesc.isArray()) {
                if (propDesc.getterMethod != null) {
                    propType = propDesc.getterMethod.getReturnType().getComponentType();
                } else {
                    propType = propDesc.setterMethod.getParameterTypes()[0].getComponentType();
                }
            } else {
                propType = propDesc.getPropertyType();
            }
        }
        // Test if propType is a 'dynaBean'
        propDesc.itemType = propType;
        propDesc.itemDynaBean = propType.isAnnotationPresent(DynaBean.class);
    }

    /**
     * Search for a {@link PropertyEditor} for the given property or item component if array or collection.
     * @param propDesc The descriptor, required
     */
    private void resolvePropertyEditorForProperty(DynaBeanPropertyDescriptor propDesc) {
        PropertyEditor pEditor;

        // Ignore String editor
        if (!String.class.equals(propDesc.getItemType())) {
            if ((pEditor = getFactory().getPropertyEditorRegistry().findCustomEditor(propDesc.getItemType(),
                    propDesc.getPropertyPath())) == null) {
                // Last, find in
                if ((pEditor = PropertyEditorManager.findEditor(propDesc.getItemType())) == null) {
                    getFactory().getPropertyEditorRegistry().registerCustomEditor(propDesc.getItemType(), pEditor);
                }
            }
            propDesc.propertyItemEditor = pEditor;
        }
    }

    /**
     * Search -if applicable- for a clone method for the given property or item component if array or collection.
     * @param propDesc The descriptor, required
     */
    private void resolvePropertyCloneMethod(DynaBeanPropertyDescriptor propDesc) {
        Class<?> pType;

        if (propDesc.isArray() || propDesc.isCollection()) {
            pType = propDesc.getItemType();
        } else {
            pType = propDesc.getPropertyType();
        }
        if (Cloneable.class.isAssignableFrom(pType)) {
            try {
                propDesc.propertyItemCloneMethod = pType.getMethod("clone", (Class<?>[]) null);
            } catch (NoSuchMethodException | SecurityException e) {
                logger.error("On assigning value for '".concat(propDesc.getPropertyName())
                        .concat("' from dynaBean '").concat(getImplementedType().getName()), e);
                throw new RuntimeException("On assigning value for '".concat(propDesc.getPropertyName())
                        .concat("' from dynaBean '").concat(getImplementedType().getName()), e);
            }
        }
    }

    /**
     * Process the annotations for the property.
     * 
     * @param propDesc The property
     */
    private void processAnnotations(DynaBeanPropertyDescriptor propDesc) {
        PropertyDefaultValue pdv;
        DynaBean db;
        String msg;

        // Check for dynabean annotation at property or property type
        if (((db = getAnnotationForProperty(propDesc, DynaBean.class)) != null)
                || ((db = getAnnotationForPropertyType(propDesc, DynaBean.class)) != null)) {
            Assert.isTrue(propDesc.getPropertyType().isInterface(),
                    "The property '" + propDesc.getPropertyName() + "' of type '" + implementedType.getName()
                            + " is using DynaBean annotation incorrectly. " + "Only interfaces can be DynaBean");
            // Check for DynaBean conditions
            propDesc.dynaBean = true;
            propDesc.defaultValue = new String[] { Boolean.toString(db.defaultInstantiate()) };
        } else {
            // Not a dynabean, check for PropertyDefaultValue
            if ((pdv = getAnnotationForProperty(propDesc, PropertyDefaultValue.class)) != null) {
                // Implementation was indicated?
                if (!void.class.equals(pdv.implementation())) {
                    // Implementation and no default
                    // The implementation is an interface (dynabean)?
                    if (pdv.implementation().isInterface()
                            || Modifier.isAbstract(pdv.implementation().getModifiers())) {
                        msg = "The implementation type (" + pdv.implementation().getName() + ") for the property '"
                                + propDesc.getPropertyName() + "' isn't a concrete and instantiable class!";

                        logger.error(msg);
                        throw new IllegalArgumentException(msg);
                    } else {
                        // Concrete class implementation!
                        propDesc.defaultImplementation = pdv.implementation();
                    }
                } else {
                    // No implementation, value was indicated?
                    if (!StringUtilities.hasText(pdv.value())) {
                        // No implementation && no value, no default implementation
                        msg = "The property '".concat(propDesc.getPropertyName())
                                .concat("' was annotated with DefaultPropertyValue but "
                                        + "no value and no default implementation was indicated. Cannot be marked as 'default'");
                        logger.error(msg);
                        throw new IllegalArgumentException(msg);
                    } else {
                        propDesc.defaultValue = pdv.value();
                        if (StringUtilities.hasText(pdv.pattern())) {
                            // Test if a date or calendar...
                            if (Date.class.isAssignableFrom(propDesc.getPropertyType())
                                    || Calendar.class.isAssignableFrom(propDesc.getPropertyType())) {
                                // dates
                                propDesc.propertyItemEditor = new DynaBeanDateEditor(pdv.pattern()[0],
                                        Calendar.class.isAssignableFrom(propDesc.getPropertyType()));
                                getFactory().getPropertyEditorRegistry().registerCustomEditor(
                                        propDesc.getPropertyType(), propDesc.getPropertyPath(),
                                        propDesc.propertyItemEditor);
                            } else {
                                msg = String.format(
                                        "On processing '%s' property of '%s' type. The pattern property of annotation is only applicable to Date or Calendar types!",
                                        propDesc.getPropertyName(), implementedType.getName());

                                logger.error(msg);
                                throw new IllegalArgumentException(msg);
                            }
                        }
                    }
                }
            } else {
                propDesc.defaultValue = null;
            }
        }
    }

    /**
     * Check for {@code annotationClass} at get/set method for the indicated {@code propDesc}. The precedence is:
     * <ol>
     * <li>{@link DynaBeanPropertyDescriptor#getGetterMethod()}</li>
     * <li>{@link DynaBeanPropertyDescriptor#getSetterMethod()}</li>
     * </ol>
     * 
     * @param propDesc The property descriptor
     * @param annotationClass The annotation class
     * @return The annotation; null if annotation was not found.
     */
    private <A extends Annotation> A getAnnotationForProperty(DynaBeanPropertyDescriptor propDesc,
            Class<A> annotationClass) {
        A annotation;

        annotation = null;
        if (propDesc.getGetterMethod() != null) {
            annotation = propDesc.getGetterMethod().getAnnotation(annotationClass);
        }
        if (annotation == null && propDesc.getSetterMethod() != null) {
            annotation = propDesc.getSetterMethod().getAnnotation(annotationClass);
        }
        return annotation;
    }

    /**
     * Check for {@code annotationClass} at property type.
     * @param propDesc The property descriptor
     * @param annotationClass The annotation class
     * @return The annotation; null if annotation was not found.
     */
    private <A extends Annotation> A getAnnotationForPropertyType(DynaBeanPropertyDescriptor propDesc,
            Class<A> annotationClass) {
        return propDesc.getPropertyType().getAnnotation(annotationClass);
    }

    /**
     * The factory associated with this descriptor.
     * 
     * @return The factory
     */
    public IDynaBeanImplementationFactory getFactory() {
        return factory;
    }

    /**
     * The implemented type.
     * 
     * @return The class of implemented type
     */
    public Class<T> getImplementedType() {
        return implementedType;
    }

    /**
     * Get the property descriptor for the indicated method name.
     * 
     * @param methodName The method name; required, should to be a 'get' or 'is' or 'set'.
     * @return The property descriptor, if found, or null if not found or not a 'property method'.
     * @see DynaBeanImplementationUtils#isPropertyMethod(String)
     */
    public DynaBeanPropertyDescriptor getPropertyByMethodName(String methodName) {
        if (StringUtils.hasText(methodName) && DynaBeanImplementationUtils.isPropertyMethod(methodName)) {
            return properties.get(fromMethodToPropertyName(methodName));
        }
        return null;
    }

    /**
     * The property descriptor collection.
     * 
     * @return The collection, can be empty but never null
     */
    public Iterable<DynaBeanPropertyDescriptor> getProperties() {
        return properties.values();
    }

    /**
     * The list of property names.
     * 
     * @return A unmodifiable list with the property names.
     */
    public Set<String> getPropertyNames() {
        return Collections.unmodifiableSet(properties.keySet());
    }

    /**
     * Gets a pattern for the 'toString' method compose.
     * 
     * @return A patter to use with {@link String#format(String, Object...)} with only the values
     */
    public String getPatternForToString() {
        return patternForToString;
    }

    /**
     * Indicates if this descriptor is valid as has completelly discover the model and the model is valid.
     * @return true if the descriptor is valid and false if not.
     */
    public boolean isValidDescriptor() {
        return validDescriptor;
    }

    /**
     * On deserialize, reconstruct the implemented type information.
     * @param in The io input stream to read from
     * @throws IOException If errors on reading from io stream
     * @throws ClassNotFoundException If any deserialized class is not found on loader
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        resolveAfterDeserialization();
    }

    /**
     * Resolve the non-serializable information after a deserialization process. Basically gets the {@link Method} for
     * getter and setters.
     */
    private void resolveAfterDeserialization() {
        DynaBeanPropertyDescriptor pb;

        factory = DynaBeanImplementationUtils.instanceFactory();

        for (Method method : implementedType.getMethods()) {
            if (isPropertyMethod(method.getName())) {
                pb = properties.get(fromMethodToPropertyName(method.getName()));
                if (isGetter(method.getName())) {
                    pb.getterMethod = method;
                } else {
                    pb.setterMethod = method;
                }
                if (pb.getPropertyItemEditor() == null) {
                    resolvePropertyEditorForProperty(pb);
                }
                if (pb.getPropertyItemCloneMethod() == null) {
                    resolvePropertyCloneMethod(pb);
                }
            }
        }
    }
}