StepCreator.java

package org.jbehave.core.steps;

import static java.util.Arrays.asList;
import static org.jbehave.core.steps.AbstractStepResult.comment;
import static org.jbehave.core.steps.AbstractStepResult.failed;
import static org.jbehave.core.steps.AbstractStepResult.ignorable;
import static org.jbehave.core.steps.AbstractStepResult.notPerformed;
import static org.jbehave.core.steps.AbstractStepResult.pending;
import static org.jbehave.core.steps.AbstractStepResult.skipped;
import static org.jbehave.core.steps.AbstractStepResult.successful;
import static org.jbehave.core.steps.ParameterConverters.ExamplesTableParametersConverter.isExamplesTableParameters;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.thoughtworks.paranamer.NullParanamer;
import com.thoughtworks.paranamer.Paranamer;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.jbehave.core.annotations.AfterScenario.Outcome;
import org.jbehave.core.annotations.Conditional;
import org.jbehave.core.annotations.FromContext;
import org.jbehave.core.annotations.Named;
import org.jbehave.core.annotations.ToContext;
import org.jbehave.core.condition.StepConditionMatchException;
import org.jbehave.core.condition.StepConditionMatcher;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.expressions.ExpressionResolver;
import org.jbehave.core.failures.BeforeOrAfterFailed;
import org.jbehave.core.failures.IgnoringStepsFailure;
import org.jbehave.core.failures.RestartingScenarioFailure;
import org.jbehave.core.failures.UUIDExceptionWrapper;
import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.Meta;
import org.jbehave.core.model.Verbatim;
import org.jbehave.core.parsers.StepMatcher;
import org.jbehave.core.reporters.StoryReporter;
import org.jbehave.core.steps.ParameterConverters.FromStringParameterConverter;
import org.jbehave.core.steps.context.StepsContext;

@SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters")
public class StepCreator {

    public static final String PARAMETER_TABLE_START = "\uff3b";
    public static final String PARAMETER_TABLE_END = "\uff3d";
    public static final String PARAMETER_VERBATIM_START = "\u301a";
    public static final String PARAMETER_VERBATIM_END = "\u301b";
    public static final String PARAMETER_VALUE_START = "\uFF5F";
    public static final String PARAMETER_VALUE_END = "\uFF60";
    public static final String PARAMETER_VALUE_NEWLINE = "\u2424";
    public static final UUIDExceptionWrapper NO_FAILURE = new UUIDExceptionWrapper("no failure");
    private static final String NEWLINE = "\n";
    private static final String SPACE = " ";
    private static final String NONE = "";
    private final Class<?> stepsType;
    private final InjectableStepsFactory stepsFactory;
    private final ParameterConverters parameterConverters;
    private final ExpressionResolver expressionResolver;
    private final ParameterControls parameterControls;
    private final Pattern delimitedNamePattern;
    private final StepMatcher stepMatcher;
    private final StepsContext stepsContext;
    private final boolean dryRun;
    private StepMonitor stepMonitor;
    private Paranamer paranamer = new NullParanamer();

    public StepCreator(Class<?> stepsType, InjectableStepsFactory stepsFactory, StepsContext stepsContext,
            ParameterConverters parameterConverters, ExpressionResolver expressionResolver,
            ParameterControls parameterControls, StepMatcher stepMatcher, StepMonitor stepMonitor, boolean dryRun) {
        this.stepsType = stepsType;
        this.stepsFactory = stepsFactory;
        this.stepsContext = stepsContext;
        this.parameterConverters = parameterConverters;
        this.expressionResolver = expressionResolver;
        this.parameterControls = parameterControls;
        this.stepMatcher = stepMatcher;
        this.stepMonitor = stepMonitor;
        this.delimitedNamePattern = Pattern.compile(parameterControls.nameDelimiterLeft() + "([\\w\\-\\h.]+?)"
                + parameterControls.nameDelimiterRight(), Pattern.DOTALL);
        this.dryRun = dryRun;
    }

    public void useStepMonitor(StepMonitor stepMonitor) {
        this.stepMonitor = stepMonitor;
    }

    public void useParanamer(Paranamer paranamer) {
        this.paranamer = paranamer;
    }

    public Object stepsInstance() {
        return stepsFactory.createInstanceOfType(stepsType);
    }

    public Step createBeforeOrAfterStep(Method method, Meta meta) {
        return new BeforeOrAfterStep(method, meta);
    }

    public Step createAfterStepUponOutcome(final Method method, final Outcome outcome, Meta storyAndScenarioMeta) {
        Step beforeOrAfterStep = createBeforeOrAfterStep(method, storyAndScenarioMeta);
        return wrapStepUponOutcome(outcome, beforeOrAfterStep);
    }

    public Map<String, String> matchedParameters(final Method method, final String stepWithoutStartingWord,
            final Map<String, String> namedParameters) {
        Map<String, String> matchedParameters = new HashMap<>();
        Matcher matcher = stepMatcher.matcher(stepWithoutStartingWord);
        if (matcher.find()) {
            // we've found a match, populate map
            ParameterName[] parameterNames = parameterNames(method);
            Type[] types = parameterTypes(method, parameterNames);

            String[] values = parameterValuesForStep(matcher, namedParameters, types, parameterNames, false);
            for (int i = 0; i < parameterNames.length; i++) {
                String name = parameterNames[i].name;
                if (name == null) {
                    name = stepMatcher.parameterNames()[i];
                }
                matchedParameters.put(name, values[i]);
            }
        }
        // else return empty map
        return matchedParameters; 
    }

    /**
     * Returns the {@link ParameterName} representations for the method,
     * providing an abstraction that supports both annotated and non-annotated
     * parameters.
     * 
     * @param method the Method
     * @return The array of {@link ParameterName}s
     */
    private ParameterName[] parameterNames(Method method) {
        ParameterName[] parameterNames;
        if (method != null) {
            String[] annotatedNames = annotatedParameterNames(method);
            String[] paranamerNames = paranamerParameterNames(method);
            String[] contextNames = contextParameterNames(method);

            parameterNames = new ParameterName[annotatedNames.length];
            for (int i = 0; i < annotatedNames.length; i++) {
                parameterNames[i] = parameterName(annotatedNames, paranamerNames, contextNames, i);
            }
        } else {
            String[] stepMatcherParameterNames = stepMatcher.parameterNames();
            parameterNames = new ParameterName[stepMatcherParameterNames.length];
            for (int i = 0; i < stepMatcherParameterNames.length; i++) {
                parameterNames[i] = new ParameterName(stepMatcherParameterNames[i], false, false);
            }
        }
        return parameterNames;
    }

    private ParameterName parameterName(String[] annotatedNames, String[] paranamerNames, String[] contextNames,
            int i) {
        boolean annotated = true;
        boolean fromContext = false;

        String name = contextNames[i];
        if (name != null) {
            fromContext = true;
        } else {
            name = annotatedNames[i];
            if (name == null) {
                name = (paranamerNames.length > i ? paranamerNames[i] : null);
                annotated = false;
            }
        }
        return new ParameterName(name, annotated, fromContext);
    }

    /**
     * Extract parameter names using {@link Named}-annotated parameters
     * 
     * @param method the Method with {@link Named}-annotated parameters
     * @return An array of annotated parameter names, which <b>may</b> include
     *         <code>null</code> values for parameters that are not annotated
     */
    private String[] annotatedParameterNames(Method method) {
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        String[] names = new String[parameterAnnotations.length];
        for (int i = 0; i < parameterAnnotations.length; i++) {
            for (Annotation annotation : parameterAnnotations[i]) {
                names[i] = annotationName(annotation);
            }
        }
        return names;
    }
    
    /**
     * Extract parameter names using {@link FromContext}-annotated parameters
     * 
     * @param method the Method with {@link FromContext}-annotated parameters
     * @return An array of annotated parameter names, which <b>may</b> include
     *         <code>null</code> values for parameters that are not annotated
     */
    private String[] contextParameterNames(Method method) {
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        String[] names = new String[parameterAnnotations.length];
        for (int i = 0; i < parameterAnnotations.length; i++) {
            for (Annotation annotation : parameterAnnotations[i]) {
                names[i] = contextName(annotation);
            }
        }
        return names;
    }

    /**
     * Returns either the value of the annotation, either {@link Named} or
     * "javax.inject.Named".
     * 
     * @param annotation the Annotation
     * @return The annotated value or <code>null</code> if no annotation is
     *         found
     */
    private String annotationName(Annotation annotation) {
        if (annotation.annotationType().isAssignableFrom(Named.class)) {
            return ((Named) annotation).value();
        } else if ("javax.inject.Named".equals(annotation.annotationType().getName())) {
            return Jsr330Helper.getNamedValue(annotation);
        } else {
            return null;
        }
    }

    /**
     * Returns the value of the annotation {@link FromContext}.
     * 
     * @param annotation the Annotation
     * @return The annotated value or <code>null</code> if no annotation is
     *         found
     */
    private String contextName(Annotation annotation) {
        if (annotation.annotationType().isAssignableFrom(FromContext.class)) {
            return ((FromContext) annotation).value();
        } else {
            return null;
        }
    }

    /**
     * Extract parameter names using
     * {@link Paranamer#lookupParameterNames(AccessibleObject, boolean)}
     * 
     * @param method the Method inspected by Paranamer
     * @return An array of parameter names looked up by Paranamer
     */
    private String[] paranamerParameterNames(Method method) {
        return paranamer.lookupParameterNames(method, false);
    }

    private Type[] parameterTypes(Method method, ParameterName[] parameterNames) {
        if (method != null) {
            return method.getGenericParameterTypes();
        }
        Type[] types = new Type[parameterNames.length];
        for (int i = 0; i < types.length; i++) {
            types[i] = String.class;
        }
        return types;
    }

    public Step createConditionalStep(final StepConditionMatcher stepConditionMatcher,
            final Map<Method, ParametrisedStep> parametrisedSteps) {
        return new ConditionalStep(stepConditionMatcher, parametrisedSteps);
    }

    public Step createParametrisedStep(final Method method, final String stepAsString,
            final String stepWithoutStartingWord, final Map<String, String> namedParameters,
            final List<Step> composedSteps) {
        return new ParametrisedStep(stepAsString, method, stepWithoutStartingWord, namedParameters, composedSteps);
    }

    public Step createParametrisedStepUponOutcome(final Method method, final String stepAsString,
            final String stepWithoutStartingWord, final Map<String, String> namedParameters,
            final List<Step> composedSteps, Outcome outcome) {
        Step parametrisedStep = createParametrisedStep(method, stepAsString, stepWithoutStartingWord, namedParameters,
                composedSteps);
        return wrapStepUponOutcome(outcome, parametrisedStep);
    }

    private Step wrapStepUponOutcome(Outcome outcome, Step step) {
        switch (outcome) {
            case ANY:
                return new UponAnyStep(step);
            case SUCCESS:
                return new UponSuccessStep(step);
            case FAILURE:
                return new UponFailureStep(step);
            default:
                return step;
        }
    }

    private String parametrisedStep(String stepAsString, Map<String, String> namedParameters, Type[] types,
            String[] parameterValues) {
        String parametrisedStep = stepAsString;
        // mark parameter values that are parsed
        boolean hasTable = hasTable(types);
        for (int position = 0; position < types.length; position++) {
            parametrisedStep = markParsedParameterValue(parametrisedStep, types[position], parameterValues[position],
                    hasTable);
        }
        // mark parameter values that are named
        for (Entry<String, String> namedParameter : namedParameters.entrySet()) {
            parametrisedStep = parameterControls.replaceAllDelimitedNames(parametrisedStep, namedParameter.getKey(),
                    markedValue(namedParameter.getValue()));
        }

        return parametrisedStep;
    }

    private boolean hasTable(Type[] types) {
        for (Type type : types) {
            if (isTable(type)) {
                return true;
            }
        }
        return false;
    }

    private String markParsedParameterValue(String stepText, Type type, String value, boolean hasTable) {
        if (value != null) {
            // only mark non-empty string as parameter (JBEHAVE-656)
            if (value.trim().length() != 0) {
                if (isTable(type)) {
                    return stepText.replace(value, markedTable(value));
                }
                if (isVerbatim(type)) {
                    return stepText.replace(value, markedVerbatim(value));
                }
                String markedValue = markedValue(value);
                // identify parameter values to mark as padded by spaces to avoid duplicated replacements of overlapping
                // values (JBEHAVE-837)
                String leftPad = SPACE;
                String rightPad = stepText.endsWith(value) ? NONE : SPACE;
                return stepText.replace(pad(value, leftPad, rightPad), pad(markedValue, leftPad, rightPad));
            }
            if (!hasTable) {
                return stepText.replace(NEWLINE, PARAMETER_VALUE_NEWLINE);
            }
        }
        return stepText;
    }

    private String markedTable(String value) {
        return pad(value, PARAMETER_TABLE_START, PARAMETER_TABLE_END);
    }

    private String markedVerbatim(String value) {
        return pad(value, PARAMETER_VERBATIM_START, PARAMETER_VERBATIM_END);
    }

    private String markedValue(String value) {
        return pad(value, PARAMETER_VALUE_START, PARAMETER_VALUE_END);
    }

    private String pad(String value, String left, String right) {
        return new StringBuilder().append(left).append(value).append(right).toString();
    }

    private boolean isTable(Type type) {
        return isExamplesTable(type) || isExamplesTableParameters(type);
    }

    private boolean isVerbatim(Type type) {
        return type instanceof Class && Verbatim.class.isAssignableFrom((Class<?>) type);
    }

    private boolean isExamplesTable(Type type) {
        return type instanceof Class && ExamplesTable.class.isAssignableFrom((Class<?>) type);
    }

    private String[] parameterValuesForStep(Matcher matcher, Map<String, String> namedParameters, Type[] types,
            ParameterName[] names, boolean overrideWithTableParameters) {
        final String[] parameters = new String[types.length];
        for (int position = 0; position < types.length; position++) {
            parameters[position] = parameterForPosition(matcher, position, names, namedParameters,
                    overrideWithTableParameters);
        }
        return parameters;
    }

    private String parameterForPosition(Matcher matcher, int position, ParameterName[] names,
            Map<String, String> namedParameters, boolean overrideWithTableParameters) {
        int namePosition = parameterPosition(names, position);
        String parameter = null;

        if (namePosition != -1) {
            String name = names[position].name;
            boolean annotated = names[position].annotated;
            boolean fromContext = names[position].fromContext;

            List<String> delimitedNames = Collections.emptyList();

            if (isGroupName(name)) {
                parameter = matchedParameter(matcher, name);
                delimitedNames = delimitedNameFor(parameter);

                if (delimitedNames.isEmpty()) {
                    monitorUsingNameForParameter(name, position, annotated);
                }
            }

            if (!delimitedNames.isEmpty()) {
                parameter = replaceAllDelimitedNames(delimitedNames, position, annotated, parameter, namedParameters);
                delimitedNames = delimitedNameFor(parameter);
                parameter = replaceAllDelimitedNames(delimitedNames, position, annotated, parameter, namedParameters);
            } else if (overrideWithTableParameters && namedParameters.containsKey(name)) {
                parameter = namedParameters.get(name);
                if (parameter != null) {
                    monitorUsingTableNameForParameter(name, position, annotated); 
                }
            }
            
            if (fromContext && parameter == null) {
                parameter = name;
                stepMonitor.usingStepsContextParameter(parameter);
            }

        }

        if (parameter == null) {
            // This allows parameters to be in a different order.
            position = position - numberOfPreviousFromContext(names, position);
            stepMonitor.usingNaturalOrderForParameter(position);
            parameter = matchedParameter(matcher, position);

            String previousParameterValue;
            do {
                previousParameterValue = parameter;

                for (String delimitedName : delimitedNameFor(parameter)) {
                    parameter = replaceAllDelimitedNames(parameter, delimitedName, namedParameters);
                }
            } while (parameter != null && !previousParameterValue.equals(parameter));
        }

        stepMonitor.foundParameter(parameter, position);

        return parameter;
    }

    private String replaceAllDelimitedNames(List<String> delimitedNames, int position, boolean annotated,
                                            String parameter, Map<String, String> namedParameters) {
        String parameterWithDelimitedNames = parameter;
        for (String delimitedName : delimitedNames) {
            monitorUsingTableNameForParameter(delimitedName, position, annotated);
            parameterWithDelimitedNames = replaceAllDelimitedNames(parameterWithDelimitedNames, delimitedName,
                    namedParameters);
        }
        return parameterWithDelimitedNames;
    }

    private String replaceAllDelimitedNames(String parameterWithDelimitedNames, String delimitedName,
            Map<String, String> namedParameters) {
        if (namedParameters.containsKey(delimitedName)) {
            return parameterControls.replaceAllDelimitedNames(parameterWithDelimitedNames, delimitedName,
                    namedParameters.get(delimitedName));
        }
        return parameterWithDelimitedNames;
    }

    private int numberOfPreviousFromContext(ParameterName[] names, int currentPosition) {
        int number = 0;

        for (int i = currentPosition - 1; i >= 0; i--) {
            if (names[i].fromContext) {
                number++;
            }
        }
        
        return number;
    }

    private void monitorUsingTableNameForParameter(String name, int position, boolean usingAnnotationNames) {
        if (usingAnnotationNames) {
            stepMonitor.usingTableAnnotatedNameForParameter(name, position);
        } else {
            stepMonitor.usingTableParameterNameForParameter(name, position);
        }
    }

    private void monitorUsingNameForParameter(String name, int position, boolean usingAnnotationNames) {
        if (usingAnnotationNames) {
            stepMonitor.usingAnnotatedNameForParameter(name, position);
        } else {
            stepMonitor.usingParameterNameForParameter(name, position);
        }
    }

    private List<String> delimitedNameFor(String parameter) {
        List<String> delimitedNames = new ArrayList<>();
        if (parameterControls.delimiterNamedParameters()) {
            Matcher matcher = delimitedNamePattern.matcher(parameter);
            while (matcher.find()) {
                delimitedNames.add(matcher.group(1));
            }
        }
        return delimitedNames;
    }

    String matchedParameter(Matcher matcher, String name) {
        String[] parameterNames = stepMatcher.parameterNames();
        for (int i = 0; i < parameterNames.length; i++) {
            String parameterName = parameterNames[i];
            if (name.equals(parameterName)) {
                return matchedParameter(matcher, i);
            }
        }
        throw new ParameterNotFound(name, parameterNames);
    }

    private String matchedParameter(Matcher matcher, int position) {
        String[] parameterNames = stepMatcher.parameterNames();
        int matchedPosition = position + 1;
        if (matchedPosition <= parameterNames.length) {
            return matcher.group(matchedPosition);
        }
        throw new ParameterNotFound(position, parameterNames);
    }

    private int parameterPosition(ParameterName[] names, int position) {
        if (names.length == 0) {
            return -1;
        }
        String positionName = names[position].name;
        for (int i = 0; i < names.length; i++) {
            String name = names[i].name;
            if (name != null && name.equals(positionName)) {
                return i;
            }
        }
        return -1;
    }

    private boolean isGroupName(String name) {
        String[] groupNames = stepMatcher.parameterNames();
        for (String groupName : groupNames) {
            if (name.equals(groupName)) {
                return true;
            }
        }
        return false;
    }

    public static Step createPendingStep(final String stepAsString, String previousNonAndStep) {
        return new PendingStep(stepAsString, previousNonAndStep);
    }

    public static Step createIgnorableStep(final String stepAsString) {
        return new IgnorableStep(stepAsString);
    }

    public static Step createComment(final String stepAsString) {
        return new Comment(stepAsString);
    }

    private void storeOutput(Object object, Method method) {
        ToContext annotation = method.getAnnotation(ToContext.class);
        if (annotation != null) {
            stepsContext.put(annotation.value(), object, annotation.retentionLevel());
        }
    }

    /**
     * This is a different class, because the @Inject jar may not be in the
     * classpath.
     */
    public static class Jsr330Helper {

        private static String getNamedValue(Annotation annotation) {
            return ((javax.inject.Named) annotation).value();
        }

    }

    @SuppressWarnings("serial")
    public static class ParameterNotFound extends RuntimeException {

        public ParameterNotFound(String name, String[] parameters) {
            super("Parameter not found for name '" + name + "' amongst '" + asList(parameters) + "'");
        }

        public ParameterNotFound(int position, String[] parameters) {
            super("Parameter not found for position '" + position + "' amongst '" + asList(parameters) + "'");
        }
    }

    public abstract static class AbstractStep implements Step {

        @Override
        public String asString(Keywords keywords) {
            return toString();
        }

        @Override
        public String toString() {
            return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE);
        }

        @Override
        public List<Step> getComposedSteps() {
            return Collections.emptyList();
        }
    }

    public abstract static class ReportingAbstractStep extends AbstractStep {

        private final StepExecutionType stepExecutionType;
        private final String stepAsString;

        public ReportingAbstractStep(StepExecutionType stepExecutionType, String stepAsString) {
            this.stepExecutionType = stepExecutionType;
            this.stepAsString = stepAsString;
        }

        @Override
        public final StepResult perform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailure) {
            storyReporter.beforeStep(new org.jbehave.core.model.Step(stepExecutionType, getStepAsString()));
            return perform(storyFailure);
        }

        protected abstract StepResult perform(UUIDExceptionWrapper storyFailure);

        @Override
        public String asString(Keywords keywords) {
            return stepAsString;
        }

        protected String getStepAsString() {
            return stepAsString;
        }
    }

    static class DelegatingStep extends AbstractStep {
        private final Step step;

        DelegatingStep(Step step) {
            this.step = step;
        }

        @Override
        public StepResult perform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return step.perform(storyReporter, storyFailureIfItHappened);
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return step.doNotPerform(storyReporter, storyFailureIfItHappened);
        }

        @Override
        public String asString(Keywords keywords) {
            return step.asString(keywords);
        }

        @Override
        public List<Step> getComposedSteps() {
            return step.getComposedSteps();
        }
    }

    private class BeforeOrAfterStep extends ReportingAbstractStep {
        private final Method method;
        private final Meta meta;

        public BeforeOrAfterStep(Method method, Meta meta) {
            super(StepExecutionType.EXECUTABLE, method.getName());
            this.method = method;
            this.meta = meta;
        }

        @Override
        public StepResult perform(UUIDExceptionWrapper storyFailureIfItHappened) {
            ParameterConverters paramConvertersWithExceptionInjector = paramConvertersWithExceptionInjector(
                    storyFailureIfItHappened);
            MethodInvoker methodInvoker = new MethodInvoker(method, paramConvertersWithExceptionInjector, paranamer,
                    meta);
            Timer timer = new Timer().start();
            try {
                Object outputObject = methodInvoker.invoke();
                storeOutput(outputObject, method);
                return successful(method).setTimings(timer.stop());
            } catch (InvocationTargetException e) {
                return failed(method, new UUIDExceptionWrapper(new BeforeOrAfterFailed(method, e.getCause())))
                        .setTimings(timer.stop());
            } catch (Throwable t) {
                return failed(method, new UUIDExceptionWrapper(new BeforeOrAfterFailed(method, t)))
                        .setTimings(timer.stop());
            }
        }

        private ParameterConverters paramConvertersWithExceptionInjector(
                UUIDExceptionWrapper storyFailureIfItHappened) {
            return parameterConverters.newInstanceAdding(new UUIDExceptionWrapperInjector(storyFailureIfItHappened));
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return perform(storyReporter, storyFailureIfItHappened);
        }

        @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
        private class UUIDExceptionWrapperInjector extends FromStringParameterConverter<UUIDExceptionWrapper> {
            private final UUIDExceptionWrapper storyFailureIfItHappened;

            public UUIDExceptionWrapperInjector(UUIDExceptionWrapper storyFailureIfItHappened) {
                this.storyFailureIfItHappened = storyFailureIfItHappened;
            }

            @Override
            public boolean canConvertTo(Type type) {
                return UUIDExceptionWrapper.class == type;
            }

            @Override
            public UUIDExceptionWrapper convertValue(String value, Type type) {
                return storyFailureIfItHappened;
            }
        }

        @Override
        public String asString(Keywords keywords) {
            return method.getName() + ";" + meta.asString(keywords);
        }
    }

    class UponAnyStep extends DelegatingStep {

        UponAnyStep(Step step) {
            super(step);
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return perform(storyReporter, storyFailureIfItHappened);
        }
    }

    class UponSuccessStep extends DelegatingStep {

        UponSuccessStep(Step step) {
            super(step);
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return skipped();
        }
    }

    class UponFailureStep extends DelegatingStep {

        UponFailureStep(Step step) {
            super(step);
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return super.perform(storyReporter, storyFailureIfItHappened);
        }

        @Override
        public StepResult perform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return skipped();
        }
    }

    public class ConditionalStep extends AbstractStep {

        private final StepConditionMatcher stepConditionMatcher;
        private final Map<Method, ParametrisedStep> parametrisedSteps;

        public ConditionalStep(StepConditionMatcher stepConditionMatcher,
                Map<Method, ParametrisedStep> parametrisedSteps) {
            this.stepConditionMatcher = stepConditionMatcher;
            this.parametrisedSteps = parametrisedSteps;
        }

        @Override
        public StepResult perform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return performConditionalStep(storyReporter, step -> step.perform(storyReporter, storyFailureIfItHappened));
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return performConditionalStep(storyReporter,
                    step -> step.doNotPerform(storyReporter, storyFailureIfItHappened));
        }

        private StepResult performConditionalStep(StoryReporter storyReporter,
                Function<ParametrisedStep, StepResult> invoker) {

            try {
                Map<Method, Conditional> unmatchedSteps = new HashMap<>();
                Map<Method, Conditional> matchedSteps = new HashMap<>();

                for (Method method : parametrisedSteps.keySet()) {
                    Conditional conditional = Optional.ofNullable(method.getAnnotation(Conditional.class))
                            .orElseGet(() -> method.getDeclaringClass().getAnnotation(Conditional.class));

                    Map<Method, Conditional> target = stepConditionMatcher.matches(conditional.condition(),
                            conditional.value()) ? matchedSteps : unmatchedSteps;
                    target.put(method, conditional);

                }

                if (matchedSteps.isEmpty()) {
                    String message = getStepName() + System.lineSeparator()
                            + "None of the following steps were matched any condition:" + System.lineSeparator()
                            + formatSteps(unmatchedSteps);
                    return reportPending(storyReporter, message);
                } else if (matchedSteps.size() > 1) {
                    String message = getStepName() + System.lineSeparator()
                            + "More than one conditional step matched the condition:" + System.lineSeparator()
                            + formatSteps(matchedSteps);
                    return reportPending(storyReporter, message);
                }

                return invoker.apply(parametrisedSteps.get(matchedSteps.keySet().iterator().next()));
            } catch (StepConditionMatchException e) {
                return reportPending(storyReporter, getStepName() + System.lineSeparator() + e.getMessage());
            }
        }

        private StepResult reportPending(StoryReporter storyReporter, String message) {
            Step pendingStep = createPendingStep(message, null);
            return pendingStep.perform(storyReporter, null);
        }

        private String formatSteps(Map<Method, Conditional> matchedSteps) {
            return matchedSteps.entrySet().stream()
                    .map(e -> formatStep(e.getKey(), e.getValue()))
                    .collect(Collectors.joining(System.lineSeparator()));
        }

        private String formatStep(Method method, Conditional condition) {
            return method.getDeclaringClass() + "." + method.getName() + " (condition: " + condition.condition() + " "
                    + condition.value() + ")";
        }

        private String getStepName() {
            return parametrisedSteps.values().iterator().next().asString(null);
        }
    }

    public class ParametrisedStep extends ReportingAbstractStep {
        private String parametrisedStep;
        private final Method method;
        private final String stepWithoutStartingWord;
        private final Map<String, String> namedParameters;
        private final List<Step> composedSteps;

        public ParametrisedStep(String stepAsString, Method method, String stepWithoutStartingWord,
                Map<String, String> namedParameters, List<Step> composedSteps) {
            super(StepExecutionType.EXECUTABLE, stepAsString);
            this.method = method;
            this.stepWithoutStartingWord = stepWithoutStartingWord;
            this.namedParameters = namedParameters;
            this.composedSteps = composedSteps;
        }

        @Override
        public List<Step> getComposedSteps() {
            return composedSteps;
        }

        @Override
        public StepResult perform(UUIDExceptionWrapper storyFailure) {
            String stepAsString = getStepAsString();
            Timer timer = new Timer().start();
            try {
                Object[] convertedParameters = parametriseStep();
                stepMonitor.beforePerforming(parametrisedStep, dryRun, method);
                if (!dryRun && method != null) {
                    Object outputObject = method.invoke(stepsInstance(), convertedParameters);
                    storeOutput(outputObject, method);
                }
                return successful(stepAsString).withParameterValues(parametrisedStep)
                        .setTimings(timer.stop());
            } catch (ParameterNotFound e) {
                // step parametrisation failed, return pending StepResult
                return pending(new PendingStep(stepAsString, null)).withParameterValues(parametrisedStep);
            } catch (InvocationTargetException e) {
                if (e.getCause() instanceof RestartingScenarioFailure) {
                    throw (RestartingScenarioFailure) e.getCause();
                }
                if (e.getCause() instanceof IgnoringStepsFailure) {
                    throw (IgnoringStepsFailure) e.getCause();
                }
                Throwable failureCause = e.getCause();
                if (failureCause instanceof UUIDExceptionWrapper) {
                    failureCause = failureCause.getCause();
                }
                return failed(stepAsString, new UUIDExceptionWrapper(stepAsString, failureCause)).withParameterValues(
                        parametrisedStep).setTimings(timer.stop());
            } catch (Throwable t) {
                return failed(stepAsString, new UUIDExceptionWrapper(stepAsString, t)).withParameterValues(
                        parametrisedStep).setTimings(timer.stop());
            } finally {
                stepMonitor.afterPerforming(parametrisedStep, dryRun, method);
            }
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            storyReporter.beforeStep(
                    new org.jbehave.core.model.Step(StepExecutionType.NOT_PERFORMED, getStepAsString()));
            try {
                parametriseStep();
            } catch (Throwable t) {
                // step parametrisation failed, but still return
                // notPerformed StepResult
            }
            return notPerformed(getStepAsString()).withParameterValues(parametrisedStep);
        }

        @Override
        public String asString(Keywords keywords) {
            if (parametrisedStep == null) {
                parametriseStep();
            }
            return parametrisedStep;
        }

        private Object[] parametriseStep() {
            Matcher matcher = stepMatcher.matcher(stepWithoutStartingWord);
            matcher.find();
            ParameterName[] names = parameterNames(method);
            Type[] types = parameterTypes(method, names);
            String[] parameterValues = parameterValuesForStep(matcher, namedParameters, types, names, true);
            Object[] convertedParameters;
            if (method == null) {
                convertedParameters = parameterValues;
            } else {
                convertedParameters = convertParameterValues(parameterValues, types, names);
            }
            addNamedParametersToExamplesTables(convertedParameters);
            parametrisedStep = parametrisedStep(getStepAsString(), namedParameters, types, parameterValues);
            return convertedParameters;
        }

        private Object[] convertParameterValues(String[] parameterValues, Type[] types, ParameterName[] names) {
            final Object[] parameters = new Object[parameterValues.length];
            for (int i = 0; i < parameterValues.length; i++) {
                String parameterValue = parameterValues[i];
                if (names[i].fromContext) {
                    parameters[i] = stepsContext.get(parameterValue);
                } else {
                    String expressionEvaluationResult = parameterValue != null
                            ? String.valueOf(expressionResolver.resolveExpressions(dryRun, parameterValue)) : null;
                    parameters[i] = parameterConverters.convert(expressionEvaluationResult, types[i]);
                }
            }
            return parameters;
        }

        private void addNamedParametersToExamplesTables(Object[] convertedParameters) {
            Stream.of(convertedParameters)
                    .filter(ExamplesTable.class::isInstance)
                    .map(ExamplesTable.class::cast)
                    .forEach(examplesTable -> examplesTable.withNamedParameters(namedParameters));
        }
    }

    public static class PendingStep extends ReportingAbstractStep {
        private final String previousNonAndStep;
        private Method method;
        private String pendingMethod;

        public PendingStep(String stepAsString, String previousNonAndStep) {
            super(StepExecutionType.PENDING, stepAsString);
            this.previousNonAndStep = previousNonAndStep;
        }

        @Override
        protected StepResult perform(UUIDExceptionWrapper storyFailure) {
            return pending(this);
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return perform(storyReporter, storyFailureIfItHappened);
        }

        public String stepAsString() {
            return getStepAsString();
        }

        public String previousNonAndStepAsString() {
            return previousNonAndStep;
        }

        public void annotatedOn(Method method) {
            this.method = method;
        }

        public boolean annotated() {
            return method != null;
        }

        public String getPendingMethod() {
            return pendingMethod;
        }

        public void setPendingMethod(String pendingMethod) {
            this.pendingMethod = pendingMethod;
        }
    }

    public static class IgnorableStep extends ReportingAbstractStep {

        public IgnorableStep(String stepAsString) {
            super(StepExecutionType.IGNORABLE, stepAsString);
        }

        @Override
        protected StepResult perform(UUIDExceptionWrapper storyFailure) {
            return ignorable(getStepAsString());
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return perform(storyReporter, storyFailureIfItHappened);
        }
    }

    public static class Comment extends ReportingAbstractStep {

        public Comment(String stepAsString) {
            super(StepExecutionType.COMMENT, stepAsString);
        }

        @Override
        protected StepResult perform(UUIDExceptionWrapper storyFailure) {
            return comment(getStepAsString());
        }

        @Override
        public StepResult doNotPerform(StoryReporter storyReporter, UUIDExceptionWrapper storyFailureIfItHappened) {
            return perform(storyReporter, storyFailureIfItHappened);
        }
    }

    public static enum StepExecutionType {
        EXECUTABLE,
        PENDING,
        IGNORABLE,
        COMMENT,
        NOT_PERFORMED;
    }

    private class MethodInvoker {
        private final Method method;
        private final ParameterConverters parameterConverters;
        private final Paranamer paranamer;
        private final Meta meta;
        private final Type[] parameterTypes;
        
        public MethodInvoker(Method method, ParameterConverters parameterConverters, Paranamer paranamer, Meta meta) {
            this.method = method;
            this.parameterConverters = parameterConverters;
            this.paranamer = paranamer;
            this.meta = meta;
            this.parameterTypes = method.getGenericParameterTypes();
        }

        public Object invoke() throws InvocationTargetException, IllegalAccessException {
            return method.invoke(stepsInstance(), parameterValuesFrom(meta));
        }

        private Parameter[] methodParameters() {
            Parameter[] parameters = new Parameter[parameterTypes.length];
            String[] annotatedNames = annotatedParameterNames(method);
            String[] paranamerNames = paranamer.lookupParameterNames(method, false);

            for (int position = 0; position < parameterTypes.length; position++) {
                String name = parameterNameFor(position, annotatedNames, paranamerNames);
                parameters[position] = new Parameter(position, parameterTypes[position], name);
            }

            return parameters;
        }

        private String parameterNameFor(int position, String[] annotatedNames, String[] paranamerNames) {
            String annotatedName = nameByPosition(annotatedNames, position);
            String paranamerName = nameByPosition(paranamerNames, position);
            if (annotatedName != null) {
                return annotatedName;
            } else if (paranamerName != null) {
                return paranamerName;
            }
            return null;
        }

        private String nameByPosition(String[] names, int position) {
            return position < names.length ? names[position] : null;
        }

        private Object[] parameterValuesFrom(Meta meta) {
            Object[] values = new Object[parameterTypes.length];
            for (Parameter parameter : methodParameters()) {
                values[parameter.position] = parameterConverters.convert(parameter.valueFrom(meta), parameter.type);
            }
            return values;
        }

        private class Parameter {
            private final int position;
            private final Type type;
            private final String name;

            public Parameter(int position, Type type, String name) {
                this.position = position;
                this.type = type;
                this.name = name;
            }

            public String valueFrom(Meta meta) {
                if (name == null) {
                    return null;
                }
                return meta.getProperty(name);
            }
        }
    }

    private static class ParameterName {
        private String name;
        private boolean annotated;
        private boolean fromContext;

        private ParameterName(String name, boolean annotated, boolean fromContext) {
            this.name = name;
            this.annotated = annotated;
            this.fromContext = fromContext;
        }
    }
}