org.protelis.lang.interpreter.util.ReflectionUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of protelis-interpreter Show documentation
Show all versions of protelis-interpreter Show documentation
The Protelis language interpreter
/*
* Copyright (C) 2021, Danilo Pianini and contributors listed in the project's build.gradle.kts or pom.xml file.
*
* This file is part of Protelis, and is distributed under the terms of the GNU General Public License,
* with a linking exception, as described in the file LICENSE.txt in this project's top directory.
*/
package org.protelis.lang.interpreter.util;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.protelis.lang.datatype.Field;
import org.protelis.lang.datatype.Fields;
import org.protelis.lang.datatype.Unit;
import org.protelis.vm.ExecutionContext;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Primitives;
import com.google.common.util.concurrent.UncheckedExecutionException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import gnu.trove.list.TIntList;
import gnu.trove.list.array.TIntArrayList;
/**
* Utilities that make easier to cope with Java Reflection.
*/
public final class ReflectionUtils {
private static final int CACHE_MAX_SIZE = 1000;
private static final LoadingCache, String, List>>, Method> METHOD_CACHE = CacheBuilder
.newBuilder().maximumSize(CACHE_MAX_SIZE).expireAfterAccess(1, TimeUnit.HOURS)
.build(new CacheLoader<>() {
@Nonnull
@Override
public Method load(@Nonnull final Triple, String, List>> key) {
final List> al = key.getRight();
final Class>[] args = new Class>[al.size()];
return loadBestMethod(key.getLeft(), key.getMiddle(), al.toArray(args));
}
});
private static final Map, Function> NUMBER_CASTER = ImmutableMap
., Function>builder()
.put(Byte.class, Number::byteValue)
.put(Byte.TYPE, Number::byteValue)
.put(Short.class, Number::shortValue)
.put(Short.TYPE, Number::shortValue)
.put(Integer.class, Number::intValue)
.put(Integer.TYPE, Number::intValue)
.put(Long.class, Number::longValue)
.put(Long.TYPE, Number::longValue)
.put(Float.class, Number::floatValue)
.put(Float.TYPE, Number::floatValue)
.put(Double.class, Number::doubleValue)
.put(Double.TYPE, Number::doubleValue)
.build();
private ReflectionUtils() {
}
private static Number castIfNeeded(final Class> dest, final Number arg) {
Objects.requireNonNull(dest);
Objects.requireNonNull(arg);
if (dest.isAssignableFrom(arg.getClass())) {
return arg;
}
final Function cast = NUMBER_CASTER.get(dest);
if (cast != null) {
return cast.apply(arg);
}
throw new IllegalStateException("Impossible cast from " + arg.getClass() + " to " + dest);
}
/**
* @param clazz
* the class under test
* @return true if the class is a subclass of {@link Number} or it is a
* number having primitive representation in Java
*/
private static boolean classIsNumber(final Class> clazz) {
return Number.class.isAssignableFrom(clazz) || NUMBER_CASTER.containsKey(clazz);
}
private static boolean classIsPrimitive(final Class> clazz) {
return Primitives.allPrimitiveTypes().contains(clazz);
}
private static boolean classIsWrapper(final Class> clazz) {
return Primitives.allWrapperTypes().contains(clazz);
}
private static boolean compatibleLength(@Nonnull final Method m, final int args, final boolean toBeInjected) {
final Class>[] paramTypes = Objects.requireNonNull(m, "Invoked method cannot be null.")
.getParameterTypes();
/*
* The method must be invoked with enough arguments to match at least the count
* of non-ExecutionContext parameters (except if varargs, in which case one less
* argument is allowed), and at most the total number of parameters (unless it
* is varargs, in which case there is no limit)
*/
final int actualArgsLength = (toBeInjected ? 1 : 0) + args;
return m.isVarArgs() ? actualArgsLength >= paramTypes.length - 1 : actualArgsLength == paramTypes.length;
}
private static boolean compatibleLength(@Nonnull final Method m, final int args, @Nullable final Class> firstArgType) {
return compatibleLength(m, args, willBeInjected(m, firstArgType));
}
private static int computePointsForWrapper(final Class> primitive, final Class> wrapper) {
final Class> wrapped = ClassUtils.primitiveToWrapper(primitive);
if (wrapped.equals(wrapper)) {
return 2;
}
if (wrapped.isAssignableFrom(wrapper)) {
return 1;
}
return 0;
}
private static String formatArguments(final Object[] args) {
return Arrays.stream(args)
.map(it -> it + ": " + it.getClass().getSimpleName())
.collect(Collectors.joining(",", "(", ")"));
}
/**
* Invokes a method. If there are fields involved, field operations are
* applied
*
* @param context
* the current {@link ExecutionContext}
* @param clazz
* the class to search for a method
* @param methodName
* the name of the method
* @param target
* the target object (can be null in case of static invocation)
* @param args
* the arguments for the method
* @return the result of the method invocation
*/
public static Object invokeFieldable(
final ExecutionContext context,
final Class> clazz,
final String methodName,
final Object target,
final Object[] args) {
if (Field.class.isAssignableFrom(clazz) && target instanceof Field) {
return invokeFieldable(
context,
((Field>) target).getExpectedType(),
methodName,
target,
args);
}
return invokeFieldable(context, searchBestMethod(clazz, methodName, args), target, args);
}
/**
* Invokes a method. If there are fields involved, field operations are
* applied
*
* @param context
* the current {@link ExecutionContext}
* @param toInvoke
* the method to be invoked
* @param target
* the target object (can be null in case of static invocation)
* @param args
* the arguments for the method
* @return the result of the method invocation
*/
public static Object invokeFieldable(
@Nonnull final ExecutionContext context,
@Nonnull final Method toInvoke,
@Nullable final Object target,
@Nonnull final Object[] args) {
final boolean toBeInjected = willBeInjected(toInvoke, args);
if (!compatibleLength(toInvoke, args.length, toBeInjected)) {
throw new IllegalArgumentException("Number of parameters of " + toInvoke
+ " does not match the provided array " + Arrays.toString(args));
}
final boolean fieldTarget = target instanceof Field;
final TIntList fieldIndexes = new TIntArrayList(args.length);
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Field
&& !Field.class.isAssignableFrom(nthArgumentType(toInvoke, toBeInjected ? i + 1 : i))) {
fieldIndexes.add(i);
}
}
if (fieldTarget || !fieldIndexes.isEmpty()) {
return Fields.apply(
(actualT, actualA) -> invokeMethod(context, toInvoke, actualT, actualA),
fieldTarget,
fieldIndexes.toArray(),
target,
args
);
}
return invokeMethod(context, toInvoke, target, args);
}
@SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "we need to intercept all runtime events")
private static Object invokeMethod(
@Nonnull final ExecutionContext context,
@Nonnull final Method method,
@Nullable final Object target,
@Nonnull final Object[] args) {
final Object[] useArgs = repackageIfRequired(context, method, args);
try {
return invokePossiblyVoidMethod(method, target, useArgs);
} catch (Exception exc) { // NOPMD: Generic exception caught by purpose
/*
* Failure: maybe some cast was required, if arguments were actually passed?
*/
if (useArgs.length == 0) {
throw new IllegalStateException(exc);
}
final Class>[] params = method.getParameterTypes();
for (int i = 0; i < params.length; i++) {
final Class> expected = params[i];
final Object actual = useArgs[i];
if (!expected.isAssignableFrom(actual.getClass())
&& classIsNumber(expected)
&& actual instanceof Number) {
useArgs[i] = castIfNeeded(expected, (Number) actual);
}
}
try {
return invokePossiblyVoidMethod(method, target, useArgs);
} catch (IllegalAccessException e) {
throw new UnsupportedOperationException("Method " + method // NOPMD: false positive
+ " cannot get invoked because it is not accessible.", e);
} catch (IllegalArgumentException e) {
final boolean isStatic = target == null;
final String errorMessage = Optional.ofNullable(e.getMessage())
.orElse("Probable argument type mismatch")
+ ": cannot invoke "
+ method
+ " with arguments " + formatArguments(useArgs)
+ (isStatic ? "" : " on " + target);
throw new UnsupportedOperationException(errorMessage, e); // NOPMD: false positive
} catch (InvocationTargetException e) {
final Throwable rootCause = e.getCause();
final String errorMessage = "Invocation of "
+ method
+ (target == null ? "" : " on " + target)
+ " with arguments " + formatArguments(useArgs)
+ " failed because of an internal "
+ (rootCause == null ? "unidentified error" : rootCause.getClass().getSimpleName())
+ "; please look at the stacktrace for further information";
throw new UnsupportedOperationException(errorMessage, e); // NOPMD: false positive
}
}
}
private static Object invokePossiblyVoidMethod(
@Nonnull final Method method,
@Nullable final Object target,
@Nonnull final Object[] args) throws IllegalAccessException, InvocationTargetException {
final Object result = method.invoke(target, args);
if (result == null && method.getReturnType().equals(Void.TYPE)) {
return Unit.UNIT;
}
return result;
}
private static Method loadBestMethod(
@Nonnull final Class> clazz,
@Nonnull final String methodName,
@Nonnull final Class>[] argClass
) {
Objects.requireNonNull(clazz, "The class on which the method will be invoked can not be null.");
Objects.requireNonNull(methodName, "Method name can not be null.");
Objects.requireNonNull(argClass, "Method arguments can not be null.");
final Method[] candidates = Arrays.stream(clazz.getMethods())
// Parameter number
.filter(m -> compatibleLength(m, argClass.length, argClass.length > 0 ? argClass[0] : null))
// Method name
.filter(m -> m.getName().equals(methodName))
// Only pick accessibile methods, mapping to superclass/interfaces if needed
.map(MethodUtils::getAccessibleMethod)
.filter(Objects::nonNull)
.toArray(Method[]::new);
if (candidates.length == 0) {
throw new IllegalArgumentException("No accessible method named " + methodName
+ " callable with " + Arrays.toString(argClass) + " parameters is available in " + clazz);
}
if (candidates.length == 1 && argClass.length == 0) {
/*
* In case of 0-arity, the single candidate can be selected directly
*/
return candidates[0];
}
/*
* Deal with Java method overloading scoring methods
*/
final List> lm = new ArrayList<>(candidates.length);
for (final Method m: candidates) {
final Class>[] expectedParameters = m.getParameterTypes();
final Class>[] actualArgClass; // NOPMD: false positive
if (shouldPushContext(expectedParameters, argClass)) {
/*
* Push "self" as implicit parameter
*/
actualArgClass = new Class>[argClass.length + 1];
actualArgClass[0] = ExecutionContext.class;
System.arraycopy(argClass, 0, actualArgClass, 1, argClass.length);
} else {
actualArgClass = argClass;
}
boolean compatible = true;
int p = 0;
for (int i = 0; compatible && i < actualArgClass.length; i++) {
final Class> expected = nthArgumentType(m, i);
final Class> actual = actualArgClass[i];
if (actual == null && !classIsPrimitive(expected) || expected.isAssignableFrom(actual)) {
/*
* No downcast nor coercion required, there is compatibility
*/
p += 3;
} else if (ExecutionContext.class.isAssignableFrom(expected)) {
/*
* Expected "self", implicitly loaded
*/
p += 3;
} else if (classIsPrimitive(expected) && classIsWrapper(actual)) {
p += computePointsForWrapper(expected, actual);
} else if (classIsPrimitive(actual) && classIsWrapper(expected)) {
p += computePointsForWrapper(actual, expected);
} else if (!(classIsNumber(expected) && classIsWrapper(actual))) {
/*
* At least one is not a number: conversion with precision loss does not apply.
*/
compatible = false;
}
}
if (compatible) {
/*
* Early intercept the case of single candidate
*/
if (candidates.length == 1) {
return m;
}
lm.add(new ImmutablePair<>(p, m));
}
}
/*
* Find best
*/
return lm.stream()
.max(Map.Entry.comparingByKey())
.map(Pair::getValue)
.orElseThrow(() -> new IllegalStateException("Method selection for " + methodName
+ " inside " + clazz
+ " has been restricted to " + Arrays.toString(candidates)
+ " however none of them is compatible with arguments " + Arrays.toString(argClass)));
}
private static Class> nthArgumentType(final Method m, final int n) {
final Class>[] expectedArgs = m.getParameterTypes();
if (m.isVarArgs() && n >= (expectedArgs.length - 1)) {
final Class> varargType = expectedArgs[expectedArgs.length - 1];
return varargType.getComponentType();
} else {
return expectedArgs[n];
}
}
private static Object[] repackageIfRequired(
@Nonnull final ExecutionContext context,
@Nonnull final Method m,
@Nonnull final Object[] args
) {
final Class>[] expectedArgs = m.getParameterTypes();
final boolean pushContext = shouldPushContext(expectedArgs, args);
if (m.isVarArgs() || pushContext) {
// We will repackage into an array of the expected length
final Object[] newargs = new Object[expectedArgs.length];
// repackage all the base args
final int start; // NOPMD: false positive
if (pushContext) {
newargs[0] = context;
start = 1;
} else {
start = 0;
}
final int copiedArgCount = expectedArgs.length - start - (m.isVarArgs() ? 1 : 0);
System.arraycopy(args, 0, newargs, start, Math.max(copiedArgCount, 0));
if (m.isVarArgs()) {
// Determine how many arguments need repackaging
final int numVarArgs = args.length - copiedArgCount;
// Make an array of the appropriate type, then fill it in
final Class> varargType = expectedArgs[copiedArgCount];
final Object[] vararg = (Object[]) Array.newInstance(varargType.getComponentType(), numVarArgs);
if (numVarArgs >= 0) {
System.arraycopy(args, expectedArgs.length - 1, vararg, 0, numVarArgs);
}
// Put the new array in the last argument and return
newargs[newargs.length - 1] = vararg;
}
return newargs;
} else {
return args;
}
}
/**
* @param clazz
* the class where to search for suitable methods
* @param methodName
* the method to be invoked
* @param args
* the arguments for the method. If a {@link Field} is passed,
* then the expected type of the field is used.
* @return the result of the invocation, or an {@link IllegalStateException}
* if something goes wrong.
*/
private static Method searchBestMethod(final Class> clazz, final String methodName, final List