BeanUtil.java

/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.lang;

import com.twelvemonkeys.util.convert.ConversionException;
import com.twelvemonkeys.util.convert.Converter;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;

/**
 * A utility class with some useful bean-related functions.
 * <p>
 * <em>NOTE: This class is not considered part of the public API and may be changed without notice</em>
 * </p>
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haku $
 *
 * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/BeanUtil.java#2 $
 */
public final class BeanUtil {

    // Disallow creating objects of this type
    private BeanUtil() {
    }

    /**
     * Gets a property value from the given object, using reflection.
     * Now supports getting values from properties of properties
     * (recursive).
     *
     * @param pObject The object to get the property from
     * @param pProperty The name of the property
     *
     * @return A string containing the value of the given property, or {@code null}
     *         if it can not be found.
     */
    public static Object getPropertyValue(Object pObject, String pProperty) {
        // TODO: Remove System.err's... Create new Exception? Hmm..
        // TODO: Support get(Object) method of Collections!
        //       Handle lists and arrays with [] (index) operator

        if (pObject == null || pProperty == null || pProperty.length() < 1) {
            return null;
        }

        Class<?> objClass = pObject.getClass();

        Object result = pObject;

        // Method for method...
        String subProp;
        int begIdx = 0;
        int endIdx = begIdx;

        while (begIdx < pProperty.length() && begIdx >= 0) {

            endIdx = pProperty.indexOf('.', endIdx + 1);
            if (endIdx > 0) {
                subProp = pProperty.substring(begIdx, endIdx);
                begIdx = endIdx + 1;
            }
            else {
                // The final property!
                // If there's just the first-level property, subProp will be
                // equal to property
                subProp = pProperty.substring(begIdx);
                begIdx = -1;
            }

            // Check for "[" and "]"
            Object[] param = null;
            Class[] paramClass = new Class[0];

            int begBracket;
            if ((begBracket = subProp.indexOf('[')) > 0) {
                // An error if there is no matching bracket
                if (!subProp.endsWith("]")) {
                    return null;
                }

                String between = subProp.substring(begBracket + 1,
                                                   subProp.length() - 1);
                subProp = subProp.substring(0, begBracket);

                // If brackets exist, check type of argument between brackets
                param = new Object[1];
                paramClass = new Class[1];

                //try {
                // TODO: isNumber returns true, even if too big for integer...
                if (StringUtil.isNumber(between)) {
                    // We have a number
                    // Integer -> array subscript -> getXXX(int i)
                    try {
                        // Insert param and it's Class
                        param[0] = Integer.valueOf(between);
                        paramClass[0] = Integer.TYPE; // int.class
                    }
                    catch (NumberFormatException e) {
                        // ??
                        // Probably too small or too large value..
                    }
                }
                else {
                    //catch (NumberFormatException e) {
                    // Not a number... Try String
                    // String -> Hashtable key -> getXXX(String str)
                    // Insert param and it's Class
                    param[0] = between.toLowerCase();
                    paramClass[0] = String.class;
                }
            }

            Method method;
            String methodName = "get" + StringUtil.capitalize(subProp);
            try {
                // Try to get the "get" method for the given property
                method = objClass.getMethod(methodName, paramClass);
            }
            catch (NoSuchMethodException e) {
                System.err.print("No method named \"" + methodName + "()\"");
                // The array might be of size 0...
                if (paramClass.length > 0 && paramClass[0] != null) {
                    System.err.print(" with the parameter " + paramClass[0].getName());
                }

                System.err.println(" in class " + objClass.getName() + "!");
                return null;
            }

            // If method for some reason should be null, give up
            if (method == null) {
                return null;
            }

            try {
                // We have a method, try to invoke it
                // The resutling object will be either the property we are
                // Looking for, or the parent

                //		System.err.println("Trying " + objClass.getName() + "." + method.getName() + "(" + ((param != null && param.length > 0) ? param[0] : "") + ")");
                result = method.invoke(result, param);
            }
            catch (InvocationTargetException e) {
                System.err.println("property=" + pProperty + " & result=" + result + " & param=" + Arrays.toString(param));
                e.getTargetException().printStackTrace();
                e.printStackTrace();
                return null;
            }
            catch (IllegalAccessException e) {
                e.printStackTrace();
                return null;
            }
            catch (NullPointerException e) {
                System.err.println(objClass.getName() + "." + method.getName() + "(" + ((paramClass.length > 0 && paramClass[0] != null) ? paramClass[0].getName() : "") + ")");
                e.printStackTrace();
                return null;
            }

            if (result != null) {
                // Get the class of the reulting object
                objClass = result.getClass();
            }
            else {
                return null;
            }
        } // while

        return result;
    }

    /**
     * Sets the property value to an object using reflection.
     * Supports setting values of properties that are properties of
     * properties (recursive).
     *
     * @param pObject      The object to get a property from
     * @param pProperty The name of the property
     * @param pValue    The property value
     *
     * @throws NoSuchMethodException if there's no write method for the
     *         given property
     * @throws InvocationTargetException if invoking the write method failed
     * @throws IllegalAccessException if the caller class has no access to the
     *         write method
     */
    public static void setPropertyValue(Object pObject, String pProperty, Object pValue)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        //
        // TODO: Support set(Object, Object)/put(Object, Object) methods
        //       of Collections!
        //       Handle lists and arrays with [] (index) operator

        Class paramType = pValue != null ? pValue.getClass() : Object.class;

        // Preserve references
        Object obj = pObject;
        String property = pProperty;

        // Recurse and find real parent if property contains a '.'
        int dotIdx = property.indexOf('.');
        if (dotIdx >= 0) {
            // Get real parent
            obj = getPropertyValue(obj, property.substring(0, dotIdx));
            // Get the property of the parent
            property = property.substring(dotIdx + 1);
        }

        // Find method
        Object[] params = {pValue};
        Method method = getMethodMayModifyParams(obj, "set" + StringUtil.capitalize(property),
                                                 new Class[] {paramType}, params);

        // Invoke it
        method.invoke(obj, params);
    }

    private static Method getMethodMayModifyParams(Object pObject, String pName, Class[] pParams, Object[] pValues)
            throws NoSuchMethodException {
        // NOTE: This method assumes pParams.length == 1 && pValues.length == 1

        Method method = null;
        Class paramType = pParams[0];

        try {
            method = pObject.getClass().getMethod(pName, pParams);
        }
        catch (NoSuchMethodException e) {
            // No direct match

            // 1: If primitive wrapper, try unwrap conversion first
            /*if (paramType.isPrimitive()) { // NOTE: Can't be primitive type
                params[0] = ReflectUtil.wrapType(paramType);
            }
            else*/ if (ReflectUtil.isPrimitiveWrapper(paramType)) {
                pParams[0] = ReflectUtil.unwrapType(paramType);
            }

            try {
                // If this does not throw an exception, it works
                method = pObject.getClass().getMethod(pName, pParams);
            }
            catch (Throwable t) {
                // Ignore
            }

            // 2: Try any super-types of paramType, to see if we have a match
            if (method == null) {
                while ((paramType = paramType.getSuperclass()) != null) {
                    pParams[0] = paramType;
                    try {
                        // If this does not throw an exception, it works
                        method = pObject.getClass().getMethod(pName, pParams);
                    }
                    catch (Throwable t) {
                        // Ignore/Continue
                        continue;
                    }

                    break;
                }
            }

            // 3: Try to find a different method with the same name, that has
            // a parameter type we can convert to...
            // NOTE: There's no ordering here..
            // TODO: Should we try to do that? What would the ordering be?
            if (method == null) {
                Method[] methods = pObject.getClass().getMethods();
                for (Method candidate : methods) {
                    if (Modifier.isPublic(candidate.getModifiers()) && candidate.getName().equals(pName)
                            && candidate.getReturnType() == Void.TYPE && candidate.getParameterTypes().length == 1) {
                        // NOTE: Assumes paramTypes.length == 1

                        Class type = candidate.getParameterTypes()[0];

                        try {
                            pValues[0] = convertValueToType(pValues[0], type);
                        }
                        catch (Throwable t) {
                            continue;
                        }

                        // We were able to convert the parameter, let's try
                        method = candidate;
                        break;
                    }
                }
            }

            // Give up...
            if (method == null) {
                throw e;
            }
        }
        return method;
    }

    private static Object convertValueToType(Object pValue, Class<?> pType) throws ConversionException {
        if (pType.isPrimitive()) {
            if (pType == Boolean.TYPE && pValue instanceof Boolean) {
                return pValue;
            }
            else if (pType == Byte.TYPE && pValue instanceof Byte) {
                return pValue;
            }
            else if (pType == Character.TYPE && pValue instanceof Character) {
                return pValue;
            }
            else if (pType == Double.TYPE && pValue instanceof Double) {
                return pValue;
            }
            else if (pType == Float.TYPE && pValue instanceof Float) {
                return pValue;
            }
            else if (pType == Integer.TYPE && pValue instanceof Integer) {
                return pValue;
            }
            else if (pType == Long.TYPE && pValue instanceof Long) {
                return pValue;
            }
            else if (pType == Short.TYPE && pValue instanceof Short) {
                return pValue;
            }
        }

        // TODO: Convert value to single-value array if needed
        // TODO: Convert CSV String to string array (or potentially any type of array)

        // TODO: Convert other types
        if (pValue instanceof String) {
            Converter converter = Converter.getInstance();
            return converter.toObject((String) pValue, pType);
        }
        else if (pType == String.class) {
            Converter converter = Converter.getInstance();
            return converter.toString(pValue);
        }
        else {
            throw new ConversionException("Cannot convert " + pValue.getClass().getName() + " to " + pType.getName());
        }
    }

    /**
     * Creates an object from the given class' single argument constructor.
     *
     * @param pClass The class to create instance from
     * @param pParam The parameters to the constructor
     *
     * @return The object created from the constructor.
     *         If the constructor could not be invoked for any reason, null is
     *         returned.
     *
     * @throws InvocationTargetException if the constructor failed
     */
    // TODO: Move to ReflectUtil
    public static <T> T createInstance(Class<T> pClass, Object pParam)
            throws InvocationTargetException {
        return createInstance(pClass, new Object[] {pParam});
    }

    /**
     * Creates an object from the given class' constructor that matches
     * the given paramaters.
     *
     * @param pClass  The class to create instance from
     * @param pParams The parameters to the constructor
     *
     * @return The object created from the constructor.
     *         If the constructor could not be invoked for any reason, null is
     *         returned.
     *
     * @throws InvocationTargetException if the constructor failed
     */
    // TODO: Move to ReflectUtil
    public static <T> T createInstance(Class<T> pClass, Object... pParams)
            throws InvocationTargetException {
        T value;

        try {
            // Create param and argument arrays
            Class[] paramTypes = null;
            if (pParams != null && pParams.length > 0) {
                paramTypes = new Class[pParams.length];
                for (int i = 0; i < pParams.length; i++) {
                    paramTypes[i] = pParams[i].getClass();
                }
            }

            // Get constructor
            Constructor<T> constructor = pClass.getConstructor(paramTypes);

            // Invoke and create instance
            value = constructor.newInstance(pParams);
        }
        /* All this to let InvocationTargetException pass on */
        catch (NoSuchMethodException nsme) {
            return null;
        }
        catch (IllegalAccessException iae) {
            return null;
        }
        catch (IllegalArgumentException iarge) {
            return null;
        }
        catch (InstantiationException ie) {
            return null;
        }
        catch (ExceptionInInitializerError err) {
            return null;
        }

        return value;
    }

    /**
     * Gets an object from any given static method, with the given parameter.
     *
     * @param pClass  The class to invoke method on
     * @param pMethod The name of the method to invoke
     * @param pParam  The parameter to the method
     *
     * @return The object returned by the static method.
     *         If the return type of the method is a primitive type, it is wrapped in
     *         the corresponding wrapper object (int is wrapped in an Integer).
     *         If the return type of the method is void, null is returned.
     *         If the method could not be invoked for any reason, null is returned.
     *
     * @throws InvocationTargetException if the invocation failed
     */
    // TODO: Move to ReflectUtil
    // TODO: Rename to invokeStatic?
    public static Object invokeStaticMethod(Class<?> pClass, String pMethod, Object pParam)
            throws InvocationTargetException {

        return invokeStaticMethod(pClass, pMethod, new Object[] {pParam});
    }

    /**
     * Gets an object from any given static method, with the given parameter.
     *
     * @param pClass  The class to invoke method on
     * @param pMethod The name of the method to invoke
     * @param pParams The parameters to the method
     *
     * @return The object returned by the static method.
     *         If the return type of the method is a primitive type, it is wrapped in
     *         the corresponding wrapper object (int is wrapped in an Integer).
     *         If the return type of the method is void, null is returned.
     *         If the method could not be invoked for any reason, null is returned.
     *
     * @throws InvocationTargetException if the invocation failed
     */
    // TODO: Move to ReflectUtil
    // TODO: Rename to invokeStatic?
    public static Object invokeStaticMethod(Class<?> pClass, String pMethod, Object... pParams)
            throws InvocationTargetException {

        Object value = null;

        try {
            // Create param and argument arrays
            Class[] paramTypes = new Class[pParams.length];
            for (int i = 0; i < pParams.length; i++) {
                paramTypes[i] = pParams[i].getClass();
            }

            // Get method
            // *** If more than one such method is found in the class, and one
            // of these methods has a RETURN TYPE that is more specific than
            // any of the others, that method is reflected; otherwise one of
            // the methods is chosen ARBITRARILY.
            // java/lang/Class.html#getMethod(java.lang.String, java.lang.Class[])
            Method method = pClass.getMethod(pMethod, paramTypes);

            // Invoke public static method
            if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) {
                value = method.invoke(null, pParams);
            }

        }
        /* All this to let InvocationTargetException pass on */
        catch (NoSuchMethodException nsme) {
            return null;
        }
        catch (IllegalAccessException iae) {
            return null;
        }
        catch (IllegalArgumentException iarge) {
            return null;
        }

        return value;
    }

    /**
     * Configures the bean according to the given mapping.
     * For each {@code Map.Entry} in {@code Map.values()},
     * a method named
     * {@code set + capitalize(entry.getKey())} is called on the bean,
     * with {@code entry.getValue()} as its argument.
     * <p>
     * Properties that has no matching set-method in the bean, are simply
     * discarded.
     * </p>
     *
     * @param pBean    The bean to configure
     * @param pMapping The mapping for the bean
     *
     * @throws NullPointerException if any of the parameters are null.
     * @throws InvocationTargetException if an error occurs when invoking the
     *         setter-method.
     */
    // TODO: Add a version that takes a ConfigurationErrorListener callback interface
    // TODO: ...or a boolean pFailOnError parameter
    // TODO: ...or return Exceptions as an array?!
    // TODO: ...or something whatsoever that makes clients able to determine something's not right 
    public static void configure(final Object pBean, final Map<String, ?> pMapping) throws InvocationTargetException {
        configure(pBean, pMapping, false);
    }

    /**
     * Configures the bean according to the given mapping.
     * For each {@code Map.Entry} in {@code Map.values()},
     * a method named
     * {@code set + capitalize(entry.getKey())} is called on the bean,
     * with {@code entry.getValue()} as its argument.
     * <p>
     * Optionally, lisp-style names are allowed, and automatically converted
     * to Java-style camel-case names.
     * </p>
     * <p>
     * Properties that has no matching set-method in the bean, are simply
     * discarded.
     * </p>
     *
     * @see StringUtil#lispToCamel(String)
     *
     * @param pBean    The bean to configure
     * @param pMapping The mapping for the bean
     * @param pLispToCamel Allow lisp-style names, and automatically convert
     *        them to Java-style camel-case.
     *
     * @throws NullPointerException if any of the parameters are null.
     * @throws InvocationTargetException if an error occurs when invoking the
     *         setter-method.
     */
    public static void configure(final Object pBean, final Map<String, ?> pMapping, final boolean pLispToCamel) throws InvocationTargetException {
        // Loop over properties in mapping
        for (final Map.Entry<String, ?> entry : pMapping.entrySet()) {
            try {
                // Configure each property in turn
                final String property = StringUtil.valueOf(entry.getKey());
                try {
                    setPropertyValue(pBean, property, entry.getValue());
                }
                catch (NoSuchMethodException ignore) {
                    // If invocation failed, convert lisp-style and try again
                    if (pLispToCamel && property.indexOf('-') > 0) {
                        setPropertyValue(pBean, StringUtil.lispToCamel(property, false), entry.getValue());
                    }
                }
            }
            catch (NoSuchMethodException nsme) {
                // This property was not configured
            }
            catch (IllegalAccessException iae) {
                // This property was not configured
            }
        }
    }
}