/*
* This file is open source software.
* See below for copyright and licensing information.
*/
package mm4j;
import java.lang.reflect.*;
import java.util.*;
/**
* Provides dynamic dispatch on all method parameters, also
* known as multi-methods. This is useful, for instance, when
* implementing event-driven code or structuring a program
* explicitly as a state-machine.
* It is likely that we'll have something like this
* natively in future versions of Java supported by the new
* <a href="http://blogs.sun.com/roller/page/gbracha?entry=invokedynamic">invokedynamic opcode</a>.
* <p>
* The latest version of the single implementation file
* <a href="http://gsd.di.uminho.pt/members/jop/mm4j/MultiMethod.java">MultiMethod.java</a>
* and of this document are available
* at the <a href="http://gsd.di.uminho.pt/members/jop/mm4j/">mm4j homepage</a>.
* Check the <a href="#changes">change log</a> for the latest news.
* <p>
* This implementation builds on reflection capabilities in the Java language.
* First, an exact match is attempted. If it does not exist, super-classes
* of parameter values are also tested, starting from right to left. Currently,
* interfaces are not considered, only super-classes. Matched methods are
* cached and reused thus avoiding that the recursive matching
* procedure is called repeatedly. This results in a fairly efficient implementation
* which on the average adds only a single map lookup to the cost of normal
* method invocation.
* <p>
* Super-classes of right-most parameters are tested first, thus naturally
* accommodating the native dynamic dispatch on this as the left-most
* parameter. To better understand searching order, consider the
* following sample program:
* <pre>
* import java.lang.reflect.*;
* import mm4j.*;
*
* public class Test {
* public void m(String a, Integer b) {
* System.out.println("m(String,Integer) with "+
* a.getClass().getName()+" "+b.getClass().getName());
* }
* public void m(Object a, Integer b) {
* System.out.println("m(Object,Integer) with "+
* a.getClass().getName()+" "+b.getClass().getName());
* }
* public void m(String a, Object b) {
* System.out.println("m(String,Object) with "+
* a.getClass().getName()+" "+b.getClass().getName());
* }
* public void m(Object a, Object b) {
* System.out.println("m(Object,Object) with "+
* a.getClass().getName()+" "+b.getClass().getName());
* }
*
* public static void main(String[] args) {
* try {
* Test t=new Test();
* MultiMethod mm=MultiMethod.getMultiMethod(t.getClass(), "m");
*
* mm.invoke(t, "a", 2);
* mm.invoke(t, "a", "b");
* mm.invoke(t, 1, "b");
* mm.invoke(t, 1,2);
*
* Method m=mm.resolve(String.class, Object.class);
* m.invoke(t, "a", 1);
* m=mm.resolve(String.class, String.class);
* m.invoke(t, "a", 1);
* } catch(Exception e) {e.printStackTrace();}
* }
* };</pre>
*
* Notice that it uses the auto-boxing in Java5 for integers.
* The sample program should produce the following output:
* <pre>
* m(String,Integer) with java.lang.String java.lang.Integer
* m(String,Object) with java.lang.String java.lang.String
* m(Object,Object) with java.lang.Integer java.lang.String
* m(Object,Object) with java.lang.Integer java.lang.Integer
* m(String,Object) with java.lang.String java.lang.Integer
* m(String,Object) with java.lang.String java.lang.Integer</pre>
*
* Contrast the result of the second invocation, which
* has (String, String) parameters and matches (String, Object), with the
* result of the fourth invocation, which has (Integer, Integer) parameters
* but matches (Object, Object) and not (Object, Integer).
* <p>
* A particularly interesting idiom is achieved by using a Java5
* variable parameter list to wrap dynamic invocation and exception handling as
* follows:
* <pre>
* import java.lang.reflect.*;
* import java.io.*;
* import mm4j.*;
*
* interface Idiom {
* public void m(Object... args) throws NoSuchMethodException, IOException;
* }
*
* public class IdiomImpl implements Idiom {
* // Alternatives
* public void m(Integer a) { }
* public void m(String a, Object b) throws IOException { }
* public void m(Integer a, String b, Object c) { }
*
* // Wrapper
* public void m(Object... args) throws NoSuchMethodException, IOException {
* try {
* mm.invoke(this, args);
* } catch(InvocationTargetException e) {
* Throwable target=e.getTargetException();
* if (target instanceof IOException)
* throw (IOException)target;
* else if (target instanceof RuntimeException)
* throw (RuntimeException)target;
*
* target.printStackTrace();
* } catch(IllegalAccessException e) {
* e.printStackTrace();
* }
* }
* private static MultiMethod mm=MultiMethod.getMultiMethod(IdiomImpl.class, "m");
*
* // Sample usage
* public static void main(String[] args) {
* try {
* Idiom t=new IdiomImpl();
*
* t.m(3);
* t.m("a", "b");
* t.m(1, "b", 3f);
* } catch(Exception e) {e.printStackTrace();}
* }
* };</pre>
* Most of the effort is related to unwrapping InvocationTargetExceptions
* to each of the possibilities. The resulting client code, in main, is however
* very user friendly.
* <hr>
* <pre>
* mm4j.MultiMethod - Simple multi-methods for Java
* Copyright (C) 2005 Jose Orlando Pereira <jop@di.uminho.pt>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of the University of Minho nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</pre>
* <hr>
* <a name="changes"></a>Changes:
* <ul>
* <li>20051125 - Bug fix for null pointer exceptions in weak hash maps.</li>
* <li>20051115 - Initial public release.</li>
* </ul>
*/
@SuppressWarnings("rawtypes")
public class MultiMethod {
/**
* Lookup a dynamic method in a specific class. This method can
* be used for invocations on instances of any derived class.
* No validation is performed at this time and therefore there is
* no guarantee that invocations will succeed, not even that there
* is a method with the same name.
*
* @param claz a class, or superclass, of the target objects
* @param name the name of the target method
* @return a dynamic method object
*/
public static MultiMethod getMultiMethod(Class claz, String name) {
String mangled=claz.getName()+"+"+name;
MultiMethod dynmeth=(MultiMethod)classCache.get(mangled);
if (dynmeth==null) {
dynmeth=new MultiMethod(claz, name);
classCache.put(mangled, dynmeth);
}
return dynmeth;
}
/**
* Invokes the method represented by this MultiMethod object that
* better fits the specified parameters on the specified object. The
* same effect can be achieved by first resolving the method and
* then using invoke. Note that this method cannot be used to invoke
* target methods with primitive or null arguments.
*
* @param obj the target object instance
* @param args the argument values
* @return the return value of the invoked method
* @throws NoSuchMethodException no matching method found
* @throws IllegalAccessException matching method exists but cannot be accessed
* @throws InvocationTargetException nested exception by target
*/
public final Object invoke(Object obj, Object... args)
throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
return resolveMethod(getTypes(args)).invoke(obj, args);
}
/**
* Resolves the method represented by this MultiMethod object that
* better fits the specified parameters types. The result can be
* used repeatedly to avoid the overhead of matching. This method is
* also able to resolve methods declared with primitive
* arguments. The resulting method can also be invoked with
* null arguments.
*
* @param types the classes of the argument of the method to be searched
* @return a specific Method object
* @throws NoSuchMethodException no matching method found
*/
public final Method resolve(Class... types) throws NoSuchMethodException {
return resolveMethod(copyTypes(types));
}
/**
* Returns the Class object representing the class or interface
* that declares the method family represented by this MultiMethod
* object.
*
* @return the declaring class
*/
public Class getDeclaringClass() {
return claz;
}
/**
* Returns the name of the target methods as a String.
*
* @return the name of the method
*/
public String getName() {
return baseName;
}
public String toString() {
return claz.getName()+"."+baseName+"(...)";
}
private static Map<String,MultiMethod> classCache=new HashMap<String,MultiMethod>();
private Class claz;
private String baseName;
private Map<Signature,Method> methodCache=new WeakHashMap<Signature,Method>();
private MultiMethod(Class claz, String methname) {
this.claz=claz;
this.baseName=methname;
}
private static final class Signature {
private Class[] types;
public Signature(Class[] types) {
this.types=types;
}
public boolean equals(Object other) {
if (other==null)
return false;
return Arrays.equals(types, ((Signature)other).types);
}
public int hashCode() {
return Arrays.hashCode(types);
}
public String toString() {
String name="(";
for(int i=0;i<types.length;i++) {
if (i!=0)
name+=", ";
name+=types[i].getName();
}
return name+")";
}
};
private static final Class[] getTypes(Object[] args) {
Class[] types=new Class[args.length];
for(int i=0;i<args.length;i++)
types[i]=args[i].getClass();
return types;
}
private static final Class[] copyTypes(Class[] args) {
Class[] types=new Class[args.length];
System.arraycopy(args, 0, types, 0, args.length);
return types;
}
@SuppressWarnings("unchecked")
private Method search(Class[] types, int base) {
if (base<0)
return null;
Class argclaz=types[base];
Method method=null;
while(method==null) {
try {
method=claz.getMethod(baseName, types);
} catch(NoSuchMethodException e) {
// see below
}
if (method!=null)
break;
types[base]=types[base].getSuperclass();
if (types[base]==null)
break;
method=search(types, base-1);
}
types[base]=argclaz;
return method;
}
private final Method resolveMethod(Class... types) throws NoSuchMethodException {
Signature sign=new Signature(types);
Method method=(Method)methodCache.get(sign);
if (method==null) {
method=search(types, types.length-1);
// We also cache methods not found.
methodCache.put(sign, method);
}
if (method==null)
throw new NoSuchMethodException("no match found for "+baseName+sign);
return method;
}
};