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