package util;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Provide facilities to access class members. Useful to access hidden class
* members, especially to perform tests.
*
* TODO There is currently one situation in which getMethod() (and therefore
* invoke() too) may not be able to retrieve the desired method: if two method
* differ only by the parameter type of some of their argument having generic
* types. It can be fixed: http://stackoverflow.com/questions/3609799
*
* TODO getMethod() does not play nice with null.
*/
public class MemberAccessor
{
/*****************************************************************************
* Set the final field with name $field on object $o to $value.
* Beware that the final field should not be initialized to an observable
* compile-time constant, or this might not work.
*/
public static void setFinal(Object o, String field, Object value)
{
try {
Field f = o.getClass().getField(field);
f.setAccessible(true);
f.set(o, value);
}
catch (NoSuchFieldException | IllegalAccessException e) {
throw new MemberAccessException(e);
}
}
/*****************************************************************************
* Invokes the method with the given name on the receiver, supplying it the
* passed parameters. Useful to call non-public methods.
*
* If there are multiple method applicable for the supplied parameters, just
* runs the first one it encounters. No ordering of methods is guaranteed,
* neither is the fact that the ordering stays the same.
*/
public static Object invoke(
Object receiver, String selector, Object... params)
{
return invoke(receiver, selector, null, params);
}
/*****************************************************************************
* Same as {@link #invoke(Object, Method, Object...), but takes an additional
* $arbiter parameter to distinguish between multiple applicable methods.
*
* The first applicable method whose parameter classes include those in
* "arbiter", in the same order (but not necessarily adjacent) is run.
*/
public static Object invoke(
Object receiver, String selector, Class<?>[] arbiter, Object... params)
{
Method method = getMethod(receiver, selector, arbiter, params);
if (method == null) {
throw new MemberAccessException(
new NoSuchMethodException("No method named \"" + selector
+ "\" matching the supplied parameters."));
}
return invoke(receiver, method, params);
}
/*****************************************************************************
* Invokes the given method on the given object and with the given parameters.
*
* The parameters are passed as though this was the regular the method. No
* trickery is needed, as would be the case with {@link Method#invoke()}. See
* {@link #repackageParams()}) to know exactly what you're avoiding.
*
* The method is rendered accessible before invoking it, and stays that way.
*/
public static Object invoke(
Object receiver, Method method, Object... params)
{
method.setAccessible(true);
try {
return method.invoke(receiver, repackageParams(method, params));
}
catch (IllegalAccessException | InvocationTargetException e) {
throw new MemberAccessException(e);
}
}
/****************************************************************************
* Return the Method that would be invoked if calling
* {@link #invoke(Object, String, Class[], Object...) with the same
* parameters. Allows for more efficient and terse code if the method is to be
* invoked multiple times.
*/
public static Method getMethod(
Object receiver, String selector, Class<?>[] arbiter, Object... params)
{
// Iterate on class hierarchy, from bottom to top.
for ( Class<?> klass = receiver.getClass()
; klass != null
; klass = klass.getSuperclass())
{
Method m = getMethod(klass, receiver, selector, arbiter, params);
if (m != null) {
return m;
}
}
return null;
}
/*****************************************************************************
* Same as {@link #getMethod(Object, String, Class[], Object...)}, but only
* search the methods declared in $klass, not in the rest of the hierarchy of
* the receiver's class.
*/
private static Method getMethod(Class<?> klass,
Object receiver, String selector, Class<?>[] arbiter, Object[] params)
{
final List<Method> named = new ArrayList<>();
// Find all methods in the class with the given name.
for (final Method m : klass.getDeclaredMethods())
{
if (m.getName().equals(selector)) {
named.add(m);
}
}
// Return the first method with the given name that matches the given
// parameters and the arbiter, if any.
for (final Method m : named)
{
final Class<?>[] actualClasses = getParamClasses(params);
final Class<?>[] formalClasses = m.getParameterTypes();
if (checkParams(m, actualClasses)
&& (arbiter == null || ArrayUtils.contains(formalClasses, arbiter)))
{
return m;
}
}
return null;
}
/*****************************************************************************
* Checks that the supplied method is able to accept parameters with the
* supplied classes.
*
* Protected for testing.
*/
protected static boolean checkParams(Method m, Class<?>[] actuals)
{
final Class<?>[] formals = m.getParameterTypes();
int upper = formals.length;
if (m.isVarArgs())
{
if (actuals.length < formals.length - 1) {
return false;
}
final Class<?> varClass = formals[--upper].getComponentType();
for (int i = upper ; i < actuals.length ; ++i) {
if (!isAssignableFrom(varClass, actuals[i])) {
return false;
}
}
}
else if (actuals.length != formals.length)
{
return false;
}
for (int i = 0 ; i < upper ; ++i)
{
if (!isAssignableFrom(formals[i], actuals[i])) {
return false;
}
}
return true;
}
/*****************************************************************************
* Returns true if a variable of type "one" can be assigned from a value of
* type "two". This does basically the same thing as the
* Class#isAssignableFrom(Class) method, but this also checks if "one" is a
* primitive class (int.class, etc) and "two" a boxed class (Integer.class,
* etc). The reverse ("one" boxed and "two" primitive), while valid in the
* sense of the above definition, is not checked.
*/
private static boolean isAssignableFrom(Class<?> one, Class<?> two)
{
if( (one == byte.class && two == Byte.class)
|| (one == short.class && two == Short.class)
|| (one == int.class && two == Integer.class)
|| (one == long.class && two == Long.class)
|| (one == char.class && two == Character.class)
|| (one == float.class && two == Float.class)
|| (one == double.class && two == Double.class)
|| (one == boolean.class && two == Boolean.class))
{
return true;
}
else {
return one.isAssignableFrom(two);
}
}
/*****************************************************************************
* Repackage the parameters supplied to invoke() in order to make them
* agreeable to Method#invoke(). The problem is that Method#invoke() expects
* the variadic arguments to be passed as an array of the appropriate type.
*
* Protected for testing.
*/
protected static Object[] repackageParams(Method m, Object[] params)
{
if (!m.isVarArgs())
{
return params;
}
else
{
final Class<?>[] paramClasses = m.getParameterTypes();
final int argCount = paramClasses.length;
final Object[] out = new Object[argCount];
// Gather all non-variadic parameters.
for (int i = 0 ; i < argCount - 1 ; ++i)
{
out[i] = params[i];
}
/* Package the variadic parameters in an array. If there are none, use an
* empty array instead. */
if (params.length == argCount - 1)
{
out[argCount - 1] = Array.newInstance(
paramClasses[argCount - 1].getComponentType(), 0);
}
else
{
final int last = argCount - 1;
final int length = params.length - last;
out[last] = Array.newInstance(
paramClasses[last].getComponentType(), length);
for (int i = 0 ; i < length ; ++i)
{
Array.set(out[last], i, params[last + i]);
}
}
return out;
}
}
/*****************************************************************************
* Returns an array containing the classes of the objects passed as
* parameters.
*/
private static Class<?>[] getParamClasses(Object... params)
{
final Class<?> classes[] = new Class<?>[params.length];
for (int i = 0 ; i < params.length ; ++i)
{
classes[i] = params[i].getClass();
}
return classes;
}
}