package org.antforge;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;
/**
* A proxy factory supports the rapid development of a transportation
* layer based on RMI for given clean interfaces. The interface classes
* do not need to be "cluttered" with specifics of the transportation
* implementation. This means no more annoying appearances of classes
* like java.rmi.Remote or java.rmi.RemotException in your interfaces.
*
* Interfaces can explicitly be marked as "remote interfaces". All methods
* exposed by such an interface can be invoked from a remote site. All
* parameters which are subclasses of such interfaces, will be passed
* by-reference and will not be serialized. This is just the same behavior
* as if the interface would extend java.rmi.Remote in the RMI world.
* The actual remote interfaces are generated on-demand at runtime.
*
* A proxy is a transparent object implementing both, the local and the
* generated remote interface. Both interfaces are usable:
*
* Cast the proxy to java.rmi.Remote and use it with any naming or
* registry service available to the RMI world.
* Cast the proxy to your local interface and don't bother whether
* it actually targets a local or a remote site.
*
* The decision how an invocation actually is dispatched can be solely
* based on whether the target object of a proxy is a remote or a local
* one. This decision is hidden inside the transportation layer.
*
* This is my attempt to show how RMI should have been designed in the
* first place. Please feel free to comment, improve, ignore or flame.
*
* @author Michael Starzinger
* @version 0.1-20100406
*/
public class ProxyFactory
{
private static ProxyFactory singleton;
private List> markedInterfaces;
private Map, String> generatedNames;
private Map, Class>> generatedInterfaces;
private Map createdProxies;
private ProxyFactory() {
// XXX We probably should use weak references here!
super();
this.markedInterfaces = new ArrayList>();
this.generatedNames = new HashMap, String>();
this.generatedInterfaces = new HashMap, Class>>();
this.createdProxies = new HashMap();
}
/**
* At the moment we only allow one proxy factory per VM because classes
* are generated during runtime and we have to ensure they are unique.
* To resolve this issue properly, move interface generation into a
* singleton and allow multiple proxy factories. Also remember that
* this factory is not at all thread-safe.
* @return The proxy factory singleton.
*/
public static ProxyFactory getFactory() {
if (singleton == null)
singleton = new ProxyFactory();
return singleton;
}
/**
* Marks the given interface as being a "remote interface".
* @param localInterface A plain-old interface class.
*/
public void markAsRemote(Class> localInterface) {
// Check if given class really is an interface.
if (!localInterface.isInterface())
throw new ProxyException("Given class is not an interface.");
// Check if given interface is a remote interface.
if (Remote.class.isAssignableFrom(localInterface))
throw new ProxyException("Given interface already is a remote interface.");
// Check if given interface is already marked as remote.
if (markedInterfaces.contains(localInterface))
return;
// Mark given interface as remote.
markedInterfaces.add(localInterface);
}
private String getRemoteInterfaceName(Class> localInterface) {
// Check if name for remote interface already generated.
if (generatedNames.containsKey(localInterface))
return generatedNames.get(localInterface);
// Otherwise trigger interface generation.
Class> remoteInterface = getRemoteInterface(localInterface);
return remoteInterface.getName();
}
private Class> getRemoteInterface(Class> localInterface) {
// Check if remote interface already generated.
if (generatedInterfaces.containsKey(localInterface))
return generatedInterfaces.get(localInterface);
// Generate a new remote interface.
Class> remoteInterface;
try {
remoteInterface = generateRemoteInterface(localInterface);
} catch (NotFoundException e) {
throw new ProxyException(e);
} catch (CannotCompileException e) {
e.printStackTrace();
throw new ProxyException(e);
}
// Remember the newly generated remote interface and return it.
generatedInterfaces.put(localInterface, remoteInterface);
return remoteInterface;
}
private Class> getOriginalInterface(Class> remoteInterface) {
// Check if remote interface was generated before.
for (Class> localInterface : generatedInterfaces.keySet())
if (generatedInterfaces.get(localInterface).equals(remoteInterface))
return localInterface;
// If we did not generate the remote interface, something is wrong.
throw new ProxyException("Unable to find original interface type.");
}
private int uniqueCounter = 1;
private String generateUniqueName() {
return "$Dynamic" + uniqueCounter++;
}
private Class> generateRemoteInterface(Class> localInterface) throws NotFoundException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
// Get all the RMI specific classes.
CtClass ccRemote = pool.get("java.rmi.Remote");
CtClass ccRemoteException = pool.get("java.rmi.RemoteException");
// Generate a new unique name for the remote interface and remember it
// separately to prevent infinite loops.
String name = generateUniqueName();
generatedNames.put(localInterface, name);
// Create a remote interface which is a subclass of java.rmi.Remote just
// like RMI wants.
CtClass cc = pool.makeInterface(name, ccRemote);
// Iterate over all methods of the interface.
for (Method method : localInterface.getDeclaredMethods()) {
// Replace all parameter types which are subclasses of an interface
// marked as being remote by their generated counterparts so that they
// can be passed by reference.
// XXX Subclass relationships are currently ignored.
ArrayList parameters = new ArrayList();
for (Class> parameterType : method.getParameterTypes()) {
if (markedInterfaces.contains(parameterType))
parameters.add(pool.get(getRemoteInterfaceName(parameterType)));
else
parameters.add(pool.get(parameterType.getName()));
}
// Create a new method with the above parameters and make the method throw
// a java.rmi.RemoteException just like RMI wants.
CtMethod cm = CtNewMethod.abstractMethod(
pool.get(method.getReturnType().getName()),
method.getName(),
parameters.toArray(new CtClass[0]),
new CtClass[] { ccRemoteException },
cc);
// Add method to the newly created interface.
cc.addMethod(cm);
}
// Actually create and load the generated remote interface.
Class> remoteInterface = cc.toClass();
//System.out.println("GENERATED REMOTE INTERFACE");
//System.out.println(" LOCAL-TYPE: " + localInterface);
//System.out.println(" REMOTE-TYPE: " + remoteInterface);
return remoteInterface;
}
/**
* Returns a transparent proxy object implementing both, the local and
* the generated remote interface.
* @param object The target object which will receive invocations.
* @param localInterface A plain-old interface class which has been
* marked as being remote before.
* @return The transparent proxy object.
*/
public Object getProxy(Object object, Class> localInterface) {
// Check if there already is a proxy for the given object.
if (createdProxies.containsKey(object))
return createdProxies.get(object);
// Check if the target object itself already is a proxy.
if (createdProxies.containsValue(object))
throw new ProxyException("Given object already is a transparent proxy.");
// Check if the given interface is marked as remote.
if (!markedInterfaces.contains(localInterface))
throw new ProxyException("Given interface is not marked as remote.");
// The given object must either implement the local or the
// generated remote interface.
Class> remoteInterface = getRemoteInterface(localInterface);
if (!localInterface.isInstance(object) && !remoteInterface.isInstance(object))
throw new ProxyException("Given object does not implement the interface.");
// Create a new proxy implementing both of the given interfaces.
// XXX Users might want to proxify several interfaces at once.
Object proxy = Proxy.newProxyInstance(
object.getClass().getClassLoader(),
new Class>[] { localInterface, remoteInterface },
new ProxyInvocationHandler(object));
//System.out.println("GENERATED PROXY FOR " + object.getClass());
//System.out.println(" LOCAL-TYPE: " + localInterface);
//System.out.println(" REMOTE-TYPE: " + remoteInterface);
// Export the created proxy as a remote object. This is necessary
// because proxies will be transported by-reference and thus need
// to be reachable from a remote site.
try {
UnicastRemoteObject.exportObject((Remote) proxy, 0);
} catch (RemoteException e) {
throw new ProxyException("Unable to export created proxy");
}
// Remember the proxy and return it.
createdProxies.put(object, proxy);
return proxy;
}
/**
* All invocations received by a proxy will be dispatched by this
* invocation handler.
*/
private class ProxyInvocationHandler implements InvocationHandler {
private final Object target;
public ProxyInvocationHandler(Object target) {
super();
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class>[] parameterTypes = method.getParameterTypes();
// We need to assemble a new list of parameter types and
// arguments for the target object.
List targetArguments = new ArrayList();
List> targetParameterTypes = new ArrayList>();
// Map parameter types and arguments for target method by
// iterating over all parameters.
for (int i = 0; i < parameterTypes.length; i++) {
Object argument = args[i];
Class> parameterType = parameterTypes[i];
// If the target is a remote object and the parameter is a
// subclass of an interface marked as remote, we need to map
// to the generated remote interface.
// XXX Subclass relationships are currently ignored.
if (Remote.class.isInstance(target) && markedInterfaces.contains(parameterType)) {
targetArguments.add(getProxy(argument, parameterType));
targetParameterTypes.add(getRemoteInterface(parameterType));
continue;
}
// If the target is a local object and the parameter is a
// generated remote interface, we need to map back to the
// original local interface.
if (!Remote.class.isInstance(target) && generatedInterfaces.containsValue(parameterType)) {
Class> localInterface = getOriginalInterface(parameterType);
targetArguments.add(getProxy(argument, localInterface));
targetParameterTypes.add(localInterface);
continue;
}
// The default behavior is to just pass the argument without
// any mapping.
targetArguments.add(argument);
targetParameterTypes.add(parameterType);
}
//System.out.println("PERFORMING INVOCATION INSIDE PROXY");
//System.out.println(" INTERFACE-METHOD: " + method);
// Find the target method.
Method targetMethod = target.getClass().getMethod(
method.getName(),
targetParameterTypes.toArray(new Class>[0]));
//System.out.println(" TARGET-METHOD: " + targetMethod);
// Actually invoke the target method.
// XXX We probably need to proxify the returned object.
Object result = targetMethod.invoke(target, targetArguments.toArray());
return result;
}
}
/**
* All errors happening inside the transport layer will be passed out
* using this exception type. Since it is an unchecked exception we
* ensure "clutter-free" code.
*/
private static class ProxyException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ProxyException(String message) {
super(message);
}
public ProxyException(Throwable cause) {
super(cause);
}
}
}