package nl.ipo.cds.validation.execute;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleProxies;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class Executor<C> {
private final static MethodHandle executeHandle = Compiler.findMethod (
Executor.class,
"execute",
MethodType.methodType (Object.class, Object.class, Object[].class)
);
public final Class<? extends C> contextClass;
public final List<Class<?>> inputClasses;
public final ExecutionPlan<C> plan;
Executor (final Class<? extends C> contextClass, final List<Class<?>> inputClasses, final ExecutionPlan<C> plan) {
this.contextClass = contextClass;
this.inputClasses = new ArrayList<> (inputClasses);
this.plan = plan;
}
public Object execute (final C context, final Object ... inputs) throws ExecutorException {
final List<ExecutionStep<C>> steps = plan.getExecutionSteps ();
// Create a "register" array to hold the result of previous computations:
final Object[] registers = new Object[steps.size ()];
// Execute the plan:
try {
for (int i = 0, n = steps.size (); i < n; ++ i) {
final ExecutionStep<C> step = steps.get (i);
final ExpressionExecutor<C> executor = step.executor;
final int[] inputSteps = step.inputs;
final Object[] arguments = new Object[executor.argumentsArrayLength];
// Add the context objects if the executor requests them (first argument is of type Object[]):
int index = 0;
if (executor.addContextObjects) {
arguments[index ++] = inputs;
}
// Add the context if the executor requests it (first or second argument is of type C):
if (executor.addContext) {
arguments[index ++] = context;
}
// Collect and add inputs after the context objects and the context as arguments to the method, or as a single varargs argument:
final int argumentCount = inputSteps.length;
if (executor.useVarargs) {
final Object[] varargs = (Object[])Array.newInstance (executor.varargsType, argumentCount);
for (int j = 0; j < argumentCount; ++ j) {
varargs[j] = registers[inputSteps[j]];
}
arguments[index ++] = varargs;
} else {
for (int j = 0; j < argumentCount; ++ j) {
arguments[index ++] = registers[inputSteps[j]];
}
}
registers[i] = executor.methodHandle.invokeWithArguments (arguments);
}
} catch (Throwable e) {
throw new ExecutorException (e);
}
return registers.length == 0 ? null : registers[registers.length - 1];
}
public <I> I forInterface (final Class<I> iface) {
if (iface == null) {
throw new IllegalArgumentException ("iface cannot be null");
}
if (!iface.isInterface ()) {
throw new IllegalArgumentException (String.format ("%s must be an interface", iface.toString ()));
}
final Method[] methods = iface.getMethods ();
if (methods.length != 1) {
throw new IllegalArgumentException (String.format ("%s must have exactly one method", iface.toString ()));
}
final Method method = methods[0];
final ExecutableExpression<C, ?> lastExpression = plan.getExecutionSteps().get (plan.getExecutionSteps ().size () - 1).executor.expression;
// Return types must match:
if (!lastExpression.getResultType ().equals (method.getReturnType ())) {
throw new IllegalArgumentException (String.format (
"Method %s has unexpected return type %s, while expecting %s",
method.toString (),
lastExpression.getResultType ().toString (),
method.getReturnType ().toString ()
));
}
// Interface must have proper input types:
final Class<?>[] parameterTypes = method.getParameterTypes ();
if (parameterTypes.length != inputClasses.size () + 1) {
throw new IllegalArgumentException (String.format (
"Method %s has an invalid number of arguments %d, expected %d",
method.toString (),
parameterTypes.length,
inputClasses.size () + 1
));
}
if (!parameterTypes[0].isAssignableFrom (contextClass)) {
throw new IllegalArgumentException (String.format (
"First parameter of %s must is of unexpected type %s, expecting (a subclass of) %s",
method.toString (),
parameterTypes[0].toString (),
contextClass.toString ()
));
}
final Class<?> actualContextClass = parameterTypes[0];
for (int i = 0; i < inputClasses.size (); ++ i) {
if (!parameterTypes[i + 1].isAssignableFrom (inputClasses.get (i))) {
throw new IllegalArgumentException (String.format (
"Parameter %d of %s must be assignable from %s",
i + 1,
method.toString (),
inputClasses.get (i).toString ()
));
}
}
// Bind the execute handle to the
final MethodHandle executeThisHandle = executeHandle.bindTo (this);
// Convert to a specific type:
final Class<?> returnType = lastExpression.getResultType ();
final Class<?>[] castParameterTypes = new Class<?>[inputClasses.size () + 1];
castParameterTypes[0] = actualContextClass;
for (int i = 0; i < inputClasses.size (); ++ i) {
castParameterTypes[i + 1] = inputClasses.get (i);
}
final MethodHandle castExecuteHandle = executeThisHandle
.asVarargsCollector (Object[].class)
.asType (MethodType.methodType (returnType, castParameterTypes));
// Create an interface wrapper for the resulting method handle:
return MethodHandleProxies.asInterfaceInstance (iface, castExecuteHandle);
}
@Override
public String toString () {
return "Executor: " + plan.toString ();
}
}