/*
* 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.ref.SoftReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import junit.framework.Assert;
import junit.framework.AssertionFailedError;
import org.apache.aries.unittest.mocks.annotations.InjectSkeleton;
import org.apache.aries.unittest.mocks.annotations.Singleton;
/**
* <p>The Skeleton class is an implementation of the
* <code>java.lang.reflect.InvocationHandler</code> that can be used for
* dynamic mock objects.
* </p>
*
* <ol>
* <li>The static newMock methods can be used to create completely new mock
* objects backed by an entirely new skeleton.
* </li>
* <li>The static getSkeleton method can be used to obtain the skeleton
* backing a given mock.
* </li>
* <li>The createMock methods can be used to create a new mock object based on
* the skeleton that is invoked.
* </li>
* <li>The registerMethodCallHandler method can be used to register a handler
* that will be invoked when a method is called.
* </li>
* <li>The registerReturnTypeHandler method can be used to register a handler
* that will be invoked when a method with a specific return type is
* invoked. It should be noted that registered ReturnTypeHandlers will be
* invoked only if a method call handler has not been registered for the
* method that was invoked.
* </li>
* <li>The setReturnValue method can be used to set a value that will be
* returned when a method is invoked.
* </li>
* <li>The checkCalls methods can be used to determine if the methods in the
* list should have been called. They return a boolean to indicate if the
* expected calls occurred.
* </li>
* <li>The assertCalls method performs the same operation as the checkCalls,
* but throws an junit.framework.AssertionFailedError if the calls don't
* match. This intended for use within the junit test framework
* </li>
* <li>If no method call or return type handlers have been registered for a
* call then if the return type is an interface then a mock that implements
* that interface will be returned, otherwise null will be returned.
* </li>
* </ol>
*/
public final class Skeleton implements InvocationHandler
{
/** A list of calls made on this skeleton */
private List<MethodCall> _methodCalls;
/** The invocation handler to call after MethodCall and ReturnType handlers */
private DefaultInvocationHandler default_Handler;
/** The method call handlers */
private Map<MethodCall, MethodCallHandler> _callHandlers;
/** The type handlers */
private Map<Class<?>, ReturnTypeHandler> _typeHandlers;
/** The parameter map */
private Map<String, Object> _mockParameters;
/** A Map of mock objects to Maps of properties */
private Map<Object, Map<String, Object>> _objectProperties;
/** A Map of exception notification listeners */
private Map<Class<?>, List<ExceptionListener>> _notificationListeners;
/** The template class used to create this Skeleton, may be null */
private Object _template;
/** Cached template objects */
private static ConcurrentMap<Object, SoftReference<Object>> _singletonMocks = new ConcurrentHashMap<Object, SoftReference<Object>>();
// Constructors
/* ------------------------------------------------------------------------ */
/* Skeleton constructor
/* ------------------------------------------------------------------------ */
/**
* constructs the skeleton with the default method call handlers and the
* default return type handlers.
*/
private Skeleton()
{
reset();
}
// Static methods create methods
/* ------------------------------------------------------------------------ */
/* newMock method
/* ------------------------------------------------------------------------ */
/**
* This method returns a completely new mock object backed by a new skeleton
* object. It is equivalent to
* <code>new Skeleton().createMock(interfaceClazzes)</code>
*
* @param interfaceClazzes the classes the mock should implement
* @return the new mock object.
*/
public final static Object newMock(Class<?> ... interfaceClazzes)
{
return new Skeleton().createMock(interfaceClazzes);
}
/* ------------------------------------------------------------------------ */
/* newMock method
/* ------------------------------------------------------------------------ */
/**
* This method returns a completely new mock object backed by a new skeleton
* object. It is equivalent to
* <code>new Skeleton().createMock(interfaceClazzes)</code>
*
* @param <T> The object type.
* @param interfaceClazz the classes the mock should implement
* @return the new mock object.
*/
public final static <T> T newMock(Class<T> interfaceClazz)
{
return interfaceClazz.cast(new Skeleton().createMock(interfaceClazz));
}
/**
* It is often the case that only a subset of methods on an interface are needed, but
* those methods that are needed are quite complex. In this case a static mock forces
* you into implementing lots of methods you do not need, and produces problems when
* new methods are added to the interface being implemented. This method can essentially
* be used to complete the interface implementation. The object passed in is an instance
* of a class that implements a subset of the methods on the supplied interface. It does
* not need to implement the interface itself. The returned object will implement the full
* interface and delegate to the methods on the templateObject where necessary.
*
* @param <T> The object type.
* @param template The template object for the mock
* @param interfaceClazz The interface to implement
* @return An implementation of the interface that delegates (where appropraite) onto the template.
*/
public final static <T> T newMock(final Object template, Class<T> interfaceClazz)
{
Class<?> templateClass = template.getClass();
if (templateClass.getAnnotation(Singleton.class) != null) {
SoftReference<Object> mock = _singletonMocks.get(template);
if (mock != null) {
Object theMock = mock.get();
if (theMock == null) {
_singletonMocks.remove(template);
} else if (interfaceClazz.isInstance(theMock)) {
return interfaceClazz.cast(theMock);
}
}
}
Skeleton s = new Skeleton();
s._template = template;
for (Method m : interfaceClazz.getMethods()) {
try {
final Method m2 = templateClass.getMethod(m.getName(), m.getParameterTypes());
MethodCall mc = new MethodCall(interfaceClazz, m.getName(), (Object[])m.getParameterTypes());
s.registerMethodCallHandler(mc, new MethodCallHandler()
{
public Object handle(MethodCall methodCall, Skeleton parent) throws Exception
{
try {
m2.setAccessible(true);
return m2.invoke(template, methodCall.getArguments());
} catch (InvocationTargetException ite) {
if(ite.getTargetException() instanceof Exception)
throw (Exception)ite.getTargetException();
else throw new Exception(ite.getTargetException());
}
}
});
} catch (NoSuchMethodException e) {
// do nothing here, it is a method not on the interface so ignore it.
}
}
Field[] fs = template.getClass().getFields();
for (Field f : fs) {
InjectSkeleton sk = f.getAnnotation(InjectSkeleton.class);
if (sk != null) {
f.setAccessible(true);
try {
f.set(template, s);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
Object o = s.createMock(interfaceClazz);
_singletonMocks.put(template, new SoftReference<Object>(o) {
@Override
public boolean enqueue()
{
_singletonMocks.remove(template);
System.out.println("Done cleanup");
return super.enqueue();
}
});
return interfaceClazz.cast(o);
}
// static mock query methods
/* ------------------------------------------------------------------------ */
/* getSkeleton method
/* ------------------------------------------------------------------------ */
/**
* This method returns the Skeleton backing the supplied mock object. If the
* supplied object is not a mock an IllegalArgumentException will be thrown.
*
* @param mock the mock object
* @return the skeleton backing the mock object
* @throws IllegalArgumentException thrown if the object is not a mock.
*/
public final static Skeleton getSkeleton(Object mock)
throws IllegalArgumentException
{
InvocationHandler ih = Proxy.getInvocationHandler(mock);
if (ih instanceof Skeleton)
{
return (Skeleton)ih;
}
throw new IllegalArgumentException("The supplied proxy (" + mock + ") was not an Aries dynamic mock ");
}
/* ------------------------------------------------------------------------ */
/* isSkeleton method
/* ------------------------------------------------------------------------ */
/**
* This method returns true if and only the provided object is backed by a
* Skeleton. Another way to think about this is if it returns true then a
* call to getSkeleton will not result in an IllegalArgumentException, and is
* guaranteed to return a Skeleton.
*
* @param mock the mock to test.
* @return true if it is backed by a skeleton.
*/
public final static boolean isSkeleton(Object mock)
{
if (Proxy.isProxyClass(mock.getClass())) {
InvocationHandler ih = Proxy.getInvocationHandler(mock);
return (ih instanceof Skeleton);
}
return false;
}
// InvocationHandler defined methods.
/* ------------------------------------------------------------------------ */
/* invoke method
/* ------------------------------------------------------------------------ */
/**
* This method is invoked by the mock objects. It constructs a MethodCall
* object representing the call and adds it to the list of calls that were
* made. (It should be noted that if the method is toString, hashCode or
* equals then they are not added to the list.) It then calls a registered
* MethodCallHandler, if a MethodCallHandler is not registered then a
* ReturnTypeHandler is invoked. If a ReturnTypeHandler is not invoked then
* the registered default InvocationHandler is called. By default the Skeleton
* is constructed with a DefaultInvocationHandler. If the invoked method has
* an interface as a return type then the DefaultInvocationHandler will return
* a new mock implementing that interface. If the return type is a class null
* will be returned.
*
* @param targetObject The mock object that was invoked.
* @param calledMethod The method that was called.
* @param arguments The arguments that were passed.
* @return The return of the method invoked.
* @throws Throwable Any exception thrown.
*/
public Object invoke(Object targetObject, Method calledMethod, Object[] arguments)
throws Throwable
{
String methodName = calledMethod.getName();
MethodCall call = new MethodCall(targetObject, methodName, arguments);
if (!DefaultMethodCallHandlers.isDefaultMethodCall(call))
{
_methodCalls.add(call);
}
Object result;
try
{
if (_callHandlers.containsKey(call))
{
MethodCallHandler handler = _callHandlers.get(call);
result = handler.handle(call, this);
}
else if (isReadWriteProperty(targetObject.getClass(), calledMethod))
{
String propertyName = methodName.substring(3);
if (methodName.startsWith("get") || methodName.startsWith("is"))
{
if (methodName.startsWith("is")) propertyName = methodName.substring(2);
Map<String, Object> properties = _objectProperties.get(targetObject);
if (properties == null)
{
properties = new HashMap<String, Object>();
_objectProperties.put(targetObject, properties);
}
if (properties.containsKey(propertyName))
{
result = properties.get(propertyName);
}
else if (_typeHandlers.containsKey(calledMethod.getReturnType()))
{
result = createReturnTypeProxy(calledMethod.getReturnType());
}
else
{
result = default_Handler.invoke(targetObject, calledMethod, arguments);
}
}
// Must be a setter.
else
{
Map<String, Object> properties = _objectProperties.get(targetObject);
if (properties == null)
{
properties = new HashMap<String, Object>();
_objectProperties.put(targetObject, properties);
}
properties.put(propertyName, arguments[0]);
result = null;
}
}
else if (_typeHandlers.containsKey(calledMethod.getReturnType()))
{
result = createReturnTypeProxy(calledMethod.getReturnType());
}
else
{
result = default_Handler.invoke(targetObject, calledMethod, arguments);
}
}
catch (Throwable t)
{
Class<?> throwableType = t.getClass();
List<ExceptionListener> listeners = _notificationListeners.get(throwableType);
if (listeners != null)
{
for (ExceptionListener listener : listeners)
{
listener.exceptionNotification(t);
}
}
throw t;
}
return result;
}
// MethodCall registration methods
/* ------------------------------------------------------------------------ */
/* registerMethodCallHandler method
/* ------------------------------------------------------------------------ */
/**
* This method registers a MethodCallHandler for the specified MethodCall.
*
* @param call The method that was called.
* @param handler The MethodCallHandler.
*/
public void registerMethodCallHandler(MethodCall call, MethodCallHandler handler)
{
deRegisterMethodCallHandler(call);
_callHandlers.put(call, handler);
}
/* ------------------------------------------------------------------------ */
/* deRegisterMethodCallHandler method
/* ------------------------------------------------------------------------ */
/**
* This method removes a registered MethodCallHandler for the specified
* MethodCall.
*
* @param call the specified MethodCall
*/
public void deRegisterMethodCallHandler(MethodCall call)
{
_callHandlers.remove(call);
}
/* ------------------------------------------------------------------------ */
/* reset method
/* ------------------------------------------------------------------------ */
/**
* This method resets the skeleton to the state it was in prior just after it
* was constructed.
*/
public void reset()
{
_methodCalls = new LinkedList<MethodCall>();
_callHandlers = new HashMap<MethodCall, MethodCallHandler>();
_typeHandlers = new HashMap<Class<?>, ReturnTypeHandler>();
DefaultReturnTypeHandlers.registerDefaultHandlers(this);
DefaultMethodCallHandlers.registerDefaultHandlers(this);
default_Handler = new DefaultInvocationHandler(this);
_mockParameters = new HashMap<String, Object>();
_objectProperties = new HashMap<Object, Map<String, Object>>();
_notificationListeners = new HashMap<Class<?>, List<ExceptionListener>>();
}
/* ------------------------------------------------------------------------ */
/* clearMethodCalls method
/* ------------------------------------------------------------------------ */
/**
* This method clears the method calls list for the skeleton
*/
public void clearMethodCalls()
{
_methodCalls = new LinkedList<MethodCall>();
}
/* ------------------------------------------------------------------------ */
/* setReturnValue method
/* ------------------------------------------------------------------------ */
/**
* This is a convenience method for registering a method call handler where
* a specific value should be returned when a method is called, rather than
* some logic needs to be applied. The value should be an object or the object
* version of the primitive for the methods return type, so if the method
* returns short the value must be an instance of java.lang.Short, not
* java.lang.Integer.
*
* @param call the method being called.
* @param value the value to be returned when that method is called.
*/
public void setReturnValue(MethodCall call, final Object value)
{
Class<?> clazz;
try {
clazz = Class.forName(call.getClassName());
} catch (ClassNotFoundException e) {
throw new IllegalStateException("This should be impossible as we have already seen this class loaded");
}
Method[] methods = clazz.getMethods();
methods: for (Method m : methods) {
if(!!!m.getName().equals(call.getMethodName()))
continue methods;
Object[] args = call.getArguments();
Class<?>[] parms = m.getParameterTypes();
if (args.length == parms.length) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Class && args[i].equals(parms[i])) {
} else if (parms[i].isInstance(args[i])) {
} else {
continue methods;
}
}
Class<?> returnType = m.getReturnType();
if (returnType.isPrimitive()) {
if (returnType == boolean.class) returnType = Boolean.class;
else if (returnType == byte.class) returnType = Byte.class;
else if (returnType == short.class) returnType = Short.class;
else if (returnType == char.class) returnType = Character.class;
else if (returnType == int.class) returnType = Integer.class;
else if (returnType == long.class) returnType = Long.class;
else if (returnType == float.class) returnType = Float.class;
else if (returnType == double.class) returnType = Double.class;
}
if (value != null && !!!returnType.isInstance(value)) {
throw new IllegalArgumentException("The object cannot be returned by the requested method: " + call);
} else break methods;
}
}
registerMethodCallHandler(call, new MethodCallHandler()
{
public Object handle(MethodCall methodCall, Skeleton parent) throws Exception
{
return value;
}
});
}
/* ------------------------------------------------------------------------ */
/* setThrow method
/* ------------------------------------------------------------------------ */
/**
* This is a convenience method for registering a method call handler where
* a specific exception should be thrown when the method is called, rather
* than some logic needs to be applied.
*
* @param call the method being called
* @param thingToThrow the exception to throw.
*/
public void setThrows(MethodCall call, final Exception thingToThrow)
{
registerMethodCallHandler(call, new MethodCallHandler()
{
public Object handle(MethodCall methodCall, Skeleton parent) throws Exception
{
thingToThrow.fillInStackTrace();
throw thingToThrow;
}
});
}
/* ------------------------------------------------------------------------ */
/* setThrow method
/* ------------------------------------------------------------------------ */
/**
* This is a convenience method for registering a method call handler where
* a specific exception should be thrown when the method is called, rather
* than some logic needs to be applied.
*
* @param call the method being called
* @param thingToThrow the exception to throw.
*/
public void setThrows(MethodCall call, final Error thingToThrow)
{
registerMethodCallHandler(call, new MethodCallHandler()
{
public Object handle(MethodCall methodCall, Skeleton parent) throws Exception
{
thingToThrow.fillInStackTrace();
throw thingToThrow;
}
});
}
// ReturnType registration methods
/* ------------------------------------------------------------------------ */
/* registerReturnTypeHandler method
/* ------------------------------------------------------------------------ */
/**
* This method registers a ReturnTypeHandler for the specified class.
*
* @param clazz The class to be handled.
* @param handler The ReturnTypeHandler
*/
public void registerReturnTypeHandler(Class<?> clazz, ReturnTypeHandler handler)
{
deRegisterReturnTypeHandler(clazz);
_typeHandlers.put(clazz, handler);
}
/* ------------------------------------------------------------------------ */
/* deRegisterReturnTypeHandler method
/* ------------------------------------------------------------------------ */
/**
* This method removes a registration for a ReturnTypeHandler for the
* specified class.
*
* @param clazz The class to deregister the handler for.
*/
public void deRegisterReturnTypeHandler(Class<?> clazz)
{
_typeHandlers.remove(clazz);
}
// Exception notification methods
/* ------------------------------------------------------------------------ */
/* registerExceptionListener method
/* ------------------------------------------------------------------------ */
/**
* This method registers an ExceptionListener when the specified Exception is
* thrown.
*
* @param throwableType The type of the Throwable
* @param listener The listener.
*/
public void registerExceptionListener(Class<?> throwableType, ExceptionListener listener)
{
List<ExceptionListener> l = _notificationListeners.get(throwableType);
if (l == null)
{
l = new ArrayList<ExceptionListener>();
_notificationListeners.put(throwableType, l);
}
l.add(listener);
}
// parameter related methods
/* ------------------------------------------------------------------------ */
/* setParameter method
/* ------------------------------------------------------------------------ */
/**
* This method allows a parameter to be set. It is intended to be used by
* MethodCallHandlers and ReturnTypeHandlers.
*
* @param key The key
* @param value The value
*/
public void setParameter(String key, Object value)
{
_mockParameters.put(key, value);
}
/* ------------------------------------------------------------------------ */
/* getParameter method
/* ------------------------------------------------------------------------ */
/**
* This method allows a parameter to be retrieved.
*
* @param key the key the parameter was set using
* @return the parameter
*/
public Object getParameter(String key)
{
return _mockParameters.get(key);
}
/* ------------------------------------------------------------------------ */
/* getTemplateObject method
/* ------------------------------------------------------------------------ */
/**
* @return the template object, if one was used when initializing this skeleton.
*/
public Object getTemplateObject()
{
return _template;
}
// default InvocationHandler related methods
/**
* @param defaultHandler The defaultHandler to set.
*/
public void setDefaultHandler(DefaultInvocationHandler defaultHandler)
{
this.default_Handler = defaultHandler;
}
// MethodCall list check methods
/* ------------------------------------------------------------------------ */
/* checkCalls method
/* ------------------------------------------------------------------------ */
/**
* This method checks that the calls in the list occurred. If the addCalls
* boolean is true then their must be an exact match. If the allCalls boolean
* is false then the calls in the list must occur in that order, but other
* calls can be in between.
*
* @param calls The expected calls list
* @param allCalls true if an exact match comparison should be performed
* @return true if they the expected calls match.
*/
public boolean checkCalls(List<MethodCall> calls, boolean allCalls)
{
boolean found = false;
if (allCalls)
{
return calls.equals(_methodCalls);
}
else
{
Iterator<MethodCall> actual = _methodCalls.iterator();
for (MethodCall expectedCall : calls)
{
found = false;
actual: while (actual.hasNext())
{
MethodCall actualCall = actual.next();
if (actualCall.equals(expectedCall))
{
found = true;
break actual;
}
}
}
}
return found;
}
/* ------------------------------------------------------------------------ */
/* checkCall method
/* ------------------------------------------------------------------------ */
/**
* Checks that the specified method has been called on this skeleton
*
* @param call the call that should have been called.
* @return true if the MethodCall occurs in the list.
*/
public boolean checkCall(MethodCall call)
{
return this._methodCalls.contains(call);
}
// MethodCall list assert methods
/* ------------------------------------------------------------------------ */
/* assertCalls method
/* ------------------------------------------------------------------------ */
/**
* This method checks that the MethodCalls objects in the given list were
* made and throws an AssertionFailedError if they were not. If allCalls is
* true the given list and the calls list must be identical. If allCalls is
* false other calls could have been made on the skeleton in between ones
* specified in the list.
*
* @param calls the list of calls
* @param allCalls whether an exact match between the lists is required
* @throws AssertionFailedError if a failure has occurred.
*/
public void assertCalled(List<MethodCall> calls, boolean allCalls) throws AssertionFailedError
{
if (allCalls)
{
if ((calls == null) && (_methodCalls == null)) return;
if (calls == null)
{
throw new AssertionFailedError("expected null, but was " + _methodCalls);
}
if (_methodCalls == null)
{
throw new AssertionFailedError("expected:" + calls + " but was null");
}
if (calls.equals(_methodCalls)) return;
// OK compare lists and decide on differences - initially all the lists are different
int startOfDifferences = 0;
// Remove the common start of sequence
boolean lastItemSame = true;
for (int i = 0; i < calls.size() && i < _methodCalls.size() && lastItemSame; i++)
{
if ((calls.get(i) == null) && (_methodCalls.get(i) == null))
{
lastItemSame = true;
}
else if ((calls.get(i) == null) || (_methodCalls.get(i) == null))
{
lastItemSame = false;
}
else
{
lastItemSame = calls.get(i).equals(_methodCalls.get(i));
}
if (lastItemSame) startOfDifferences++;
}//for
// Now remove the common bit at the end
int endOfDifferencesInExpected = calls.size();
int endOfDifferencesInReceived = _methodCalls.size();
lastItemSame = true;
while ((endOfDifferencesInExpected > startOfDifferences)
&& (endOfDifferencesInReceived > startOfDifferences)
&& lastItemSame)
{
int ap = endOfDifferencesInExpected - 1;
int bp = endOfDifferencesInReceived - 1;
if ((calls.get(ap) == null) && (_methodCalls.get(bp) == null))
{
lastItemSame = true;
}
else if ((calls.get(ap) == null) || (_methodCalls.get(bp) == null))
{
lastItemSame = false;
}
else
{
lastItemSame = calls.get(ap).equals(_methodCalls.get(bp));
}
if (lastItemSame)
{
endOfDifferencesInExpected--;
endOfDifferencesInReceived--;
}
}//while
String failureText;
// OK, now build the failureText
if (endOfDifferencesInExpected == startOfDifferences)
{
failureText =
"Expected calls and actual calls differed because "
+ _methodCalls.subList(startOfDifferences, endOfDifferencesInReceived)
+ " inserted after element "
+ startOfDifferences;
}
else if (endOfDifferencesInReceived == startOfDifferences)
{
failureText =
"Expected calls and actual calls differed because "
+ calls.subList(startOfDifferences, endOfDifferencesInExpected)
+ " missing after element "
+ startOfDifferences;
}
else
{
if ((endOfDifferencesInExpected == startOfDifferences + 1)
&& (endOfDifferencesInReceived == startOfDifferences + 1))
{
failureText =
"Expected calls and actual calls differed because element "
+ startOfDifferences
+ " is different (calls:"
+ calls.get(startOfDifferences)
+ " but was:"+_methodCalls.get(startOfDifferences)+") ";
}
else if (endOfDifferencesInExpected == startOfDifferences + 1)
{
failureText =
"Expected calls and actual calls differed because element "
+ startOfDifferences
+ " ("
+ calls.get(startOfDifferences)
+ ") has been replaced by "
+ _methodCalls.subList(startOfDifferences, endOfDifferencesInReceived);
}
else
{
failureText =
"Expected calls and actual calls differed because elements between "
+ startOfDifferences
+ " and "
+ (endOfDifferencesInExpected - 1)
+ " are different (expected:"
+ calls.subList(startOfDifferences, endOfDifferencesInExpected)
+ " but was:"
+ _methodCalls.subList(startOfDifferences, endOfDifferencesInReceived)
+ ")";
}//if
}//if
throw new AssertionFailedError(failureText + " expected:" + calls + " but was:" + _methodCalls);
}
else
{
Iterator<MethodCall> expected = calls.iterator();
Iterator<MethodCall> actual = _methodCalls.iterator();
while (expected.hasNext())
{
boolean found = false;
MethodCall expectedCall = expected.next();
MethodCall actualCall = null;
actual: while (actual.hasNext())
{
actualCall = actual.next();
if (actualCall.equals(expectedCall))
{
found = true;
break actual;
}
}
if (!found)
{
throw new AssertionFailedError( "The method call " +
expectedCall +
" was expected but has not occurred (actual calls = "+_methodCalls+")");
}
}
}
}
/* ------------------------------------------------------------------------ */
/* assertCall method
/* ------------------------------------------------------------------------ */
/**
* This does the same as checkCall, but throws an
* junit.framework.AssertionFailedError if the call did not occur.
*
* @param call the call that was expected
*/
public void assertCalled(MethodCall call)
{
if (!checkCall(call))
{
throw new AssertionFailedError("The method call " +
call +
" was expected but has not occurred. Actual calls: " + getMethodCallsAsString());
}
}
/* ------------------------------------------------------------------------ */
/* assertCalledExactNumberOfTimes method
/* ------------------------------------------------------------------------ */
/**
* This method asserts that the method specified in the call parameter has
* been called the number of times specified by numberOfCalls. If
* numberOfCalls is zero this method is equivalent to assertNotCalled.
*
* @param call The call that was made.
* @param numberOfCalls The number of times the call should have been made.
*/
public void assertCalledExactNumberOfTimes(MethodCall call, int numberOfCalls)
{
int callCount = 0;
for (MethodCall callMade : _methodCalls)
{
if (callMade.equals(call))
{
callCount++;
}
}
if (numberOfCalls != callCount)
{
throw new AssertionFailedError("The method call " + call +
" should have been called " + numberOfCalls +
" time(s), but was called " + callCount + " time(s)");
}
}
/* ------------------------------------------------------------------------ */
/* assertNotCalled method
/* ------------------------------------------------------------------------ */
/**
* This method throws an junit.framework.AssertionFailedError if the specified
* call was invoked on the skeleton.
*
* @param call the call to check.
*/
public void assertNotCalled(MethodCall call)
{
Assert.assertFalse( "The method call " +
call +
" occurred in the skeleton " +
this.toString(), checkCall(call));
}
/* ------------------------------------------------------------------------ */
/* assertMockNotCalled method
/* ------------------------------------------------------------------------ */
/**
* This method throws an junit.framework.AssertionFailedError if the skeleton
* has had any methods invoked on it.
*/
public void assertSkeletonNotCalled()
{
Assert.assertEquals("The skeleton " + this.toString() +
" has had the following method invoked on it " + getMethodCallsAsString(),
0, _methodCalls.size());
}
// Instance mock creation methods
/* ------------------------------------------------------------------------ */
/* createMock method
/* ------------------------------------------------------------------------ */
/**
* Creates a new Mock using this skeleton backing it.
*
* @param interfaceClasses an array of interface the mock should implement.
* @return the mock
*/
public Object createMock(Class<?> ... interfaceClasses)
{
ClassLoader cl;
if (interfaceClasses.length > 0) cl = interfaceClasses[0].getClassLoader();
else cl = Thread.currentThread().getContextClassLoader();
return Proxy.newProxyInstance(cl, interfaceClasses, this);
}
/* ------------------------------------------------------------------------ */
/* createMock method
/* ------------------------------------------------------------------------ */
/**
* Creates a new Mock using this skeleton backing it.
*
* @param <T> The object type
* @param interfaceClass an array of interface the mock should implement.
* @return the mock
*/
public <T> T createMock(Class<T> interfaceClass)
{
return interfaceClass.cast(Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[] {interfaceClass}, this));
}
/* ------------------------------------------------------------------------ */
/* invokeReturnTypeHandlers method
/* ------------------------------------------------------------------------ */
/**
* This method invokes the return type proxy for the specified class. If a
* ReturnTypeHandler for that type has not been registered then if the class
* represents an interface a new mock will be returned, backed by this
* skeleton, otherwise null will be returned.
*
* @param type the type to be invoked.
* @return the returned object.
* @throws Exception if an error occurs when invoking the return type handler.
*/
public Object invokeReturnTypeHandlers(Class<?> type) throws Exception
{
if (_typeHandlers.containsKey(type))
{
ReturnTypeHandler rth = _typeHandlers.get(type);
return rth.handle(type, this);
}
else if (type.isInterface())
{
return createMock(type);
}
else
{
return null;
}
}
// Miscellaneous methods that have been deprecated and will be removed.
/* ------------------------------------------------------------------------ */
/* createReturnTypeProxy method
/* ------------------------------------------------------------------------ */
/**
* create a proxy for given return type.
*
* @deprecated use invokeReturnTypeHandlers instead
*
* @param type The return type for which a handler is required
* @return ReturnTypeHandler The return type handler
* @throws Exception Thrown if an exception occurs.
*/
/* ------------------------------------------------------------------------ */
@Deprecated
private final Object createReturnTypeProxy(Class<?> type) throws Exception
{
return invokeReturnTypeHandlers(type);
}
/* ------------------------------------------------------------------------ */
/* isReadWriteProperty method
/* ------------------------------------------------------------------------ */
/**
* This method returns true if the method passed a getter for a read write
* java bean property. This is worked out by checking that a setter and getter
* exist for the property and that the setter and getter take and return
* exactly the same time.
*
* @param objClass The object the read write method has been invoked on.
* @param method The method to be checked.
* @return true if it is a getter or setter for a read write property.
*/
private boolean isReadWriteProperty(Class<?> objClass, Method method)
{
String methodName = method.getName();
String propertyName = methodName.substring(3);
Class<?>[] parameters = method.getParameterTypes();
Class<?> clazz;
boolean result = false;
if (methodName.startsWith("get") && parameters.length == 0)
{
clazz = method.getReturnType();
try
{
objClass.getMethod("set" + propertyName, clazz);
result = true;
}
catch (NoSuchMethodException e)
{
if (isPrimitive(clazz))
{
clazz = getOtherForm(clazz);
try
{
objClass.getMethod("set" + propertyName, clazz);
result = true;
}
catch (NoSuchMethodException e1)
{
}
}
}
}
else if (methodName.startsWith("is") && parameters.length == 0)
{
clazz = method.getReturnType();
if (clazz.equals(Boolean.class) || clazz.equals(boolean.class))
{
propertyName = methodName.substring(2);
try
{
objClass.getMethod("set" + propertyName, clazz);
result = true;
}
catch (NoSuchMethodException e)
{
if (isPrimitive(clazz))
{
clazz = getOtherForm(clazz);
try
{
objClass.getMethod("set" + propertyName, clazz);
result = true;
}
catch (NoSuchMethodException e1)
{
}
}
}
}
}
else if (methodName.startsWith("set") && parameters.length == 1)
{
clazz = parameters[0];
try
{
Method getter = objClass.getMethod("get" + propertyName, new Class[0]);
result = checkClasses(getter.getReturnType(), clazz);
}
catch (NoSuchMethodException e)
{
if (isPrimitive(clazz))
{
clazz = getOtherForm(clazz);
try
{
Method getter = objClass.getMethod("get" + propertyName, new Class[0]);
result = checkClasses(getter.getReturnType(), clazz);
}
catch (NoSuchMethodException e1)
{
if (clazz.equals(Boolean.class) || clazz.equals(boolean.class))
{
try
{
Method getter = objClass.getMethod("is" + propertyName, new Class[0]);
result = checkClasses(getter.getReturnType(), clazz);
}
catch (NoSuchMethodException e2)
{
clazz = getOtherForm(clazz);
try
{
Method getter = objClass.getMethod("is" + propertyName, new Class[0]);
result = checkClasses(getter.getReturnType(), clazz);
}
catch (NoSuchMethodException e3)
{
}
}
}
}
}
}
}
return result;
}
/* ------------------------------------------------------------------------ */
/* isPrimitive method
/* ------------------------------------------------------------------------ */
/**
* This method returns true if the class object represents a primitive or the
* object version of a primitive.
*
* @param clazz The class to be checked.
* @return true if it is a primitive or a wrapper.
*/
private boolean isPrimitive(Class<?> clazz)
{
boolean result = false;
if (clazz.isPrimitive())
{
result = true;
}
else
{
result = clazz.equals(Boolean.class) || clazz.equals(Byte.class) ||
clazz.equals(Short.class) || clazz.equals(Character.class) ||
clazz.equals(Integer.class) || clazz.equals(Long.class) ||
clazz.equals(Float.class) || clazz.equals(Double.class);
}
return result;
}
/* ------------------------------------------------------------------------ */
/* getOtherForm method
/* ------------------------------------------------------------------------ */
/**
* This method takes a class representing either a primitive or an object
* wrapper. If the class is a primitive type the object wrapper class is
* returned. If the class is an object wrapper class the primitive type is
* returned.
*
* @param clazz
* @return the class representing the primitive object wrapper.
*/
private Class<?> getOtherForm(Class<?> clazz)
{
Class<?> result = null;
if (clazz.equals(boolean.class)) result = Boolean.class;
else if (clazz.equals(byte.class)) result = Byte.class;
else if (clazz.equals(short.class)) result = Short.class;
else if (clazz.equals(char.class)) result = Character.class;
else if (clazz.equals(int.class)) result = Integer.class;
else if (clazz.equals(long.class)) result = Long.class;
else if (clazz.equals(float.class)) result = Float.class;
else if (clazz.equals(double.class)) result = Double.class;
else if (clazz.equals(Boolean.class)) result = boolean.class;
else if (clazz.equals(Byte.class)) result = byte.class;
else if (clazz.equals(Short.class)) result = short.class;
else if (clazz.equals(Character.class)) result = char.class;
else if (clazz.equals(Integer.class)) result = int.class;
else if (clazz.equals(Long.class)) result = long.class;
else if (clazz.equals(Float.class)) result = float.class;
else if (clazz.equals(Double.class)) result = double.class;
return result;
}
/* ------------------------------------------------------------------------ */
/* checkClasses method
/* ------------------------------------------------------------------------ */
/**
* This method returns true if the two classes are the same or if one is
* primitive that the other is a primitive wrapper.
*
* @param type1
* @param type2
* @return true if the classes are compatible.
*/
private boolean checkClasses(Class<?> type1, Class<?> type2)
{
boolean result = false;
if ((type1.isPrimitive() && type2.isPrimitive()) ||
(!type1.isPrimitive() && !type2.isPrimitive()))
{
result = type1.equals(type2);
}
else
{
result = (type1.equals(boolean.class) && type2.equals(Boolean.class)) ||
(type1.equals(byte.class) && type2.equals(Byte.class)) ||
(type1.equals(short.class) && type2.equals(Short.class)) ||
(type1.equals(char.class) && type2.equals(Character.class)) ||
(type1.equals(int.class) && type2.equals(Integer.class)) ||
(type1.equals(long.class) && type2.equals(Long.class)) ||
(type1.equals(float.class) && type2.equals(Float.class)) ||
(type1.equals(double.class) && type2.equals(Double.class)) ||
(type2.equals(boolean.class) && type1.equals(Boolean.class)) ||
(type2.equals(byte.class) && type1.equals(Byte.class)) ||
(type2.equals(short.class) && type1.equals(Short.class)) ||
(type2.equals(char.class) && type1.equals(Character.class)) ||
(type2.equals(int.class) && type1.equals(Integer.class)) ||
(type2.equals(long.class) && type1.equals(Long.class)) ||
(type2.equals(float.class) && type1.equals(Float.class)) ||
(type2.equals(double.class) && type1.equals(Double.class));
}
return result;
}
/* ------------------------------------------------------------------------ */
/* getMethodCallsAsString method
/* ------------------------------------------------------------------------ */
/**
* This method builds a String that contains the method calls that have been
* made on this skeleton. It puts each call on a separate line.
*
* @return the string representation of the method call.
*/
private String getMethodCallsAsString()
{
StringBuilder builder = new StringBuilder();
for (MethodCall call : _methodCalls)
{
builder.append(call);
builder.append("\r\n");
}
return builder.toString();
}
}