Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
PatternVariantBuilder |
|
| 2.8;2.8 |
1 | package org.jbehave.core.steps; | |
2 | ||
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 | import static java.util.Arrays.asList; | |
11 | ||
12 | /** | |
13 | * <p> | |
14 | * Builds a set of pattern variants of given pattern input, supporting a custom | |
15 | * directives. Depending on the directives present, one or more resulting | |
16 | * variants are created. | |
17 | * </p> | |
18 | * <p> | |
19 | * Currently supported directives are | |
20 | * </p> | |
21 | * <table border="1"> | |
22 | * <thead> | |
23 | * <tr> | |
24 | * <td>Pattern</td> | |
25 | * <td>Result</td> | |
26 | * </tr> | |
27 | * </thead> <tbody> | |
28 | * <tr> | |
29 | * <td>..A {x|y} B..</td> | |
30 | * <td> | |
31 | * <ul> | |
32 | * <li>..A x B..</li> | |
33 | * <li>..A y B..</li> | |
34 | * </ul> | |
35 | * </td> | |
36 | * </tr> | |
37 | * <tr> | |
38 | * <td>..A {x|y|} B..</td> | |
39 | * <td> | |
40 | * <ul> | |
41 | * <li>..A x B..</li> | |
42 | * <li>..A y B..</li> | |
43 | * <li>..A B..</li> | |
44 | * </ul> | |
45 | * </td> | |
46 | * </tr> | |
47 | * <tr> | |
48 | * <td>..A {x} B..</td> | |
49 | * <td> | |
50 | * <ul> | |
51 | * <li>..A x B..</li> | |
52 | * </ul> | |
53 | * </td> | |
54 | * </tr> | |
55 | * </table> | |
56 | * | |
57 | * <p> | |
58 | * These directives can be used to conveniently create several variants of a | |
59 | * step pattern, without having to repeat it as a whole as one or more aliases. | |
60 | * </p> | |
61 | * <p> | |
62 | * Examples: | |
63 | * </p> | |
64 | * <ul> | |
65 | * <li> | |
66 | * <p> | |
67 | * <code> | |
68 | * | |
69 | * @Then("the result {must |has to |}be $x")<br> public void checkResult(int | |
70 | * x)...<br></code> | |
71 | * </p> | |
72 | * <p> | |
73 | * Would match any of these variants from a story file: | |
74 | * <ul> | |
75 | * <li>Then the result must be 42</li> <li>Then the result has to be | |
76 | * 42</li> <li>Then the result be 42</li> | |
77 | * </ul> | |
78 | * </p> | |
79 | * </li> <li> | |
80 | * <p> | |
81 | * <code> | |
82 | * @When("$A {+|plus|is added to} $B")<br> public void add(int A, int B)...<br> | |
83 | * </code> | |
84 | * </p> | |
85 | * <p> | |
86 | * Would match any of these variants from a story file: | |
87 | * <ul> | |
88 | * <li>When 42 + 23</li> <li>When 42 plus 23</li> <li>When 42 is added | |
89 | * to 23</li> | |
90 | * </ul> | |
91 | * </p> | |
92 | * </li> | |
93 | * </ul> | |
94 | * | |
95 | * @author Daniel Schneller | |
96 | */ | |
97 | public class PatternVariantBuilder { | |
98 | ||
99 | /** | |
100 | * Regular expression that locates patterns to be evaluated in the input | |
101 | * pattern. | |
102 | */ | |
103 | 92 | private final Pattern regex = Pattern.compile("([^\\n{]*+)(\\{(([^|}]++)(\\|)?+)*+\\})([^\\n]*+)"); |
104 | ||
105 | private final Set<String> variants; | |
106 | ||
107 | private final String input; | |
108 | ||
109 | /** | |
110 | * Creates a builder and calculates all variants for given input. When there | |
111 | * are no variants found in the input, it will itself be the only result. | |
112 | * | |
113 | * @param input to be evaluated | |
114 | */ | |
115 | 92 | public PatternVariantBuilder(String input) { |
116 | 92 | this.input = input; |
117 | 92 | this.variants = variantsFor(input); |
118 | 92 | } |
119 | ||
120 | public String getInput() { | |
121 | 2 | return input; |
122 | } | |
123 | ||
124 | /** | |
125 | * <p> | |
126 | * Parses the {@link #input} received at construction and generates the | |
127 | * variants. When there are multiple patterns in the input, the method will | |
128 | * recurse on itself to generate the variants for the tailing end after the | |
129 | * first matched pattern. | |
130 | * </p> | |
131 | * <p> | |
132 | * Generated variants are stored in a {@link Set}, so there will never be | |
133 | * any duplicates, even if the input's patterns were to result in such. | |
134 | * </p> | |
135 | */ | |
136 | private Set<String> variantsFor(String input) { | |
137 | // Store current invocation's results | |
138 | 118 | Set<String> variants = new HashSet<String>(); |
139 | ||
140 | 118 | Matcher m = regex.matcher(input); |
141 | 118 | boolean matches = m.matches(); |
142 | ||
143 | 118 | if (!matches) { |
144 | // if the regex does not find any patterns, | |
145 | // simply add the input as is | |
146 | 106 | variants.add(input); |
147 | // end recursion | |
148 | 106 | return variants; |
149 | } | |
150 | ||
151 | // isolate the part before the first pattern | |
152 | 12 | String head = m.group(1); |
153 | ||
154 | // isolate the pattern itself, removing its wrapping {} | |
155 | 12 | String patternGroup = m.group(2).replaceAll("[\\{\\}]", ""); |
156 | ||
157 | // isolate the remaining part of the input | |
158 | 12 | String tail = m.group(6); |
159 | ||
160 | // split the pattern into its options and add an empty | |
161 | // string if it ends with a separator | |
162 | 12 | List<String> patternParts = new ArrayList<String>(); |
163 | 12 | patternParts.addAll(asList(patternGroup.split("\\|"))); |
164 | 12 | if (patternGroup.endsWith("|")) { |
165 | 6 | patternParts.add(""); |
166 | } | |
167 | ||
168 | // Iterate over the current pattern's | |
169 | // variants and construct the result. | |
170 | 12 | for (String part : patternParts) { |
171 | 26 | StringBuilder builder = new StringBuilder(); |
172 | 26 | if (head != null) { |
173 | 26 | builder.append(head); |
174 | } | |
175 | 26 | builder.append(part); |
176 | ||
177 | // recurse on the tail of the input | |
178 | // to handle the next pattern | |
179 | 26 | Set<String> tails = variantsFor(tail); |
180 | ||
181 | // append all variants of the tail end | |
182 | // and add each of them to the part we have | |
183 | // built up so far. | |
184 | 26 | for (String tailVariant : tails) { |
185 | 32 | StringBuilder tailBuilder = new StringBuilder(builder.toString()); |
186 | 32 | tailBuilder.append(tailVariant); |
187 | 32 | variants.add(tailBuilder.toString()); |
188 | 32 | } |
189 | 26 | } |
190 | 12 | return variants; |
191 | } | |
192 | ||
193 | /** | |
194 | * Returns a new copy set of all variants with no whitespace compression. | |
195 | * | |
196 | * @return a {@link Set} of all variants without whitespace compression | |
197 | * @see #allVariants(boolean) | |
198 | */ | |
199 | public Set<String> allVariants() { | |
200 | 91 | return allVariants(false); |
201 | } | |
202 | ||
203 | /** | |
204 | * <p> | |
205 | * Returns a new copy set of all variants. Any two or more consecutive white | |
206 | * space characters will be condensed into a single space if boolean flag is | |
207 | * set. | |
208 | * </p> | |
209 | * <p> | |
210 | * Otherwise, any whitespace will be left as is. | |
211 | * </p> | |
212 | * | |
213 | * @param compressWhitespace whether or not to compress whitespace | |
214 | * @return a {@link Set} of all variants | |
215 | */ | |
216 | public Set<String> allVariants(boolean compressWhitespace) { | |
217 | 92 | if (!compressWhitespace) { |
218 | 91 | return new HashSet<String>(variants); |
219 | } | |
220 | 1 | Set<String> compressed = new HashSet<String>(); |
221 | 1 | for (String variant : variants) { |
222 | 4 | compressed.add(variant.replaceAll("\\s{2,}", " ")); |
223 | 4 | } |
224 | 1 | return compressed; |
225 | } | |
226 | ||
227 | } |