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

software.amazon.awssdk.testutils.smoketest.ReflectionUtils Maven / Gradle / Ivy

/*
 * Copyright 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 software.amazon.awssdk.testutils.smoketest;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import org.slf4j.LoggerFactory;

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

    private static final Random RANDOM = new Random();

    private ReflectionUtils() {
    }

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

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

    public static  T newInstance(Class clazz, Object... params) {
        Constructor 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) {
            Throwable cause = ex.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            throw new IllegalStateException(
                    "Unexpected checked exception thrown from "
                    + constructor.toGenericString(),
                    ex);
        }
    }

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

        for (Constructor constructor : clazz.getConstructors()) {
            Class[] paramTypes = constructor.getParameterTypes();
            if (matches(paramTypes, params)) {
                @SuppressWarnings("unchecked")
                Constructor rval = (Constructor) 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 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 getAllByPath(Object target, List path) {
        List 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 path,
            int depth,
            List 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) {
            Throwable cause = exception.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            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 path) {

        Object obj = target;
        Iterator 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 list = (List) 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 map = (Map) target;
            return digInMap(map, field);

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

    private static Object digInList(List 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 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 (Exception 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) {
            Throwable cause = exception.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            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 collection = (Collection) 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) {
                Throwable cause = exception.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                }
                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(Locale.ENGLISH) +
                       propertyName.substring(1);

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

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

        LoggerFactory.getLogger(ReflectionUtils.class).warn("No accessor for property '{}' found in class {}",
                                                            propertyName,
                                                            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 path) {

        Object obj = target;
        Iterator 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;
    }

    public static void setField(Object instance, Field field, Object arg) {
        try {
            field.set(instance, arg);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static  Object getField(T instance, Field field) {
        try {
            return field.get(instance);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static  T newInstanceWithAllFieldsSet(Class clz) {
        List> emptyRandomSuppliers = new ArrayList<>();
        return newInstanceWithAllFieldsSet(clz, emptyRandomSuppliers);
    }

    public static  T newInstanceWithAllFieldsSet(Class clz, RandomSupplier... suppliers) {
        return newInstanceWithAllFieldsSet(clz, Arrays.asList(suppliers));
    }

    public static  T newInstanceWithAllFieldsSet(Class clz, List> suppliers) {
        T instance = newInstance(clz);
        for (Field field : clz.getDeclaredFields()) {
            if (Modifier.isStatic(field.getModifiers())) {
                continue;
            }

            Class type = field.getType();
            AccessController.doPrivileged((PrivilegedAction) () -> {
                field.setAccessible(true);
                return null;
            });

            RandomSupplier supplier = findSupplier(suppliers, type);
            if (supplier != null) {
                setField(instance, field, supplier.getNext());
            } else if (type.isAssignableFrom(int.class) || type.isAssignableFrom(Integer.class)) {
                setField(instance, field, RANDOM.nextInt(Integer.MAX_VALUE));
            } else if (type.isAssignableFrom(long.class) || type.isAssignableFrom(Long.class)) {
                setField(instance, field, (long) RANDOM.nextInt(Integer.MAX_VALUE));
            } else if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class)) {
                Object bool = getField(instance, field);
                if (bool == null) {
                    setField(instance, field, RANDOM.nextBoolean());
                } else {
                    setField(instance, field, !Boolean.parseBoolean(bool.toString()));
                }
            } else if (type.isAssignableFrom(String.class)) {
                setField(instance, field, UUID.randomUUID().toString());
            } else {
                throw new RuntimeException(String.format("Could not set value for type %s no supplier available.", type));
            }
        }
        return instance;
    }

    private static RandomSupplier findSupplier(List> suppliers, Class type) {
        for (RandomSupplier supplier : suppliers) {
            if (type.isAssignableFrom(supplier.targetClass())) {
                return supplier;
            }
        }
        return null;
    }

    interface RandomSupplier {
        T getNext();

        Class targetClass();
    }
}