PatternVariantBuilder.java

  1. package org.jbehave.core.steps;

  2. import static java.util.Arrays.asList;

  3. import java.util.ArrayList;
  4. import java.util.HashSet;
  5. import java.util.List;
  6. import java.util.Set;
  7. import java.util.regex.Matcher;
  8. import java.util.regex.Pattern;

  9. /**
  10.  * <p>
  11.  * Builds a set of pattern variants of given pattern input, supporting a custom
  12.  * directives. Depending on the directives present, one or more resulting
  13.  * variants are created.
  14.  * </p>
  15.  * <p>
  16.  * Currently supported directives are
  17.  * </p>
  18.  * <table border="1">
  19.  * <thead>
  20.  * <tr>
  21.  * <td>Pattern</td>
  22.  * <td>Result</td>
  23.  * </tr>
  24.  * </thead> <tbody>
  25.  * <tr>
  26.  * <td>..A {x|y} B..</td>
  27.  * <td>
  28.  * <ul>
  29.  * <li>..A x B..</li>
  30.  * <li>..A y B..</li>
  31.  * </ul>
  32.  * </td>
  33.  * </tr>
  34.  * <tr>
  35.  * <td>..A {x|y|} B..</td>
  36.  * <td>
  37.  * <ul>
  38.  * <li>..A x B..</li>
  39.  * <li>..A y B..</li>
  40.  * <li>..A B..</li>
  41.  * </ul>
  42.  * </td>
  43.  * </tr>
  44.  * <tr>
  45.  * <td>..A {x} B..</td>
  46.  * <td>
  47.  * <ul>
  48.  * <li>..A x B..</li>
  49.  * </ul>
  50.  * </td>
  51.  * </tr>
  52.  * </table>
  53.  * <p>
  54.  * These directives can be used to conveniently create several variants of a
  55.  * step pattern, without having to repeat it as a whole as one or more aliases.
  56.  * </p>
  57.  * <p>
  58.  * Examples:
  59.  * </p>
  60.  * <ul>
  61.  *     <li>
  62.  *         <p>
  63.  *             <code>@Then("the result {must |has to |}be $x")<br> public void checkResult(int x)...</code>
  64.  *         </p>
  65.  *         <p>
  66.  *             Would match any of these variants from a story file:
  67.  *             <ul>
  68.  *                 <li>Then the result must be 42</li>
  69.  *                 <li>Then the result has to be 42</li>
  70.  *                 <li>Then the result be 42</li>
  71.  *             </ul>
  72.  *         </p>
  73.  *     </li>
  74.  *     <li>
  75.  *         <p>
  76.  *             <code>@When("$A {+|plus|is added to} $B")<br> public void add(int A, int B)...</code>
  77.  *         </p>
  78.  *         <p>
  79.  *             Would match any of these variants from a story file:
  80.  *             <ul>
  81.  *                 <li>When 42 + 23</li>
  82.  *                 <li>When 42 plus 23</li>
  83.  *                 <li>When 42 is added to 23</li>
  84.  *             </ul>
  85.  *         </p>
  86.  *     </li>
  87.  * </ul>
  88.  *
  89.  * @author Daniel Schneller
  90.  */
  91. public class PatternVariantBuilder {

  92.     /**
  93.      * Regular expression that locates patterns to be evaluated in the input
  94.      * pattern.
  95.      */
  96.     private final Pattern regex = Pattern.compile("([^\\n{]*+)(\\{(([^|}]++)(\\|)?+)*+\\})([^\\n]*+)");

  97.     private final Set<String> variants;

  98.     private final String input;

  99.     /**
  100.      * Creates a builder and calculates all variants for given input. When there
  101.      * are no variants found in the input, it will itself be the only result.
  102.      *
  103.      * @param input to be evaluated
  104.      */
  105.     public PatternVariantBuilder(String input) {
  106.         this.input = input;
  107.         this.variants = variantsFor(input);
  108.     }

  109.     public String getInput() {
  110.         return input;
  111.     }

  112.     /**
  113.      * <p>
  114.      * Parses the {@link #input} received at construction and generates the
  115.      * variants. When there are multiple patterns in the input, the method will
  116.      * recurse on itself to generate the variants for the tailing end after the
  117.      * first matched pattern.
  118.      * </p>
  119.      * <p>
  120.      * Generated variants are stored in a {@link Set}, so there will never be
  121.      * any duplicates, even if the input's patterns were to result in such.
  122.      * </p>
  123.      */
  124.     private Set<String> variantsFor(String input) {
  125.         // Store current invocation's results
  126.         Set<String> variants = new HashSet<>();

  127.         Matcher m = regex.matcher(input);
  128.         boolean matches = m.matches();

  129.         if (!matches) {
  130.             // if the regex does not find any patterns,
  131.             // simply add the input as is
  132.             variants.add(input);
  133.             // end recursion
  134.             return variants;
  135.         }

  136.         // isolate the part before the first pattern
  137.         String head = m.group(1);

  138.         // isolate the pattern itself, removing its wrapping {}
  139.         String patternGroup = m.group(2).replaceAll("[\\{\\}]", "");

  140.         // isolate the remaining part of the input
  141.         String tail = m.group(6);

  142.         // split the pattern into its options and add an empty
  143.         // string if it ends with a separator
  144.         List<String> patternParts = new ArrayList<>();
  145.         patternParts.addAll(asList(patternGroup.split("\\|")));
  146.         if (patternGroup.endsWith("|")) {
  147.             patternParts.add("");
  148.         }

  149.         // Iterate over the current pattern's
  150.         // variants and construct the result.
  151.         for (String part : patternParts) {
  152.             StringBuilder builder = new StringBuilder();
  153.             if (head != null) {
  154.                 builder.append(head);
  155.             }
  156.             builder.append(part);

  157.             // recurse on the tail of the input
  158.             // to handle the next pattern
  159.             Set<String> tails = variantsFor(tail);

  160.             // append all variants of the tail end
  161.             // and add each of them to the part we have
  162.             // built up so far.
  163.             for (String tailVariant : tails) {
  164.                 StringBuilder tailBuilder = new StringBuilder(builder.toString());
  165.                 tailBuilder.append(tailVariant);
  166.                 variants.add(tailBuilder.toString());
  167.             }
  168.         }
  169.         return variants;
  170.     }

  171.     /**
  172.      * Returns a new copy set of all variants with no whitespace compression.
  173.      *
  174.      * @return a {@link Set} of all variants without whitespace compression
  175.      * @see #allVariants(boolean)
  176.      */
  177.     public Set<String> allVariants() {
  178.         return allVariants(false);
  179.     }

  180.     /**
  181.      * <p>
  182.      * Returns a new copy set of all variants. Any two or more consecutive white
  183.      * space characters will be condensed into a single space if boolean flag is
  184.      * set.
  185.      * </p>
  186.      * <p>
  187.      * Otherwise, any whitespace will be left as is.
  188.      * </p>
  189.      *
  190.      * @param compressWhitespace whether or not to compress whitespace
  191.      * @return a {@link Set} of all variants
  192.      */
  193.     public Set<String> allVariants(boolean compressWhitespace) {
  194.         if (!compressWhitespace) {
  195.             return new HashSet<>(variants);
  196.         }
  197.         Set<String> compressed = new HashSet<>();
  198.         for (String variant : variants) {
  199.             compressed.add(variant.replaceAll("\\s{2,}", " "));
  200.         }
  201.         return compressed;
  202.     }

  203. }