org.unitils.inject.InjectModule.java Source code

Java tutorial

Introduction

Here is the source code for org.unitils.inject.InjectModule.java

Source

/*
 *
 *  * Copyright 2010,  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.inject;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.unitils.core.Module;
import org.unitils.core.TestListener;
import org.unitils.core.UnitilsException;
import org.unitils.core.util.ObjectToInjectHolder;
import org.unitils.inject.annotation.*;
import org.unitils.inject.util.InjectionUtils;
import org.unitils.inject.util.PropertyAccess;
import org.unitils.inject.util.Restore;
import org.unitils.inject.util.ValueToRestore;
import org.unitils.util.PropertyUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;

import static java.lang.reflect.Modifier.isAbstract;
import static org.unitils.util.AnnotationUtils.getFieldsAnnotatedWith;
import static org.unitils.util.ModuleUtils.getAnnotationPropertyDefaults;
import static org.unitils.util.ModuleUtils.getEnumValueReplaceDefault;
import static org.unitils.util.ReflectionUtils.*;

/**
 * Module for injecting annotated objects into other objects. The intended usage is to inject mock objects, but it can
 * be used for regular objects too.
 * <p/>
 * Both explicit injection and automatic injection by type are supported. An object annotated with {@link InjectInto} is
 * explicitly injected into a target object. An object annotated with {@link InjectIntoByType} is automatically injected into a
 * target property with the same type as the declared type of the annotated object.
 * <p/>
 * Explicit and automatic injection into static fields is also supported, by means of the {@link InjectIntoStatic} and {@link
 * InjectIntoStaticByType} annotations.
 * <p/>
 * The target object can either be specified explicitly, or implicitly by annotating an object with {@link TestedObject}
 *
 * @author Filip Neven
 * @author Tim Ducheyne
 */
public class InjectModule implements Module {

    /* The logger instance for this class */
    private static Log logger = LogFactory.getLog(InjectModule.class);

    /* Property key indicating if the tested objects should automatically be created if they are not created yet */
    private static final String PROPKEY_CREATE_TESTEDOBJECTS_IF_NULL_ENABLED = "InjectModule.TestedObject.createIfNull.enabled";

    /* Map holding the default configuration of the inject annotations */
    private Map<Class<? extends Annotation>, Map<String, String>> defaultAnnotationPropertyValues;
    /* List holding all values to restore after test was performed */
    private List<ValueToRestore> valuesToRestoreAfterTest = new ArrayList<ValueToRestore>();

    /* Indicates if tested object instance should be created if they are not created yet */
    private boolean createTestedObjectsIfNullEnabled;

    /**
     * Initializes this module using the given configuration.
     *
     * @param configuration The configuration, not null
     */
    public void init(Properties configuration) {
        defaultAnnotationPropertyValues = getAnnotationPropertyDefaults(InjectModule.class, configuration,
                InjectInto.class, InjectIntoStatic.class, InjectIntoByType.class, InjectIntoStaticByType.class);
        createTestedObjectsIfNullEnabled = PropertyUtils.getBoolean(PROPKEY_CREATE_TESTEDOBJECTS_IF_NULL_ENABLED,
                configuration);
    }

    /**
     * No after initialization needed for this module
     */
    public void afterInit() {
    }

    /**
     * For all fields annotated with {@link TestedObject} that are still null after the test fixture, an object is
     * created of the field's declared type and assigned to the field. If the field's declared type is an interface or
     * abstract class, or if the type doesn't have a default constructor, a warning is produced.
     *
     * @param testObject The test instance, not null
     */
    public void createTestedObjectsIfNull(Object testObject) {
        Set<Field> testedObjectFields = getFieldsAnnotatedWith(testObject.getClass(), TestedObject.class);
        for (Field testedObjectField : testedObjectFields) {
            if (getFieldValue(testObject, testedObjectField) == null) {
                createObjectForField(testObject, testedObjectField);
            }
        }
    }

    /**
     * Creates an objects of the given fields' declared type and assigns it to this field on the given testObject
     *
     * @param testObject        The test instance, not null
     * @param testedObjectField The tested object field, not null
     */
    protected void createObjectForField(Object testObject, Field testedObjectField) {
        Class<?> declaredClass = testedObjectField.getType();
        if (declaredClass.isInterface()) {
            logger.warn("Field " + testedObjectField.getName() + " (annotated with @TestedObject) has type "
                    + testedObjectField.getType().getSimpleName()
                    + " which is an interface type. It is not automatically instantiated.");

        } else if (isAbstract(declaredClass.getModifiers())) {
            logger.warn("Field " + testedObjectField.getName() + " (annotated with @TestedObject) has type "
                    + testedObjectField.getDeclaringClass().getSimpleName()
                    + " which is an abstract class. It is not automatically instantiated.");

        } else {
            try {
                declaredClass.getDeclaredConstructor();
                Object instance = createInstanceOfType(declaredClass, true);
                setFieldValue(testObject, testedObjectField, instance);
            } catch (NoSuchMethodException e) {
                logger.warn("Field " + testedObjectField.getName() + " (annotated with @TestedObject) has type "
                        + testedObjectField.getDeclaringClass().getSimpleName()
                        + " which has no default (parameterless) constructor. It is not automatically instantiated.");
            }
        }
    }

    /**
     * Performs all supported kinds of injection on the given object's fields
     *
     * @param test The instance to inject into, not null
     */
    public void injectObjects(Object test) {
        injectAll(test);
        injectAllByType(test);
        injectAllStatic(test);
        injectAllStaticByType(test);
    }

    /**
     * Injects all fields that are annotated with {@link InjectInto}.
     *
     * @param test The instance to inject into, not null
     */
    public void injectAll(Object test) {
        Set<Field> fields = getFieldsAnnotatedWith(test.getClass(), InjectInto.class);
        for (Field field : fields) {
            inject(test, field);
        }
    }

    /**
     * Auto-injects all fields that are annotated with {@link InjectIntoByType}
     *
     * @param test The instance to inject into, not null
     */
    public void injectAllByType(Object test) {
        Set<Field> fields = getFieldsAnnotatedWith(test.getClass(), InjectIntoByType.class);
        for (Field field : fields) {
            injectByType(test, field);
        }
    }

    /**
     * Injects all fields that are annotated with {@link InjectIntoStatic}.
     *
     * @param test The instance to inject into, not null
     */
    public void injectAllStatic(Object test) {
        Set<Field> fields = getFieldsAnnotatedWith(test.getClass(), InjectIntoStatic.class);
        for (Field field : fields) {
            injectStatic(test, field);
        }
    }

    /**
     * Auto-injects all fields that are annotated with {@link InjectIntoStaticByType}
     *
     * @param test The instance to inject into, not null
     */
    public void injectAllStaticByType(Object test) {
        Set<Field> fields = getFieldsAnnotatedWith(test.getClass(), InjectIntoStaticByType.class);
        for (Field field : fields) {
            injectStaticByType(test, field);
        }
    }

    /**
     * Restores the values that were stored using {@link #storeValueToRestoreAfterTest(Class, String, Class, org.unitils.inject.util.PropertyAccess, Object, org.unitils.inject.util.Restore)}.
     */
    public void restoreStaticInjectedObjects() {
        for (ValueToRestore valueToRestore : valuesToRestoreAfterTest) {
            restore(valueToRestore);
        }
    }

    /**
     * Injects the fieldToInject. The target is either an explicitly specified target field of the test, or into the
     * field(s) that is/are annotated with {@link TestedObject}
     *
     * @param test          The instance to inject into, not null
     * @param fieldToInject The field from which the value is injected into the target, not null
     */
    protected void inject(Object test, Field fieldToInject) {
        InjectInto injectIntoAnnotation = fieldToInject.getAnnotation(InjectInto.class);

        String ognlExpression = injectIntoAnnotation.property();
        if (StringUtils.isEmpty(ognlExpression)) {
            throw new UnitilsException(
                    getSituatedErrorMessage(InjectInto.class, fieldToInject, "Property cannot be empty"));
        }
        Object objectToInject = getObjectToInject(test, fieldToInject);

        List<Object> targets = getTargets(InjectInto.class, fieldToInject, injectIntoAnnotation.target(), test);
        if (targets.size() == 0) {
            throw new UnitilsException(getSituatedErrorMessage(InjectInto.class, fieldToInject,
                    "The target should either be specified explicitly using the target property, or by using the @"
                            + TestedObject.class.getSimpleName() + " annotation"));
        }
        for (Object target : targets) {
            try {
                InjectionUtils.injectInto(objectToInject, target, ognlExpression);

            } catch (UnitilsException e) {
                throw new UnitilsException(getSituatedErrorMessage(InjectInto.class, fieldToInject, e.getMessage()),
                        e);
            }
        }
    }

    /**
     * Injects the fieldToAutoInjectStatic into the specified target class.
     *
     * @param test                Instance to inject into, not null
     * @param fieldToInjectStatic The field from which the value is injected into the target, not null
     */
    protected void injectStatic(Object test, Field fieldToInjectStatic) {
        InjectIntoStatic injectIntoStaticAnnotation = fieldToInjectStatic.getAnnotation(InjectIntoStatic.class);

        Class<?> targetClass = injectIntoStaticAnnotation.target();
        String property = injectIntoStaticAnnotation.property();
        if (StringUtils.isEmpty(property)) {
            throw new UnitilsException(getSituatedErrorMessage(InjectIntoStatic.class, fieldToInjectStatic,
                    "Property cannot be empty"));
        }
        Object objectToInject = getObjectToInject(test, fieldToInjectStatic);

        Restore restore = getEnumValueReplaceDefault(InjectIntoStatic.class, "restore",
                injectIntoStaticAnnotation.restore(), defaultAnnotationPropertyValues);
        try {
            Object oldValue = InjectionUtils.injectIntoStatic(objectToInject, targetClass, property);
            storeValueToRestoreAfterTest(targetClass, property, fieldToInjectStatic.getType(), null, oldValue,
                    restore);

        } catch (UnitilsException e) {
            throw new UnitilsException(
                    getSituatedErrorMessage(InjectIntoStatic.class, fieldToInjectStatic, e.getMessage()), e);
        }
    }

    /**
     * Auto-injects the fieldToInject by trying to match the fields declared type with a property of the target.
     * The target is either an explicitly specified target field of the test, or the field(s) that is/are annotated with
     * {@link TestedObject}
     *
     * @param test          The instance to inject into, not null
     * @param fieldToInject The field from which the value is injected into the target, not null
     */
    protected void injectByType(Object test, Field fieldToInject) {
        InjectIntoByType injectIntoByTypeAnnotation = fieldToInject.getAnnotation(InjectIntoByType.class);

        Object objectToInject = getObjectToInject(test, fieldToInject);
        Type objectToInjectType = getObjectToInjectType(test, fieldToInject);
        PropertyAccess propertyAccess = getEnumValueReplaceDefault(InjectIntoByType.class, "propertyAccess",
                injectIntoByTypeAnnotation.propertyAccess(), defaultAnnotationPropertyValues);

        List<Object> targets = getTargets(InjectIntoByType.class, fieldToInject,
                injectIntoByTypeAnnotation.target(), test);
        if (targets.size() == 0) {
            throw new UnitilsException(getSituatedErrorMessage(InjectIntoByType.class, fieldToInject,
                    "The target should either be specified explicitly using the target property, or by using the @"
                            + TestedObject.class.getSimpleName() + " annotation"));
        }
        for (Object target : targets) {
            try {
                InjectionUtils.injectIntoByType(objectToInject, objectToInjectType, target, propertyAccess);

            } catch (UnitilsException e) {
                throw new UnitilsException(
                        getSituatedErrorMessage(InjectIntoByType.class, fieldToInject, e.getMessage()), e);
            }
        }
    }

    /**
     * Auto-injects the fieldToInject by trying to match the fields declared type with a property of the target class.
     * The target is either an explicitly specified target field of the test, or the field that is annotated with
     * {@link TestedObject}
     *
     * @param test                    The instance to inject into, not null
     * @param fieldToAutoInjectStatic The field from which the value is injected into the target, not null
     */
    protected void injectStaticByType(Object test, Field fieldToAutoInjectStatic) {
        InjectIntoStaticByType injectIntoStaticByTypeAnnotation = fieldToAutoInjectStatic
                .getAnnotation(InjectIntoStaticByType.class);

        Class<?> targetClass = injectIntoStaticByTypeAnnotation.target();
        Object objectToInject = getObjectToInject(test, fieldToAutoInjectStatic);
        Type objectToInjectType = getObjectToInjectType(test, fieldToAutoInjectStatic);
        Class objectToInjectClass = getClassForType(objectToInjectType);

        Restore restore = getEnumValueReplaceDefault(InjectIntoStaticByType.class, "restore",
                injectIntoStaticByTypeAnnotation.restore(), defaultAnnotationPropertyValues);
        PropertyAccess propertyAccess = getEnumValueReplaceDefault(InjectIntoStaticByType.class, "propertyAccess",
                injectIntoStaticByTypeAnnotation.propertyAccess(), defaultAnnotationPropertyValues);
        try {
            Object oldValue = InjectionUtils.injectIntoStaticByType(objectToInject, objectToInjectType, targetClass,
                    propertyAccess);
            storeValueToRestoreAfterTest(targetClass, null, objectToInjectClass, propertyAccess, oldValue, restore);

        } catch (UnitilsException e) {
            throw new UnitilsException(
                    getSituatedErrorMessage(InjectIntoStaticByType.class, fieldToAutoInjectStatic, e.getMessage()),
                    e);
        }
    }

    /**
     * Gets the value from the given field. If the value is a holder for an object to inject, the wrapped object is returned.
     * For example in case of a field declared as Mock<MyClass>, this will return the proxy of the mock instead of the mock itself.
     *
     * @param test          The test, not null
     * @param fieldToInject The field, not null
     * @return The object
     */
    protected Object getObjectToInject(Object test, Field fieldToInject) {
        Object fieldValue = getFieldValue(test, fieldToInject);
        if (fieldValue instanceof ObjectToInjectHolder<?>) {
            return ((ObjectToInjectHolder<?>) fieldValue).getObjectToInject();
        }
        return fieldValue;
    }

    /**
     * Gets the type of the given field. If the field is a holder for an object to inject, the wrapped type is returned.
     * For example in case of a field declared as Mock<MyClass>, this will return MyClass instead of Mock<MyClass>
     *
     * @param test          The test, not null
     * @param fieldToInject The field, not null
     * @return The object
     */
    protected Type getObjectToInjectType(Object test, Field fieldToInject) {
        Object fieldValue = getFieldValue(test, fieldToInject);
        if (fieldValue instanceof ObjectToInjectHolder<?>) {
            return ((ObjectToInjectHolder<?>) fieldValue).getObjectToInjectType(fieldToInject);
        }
        return fieldToInject.getType();
    }

    /**
     * Restores the given value.
     *
     * @param valueToRestore the value, not null
     */
    protected void restore(ValueToRestore valueToRestore) {
        Object value = valueToRestore.getValue();
        Class<?> targetClass = valueToRestore.getTargetClass();

        String property = valueToRestore.getProperty();
        if (property != null) {
            // regular injection
            InjectionUtils.injectIntoStatic(value, targetClass, property);

        } else {
            // auto injection
            InjectionUtils.injectIntoStaticByType(value, valueToRestore.getFieldType(), targetClass,
                    valueToRestore.getPropertyAccessType());
        }
    }

    /**
     * Stores the old value that was replaced during the injection so that it can be restored after the test was
     * performed. The value that is stored depends on the restore value: OLD_VALUE will store the value that was replaced,
     * NULL_OR_0_VALUE will store 0 or null depending whether it is a primitive or not, NO_RESTORE stores nothing.
     *
     * @param targetClass    The target class, not null
     * @param property       The OGNL expression that defines where the object will be injected, null for auto inject
     * @param fieldType      The type, not null
     * @param propertyAccess The access type in case auto injection is used
     * @param oldValue       The value that was replaced during the injection
     * @param restore        The type of reset, not DEFAULT
     */
    protected void storeValueToRestoreAfterTest(Class<?> targetClass, String property, Class<?> fieldType,
            PropertyAccess propertyAccess, Object oldValue, Restore restore) {
        if (Restore.NO_RESTORE == restore || Restore.DEFAULT == restore) {
            return;
        }

        ValueToRestore valueToRestore;
        if (Restore.OLD_VALUE == restore) {
            valueToRestore = new ValueToRestore(targetClass, property, fieldType, propertyAccess, oldValue);

        } else if (Restore.NULL_OR_0_VALUE == restore) {
            valueToRestore = new ValueToRestore(targetClass, property, fieldType, propertyAccess,
                    fieldType.isPrimitive() ? 0 : null);

        } else {
            throw new RuntimeException("Unknown value for " + Restore.class.getSimpleName() + " " + restore);
        }
        valuesToRestoreAfterTest.add(valueToRestore);
    }

    /**
     * Returns the target(s) for the injection, given the specified name of the target and the test object. If
     * targetName is not equal to an empty string, the targets are the testObject's fields that are annotated with
     * {@link TestedObject}.
     *
     * @param annotationClass The class of the annotation, not null
     * @param annotatedField  The annotated field, not null
     * @param targetName      The explicit target name or empty string for TestedObject targets
     * @param test            The test instance
     * @return The target(s) for the injection
     */
    protected List<Object> getTargets(Class<? extends Annotation> annotationClass, Field annotatedField,
            String targetName, Object test) {
        List<Object> targets;
        if ("".equals(targetName)) {
            // Default targetName, so it is probably not specified. Return all objects that are annotated with the TestedObject annotation.
            Set<Field> testedObjectFields = getFieldsAnnotatedWith(test.getClass(), TestedObject.class);
            targets = new ArrayList<Object>(testedObjectFields.size());
            for (Field testedObjectField : testedObjectFields) {
                Object target = getTarget(test, testedObjectField);
                targets.add(target);
            }
        } else {
            Field field = getFieldWithName(test.getClass(), targetName, false);
            if (field == null) {
                throw new UnitilsException(getSituatedErrorMessage(annotationClass, annotatedField,
                        "Target with name " + targetName + " does not exist"));
            }
            Object target = getTarget(test, field);
            targets = Collections.singletonList(target);
        }
        return targets;
    }

    protected Object getTarget(Object test, Field field) {
        Object target = getFieldValue(test, field);
        if (target instanceof ObjectToInjectHolder<?>) {
            target = ((ObjectToInjectHolder<?>) target).getObjectToInject();
        }
        return target;
    }

    /**
     * Given the errorDescription, returns a situated error message, i.e. specifying the annotated field and the
     * annotation type that was used.
     *
     * @param annotationClass  The injection annotation, not null
     * @param annotatedField   The annotated field, not null
     * @param errorDescription A custom description, not null
     * @return A situated error message
     */
    protected String getSituatedErrorMessage(Class<? extends Annotation> annotationClass, Field annotatedField,
            String errorDescription) {
        return "Error while processing @" + annotationClass.getSimpleName() + " annotation on field "
                + annotatedField.getName() + " of class " + annotatedField.getDeclaringClass().getSimpleName()
                + ": " + errorDescription;
    }

    /**
     * @return The {@link org.unitils.core.TestListener} for this module
     */
    public TestListener getTestListener() {
        return new InjectTestListener();
    }

    /**
     * The {@link org.unitils.core.TestListener} for this module
     */
    protected class InjectTestListener extends TestListener {

        /**
         * Before executing a test method (i.e. after the fixture methods), the injection is performed, since
         * objects to inject or targets are possibly instantiated during the fixture.
         *
         * @param testObject The test object, not null
         * @param testMethod The test method, not null
         */
        @Override
        public void beforeTestMethod(Object testObject, Method testMethod) {

            if (createTestedObjectsIfNullEnabled) {
                createTestedObjectsIfNull(testObject);
            }
            injectObjects(testObject);
        }

        /**
         * After test execution, if requested restore all values that were replaced in the static injection.
         *
         * @param testObject The test object, not null
         * @param testMethod The test method, not null
         * @param throwable  The throwable thrown during the test, null if none was thrown
         */
        @Override
        public void afterTestMethod(Object testObject, Method testMethod, Throwable throwable) {
            restoreStaticInjectedObjects();
        }
    }

}