/*
* Copyright 2008,2009 Toni Menzel
* Copyright 2008 Alin Dreghiciu
*
* Licensed 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.ops4j.pax.exam.junit.extender.impl.internal;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.osgi.framework.BundleContext;
import static org.ops4j.lang.NullArgumentException.*;
import org.ops4j.pax.exam.Inject;
import org.ops4j.pax.exam.junit.extender.CallableTestMethod;
/**
* {@link Callable} implementation.
*
* @author Toni Menzel (tonit)
* @author Alin Dreghiciu (adreghiciu@gmail.com)
* @since May 29, 2008
*/
class CallableTestMethodImpl
implements CallableTestMethod
{
/**
* Logger.
*/
private static final Log LOG = LogFactory.getLog( CallableTestMethodImpl.class );
/**
* Bundle context of the bundle containing the test class (cannot be null).
*/
private BundleContext m_bundleContext;
/**
* Test class name (cannot be null or empty).
*/
private final String m_testClassName;
/**
* Test method name (cannot be null or empty).
*/
private final String m_testMethodName;
/**
* Constructor.
*
* @param bundleContext bundle context of the bundle containing the test class (cannot be null)
* @param testClassName test class name (cannot be null or empty)
* @param testMethodName test method name (cannot be null or empty)
*
* @throws IllegalArgumentException - If bundle context is null
* - If test class name is null or empty
* - If test method name is null or empty
*/
CallableTestMethodImpl( final BundleContext bundleContext,
final String testClassName,
final String testMethodName )
{
validateNotNull( bundleContext, "Bundle context" );
validateNotEmpty( testClassName, true, "Test class name" );
validateNotEmpty( testMethodName, true, "Test method name" );
m_bundleContext = bundleContext;
m_testClassName = testClassName;
m_testMethodName = testMethodName;
}
/**
* {@inheritDoc}
*/
public void call()
throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException
{
final Class testClass = m_bundleContext.getBundle().loadClass( m_testClassName );
int encountered = 0;
for( final Method testMethod : testClass.getMethods() )
{
if( testMethod.getName().equals( m_testMethodName ) )
{
injectContextAndInvoke( testClass.newInstance(), testMethod );
encountered++;
}
}
if( encountered == 0 )
{
throw new RuntimeException( " test " + m_testMethodName + " not found in test class " + testClass.getName() );
}
}
/**
* Invokes the bundle context (if possible and required) and executes the test method.
*
* @param testInstance an instance of the test class
* @param testMethod test method
*
* @throws IllegalAccessException - Re-thrown from reflection invokation
* @throws InvocationTargetException - Re-thrown from reflection invokation
*/
private void injectContextAndInvoke( final Object testInstance,
final Method testMethod )
throws IllegalAccessException, InvocationTargetException
{
final Class<?>[] paramTypes = testMethod.getParameterTypes();
injectFieldInstances( testInstance.getClass(), testInstance );
boolean cleanup = false;
try
{
runBefores( testInstance );
// if there is only one param and is of type BundleContext we inject it, otherwise just call
// this means that if there are actual params the call will fail, but that is okay as it will be reported back
if( paramTypes.length == 1
&& paramTypes[ 0 ].isAssignableFrom( BundleContext.class ) )
{
testMethod.invoke( testInstance, m_bundleContext );
}
else
{
testMethod.invoke( testInstance );
}
cleanup = true;
runAfters( testInstance );
}
finally
{
if( !cleanup )
{
try
{
runAfters( testInstance );
} catch( Throwable throwable )
{
LOG.warn( "Got the exception when calling the runAfters. [Exception]: " + throwable );
}
}
}
}
/**
* Run all methods annotated with {@link Before}.
*
* @param testInstance an instance of the test class (cannot be null)
*
* @throws IllegalAccessException - Re-thrown from reflection invokation
* @throws InvocationTargetException - Re-thrown from reflection invokation
*/
private void runBefores( final Object testInstance )
throws IllegalAccessException, InvocationTargetException
{
for( final Method beforeMethod : getAnnotatedMethods( testInstance.getClass(), Before.class ) )
{
final Class<?>[] paramTypes = beforeMethod.getParameterTypes();
if( paramTypes.length == 1
&& paramTypes[ 0 ].isAssignableFrom( BundleContext.class ) )
{
beforeMethod.invoke( testInstance, m_bundleContext );
}
else
{
beforeMethod.invoke( testInstance );
}
}
}
/**
* Run all methods annotated with {@link After}.
*
* @param testInstance an instance of the test class (cannot be null)
*
* @throws IllegalAccessException - Re-thrown from reflection invokation
* @throws InvocationTargetException - Re-thrown from reflection invokation
*/
private void runAfters( final Object testInstance )
throws IllegalAccessException, InvocationTargetException
{
for( final Method afterMethod : getAnnotatedMethods( testInstance.getClass(), After.class ) )
{
final Class<?>[] paramTypes = afterMethod.getParameterTypes();
if( paramTypes.length == 1
&& paramTypes[ 0 ].isAssignableFrom( BundleContext.class ) )
{
afterMethod.invoke( testInstance, m_bundleContext );
}
else
{
afterMethod.invoke( testInstance );
}
}
}
/**
* Injects instances into fields found in testInstance and its superclases.
*
* @param clazz
* @param inst
*/
private void injectFieldInstances( Class clazz, Object inst )
throws IllegalAccessException
{
if( clazz.getSuperclass() != null )
{
injectFieldInstances( clazz.getSuperclass(), inst );
}
for( Field field : clazz.getDeclaredFields() )
{
setIfMatching( inst, field, m_bundleContext );
}
}
/**
* @param testInstance object instance where you found field
* @param field field that is going to be set
* @param o target value of field
*/
private void setIfMatching( Object testInstance, Field field, Object o )
throws IllegalAccessException
{
if( isInjectionField( field ) && isMatchingType( field, o.getClass() ) )
{
field.setAccessible( true );
field.set( testInstance, o );
}
}
/**
* Just checks if type of field is a assignable from clazz.
*
* @param field
* @param clazz
*/
private boolean isMatchingType( Field field, Class clazz )
{
boolean result = field.getType().isAssignableFrom( clazz );
LOG.debug( "Trying to match " + field.getType() + " with injection " + clazz.getName() + ": " + result);
return result;
}
/**
* Tests if the given field has the {@link @Inject} annotation.
* Due to some osgi quirks, currently direct getAnnotation( Inject.class ) does not work..:(
*
* @param field field to be tested
*
* @return trze if it has the Inject annotation. Otherwise false.
*/
public boolean isInjectionField( Field field )
{
// Usually, this should be enough.
if( field.getAnnotation( Inject.class ) != null )
{
return true;
}
else
{
// the above one fails in some cases currently (returns null) while annotation is there.
// So this is a fallback:
for( Annotation annot : field.getAnnotations() )
{
if( annot.annotationType().getName().equals( Inject.class.getName() ) )
{
return true;
}
}
}
return false;
}
/**
* Find all methods marked with a specific annotation.
*
* @param testClass class to be inspected
* @param annotationClass annotation class to be found
*
* @return list of annotated methods (cannot be null)
*/
public List<Method> getAnnotatedMethods( final Class testClass,
final Class<? extends Annotation> annotationClass )
{
final List<Method> results = new ArrayList<Method>();
for( final Class<?> clazz : getSuperClasses( testClass ) )
{
final Method[] methods = clazz.getDeclaredMethods();
for( final Method method : methods )
{
final Annotation annotation = method.getAnnotation( annotationClass );
if( annotation != null )
{
results.add( method );
}
}
}
return results;
}
/**
* Finds all superclasses of a certain class including itself.
*
* @param testClass class whom superclasses should be found
*
* @return list of superclasses (cannot be null)
*/
private List<Class<?>> getSuperClasses( final Class<?> testClass )
{
final ArrayList<Class<?>> results = new ArrayList<Class<?>>();
Class<?> current = testClass;
while( current != null )
{
results.add( current );
current = current.getSuperclass();
}
return results;
}
}