Commits

Jason S committed 79fb846

added argument/method compatibility measures

Comments (0)

Files changed (5)

src/com/example/test/reflect2/ArgumentCompatibility.java

+package com.example.test.reflect2;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author jason_s
+ * helper enum for checking argument compatibility
+ */
+public enum ArgumentCompatibility 
+{
+	/** Expected and actual argument classes are identical */
+	PERFECT(0,true), 
+	/** Expected argument: primitive 
+	 *  actual argument: boxed primitive of the same type */
+	UNBOXED_PRIMITIVES(1,true),
+	/** Expected argument: superclass
+	 *  actual argument: subclass of the superclass */ 
+	SUPERCLASS(2,true),
+	/** Expected argument: primitive (boxed or unboxed)
+	 *  actual argument: another primitive (boxed or unboxed) */ 
+	PRIMITIVE_CAST(3,true),
+	
+	INCOMPATIBLE(10,false),
+	INCOMPATIBLE_METHOD_NAME(10,false),
+	INCOMPATIBLE_ARGUMENT_LENGTH(10,false),
+	INCOMPATIBLE_ARGUMENT_TYPE(10,false),
+	INCOMPATIBLE_PRIMITIVE_WITH_NULL(10,false),
+	;
+	
+	final private int rank;
+	final private boolean ok;
+	/** @return relative rank */
+	public int getRank() { return this.rank; }
+	/** @return true if compatible */ 
+	public boolean isCompatible() { return this.ok; } 
+	ArgumentCompatibility(int distance, boolean ok) {
+		this.rank = distance;
+		this.ok = ok;
+	}
+	
+	final static public Comparator<ArgumentCompatibility> RANK_COMPARATOR
+	= new Comparator<ArgumentCompatibility>() 
+	{
+		@Override public int compare(ArgumentCompatibility c1, ArgumentCompatibility c2)
+		{
+			if (c1.getRank() == c2.getRank())
+				return 0;
+			else if (c1.getRank() < c2.getRank())
+				return 1;
+			else
+				return -1;
+		}		
+	};
+	
+	ArgumentCompatibility union(ArgumentCompatibility other)
+	{
+		if (isCloserThan(other))
+			return other;
+		else
+			return this;
+	}
+	
+	boolean isCloserThan(ArgumentCompatibility other)
+	{
+		return other.getRank() > getRank();
+	}
+	
+	static final private Map<Class<?>, Class<?>> primitiveMap = createPrimitiveMap();
+	private static Map<Class<?>, Class<?>> createPrimitiveMap() {
+		Map<Class<?>, Class<?>> map = new HashMap<Class<?>, Class<?>>();
+		Class<?>[] unboxed = {Long.TYPE, Integer.TYPE, Short.TYPE, Byte.TYPE, Double.TYPE, Float.TYPE, Boolean.TYPE, Character.TYPE, Void.TYPE};
+		Class<?>[] boxed = {Long.class, Integer.class, Short.class, Byte.class, Double.class, Float.class, Boolean.class, Character.class, Void.class};
+		for (int i = 0; i < unboxed.length; ++i)
+		{
+			map.put(unboxed[i], unboxed[i]);
+			map.put(boxed[i], unboxed[i]);
+		}
+		return Collections.unmodifiableMap(map);
+	}
+	static public Class<?> getPrimitiveClassFor(Class<?> sourceType) {
+		return primitiveMap.get(sourceType);
+	}    	
+
+	
+	
+	static public ArgumentCompatibility checkConstructor(Constructor<?> con, Class<?>[] argsClass, Object[] args) {
+		Class<?>[] types = con.getParameterTypes();
+		if (types.length != argsClass.length)
+			return INCOMPATIBLE_ARGUMENT_LENGTH;
+		
+		ArgumentCompatibility c = PERFECT;
+		for (int i = 0; i < types.length; i++) 
+		{
+			ArgumentCompatibility ci = checkType(types[i], argsClass[i], args[i]);
+			if (!ci.isCompatible())
+				return ci;
+			c = c.union(ci);
+		}
+		return c;
+	}
+	
+	static public ArgumentCompatibility checkType(Class<?> destinationType, Class<?> sourceType, Object arg)
+	{
+		if (destinationType.isPrimitive())
+		{
+			if (arg == null)
+				return INCOMPATIBLE_PRIMITIVE_WITH_NULL;
+			
+			if (destinationType.equals(sourceType))
+				return PERFECT;
+			Class<?> prs = getPrimitiveClassFor(sourceType);
+			if (prs == null)
+				return INCOMPATIBLE_ARGUMENT_TYPE;
+			if (destinationType == prs)
+				return UNBOXED_PRIMITIVES;
+		}
+		else
+		{
+			if (destinationType.equals(sourceType))
+				return PERFECT;
+			else if (destinationType.isAssignableFrom(sourceType))
+				return SUPERCLASS;
+		}
+		
+		Class<?> prd = getPrimitiveClassFor(destinationType);
+		Class<?> prs = getPrimitiveClassFor(sourceType);
+		if (prd == null || prs == null)
+		{
+			return INCOMPATIBLE_ARGUMENT_TYPE;
+		}
+		
+		/* 
+		 * OK, at this point both are primitives and 
+		 * are not equal types.
+		 */    		
+		 return PRIMITIVE_CAST;
+	}
+}    	

src/com/example/test/reflect2/InvokeTestHarness.java

 	static public class TestItemListBuilder
 	{
 		final private List<AbstractTestItem<?>> items;
+		private Object obj = null;
+		private String methodName = null;
 		public TestItemListBuilder()
 		{
 			this.items = new ArrayList<AbstractTestItem<?>>();
 		}
-		public TestItemListBuilder addTest(Object obj, String method, Object expectedReturnValue, Object... args)
+		public TestItemListBuilder setObject(Object obj) { 	this.obj = obj; return this; }
+		public TestItemListBuilder setMethodName(String method) { this.methodName = method; return this; }
+		public TestItemListBuilder addTestItem(Object obj1, String method1, Object expectedReturnValue, Object... args)
 		{
-			this.items.add(new TestItem(obj, method, expectedReturnValue, args));
+			this.items.add(new TestItem(obj1, method1, expectedReturnValue, args));
 			return this;
 		}
+		public TestItemListBuilder addItem(Object expectedReturnValue, Object... args)
+		{
+			this.items.add(new TestItem(this.obj, this.methodName, expectedReturnValue, args));
+			return this;			
+		}
 		public TestItemListBuilder addRecordingTest(MethodRecordingObject obj, String method, String expectedSignature, Object... args)
 		{
 			this.items.add(new MethodRecordingTestItem(obj, method, expectedSignature, args));
 			return this;
 		}
+		public TestItemListBuilder addRecordingItem(String expectedSignature, Object... args)
+		{
+			MethodRecordingObject mro = (MethodRecordingObject) this.obj;
+			this.items.add(new MethodRecordingTestItem(mro, this.methodName, expectedSignature, args));
+			return this;
+		}
 		public List<AbstractTestItem<?>> build() { return this.items; }
 	}
 	

src/com/example/test/reflect2/MethodCompatibility.java

+/**
+ * 
+ */
+package com.example.test.reflect2;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MethodCompatibility implements Comparable<MethodCompatibility>
+{
+	final private ArgumentCompatibility worstArgumentCompatibility;
+	final private int multiplicity;
+	final private int ancestorDistance;
+	
+	public MethodCompatibility(ArgumentCompatibility worst, int multiplicity, int ancestorDistance)
+	{
+		this.worstArgumentCompatibility = worst;
+		this.multiplicity = multiplicity;
+		this.ancestorDistance = ancestorDistance;
+	}
+	
+	@Override public int compareTo(MethodCompatibility other) {
+		int cmp = ArgumentCompatibility.RANK_COMPARATOR.compare(
+				this.worstArgumentCompatibility, other.getWorstArgumentCompatibility());
+		if (cmp != 0)
+			return cmp;
+		if (this.multiplicity < other.getMultiplicity())
+			return -1;
+		if (this.multiplicity > other.getMultiplicity())
+			return 1;
+		if (this.ancestorDistance < other.getAncestorDistance())
+			return -1;
+		if (this.ancestorDistance < other.getAncestorDistance())
+			return 1;
+		return 0;
+	}
+	
+	public int getAncestorDistance() { return this.ancestorDistance; }
+	public int getMultiplicity() { return this.multiplicity; }
+	public ArgumentCompatibility getWorstArgumentCompatibility() { return this.worstArgumentCompatibility; }
+	
+	static final public MethodCompatibility incompatibleArgumentLength =
+		new MethodCompatibility(ArgumentCompatibility.INCOMPATIBLE_ARGUMENT_LENGTH, 0, 0);
+	static final private Map<ArgumentCompatibility, MethodCompatibility> incompatibleMap = createIncompatibleMap();
+	private static Map<ArgumentCompatibility, MethodCompatibility> createIncompatibleMap() {
+		Map<ArgumentCompatibility, MethodCompatibility> map = new HashMap<ArgumentCompatibility, MethodCompatibility>();
+		for (ArgumentCompatibility c : ArgumentCompatibility.values())
+			map.put(c, new MethodCompatibility(c, 0, 0));
+		return Collections.unmodifiableMap(map);
+	}
+	
+	static public MethodCompatibility checkMethod(int ancestorDistance, Method m, Object[] args) {
+		final int N = args.length;
+		Class<?>[] argsClass = new Class<?>[N];
+		for (int i = 0; i < N; ++i)
+		{
+			argsClass[i] = args[i].getClass();
+		}
+		return checkMethod(ancestorDistance, m, argsClass, args);
+	}
+	
+	public static MethodCompatibility checkMethod(int ancestorDistance, Method m, Class<?>[] argsClass, Object[] args) {
+		Class<?>[] types = m.getParameterTypes();
+		if (types.length != argsClass.length)
+			return incompatibleArgumentLength;
+		
+		ArgumentCompatibility mc = ArgumentCompatibility.PERFECT;
+		int multiplicity = 0;
+		for (int i = 0; i < types.length; i++) 
+		{
+			ArgumentCompatibility ac = ArgumentCompatibility.checkType(types[i], argsClass[i], args[i]);
+			if (!ac.isCompatible())
+				return incompatibleMap.get(ac);
+			if (mc.isCloserThan(ac))
+			{
+				mc = ac;
+				multiplicity = 1;
+			}
+			else if (!ac.isCloserThan(mc))
+				++multiplicity;
+		}
+		return new MethodCompatibility(mc, multiplicity, ancestorDistance);
+	}
+	@Override public String toString() { return getWorstArgumentCompatibility()+": x"+getMultiplicity()+" <"+getAncestorDistance()+">"; }
+	
+}

src/com/example/test/reflect2/ReflectInvocations.java

 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
 import java.util.Set;
 
 public class ReflectInvocations {
 	{
 		Class<?> cl = obj.getClass();
 		
-		Set<Method> methods = new HashSet<Method>();
-		collectCompatibleMethods(methods, cl, methodName, arguments);
+		Map<Method, MethodCompatibility> methods = new HashMap<Method, MethodCompatibility>();
+		MethodCompatibility cbest = MethodCompatibility.incompatibleArgumentLength;
+		cbest = collectCompatibleMethods(cbest, 0, methods, cl, methodName, arguments);
 		if (methods.isEmpty())
 			throw new RuntimeException("No method "+methodName+" found that is compatible with the specified arguments");
-		return methods.iterator().next();
+		Iterator<Map.Entry<Method, MethodCompatibility>> it = methods.entrySet().iterator();
+		while (it.hasNext())
+		{
+			Map.Entry<Method, MethodCompatibility> entry = it.next();
+			if (cbest.compareTo(entry.getValue()) < 0)
+				it.remove();
+		}
+		if (methods.size() > 1)
+			throw new RuntimeException("Ambiguous method call: "+methods.size()+" of compatibility "+cbest+":\n"+methods);
+		return methods.keySet().iterator().next();
 	}
 
-	static private void collectCompatibleMethods(Set<Method> methods, 
+	static private MethodCompatibility collectCompatibleMethods(
+			MethodCompatibility init,
+			int ancestorDistance,
+			Map<Method, MethodCompatibility> methods, 
 			Class<?> cl, String methodName, Object[] arguments) 
 	{
+		MethodCompatibility cbest = init;
 		for (Method m : cl.getDeclaredMethods())
 		{
-			if (!methods.contains(m)
+			if (!methods.containsKey(m)
 					&& methodName.equals(m.getName())
-					&& isPublic(m)
-					&& isCompatibleMethod(m, arguments))
+					&& isPublic(m))
 			{
-				methods.add(m);
+				MethodCompatibility c = MethodCompatibility.checkMethod(ancestorDistance, m, arguments);
+				if (c.getWorstArgumentCompatibility().isCompatible())
+				{
+					int cmp = c.compareTo(cbest);
+					if (cmp > 0)
+						cbest = c;
+					if (cmp >= 0)
+						methods.put(m, c);
+				}
 			}
 		}
 		Class<?> superclass = cl.getSuperclass();
 		if (superclass != null)
-			collectCompatibleMethods(methods, superclass, methodName, arguments);
+			cbest = collectCompatibleMethods(cbest, ancestorDistance+1, methods, superclass, methodName, arguments);
 		
 		for (Class<?> cli : cl.getInterfaces())
 		{
-			collectCompatibleMethods(methods, cli, methodName, arguments);
+			cbest = collectCompatibleMethods(cbest, ancestorDistance+1, methods, cli, methodName, arguments);
 		}
+		return cbest;
 	}
 
 	static private boolean isPublic(Method m) {

src/com/example/test/reflect2/Test1.java

 package com.example.test.reflect2;
 
 import java.util.List;
+import com.example.test.reflect1.IFoo;
 import com.example.test.reflect1.MethodRecordingObject;
 import com.example.test.reflect1.ThingFactory;
 
 		for (int i = 1; i <= n; ++i)
 			thing[i] = factory.getThing(i);
 		
-		String plainMethod = "compute";
 		Object object0 = new Object();
 		Test1 test = new Test1(
 			InvokeTestHarness.testItemsBuilder()
-				.addTest(thing[1], plainMethod, 1, object0, "hi")
-				.addTest(thing[1], plainMethod, 2, object0, object0)
-				.addTest(thing[1], plainMethod, 3, 1, "hi")
+				.setObject(thing[1]).setMethodName("compute")
+				.addItem(    1, object0, "hi")
+				.addItem(    2, object0, object0)
+				.addItem(    3, 1, "hi")				
+				.addItem(   11, thing[1], 33)
+				.addItem("S12", object0, 3.14159)
+				.addItem(   13, "hi there", 22)
 				.build());
 		test.run();
 	}