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.core.util;
017:
018: import org.unitils.core.UnitilsException;
019: import static org.unitils.util.AnnotationUtils.getFieldsAnnotatedWith;
020: import static org.unitils.util.AnnotationUtils.getMethodsAnnotatedWith;
021: import static org.unitils.util.ReflectionUtils.invokeMethod;
022:
023: import java.lang.annotation.Annotation;
024: import java.lang.reflect.Field;
025: import java.lang.reflect.InvocationTargetException;
026: import java.lang.reflect.Method;
027: import java.util.ArrayList;
028: import java.util.HashMap;
029: import java.util.List;
030: import java.util.Map;
031:
032: /**
033: * Class for managing and creating instances of a given type. A given annotation controls how a new instance will be created.
034: * <p/>
035: * Instances will be created if an annotation instance is found that specifies values or if a custom create method
036: * is found. Custom create methods are methods that are marked with the annotation and have one of following signatures:
037: * <ul>
038: * <li>T createMethodName() or</li>
039: * <li>T createMethodName(List<String> values)</li>
040: * </ul>
041: * For the second version the found annotation values are passed to the creation method.
042: * <p/>
043: * Subclass overrides superclass configuration. That is, when a subclass and superclass contain an annotation with values,
044: * only the values of the sub-class will be used. Same is true for custom create methods. Methods in subclasses override
045: * methods in superclasses.
046: * <p/>
047: * Lets explain all this with an example:
048: * <pre><code>
049: * ' @MyAnnotation("supervalue")
050: * ' public class SuperClass {
051: * '
052: * ' @MyAnnotation
053: * ' protected MyType createMyType(List<String> values)
054: * ' }
055: * '
056: * ' @MyAnnotation({"value1", "value2"})
057: * ' public class MyClass extends SuperClass {
058: * '
059: * '}
060: * </code></pre>
061: * Following steps are performed: there is annotation with 2 values on the sub class. These values override the value of
062: * the annotation in the superclass and will be used for creating a new instance. The 2 values are then passed to the
063: * createMyType custom create method to create the actual instance.
064: * <p/>
065: * If no custom create method is found, the default {@link #createInstanceForValues} method is called for creating
066: * the instance.
067: * <p/>
068: * Created instances are cached on the level in the hierarchy that caused the creation of the instance. That is,
069: * if a subclass did not contain any annotations with values or any custom create methods, the super class is tried,
070: * if an instance for that super class was already created, that instance will be returned. This way, instances are
071: * reused as much as possible.
072: * <p/>
073: * If an instance needs to be recreated (for example because a test made modification to it), it can be removed from
074: * the cache by calling {@link #invalidateInstance}
075: *
076: * @author Tim Ducheyne
077: * @author Filip Neven
078: * @param <T> Type of the object that is configured by the annotations
079: * @param <A> Type of the annotation that is used for configuring the instance
080: */
081: public abstract class AnnotatedInstanceManager<T, A extends Annotation> {
082:
083: /**
084: * All created intances per class
085: */
086: protected Map<Class<?>, T> instances = new HashMap<Class<?>, T>();
087:
088: /**
089: * The type of the managed instances
090: */
091: protected Class<T> instanceClass;
092:
093: /**
094: * The annotation type
095: */
096: protected Class<A> annotationClass;
097:
098: /**
099: * Creates a manager
100: *
101: * @param instanceClass The type of the managed instances
102: * @param annotationClass The annotation type
103: */
104: protected AnnotatedInstanceManager(Class<T> instanceClass,
105: Class<A> annotationClass) {
106: this .instanceClass = instanceClass;
107: this .annotationClass = annotationClass;
108: }
109:
110: /**
111: * Gets an instance for the given test. This will first look for values of annotations on the test class and
112: * its super classes. If there is a custom create method, that method is then used to create the instance
113: * (passing the values). If no create was found, {@link #createInstanceForValues} is called to create the instance.
114: *
115: * @param testObject The test object, not null
116: * @return The instance, null if not found
117: */
118: protected T getInstance(Object testObject) {
119: return getInstanceImpl(testObject, testObject.getClass());
120: }
121:
122: /**
123: * Registers an instance for a given class. This will cause {@link #getInstance} to return the given instance
124: * if the testObject is of the given test type.
125: *
126: * @param testClass The test type, not null
127: * @param instance The instance, not null
128: */
129: protected void registerInstance(Class<?> testClass, T instance) {
130: instances.put(testClass, instance);
131: }
132:
133: /**
134: * Checks whether {@link #getInstance} will return an instance. If false is returned, {@link #getInstance} will
135: * return null.
136: *
137: * @param testObject The test object, not null
138: * @return True if an instance is linked to the given test object
139: */
140: protected boolean hasInstance(Object testObject) {
141: return hasInstanceImpl(testObject, testObject.getClass());
142: }
143:
144: /**
145: * Forces the recreation of the instance the next time that it is requested. If classes are given as argument
146: * only instances on those class levels will be reset. If no classes are given, all cached
147: * instances will be reset.
148: *
149: * @param classes The classes for which to reset the instances
150: */
151: protected void invalidateInstance(Class<?>... classes) {
152: if (classes == null || classes.length == 0) {
153: instances.clear();
154: return;
155: }
156: for (Class<?> clazz : classes) {
157: instances.remove(clazz);
158: }
159: }
160:
161: /**
162: * Recursive implementation of {@link #hasInstance(Object)}.
163: *
164: * @param testObject The test object, not null
165: * @param testClass The level in the hierarchy
166: * @return True if an instance is linked to the given test object
167: */
168: protected boolean hasInstanceImpl(Object testObject,
169: Class<?> testClass) {
170: // nothing to do (ends the recursion)
171: if (testClass == null || testClass == Object.class) {
172: return false;
173: }
174:
175: // check whether it already exists for the test class (eg registered instance)
176: if (instances.containsKey(testClass)) {
177: return true;
178: }
179:
180: // check annotation values
181: if (!getAnnotationValues(testClass).isEmpty()) {
182: return true;
183: }
184:
185: // check custom create method
186: if (getCustomCreateMethod(testClass, false) != null) {
187: return true;
188: }
189:
190: // nothing found on this level, check super-class
191: return hasInstanceImpl(testObject, testClass.getSuperclass());
192: }
193:
194: /**
195: * Recursive implementation of {@link #getInstance(Object)}.
196: *
197: * @param testObject The test object, not null
198: * @param testClass The level in the hierarchy
199: * @return The instance, null if not found
200: */
201: protected T getInstanceImpl(Object testObject, Class<?> testClass) {
202: // nothing to do (ends the recursion)
203: if (testClass == null || testClass == Object.class) {
204: return null;
205: }
206:
207: // check whether it already exists for the test class (eg registered instance)
208: T instance = instances.get(testClass);
209: if (instance != null) {
210: return instance;
211: }
212:
213: // get annotation values of this class
214: List<String> annotationValues = getAnnotationValues(testClass);
215:
216: // get custom create methods of this class
217: Method customCreateMethod = getCustomCreateMethod(testClass,
218: false);
219:
220: // invoke custom create method, if there is one
221: if (customCreateMethod != null) {
222: instance = invokeCustomCreateMethod(customCreateMethod,
223: testObject, annotationValues);
224: } else if (!annotationValues.isEmpty()) {
225: customCreateMethod = getCustomCreateMethod(testClass, true);
226: if (customCreateMethod != null) {
227: // if there are values but no custom create method, use default creation mechanism
228: instance = invokeCustomCreateMethod(customCreateMethod,
229: testObject, annotationValues);
230: } else {
231: instance = createInstanceForValues(annotationValues);
232: }
233: }
234:
235: // if nothing found on this level, try super-class
236: if (instance == null) {
237: return getInstanceImpl(testObject, testClass
238: .getSuperclass());
239: }
240:
241: // initialize instance if needed
242: afterInstanceCreate(instance, testObject, testClass);
243:
244: // store instance in cache
245: registerInstance(testClass, instance);
246: return instance;
247: }
248:
249: /**
250: * Hook method that can be overriden to perform extra initialization after the instance was created.
251: *
252: * @param instance The instance, not null
253: * @param testObject The test object, not null
254: * @param testClass The level in the hierarchy
255: */
256: protected void afterInstanceCreate(T instance, Object testObject,
257: Class<?> testClass) {
258: }
259:
260: /**
261: * Gets the values of the annotations on the given class.
262: * This will look for class-level, method-level and field-level annotations with values.
263: * If more than 1 such annotation is found, an exception is raised.
264: * If no annotation was found, an empty list is returned.
265: *
266: * @param testClass The test class, not null
267: * @return The values of the annotation, empty list if none found
268: */
269: protected List<String> getAnnotationValues(Class<?> testClass) {
270: // check class level annotation values
271: List<A> annotations = new ArrayList<A>();
272: A annotation = testClass.getAnnotation(annotationClass);
273: if (annotation != null
274: && getAnnotationValues(annotation) != null) {
275: annotations.add(annotation);
276: }
277:
278: // check field level annotation values
279: List<Field> fields = getFieldsAnnotatedWith(testClass,
280: annotationClass);
281: for (Field field : fields) {
282: annotation = field.getAnnotation(annotationClass);
283: if (annotation != null
284: && getAnnotationValues(annotation) != null) {
285: annotations.add(annotation);
286: }
287: }
288:
289: // check custom create methods and method level annotation values
290: List<Method> methods = getMethodsAnnotatedWith(testClass,
291: annotationClass, false);
292: for (Method method : methods) {
293: annotation = method.getAnnotation(annotationClass);
294: if (annotation != null
295: && getAnnotationValues(annotation) != null) {
296: annotations.add(annotation);
297: }
298: }
299:
300: // check whether there is more than 1 annotation with values
301: if (annotations.size() > 1) {
302: throw new UnitilsException("There can only be 1 @"
303: + annotationClass.getSimpleName()
304: + " annotation with values per class.");
305: }
306:
307: // if nothing found, return empty list
308: if (annotations.isEmpty()) {
309: return new ArrayList<String>();
310: }
311:
312: // found exactly 1 annotation ==> get values
313: annotation = annotations.get(0);
314: return getAnnotationValues(annotation);
315: }
316:
317: /**
318: * Gets the custom create methods on the given class.
319: * If there is more than 1 create method found, an exception is raised.
320: * If no create method was found, null is returned.
321: * If searchSuperClasses is true, it will also look in super classes for create methods.
322: *
323: * @param testClass The test class, not null
324: * @param searchSuperClasses True to look recursively in superclasses
325: * @return The instance, null if no create method was found
326: */
327: protected Method getCustomCreateMethod(Class<?> testClass,
328: boolean searchSuperClasses) {
329: // nothing to do (ends the recursion)
330: if (testClass == null || testClass == Object.class) {
331: return null;
332: }
333:
334: // get all annotated methods from the given test class, no superclasses
335: List<Method> methods = getMethodsAnnotatedWith(testClass,
336: annotationClass, false);
337:
338: // look for correct signature (no return value)
339: List<Method> customCreateMethods = new ArrayList<Method>();
340: for (Method method : methods) {
341: // do not invoke setter methods
342: if (method.getReturnType() != Void.TYPE) {
343: customCreateMethods.add(method);
344: }
345: }
346:
347: // check whether there is more than 1 custom create method
348: if (customCreateMethods.size() > 1) {
349: throw new UnitilsException(
350: "There can only be 1 method per class annotated with @"
351: + annotationClass.getSimpleName()
352: + " for creating a session factory.");
353: }
354:
355: // if nothing found, look in superclass or return null
356: if (customCreateMethods.isEmpty()) {
357: if (searchSuperClasses) {
358: return getCustomCreateMethod(testClass.getSuperclass(),
359: searchSuperClasses);
360: }
361: return null;
362: }
363:
364: // found exactly 1 custom create method ==> check correct signature
365: Method customCreateMethod = customCreateMethods.get(0);
366: if (!isCustomCreateMethod(customCreateMethod)) {
367: throw new UnitilsException(
368: "Custom create method annotated with @"
369: + annotationClass.getSimpleName()
370: + " should have following signature: "
371: + instanceClass.getName()
372: + " myMethod( List<String> locations ) or "
373: + instanceClass.getName() + " myMethod()");
374: }
375: return customCreateMethod;
376: }
377:
378: /**
379: * Checks whether the given method is a custom create method. A custom create method must have following signature:
380: * <ul>
381: * <li>T createMethodName() or</li>
382: * <li>T createMethodName(List<String> values)</li>
383: * </ul>
384: *
385: * @param method The method, not null
386: * @return True if it has the correct signature
387: */
388: protected boolean isCustomCreateMethod(Method method) {
389: Class<?>[] argumentTypes = method.getParameterTypes();
390: if (argumentTypes.length > 1) {
391: return false;
392: }
393: if (argumentTypes.length == 1 && argumentTypes[0] != List.class) {
394: return false;
395: }
396: return instanceClass.isAssignableFrom(method.getReturnType());
397: }
398:
399: /**
400: * Creates an instance by calling a custom create method (if there is one). Such a create method should have one of
401: * following exact signatures:
402: * <ul>
403: * <li>Configuration createMethodName() or</li>
404: * <li>Configuration createMethodName(List<String> locations)</li>
405: * </ul>
406: * The second version receives the given locations. They both should return an instance (not null)
407: *
408: * @param customCreateMethod The create method, not null
409: * @param testObject The test object, not null
410: * @param annotationValues The specified locations if there are any, not null
411: * @return The instance, null if no create method was found
412: */
413: @SuppressWarnings({"unchecked"})
414: protected T invokeCustomCreateMethod(Method customCreateMethod,
415: Object testObject, List<String> annotationValues) {
416: T result;
417: try {
418: // call method
419: if (customCreateMethod.getParameterTypes().length == 0) {
420: result = (T) invokeMethod(testObject,
421: customCreateMethod);
422: } else {
423: result = (T) invokeMethod(testObject,
424: customCreateMethod, annotationValues);
425: }
426: } catch (InvocationTargetException e) {
427: throw new UnitilsException("Method "
428: + testObject.getClass().getSimpleName() + "."
429: + customCreateMethod + " (annotated with "
430: + annotationClass.getSimpleName()
431: + ") has thrown an exception", e.getCause());
432: }
433: // check whether create returned a value
434: if (result == null) {
435: throw new UnitilsException("Method "
436: + testObject.getClass().getSimpleName() + "."
437: + customCreateMethod + " (annotated with "
438: + annotationClass.getSimpleName()
439: + ") has returned null.");
440: }
441: return result;
442: }
443:
444: /**
445: * Gets the values that are specified for the given annotation. An array with 1 empty string should
446: * be considered to be empty and null should be returned.
447: *
448: * @param annotation The annotation, not null
449: * @return The values, null if no values were specified
450: */
451: abstract protected List<String> getAnnotationValues(A annotation);
452:
453: /**
454: * Creates an instance for the given values.
455: *
456: * @param values The values, not null
457: * @return The instance, not null
458: */
459: abstract protected T createInstanceForValues(List<String> values);
460: }
|