/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.aries.unittest.mocks; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * <p>This class represents a method call that has been or is expected to be * made. It encapsulates the class that the call was made on, the method * that was invoked and the arguments passed.</p> */ public final class MethodCall { /** An empty object array */ private static Object[] EMPTY_OBJECT_ARRAY = new Object[0]; /** The name of the class invoked */ private String _className; /** The array of interfaces implemented by the class */ private Class<?>[] _interfaces = new Class[0]; /** The method invoked */ private String _methodName; /** The arguments passed */ private Object[] _arguments = EMPTY_OBJECT_ARRAY; /** The object invoked */ private Object _invokedObject; /** A list of comparators to use, instead of the objects .equals methods */ private static Map<Class<?>, Comparator<?>> equalsHelpers = new HashMap<Class<?>, Comparator<?>>(); /* ------------------------------------------------------------------------ */ /* MethodCall method /* ------------------------------------------------------------------------ */ /** * This constructor allows a MethodCall to be created when the class can be * located statically, rather than dynamically. * * @param clazz The class. * @param methodName The method name. * @param arguments The arguments. */ public MethodCall(Class<?> clazz, String methodName, Object ... arguments) { _className = clazz.getName(); _methodName = methodName; _arguments = arguments; } /* ------------------------------------------------------------------------ */ /* MethodCall method /* ------------------------------------------------------------------------ */ /** * This method is used by the Skeleton in order create an instance of a * MethodCall representing an invoked interface. * * NOTE: If possible changing this so the constructor does not need to be * default visibility would be good, given the problems with default * visibility. * * @param invokedObject The object that was invoked. * @param methodName The name of the method invoked. * @param arguments The arguments passed. */ MethodCall(Object invokedObject, String methodName, Object ... arguments) { _className = invokedObject.getClass().getName(); _interfaces = invokedObject.getClass().getInterfaces(); _methodName = methodName; this._arguments = (arguments == null) ? EMPTY_OBJECT_ARRAY : arguments; _invokedObject = invokedObject; } /* ------------------------------------------------------------------------ */ /* getArguments method /* ------------------------------------------------------------------------ */ /** * This method returns the arguments. * * @return The arguments. */ public Object[] getArguments() { return _arguments; } /* ------------------------------------------------------------------------ */ /* getClassName method /* ------------------------------------------------------------------------ */ /** * Returns the name of the class the method was invoked or was defined on. * * @return the classname. */ public String getClassName() { return _className; } /* ------------------------------------------------------------------------ */ /* getMethodName method /* ------------------------------------------------------------------------ */ /** * Returns the name of the method that was (or will be) invoked. * * @return the method name */ public String getMethodName() { return _methodName; } /* ------------------------------------------------------------------------ */ /* checkClassName method /* ------------------------------------------------------------------------ */ /** * This method checks that the class names specified in the method call are * compatible, i.e. one is a superclass of the other. * * @param one The first method call. * @param two The second method call. * @return true if the classes can be assigned to each other. */ private boolean checkClassName(MethodCall one, MethodCall two) { // TODO make this stuff work better. if (one._className.equals("java.lang.Object")) { return true; } else if (two._className.equals("java.lang.Object")) { return true; } else if (one._className.equals(two._className)) { return true; } else { // check the other class name is one of the implemented interfaces boolean result = false; for (int i = 0; i < two._interfaces.length; i++) { if (two._interfaces[i].getName().equals(one._className)) { result = true; break; } } if (!result) { for (int i = 0; i < one._interfaces.length; i++) { if (one._interfaces[i].getName().equals(two._className)) { result = true; break; } } } return result; } } /* ------------------------------------------------------------------------ */ /* equals method /* ------------------------------------------------------------------------ */ /** * Returns true if and only if the two object represent the same call. * * @param obj The object to be compared. * @return true if the specified object is the same as this. */ @Override public boolean equals(Object obj) { if (obj == null) return false; if (obj == this) return true; if (obj instanceof MethodCall) { MethodCall other = (MethodCall)obj; if (!checkClassName(this, other)) { return false; } if (!other._methodName.equals(this._methodName)) return false; if (other._arguments.length != this._arguments.length) return false; for (int i = 0; i < this._arguments.length; i++) { boolean thisArgNull = this._arguments[i] == null; boolean otherArgClazz = other._arguments[i] instanceof Class; boolean otherArgNull = other._arguments[i] == null; boolean thisArgClazz = this._arguments[i] instanceof Class; if (thisArgNull) { if (otherArgNull) { // This is OK } else if (otherArgClazz) { // This is also OK } else { return false; } // this argument is OK. } else if (otherArgNull) { if (thisArgClazz) { // This is OK } else { return false; } // this argument is OK. } else if (otherArgClazz) { if (thisArgClazz) { Class<?> otherArgClass = (Class<?>) other._arguments[i]; Class<?> thisArgClass = (Class<?>) this._arguments[i]; if (otherArgClass.equals(Class.class) || thisArgClass.equals(Class.class)) { // do nothing } else if (!(otherArgClass.isAssignableFrom(thisArgClass) || thisArgClass.isAssignableFrom(otherArgClass))) { return false; } } else { Class<?> clazz = (Class<?>)other._arguments[i]; if (clazz.isPrimitive()) { if (clazz.equals(byte.class)) { return this._arguments[i].getClass().equals(Byte.class); } else if (clazz.equals(boolean.class)) { return this._arguments[i].getClass().equals(Boolean.class); } else if (clazz.equals(short.class)) { return this._arguments[i].getClass().equals(Short.class); } else if (clazz.equals(char.class)) { return this._arguments[i].getClass().equals(Character.class); } else if (clazz.equals(int.class)) { return this._arguments[i].getClass().equals(Integer.class); } else if (clazz.equals(long.class)) { return this._arguments[i].getClass().equals(Long.class); } else if (clazz.equals(float.class)) { return this._arguments[i].getClass().equals(Float.class); } else if (clazz.equals(double.class)) { return this._arguments[i].getClass().equals(Double.class); } } else { if (!clazz.isInstance(this._arguments[i])) { return false; } } } } else if (thisArgClazz) { Class<?> clazz = (Class<?>)this._arguments[i]; if (clazz.isPrimitive()) { if (clazz.equals(byte.class)) { return other._arguments[i].getClass().equals(Byte.class); } else if (clazz.equals(boolean.class)) { return other._arguments[i].getClass().equals(Boolean.class); } else if (clazz.equals(short.class)) { return other._arguments[i].getClass().equals(Short.class); } else if (clazz.equals(char.class)) { return other._arguments[i].getClass().equals(Character.class); } else if (clazz.equals(int.class)) { return other._arguments[i].getClass().equals(Integer.class); } else if (clazz.equals(long.class)) { return other._arguments[i].getClass().equals(Long.class); } else if (clazz.equals(float.class)) { return other._arguments[i].getClass().equals(Float.class); } else if (clazz.equals(double.class)) { return other._arguments[i].getClass().equals(Double.class); } } else { if (!clazz.isInstance(other._arguments[i])) { return false; } } } else if (this._arguments[i] instanceof Object[] && other._arguments[i] instanceof Object[]) { return equals((Object[])this._arguments[i], (Object[])other._arguments[i]); } else { int result = compareUsingComparators(this._arguments[i], other._arguments[i]); if (result == 0) continue; else if (result == 1) return false; else if (!!!this._arguments[i].equals(other._arguments[i])) return false; } } } return true; } /** * Compare two arrays calling out to the custom comparators and handling * AtomicIntegers nicely. * * TODO remove the special casing for AtomicInteger. * * @param arr1 * @param arr2 * @return true if the arrays are equals, false otherwise. */ private boolean equals(Object[] arr1, Object[] arr2) { if (arr1.length != arr2.length) return false; for (int k = 0; k < arr1.length; k++) { if (arr1[k] == arr2[k]) continue; if (arr1[k] == null && arr2[k] != null) return false; if (arr1[k] != null && arr2[k] == null) return false; int result = compareUsingComparators(arr1[k], arr2[k]); if (result == 0) continue; else if (result == 1) return false; if (arr1[k] instanceof AtomicInteger && arr2[k] instanceof AtomicInteger && ((AtomicInteger)arr1[k]).intValue() == ((AtomicInteger)arr2[k]).intValue()) continue; if (!!!arr1[k].equals(arr2[k])) return false; } return true; } /** * Attempt to do the comparison using the comparators. This logic returns: * * <ul> * <li>0 if they are equal</li> * <li>1 if they are not equal</li> * <li>-1 no comparison was run</li> * </ul> * * @param o1 The first object. * @param o2 The second object. * @return 0, 1 or -1 depending on whether the objects were equal, not equal or no comparason was run. */ private int compareUsingComparators(Object o1, Object o2) { if (o1.getClass() == o2.getClass()) { @SuppressWarnings("unchecked") Comparator<Object> compare = (Comparator<Object>) equalsHelpers.get(o1.getClass()); if (compare != null) { if (compare.compare(o1, o2) == 0) return 0; else return 1; } } return -1; } /* ------------------------------------------------------------------------ */ /* hashCode method /* ------------------------------------------------------------------------ */ /** * Returns the hashCode (obtained by returning the hashCode of the * methodName). * * @return The hashCode */ @Override public int hashCode() { return _methodName.hashCode(); } /* ------------------------------------------------------------------------ */ /* toString method /* ------------------------------------------------------------------------ */ /** * Returns a string representation of the method call. * * @return string representation. */ @Override public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append(this._className); buffer.append('.'); buffer.append(this._methodName); buffer.append("("); for (int i = 0; i < this._arguments.length; i++) { if (this._arguments[i] != null) { if (this._arguments[i] instanceof Class) { buffer.append(((Class<?>)this._arguments[i]).getName()); } else if (Proxy.isProxyClass(this._arguments[i].getClass())) { // If the object is a dynamic proxy, just use the proxy class name to avoid calling toString on the proxy buffer.append(this._arguments[i].getClass().getName()); } else if (this._arguments[i] instanceof Object[]) { buffer.append(Arrays.toString((Object[])this._arguments[i])); } else { buffer.append(String.valueOf(this._arguments[i])); } } else { buffer.append("null"); } if (i + 1 < this._arguments.length) buffer.append(", "); } buffer.append(")"); String string = buffer.toString(); return string; } /* ------------------------------------------------------------------------ */ /* getInterfaces method /* ------------------------------------------------------------------------ */ /** * This method returns the list of interfaces implemented by the class that * was called. * * @return Returns the interfaces. */ public Class<?>[] getInterfaces() { return this._interfaces; } /* ------------------------------------------------------------------------ */ /* getInvokedObject method /* ------------------------------------------------------------------------ */ /** * This method returns the invoked object. * * @return The object that was invoked or null if an expected call. */ public Object getInvokedObject() { return _invokedObject; } /* ------------------------------------------------------------------------ */ /* registerEqualsHelper method /* ------------------------------------------------------------------------ */ /** * The native equals for an object may not provide the behaviour required by * the tests. As an example AtomicInteger does not define a .equals, but tests * may wish to compare it being passed in a method call for equality. This * method allows a Comparator to be specified for any type and the Comparator * will be used to determine equality in place of the .equals method. * * <p>The Comparator must not throw exceptions, and must return 0 for equality * or any other integer for inequality. * </p> * * @param <T> the type of the class and comparator. * @param type the type of the class for which the comparator will be called. * @param comparator the comparator to call. */ public static <T> void registerEqualsHelper(Class<T> type, Comparator<T> comparator) { equalsHelpers.put(type, comparator); } /* ------------------------------------------------------------------------ */ /* removeEqualsHelper method /* ------------------------------------------------------------------------ */ /** * This method removes any registered comparator specified for the given type. * * @param type the type to remove the comparator from. */ public static void removeEqualsHelper(Class<?> type) { equalsHelpers.remove(type); } }