##### Coverage Report - org.jbehave.core.model.ExamplesTable

Classes in this File | Line Coverage | Branch Coverage | Complexity
ExamplesTable |
99% | 140/141
100% | 46/46
| 1.84375;1.844
ExamplesTable$RowNotFound |
100% | 2/2
N/A | N/A
| 1.84375;1.844

1 |  |
     package org.jbehave.core.model;
2 |  |
3 |  |
     import java.io.ByteArrayInputStream;
4 |  |
     import java.io.IOException;
5 |  |
     import java.io.PrintStream;
6 |  |
     import java.util.ArrayList;
7 |  |
     import java.util.Collections;
8 |  |
     import java.util.HashMap;
9 |  |
     import java.util.LinkedHashMap;
10 |  |
     import java.util.List;
11 |  |
     import java.util.Map;
12 |  |
     import java.util.Properties;
13 |  |
     import java.util.regex.Matcher;
14 |  |
     import java.util.regex.Pattern;
15 |  |
16 |  |
     import org.apache.commons.lang.builder.ToStringBuilder;
17 |  |
     import org.apache.commons.lang.builder.ToStringStyle;
18 |  |
     import org.jbehave.core.model.TableTransformers.TableTransformer;
19 |  |
     import org.jbehave.core.steps.ChainedRow;
20 |  |
     import org.jbehave.core.steps.ConvertedParameters;
21 |  |
     import org.jbehave.core.steps.ParameterConverters;
22 |  |
     import org.jbehave.core.steps.Parameters;
23 |  |
     import org.jbehave.core.steps.Row;
24 |  |
25 |  |
     import static java.lang.Boolean.parseBoolean;
26 |  |
     import static java.util.regex.Pattern.DOTALL;
27 |  |
     import static java.util.regex.Pattern.compile;
28 |  |
29 |  |
     /**
30 |  |
      * <p>
31 |  |
      * Represents a tabular structure that holds rows of example data for parameters
32 |  |
      * named via the column headers:
33 |  |
      * <p/>
34 |  |
      *
35 |  |
      * <pre>
36 |  |
      * |header 1|header 2| .... |header n|
37 |  |
      * |value 11|value 12| .... |value 1n|
38 |  |
      * ...
39 |  |
      * |value m1|value m2| .... |value mn|
40 |  |
      * </pre>
41 |  |
      * <p>
42 |  |
      * Different header and value column separators can be specified to replace the
43 |  |
      * default separator "|":
44 |  |
      * </p>
45 |  |
      *
46 |  |
      * <pre>
47 |  |
      * !!header 1!!header 2!! .... !!header n!!
48 |  |
      * !value 11!value 12! .... !value 1n!
49 |  |
      * ...
50 |  |
      * !value m1!value m2| .... !value mn!
51 |  |
      * </pre>
52 |  |
      * <p>
53 |  |
      * Rows starting with an ignorable separator are allowed and ignored:
54 |  |
      * </p>
55 |  |
      *
56 |  |
      * <pre>
57 |  |
      * |header 1|header 2| .... |header n|
58 |  |
      * |-- A commented row --|
59 |  |
      * |value 11|value 12| .... |value 1n|
60 |  |
      * ...
61 |  |
      * |-- Another commented row --|
62 |  |
      * |value m1|value m2| .... |value mn|
63 |  |
      * </pre>
64 |  |
      * <p>
65 |  |
      * Ignorable separator is configurable and defaults to "|--".
66 |  |
      * </p>
67 |  |
      * <p>
68 |  |
      * The separators are also configurable via inlined properties:
69 |  |
      *
70 |  |
      * <pre>
71 |  |
      * {ignorableSeparator=!--,headerSeparator=!,valueSeparator=!}
72 |  |
      * !header 1!header 2! .... !header n!
73 |  |
      * !-- A commented row --!
74 |  |
      * !value 11!value 12! .... !value 1n!
75 |  |
      * ...
76 |  |
      * !-- Another commented row --!
77 |  |
      * !value m1!value m2! .... !value mn!
78 |  |
      * </pre>
79 |  |
      *
80 |  |
      * </p>
81 |  |
      * <p>
82 |  |
      * By default all column values are trimmed. To avoid trimming the values, use
83 |  |
      * the "trim" inlined property:
84 |  |
      *
85 |  |
      * <pre>
86 |  |
      * {trim=false}
87 |  |
      * | header 1 | header 2 | .... | header n |
88 |  |
      * | value 11 | value 12 | .... | value 1n |
89 |  |
      * </pre>
90 |  |
      *
91 |  |
      * </p>
92 |  |
      *
93 |  |
      * <p>
94 |  |
      * The table allows the retrieval of row values as converted parameters. Use
95 |  |
      * {@link #getRowAsParameters(int)} and invoke
96 |  |
      * {@link Parameters#valueAs(String, Class)} specifying the header and the class
97 |  |
      * type of the parameter.
98 |  |
      * </p>
99 |  |
      *
100 |  |
      * <p>
101 |  |
      * The table allows the transformation of its string representation via the
102 |  |
      * "transformer" inlined property:
103 |  |
      *
104 |  |
      * <pre>
105 |  |
      * {transformer=myTransformerName}
106 |  |
      * |header 1|header 2| .... |header n|
107 |  |
      * |value 11|value 12| .... |value 1n|
108 |  |
      * ...
109 |  |
      * |value m1|value m2| .... |value mn|
110 |  |
      * </pre>
111 |  |
      *
112 |  |
      * The transformer needs to be registered by name via the
113 |  |
      * {@link TableTransformers#useTransformer(String, TableTransformer)}. A few
114 |  |
      * transformers are already registered by default in {@link TableTransformers}.
115 |  |
      * </p>
116 |  |
      *
117 |  |
      * <p>
118 |  |
      * Once created, the table row can be modified, via the
119 |  |
      * {@link #withRowValues(int, Map)} method, by specifying the map of row values
120 |  |
      * to be changed.
121 |  |
      * </p>
122 |  |
      *
123 |  |
      * <p>
124 |  |
      * A table can also be created by providing the entire data content, via the
125 |  |
      * {@link #withRows(List<Map<String,String>>)} method.
126 |  |
      *
127 |  |
      * </p>
128 |  |
      * The parsing code assumes that the number of columns for data rows is the same
129 |  |
      * as in the header, if a row has less fields, the remaining are filled with
130 |  |
      * empty values, if it has more, the fields are ignored.
131 |  |
      * <p>
132 |  |
      */
133 |  |
     public class ExamplesTable {
134 | 1 |
         private static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
135 |  |
         private static final String EMPTY_VALUE = "";
136 |  |
137 | 1 |
         public static final ExamplesTable EMPTY = new ExamplesTable("");
138 |  |
139 |  |
         private static final String ROW_SEPARATOR = "\r?\n";
140 |  |
         private static final String NEW_LINE = "\n";
141 |  |
         private static final String HEADER_SEPARATOR = "|";
142 |  |
         private static final String VALUE_SEPARATOR = "|";
143 |  |
         private static final String IGNORABLE_SEPARATOR = "|--";
144 |  |
145 |  |
         private final String tableAsString;
146 |  |
         private final String headerSeparator;
147 |  |
         private final String valueSeparator;
148 |  |
         private final String ignorableSeparator;
149 |  |
         private final ParameterConverters parameterConverters;
150 |  |
         private final TableTransformers tableTransformers;
151 | 182 |
         private final List<String> headers = new ArrayList<String>();
152 | 182 |
         private final List<Map<String, String>> data = new ArrayList<Map<String, String>>();
153 | 182 |
         private final Properties properties = new Properties();
154 | 182 |
         private String propertiesAsString = "";
155 | 182 |
         private Map<String, String> namedParameters = new HashMap<String, String>();
156 | 182 |
         private boolean trim = true;
157 |  |
158 |  |
         private final Row defaults;
159 |  |
160 |  |
         public ExamplesTable(String tableAsString) {
161 | 37 |
             this(tableAsString, HEADER_SEPARATOR, VALUE_SEPARATOR);
162 | 37 |
         }
163 |  |
164 |  |
         public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator) {
165 | 39 |
             this(tableAsString, headerSeparator, valueSeparator, IGNORABLE_SEPARATOR, new ParameterConverters());
166 | 39 |
         }
167 |  |
168 |  |
         public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator,
169 |  |
                 String ignorableSeparator, ParameterConverters parameterConverters) {
170 | 39 |
             this(tableAsString, headerSeparator, valueSeparator, ignorableSeparator, parameterConverters, new TableTransformers());
171 | 39 |
         }
172 |  |
173 |  |
         public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator,
174 | 181 |
                 String ignorableSeparator, ParameterConverters parameterConverters, TableTransformers tableTransformers) {
175 | 181 |
             this.tableAsString = tableAsString;
176 | 181 |
             this.headerSeparator = headerSeparator;
177 | 181 |
             this.valueSeparator = valueSeparator;
178 | 181 |
             this.ignorableSeparator = ignorableSeparator;
179 | 181 |
             this.parameterConverters = parameterConverters;
180 | 181 |
             this.tableTransformers = tableTransformers;
181 | 181 |
             this.defaults = new ConvertedParameters(EMPTY_MAP, parameterConverters);
182 | 181 |
             parse();
183 | 181 |
         }
184 |  |
185 |  |
         private void parse() {
186 | 181 |
             String tableWithoutProperties = stripProperties(tableAsString.trim());
187 | 181 |
             parseProperties(propertiesAsString);
188 | 181 |
             trim = parseBoolean(properties.getProperty("trim", "true"));
189 | 181 |
             parseTable(tableWithoutProperties);
190 | 181 |
         }
191 |  |
192 | 1 |
         private ExamplesTable(ExamplesTable other, Row defaults) {
193 | 1 |
             this.data.addAll(other.data);
194 | 1 |
             this.tableAsString = other.tableAsString;
195 | 1 |
             this.headerSeparator = other.headerSeparator;
196 | 1 |
             this.valueSeparator = other.valueSeparator;
197 | 1 |
             this.ignorableSeparator = other.ignorableSeparator;
198 | 1 |
             this.parameterConverters = other.parameterConverters;
199 | 1 |
             this.tableTransformers = other.tableTransformers;
200 | 1 |
             this.headers.addAll(other.headers);
201 | 1 |
             this.properties.putAll(other.properties);
202 | 1 |
             this.defaults = defaults;
203 | 1 |
         }
204 |  |
205 |  |
         private String stripProperties(String tableAsString) {
206 | 181 |
             Pattern pattern = compile("\\{(.*?)\\}\\s*(.*)", DOTALL);
207 | 181 |
             Matcher matcher = pattern.matcher(tableAsString);
208 | 181 |
             if (matcher.matches()) {
209 | 4 |
                 propertiesAsString = matcher.group(1);
210 | 4 |
                 return matcher.group(2);
211 |  |
             }
212 | 177 |
             return tableAsString;
213 |  |
         }
214 |  |
215 |  |
         private void parseProperties(String propertiesAsString) {
216 | 181 |
             properties.clear();
217 | 181 |
             properties.setProperty("ignorableSeparator", ignorableSeparator);
218 | 181 |
             properties.setProperty("headerSeparator", headerSeparator);
219 | 181 |
             properties.setProperty("valueSeparator", valueSeparator);
220 |  |
             try {
221 | 181 |
                 properties.load(new ByteArrayInputStream(propertiesAsString.replace(",", NEW_LINE).getBytes()));
222 | 0 |
             } catch (IOException e) {
223 |  |
                 // carry on
224 | 181 |
             }
225 | 181 |
         }
226 |  |
227 |  |
         private void parseTable(String tableAsString) {
228 | 181 |
             headers.clear();
229 | 181 |
             data.clear();
230 | 181 |
             String transformer = properties.getProperty("transformer");
231 | 181 |
             if (transformer != null) {
232 | 2 |
                 tableAsString = tableTransformers.transform(transformer, tableAsString, properties);
233 |  |
             }
234 | 181 |
             parseByRows(headers, data, tableAsString);
235 | 181 |
         }
236 |  |
237 |  |
         private void parseByRows(List<String> headers, List<Map<String, String>> data, String tableAsString) {
238 | 181 |
             String[] rows = tableAsString.split(ROW_SEPARATOR);
239 | 2466 |
             for (int row = 0; row < rows.length; row++) {
240 | 2285 |
                 String rowAsString = rows[row].trim();
241 | 2285 |
                 if (rowAsString.startsWith(properties.getProperty("ignorableSeparator")) || rowAsString.length() == 0) {
242 |  |
                     // skip ignorable or empty lines
243 | 127 |
                     continue;
244 | 2152 |
                 } else if (headers.isEmpty()) {
245 | 55 |
                     headers.addAll(TableUtils.parseRow(rowAsString, properties.getProperty("headerSeparator"), trim));
246 |  |
                 } else {
247 | 2097 |
                     List<String> columns = TableUtils.parseRow(rowAsString, properties.getProperty("valueSeparator"), trim);
248 | 2097 |
                     Map<String, String> map = new LinkedHashMap<String, String>();
249 | 22300 |
                     for (int column = 0; column < columns.size(); column++) {
250 | 20203 |
                         if (column < headers.size()) {
251 | 20202 |
                             map.put(headers.get(column), columns.get(column));
252 |  |
                         }
253 |  |
                     }
254 | 2097 |
                     data.add(map);
255 |  |
                 }
256 |  |
             }
257 | 181 |
         }
258 |  |
259 |  |
         public ExamplesTable withDefaults(Parameters defaults) {
260 | 1 |
             return new ExamplesTable(this, new ChainedRow(defaults, this.defaults));
261 |  |
         }
262 |  |
263 |  |
         public ExamplesTable withNamedParameters(Map<String, String> namedParameters) {
264 | 1 |
             this.namedParameters = namedParameters;
265 | 1 |
             return this;
266 |  |
         }
267 |  |
268 |  |
         public ExamplesTable withRowValues(int row, Map<String, String> values) {
269 | 2 |
             getRow(row).putAll(values);
270 | 2 |
             for (String header : values.keySet()) {
271 | 3 |
                 if (!headers.contains(header)) {
272 | 1 |
                     headers.add(header);
273 |  |
                 }
274 |  |
             }
275 | 2 |
             return this;
276 |  |
         }
277 |  |
278 |  |
         public ExamplesTable withRows(List<Map<String, String>> values) {
279 | 1 |
             this.data.clear();
280 | 1 |
             this.data.addAll(values);
281 | 1 |
             this.headers.clear();
282 | 1 |
             this.headers.addAll(values.get(0).keySet());
283 | 1 |
             return this;
284 |  |
         }
285 |  |
286 |  |
         public Properties getProperties() {
287 | 4 |
             return properties;
288 |  |
         }
289 |  |
290 |  |
         public List<String> getHeaders() {
291 | 27 |
             return headers;
292 |  |
         }
293 |  |
294 |  |
         public Map<String, String> getRow(int row) {
295 | 222 |
             if (row > data.size() - 1) {
296 | 1 |
                 throw new RowNotFound(row);
297 |  |
             }
298 | 221 |
             Map<String, String> values = data.get(row);
299 | 221 |
             if (headers.size() != values.keySet().size()) {
300 | 2 |
                 for (String header : headers) {
301 | 5 |
                     if (!values.containsKey(header)) {
302 | 2 |
                         values.put(header, EMPTY_VALUE);
303 |  |
                     }
304 |  |
                 }
305 |  |
             }
306 | 221 |
             return values;
307 |  |
         }
308 |  |
309 |  |
         public Parameters getRowAsParameters(int row) {
310 | 9 |
             return getRowAsParameters(row, false);
311 |  |
         }
312 |  |
313 |  |
         public Parameters getRowAsParameters(int row, boolean replaceNamedParameters) {
314 | 13 |
             Map<String, String> rowValues = getRow(row);
315 | 12 |
             return createParameters((replaceNamedParameters ? replaceNamedParameters(rowValues) : rowValues));
316 |  |
         }
317 |  |
318 |  |
         private Map<String, String> replaceNamedParameters(Map<String, String> row) {
319 | 1 |
             Map<String, String> replaced = new HashMap<String, String>();
320 | 1 |
             for (String key : row.keySet()) {
321 | 2 |
                 String replacedValue = row.get(key);
322 | 2 |
                 for (String namedKey : namedParameters.keySet()) {
323 | 2 |
                     String namedValue = namedParameters.get(namedKey);
324 | 2 |
                     replacedValue = replacedValue.replaceAll(namedKey, namedValue);
325 | 2 |
                 }
326 | 2 |
                 replaced.put(key, replacedValue);
327 | 2 |
             }
328 | 1 |
             return replaced;
329 |  |
         }
330 |  |
331 |  |
         public int getRowCount() {
332 | 196 |
             return data.size();
333 |  |
         }
334 |  |
335 |  |
         public List<Map<String, String>> getRows() {
336 | 50 |
             List<Map<String, String>> rows = new ArrayList<Map<String, String>>();
337 | 143 |
             for (int row = 0; row < getRowCount(); row++) {
338 | 93 |
                 rows.add(getRow(row));
339 |  |
             }
340 | 50 |
             return rows;
341 |  |
         }
342 |  |
343 |  |
         public List<Parameters> getRowsAsParameters() {
344 | 2 |
             return getRowsAsParameters(false);
345 |  |
         }
346 |  |
347 |  |
         public List<Parameters> getRowsAsParameters(boolean replaceNamedParameters) {
348 | 3 |
             List<Parameters> rows = new ArrayList<Parameters>();
349 |  |
350 | 7 |
             for (int row = 0; row < getRowCount(); row++) {
351 | 4 |
                 rows.add(getRowAsParameters(row, replaceNamedParameters));
352 |  |
             }
353 |  |
354 | 3 |
             return rows;
355 |  |
         }
356 |  |
357 |  |
         private Parameters createParameters(Map<String, String> values) {
358 | 12 |
             return new ConvertedParameters(new ChainedRow(new ConvertedParameters(values, parameterConverters), defaults),
359 |  |
                     parameterConverters);
360 |  |
         }
361 |  |
362 |  |
         public String getHeaderSeparator() {
363 | 2 |
             return headerSeparator;
364 |  |
         }
365 |  |
366 |  |
         public String getValueSeparator() {
367 | 2 |
             return valueSeparator;
368 |  |
         }
369 |  |
370 |  |
         public String asString() {
371 | 56 |
             if (data.isEmpty()) {
372 | 35 |
                 return EMPTY_VALUE;
373 |  |
             }
374 | 21 |
             return format();
375 |  |
         }
376 |  |
377 |  |
         public void outputTo(PrintStream output) {
378 | 1 |
             output.print(asString());
379 | 1 |
         }
380 |  |
381 |  |
         private String format() {
382 | 21 |
             StringBuffer sb = new StringBuffer();
383 | 21 |
             for (String header : headers) {
384 | 48 |
                 sb.append(headerSeparator).append(header);
385 |  |
             }
386 | 21 |
             sb.append(headerSeparator).append(NEW_LINE);
387 | 21 |
             for (Map<String, String> row : getRows()) {
388 | 38 |
                 for (String header : headers) {
389 | 88 |
                     sb.append(valueSeparator);
390 | 88 |
                     sb.append(row.get(header));
391 |  |
                 }
392 | 38 |
                 sb.append(valueSeparator).append(NEW_LINE);
393 |  |
             }
394 | 21 |
             return sb.toString();
395 |  |
         }
396 |  |
397 |  |
         @Override
398 |  |
         public String toString() {
399 | 36 |
             return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
400 |  |
         }
401 |  |
402 |  |
         @SuppressWarnings("serial")
403 |  |
         public static class RowNotFound extends RuntimeException {
404 |  |
405 |  |
             public RowNotFound(int row) {
406 | 1 |
                 super(Integer.toString(row));
407 | 1 |
             }
408 |  |
409 |  |
         }
410 |  |
     }