StepCandidate.java

package org.jbehave.core.steps;

import static java.text.MessageFormat.format;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.thoughtworks.paranamer.Paranamer;

import org.apache.commons.lang3.StringUtils;
import org.jbehave.core.annotations.AfterScenario.Outcome;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Pending;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.configuration.Keywords.StartingWordNotFound;
import org.jbehave.core.parsers.StepMatcher;
import org.jbehave.core.parsers.StepPatternParser;
import org.jbehave.core.steps.context.StepsContext;

/**
 * A StepCandidate is associated to a Java method annotated with {@link Given},
 * {@link When}, {@link Then} in a steps instance class. The StepCandidate is
 * responsible for matching the textual step against the pattern contained in
 * the method annotation via the {@link StepMatcher} and for the creation of the
 * matched executable step via the {@link StepCreator}.  The name of a StepCandidate
 * is the combination of its starting word and its pattern.
 */
public class StepCandidate {

    public static final String NAME_FORMAT = "{0} {1}";
    private final String patternAsString;
    private final Integer priority;
    private final StepType stepType;
    private final Method method;
    private final Class<?> stepsType;
    private final InjectableStepsFactory stepsFactory;
    private final Keywords keywords;
    private final StepMatcher stepMatcher;
    private final StepCreator stepCreator;
    private final String parameterPrefix;
    private String[] composedSteps;
    private StepMonitor stepMonitor = new SilentStepMonitor();

    public StepCandidate(String patternAsString, int priority, StepType stepType, Method method, Class<?> stepsType,
            InjectableStepsFactory stepsFactory, StepsContext stepsContext, Keywords keywords,
            StepPatternParser stepPatternParser, ParameterConverters parameterConverters,
            ParameterControls parameterControls) {
        this(patternAsString, priority, stepType, method, stepsType, stepsFactory, stepsContext, keywords,
                stepPatternParser.parseStep(stepType, patternAsString), stepPatternParser.getPrefix(),
                parameterConverters, parameterControls);
    }

    private StepCandidate(String patternAsString, int priority, StepType stepType, Method method, Class<?> stepsType,
            InjectableStepsFactory stepsFactory, StepsContext stepsContext, Keywords keywords,
            StepMatcher stepMatcher, String parameterPrefixString, ParameterConverters parameterConverters,
            ParameterControls parameterControls) {
        this(patternAsString, priority, stepType, method, stepsType, stepsFactory, keywords, stepMatcher,
                parameterPrefixString, new StepCreator(stepsType, stepsFactory, stepsContext, parameterConverters,
                        parameterControls, stepMatcher, new SilentStepMonitor()));
    }

    public StepCandidate(String patternAsString, int priority, StepType stepType, Method method, Class<?> stepsType,
            InjectableStepsFactory stepsFactory, Keywords keywords, StepMatcher stepMatcher,
            String parameterPrefixString, StepCreator stepCreator) {
        this.patternAsString = patternAsString;
        this.priority = priority;
        this.stepType = stepType;
        this.method = method;
        this.stepsType = stepsType;
        this.stepsFactory = stepsFactory;
        this.keywords = keywords;
        this.stepMatcher = stepMatcher;
        this.stepCreator = stepCreator;
        this.parameterPrefix = parameterPrefixString;
    }

    public Method getMethod() {
        return method;
    }

    public String getName() {
        return format(NAME_FORMAT, getStartingWord(), getPatternAsString());
    }

    public Integer getPriority() {
        return priority;
    }

    public String getPatternAsString() {
        return patternAsString;
    }

    public Object getStepsInstance() {
        return stepsFactory != null ? stepsFactory.createInstanceOfType(stepsType) : null;
    }

    public Class<?> getStepsType() {
        return stepsType;
    }

    public StepType getStepType() {
        return stepType;
    }

    public String getStartingWord() {
        return keywords.startingWordFor(stepType);
    }

    public String getParameterPrefix() {
        return parameterPrefix;
    }

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

    public void doDryRun(boolean dryRun) {
        this.stepCreator.doDryRun(dryRun);
    }

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

    public void composedOf(String[] steps) {
        this.composedSteps = steps;
    }

    public boolean isComposite() {
        return composedSteps != null && composedSteps.length > 0;
    }

    public String[] composedSteps() {
        return composedSteps;
    }

    public boolean ignore(String stepAsString) {
        try {
            String ignoreWord = keywords.startingWordFor(StepType.IGNORABLE);
            return isIgnoredStep(stepAsString, ignoreWord);
        } catch (StartingWordNotFound e) {
            return false;
        }
    }

    public boolean comment(String stepAsString) {
        try {
            String ignoreWord = keywords.startingWordFor(StepType.IGNORABLE);
            return keywords.stepStartsWithWord(stepAsString, ignoreWord) && !isIgnoredStep(stepAsString, ignoreWord);
        } catch (StartingWordNotFound e) {
            return false;
        }
    }

    private boolean isIgnoredStep(String stepAsString, String ignoreWord) {
        for (Map.Entry<StepType, String> stepStartingWord : keywords.startingWordsByType().entrySet()) {
            if (stepStartingWord.getKey() != StepType.IGNORABLE) {
                if (keywords.stepStartsWithWords(stepAsString, ignoreWord, stepStartingWord.getValue())) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean isPending() {
        return method != null && method.isAnnotationPresent(Pending.class);
    }

    public boolean matches(String stepAsString) {
        return matches(stepAsString, null);
    }

    public boolean matches(String step, String previousNonAndStep) {
        try {
            boolean matchesType = true;
            if (keywords.isAndStep(step)) {
                if (previousNonAndStep == null) {
                    // cannot handle AND step with no previous step
                    matchesType = false;
                } else {
                    // previous step type should match candidate step type
                    matchesType = stepType == keywords.stepTypeFor(previousNonAndStep);
                }
            }
            stepMonitor.stepMatchesType(step, previousNonAndStep, matchesType, stepType, method, stepsType);
            boolean matchesPattern = stepMatcher.matcher(stripStartingWord(step)).matches();
            stepMonitor.stepMatchesPattern(step, matchesPattern, stepMatcher.pattern(), method, stepsType);
            // must match both type and pattern
            return matchesType && matchesPattern;
        } catch (StartingWordNotFound e) {
            return false;
        }
    }

    public Step createMatchedStep(String stepAsString, Map<String, String> namedParameters, List<Step> composedSteps) {
        return stepCreator.createParametrisedStep(method, stepAsString, stripStartingWord(stepAsString),
                namedParameters, composedSteps);
    }

    public Step createMatchedStepUponOutcome(String stepAsString, Map<String, String> namedParameters,
            List<Step> composedSteps, Outcome outcome) {
        return stepCreator.createParametrisedStepUponOutcome(method, stepAsString, stripStartingWord(stepAsString),
                namedParameters, composedSteps, outcome);
    }

    public void addComposedSteps(List<Step> steps, String stepAsString, Map<String, String> namedParameters,
            List<StepCandidate> allCandidates) {
        Map<String, String> matchedParameters = stepCreator.matchedParameters(method,
                keywords.stepWithoutStartingWord(stepAsString), namedParameters);

        Map<String, String> mergedParameters = new HashMap<>(namedParameters);
        mergedParameters.putAll(matchedParameters);

        String previousNonAndStep = null;
        for (String composedStep : composedSteps) {
            addComposedStep(steps, composedStep, previousNonAndStep, mergedParameters, allCandidates);
            if (!(keywords.isAndStep(composedStep) || keywords.isIgnorableStep(composedStep))) {
                // only update previous step if not AND or IGNORABLE step
                previousNonAndStep = composedStep;
            }
        }
    }

    private void addComposedStep(List<Step> steps, String composedStep, String previousNonAndStep,
            Map<String, String> matchedParameters, List<StepCandidate> allCandidates) {
        if (ignore(composedStep)) {
            // ignorable steps are added so they can be reported
            steps.add(StepCreator.createIgnorableStep(composedStep));
        } else if (comment(composedStep)) {
            // comments are added so they can be reported
            steps.add(StepCreator.createComment(composedStep));
        } else {
            StepCandidate candidate = findComposedCandidate(composedStep, previousNonAndStep, allCandidates);
            if (candidate != null) {
                List<Step> composedSteps = new ArrayList<>();
                if (candidate.isComposite()) {
                    // candidate is itself composite: recursively add composed steps
                    candidate.addComposedSteps(composedSteps, composedStep, matchedParameters, allCandidates);
                }
                steps.add(candidate.createMatchedStep(composedStep, matchedParameters, composedSteps));
            } else {
                steps.add(StepCreator.createPendingStep(composedStep, previousNonAndStep));
            }
        }
    }

    private StepCandidate findComposedCandidate(String composedStep, String previousNonAndStep,
            List<StepCandidate> allCandidates) {
        StepType stepType;
        if (keywords.isAndStep(composedStep)) {
            if (previousNonAndStep != null) {
                stepType = keywords.stepTypeFor(previousNonAndStep);
            } else {
                // cannot handle AND step with no previous step
                return null;
            }
        } else {
            stepType = keywords.stepTypeFor(composedStep);
        }
        for (StepCandidate candidate : allCandidates) {
            if (stepType == candidate.getStepType() && (StringUtils.endsWith(composedStep,
                    candidate.getPatternAsString()) || candidate.matches(composedStep, previousNonAndStep))) {
                return candidate;
            }
        }
        return null;
    }

    protected String stripStartingWord(String stepAsString) {
        return keywords.stepWithoutStartingWord(stepAsString, stepType);
    }

    protected StepCreator getStepCreator() {
        return stepCreator;
    }

    protected Keywords getKeywords() {
        return keywords;
    }

    protected StepMatcher getStepMatcher() {
        return stepMatcher;
    }

    protected StepMonitor getStepMonitor() {
        return stepMonitor;
    }

    @Override
    public String toString() {
        return stepType + " " + patternAsString;
    }

}