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