package org.springmodules.javaspaces; import java.io.Serializable; import java.lang.reflect.Method; import net.jini.core.lease.Lease; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.LazyLoader; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.remoting.RemoteAccessException; import org.springmodules.javaspaces.entry.AbstractMethodCallEntry; import org.springmodules.javaspaces.entry.MethodResultEntry; import org.springmodules.javaspaces.entry.RunnableMethodCallEntry; import org.springmodules.javaspaces.entry.ServiceSeekingMethodCallEntry; import org.springmodules.javaspaces.entry.UidFactory; import org.springmodules.javaspaces.entry.support.DefaultUidFactory; /** * Generic invoker for Java space-implemented methods. It allows method * invocations on a given proxy interface to be transparently implemented as * write/take operations of a generic entry to a JavaSpace. This is used as the * terminal interceptor for a Spring AOP proxy. See the Spring implementation of * local and remote EJB proxies, which illustrates the same concepts. * <p> * This class supports two modes of JavaSpace usage: * <li>The endpoint hosts the service, and only the method to be invoked and * arguments are passed around the network. This is somewhat like a SLSB * approach to remoting, although there is no assumption as to where the * endpoint is. * <li>The endpoint does not necessarily host the service. The code to execute * as well as the method and arguments are shipped to the node that takes from * the space. This is a "runnable entry" approach. To enable this, simply set * the serializableTarget property on this object to an object that can be * serialized to implement the method. Typically this will be dependency * injected with Spring, allowing the object to be configured. Make sure this * object is serializable. Alternatively, override the serializableTarget() * method to return a custom object. * <p> * This interceptor is generic. One interceptor instance can serve multiple * methods. * * @author Rod Johnson * @author Costin Leau */ public class JavaSpaceInterceptor implements MethodInterceptor { private static final Log log = LogFactory.getLog(JavaSpaceInterceptor.class); public class LazyResult { AbstractMethodCallEntry call; public LazyResult(AbstractMethodCallEntry call) { this.call = call; } public Object getResult() { try { return handleResultRetrieval(call); } catch (Throwable throwable) { // TODO: handle this properly throw new RuntimeException(throwable); } } } private JavaSpaceTemplate jsTemplate; /** * TODO this parameter causes an object to be serialized Can inject it */ private Object serializableTarget; private long timeoutMillis = 100; private boolean synchronous = true; private UidFactory uidFactory = new DefaultUidFactory(); public JavaSpaceInterceptor() { } public void setTimeoutMillis(long timeoutMillis) { this.timeoutMillis = timeoutMillis; } /** * The target will be set by dependency injection. If it is set (ie is * non-null) a RunnableMethodCallEntry will be created that includes the * serialized target as well as the method and arguments. If this property * is not set, the endpoint will be assumed to have a service on which the * method invocation should be made. * * @param serializableTarget */ public void setSerializableTarget(Object serializableTarget) { this.serializableTarget = serializableTarget; } public JavaSpaceInterceptor(JavaSpaceTemplate jsTemplate) throws Exception { setJavaSpaceTemplate(jsTemplate); } /** * Set the JavaSpaceTemplate Spring helper used to simplify working with the * JavaSpace API. * * @param jsTemplate */ public void setJavaSpaceTemplate(JavaSpaceTemplate jsTemplate) { this.jsTemplate = jsTemplate; } /** * Implementation of the AOP Alliance MethodInterceptor interface. This will * be terminal interceptor in the AOP interceptor chain. It creates a * generic entry representing the method call, writes it to the space and * returns a MethodResultEntry in response. The call can be blocking * (synchronous) or non-blocking (asynchronous). */ public Object invoke(MethodInvocation mi) throws Throwable { AbstractMethodCallEntry call = null; Object target = getSerializableTarget(mi); // TODO Could also decide whether to serializable target // based on a method annotation if (target == null) { call = createServiceSeekingMethodCallEntry(mi.getMethod(), mi.getArguments()); } else { call = createRunnableMethodCallEntry(mi.getMethod(), mi.getArguments(), target); } if (log.isDebugEnabled()) log.debug("Invoke: Call is " + call); jsTemplate.write(call, timeoutMillis); // if synch do a blocking waiting if (synchronous) return handleResultRetrieval(call); // else return a lazy result return createLazyResult(call); } /** * Gets the ServiceSeekingMethodCallEntry * @param method the method * @param args the method agrs * @return ServiceSeekingMethodCallEntry */ protected ServiceSeekingMethodCallEntry createServiceSeekingMethodCallEntry(Method method, Object[] args){ return new ServiceSeekingMethodCallEntry(method, args, getUidFactory().generateUid()); } /** * Gets RunnableMethodCallEntry * @param method the method * @param args the method arg's * @param target the target * @return RunnableMethodCallEntry */ protected RunnableMethodCallEntry createRunnableMethodCallEntry(Method method, Object[] args, Object target){ return new RunnableMethodCallEntry(method, args, target, getUidFactory().generateUid()); } /** * Create a lazy proxy through CGLib. * * @param call * @return */ protected Object createLazyResult(final AbstractMethodCallEntry call) { // CGLib usage LazyLoader lazyCallback = new LazyLoader() { public Object loadObject() throws Exception { if (log.isTraceEnabled()) log.trace("creating lazy proxy"); try { return handleResultRetrieval(call); } catch (Throwable e) { throw new RuntimeException(e); } } }; return Enhancer.create(call.getMethod().getReturnType(), lazyCallback); } protected Object handleResultRetrieval(AbstractMethodCallEntry call) throws Throwable { MethodResultEntry result = null; result = takeResult(call); // If it's an exception, throw it to the caller. The // exception will propogate up the call chain. if (!result.successful()) { throw result.getFailure(); } else { return result.getResult(); } } protected MethodResultEntry takeResult(AbstractMethodCallEntry call) throws Throwable { MethodResultEntry result = null; MethodResultEntry template = createMethodResultEntry(call); result = (MethodResultEntry) jsTemplate.take(template, timeoutMillis); if (log.isDebugEnabled()) log.debug("Result=" + result); // if no result found try to clean up and thrown an exception if (result == null) { // TODO this is not being tested jsTemplate.takeIfExists(call, timeoutMillis); throw new RemoteAccessException("Couldn't get result for " + call); } return result; } /** * Create the specific method result entry * @param call the call entry method * @return the method result entry. */ protected MethodResultEntry createMethodResultEntry(AbstractMethodCallEntry call){ return new MethodResultEntry((Method) null, call.uid, null); } /** * Subclasses can override this method if desired to obtain a target in a * different way, potentially based on the method to be invoked. This * implementation simply returns the value of the serializableTarget bean * property. * * @return the target to include in a RunnableMethodCallEntry. If the return * value is null, a ServiceSeekingMethodCallEntry will be created, * which requires endpoints to host the necessary service. */ protected Object getSerializableTarget(MethodInvocation mi) { return serializableTarget; } /** * @return Returns the synchronous. */ public boolean isSynchronous() { return synchronous; } /** * @param synchronous * The synchronous to set. */ public void setSynchronous(boolean synchronous) { this.synchronous = synchronous; } /** * @return Returns the uidFactory. */ public UidFactory getUidFactory() { return uidFactory; } /** * Sets the uid factory to use for generating Entry uids. By default, * DefaultUidFactory is used. * * @param uidFactory * The uidFactory to set. */ public void setUidFactory(UidFactory uidFactory) { this.uidFactory = uidFactory; } }