MethodDispatcher.java
package org.codefilarete.tool.reflect;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.codefilarete.tool.InvocationHandlerSupport;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.StringAppender;
import org.codefilarete.tool.Strings;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.function.Functions;
/**
* A class aimed at creating proxy that will redirect some interface methods to some concrete implementation, fallback non-redirecting method
* to a concrete instance.
* This allows to "extend" or decorate an instance (the fallback one) with some additional feature.
*
* @author Guillaume Mary
*/
public class MethodDispatcher {
/**
* Methods (and its config) to be invoked according to effective method call.
* These are stored per a light signature to take polymorphism and multiple inheritance into account.
* Indeed, the light signature doesn't contain declaring class nor return type : only method name and arguments types.
*/
protected final Map<String /* simple method signature */, Interceptor> interceptors = new HashMap<>();
/** Final target when no interceptor handled method, not null after {@link #build(Class)} invocation */
private Supplier<?> fallback;
public Map<String, Interceptor> getInterceptors() {
return interceptors;
}
/**
* Redirects all interfazz methods to extensionDelegate
* @param interfazz an interface, must be extended by the one that will be given by {@link #build(Class)} (should be a super type of it)
* @param extensionDelegate an instance implementing X so methods of X can be redirected to it
* @param <X> a interface type
* @return this
*/
public <X> MethodDispatcher redirect(Class<X> interfazz, X extensionDelegate) {
return redirect(interfazz, extensionDelegate, false);
}
/**
* Same as {@link #redirect(Class, Object)} but proxy will be returned by all methods invocation : result of them will be ignored.
* Useful when return types of extensionDelegate methods don't match those of {@link #build(Class)} (kind of rare case).
*
* @param interfazz an interface, must be extended by the one that will be given to {@link #build(Class)} (should be a super type of it)
* @param extensionDelegate an instance implementing X so methods of X can be redirected to it
* @param <X> an interface type
* @return this
*/
public <X> MethodDispatcher redirect(Class<X> interfazz, X extensionDelegate, boolean returnProxy) {
for (Method method : interfazz.getMethods()) {
addInterceptor(method, extensionDelegate, returnProxy);
}
return this;
}
/**
* Same as {@link #redirect(Class, Object)} but given object will be returned by all method invocation.
* Useful when creating a fluent API where given interfazz methods should be invoked once, then methods return type is not herself but another
* one, the one that given returningMethodsTarget must implement (kind of rare case).
*
* @param interfazz an interface, must be extended by the one that will be given to {@link #build(Class)} (should be a super type of it)
* @param extensionDelegate an instance implementing X so methods of X can be redirected to it
* @param returningMethodsTarget an instance that implements all interfazz' methods return types
* @param <X> an interface type
* @return this
*/
public <X> MethodDispatcher redirect(Class<X> interfazz, X extensionDelegate, Object returningMethodsTarget) {
for (Method method : interfazz.getMethods()) {
addInterceptor(method, extensionDelegate, returningMethodsTarget);
}
return this;
}
protected <X> void addInterceptor(Method method, X extensionDelegate, boolean returnProxy) {
interceptors.put(giveSignature(method), new Interceptor(method, extensionDelegate, returnProxy));
}
protected <X> void addInterceptor(Method method, X extensionDelegate, Object returningMethodsTarget) {
interceptors.put(giveSignature(method), new Interceptor(method, extensionDelegate, returningMethodsTarget));
}
/**
* Gives a very ligth signature of a method (no return type, no modifiers) : only name and arguments
*
* @param method a method to get a signature
* @return a lightweight version of the signature of the given method
*/
protected String giveSignature(Method method) {
StringAppender signature = new StringAppender();
signature.cat(method.getName());
signature.ccat(method.getParameterTypes(), ",");
return signature.toString();
}
public <X> X build(Class<X> interfazz) {
assertInterceptingMethodsAreFromInterfaces();
assertClassImplementsInterceptingInterface(interfazz);
// Which ClassLoader ? Thread, fallback, this ?
// we don't use the Thread one because it can live forever so it might lead to memory leak
// we don't use fallback because it can be null
ClassLoader classLoader = getClass().getClassLoader();
Set<Class<?>> targetInterfaces = Iterables.collect(interceptors.values(), Functions.chain(Interceptor::getMethod, Method::getDeclaringClass), HashSet::new);
// we must add the X interface to the list that will be proxied, else we'll get a "com.sun.proxy.$Proxy4 cannot be cast to X"
targetInterfaces.add(interfazz);
// building invocationHandler : we create a holder for the proxy because it must be referenced in some cases
Object[] proxyHolder = new Object[1];
InvocationHandler dispatcher = new InvocationHandlerSupport((input, method, args) -> {
// looking for method to be really invoked
String key = giveSignature(method);
Interceptor interceptor = interceptors.get(key);
Object targetInstance;
boolean returnProxy = false;
Object returningMethodsTarget = null;
if (interceptor != null) {
// NB: the method finally invoked may not be the same as the one invoked
// in polymorphism and multiple inheritance cases
method = interceptor.getMethod();
targetInstance = interceptor.getMethodTarget();
returnProxy = interceptor.isReturnProxy();
returningMethodsTarget = interceptor.getReturningMethodsTarget();
} else {
if (fallback == null && !Reflections.isStatic(method)) {
throw new NullPointerException("No fallback instance was declared, therefore calling " + Reflections.toString(method)
+ " would throw NullPointerException: try to set one or redirect given method to a compatible instance");
}
targetInstance = fallback.get();
}
Object result = invoke(targetInstance, method, args);
// Handling return cases
if (returnProxy) {
result = proxyHolder[0];
} else if (returningMethodsTarget != null) {
result = returningMethodsTarget;
}
return result;
}) {
/** We handle toString() to have some message indicating who we are to avoid "ghost" behavior and hard debug */
@Override
public String toString() {
return "Dispatcher to " + fallback.get().toString();
}
};
proxyHolder[0] = Proxy.newProxyInstance(classLoader, targetInterfaces.toArray(new Class[0]), dispatcher);
return (X) proxyHolder[0];
}
private <X> void assertClassImplementsInterceptingInterface(Class<X> interfazz) {
interceptors.values().forEach(m -> {
if (!m.getMethod().getDeclaringClass().isAssignableFrom(interfazz)) {
throw new IllegalArgumentException(
Reflections.toString(interfazz) + " doesn't implement " + Reflections.toString(m.getMethod().getDeclaringClass()));
}
});
}
private void assertInterceptingMethodsAreFromInterfaces() {
interceptors.values().forEach(m -> {
if (!m.getMethod().getDeclaringClass().isInterface()) {
throw new UnsupportedOperationException("Cannot intercept concrete method : " + Reflections.toString(m.getMethod()));
}
});
}
/**
* Indicates on which instance unregistered methods will be invoked
* @param fallback the instance on which unregistered methods will be invoked
* @return this
*/
public MethodDispatcher fallbackOn(Object fallback) {
return fallbackOn(() -> fallback);
}
public MethodDispatcher fallbackOn(Supplier<?> fallback) {
this.fallback = fallback;
return this;
}
/**
* Invokes a method on a target
* @param target instance target of the method
* @param method the method to be called
* @param args method arguments
* @return the result of invocation of the given method and arguments on given instance
* @throws Throwable the one thrown by method invocation or one wrapping it for better understanding
*/
protected Object invoke(Object target, Method method, Object[] args) throws Throwable {
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof java.lang.ClassCastException) {
throw new WrongTypeReturnedException((java.lang.ClassCastException) cause, method);
}
// we rethrow the main exception so caller will not be polluted by InvocationTargetException
throw cause;
} catch (IllegalArgumentException e) {
// See ExceptionConverter for some code correlation
IllegalArgumentException result = e;
if ("object is not an instance of declaring class".equals(e.getMessage())) {
Class<?> declaringClass = method.getDeclaringClass();
String message = "Wrong given instance while invoking " + Reflections.toString(method);
message += ": expected " + declaringClass.getName() + " but " + target + " was given";
result = new IllegalArgumentException(message, e);
}
throw result;
}
}
protected static class Interceptor {
private final Method method;
private final Object methodTarget;
private final boolean returnProxy;
private final Object returningMethodsTarget;
public Interceptor(Method method, Object methodTarget, boolean returnProxy) {
this.method = method;
this.methodTarget = methodTarget;
this.returnProxy = returnProxy;
this.returningMethodsTarget = null;
}
public Interceptor(Method method, Object methodTarget, Object returningMethodsTarget) {
this.method = method;
this.methodTarget = methodTarget;
this.returnProxy = false;
this.returningMethodsTarget = returningMethodsTarget;
}
public Method getMethod() {
return method;
}
public Object getMethodTarget() {
return methodTarget;
}
public boolean isReturnProxy() {
return returnProxy;
}
public Object getReturningMethodsTarget() {
return returningMethodsTarget;
}
}
public static class WrongTypeReturnedException extends RuntimeException {
private final Class expected;
private final Class given;
private final Method method;
public WrongTypeReturnedException(java.lang.ClassCastException classCastException, Method method) {
this(Reflections.forName(Strings.tail(classCastException.getMessage(), " cannot be cast to ").toString()),
Reflections.forName(Strings.head(classCastException.getMessage(), " cannot be cast to ").toString()),
method,
classCastException);
}
public WrongTypeReturnedException(Class expected, Class given, Method method, java.lang.ClassCastException cause) {
super(cause);
this.expected = expected;
this.given = given;
this.method = method;
}
@Override
public String getMessage() {
return "Code handling " + Reflections.toString(method) + " is expected to return a " + Reflections.toString(expected) + " but returned a " + Reflections.toString(given);
}
}
}