org.perfectable.introspection.proxy.MethodInvocation Maven / Gradle / Ivy
package org.perfectable.introspection.proxy;
import org.perfectable.introspection.query.ConstructorQuery;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import com.google.common.primitives.Primitives;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static org.perfectable.introspection.Introspections.introspect;
/**
* Capture of a information needed to invoke a method.
*
* This class collects and mimics arguments received by different proxy frameworks when method is intercepted on
* proxy object, represents it in uniform fashion and allows some manipulations. Instances of this class can be also
* constructed synthetically, as if the call was made but intercepted before execution.
*
*
This class handles three elements of a call: Method that was called, a receiver, that is object on which method
* was called, and arguments that were passed with the call. Receiver can be omitted, if the method is static.
*
*
There are two most important methods on this class: {@link #invoke} which will execute the method with specified
* arguments and receiver, and return result or throw an exception. The other method is {@link #decompose} which
* allows introspection of structure of this invocation.
*
*
Objects of this class are unmodifiable, that is methods of this class that would change the object actually
* produce new changed object. It is not immutable, because both receiver and arguments passed can, and often are,
* mutable.
*
* @param Type of method receiver (i.e. {@code this})
*/
public final class MethodInvocation implements Invocation {
private static final Object[] EMPTY_ARGUMENTS = new Object[0];
private final Method method;
@Nullable
private final T receiver;
private final Object[] arguments;
@LazyInit
private transient MethodHandle handle;
private static final MethodHandle PRIVATE_LOOKUP_CONSTRUCTOR = findPrivateLookupConstructor();
/**
* Create invocation that was intercepted from proxy mechanism.
*
* This method assumes that if {@code method} is vararg, and requires X non-vararg parameters, the
* {@code arguments} passed here contains exactly X+1 elements, where X one are the non-vararg arguments that are
* compatible with non-vararg parameter type, and the last element is an array of elements of the vararg type.
* This is how the method call is represented in runtime for varargs. This method will 'unroll' the last array
* argument and create invocation that has flat arguments array.
*
*
For non-varargs {@code method}, this method is identical to {@link #of}, i.e. {@code arguments} must be a
* flat array with element count exactly equal to method parameter count, and with compatible types.
*
* @param method method that was/will be called on invocation
* @param receiver receiver of the method call (i.e. {@code this})
* @param arguments arguments in a runtime representation
* @param type of receiver
* @return method invocation comprised from passed arguments
* @throws IllegalArgumentException when method invocation is illegal and will not succeed: ex. method is static
* and receiver was provided (or other way around), receiver is of wrong type for the provided method,
* or arguments are not matching method parameter types.
*/
public static MethodInvocation intercepted(Method method,
@Nullable T receiver, @Nullable Object... arguments) {
Object[] actualArguments = flattenVariableArguments(method, arguments);
return of(method, receiver, actualArguments);
}
/**
* Create synthetic invocation from scratch.
*
* This method assumes that if {@code method} is vararg, and requires X non-vararg parameters,
* {@code arguments} contain at least X elements, where each of these elements is compatible with corresponding
* parameter of the method, and any amount of elements that are compatible with the variable parameter of the
* method.
*
*
For non-varargs {@code method}, this method expects an array with element count exactly equal to method
* parameter count, and with compatible types.
*
* @param method method that was/will be called on invocation
* @param receiver receiver of the method call (i.e. {@code this})
* @param arguments arguments in a source representation
* @param type of receiver
* @return method invocation comprised from passed arguments
* @throws IllegalArgumentException when method invocation is illegal and will not succeed: ex. method is static
* and receiver was provided (or other way around), receiver is of wrong type for the provided method,
* or arguments are not matching method parameter types.
*/
public static MethodInvocation of(Method method, @Nullable T receiver, Object... arguments) {
requireNonNull(method);
// receiver might be null
requireNonNull(arguments);
verifyCallability(method, receiver, arguments);
Object[] argumentsClone = arguments.clone();
return new MethodInvocation<>(method, receiver, argumentsClone);
}
@SuppressWarnings("ArrayIsStoredDirectly")
private MethodInvocation(Method method, @Nullable T receiver,
Object... arguments) {
this.method = method;
this.receiver = receiver;
this.arguments = arguments;
}
/**
* Executes the configured invocation.
*
* @return result of non-throwing invocation. If the method was {@code void}, the result will be null.
* @throws Throwable result of throwing invocation. This will be exactly the exception that method thrown.
*/
@CanIgnoreReturnValue
@Nullable
@Override
public Object invoke() throws Throwable {
if (handle == null) {
handle = createHandle();
}
return handle.invoke();
}
/**
* Interface that allows decomposition of the invocation.
*
* @param type of receiver expected
* @param type of result of decomposition.
*/
@FunctionalInterface
public interface Decomposer {
/**
* Decomposition method.
*
* @param method method that was called
* @param receiver receiver that the method was called on, or null if the method was static
* @param arguments arguments passed to the method, in source (flat) representation
* @return result of decomposition
*/
R decompose(Method method, @Nullable T receiver, Object... arguments);
}
/**
* Decomposes the invocation to its parts.
*
* This method allows to transform this invocation by its parts into other object.
*
*
For example, decomposition might produce log message of method called:
*
* Decomposer<Object, String> stringifingDecomposer = (method, receiver, arguments) ->
* String.format("Method %s was called on %s with %s", method, receiver, arguments);
* LOGGER.debug(invocation.decompose(stringifingDecomposer))
*
*
* Another example: decomposer might substitute invocation method for another one:
*
* Decomposer<Object, MethodInvocation<?>> replacingDecomposer = (method, receiver, arguments) ->
* MethodInvocation.of(anotherMethod, receiver, arguments);
* MethodInvocation<?> replacedMethodInvocation = invocation.decompose(replacingDecomposer))
* return replacedMethodInvocation.invoke();
*
*
* @param decomposer decomposer to use for this invocation
* @param return type of decomposition
* @return whatever decomposer returned on its {@link Decomposer#decompose} call
*/
@CanIgnoreReturnValue
public R decompose(Decomposer super T, R> decomposer) {
return decomposer.decompose(method, receiver, arguments.clone());
}
/**
* Creates new invocation with replaced method.
*
* New method is checked for compatibility with both receiver and arguments.
*
* @param newMethod another method to be used
* @return new invocation with new method, same receiver and same arguments
* @throws IllegalArgumentException when new method is incompatible with receiver or arguments in any way
*/
public MethodInvocation withMethod(Method newMethod) {
verifyReceiverCompatibility(newMethod, receiver);
verifyArgumentsCompatibility(newMethod, arguments);
return new MethodInvocation<>(newMethod, receiver, arguments);
}
/**
* Creates new invocation with replaced receiver.
*
* New receiver is checked for compatibility with method.
*
* @param newReceiver another receiver to be used
* @param extension type of the receiver, to allow concretization of result
* @return new invocation with same method, new receiver and same arguments
* @throws IllegalArgumentException when new receiver is incompatible with method
*/
public MethodInvocation withReceiver(X newReceiver) {
verifyReceiverCompatibility(method, newReceiver);
return new MethodInvocation<>(method, newReceiver, arguments);
}
/**
* Creates new invocation with replaced arguments.
*
* New arguments is checked for compatibility with method.
*
* @param newArguments new arguments to be used
* @return new invocation with same method, same receiver and new arguments
* @throws IllegalArgumentException when new arguments is incompatible with method
*/
public MethodInvocation withArguments(Object... newArguments) {
verifyArgumentsCompatibility(method, newArguments);
return new MethodInvocation<>(method, receiver, newArguments);
}
@Override
public int hashCode() {
return Objects.hash(this.method, this.receiver, Arrays.hashCode(this.arguments));
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof MethodInvocation>)) {
return false;
}
MethodInvocation> other = (MethodInvocation>) obj;
return Objects.equals(this.method, other.method)
&& Objects.equals(this.receiver, other.receiver)
&& Arrays.equals(this.arguments, other.arguments);
}
private static void verifyCallability(Method method, @Nullable Object receiver, Object... arguments) {
verifyReceiverCompatibility(method, receiver);
verifyArgumentsCompatibility(method, arguments);
}
private static void verifyReceiverCompatibility(Method method, @Nullable Object receiver) {
if ((method.getModifiers() & Modifier.STATIC) == 0) {
checkArgument(receiver != null,
"Method %s is not static, got null as receiver", method);
Class> requiredType = method.getDeclaringClass();
checkArgument(requiredType.isInstance(receiver),
"Method %s requires %s as receiver, got %s", method, requiredType, receiver);
}
else {
checkArgument(receiver == null,
"Method %s is static, got %s as receiver", method, receiver);
}
}
private static void verifyArgumentsCompatibility(Method method, Object... arguments) {
Class>[] formals = method.getParameterTypes();
boolean isVarArgs = method.isVarArgs();
if (isVarArgs) {
checkArgument(arguments.length >= formals.length - 1,
"Method %s requires at least %s arguments, got %s", method, formals.length - 1, arguments.length);
}
else {
checkArgument(arguments.length == formals.length,
"Method %s requires %s arguments, got %s", method, formals.length, arguments.length);
}
for (int i = 0; i < arguments.length; i++) {
Class> parameterType;
if (isVarArgs && i >= formals.length - 1) {
parameterType = formals[formals.length - 1].getComponentType();
}
else {
parameterType = formals[i];
}
Object argument = arguments[i];
if (argument == null) {
checkArgument(!parameterType.isPrimitive(),
"Method %s has primitive %s as parameter %s, got null argument", method, parameterType, i + 1);
}
else {
Class> argumentType = argument.getClass();
Class> wrappedParameterType = Primitives.wrap(parameterType);
checkArgument(wrappedParameterType.isAssignableFrom(argumentType),
"Method %s takes %s as parameter %s, got %s as argument",
method, wrappedParameterType, i + 1, argument);
}
}
}
private static Object[] flattenVariableArguments(Method method, @Nullable Object[] actuals) {
if (actuals == null) {
return EMPTY_ARGUMENTS;
}
if (!method.isVarArgs()) {
return actuals;
}
Class>[] formals = method.getParameterTypes();
Object variableActual = actuals[actuals.length - 1];
int variableLength = Array.getLength(variableActual);
int resultSize = (formals.length - 1) + variableLength;
Object[] result = new Object[resultSize];
System.arraycopy(actuals, 0, result, 0, formals.length - 1);
for (int i = 0; i < variableLength; i++) {
result[formals.length - 1 + i] = Array.get(variableActual, i);
}
return result;
}
@SuppressWarnings("IllegalCatch")
private MethodHandle createHandle() {
MethodHandles.Lookup lookup;
try {
lookup = (MethodHandles.Lookup) PRIVATE_LOOKUP_CONSTRUCTOR.invoke(method.getDeclaringClass());
}
catch (Throwable throwable) {
throw new AssertionError(throwable);
}
MethodHandle methodHandle;
try {
methodHandle = lookup.unreflect(method);
}
catch (IllegalAccessException e) {
throw new AssertionError(e);
}
if (receiver != null) {
methodHandle = methodHandle.bindTo(receiver);
}
if (method.isVarArgs()) {
Class>[] parameterTypes = method.getParameterTypes();
Class> lastParameterType = parameterTypes[parameterTypes.length - 1];
int overflowArguments = arguments.length - parameterTypes.length + 1;
methodHandle = methodHandle.asCollector(lastParameterType, overflowArguments);
}
return MethodHandles.insertArguments(methodHandle, 0, arguments);
}
private static MethodHandle findPrivateLookupConstructor() {
int allModifiers = MethodHandles.Lookup.PUBLIC | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PRIVATE;
MethodHandles.Lookup lookup = MethodHandles.lookup();
ConstructorQuery generalConstructorQuery = introspect(MethodHandles.Lookup.class)
.constructors();
Optional> noPreviousClassConstructorOption =
generalConstructorQuery
.parameters(Class.class, int.class)
.asAccessible()
.option();
if (noPreviousClassConstructorOption.isPresent()) {
Constructor lookupConstructor = noPreviousClassConstructorOption.get();
MethodHandle methodHandle;
try {
methodHandle = lookup.unreflectConstructor(lookupConstructor);
}
catch (IllegalAccessException e) {
throw new AssertionError(e);
}
return MethodHandles.insertArguments(methodHandle, 1, allModifiers);
}
Optional> withPreviousClassConstructorOption = generalConstructorQuery
.parameters(Class.class, Class.class, int.class)
.asAccessible()
.option();
if (withPreviousClassConstructorOption.isPresent()) {
Constructor lookupConstructor = withPreviousClassConstructorOption.get();
MethodHandle methodHandle;
try {
methodHandle = lookup.unreflectConstructor(lookupConstructor);
}
catch (IllegalAccessException e) {
throw new AssertionError(e);
}
return MethodHandles.insertArguments(methodHandle, 1, null, allModifiers);
}
throw new AssertionError("Finding private lookup constructor was unsuccessful");
}
}