/*
* 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.core.util;
import org.unitils.core.UnitilsException;
import static org.unitils.util.AnnotationUtils.getFieldsAnnotatedWith;
import static org.unitils.util.AnnotationUtils.getMethodsAnnotatedWith;
import static org.unitils.util.ReflectionUtils.invokeMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Class for managing and creating instances of a given type. A given annotation controls how a new instance will be created.
* <p/>
* Instances will be created if an annotation instance is found that specifies values or if a custom create method
* is found. Custom create methods are methods that are marked with the annotation and have one of following signatures:
* <ul>
* <li>T createMethodName() or</li>
* <li>T createMethodName(List<String> values)</li>
* </ul>
* For the second version the found annotation values are passed to the creation method.
* <p/>
* Subclass overrides superclass configuration. That is, when a subclass and superclass contain an annotation with values,
* only the values of the sub-class will be used. Same is true for custom create methods. Methods in subclasses override
* methods in superclasses.
* <p/>
* Lets explain all this with an example:
* <pre><code>
* ' @MyAnnotation("supervalue")
* ' public class SuperClass {
* '
* ' @MyAnnotation
* ' protected MyType createMyType(List<String> values)
* ' }
* '
* ' @MyAnnotation({"value1", "value2"})
* ' public class MyClass extends SuperClass {
* '
* '}
* </code></pre>
* Following steps are performed: there is annotation with 2 values on the sub class. These values override the value of
* the annotation in the superclass and will be used for creating a new instance. The 2 values are then passed to the
* createMyType custom create method to create the actual instance.
* <p/>
* If no custom create method is found, the default {@link #createInstanceForValues} method is called for creating
* the instance.
* <p/>
* Created instances are cached on the level in the hierarchy that caused the creation of the instance. That is,
* if a subclass did not contain any annotations with values or any custom create methods, the super class is tried,
* if an instance for that super class was already created, that instance will be returned. This way, instances are
* reused as much as possible.
* <p/>
* If an instance needs to be recreated (for example because a test made modification to it), it can be removed from
* the cache by calling {@link #invalidateInstance}
*
* @author Tim Ducheyne
* @author Filip Neven
* @param <T> Type of the object that is configured by the annotations
* @param <A> Type of the annotation that is used for configuring the instance
*/
public abstract class AnnotatedInstanceManager<T, A extends Annotation> {
/**
* All created intances per class
*/
protected Map<Class<?>, T> instances = new HashMap<Class<?>, T>();
/**
* The type of the managed instances
*/
protected Class<T> instanceClass;
/**
* The annotation type
*/
protected Class<A> annotationClass;
/**
* Creates a manager
*
* @param instanceClass The type of the managed instances
* @param annotationClass The annotation type
*/
protected AnnotatedInstanceManager(Class<T> instanceClass, Class<A> annotationClass) {
this.instanceClass = instanceClass;
this.annotationClass = annotationClass;
}
/**
* Gets an instance for the given test. This will first look for values of annotations on the test class and
* its super classes. If there is a custom create method, that method is then used to create the instance
* (passing the values). If no create was found, {@link #createInstanceForValues} is called to create the instance.
*
* @param testObject The test object, not null
* @return The instance, null if not found
*/
protected T getInstance(Object testObject) {
return getInstanceImpl(testObject, testObject.getClass());
}
/**
* Registers an instance for a given class. This will cause {@link #getInstance} to return the given instance
* if the testObject is of the given test type.
*
* @param testClass The test type, not null
* @param instance The instance, not null
*/
protected void registerInstance(Class<?> testClass, T instance) {
instances.put(testClass, instance);
}
/**
* Checks whether {@link #getInstance} will return an instance. If false is returned, {@link #getInstance} will
* return null.
*
* @param testObject The test object, not null
* @return True if an instance is linked to the given test object
*/
protected boolean hasInstance(Object testObject) {
return hasInstanceImpl(testObject, testObject.getClass());
}
/**
* Forces the recreation of the instance the next time that it is requested. If classes are given as argument
* only instances on those class levels will be reset. If no classes are given, all cached
* instances will be reset.
*
* @param classes The classes for which to reset the instances
*/
protected void invalidateInstance(Class<?>... classes) {
if (classes == null || classes.length == 0) {
instances.clear();
return;
}
for (Class<?> clazz : classes) {
instances.remove(clazz);
}
}
/**
* Recursive implementation of {@link #hasInstance(Object)}.
*
* @param testObject The test object, not null
* @param testClass The level in the hierarchy
* @return True if an instance is linked to the given test object
*/
protected boolean hasInstanceImpl(Object testObject, Class<?> testClass) {
// nothing to do (ends the recursion)
if (testClass == null || testClass == Object.class) {
return false;
}
// check whether it already exists for the test class (eg registered instance)
if (instances.containsKey(testClass)) {
return true;
}
// check annotation values
if (!getAnnotationValues(testClass).isEmpty()) {
return true;
}
// check custom create method
if (getCustomCreateMethod(testClass, false) != null) {
return true;
}
// nothing found on this level, check super-class
return hasInstanceImpl(testObject, testClass.getSuperclass());
}
/**
* Recursive implementation of {@link #getInstance(Object)}.
*
* @param testObject The test object, not null
* @param testClass The level in the hierarchy
* @return The instance, null if not found
*/
protected T getInstanceImpl(Object testObject, Class<?> testClass) {
// nothing to do (ends the recursion)
if (testClass == null || testClass == Object.class) {
return null;
}
// check whether it already exists for the test class (eg registered instance)
T instance = instances.get(testClass);
if (instance != null) {
return instance;
}
// get annotation values of this class
List<String> annotationValues = getAnnotationValues(testClass);
// get custom create methods of this class
Method customCreateMethod = getCustomCreateMethod(testClass, false);
// invoke custom create method, if there is one
if (customCreateMethod != null) {
instance = createCustomCreatedInstance(customCreateMethod, testObject, testClass, annotationValues);
} else if (!annotationValues.isEmpty()) {
customCreateMethod = getCustomCreateMethod(testClass, true);
if (customCreateMethod != null) {
// if there are values but no custom create method, use default creation mechanism
instance = createCustomCreatedInstance(customCreateMethod, testObject, testClass, annotationValues);
} else {
instance = createInstanceForValues(testObject, testClass, annotationValues);
}
}
// if nothing found on this level, try super-class
if (instance == null) {
return getInstanceImpl(testObject, testClass.getSuperclass());
}
// initialize instance if needed
afterInstanceCreate(instance, testObject, testClass);
// store instance in cache
registerInstance(testClass, instance);
return instance;
}
/**
* Hook method that can be overriden to perform extra initialization after the instance was created.
*
* @param instance The instance, not null
* @param testObject The test object, not null
* @param testClass The level in the hierarchy
*/
protected void afterInstanceCreate(T instance, Object testObject, Class<?> testClass) {
}
/**
* Gets the values of the annotations on the given class.
* This will look for class-level, method-level and field-level annotations with values.
* If more than 1 such annotation is found, an exception is raised.
* If no annotation was found, an empty list is returned.
*
* @param testClass The test class, not null
* @return The values of the annotation, empty list if none found
*/
protected List<String> getAnnotationValues(Class<?> testClass) {
// check class level annotation values
List<A> annotations = new ArrayList<A>();
A annotation = testClass.getAnnotation(annotationClass);
if (annotation != null && !getAnnotationValues(annotation).isEmpty()) {
annotations.add(annotation);
}
// check field level annotation values
Set<Field> fields = getFieldsAnnotatedWith(testClass, annotationClass);
for (Field field : fields) {
annotation = field.getAnnotation(annotationClass);
if (annotation != null && !getAnnotationValues(annotation).isEmpty()) {
annotations.add(annotation);
}
}
// check custom create methods and method level annotation values
Set<Method> methods = getMethodsAnnotatedWith(testClass, annotationClass, false);
for (Method method : methods) {
annotation = method.getAnnotation(annotationClass);
if (annotation != null && !getAnnotationValues(annotation).isEmpty()) {
annotations.add(annotation);
}
}
// check whether there is more than 1 annotation with values
if (annotations.size() > 1) {
throw new UnitilsException("There can only be 1 @" + annotationClass.getSimpleName() + " annotation with values per class.");
}
// if nothing found, return empty list
if (annotations.isEmpty()) {
return new ArrayList<String>();
}
// found exactly 1 annotation ==> get values
annotation = annotations.get(0);
return getAnnotationValues(annotation);
}
/**
* Gets the custom create methods on the given class.
* If there is more than 1 create method found, an exception is raised.
* If no create method was found, null is returned.
* If searchSuperClasses is true, it will also look in super classes for create methods.
*
* @param testClass The test class, not null
* @param searchSuperClasses True to look recursively in superclasses
* @return The instance, null if no create method was found
*/
protected Method getCustomCreateMethod(Class<?> testClass, boolean searchSuperClasses) {
// nothing to do (ends the recursion)
if (testClass == null || testClass == Object.class) {
return null;
}
// get all annotated methods from the given test class, no superclasses
Set<Method> methods = getMethodsAnnotatedWith(testClass, annotationClass, false);
// look for correct signature (no return value)
List<Method> customCreateMethods = new ArrayList<Method>();
for (Method method : methods) {
// do not invoke setter methods
if (method.getReturnType() != Void.TYPE) {
customCreateMethods.add(method);
}
}
// check whether there is more than 1 custom create method
if (customCreateMethods.size() > 1) {
throw new UnitilsException("There can only be 1 method per class annotated with @" + annotationClass.getSimpleName() + " for creating a session factory.");
}
// if nothing found, look in superclass or return null
if (customCreateMethods.isEmpty()) {
if (searchSuperClasses) {
return getCustomCreateMethod(testClass.getSuperclass(), searchSuperClasses);
}
return null;
}
// found exactly 1 custom create method ==> check correct signature
Method customCreateMethod = customCreateMethods.get(0);
if (!isCustomCreateMethod(customCreateMethod)) {
throw new UnitilsException("Custom create method annotated with @" + annotationClass.getSimpleName() + " should have following signature: " + getCustomCreateMethodReturnType().getName() + " myMethod( List<String> locations ) or " + getCustomCreateMethodReturnType().getName() + " myMethod()");
}
return customCreateMethod;
}
/**
* Checks whether the given method is a custom create method. A custom create method must have following signature:
* <ul>
* <li>T createMethodName() or</li>
* <li>T createMethodName(List<String> values)</li>
* </ul>
*
* @param method The method, not null
* @return True if it has the correct signature
*/
protected boolean isCustomCreateMethod(Method method) {
Class<?>[] argumentTypes = method.getParameterTypes();
if (argumentTypes.length > 1) {
return false;
}
if (argumentTypes.length == 1 && argumentTypes[0] != List.class) {
return false;
}
return getCustomCreateMethodReturnType().isAssignableFrom(method.getReturnType());
}
protected T createCustomCreatedInstance(Method customCreateMethod, Object testObject, Class<?> testClass, List<String> annotationValues) {
Object customCreateMethodResult = invokeCustomCreateMethod(customCreateMethod, testObject, annotationValues);
return createCustomCreatedInstanceFromCustomCreateMethodResult(testObject, testClass, customCreateMethodResult);
}
/**
* Creates an instance by calling a custom create method (if there is one). Such a create method should have one of
* following exact signatures:
* <ul>
* <li>Configuration createMethodName() or</li>
* <li>Configuration createMethodName(List<String> locations)</li>
* </ul>
* The second version receives the given locations. They both should return an instance (not null)
*
* @param customCreateMethod The create method, not null
* @param testObject The test object, not null
* @param annotationValues The specified locations if there are any, not null
* @return The instance, null if no create method was found
*/
@SuppressWarnings({"unchecked"})
protected Object invokeCustomCreateMethod(Method customCreateMethod, Object testObject, List<String> annotationValues) {
Object result;
try {
// call method
if (customCreateMethod.getParameterTypes().length == 0) {
result = invokeMethod(testObject, customCreateMethod);
} else {
result = invokeMethod(testObject, customCreateMethod, annotationValues);
}
} catch (InvocationTargetException e) {
throw new UnitilsException("Method " + testObject.getClass().getSimpleName() + "." + customCreateMethod + " (annotated with " + annotationClass.getSimpleName() + ") has thrown an exception", e.getCause());
}
// check whether create returned a value
if (result == null) {
throw new UnitilsException("Method " + testObject.getClass().getSimpleName() + "." + customCreateMethod + " (annotated with " + annotationClass.getSimpleName() + ") has returned null.");
}
return result;
}
@SuppressWarnings("unchecked")
protected T createCustomCreatedInstanceFromCustomCreateMethodResult(
Object testObject, Class<?> testClass, Object customCreateMethodResult) {
return (T) customCreateMethodResult;
}
/**
* @return The return type of a custom create method. By default, this is the type of the managed instance.
* Subclasses that override this method are themselves responsible for making sure that the returned object
* can be used for creating an instance of the managed class.
*/
protected Class<?> getCustomCreateMethodReturnType() {
return instanceClass;
}
/**
* Gets the values that are specified for the given annotation. An array with 1 empty string should
* be considered to be empty and null should be returned.
*
* @param annotation The annotation, not null
* @return The values, null if no values were specified
*/
abstract protected List<String> getAnnotationValues(A annotation);
/**
* Creates an instance for the given values.
* @param testObject TODO
* @param testClass TODO
* @param values The values, not null
*
* @return The instance, not null
*/
abstract protected T createInstanceForValues(Object testObject, Class<?> testClass, List<String> values);
}