/*
* Copyright 2008, Unitils.org
*
* 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.unitils.spring;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.unitils.util.AnnotationUtils.getFieldsAnnotatedWith;
import static org.unitils.util.AnnotationUtils.getMethodOrClassLevelAnnotationProperty;
import static org.unitils.util.AnnotationUtils.getMethodsAnnotatedWith;
import static org.unitils.util.PropertyUtils.getInstance;
import static org.unitils.util.ReflectionUtils.getPropertyName;
import static org.unitils.util.ReflectionUtils.invokeMethod;
import static org.unitils.util.ReflectionUtils.isSetter;
import static org.unitils.util.ReflectionUtils.setFieldValue;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.transaction.PlatformTransactionManager;
import org.unitils.core.Module;
import org.unitils.core.TestListener;
import org.unitils.core.Unitils;
import org.unitils.core.UnitilsException;
import org.unitils.database.DatabaseModule;
import org.unitils.database.annotations.Transactional;
import org.unitils.database.transaction.impl.UnitilsTransactionManagementConfiguration;
import org.unitils.spring.annotation.SpringApplicationContext;
import org.unitils.spring.annotation.SpringBean;
import org.unitils.spring.annotation.SpringBeanByName;
import org.unitils.spring.annotation.SpringBeanByType;
import org.unitils.spring.util.ApplicationContextFactory;
import org.unitils.spring.util.ApplicationContextManager;
import org.unitils.util.ReflectionUtils;
/**
* A module for Spring enabling a test class by offering an easy way to load application contexts and
* an easy way of retrieving beans from the context and injecting them in the test.
* <p/>
* The application context loading can be achieved by using the {@link SpringApplicationContext} annotation. These
* contexts are cached, so a context will be reused when possible. For example suppose a superclass loads a context and
* a test-subclass wants to use this context, it will not create a new one. {@link #invalidateApplicationContext} }
* can be used to force a reloading of a context if needed.
* <p/>
* Spring bean retrieval can be done by annotating the corresponding fields in the test with following
* annotations: {@link SpringBean}, {@link SpringBeanByName} and {@link SpringBeanByType}.
* <p/>
* See the javadoc of these annotations for more info on how you can use them.
*
* @author Tim Ducheyne
* @author Filip Neven
*/
public class SpringModule implements Module {
/* Property key of the class name of the application context factory */
public static final String PROPKEY_APPLICATION_CONTEXT_FACTORY_CLASS_NAME = "SpringModule.applicationContextFactory.implClassName";
/* Manager for storing and creating spring application contexts */
private ApplicationContextManager applicationContextManager;
/* TestContext used by the spring testcontext framework*/
// private TestContext testContext;
/**
* Initializes this module using the given configuration
*
* @param configuration The configuration, not null
*/
public void init(Properties configuration) {
// create application context manager that stores and creates the application contexts
ApplicationContextFactory applicationContextFactory = getInstance(PROPKEY_APPLICATION_CONTEXT_FACTORY_CLASS_NAME, configuration);
applicationContextManager = new ApplicationContextManager(applicationContextFactory);
}
/**
* No after initialization needed for this module
*/
public void afterInit() {
// Make sure that, if a custom transaction manager is configured in the spring ApplicationContext associated with
// the current test, it is used for managing transactions.
if (isDatabaseModuleEnabled()) {
getDatabaseModule().registerTransactionManagementConfiguration(new UnitilsTransactionManagementConfiguration() {
public boolean isApplicableFor(Object testObject) {
if (!isApplicationContextConfiguredFor(testObject)) {
return false;
}
ApplicationContext context = getApplicationContext(testObject);
return context.getBeansOfType(getPlatformTransactionManagerClass()).size() != 0;
}
@SuppressWarnings("unchecked")
public PlatformTransactionManager getSpringPlatformTransactionManager(Object testObject) {
ApplicationContext context = getApplicationContext(testObject);
Class<PlatformTransactionManager> platformTransactionManagerClass =
(Class<PlatformTransactionManager>) getPlatformTransactionManagerClass();
Map<String, PlatformTransactionManager> platformTransactionManagers = (Map<String, PlatformTransactionManager>) context.getBeansOfType(platformTransactionManagerClass);
if (platformTransactionManagers.size() == 0) {
throw new UnitilsException("Could not find a bean of type " + platformTransactionManagerClass.getSimpleName()
+ " in the spring ApplicationContext for this class");
}
if (platformTransactionManagers.size() > 1) {
Method testMethod = Unitils.getInstance().getTestContext().getTestMethod();
String transactionManagerName = getMethodOrClassLevelAnnotationProperty(Transactional.class, "transactionManagerName", "",
testMethod, testObject.getClass());
if (isEmpty(transactionManagerName))
throw new UnitilsException("Found more than one bean of type " + platformTransactionManagerClass.getSimpleName()
+ " in the spring ApplicationContext for this class. Use the transactionManagerName on the @Transactional"
+ " annotation to select the correct one.");
if (!platformTransactionManagers.containsKey(transactionManagerName))
throw new UnitilsException("No bean of type " + platformTransactionManagerClass.getSimpleName()
+ " found in the spring ApplicationContext with the name " + transactionManagerName);
return platformTransactionManagers.get(transactionManagerName);
}
return platformTransactionManagers.values().iterator().next();
}
public boolean isTransactionalResourceAvailable(Object testObject) {
return true;
}
public Integer getPreference() {
return 20;
}
protected Class<?> getPlatformTransactionManagerClass() {
return ReflectionUtils.getClassWithName("org.springframework.transaction.PlatformTransactionManager");
}
});
}
}
/**
* Gets the spring bean with the given name. The given test instance, by using {@link SpringApplicationContext},
* determines the application context in which to look for the bean.
* <p/>
* A UnitilsException is thrown when the no bean could be found for the given name.
*
* @param testObject The test instance, not null
* @param name The name, not null
* @return The bean, not null
*/
public Object getSpringBean(Object testObject, String name) {
try {
return getApplicationContext(testObject).getBean(name);
} catch (BeansException e) {
throw new UnitilsException("Unable to get Spring bean. No Spring bean found for name " + name);
}
}
/**
* Gets the spring bean with the given type. The given test instance, by using {@link SpringApplicationContext},
* determines the application context in which to look for the bean.
* If more there is not exactly 1 possible bean assignment, an UnitilsException will be thrown.
*
* @param testObject The test instance, not null
* @param type The type, not null
* @return The bean, not null
*/
public <T> T getSpringBeanByType(Object testObject, Class<T> type) {
Map<String, T> beans = getApplicationContext(testObject).getBeansOfType(type);
if (beans == null || beans.size() == 0) {
throw new UnitilsException("Unable to get Spring bean by type. No Spring bean found for type " + type.getSimpleName());
}
if (beans.size() > 1) {
throw new UnitilsException("Unable to get Spring bean by type. More than one possible Spring bean for type " + type.getSimpleName() + ". Possible beans; " + beans);
}
return beans.values().iterator().next();
}
/**
* @param testObject The test object
* @return Whether an ApplicationContext has been configured for the given testObject
*/
public boolean isApplicationContextConfiguredFor(Object testObject) {
//checkForIncompatibleUse(testObject);
return applicationContextManager.hasApplicationContext(testObject);
}
/**
* Gets the application context for this test. A new one will be created if it does not exist yet. If a superclass
* has also declared the creation of an application context, this one will be retrieved (or created if it was not
* created yet) and used as parent context for this classes context.
* <p/>
* If needed, an application context will be created using the settings of the {@link SpringApplicationContext}
* annotation.
* <p/>
* If a class level {@link SpringApplicationContext} annotation is found, the passed locations will be loaded using
* a <code>ClassPathXmlApplicationContext</code>.
* Custom creation methods can be created by annotating them with {@link SpringApplicationContext}. They
* should have an <code>ApplicationContext</code> as return type and either no or exactly 1 argument of type
* <code>ApplicationContext</code>. In the latter case, the current configured application context is passed as the argument.
* <p/>
* A UnitilsException will be thrown if no context could be retrieved or created.
*
* @param testObject The test instance, not null
* @return The application context, not null
*/
public ApplicationContext getApplicationContext(Object testObject) {
// Verify if the spring testcontext framework is used, and if an ApplicationContext has been configured
// using @ContextConfiguration. If yes, any unitils specific configured ApplicationContext is ignored
/*checkForIncompatibleUse(testObject);
if (isContextConfigurationAnnotationAvailable(testObject)) {
try {
return testContext.getApplicationContext();
} catch (Exception e) {
throw new UnitilsException(e);
}
}*/
return applicationContextManager.getApplicationContext(testObject);
}
/**
* Verify that the spring testcontext framework and unitils are not used together in an incompatible
* way: Check if not using the unitils core module system, and spring's @ContextConfiguration annotation for
* configuring the applicationcontext
*
* @param testObject The test instance, not null
*/
/*protected void checkForIncompatibleUse(Object testObject) {
if (isContextConfigurationAnnotationAvailable(testObject) && testContext == null) {
throw new UnitilsException("You've annotated your class with @" + ContextConfiguration.class.getSimpleName()
+ " but you're not using one of spring's base classes to execute your test");
}
}*/
/**
* @param testObject The test instance, not null
*
* @return Whether an @ContextConfiguration annotation can be found somewhere in the hierarchy
*/
/*protected boolean isContextConfigurationAnnotationAvailable(Object testObject) {
ContextConfiguration contextConfigurationAnnotation = AnnotationUtils.getClassLevelAnnotation(
ContextConfiguration.class, testObject.getClass());
return contextConfigurationAnnotation != null;
}*/
/**
* Forces the reloading of the application context the next time that it is requested. If classes are given
* only contexts that are linked to those classes will be reset. If no classes are given, all cached
* contexts will be reset.
*
* @param classes The classes for which to reset the contexts
*/
public void invalidateApplicationContext(Class<?>... classes) {
applicationContextManager.invalidateApplicationContext(classes);
}
/**
* Gets the application context for this class and sets it on the fields and setter methods that are
* annotated with {@link SpringApplicationContext}. If no application context could be created, an
* UnitilsException will be raised.
*
* @param testObject The test instance, not null
*/
public void injectApplicationContext(Object testObject) {
// inject into fields annotated with @SpringApplicationContext
Set<Field> fields = getFieldsAnnotatedWith(testObject.getClass(), SpringApplicationContext.class);
for (Field field : fields) {
try {
setFieldValue(testObject, field, getApplicationContext(testObject));
} catch (UnitilsException e) {
throw new UnitilsException("Unable to assign the application context to field annotated with @" + SpringApplicationContext.class.getSimpleName(), e);
}
}
// inject into setter methods annotated with @SpringApplicationContext
Set<Method> methods = getMethodsAnnotatedWith(testObject.getClass(), SpringApplicationContext.class, false);
for (Method method : methods) {
// ignore custom create methods
if (method.getReturnType() != Void.TYPE) {
continue;
}
try {
invokeMethod(testObject, method, getApplicationContext(testObject));
} catch (Exception e) {
throw new UnitilsException("Unable to assign the application context to setter annotated with @" + SpringApplicationContext.class.getSimpleName(), e);
}
}
}
/**
* Injects spring beans into all fields that are annotated with {@link SpringBean}.
*
* @param testObject The test instance, not null
*/
public void injectSpringBeans(Object testObject) {
// assign to fields
Set<Field> fields = getFieldsAnnotatedWith(testObject.getClass(), SpringBean.class);
for (Field field : fields) {
try {
SpringBean springBeanAnnotation = field.getAnnotation(SpringBean.class);
setFieldValue(testObject, field, getSpringBean(testObject, springBeanAnnotation.value()));
} catch (UnitilsException e) {
throw new UnitilsException("Unable to assign the Spring bean value to field annotated with @" + SpringBean.class.getSimpleName(), e);
}
}
// assign to setters
Set<Method> methods = getMethodsAnnotatedWith(testObject.getClass(), SpringBean.class);
for (Method method : methods) {
try {
if (!isSetter(method)) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBean.class.getSimpleName() + ". Method " +
method.getName() + " is not a setter method.");
}
SpringBean springBeanAnnotation = method.getAnnotation(SpringBean.class);
invokeMethod(testObject, method, getSpringBean(testObject, springBeanAnnotation.value()));
} catch (UnitilsException e) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBean.class.getSimpleName(), e);
} catch (InvocationTargetException e) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBean.class.getSimpleName() + ". Method " +
"has thrown an exception.", e.getCause());
}
}
}
/**
* Injects spring beans into all fields methods that are annotated with {@link SpringBeanByType}.
*
* @param testObject The test instance, not null
*/
public void injectSpringBeansByType(Object testObject) {
// assign to fields
Set<Field> fields = getFieldsAnnotatedWith(testObject.getClass(), SpringBeanByType.class);
for (Field field : fields) {
try {
setFieldValue(testObject, field, getSpringBeanByType(testObject, field.getType()));
} catch (UnitilsException e) {
throw new UnitilsException("Unable to assign the Spring bean value to field annotated with @" + SpringBeanByType.class.getSimpleName(), e);
}
}
// assign to setters
Set<Method> methods = getMethodsAnnotatedWith(testObject.getClass(), SpringBeanByType.class);
for (Method method : methods) {
try {
if (!isSetter(method)) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBeanByType.class.getSimpleName() + ". Method " +
method.getName() + " is not a setter method.");
}
invokeMethod(testObject, method, getSpringBeanByType(testObject, method.getParameterTypes()[0]));
} catch (UnitilsException e) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBeanByType.class.getSimpleName(), e);
} catch (InvocationTargetException e) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBeanByType.class.getSimpleName() + ". Method " +
"has thrown an exception.", e.getCause());
}
}
}
/**
* Injects spring beans into all fields that are annotated with {@link SpringBeanByName}.
*
* @param testObject The test instance, not null
*/
public void injectSpringBeansByName(Object testObject) {
// assign to fields
Set<Field> fields = getFieldsAnnotatedWith(testObject.getClass(), SpringBeanByName.class);
for (Field field : fields) {
try {
setFieldValue(testObject, field, getSpringBean(testObject, field.getName()));
} catch (UnitilsException e) {
throw new UnitilsException("Unable to assign the Spring bean value to field annotated with @" + SpringBeanByName.class.getSimpleName(), e);
}
}
// assign to setters
Set<Method> methods = getMethodsAnnotatedWith(testObject.getClass(), SpringBeanByName.class);
for (Method method : methods) {
try {
if (!isSetter(method)) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBeanByName.class.getSimpleName() + ". Method " +
method.getName() + " is not a setter method.");
}
invokeMethod(testObject, method, getSpringBean(testObject, getPropertyName(method)));
} catch (UnitilsException e) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBeanByName.class.getSimpleName(), e);
} catch (InvocationTargetException e) {
throw new UnitilsException("Unable to assign the Spring bean value to method annotated with @" + SpringBeanByName.class.getSimpleName() + ". Method " +
"has thrown an exception.", e.getCause());
}
}
}
/*public void registerTestContext(TestContext testContext) {
this.testContext = testContext;
}*/
protected void closeApplicationContextIfNeeded(Object testObject) {
if (this.isApplicationContextConfiguredFor(testObject)) {
this.invalidateApplicationContext(testObject.getClass());
}
}
protected boolean isDatabaseModuleEnabled() {
return Unitils.getInstance().getModulesRepository().isModuleEnabled(DatabaseModule.class);
}
protected DatabaseModule getDatabaseModule() {
return Unitils.getInstance().getModulesRepository().getModuleOfType(DatabaseModule.class);
}
/**
* @return The {@link TestListener} for this module
*/
public TestListener getTestListener() {
return new SpringTestListener();
}
/**
* The {@link TestListener} for this module
*/
protected class SpringTestListener extends TestListener {
@Override
public void beforeTestSetUp(Object testObject, Method testMethod) {
injectApplicationContext(testObject);
injectSpringBeans(testObject);
injectSpringBeansByType(testObject);
injectSpringBeansByName(testObject);
}
/**
* @see org.unitils.core.TestListener#afterTestTearDown(java.lang.Object, java.lang.reflect.Method)
*/
@Override
public void afterTestTearDown(Object testObject, Method testMethod) {
closeApplicationContextIfNeeded(testObject);
}
}
}