NeedleStepsFactory.java

package org.jbehave.core.steps.needle;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jbehave.core.annotations.AsParameterConverter;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.steps.AbstractStepsFactory;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.ParameterConverters.MethodReturningConverter;
import org.jbehave.core.steps.ParameterConverters.ParameterConverter;
import org.jbehave.core.steps.Steps;
import org.jbehave.core.steps.needle.configuration.CollectInjectionProvidersFromStepsInstance;
import org.jbehave.core.steps.needle.configuration.CreateInstanceByDefaultConstructor;
import org.jbehave.core.steps.needle.configuration.JBehaveNeedleConfiguration;
import org.needle4j.NeedleTestcase;
import org.needle4j.injection.InjectionProvider;
import org.needle4j.reflection.ReflectionUtil;

/**
 * An {@link InjectableStepsFactory} that uses a Needle {@link InjectionProvider} for the composition and instantiation
 * of all components that contain JBehave annotated methods.
 * @author Simon Zambrovski (simon.zambrovski@holisticon.de)
 * @author Jan Galinski (jan.galinski@holisticon.de)
 */
public class NeedleStepsFactory extends NeedleTestcase implements InjectableStepsFactory {

    private final Map<Class<?>, Object> cachedTypeInstances = new LinkedHashMap<>();

    private Configuration configuration;
    private Class<?>[] steps;

    /**
     * Creates factory with given configuration and step instances.
     * @param configuration
     *        JBehave configuration
     * @param steps
     *        step classes
     */
    public NeedleStepsFactory(final Configuration configuration, final Class<?>... steps) {
        this(configuration, null, steps);
    }

    /**
     * Creates factory with given configuration, injection providers and step instances.
     * @param configuration
     *        JBehave configuration
     * @param injectionProviders
     *        injection providers.
     * @param steps
     *        step classes
     */
    public NeedleStepsFactory(final Configuration configuration, final Set<InjectionProvider<?>> injectionProviders,
            final Class<?>... steps) {
        super(setUpInjectionProviders(JBehaveNeedleConfiguration.RESOURCE_JBEHAVE_NEEDLE));
        if (injectionProviders != null) {
            addInjectionProvider(toArray(injectionProviders));
        }
        if (this.configuration == null) {
            this.configuration = new MostUsefulConfiguration();
        } else {
            this.configuration = configuration;
        }
        this.steps = steps;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<CandidateSteps> createCandidateSteps() {
        final List<CandidateSteps> result = new ArrayList<>();
        for (final Class<?> type : steps) {
            if (hasAnnotatedMethods(type)) {
                configuration.parameterConverters().addConverters(methodReturningConverters(type));
                result.add(new Steps(configuration, type, this));
            }
        }
        return result;
    }

    @Override
    public Object createInstanceOfType(final Class<?> type) {
        final Object instance = cachedTypeInstances.get(type);
        if (instance == null) {
            try {
                final Object stepsInstance = createInstanceUsingNeedleTestCase(type);
                final InjectionProvider<?>[] foundProviders = CollectInjectionProvidersFromStepsInstance.INSTANCE.apply(
                        stepsInstance);

                addInjectionProvider(foundProviders);

                initTestcase(stepsInstance);

                cachedTypeInstances.put(type, stepsInstance);
                return stepsInstance;
            } catch (final Exception e) {
                throw new IllegalStateException(e);
            }
        }
        return instance;
    }

    /**
     * Uses private instantiation methods of NeedleTestCase via
     * {@link ReflectionUtil#invokeMethod(Object, String, Object...)}. First tries to create new instance with
     * constructor injection, then falls back to default constructor. If creation fails, an IllegalStateException is
     * thrown.
     *
     * @param type type of instance to create
     * @return new instance of type. Never <code>null</code>
     * @throws IllegalStateException when creation fails.
     */
    private Object createInstanceUsingNeedleTestCase(final Class<?> type) throws IllegalStateException {
        try {
            Object instance = ReflectionUtil.invokeMethod(this, "getInstanceByConstructorInjection", type);
            if (instance == null) {
                instance = CreateInstanceByDefaultConstructor.INSTANCE.apply(type);
            }
            if (instance == null) {
                throw new IllegalStateException("failed to create instance of type " + type.getCanonicalName());
            }
            return instance;
        } catch (final Exception e) {
            throw new IllegalStateException(e);
        }

    }

    /**
     * Create parameter converters from methods annotated with @AsParameterConverter
     * @see AbstractStepsFactory
     */
    private List<ParameterConverter> methodReturningConverters(final Class<?> type) {
        final List<ParameterConverter> converters = new ArrayList<>();
        for (final Method method : type.getMethods()) {
            if (method.isAnnotationPresent(AsParameterConverter.class)) {
                converters.add(new MethodReturningConverter(method, type, this));
            }
        }
        return converters;
    }

    /**
     * Add injection providers.
     * @param providers
     *        add injection providers after factory construction.
     */
    public void addInjectionProviders(final Set<InjectionProvider<?>> providers) {
        if (providers != null) {
            addInjectionProvider(toArray(providers));
        }
    }

    /**
     * Determines if the given type is a {@link Class} containing at least one method annotated with annotations from
     * package "org.jbehave.core.annotations".
     * @param type
     *        the Type of the steps instance
     * @return A boolean, <code>true</code> if at least one annotated method is found.
     * @see AbstractStepsFactory
     */
    static boolean hasAnnotatedMethods(final Type type) {
        if (type instanceof Class<?>) {
            for (final Method method : ((Class<?>)type).getMethods()) {
                for (final Annotation annotation : method.getAnnotations()) {
                    if (annotation.annotationType().getName().startsWith("org.jbehave.core.annotations")) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Read injection providers configuration from a resource.
     * @param resourceName
     *        resource name
     * @return injection providers.
     */
    static InjectionProvider<?>[] setUpInjectionProviders(final String resourceName) {
        return new JBehaveNeedleConfiguration(resourceName).getInjectionProviders();
    }

    /**
     * Set to array.
     * @param injectionProviders
     *        set of providers
     * @return array of providers
     */
    static InjectionProvider<?>[] toArray(final Set<InjectionProvider<?>> injectionProviders) {
        return injectionProviders.toArray(new InjectionProvider<?>[injectionProviders.size()]);
    }

}