package org.testory.proxy.proxer;
import static java.lang.reflect.Modifier.isPublic;
import static org.testory.proxy.Invocation.invocation;
import static org.testory.proxy.ProxyException.check;
import static org.testory.proxy.Typing.typing;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.objenesis.ObjenesisStd;
import org.testory.proxy.Handler;
import org.testory.proxy.Proxer;
import org.testory.proxy.Typing;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.NoOp;
public class CglibProxer implements Proxer {
public Object proxy(Typing typing, Handler handler) {
check(typing != null);
check(handler != null);
return newProxyByCglib(tryAsProxiable(typing), handler);
}
private static Object newProxyByCglib(Typing typing, Handler handler) {
Enhancer enhancer = new Enhancer() {
/** includes all constructors */
protected void filterConstructors(Class sc, List constructors) {}
};
enhancer.setClassLoader(Thread.currentThread().getContextClassLoader());
enhancer.setUseFactory(true);
enhancer.setSuperclass(typing.superclass);
enhancer.setInterfaces(typing.interfaces.toArray(new Class[0]));
enhancer.setCallbackTypes(new Class[] { MethodInterceptor.class, NoOp.class });
enhancer.setCallbackFilter(new CallbackFilter() {
/** ignores bridge methods */
public int accept(Method method) {
return method.isBridge() ? 1 : 0;
}
});
Class<?> proxyClass = enhancer.createClass();
Factory proxy = (Factory) new ObjenesisStd().newInstance(proxyClass);
proxy.setCallbacks(new Callback[] { asMethodInterceptor(handler), new SerializableNoOp() });
return proxy;
}
private static Typing tryAsProxiable(Typing typing) {
return tryPeel(tryWithoutFactory(tryWithoutObjectBecauseOfCglibBug(typing)));
}
private static Typing tryPeel(Typing typing) {
return isPeelable(typing.superclass)
? tryPeel(peel(typing))
: typing;
}
private static boolean isPeelable(Class<?> type) {
return !isPublic(type.getModifiers()) && isFromJdk(type) && isContainer(type);
}
private static boolean isFromJdk(Class<?> type) {
return type.getPackage() == Package.getPackage("java.util");
}
private static boolean isContainer(Class<?> type) {
return Collection.class.isAssignableFrom(type)
|| Map.class.isAssignableFrom(type)
|| Iterator.class.isAssignableFrom(type)
|| (type != null && isContainer(type.getDeclaringClass()));
}
private static Typing tryWithoutFactory(Typing typing) {
return Arrays.asList(typing.superclass.getInterfaces()).contains(Factory.class)
? withoutFactory(typing)
: typing;
}
private static Typing withoutFactory(Typing typing) {
Typing peeled = peel(typing);
Class<?> superclass = peeled.superclass;
Set<Class<?>> interfaces = new HashSet<>(peeled.interfaces);
interfaces.remove(Factory.class);
return typing(superclass, interfaces);
}
private static Typing peel(Typing typing) {
Class<?> superclass = typing.superclass.getSuperclass();
Set<Class<?>> interfaces = new HashSet<>(typing.interfaces);
interfaces.addAll(Arrays.asList(typing.superclass.getInterfaces()));
return typing(superclass, interfaces);
}
public static class ProxiableObject {}
private static Typing tryWithoutObjectBecauseOfCglibBug(Typing typing) {
return typing.superclass == Object.class
? typing(ProxiableObject.class, typing.interfaces)
: typing;
}
private static MethodInterceptor asMethodInterceptor(final Handler handler) {
return new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
return isFinalize(method)
? null
: handler.handle(invocation(method, obj, Arrays.asList(args)));
}
};
}
private static boolean isFinalize(Method method) {
return method.getName().equals("finalize") && method.getParameterTypes().length == 0;
}
private static class SerializableNoOp implements NoOp, Serializable {
private static final long serialVersionUID = 4961170565306875478L;
private SerializableNoOp() {}
}
}