package org.dcache.cells;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import dmg.cells.nucleus.CellMessage;
import dmg.cells.nucleus.CellMessageReceiver;
import org.dcache.util.ReflectionUtils;
/**
* Automatic dispatch of dCache messages to message handlers.
*/
public class CellMessageDispatcher
{
/** Cached message handlers for fast dispatch. */
private final Map<Class<? extends Serializable>, Collection<Receiver>>
_receivers = new HashMap<>();
/** Name of receiver methods. */
private final String _receiverName;
/**
* Registered message listeners.
*
* @see addMessageListener
*/
private final Collection<CellMessageReceiver> _messageListeners =
new CopyOnWriteArrayList<>();
public CellMessageDispatcher(String receiverName)
{
_receiverName = receiverName;
}
/**
* Returns true if <code>c</code> has a method suitable for
* message delivery.
*/
private boolean hasListener(Class<? extends CellMessageReceiver> c)
{
for (Method m : c.getMethods()) {
if (m.getName().equals(_receiverName)) {
return true;
}
}
return false;
}
/**
* Adds a listener for dCache messages.
*
* The object is scanned for public methods with the signature
* <code>name(Object message)</code> or <code>name(CellMessage
* envelope, Object message)</code>, where <code>name</code> is
* the receiver name, <code>envelope</code> is the envelope
* containing the message.
*
* After registration, all cell messages with a message object
* matching the type of the argument will be send to object.
*
* Message dispatching is performed in the <code>call</code>
* method. If that method is overridden in derivatives, the
* derivative must make sure that <code>call</code> is still
* called.
*/
public void addMessageListener(CellMessageReceiver o)
{
Class<? extends CellMessageReceiver> c = o.getClass();
if (hasListener(c)) {
synchronized (_receivers) {
if (_messageListeners.add(o)) {
_receivers.clear();
}
}
}
}
/**
* Removes a listener previously added with addMessageListener.
*/
public void removeMessageListener(CellMessageReceiver o)
{
synchronized (_receivers) {
if (_messageListeners.remove(o)) {
_receivers.clear();
}
}
}
/**
* Returns the message types that can be reveived by an object of
* the given class.
*/
public Collection<Class<? extends Serializable>> getMessageTypes(Object o)
{
Class<?> c = o.getClass();
Collection<Class<? extends Serializable>> types = new ArrayList<>();
for (Method method : c.getMethods()) {
if (method.getName().equals(_receiverName)) {
Class<?>[] parameterTypes = method.getParameterTypes();
switch (parameterTypes.length) {
case 1:
types.add(parameterTypes[0].asSubclass(Serializable.class));
break;
case 2:
if (CellMessage.class.isAssignableFrom(parameterTypes[0])) {
types.add(parameterTypes[1].asSubclass(Serializable.class));
}
break;
}
}
}
return types;
}
/**
* Finds the objects and methods, in other words the receivers, of
* messages of a given type.
*
* FIXME: This is still not quite the right thing: if you have
* messageArrived(CellMessage, X) and messageArrived(Y) and Y is
* more specific than X, then you would expect the latter to be
* called for message Y. This is not yet the case.
*/
private Collection<Receiver> findReceivers(Class<?> c)
{
synchronized (_receivers) {
Collection<Receiver> receivers = new ArrayList<>();
for (CellMessageReceiver listener : _messageListeners) {
Method m = ReflectionUtils.resolve(listener.getClass(),
_receiverName,
CellMessage.class, c);
if (m != null) {
m.setAccessible(true);
receivers.add(new LongReceiver(listener, m));
continue;
}
m = ReflectionUtils.resolve(listener.getClass(),
_receiverName,
c);
if (m != null) {
m.setAccessible(true);
receivers.add(new ShortReceiver(listener, m));
}
}
return receivers;
}
}
private String multipleRepliesError(Collection<Receiver> receivers, Object message)
{
return String.format("Processing of message [%s] of type %s failed: Multiple replies were generated by %s.", message, message.getClass().getName(), receivers);
}
/**
* Delivers messages to registered message listeners. The return
* value is determined by the following rules (in order):
*
* 1. If any message listener throws an unchecked exception other
* than IllegalArgumentException or IllegalStateException, that
* exception is rethrown.
*
* 2. If more than one message listener returns a non-null value
* or throws a checked exception, IllegalArgumentException or
* IllegalStateException, then a RuntimeException is thrown
* reporting that multiple replies have been generated. This is
* a coding error.
*
* 3. If one message listener returns a non null value then that
* value is returned. If one message listener throws a checked
* exception, IllegalArgumentException or
* IllegalStateException, then that exception is returned.
*
* 4. Otherwise null is returned.
*/
public Object call(CellMessage envelope)
{
Serializable message = envelope.getMessageObject();
Class<? extends Serializable> c = message.getClass();
Collection<Receiver> receivers;
synchronized (_receivers) {
receivers = _receivers.get(c);
if (receivers == null) {
receivers = findReceivers(c);
_receivers.put(c, receivers);
}
}
Object result = null;
for (Receiver receiver : receivers) {
try {
Object obj = receiver.deliver(envelope, message);
if (obj != null) {
if (result != null) {
throw new RuntimeException(multipleRepliesError(receivers, message));
}
result = obj;
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot process message due to access error", e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalArgumentException ||
cause instanceof IllegalStateException ||
receiver.isDeclaredToThrow(cause.getClass())) {
/* We recognize IllegalArgumentException,
* IllegalStateException, and any exception
* declared to be thrown by the method as part of
* the public contract of the receiver and
* propagate the exception back to the client.
*/
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
} else {
/* Since any Throwable not a RuntimeException and
* not an Error should have been declared to be
* thrown by the method, this branch should be
* unreachable.
*/
throw new RuntimeException("Bug: This should have been unreachable. Please report to support@dcache.org.", cause);
}
if (result != null) {
throw new RuntimeException(multipleRepliesError(receivers, message));
}
result = cause;
}
}
return result;
}
/**
* Helper class for message dispatching.
*/
abstract static class Receiver
{
protected final CellMessageReceiver _object;
protected final Method _method;
public Receiver(CellMessageReceiver object, Method method)
{
_object = object;
_method = method;
}
public abstract Object deliver(CellMessage envelope, Object message)
throws IllegalAccessException, InvocationTargetException;
public String toString()
{
return String.format("Object: %1$s; Method: %2$s", _object, _method);
}
public boolean isDeclaredToThrow(Class<?> exceptionClass)
{
for (Class<?> clazz: _method.getExceptionTypes()) {
if (clazz.isAssignableFrom(exceptionClass)) {
return true;
}
}
return false;
}
}
static class ShortReceiver extends Receiver
{
public ShortReceiver(CellMessageReceiver object, Method method)
{
super(object, method);
}
@Override
public Object deliver(CellMessage envelope, Object message)
throws IllegalAccessException, InvocationTargetException
{
return _method.invoke(_object, message);
}
}
static class LongReceiver extends Receiver
{
public LongReceiver(CellMessageReceiver object, Method method)
{
super(object, method);
}
@Override
public Object deliver(CellMessage envelope, Object message)
throws IllegalAccessException, InvocationTargetException
{
return _method.invoke(_object, envelope, message);
}
}
}