/* * 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(); } }