/*
* Copyright 2011-2017 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.
* You may obtain a copy of the License at:
*
* http://aws.amazon.com/apache2.0
*
* 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.amazonaws.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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import org.apache.commons.logging.LogFactory;
/**
* Utility methods for doing reflection.
*/
public final class ReflectionUtils {
private static final Random RANDOM = new Random();
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 ex) {
throw new IllegalStateException(
"Could not invoke " + constructor.toGenericString(),
ex);
} catch (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<Object>();
// 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 (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) {
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;
}
public static void setField(Object instance, Field field, Object arg) {
try {
field.set(instance, arg);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static <T> Object getField(T instance, Field field) {
try {
return field.get(instance);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static <T> T newInstanceWithAllFieldsSet(Class<T> clz) {
List<RandomSupplier<?>> emptyRandomSuppliers = new ArrayList<RandomSupplier<?>>();
return newInstanceWithAllFieldsSet(clz, emptyRandomSuppliers);
}
public static <T> T newInstanceWithAllFieldsSet(Class<T> clz, RandomSupplier<?>...suppliers) {
return newInstanceWithAllFieldsSet(clz, Arrays.asList(suppliers));
}
public static <T> T newInstanceWithAllFieldsSet(Class<T> clz, List<RandomSupplier<?>> suppliers) {
T instance = newInstance(clz);
for(Field field: clz.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
final Class<?> type = field.getType();
field.setAccessible(true);
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, Math.abs(RANDOM.nextInt()));
} else if (type.isAssignableFrom(long.class) || type.isAssignableFrom(Long.class)) {
setField(instance, field, Math.abs(RANDOM.nextLong()));
} 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.valueOf(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<RandomSupplier<?>> suppliers, Class<?> type) {
for (RandomSupplier<?> supplier : suppliers) {
if (type.isAssignableFrom(supplier.targetClass())) {
return supplier;
}
}
return null;
}
interface RandomSupplier<T> {
T getNext();
Class<T> targetClass();
}
private ReflectionUtils() {
}
}