/*
* Copyright (C) 2011 Virginia Tech Department of Computer Science
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sofia.internal.events;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//-------------------------------------------------------------------------
/**
* Represents a reflective dispatcher with an internal cache of looked-up
* Method objects.
*
* @author Tony Allevato, Stephen Edwards
*/
public class EventDispatcher
{
//~ Fields ................................................................
// The name of the method that this dispatcher calls.
private String methodName;
// A cache of matching method transformers for a particular class.
private HashMap<CacheKey,
List<MethodTransformer>> transformerCache;
private static final Map<Class<?>, Class<?>> wrapperEquivalent =
new HashMap<Class<?>, Class<?>>();
static {
wrapperEquivalent.put(Boolean.class, boolean.class);
wrapperEquivalent.put(Byte.class, byte.class);
wrapperEquivalent.put(Character.class, char.class);
wrapperEquivalent.put(Double.class, double.class);
wrapperEquivalent.put(Float.class, float.class);
wrapperEquivalent.put(Integer.class, int.class);
wrapperEquivalent.put(Short.class, short.class);
wrapperEquivalent.put(boolean.class, Boolean.class);
wrapperEquivalent.put(byte.class, Byte.class);
wrapperEquivalent.put(char.class, Character.class);
wrapperEquivalent.put(double.class, Double.class);
wrapperEquivalent.put(float.class, Float.class);
wrapperEquivalent.put(int.class, Integer.class);
wrapperEquivalent.put(short.class, Short.class);
}
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new event dispatcher with the specified method name.
*
* @param method the name of the method that this dispatcher will call
*/
public EventDispatcher(String method)
{
methodName = method;
transformerCache =
new HashMap<CacheKey, List<MethodTransformer>>();
}
//~ Public methods ........................................................
// ----------------------------------------------------------
/**
* Gets a value indicating whether a receiver has a method that satisfies
* this dispatcher, given the specified arguments.
*
* @param receiver the receiver of the method call
* @param args the arguments that would be passed to the method
* @return true if the receiver has a method that satisfies this
* dispatcher, otherwise false
*/
public boolean isSupportedBy(Object receiver, Object... args)
{
List<MethodTransformer> transformers =
getMethodTransformers(receiver, args);
return !transformers.isEmpty();
}
// ----------------------------------------------------------
/**
* Dispatches the event to the specified receiver, walking up the
* containment hierarchy as needed to notify parents of the event.
*
* @param receiver the receiver of the method call
* @param args the arguments that would be passed to the method
* @return true if the event should not be dispatched further (because one
* of the handlers returned true), false if dispatch should continue
*/
public boolean dispatch(Object receiver, Object... args)
{
List<MethodTransformer> transformers =
getMethodTransformers(receiver, args);
if (!transformers.isEmpty())
{
for (MethodTransformer transformer : transformers)
{
Object result = invokeTransformer(transformer, receiver, args);
if (Boolean.TRUE.equals(result))
{
return true;
}
}
}
return false;
}
// ----------------------------------------------------------
/**
* Transforms an argument list using the specified transformer and invokes
* the method. This is a hook where subclasses can override the logic, if
* necessary.
*
* @param transformer the transformer
* @param receiver the receiving object
* @param args the arguments to the method
* @return the result of invoking the method
*/
protected Object invokeTransformer(MethodTransformer transformer,
Object receiver, Object... args)
{
return transformer.invoke(receiver, args);
}
// ----------------------------------------------------------
/**
* TODO document
*
* @param receiver
* @param argTypes
* @return
*/
protected List<MethodTransformer> lookupTransformers(
Object receiver, List<Class<?>> argTypes)
{
List<MethodTransformer> transformers =
new ArrayList<MethodTransformer>();
Method method = lookupMethod(receiver, argTypes);
if (method != null)
{
MethodTransformer identity = new MethodTransformer(
Arrays.asList(method.getParameterTypes()));
identity.methodCache.put(receiver.getClass(), method);
transformers.add(identity);
}
return transformers;
}
// ------------------------------------------------------
protected Method lookupMethod(Object receiver, List<Class<?>> argTypes)
{
//System.out.println("Looking for "
// + receiver.getClass().getCanonicalName() + "."
// + methodName + "(" + argTypes.toString() + ")...");
Class<?> clazz = receiver.getClass();
Method bestMatch = null;
int[] bestScore = new int[argTypes.size()];
int[] nextScore = new int[argTypes.size()];
while (clazz != null)
{
for (Method candidate : clazz.getDeclaredMethods())
{
if (methodName.equals(candidate.getName()))
{
try
{
//System.out.println(" checking "
// + candidate.toGenericString());
// Check this method and leave results in nextScore
scoreMethod(candidate, argTypes, nextScore);
if (bestMatch == null
|| isBetter(bestScore, nextScore))
{
bestMatch = candidate;
// Rotate nextScore into the bestScore position
// then reuse the old bestScore array next iter.
int[] tmp = bestScore;
bestScore = nextScore;
nextScore = tmp;
}
}
catch (IllegalArgumentException e)
{
// This method isn't compatible with the
// given arguments, so ignore it.
}
}
}
clazz = clazz.getSuperclass();
}
if (bestMatch != null)
{
//System.out.println(" ...found!");
}
return bestMatch;
}
// ----------------------------------------------------------
private List<Class<?>> classesForObjects(Object... objects)
{
List<Class<?>> types = new ArrayList<Class<?>>(objects.length);
for (int i = 0; i < objects.length; i++)
{
if (objects[i] == null)
{
types.add(null);
}
else
{
types.add(objects[i].getClass());
}
}
return types;
}
// ----------------------------------------------------------
private List<MethodTransformer> getMethodTransformers(Object receiver,
Object... args)
{
List<MethodTransformer> transformers = null;
CacheKey key = new CacheKey(receiver, args);
if (!transformerCache.containsKey(key))
{
transformers = lookupTransformers(
receiver, key.getParameterTypes());
transformerCache.put(key, transformers);
}
else
{
transformers = transformerCache.get(key);
}
return transformers;
}
// ----------------------------------------------------------
private int argConversionCost(
Class<?> actualParamType, Class<?> formalParamType)
throws IllegalArgumentException
{
if (actualParamType == null && !formalParamType.isPrimitive())
{
// Assume that a null value can go into anything that isn't a
// primitive type.
return 0;
}
if (formalParamType.equals(actualParamType))
{
// Identical types
return 0;
}
if (formalParamType.equals(wrapperEquivalent.get(actualParamType)))
{
// Treat auto-boxing/unboxing as free
return 0;
}
if (formalParamType.isAssignableFrom(actualParamType))
{
// Calculate distance
int distance = 1;
outerLoop:
while (actualParamType != null)
{
if (formalParamType.equals(actualParamType.getSuperclass()))
{
return distance;
}
for (Class<?> iface : actualParamType.getInterfaces())
{
if (iface.equals(formalParamType))
{
return distance;
}
}
for (Class<?> iface : actualParamType.getInterfaces())
{
if (formalParamType.isAssignableFrom(iface))
{
actualParamType = iface;
distance++;
continue outerLoop;
}
}
actualParamType = actualParamType.getSuperclass();
distance++;
}
return distance;
}
throw new IllegalArgumentException("incompatible types");
}
// ----------------------------------------------------------
private void scoreMethod(
Method m, List<Class<?>> actualArgTypes, int[] scores)
throws IllegalArgumentException
{
Class<?>[] formals = m.getParameterTypes();
if (formals.length != actualArgTypes.size())
{
throw new IllegalArgumentException(
"incompatible number of arguments");
}
for (int i = 0; i < formals.length; i++)
{
scores[i] = argConversionCost(actualArgTypes.get(i), formals[i]);
}
}
// ----------------------------------------------------------
private boolean isBetter(int[] oldScore, int[] newScore)
{
int oldTotal = 0;
for (int i : oldScore)
{
oldTotal += i;
}
int newTotal = 0;
for (int i : newScore)
{
newTotal += i;
}
if (oldTotal != newTotal)
{
return newTotal < oldTotal;
}
else
{
for (int i = 0; i < oldScore.length; i++)
{
if (oldScore[i] != newScore[i])
{
return newScore[i] < oldScore[i];
}
}
return false;
}
}
//~ Inner classes .........................................................
// ------------------------------------------------------
/**
* Subclasses of {@code EventDispatcher} should subclass this internally in
* order to support multiple method signatures.
*/
protected class MethodTransformer
{
//~ Fields ............................................................
protected final List<Class<?>> argTypes;
protected final Map<Class<?>, Method> methodCache;
//~ Constructors ......................................................
// ------------------------------------------------------
public MethodTransformer(Class<?>... argTypes)
{
this(Arrays.asList(argTypes));
}
// ------------------------------------------------------
public MethodTransformer(List<Class<?>> argTypes)
{
this.argTypes = argTypes;
this.methodCache = new HashMap<Class<?>, Method>();
}
//~ Methods ...........................................................
// ------------------------------------------------------
public void addIfSupportedBy(Object receiver,
List<MethodTransformer> transformers)
{
Method method = lookupMethod(receiver, argTypes);
if (method != null)
{
methodCache.put(receiver.getClass(), method);
transformers.add(this);
}
}
// ------------------------------------------------------
public boolean isCompatible(Object... args)
{
//System.out.println("---\nChecking compat of " + argTypes
// + " and " + Arrays.toString(args));
if (args.length != argTypes.size())
{
//System.out.println(" Not the same number.");
return false;
}
else
{
for (int i = 0; i < args.length; i++)
{
Class<?> formal = argTypes.get(i);
Class<?> actual = args[i] != null ?
args[i].getClass() : null;
if (formal != null && actual != null &&
!formal.isAssignableFrom(actual))
{
//System.out.println(" Not compatible.");
return false;
}
}
//System.out.println(" Compatible.");
return true;
}
}
// ------------------------------------------------------
public Object invoke(Object receiver, Object... args)
{
try
{
//System.out.println("Invoking " + method.toGenericString()
// + " with " + Arrays.toString(args));
return methodCache.get(receiver.getClass()).invoke(
receiver, transform(args));
}
catch (InvocationTargetException e)
{
Throwable cause = e.getCause();
if (cause instanceof Error)
{
throw (Error) cause;
}
else if (cause instanceof RuntimeException)
{
throw (RuntimeException) cause;
}
else
{
throw new RuntimeException(cause);
}
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
// ----------------------------------------------------------
/**
* Transforms the specified argument list to one that will be passed to
* the handler method. By default, it returns the same argument list.
* Override this to transform the arguments (such as from a MotionEvent
* to floats for x/y).
*
* @param args the arguments
* @return the transformed argument list
*/
protected Object[] transform(Object... args)
{
return args;
}
}
// ----------------------------------------------------------
private class CacheKey
{
private Class<?> receiverType;
private List<Class<?>> argTypes;
// ----------------------------------------------------------
public CacheKey(Object receiver, Object... args)
{
receiverType = receiver.getClass();
argTypes = classesForObjects(args);
}
// ----------------------------------------------------------
@SuppressWarnings("unused")
public Class<?> getReceiverType()
{
return receiverType;
}
// ----------------------------------------------------------
public List<Class<?>> getParameterTypes()
{
return argTypes;
}
// ----------------------------------------------------------
public boolean equals(Object other)
{
if (other instanceof CacheKey)
{
CacheKey otherMethod = (CacheKey) other;
return receiverType.equals(otherMethod.receiverType) &&
argTypes.equals(otherMethod.argTypes);
}
else
{
return false;
}
}
// ----------------------------------------------------------
public int hashCode()
{
return receiverType.hashCode() ^ (argTypes.hashCode() << 13);
}
}
}