Coverage Report - org.jbehave.core.model.ExamplesTable
 
Classes in this File Line Coverage Branch Coverage Complexity
ExamplesTable
99%
140/141
100%
46/46
1.844
ExamplesTable$RowNotFound
100%
2/2
N/A
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 = "\n";
 140  
     private static final String HEADER_SEPARATOR = "|";
 141  
     private static final String VALUE_SEPARATOR = "|";
 142  
     private static final String IGNORABLE_SEPARATOR = "|--";
 143  
 
 144  
     private final String tableAsString;
 145  
     private final String headerSeparator;
 146  
     private final String valueSeparator;
 147  
     private final String ignorableSeparator;
 148  
     private final ParameterConverters parameterConverters;
 149  
     private final TableTransformers tableTransformers;
 150  180
     private final List<String> headers = new ArrayList<String>();
 151  180
     private final List<Map<String, String>> data = new ArrayList<Map<String, String>>();
 152  180
     private final Properties properties = new Properties();
 153  180
     private String propertiesAsString = "";
 154  180
     private Map<String, String> namedParameters = new HashMap<String, String>();
 155  180
     private boolean trim = true;
 156  
 
 157  
     private final Row defaults;
 158  
 
 159  
     public ExamplesTable(String tableAsString) {
 160  37
         this(tableAsString, HEADER_SEPARATOR, VALUE_SEPARATOR);
 161  37
     }
 162  
 
 163  
     public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator) {
 164  39
         this(tableAsString, headerSeparator, valueSeparator, IGNORABLE_SEPARATOR, new ParameterConverters());
 165  39
     }
 166  
 
 167  
     public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator,
 168  
             String ignorableSeparator, ParameterConverters parameterConverters) {
 169  39
         this(tableAsString, headerSeparator, valueSeparator, ignorableSeparator, parameterConverters, new TableTransformers());
 170  39
     }
 171  
     
 172  
     public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator,
 173  179
             String ignorableSeparator, ParameterConverters parameterConverters, TableTransformers tableTransformers) {
 174  179
         this.tableAsString = tableAsString;
 175  179
         this.headerSeparator = headerSeparator;
 176  179
         this.valueSeparator = valueSeparator;
 177  179
         this.ignorableSeparator = ignorableSeparator;
 178  179
         this.parameterConverters = parameterConverters;
 179  179
         this.tableTransformers = tableTransformers;
 180  179
         this.defaults = new ConvertedParameters(EMPTY_MAP, parameterConverters);
 181  179
         parse();
 182  179
     }
 183  
 
 184  
     private void parse() {
 185  179
         String tableWithoutProperties = stripProperties(tableAsString.trim());
 186  179
         parseProperties(propertiesAsString);
 187  179
         trim = parseBoolean(properties.getProperty("trim", "true"));
 188  179
         parseTable(tableWithoutProperties);
 189  179
     }
 190  
 
 191  1
     private ExamplesTable(ExamplesTable other, Row defaults) {
 192  1
         this.data.addAll(other.data);
 193  1
         this.tableAsString = other.tableAsString;
 194  1
         this.headerSeparator = other.headerSeparator;
 195  1
         this.valueSeparator = other.valueSeparator;
 196  1
         this.ignorableSeparator = other.ignorableSeparator;
 197  1
         this.parameterConverters = other.parameterConverters;
 198  1
         this.tableTransformers = other.tableTransformers;
 199  1
         this.headers.addAll(other.headers);
 200  1
         this.properties.putAll(other.properties);
 201  1
         this.defaults = defaults;
 202  1
     }
 203  
 
 204  
     private String stripProperties(String tableAsString) {
 205  179
         Pattern pattern = compile("\\{(.*?)\\}\\s*(.*)", DOTALL);
 206  179
         Matcher matcher = pattern.matcher(tableAsString);
 207  179
         if (matcher.matches()) {
 208  4
             propertiesAsString = matcher.group(1);
 209  4
             return matcher.group(2);
 210  
         }
 211  175
         return tableAsString;
 212  
     }
 213  
 
 214  
     private void parseProperties(String propertiesAsString) {
 215  179
         properties.clear();
 216  179
         properties.setProperty("ignorableSeparator", ignorableSeparator);
 217  179
         properties.setProperty("headerSeparator", headerSeparator);
 218  179
         properties.setProperty("valueSeparator", valueSeparator);
 219  
         try {
 220  179
             properties.load(new ByteArrayInputStream(propertiesAsString.replace(",", ROW_SEPARATOR).getBytes()));
 221  0
         } catch (IOException e) {
 222  
             // carry on
 223  179
         }
 224  179
     }
 225  
 
 226  
     private void parseTable(String tableAsString) {
 227  179
         headers.clear();
 228  179
         data.clear();
 229  179
         String transformer = properties.getProperty("transformer");
 230  179
         if (transformer != null) {
 231  2
             tableAsString = tableTransformers.transform(transformer, tableAsString, properties);
 232  
         }
 233  179
         parseByRows(headers, data, tableAsString);
 234  179
     }
 235  
 
 236  
     private void parseByRows(List<String> headers, List<Map<String, String>> data, String tableAsString) {
 237  179
         String[] rows = tableAsString.split(ROW_SEPARATOR);
 238  2461
         for (int row = 0; row < rows.length; row++) {
 239  2282
             String rowAsString = rows[row].trim();
 240  2282
             if (rowAsString.startsWith(properties.getProperty("ignorableSeparator")) || rowAsString.length() == 0) {
 241  
                 // skip ignorable or empty lines
 242  126
                 continue;
 243  2150
             } else if (headers.isEmpty()) {
 244  54
                 headers.addAll(TableUtils.parseRow(rowAsString, properties.getProperty("headerSeparator"), trim));
 245  
             } else {
 246  2096
                 List<String> columns = TableUtils.parseRow(rowAsString, properties.getProperty("valueSeparator"), trim);
 247  2096
                 Map<String, String> map = new LinkedHashMap<String, String>();
 248  22295
                 for (int column = 0; column < columns.size(); column++) {
 249  20199
                     if (column < headers.size()) {
 250  20198
                         map.put(headers.get(column), columns.get(column));
 251  
                     }
 252  
                 }
 253  2096
                 data.add(map);
 254  
             }
 255  
         }
 256  179
     }
 257  
 
 258  
     public ExamplesTable withDefaults(Parameters defaults) {
 259  1
         return new ExamplesTable(this, new ChainedRow(defaults, this.defaults));
 260  
     }
 261  
 
 262  
     public ExamplesTable withNamedParameters(Map<String, String> namedParameters) {
 263  1
         this.namedParameters = namedParameters;
 264  1
         return this;
 265  
     }
 266  
 
 267  
     public ExamplesTable withRowValues(int row, Map<String, String> values) {
 268  2
         getRow(row).putAll(values);
 269  2
         for (String header : values.keySet()) {
 270  3
             if (!headers.contains(header)) {
 271  1
                 headers.add(header);
 272  
             }
 273  
         }
 274  2
         return this;
 275  
     }
 276  
 
 277  
     public ExamplesTable withRows(List<Map<String, String>> values) {
 278  1
         this.data.clear();
 279  1
         this.data.addAll(values);
 280  1
         this.headers.clear();
 281  1
         this.headers.addAll(values.get(0).keySet());
 282  1
         return this;
 283  
     }
 284  
 
 285  
     public Properties getProperties() {
 286  4
         return properties;
 287  
     }
 288  
 
 289  
     public List<String> getHeaders() {
 290  27
         return headers;
 291  
     }
 292  
 
 293  
     public Map<String, String> getRow(int row) {
 294  222
         if (row > data.size() - 1) {
 295  1
             throw new RowNotFound(row);
 296  
         }
 297  221
         Map<String, String> values = data.get(row);
 298  221
         if (headers.size() != values.keySet().size()) {
 299  2
             for (String header : headers) {
 300  5
                 if (!values.containsKey(header)) {
 301  2
                     values.put(header, EMPTY_VALUE);
 302  
                 }
 303  
             }
 304  
         }
 305  221
         return values;
 306  
     }
 307  
 
 308  
     public Parameters getRowAsParameters(int row) {
 309  9
         return getRowAsParameters(row, false);
 310  
     }
 311  
 
 312  
     public Parameters getRowAsParameters(int row, boolean replaceNamedParameters) {
 313  13
         Map<String, String> rowValues = getRow(row);
 314  12
         return createParameters((replaceNamedParameters ? replaceNamedParameters(rowValues) : rowValues));
 315  
     }
 316  
 
 317  
     private Map<String, String> replaceNamedParameters(Map<String, String> row) {
 318  1
         Map<String, String> replaced = new HashMap<String, String>();
 319  1
         for (String key : row.keySet()) {
 320  2
             String replacedValue = row.get(key);
 321  2
             for (String namedKey : namedParameters.keySet()) {
 322  2
                 String namedValue = namedParameters.get(namedKey);
 323  2
                 replacedValue = replacedValue.replaceAll(namedKey, namedValue);
 324  2
             }
 325  2
             replaced.put(key, replacedValue);
 326  2
         }
 327  1
         return replaced;
 328  
     }
 329  
 
 330  
     public int getRowCount() {
 331  195
         return data.size();
 332  
     }
 333  
 
 334  
     public List<Map<String, String>> getRows() {
 335  50
         List<Map<String, String>> rows = new ArrayList<Map<String, String>>();
 336  143
         for (int row = 0; row < getRowCount(); row++) {
 337  93
             rows.add(getRow(row));
 338  
         }
 339  50
         return rows;
 340  
     }
 341  
 
 342  
     public List<Parameters> getRowsAsParameters() {
 343  2
         return getRowsAsParameters(false);
 344  
     }
 345  
 
 346  
     public List<Parameters> getRowsAsParameters(boolean replaceNamedParameters) {
 347  3
         List<Parameters> rows = new ArrayList<Parameters>();
 348  
 
 349  7
         for (int row = 0; row < getRowCount(); row++) {
 350  4
             rows.add(getRowAsParameters(row, replaceNamedParameters));
 351  
         }
 352  
 
 353  3
         return rows;
 354  
     }
 355  
 
 356  
     private Parameters createParameters(Map<String, String> values) {
 357  12
         return new ConvertedParameters(new ChainedRow(new ConvertedParameters(values, parameterConverters), defaults),
 358  
                 parameterConverters);
 359  
     }
 360  
 
 361  
     public String getHeaderSeparator() {
 362  2
         return headerSeparator;
 363  
     }
 364  
 
 365  
     public String getValueSeparator() {
 366  2
         return valueSeparator;
 367  
     }
 368  
 
 369  
     public String asString() {
 370  56
         if (data.isEmpty()) {
 371  35
             return EMPTY_VALUE;
 372  
         }
 373  21
         return format();
 374  
     }
 375  
 
 376  
     public void outputTo(PrintStream output) {
 377  1
         output.print(asString());
 378  1
     }
 379  
 
 380  
     private String format() {
 381  21
         StringBuffer sb = new StringBuffer();
 382  21
         for (String header : headers) {
 383  48
             sb.append(headerSeparator).append(header);
 384  
         }
 385  21
         sb.append(headerSeparator).append(ROW_SEPARATOR);
 386  21
         for (Map<String, String> row : getRows()) {
 387  38
             for (String header : headers) {
 388  88
                 sb.append(valueSeparator);
 389  88
                 sb.append(row.get(header));
 390  
             }
 391  38
             sb.append(valueSeparator).append(ROW_SEPARATOR);
 392  
         }
 393  21
         return sb.toString();
 394  
     }
 395  
 
 396  
     @Override
 397  
     public String toString() {
 398  36
         return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
 399  
     }
 400  
 
 401  
     @SuppressWarnings("serial")
 402  
     public static class RowNotFound extends RuntimeException {
 403  
 
 404  
         public RowNotFound(int row) {
 405  1
             super(Integer.toString(row));
 406  1
         }
 407  
 
 408  
     }
 409  
 }