/* * 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(); } }