PatternVariantBuilder.java

package org.jbehave.core.steps;

import static java.util.Arrays.asList;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>
 * Builds a set of pattern variants of given pattern input, supporting a custom
 * directives. Depending on the directives present, one or more resulting
 * variants are created.
 * </p>
 * <p>
 * Currently supported directives are
 * </p>
 * <table border="1">
 * <thead>
 * <tr>
 * <td>Pattern</td>
 * <td>Result</td>
 * </tr>
 * </thead> <tbody>
 * <tr>
 * <td>..A {x|y} B..</td>
 * <td>
 * <ul>
 * <li>..A x B..</li>
 * <li>..A y B..</li>
 * </ul>
 * </td>
 * </tr>
 * <tr>
 * <td>..A {x|y|} B..</td>
 * <td>
 * <ul>
 * <li>..A x B..</li>
 * <li>..A y B..</li>
 * <li>..A B..</li>
 * </ul>
 * </td>
 * </tr>
 * <tr>
 * <td>..A {x} B..</td>
 * <td>
 * <ul>
 * <li>..A x B..</li>
 * </ul>
 * </td>
 * </tr>
 * </table>
 * <p>
 * These directives can be used to conveniently create several variants of a
 * step pattern, without having to repeat it as a whole as one or more aliases.
 * </p>
 * <p>
 * Examples:
 * </p>
 * <ul>
 *     <li>
 *         <p>
 *             <code>@Then("the result {must |has to |}be $x")<br> public void checkResult(int x)...</code>
 *         </p>
 *         <p>
 *             Would match any of these variants from a story file:
 *             <ul>
 *                 <li>Then the result must be 42</li>
 *                 <li>Then the result has to be 42</li>
 *                 <li>Then the result be 42</li>
 *             </ul>
 *         </p>
 *     </li>
 *     <li>
 *         <p>
 *             <code>@When("$A {+|plus|is added to} $B")<br> public void add(int A, int B)...</code>
 *         </p>
 *         <p>
 *             Would match any of these variants from a story file:
 *             <ul>
 *                 <li>When 42 + 23</li>
 *                 <li>When 42 plus 23</li>
 *                 <li>When 42 is added to 23</li>
 *             </ul>
 *         </p>
 *     </li>
 * </ul>
 *
 * @author Daniel Schneller
 */
public class PatternVariantBuilder {

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

    private final Set<String> variants;

    private final String input;

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

    public String getInput() {
        return input;
    }

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

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

        if (!matches) {
            // if the regex does not find any patterns,
            // simply add the input as is
            variants.add(input);
            // end recursion
            return variants;
        }

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

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

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

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

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

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

            // append all variants of the tail end
            // and add each of them to the part we have
            // built up so far.
            for (String tailVariant : tails) {
                StringBuilder tailBuilder = new StringBuilder(builder.toString());
                tailBuilder.append(tailVariant);
                variants.add(tailBuilder.toString());
            }
        }
        return variants;
    }

    /**
     * Returns a new copy set of all variants with no whitespace compression.
     * 
     * @return a {@link Set} of all variants without whitespace compression
     * @see #allVariants(boolean)
     */
    public Set<String> allVariants() {
        return allVariants(false);
    }

    /**
     * <p>
     * Returns a new copy set of all variants. Any two or more consecutive white
     * space characters will be condensed into a single space if boolean flag is
     * set.
     * </p>
     * <p>
     * Otherwise, any whitespace will be left as is.
     * </p>
     * 
     * @param compressWhitespace whether or not to compress whitespace
     * @return a {@link Set} of all variants
     */
    public Set<String> allVariants(boolean compressWhitespace) {
        if (!compressWhitespace) {
            return new HashSet<>(variants);
        }
        Set<String> compressed = new HashSet<>();
        for (String variant : variants) {
            compressed.add(variant.replaceAll("\\s{2,}", " "));
        }
        return compressed;
    }

}