001: /*
002: * Copyright 2006-2007, Unitils.org
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.unitils.inject;
017:
018: import static org.unitils.util.AnnotationUtils.getFieldsAnnotatedWith;
019: import static org.unitils.util.ModuleUtils.getAnnotationPropertyDefaults;
020: import static org.unitils.util.ModuleUtils.getEnumValueReplaceDefault;
021: import static org.unitils.util.ReflectionUtils.createInstanceOfType;
022: import static org.unitils.util.ReflectionUtils.getFieldValue;
023: import static org.unitils.util.ReflectionUtils.getFieldWithName;
024: import static org.unitils.util.ReflectionUtils.setFieldValue;
025:
026: import java.lang.annotation.Annotation;
027: import java.lang.reflect.Field;
028: import java.lang.reflect.Method;
029: import java.lang.reflect.Modifier;
030: import java.util.ArrayList;
031: import java.util.Collections;
032: import java.util.List;
033: import java.util.Map;
034: import java.util.Properties;
035:
036: import org.apache.commons.lang.StringUtils;
037: import org.apache.commons.logging.Log;
038: import org.apache.commons.logging.LogFactory;
039: import org.unitils.core.Module;
040: import org.unitils.core.TestListener;
041: import org.unitils.core.UnitilsException;
042: import org.unitils.inject.annotation.InjectInto;
043: import org.unitils.inject.annotation.InjectIntoByType;
044: import org.unitils.inject.annotation.InjectIntoStatic;
045: import org.unitils.inject.annotation.InjectIntoStaticByType;
046: import org.unitils.inject.annotation.TestedObject;
047: import org.unitils.inject.util.InjectionUtils;
048: import org.unitils.inject.util.PropertyAccess;
049: import org.unitils.inject.util.Restore;
050: import org.unitils.inject.util.ValueToRestore;
051: import org.unitils.util.PropertyUtils;
052:
053: /**
054: * Module for injecting annotated objects into other objects. The intended usage is to inject mock objects, but it can
055: * be used for regular objects too.
056: * <p/>
057: * Both explicit injection and automatic injection by type are supported. An object annotated with {@link InjectInto} is
058: * explicitly injected into a target object. An object annotated with {@link InjectIntoByType} is automatically injected into a
059: * target property with the same type as the declared type of the annotated object.
060: * <p/>
061: * Explicit and automatic injection into static fields is also supported, by means of the {@link InjectIntoStatic} and {@link
062: * InjectIntoStaticByType} annotations.
063: * <p/>
064: * The target object can either be specified explicitly, or implicitly by annotating an object with {@link TestedObject}
065: *
066: * @author Filip Neven
067: * @author Tim Ducheyne
068: */
069: public class InjectModule implements Module {
070:
071: /* The logger instance for this class */
072: private static Log logger = LogFactory.getLog(InjectModule.class);
073:
074: /* Property key indicating if the tested objects should automatically be created if they are not created yet */
075: private static final String PROPKEY_CREATE_TESTEDOBJECTS_IF_NULL_ENABLED = "InjectModule.TestedObject.createIfNull.enabled";
076:
077: /* Map holding the default configuration of the inject annotations */
078: private Map<Class<? extends Annotation>, Map<String, String>> defaultAnnotationPropertyValues;
079:
080: /* List holding all values to restore after test was performed */
081: private List<ValueToRestore> valuesToRestoreAfterTest = new ArrayList<ValueToRestore>();
082:
083: /* Indicates if tested object instance should be created if they are not created yet */
084: private boolean createTestedObjectsIfNullEnabled;
085:
086: /**
087: * Initializes this module using the given configuration.
088: *
089: * @param configuration The configuration, not null
090: */
091: public void init(Properties configuration) {
092: defaultAnnotationPropertyValues = getAnnotationPropertyDefaults(
093: InjectModule.class, configuration, InjectInto.class,
094: InjectIntoStatic.class, InjectIntoByType.class,
095: InjectIntoStaticByType.class);
096: createTestedObjectsIfNullEnabled = PropertyUtils.getBoolean(
097: PROPKEY_CREATE_TESTEDOBJECTS_IF_NULL_ENABLED,
098: configuration);
099: }
100:
101: /**
102: * For all fields annotated with {@link TestedObject} that are still null after the test fixture, an object is
103: * created of the field's declared type and assigned to the field. If the field's declared type is an interface or
104: * abstract class, or if the type doesn't have a default constructor, a warning is produced.
105: *
106: * @param testObject The test instance, not null
107: */
108: public void createTestedObjectsIfNull(Object testObject) {
109: List<Field> testedObjectFields = getFieldsAnnotatedWith(
110: testObject.getClass(), TestedObject.class);
111: for (Field testedObjectField : testedObjectFields) {
112: if (getFieldValue(testObject, testedObjectField) == null) {
113: createObjectForField(testObject, testedObjectField);
114: }
115: }
116: }
117:
118: /**
119: * Creates an objects of the given fields' declared type and assigns it to this field on the given testObject
120: *
121: * @param testObject The test instance, not null
122: * @param testedObjectField The tested object field, not null
123: */
124: protected void createObjectForField(Object testObject,
125: Field testedObjectField) {
126: Class<?> declaredClass = testedObjectField.getType();
127: if (declaredClass.isInterface()) {
128: logger
129: .warn("Field "
130: + testedObjectField.getName()
131: + " (annotated with @TestedObject) has type "
132: + testedObjectField.getType()
133: .getSimpleName()
134: + " which is an interface type. It is not automatically instantiated.");
135: } else if (Modifier.isAbstract(declaredClass.getModifiers())) {
136: logger
137: .warn("Field "
138: + testedObjectField.getName()
139: + " (annotated with @TestedObject) has type "
140: + testedObjectField.getDeclaringClass()
141: .getSimpleName()
142: + " which is an abstract class. It is not automatically instantiated.");
143: } else {
144: try {
145: declaredClass.getDeclaredConstructor();
146: Object instance = createInstanceOfType(declaredClass,
147: true);
148: setFieldValue(testObject, testedObjectField, instance);
149: } catch (NoSuchMethodException e) {
150: logger
151: .warn("Field "
152: + testedObjectField.getName()
153: + " (annotated with @TestedObject) has type "
154: + testedObjectField.getDeclaringClass()
155: .getSimpleName()
156: + " which has no default (parameterless) constructor. It is not automatically instantiated.");
157: }
158: }
159: }
160:
161: /**
162: * Performs all supported kinds of injection on the given object's fields
163: *
164: * @param test The instance to inject into, not null
165: */
166: public void injectObjects(Object test) {
167: injectAll(test);
168: injectAllByType(test);
169: injectAllStatic(test);
170: injectAllStaticByType(test);
171: }
172:
173: /**
174: * Injects all fields that are annotated with {@link InjectInto}.
175: *
176: * @param test The instance to inject into, not null
177: */
178: public void injectAll(Object test) {
179: List<Field> fields = getFieldsAnnotatedWith(test.getClass(),
180: InjectInto.class);
181: for (Field field : fields) {
182: inject(test, field);
183: }
184: }
185:
186: /**
187: * Auto-injects all fields that are annotated with {@link InjectIntoByType}
188: *
189: * @param test The instance to inject into, not null
190: */
191: public void injectAllByType(Object test) {
192: List<Field> fields = getFieldsAnnotatedWith(test.getClass(),
193: InjectIntoByType.class);
194: for (Field field : fields) {
195: injectByType(test, field);
196: }
197: }
198:
199: /**
200: * Injects all fields that are annotated with {@link InjectIntoStatic}.
201: *
202: * @param test The instance to inject into, not null
203: */
204: public void injectAllStatic(Object test) {
205: List<Field> fields = getFieldsAnnotatedWith(test.getClass(),
206: InjectIntoStatic.class);
207: for (Field field : fields) {
208: injectStatic(test, field);
209: }
210: }
211:
212: /**
213: * Auto-injects all fields that are annotated with {@link InjectIntoStaticByType}
214: *
215: * @param test The instance to inject into, not null
216: */
217: public void injectAllStaticByType(Object test) {
218: List<Field> fields = getFieldsAnnotatedWith(test.getClass(),
219: InjectIntoStaticByType.class);
220: for (Field field : fields) {
221: injectStaticByType(test, field);
222: }
223: }
224:
225: /**
226: * Restores the values that were stored using {@link #storeValueToRestoreAfterTest}.
227: */
228: public void restoreStaticInjectedObjects() {
229: for (ValueToRestore valueToRestore : valuesToRestoreAfterTest) {
230: restore(valueToRestore);
231: }
232: }
233:
234: /**
235: * Injects the fieldToInject. The target is either an explicitly specified target field of the test, or into the
236: * field(s) that is/are annotated with {@link TestedObject}
237: *
238: * @param test The instance to inject into, not null
239: * @param fieldToInject The field from which the value is injected into the target, not null
240: */
241: protected void inject(Object test, Field fieldToInject) {
242: InjectInto injectIntoAnnotation = fieldToInject
243: .getAnnotation(InjectInto.class);
244:
245: String ognlExpression = injectIntoAnnotation.property();
246: if (StringUtils.isEmpty(ognlExpression)) {
247: throw new UnitilsException(getSituatedErrorMessage(
248: InjectInto.class, fieldToInject,
249: "Property cannot be empty"));
250: }
251: Object objectToInject = getFieldValue(test, fieldToInject);
252:
253: List<Object> targets = getTargets(InjectInto.class,
254: fieldToInject, injectIntoAnnotation.target(), test);
255: if (targets.size() == 0) {
256: throw new UnitilsException(
257: getSituatedErrorMessage(
258: InjectInto.class,
259: fieldToInject,
260: "The target should either be "
261: + "specified explicitly using the target property, or by using the @"
262: + TestedObject.class
263: .getSimpleName()
264: + " annotation"));
265: }
266:
267: for (Object target : targets) {
268: try {
269: InjectionUtils.injectInto(objectToInject, target,
270: ognlExpression);
271:
272: } catch (UnitilsException e) {
273: throw new UnitilsException(
274: getSituatedErrorMessage(InjectInto.class,
275: fieldToInject, e.getMessage()), e);
276: }
277: }
278: }
279:
280: /**
281: * Injects the fieldToAutoInjectStatic into the specified target class.
282: *
283: * @param test Instance to inject into, not null
284: * @param fieldToInjectStatic The field from which the value is injected into the target, not null
285: */
286: protected void injectStatic(Object test, Field fieldToInjectStatic) {
287: InjectIntoStatic injectIntoStaticAnnotation = fieldToInjectStatic
288: .getAnnotation(InjectIntoStatic.class);
289:
290: Class<?> targetClass = injectIntoStaticAnnotation.target();
291: String property = injectIntoStaticAnnotation.property();
292: if (StringUtils.isEmpty(property)) {
293: throw new UnitilsException(getSituatedErrorMessage(
294: InjectIntoStatic.class, fieldToInjectStatic,
295: "Property cannot be empty"));
296: }
297: Object objectToInject = getFieldValue(test, fieldToInjectStatic);
298:
299: Restore restore = getEnumValueReplaceDefault(
300: InjectIntoStatic.class, "restore",
301: injectIntoStaticAnnotation.restore(),
302: defaultAnnotationPropertyValues);
303: try {
304: Object oldValue = InjectionUtils.injectIntoStatic(
305: objectToInject, targetClass, property);
306: storeValueToRestoreAfterTest(targetClass, property,
307: fieldToInjectStatic.getType(), null, oldValue,
308: restore);
309:
310: } catch (UnitilsException e) {
311: throw new UnitilsException(getSituatedErrorMessage(
312: InjectIntoStatic.class, fieldToInjectStatic, e
313: .getMessage()), e);
314: }
315: }
316:
317: /**
318: * Auto-injects the fieldToInject by trying to match the fields declared type with a property of the target.
319: * The target is either an explicitly specified target field of the test, or the field(s) that is/are annotated with
320: * {@link TestedObject}
321: *
322: * @param test The instance to inject into, not null
323: * @param fieldToInject The field from which the value is injected into the target, not null
324: */
325: protected void injectByType(Object test, Field fieldToInject) {
326: InjectIntoByType injectIntoByTypeAnnotation = fieldToInject
327: .getAnnotation(InjectIntoByType.class);
328:
329: Object objectToInject = getFieldValue(test, fieldToInject);
330: PropertyAccess propertyAccess = getEnumValueReplaceDefault(
331: InjectIntoByType.class, "propertyAccess",
332: injectIntoByTypeAnnotation.propertyAccess(),
333: defaultAnnotationPropertyValues);
334:
335: List<Object> targets = getTargets(InjectIntoByType.class,
336: fieldToInject, injectIntoByTypeAnnotation.target(),
337: test);
338: if (targets.size() == 0) {
339: throw new UnitilsException(
340: getSituatedErrorMessage(
341: InjectIntoByType.class,
342: fieldToInject,
343: "The target should either be "
344: + "specified explicitly using the target property, or by using the @"
345: + TestedObject.class
346: .getSimpleName()
347: + " annotation"));
348: }
349:
350: for (Object target : targets) {
351: try {
352: InjectionUtils
353: .injectIntoByType(objectToInject, fieldToInject
354: .getType(), target, propertyAccess);
355:
356: } catch (UnitilsException e) {
357: throw new UnitilsException(getSituatedErrorMessage(
358: InjectIntoByType.class, fieldToInject, e
359: .getMessage()), e);
360: }
361: }
362: }
363:
364: /**
365: * Auto-injects the fieldToInject by trying to match the fields declared type with a property of the target class.
366: * The target is either an explicitly specified target field of the test, or the field that is annotated with
367: * {@link TestedObject}
368: *
369: * @param test The instance to inject into, not null
370: * @param fieldToAutoInjectStatic The field from which the value is injected into the target, not null
371: */
372: protected void injectStaticByType(Object test,
373: Field fieldToAutoInjectStatic) {
374: InjectIntoStaticByType injectIntoStaticByTypeAnnotation = fieldToAutoInjectStatic
375: .getAnnotation(InjectIntoStaticByType.class);
376:
377: Class<?> targetClass = injectIntoStaticByTypeAnnotation
378: .target();
379: Object objectToInject = getFieldValue(test,
380: fieldToAutoInjectStatic);
381:
382: Restore restore = getEnumValueReplaceDefault(
383: InjectIntoStaticByType.class, "restore",
384: injectIntoStaticByTypeAnnotation.restore(),
385: defaultAnnotationPropertyValues);
386: PropertyAccess propertyAccess = getEnumValueReplaceDefault(
387: InjectIntoStaticByType.class, "propertyAccess",
388: injectIntoStaticByTypeAnnotation.propertyAccess(),
389: defaultAnnotationPropertyValues);
390: try {
391: Object oldValue = InjectionUtils.injectIntoStaticByType(
392: objectToInject, fieldToAutoInjectStatic.getType(),
393: targetClass, propertyAccess);
394: storeValueToRestoreAfterTest(targetClass, null,
395: fieldToAutoInjectStatic.getType(), propertyAccess,
396: oldValue, restore);
397:
398: } catch (UnitilsException e) {
399: throw new UnitilsException(getSituatedErrorMessage(
400: InjectIntoStaticByType.class,
401: fieldToAutoInjectStatic, e.getMessage()), e);
402: }
403: }
404:
405: /**
406: * Restores the given value.
407: *
408: * @param valueToRestore the value, not null
409: */
410: protected void restore(ValueToRestore valueToRestore) {
411:
412: Object value = valueToRestore.getValue();
413: Class<?> targetClass = valueToRestore.getTargetClass();
414:
415: String property = valueToRestore.getProperty();
416: if (property != null) {
417: // regular injection
418: InjectionUtils.injectIntoStatic(value, targetClass,
419: property);
420:
421: } else {
422: // auto injection
423: InjectionUtils.injectIntoStaticByType(value, valueToRestore
424: .getFieldType(), targetClass, valueToRestore
425: .getPropertyAccessType());
426: }
427: }
428:
429: /**
430: * Stores the old value that was replaced during the injection so that it can be restored after the test was
431: * performed. The value that is stored depends on the restore value: OLD_VALUE will store the value that was replaced,
432: * NULL_OR_0_VALUE will store 0 or null depeding whether it is a primitive or not, NO_RESTORE stores nothing.
433: *
434: * @param targetClass The target class, not null
435: * @param property The OGNL expression that defines where the object will be injected, null for auto inject
436: * @param fieldType The type, not null
437: * @param propertyAccess The access type in case auto injection is used
438: * @param oldValue The value that was replaced during the injection
439: * @param restore The type of reset, not DEFAULT
440: */
441: protected void storeValueToRestoreAfterTest(Class<?> targetClass,
442: String property, Class<?> fieldType,
443: PropertyAccess propertyAccess, Object oldValue,
444: Restore restore) {
445: if (Restore.NO_RESTORE == restore || Restore.DEFAULT == restore) {
446: return;
447: }
448:
449: ValueToRestore valueToRestore;
450: if (Restore.OLD_VALUE == restore) {
451: valueToRestore = new ValueToRestore(targetClass, property,
452: fieldType, propertyAccess, oldValue);
453:
454: } else if (Restore.NULL_OR_0_VALUE == restore) {
455: valueToRestore = new ValueToRestore(targetClass, property,
456: fieldType, propertyAccess,
457: fieldType.isPrimitive() ? 0 : null);
458:
459: } else {
460: throw new RuntimeException("Unkown value for "
461: + Restore.class.getSimpleName() + " " + restore);
462: }
463: valuesToRestoreAfterTest.add(valueToRestore);
464: }
465:
466: /**
467: * Returns the target(s) for the injection, given the specified name of the target and the test object. If
468: * targetName is not equal to an empty string, the targets are the testObject's fields that are annotated with
469: * {@link TestedObject}.
470: *
471: * @param annotationClass The class of the annotation, not null
472: * @param annotatedField The annotated field, not null
473: * @param targetName The explicit target name or empty string for TestedObject targets
474: * @param test The test instance
475: * @return The target(s) for the injection
476: */
477: protected List<Object> getTargets(
478: Class<? extends Annotation> annotationClass,
479: Field annotatedField, String targetName, Object test) {
480:
481: List<Object> targets;
482: if ("".equals(targetName)) {
483: // Default targetName, so it is probably not specfied. Return all objects that are annotated with the TestedObject annotation.
484: List<Field> testedObjectFields = getFieldsAnnotatedWith(
485: test.getClass(), TestedObject.class);
486: targets = new ArrayList<Object>(testedObjectFields.size());
487: for (Field testedObjectField : testedObjectFields) {
488: targets.add(getFieldValue(test, testedObjectField));
489: }
490: } else {
491: Field field = getFieldWithName(test.getClass(), targetName,
492: false);
493: if (field == null) {
494: throw new UnitilsException(getSituatedErrorMessage(
495: annotationClass, annotatedField,
496: "Target with name " + targetName
497: + " does not exist"));
498: }
499: Object target = getFieldValue(test, field);
500: targets = Collections.singletonList(target);
501: }
502: return targets;
503: }
504:
505: /**
506: * Given the errorDescription, returns a situated error message, i.e. specifying the annotated field and the
507: * annotation type that was used.
508: *
509: * @param annotationClass The injection annotation, not null
510: * @param annotatedField The annotated field, not null
511: * @param errorDescription A custom description, not null
512: * @return A situated error message
513: */
514: protected String getSituatedErrorMessage(
515: Class<? extends Annotation> annotationClass,
516: Field annotatedField, String errorDescription) {
517: return "Error while processing @"
518: + annotationClass.getSimpleName()
519: + " annotation on field " + annotatedField.getName()
520: + " of class "
521: + annotatedField.getDeclaringClass().getSimpleName()
522: + ": " + errorDescription;
523: }
524:
525: /**
526: * @return The {@link TestListener} for this module
527: */
528: public TestListener createTestListener() {
529: return new InjectTestListener();
530: }
531:
532: /**
533: * The {@link TestListener} for this module
534: */
535: protected class InjectTestListener extends TestListener {
536:
537: /**
538: * Before executing a test method (i.e. after the fixture methods), the injection is performed, since
539: * objects to inject or targets are possibly instantiated during the fixture.
540: *
541: * @param testObject The test object, not null
542: * @param testMethod The test method, not null
543: */
544: @Override
545: public void beforeTestMethod(Object testObject,
546: Method testMethod) {
547:
548: if (createTestedObjectsIfNullEnabled) {
549: createTestedObjectsIfNull(testObject);
550: }
551: injectObjects(testObject);
552: }
553:
554: /**
555: * After test execution, if requested restore all values that were replaced in the static injection.
556: *
557: * @param testObject The test object, not null
558: * @param testMethod The test method, not null
559: * @param throwable The throwable thrown during the test, null if none was thrown
560: */
561: @Override
562: public void afterTestMethod(Object testObject,
563: Method testMethod, Throwable throwable) {
564: restoreStaticInjectedObjects();
565: }
566: }
567:
568: }
|