org.unitils.spring.SpringModule.java Source code

Java tutorial

Introduction

Here is the source code for org.unitils.spring.SpringModule.java

Source

/*
 * 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<?> platformTransactionManagerClass = 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);
        }
    }

}