package org.needle4j;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import org.needle4j.annotation.InjectInto;
import org.needle4j.annotation.InjectIntoMany;
import org.needle4j.annotation.ObjectUnderTest;
import org.needle4j.common.MapEntry;
import org.needle4j.injection.InjectionConfiguration;
import org.needle4j.injection.InjectionProvider;
import org.needle4j.injection.InjectionTargetInformation;
import org.needle4j.mock.MockProvider;
import org.needle4j.mock.SpyProvider;
import org.needle4j.predicate.IsSupportedAnnotationPredicate;
import org.needle4j.reflection.ReflectionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract test case to process and initialize all fields annotated with
* {@link ObjectUnderTest}. After initialization, {@link InjectIntoMany} and
* {@link InjectInto} annotations are processed for optional additional
* injections.
* <p>
* Supported injections are:
* </p>
* <ol>
* <li>Constructor injection</li>
* <li>Field injection</li>
* <li>Method injection</li>
* </ol>
*
* @see ObjectUnderTest
* @see InjectInto
* @see InjectIntoMany
* @see InjectionProvider
*/
public abstract class NeedleTestcase {
private static final Logger LOG = LoggerFactory.getLogger(NeedleTestcase.class);
private NeedleContext context;
private final InjectionConfiguration configuration;
private final IsSupportedAnnotationPredicate isSupportedAnnotationPredicate;
/**
* Create an instance of {@link NeedleTestcase} with optional additional
* injection provider.
*
* @param injectionProviders
* optional additional injection provider
* @see InjectionProvider
*/
protected NeedleTestcase(final InjectionProvider<?>... injectionProviders) {
this(new InjectionConfiguration(), injectionProviders);
}
protected NeedleTestcase(final InjectionConfiguration configuration,
final InjectionProvider<?>... injectionProviders) {
this.configuration = configuration;
this.isSupportedAnnotationPredicate = new IsSupportedAnnotationPredicate(configuration);
addInjectionProvider(injectionProviders);
}
protected final void addInjectionProvider(final InjectionProvider<?>... injectionProvider) {
configuration.addInjectionProvider(injectionProvider);
}
/**
* Initialize all fields annotated with {@link ObjectUnderTest}. Is an
* object under test annotated field already initialized, only the injection
* of dependencies will be executed. After initialization,
* {@link InjectIntoMany} and {@link InjectInto} annotations are processed
* for optional additional injections.
*
* @param test
* an instance of the test
* @throws Exception
* thrown if an initialization error occurs.
*/
protected final void initTestcase(final Object test) throws Exception {
LOG.info("init testcase {}", test);
context = new NeedleContext(test);
final List<Field> fields = context.getAnnotatedTestcaseFields(ObjectUnderTest.class);
LOG.debug("found fields {}", fields);
for (final Field field : fields) {
LOG.debug("found field {}", field.getName());
final ObjectUnderTest objectUnderTestAnnotation = field.getAnnotation(ObjectUnderTest.class);
try {
final Object instance = setInstanceIfNotNull(field, objectUnderTestAnnotation, test);
initInstance(instance);
} catch (final InstantiationException e) {
LOG.error(e.getMessage(), e);
} catch (final IllegalAccessException e) {
LOG.error(e.getMessage(), e);
}
}
configuration.getChainedNeedleProcessor().process(context);
// TODO find way to include postconstruct processor in chain
beforePostConstruct();
configuration.getPostConstructProcessor().process(context);
}
/**
* init mocks
*/
protected void beforePostConstruct() {
}
/**
* Inject dependencies into the given instance. First, all field injections
* are executed, if there exists an {@link InjectionProvider} for the
* target. Then the method injection is executed, if the injection
* annotations are supported.
*
* @param instance
* the instance to initialize.
* @throws ObjectUnderTestInstantiationException
*/
protected final void initInstance(final Object instance) {
initFields(instance);
initMethodInjection(instance);
}
private void initMethodInjection(final Object instance) {
final List<Method> methods = ReflectionUtil.getMethods(instance.getClass());
for (final Method method : methods) {
// if the method is not annotated with at least one supported
// annotation, skip it!
if (!isSupportedAnnotationPredicate.applyAny(method.getDeclaredAnnotations())) {
continue;
}
final Class<?>[] parameterTypes = method.getParameterTypes();
final InjectionTargetInformationFactory injectionTargetInformationFactory = new InjectionTargetInformationFactory() {
@Override
public InjectionTargetInformation create(final Class<?> parameterType, final int parameterIndex) {
return new InjectionTargetInformation(parameterType, method,
method.getGenericParameterTypes()[parameterIndex],
method.getParameterAnnotations()[parameterIndex]);
}
};
final Object[] arguments = createArguments(parameterTypes, injectionTargetInformationFactory);
try {
ReflectionUtil.invokeMethod(method, instance, arguments);
} catch (final Exception e) {
LOG.warn("could not invoke method", e);
}
}
}
private Object[] createArguments(final Class<?>[] parameterTypes,
final InjectionTargetInformationFactory injectionTargetInformationFactory) {
final Object[] arguments = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
final InjectionTargetInformation injectionTargetInformation = injectionTargetInformationFactory.create(
parameterTypes[i], i);
final Entry<Object, Object> injection = inject(injectionTargetInformation);
if (injection != null) {
arguments[i] = injection.getValue();
}
}
return arguments;
}
private void initFields(final Object instance) {
final List<Field> fields = ReflectionUtil.getAllFieldsWithSupportedAnnotation(instance.getClass(),
isSupportedAnnotationPredicate);
for (final Field field : fields) {
final InjectionTargetInformation injectionTargetInformation = new InjectionTargetInformation(
field.getType(), field);
final Entry<Object, Object> injection = inject(injectionTargetInformation);
if (injection != null) {
try {
ReflectionUtil.setField(field, instance, injection.getValue());
} catch (final Exception e) {
LOG.error(e.getMessage(), e);
}
}
}
}
private Object setInstanceIfNotNull(final Field field, final ObjectUnderTest objectUnderTestAnnotation,
final Object test) throws Exception {
final SpyProvider spyProvider = configuration.getSpyProvider();
final String id = objectUnderTestAnnotation.id().equals("") ? field.getName() : objectUnderTestAnnotation.id();
Object instance = ReflectionUtil.getFieldValue(test, field);
if (instance == null) {
final Class<?> implementation = objectUnderTestAnnotation.implementation() != Void.class ? objectUnderTestAnnotation
.implementation() : field.getType();
if (implementation.isInterface()) {
throw new ObjectUnderTestInstantiationException("could not create an instance of object under test "
+ implementation + ", no implementation class configured");
}
instance = getInstanceByConstructorInjection(implementation);
if (instance == null) {
instance = createInstanceByNoArgConstructor(implementation);
}
// create spy if required, else just return unmodified instance
if (spyProvider.isSpyRequested(field)) {
instance = spyProvider.createSpyComponent(instance);
}
setField(field, test, instance);
}
// field value was already set in test
else {
if (spyProvider.isSpyRequested(field)) {
setField(field, test, spyProvider.createSpyComponent(instance));
}
}
context.addObjectUnderTest(id, instance, objectUnderTestAnnotation);
return instance;
}
private void setField(final Field field, final Object test, final Object instance)
throws ObjectUnderTestInstantiationException {
try {
ReflectionUtil.setField(field, test, instance);
} catch (final Exception e) {
throw new ObjectUnderTestInstantiationException(e);
}
}
protected Object createInstanceByNoArgConstructor(final Class<?> implementation)
throws ObjectUnderTestInstantiationException {
try {
implementation.getConstructor();
return implementation.newInstance();
} catch (final NoSuchMethodException e) {
throw new ObjectUnderTestInstantiationException("could not create an instance of object under test "
+ implementation + ",implementation has no public no-arguments constructor", e);
} catch (final InstantiationException e) {
throw new ObjectUnderTestInstantiationException(e);
} catch (final IllegalAccessException e) {
throw new ObjectUnderTestInstantiationException(e);
}
}
protected Object getInstanceByConstructorInjection(final Class<?> implementation)
throws ObjectUnderTestInstantiationException {
final Constructor<?>[] constructors = implementation.getConstructors();
for (final Constructor<?> constructor : constructors) {
// has the constructor at least one supported injection annotation?
if (!isSupportedAnnotationPredicate.applyAny(constructor.getAnnotations())) {
continue;
}
final Class<?>[] parameterTypes = constructor.getParameterTypes();
final InjectionTargetInformationFactory injectionTargetInformationFactory = new InjectionTargetInformationFactory() {
@Override
public InjectionTargetInformation create(final Class<?> parameterType, final int parameterIndex) {
return new InjectionTargetInformation(parameterType, constructor,
constructor.getGenericParameterTypes()[parameterIndex],
constructor.getParameterAnnotations()[parameterIndex]);
}
};
final Object[] arguments = createArguments(parameterTypes, injectionTargetInformationFactory);
try {
return constructor.newInstance(arguments);
} catch (final Exception e) {
throw new ObjectUnderTestInstantiationException(e);
}
}
return null;
}
/**
* Returns the injected object for the given key, or null if no object was
* injected with the given key.
*
* @param key
* the key of the injected object, see
* {@link InjectionProvider#getKey(InjectionTargetInformation)}
* @return the injected object or null
*/
@SuppressWarnings("unchecked")
public <X> X getInjectedObject(final Object key) {
return (X) context.getInjectedObject(key);
}
/**
* Returns an instance of the configured {@link MockProvider}
*
* @return the configured {@link MockProvider}
*/
@SuppressWarnings("unchecked")
public <X extends MockProvider> X getMockProvider() {
return (X) configuration.getMockProvider();
}
private interface InjectionTargetInformationFactory {
InjectionTargetInformation create(Class<?> parameterType, int parameterIndex);
}
private Entry<Object, Object> inject(final InjectionTargetInformation injectionTargetInformation) {
for (final Collection<InjectionProvider<?>> collection : configuration.getInjectionProvider()) {
final Entry<Object, Object> injection = configuration.handleInjectionProvider(collection,
injectionTargetInformation);
if (injection != null) {
final Object injectionKey = injection.getKey();
// check if mock object already created
final Object injectionValue = context.getInjectedObject(injectionKey) == null ? injection.getValue()
: context.getInjectedObject(injectionKey);
context.addInjectedObject(injectionKey, injectionValue);
return new MapEntry<Object, Object>(injectionKey, injectionValue);
}
}
return null;
}
}