com.mani.cucumber.ReflectionUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.mani.cucumber.ReflectionUtils.java

Source

/*
 * Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.mani.cucumber;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.LogFactory;

/**
 * Utility methods for doing reflection.
 */
public final class ReflectionUtils {

    public static <T> Class<T> loadClass(Class<?> base, String name) {
        return loadClass(base.getClassLoader(), name);
    }

    public static <T> Class<T> loadClass(ClassLoader classloader, String name) {
        try {
            @SuppressWarnings("unchecked")
            Class<T> loaded = (Class<T>) classloader.loadClass(name);
            return loaded;
        } catch (ClassNotFoundException exception) {
            throw new IllegalStateException("Cannot find class " + name, exception);
        }
    }

    public static <T> T newInstance(Class<T> clazz, Object... params) {
        Constructor<T> constructor = findConstructor(clazz, params);

        try {

            return constructor.newInstance(params);

        } catch (InstantiationException | IllegalAccessException ex) {
            throw new IllegalStateException("Could not invoke " + constructor.toGenericString(), ex);

        } catch (InvocationTargetException ex) {
            if (ex.getCause() instanceof RuntimeException) {
                throw (RuntimeException) ex.getCause();
            }
            throw new IllegalStateException(
                    "Unexpected checked exception thrown from " + constructor.toGenericString(), ex);
        }
    }

    private static <T> Constructor<T> findConstructor(Class<T> clazz, Object[] params) {

        for (Constructor<?> constructor : clazz.getConstructors()) {
            Class<?>[] paramTypes = constructor.getParameterTypes();
            if (matches(paramTypes, params)) {
                @SuppressWarnings("unchecked")
                Constructor<T> rval = (Constructor<T>) constructor;
                return rval;
            }
        }

        throw new IllegalStateException("No appropriate constructor found for " + clazz.getCanonicalName());
    }

    private static boolean matches(Class<?>[] paramTypes, Object[] params) {
        if (paramTypes.length != params.length) {
            return false;
        }

        for (int i = 0; i < params.length; ++i) {
            if (!paramTypes[i].isAssignableFrom(params[i].getClass())) {
                return false;
            }
        }

        return true;
    }

    /**
     * Evaluates the given path expression on the given object and returns the
     * object found.
     *
     * @param target the object to reflect on
     * @param path the path to evaluate
     * @return the result of evaluating the path against the given object
     */
    public static Object getByPath(Object target, List<String> path) {
        Object obj = target;

        for (String field : path) {
            if (obj == null) {
                return null;
            }
            obj = evaluate(obj, trimType(field));
        }

        return obj;
    }

    /**
     * Evaluates the given path expression and returns the list of all matching
     * objects. If the path expression does not contain any wildcards, this
     * will return a list of at most one item. If the path contains one or more
     * wildcards, the returned list will include the full set of values
     * obtained by evaluating the expression with all legal value for the
     * given wildcard.
     *
     * @param target the object to evaluate the expression against
     * @param path the path expression to evaluate
     * @return the list of matching values
     */
    public static List<Object> getAllByPath(Object target, List<String> path) {
        List<Object> results = new LinkedList<>();

        // TODO: Can we unroll this and do it iteratively?
        getAllByPath(target, path, 0, results);

        return results;
    }

    private static void getAllByPath(Object target, List<String> path, int depth, List<Object> results) {

        if (target == null) {
            return;
        }

        if (depth == path.size()) {
            results.add(target);
            return;
        }

        String field = trimType(path.get(depth));

        if (field.equals("*")) {

            if (!(target instanceof Iterable)) {
                throw new IllegalStateException("Cannot evaluate '*' on object " + target);
            }

            Iterable<?> collection = (Iterable<?>) target;
            for (Object obj : collection) {
                getAllByPath(obj, path, depth + 1, results);
            }

        } else {
            Object obj = evaluate(target, field);
            getAllByPath(obj, path, depth + 1, results);
        }
    }

    private static String trimType(String field) {
        int index = field.indexOf(':');
        if (index == -1) {
            return field;
        }
        return field.substring(0, index);
    }

    /**
     * Uses reflection to evaluate a single element of a path expression on
     * the given object. If the object is a list and the expression is a
     * number, this returns the expression'th element of the list. Otherwise,
     * this looks for a method named "get${expression}" and returns the result
     * of calling it.
     *
     * @param target the object to evaluate the expression against
     * @param expression the expression to evaluate
     * @return the result of evaluating the expression
     */
    private static Object evaluate(Object target, String expression) {
        try {
            if (target instanceof List) {

                List<?> list = (List<?>) target;

                int index = Integer.parseInt(expression);
                if (index < 0) {
                    index += list.size();
                }

                return list.get(index);

            } else {

                Method getter = findAccessor(target, expression);
                if (getter == null) {
                    return null;
                }

                return getter.invoke(target);

            }
        } catch (IllegalAccessException exception) {
            throw new IllegalStateException("BOOM", exception);

        } catch (InvocationTargetException exception) {
            if (exception.getCause() instanceof RuntimeException) {
                throw (RuntimeException) exception.getCause();
            }
            throw new RuntimeException("BOOM", exception);
        }
    }

    /**
     * Sets the value of the attribute at the given path in the target object,
     * creating any intermediate values (using the default constructor for the
     * type) if need be.
     *
     * @param target the object to modify
     * @param value the value to add
     * @param path the path into the target object at which to add the value
     */
    public static void setByPath(Object target, Object value, List<String> path) {

        Object obj = target;
        Iterator<String> iter = path.iterator();

        while (iter.hasNext()) {

            String field = iter.next();
            if (iter.hasNext()) {
                obj = digIn(obj, field);
            } else {
                setValue(obj, trimType(field), value);
            }

        }
    }

    /**
     * Uses reflection to dig into a chain of objects in preparation for
     * setting a value somewhere within the tree. Gets the value of the given
     * property of the target object and, if it is null, creates a new instance
     * of the appropriate type and sets it on the target object. Returns the
     * gotten or created value.
     *
     * @param target the target object to reflect on
     * @param field the field to dig into
     * @return the gotten or created value
     */
    private static Object digIn(Object target, String field) {
        if (target instanceof List) {

            // The 'field' will tell us what type of objects belong in the list.
            @SuppressWarnings("unchecked")
            List<Object> list = (List<Object>) target;
            return digInList(list, field);

        } else if (target instanceof Map) {

            // The 'field' will tell us what type of objects belong in the map.
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) target;
            return digInMap(map, field);

        } else {
            return digInObject(target, field);
        }
    }

    private static Object digInList(List<Object> target, String field) {
        int index = field.indexOf(':');
        if (index == -1) {
            throw new IllegalStateException(
                    "Invalid path expression: cannot " + "evaluate '" + field + "' on a List");
        }

        String offset = field.substring(0, index);
        String type = field.substring(index + 1);

        if (offset.equals("*")) {
            throw new UnsupportedOperationException("What does this even mean?");
        }

        int intOffset = Integer.parseInt(offset);
        if (intOffset < 0) {
            // Offset from the end of the list
            intOffset += target.size();
            if (intOffset < 0) {
                throw new IndexOutOfBoundsException(Integer.toString(intOffset));
            }
        }

        if (intOffset < target.size()) {
            return target.get(intOffset);
        }

        // Extend with default instances if need be.
        while (intOffset > target.size()) {
            target.add(createDefaultInstance(type));
        }

        Object result = createDefaultInstance(type);
        target.add(result);

        return result;
    }

    private static Object digInMap(Map<String, Object> target, String field) {
        int index = field.indexOf(':');
        if (index == -1) {
            throw new IllegalStateException(
                    "Invalid path expression: cannot " + "evaluate '" + field + "' on a List");
        }

        String member = field.substring(0, index);
        String type = field.substring(index + 1);

        Object result = target.get(member);
        if (result != null) {
            return result;
        }

        result = createDefaultInstance(type);
        target.put(member, result);

        return result;
    }

    public static Object createDefaultInstance(String type) {
        try {

            return ReflectionUtils.class.getClassLoader().loadClass(type).newInstance();

        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException("BOOM", e);
        }
    }

    private static Object digInObject(Object target, String field) {
        Method getter = findAccessor(target, field);
        if (getter == null) {
            throw new IllegalStateException(
                    "No accessor found for '" + field + "' found in class " + target.getClass().getName());
        }

        try {

            Object obj = getter.invoke(target);
            if (obj == null) {
                obj = getter.getReturnType().newInstance();
                Method setter = findMethod(target, "set" + field, obj.getClass());
                setter.invoke(target, obj);
            }

            return obj;

        } catch (InstantiationException exception) {
            throw new IllegalStateException("Unable to create a new instance", exception);

        } catch (IllegalAccessException exception) {
            throw new IllegalStateException("Unable to access getter, setter, or constructor", exception);

        } catch (InvocationTargetException exception) {
            if (exception.getCause() instanceof RuntimeException) {
                throw (RuntimeException) exception.getCause();
            }
            throw new IllegalStateException("Checked exception thrown from getter or setter method", exception);
        }
    }

    /**
     * Uses reflection to set the value of the given property on the target
     * object.
     *
     * @param target the object to reflect on
     * @param field the name of the property to set
     * @param value the new value of the property
     */
    private static void setValue(Object target, String field, Object value) {
        // TODO: Should we do this for all numbers, not just '0'?
        if ("0".equals(field)) {

            if (!(target instanceof Collection)) {
                throw new IllegalArgumentException("Cannot evaluate '0' on object " + target);
            }

            @SuppressWarnings("unchecked")
            Collection<Object> collection = (Collection<Object>) target;
            collection.add(value);

        } else {

            Method setter = findMethod(target, "set" + field, value.getClass());

            try {
                setter.invoke(target, value);

            } catch (IllegalAccessException exception) {
                throw new IllegalStateException("Unable to access setter method", exception);

            } catch (InvocationTargetException exception) {
                if (exception.getCause() instanceof RuntimeException) {
                    throw (RuntimeException) exception.getCause();
                }
                throw new IllegalStateException("Checked exception thrown from setter method", exception);
            }
        }
    }

    /**
     * Returns the accessor method for the specified member property.
     * For example, if the member property is "Foo", this method looks
     * for a "getFoo()" method and an "isFoo()" method.
     *
     * If no accessor is found, this method throws an IllegalStateException.
     *
     * @param target the object to reflect on
     * @param propertyName the name of the property to search for
     * @return the accessor method
     * @throws IllegalStateException if no matching method is found
     */
    public static Method findAccessor(Object target, String propertyName) {

        propertyName = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);

        try {
            return target.getClass().getMethod("get" + propertyName);
        } catch (NoSuchMethodException nsme) {
        }

        try {
            return target.getClass().getMethod("is" + propertyName);
        } catch (NoSuchMethodException nsme) {
        }

        LogFactory.getLog(ReflectionUtils.class).warn("No accessor for property '" + propertyName + "' "
                + "found in class " + target.getClass().getName());

        return null;
    }

    /**
     * Finds a method of the given name that will accept a parameter of the
     * given type. If more than one method matches, returns the first such
     * method found.
     *
     * @param target the object to reflect on
     * @param name the name of the method to search for
     * @param parameterType the type of the parameter to be passed
     * @return the matching method
     * @throws IllegalStateException if no matching method is found
     */
    public static Method findMethod(Object target, String name, Class<?> parameterType) {

        for (Method method : target.getClass().getMethods()) {
            if (!method.getName().equals(name)) {
                continue;
            }

            Class<?>[] parameters = method.getParameterTypes();
            if (parameters.length != 1) {
                continue;
            }

            if (parameters[0].isAssignableFrom(parameterType)) {
                return method;
            }
        }

        throw new IllegalStateException(
                "No method '" + name + "(" + parameterType + ") on type " + target.getClass());
    }

    public static Class<?> getParameterTypes(Object target, List<String> path) {

        Object obj = target;
        Iterator<String> iter = path.iterator();

        while (iter.hasNext()) {

            String field = iter.next();
            if (iter.hasNext()) {
                obj = digIn(obj, field);
            } else {
                return findAccessor(obj, field).getReturnType();
            }
        }
        return null;
    }

    private ReflectionUtils() {
    }
}