package org.jbehave.core.steps;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
/**
* <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 PrioritisingStrategy prioritisingStrategy;
/**
* 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;
}
/**
* 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) {
List<Stepdoc> stepdocs = new LinkedList<Stepdoc>();
for (StepCandidate candidate : collectCandidates(candidateSteps)) {
stepdocs.add(new Stepdoc(candidate));
}
return stepdocs;
}
/**
* 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) {
List<Stepdoc> matching = new ArrayList<Stepdoc>();
for (StepCandidate candidate : collectCandidates(candidateSteps)) {
if (candidate.matches(stepAsText)) {
matching.add(new Stepdoc(candidate));
}
}
return matching;
}
/**
* 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<Object>();
for (CandidateSteps steps : candidateSteps) {
if (steps instanceof Steps) {
instances.add(((Steps) steps).instance());
}
}
return instances;
}
/**
* Collects a list of step candidates from {@link CandidateSteps} instances.
*
* @param candidateSteps
* the list {@link CandidateSteps} instances
* @return A List of {@link StepCandidate}
*/
public List<StepCandidate> collectCandidates(List<CandidateSteps> candidateSteps) {
List<StepCandidate> collected = new ArrayList<StepCandidate>();
for (CandidateSteps steps : candidateSteps) {
collected.addAll(steps.listCandidates());
}
return collected;
}
/**
* 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 {
public List<StepCandidate> prioritise(String stepAsText, List<StepCandidate> candidates) {
Collections.sort(candidates, new Comparator<StepCandidate>() {
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();
public List<StepCandidate> prioritise(final String stepAsText, List<StepCandidate> candidates) {
Collections.sort(candidates, new Comparator<StepCandidate>() {
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 {
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;
}
}
}
}