StepFinder.java

  1. package org.jbehave.core.steps;

  2. import java.util.ArrayList;
  3. import java.util.Collections;
  4. import java.util.Comparator;
  5. import java.util.List;
  6. import java.util.function.Predicate;
  7. import java.util.stream.Collectors;

  8. import org.apache.commons.lang3.StringUtils;
  9. import org.jbehave.core.annotations.Given;
  10. import org.jbehave.core.annotations.Then;
  11. import org.jbehave.core.condition.ReflectionBasedStepConditionMatcher;
  12. import org.jbehave.core.condition.StepConditionMatcher;
  13. import org.jbehave.core.embedder.AllStepCandidates;

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

  30.     private final PrioritisingStrategy prioritisingStrategy;
  31.     private final StepConditionMatcher stepConditionMatcher;

  32.     /**
  33.      * Creates a StepFinder with a {@link ByPriorityField} strategy
  34.      */
  35.     public StepFinder() {
  36.         this(new ByPriorityField());
  37.     }

  38.     /**
  39.      * Creates a StepFinder with a custom strategy
  40.      *
  41.      * @param prioritisingStrategy
  42.      *            the PrioritisingStrategy
  43.      */
  44.     public StepFinder(PrioritisingStrategy prioritisingStrategy) {
  45.         this.prioritisingStrategy = prioritisingStrategy;
  46.         this.stepConditionMatcher = new ReflectionBasedStepConditionMatcher();
  47.     }

  48.     /**
  49.      * Creates a StepFinder with a custom step condition matcher
  50.      *
  51.      * @param stepConditionMatcher
  52.      *            the StepConditionMatcher
  53.      */
  54.     public StepFinder(StepConditionMatcher stepConditionMatcher) {
  55.         this.prioritisingStrategy = new ByPriorityField();
  56.         this.stepConditionMatcher = stepConditionMatcher;
  57.     }

  58.     /**
  59.      * Returns the stepdocs for the candidates collected from the given
  60.      * {@link CandidateSteps}.
  61.      *
  62.      * @param candidateSteps
  63.      *            the List of CandidateSteps
  64.      * @return The List of Stepdocs, one for each {@link StepCandidate}.
  65.      */
  66.     public List<Stepdoc> stepdocs(List<CandidateSteps> candidateSteps) {
  67.         return createStepdocs(candidate -> true, candidateSteps);
  68.     }

  69.     /**
  70.      * Finds matching steps, represented as {@link Stepdoc}s, for a given
  71.      * textual step and a list of {@link CandidateSteps}.
  72.      *
  73.      * @param stepAsText
  74.      *            the textual step
  75.      * @param candidateSteps
  76.      *            the List of CandidateSteps
  77.      * @return The list of Stepdocs, one for each matched {@link StepCandidate}.
  78.      */
  79.     public List<Stepdoc> findMatching(String stepAsText, List<CandidateSteps> candidateSteps) {
  80.         return createStepdocs(candidate -> candidate.matches(stepAsText), candidateSteps);
  81.     }

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

  89.     /**
  90.      * Returns the steps instances associated to CandidateSteps
  91.      *
  92.      * @param candidateSteps
  93.      *            the List of CandidateSteps
  94.      * @return The List of steps instances
  95.      */
  96.     public List<Object> stepsInstances(List<CandidateSteps> candidateSteps) {
  97.         List<Object> instances = new ArrayList<>();
  98.         for (CandidateSteps steps : candidateSteps) {
  99.             if (steps instanceof Steps) {
  100.                 instances.add(((Steps) steps).instance());
  101.             }
  102.         }
  103.         return instances;
  104.     }

  105.     /**
  106.      * Prioritises the list of step candidates that match a given step.
  107.      *
  108.      * @param stepAsText
  109.      *            the textual step to match
  110.      * @param candidates
  111.      *            the List of StepCandidate
  112.      * @return The prioritised list according to the
  113.      *         {@link PrioritisingStrategy}.
  114.      */
  115.     public List<StepCandidate> prioritise(String stepAsText, List<StepCandidate> candidates) {
  116.         return prioritisingStrategy.prioritise(stepAsText, candidates);
  117.     }

  118.     /**
  119.      * Defines the priorising strategy of step candidates
  120.      */
  121.     public static interface PrioritisingStrategy {

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

  123.     }

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

  130.         @Override
  131.         public List<StepCandidate> prioritise(String stepAsText, List<StepCandidate> candidates) {
  132.             Collections.sort(candidates, new Comparator<StepCandidate>() {
  133.                 @Override
  134.                 public int compare(StepCandidate o1, StepCandidate o2) {
  135.                     return o2.getPriority().compareTo(o1.getPriority());
  136.                 }
  137.             });
  138.             return candidates;
  139.         }

  140.     }

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

  146.         private LevenshteinDistance ld = new LevenshteinDistance();

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

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

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

  167.             });
  168.             return candidates;
  169.         }

  170.         private class LevenshteinDistance {

  171.             @SuppressWarnings("checkstyle:LocalVariableName")
  172.             public int calculate(String s, String t) {
  173.                 int[][] d; // matrix
  174.                 int n; // length of s
  175.                 int m; // length of t
  176.                 int i; // iterates through s
  177.                 int j; // iterates through t
  178.                 char s_i; // ith character of s
  179.                 char t_j; // jth character of t
  180.                 int cost; // cost

  181.                 // Step 1
  182.                 n = s.length();
  183.                 m = t.length();
  184.                 if (n == 0) {
  185.                     return m;
  186.                 }
  187.                 if (m == 0) {
  188.                     return n;
  189.                 }
  190.                 d = new int[n + 1][m + 1];
  191.                 // Step 2
  192.                 for (i = 0; i <= n; i++) {
  193.                     d[i][0] = i;
  194.                 }
  195.                 for (j = 0; j <= m; j++) {
  196.                     d[0][j] = j;
  197.                 }
  198.                 // Step 3
  199.                 for (i = 1; i <= n; i++) {
  200.                     s_i = s.charAt(i - 1);
  201.                     // Step 4
  202.                     for (j = 1; j <= m; j++) {
  203.                         t_j = t.charAt(j - 1);
  204.                         // Step 5
  205.                         if (s_i == t_j) {
  206.                             cost = 0;
  207.                         } else {
  208.                             cost = 1;
  209.                         }
  210.                         // Step 6
  211.                         d[i][j] = minimum(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
  212.                     }
  213.                 }
  214.                 // Step 7
  215.                 return d[n][m];
  216.             }

  217.             private int minimum(int a, int b, int c) {
  218.                 int mi = a;
  219.                 if (b < mi) {
  220.                     mi = b;
  221.                 }
  222.                 if (c < mi) {
  223.                     mi = c;
  224.                 }
  225.                 return mi;
  226.             }

  227.         }

  228.     }

  229. }