StepFinder.java

package org.jbehave.core.steps;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.condition.ReflectionBasedStepConditionMatcher;
import org.jbehave.core.condition.StepConditionMatcher;
import org.jbehave.core.embedder.AllStepCandidates;

/**
 * <p>
 * StepFinder is reponsible for finding and prioritising step candidates or
 * finding steps instances from {@link CandidateSteps}, which are created using
 * an {@link InjectableStepsFactory}.
 * </p>
 * <p>
 * The {@link StepCandidate}s are responsible for the matching of a particular
 * textual step and are sometimes represented as {@link Stepdoc}s, each of which
 * is simply a facade documenting a candidate. The candidates can be prioritised
 * via an injectable {@link PrioritisingStrategy}, defaulting to
 * {@link ByPriorityField}. A more sophisticated strategy that can be used is
 * the {@link ByLevenshteinDistance}.
 * </p>
 */
public class StepFinder {

    private final PrioritisingStrategy prioritisingStrategy;
    private final StepConditionMatcher stepConditionMatcher;

    /**
     * Creates a StepFinder with a {@link ByPriorityField} strategy
     */
    public StepFinder() {
        this(new ByPriorityField());
    }

    /**
     * Creates a StepFinder with a custom strategy
     * 
     * @param prioritisingStrategy
     *            the PrioritisingStrategy
     */
    public StepFinder(PrioritisingStrategy prioritisingStrategy) {
        this.prioritisingStrategy = prioritisingStrategy;
        this.stepConditionMatcher = new ReflectionBasedStepConditionMatcher();
    }

    /**
     * Creates a StepFinder with a custom step condition matcher
     * 
     * @param stepConditionMatcher
     *            the StepConditionMatcher
     */
    public StepFinder(StepConditionMatcher stepConditionMatcher) {
        this.prioritisingStrategy = new ByPriorityField();
        this.stepConditionMatcher = stepConditionMatcher;
    }

    /**
     * Returns the stepdocs for the candidates collected from the given
     * {@link CandidateSteps}.
     * 
     * @param candidateSteps
     *            the List of CandidateSteps
     * @return The List of Stepdocs, one for each {@link StepCandidate}.
     */
    public List<Stepdoc> stepdocs(List<CandidateSteps> candidateSteps) {
        return createStepdocs(candidate -> true, candidateSteps);
    }

    /**
     * Finds matching steps, represented as {@link Stepdoc}s, for a given
     * textual step and a list of {@link CandidateSteps}.
     * 
     * @param stepAsText
     *            the textual step
     * @param candidateSteps
     *            the List of CandidateSteps
     * @return The list of Stepdocs, one for each matched {@link StepCandidate}.
     */
    public List<Stepdoc> findMatching(String stepAsText, List<CandidateSteps> candidateSteps) {
        return createStepdocs(candidate -> candidate.matches(stepAsText), candidateSteps);
    }

    private List<Stepdoc> createStepdocs(Predicate<StepCandidate> candidateFilter,
            List<CandidateSteps> candidateSteps) {
        return new AllStepCandidates(stepConditionMatcher, candidateSteps).getRegularSteps().stream()
                .filter(candidateFilter)
                .map(Stepdoc::new)
                .collect(Collectors.toList());
    }

    /**
     * Returns the steps instances associated to CandidateSteps
     * 
     * @param candidateSteps
     *            the List of CandidateSteps
     * @return The List of steps instances
     */
    public List<Object> stepsInstances(List<CandidateSteps> candidateSteps) {
        List<Object> instances = new ArrayList<>();
        for (CandidateSteps steps : candidateSteps) {
            if (steps instanceof Steps) {
                instances.add(((Steps) steps).instance());
            }
        }
        return instances;
    }

    /**
     * Prioritises the list of step candidates that match a given step.
     * 
     * @param stepAsText
     *            the textual step to match
     * @param candidates
     *            the List of StepCandidate
     * @return The prioritised list according to the
     *         {@link PrioritisingStrategy}.
     */
    public List<StepCandidate> prioritise(String stepAsText, List<StepCandidate> candidates) {
        return prioritisingStrategy.prioritise(stepAsText, candidates);
    }

    /**
     * Defines the priorising strategy of step candidates
     */
    public static interface PrioritisingStrategy {

        List<StepCandidate> prioritise(String stepAsString, List<StepCandidate> candidates);

    }

    /**
     * Strategy to priorise step candidates by the
     * {@link StepCandidate#getPriority()} field which is settable in the
     * {@link Given}, {@link When}, {@link Then} annotations.
     */
    public static class ByPriorityField implements PrioritisingStrategy {

        @Override
        public List<StepCandidate> prioritise(String stepAsText, List<StepCandidate> candidates) {
            Collections.sort(candidates, new Comparator<StepCandidate>() {
                @Override
                public int compare(StepCandidate o1, StepCandidate o2) {
                    return o2.getPriority().compareTo(o1.getPriority());
                }
            });
            return candidates;
        }

    }

    /**
     * Strategy to priorise candidate steps by <a
     * href="http://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein Distance</a>
     */
    public static class ByLevenshteinDistance implements PrioritisingStrategy {

        private LevenshteinDistance ld = new LevenshteinDistance();

        @Override
        public List<StepCandidate> prioritise(final String stepAsText, List<StepCandidate> candidates) {
            Collections.sort(candidates, new Comparator<StepCandidate>() {
                @Override
                public int compare(StepCandidate o1, StepCandidate o2) {
                    String scoringPattern1 = scoringPattern(o1);
                    String scoringPattern2 = scoringPattern(o2);
                    String stepWithoutStartingWord = trimStartingWord(stepAsText);
                    Integer score1 = 0 - ld.calculate(scoringPattern1, stepWithoutStartingWord);
                    Integer score2 = 0 - ld.calculate(scoringPattern2, stepWithoutStartingWord);
                    int result = score2.compareTo(score1);
                    // default to strategy by priority if no score result
                    return result != 0 ? result : o2.getPriority().compareTo(o1.getPriority());
                }

                private String scoringPattern(StepCandidate candidate) {
                    return candidate.getPatternAsString().replaceAll("\\s\\$\\w+\\s", " ").replaceAll("\\$\\w + ", "");
                }

                private String trimStartingWord(String stepAsString) {
                    return StringUtils.substringAfter(stepAsString, " ");
                }

            });
            return candidates;
        }

        private class LevenshteinDistance {

            @SuppressWarnings("checkstyle:LocalVariableName")
            public int calculate(String s, String t) {
                int[][] d; // matrix
                int n; // length of s
                int m; // length of t
                int i; // iterates through s
                int j; // iterates through t
                char s_i; // ith character of s
                char t_j; // jth character of t
                int cost; // cost

                // Step 1
                n = s.length();
                m = t.length();
                if (n == 0) {
                    return m;
                }
                if (m == 0) {
                    return n;
                }
                d = new int[n + 1][m + 1];
                // Step 2
                for (i = 0; i <= n; i++) {
                    d[i][0] = i;
                }
                for (j = 0; j <= m; j++) {
                    d[0][j] = j;
                }
                // Step 3
                for (i = 1; i <= n; i++) {
                    s_i = s.charAt(i - 1);
                    // Step 4
                    for (j = 1; j <= m; j++) {
                        t_j = t.charAt(j - 1);
                        // Step 5
                        if (s_i == t_j) {
                            cost = 0;
                        } else {
                            cost = 1;
                        }
                        // Step 6
                        d[i][j] = minimum(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
                    }
                }
                // Step 7
                return d[n][m];
            }

            private int minimum(int a, int b, int c) {
                int mi = a;
                if (b < mi) {
                    mi = b;
                }
                if (c < mi) {
                    mi = c;
                }
                return mi;
            }

        }

    }

}