Coverage Report - org.jbehave.core.model.ExamplesTable
 
Classes in this File Line Coverage Branch Coverage Complexity
ExamplesTable
99%
141/142
100%
52/52
1.882
ExamplesTable$RowNotFound
100%
2/2
N/A
1.882
 
 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.StringUtils;
 17  
 import org.apache.commons.lang.builder.ToStringBuilder;
 18  
 import org.apache.commons.lang.builder.ToStringStyle;
 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  
  * By default all column values are trimmed. To avoid trimming the values:
 69  
  * 
 70  
  * <pre>
 71  
  * {trim=false}
 72  
  * | header 1 | header 2 | .... | header n |
 73  
  * | value 11 | value 12 | .... | value 1n |
 74  
  * </pre>
 75  
  * 
 76  
  * </p>
 77  
  * 
 78  
  * <p>
 79  
  * The table also allows the retrieval of row values as converted parameters.
 80  
  * Use {@link #getRowAsParameters(int)} and invoke
 81  
  * {@link Parameters#valueAs(String, Class)} specifying the header and the class
 82  
  * type of the parameter.
 83  
  * </p>
 84  
  * 
 85  
  * <p>
 86  
  * Once created, the table row can be modified, via the
 87  
  * {@link #withRowValues(int, Map)} method, by specifying the map of row values
 88  
  * to be changed.
 89  
  * </p>
 90  
  * 
 91  
  * <p>
 92  
  * A table can also be created by providing the entire data content, via the
 93  
  * {@link #withRows(List<Map<String,String>>)} method.
 94  
  */
 95  
 public class ExamplesTable {
 96  1
     private static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
 97  
     private static final String EMPTY_VALUE = "";
 98  
 
 99  1
     public static final ExamplesTable EMPTY = new ExamplesTable("");
 100  
 
 101  
     private static final String ROW_SEPARATOR = "\n";
 102  
     private static final String HEADER_SEPARATOR = "|";
 103  
     private static final String VALUE_SEPARATOR = "|";
 104  
     private static final String IGNORABLE_SEPARATOR = "|--";
 105  
 
 106  172
     private final List<Map<String, String>> data = new ArrayList<Map<String, String>>();
 107  
     private final String tableAsString;
 108  
     private final String headerSeparator;
 109  
     private final String valueSeparator;
 110  
     private final String ignorableSeparator;
 111  
     private final ParameterConverters parameterConverters;
 112  172
     private final List<String> headers = new ArrayList<String>();
 113  172
     private final Properties properties = new Properties();
 114  172
     private Map<String, String> namedParameters = new HashMap<String, String>();
 115  172
     private boolean trim = true;
 116  
 
 117  
     private final Row defaults;
 118  
 
 119  
     public ExamplesTable(String tableAsString) {
 120  33
         this(tableAsString, HEADER_SEPARATOR, VALUE_SEPARATOR);
 121  33
     }
 122  
 
 123  
     public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator) {
 124  35
         this(tableAsString, headerSeparator, valueSeparator, IGNORABLE_SEPARATOR, new ParameterConverters());
 125  35
     }
 126  
 
 127  
     public ExamplesTable(String tableAsString, String headerSeparator, String valueSeparator,
 128  171
             String ignorableSeparator, ParameterConverters parameterConverters) {
 129  171
         this.tableAsString = tableAsString;
 130  171
         this.headerSeparator = headerSeparator;
 131  171
         this.valueSeparator = valueSeparator;
 132  171
         this.ignorableSeparator = ignorableSeparator;
 133  171
         this.parameterConverters = parameterConverters;
 134  171
         this.defaults = new ConvertedParameters(EMPTY_MAP, parameterConverters);
 135  171
         parse(tableAsString);
 136  171
     }
 137  
 
 138  1
     private ExamplesTable(ExamplesTable other, Row defaults) {
 139  1
         this.data.addAll(other.data);
 140  1
         this.tableAsString = other.tableAsString;
 141  1
         this.headerSeparator = other.headerSeparator;
 142  1
         this.valueSeparator = other.valueSeparator;
 143  1
         this.ignorableSeparator = other.ignorableSeparator;
 144  1
         this.parameterConverters = other.parameterConverters;
 145  1
         this.headers.addAll(other.headers);
 146  1
         this.properties.putAll(other.properties);
 147  1
         this.defaults = defaults;
 148  1
     }
 149  
 
 150  
     private void parse(String tableAsString) {
 151  171
         data.clear();
 152  171
         headers.clear();
 153  171
         String[] rows = splitInRows(stripProperties(tableAsString.trim()));
 154  2430
         for (int row = 0; row < rows.length; row++) {
 155  2259
             String rowAsString = rows[row];
 156  2259
             if (rowAsString.startsWith(ignorableSeparator)) {
 157  
                 // skip rows that start with ignorable separator
 158  2
                 continue;
 159  2257
             } else if (row == 0) {
 160  171
                 List<String> columns = columnsFor(rowAsString, headerSeparator);
 161  171
                 headers.addAll(columns);
 162  171
             } else {
 163  2086
                 List<String> columns = columnsFor(rowAsString, valueSeparator);
 164  2086
                 Map<String, String> map = createRowMap();
 165  22263
                 for (int column = 0; column < columns.size(); column++) {
 166  20177
                     map.put(headers.get(column), columns.get(column));
 167  
                 }
 168  2086
                 data.add(map);
 169  
             }
 170  
         }
 171  171
     }
 172  
 
 173  
     private String stripProperties(String tableAsString) {
 174  171
         Pattern pattern = compile("\\{(.*?)\\}\\s*(.*)", DOTALL);
 175  171
         Matcher matcher = pattern.matcher(tableAsString);
 176  171
         if (matcher.matches()) {
 177  1
             parseProperties(matcher.group(1));
 178  1
             return matcher.group(2);
 179  
         }
 180  170
         return tableAsString;
 181  
     }
 182  
 
 183  
     private void parseProperties(String propertiesAsString) {
 184  1
         properties.clear();
 185  
         try {
 186  1
             properties.load(new ByteArrayInputStream(propertiesAsString.replace(",", ROW_SEPARATOR).getBytes()));
 187  0
         } catch (IOException e) {
 188  
             // carry on
 189  1
         }
 190  1
         trim = parseBoolean(properties.getProperty("trim", "true"));
 191  1
     }
 192  
 
 193  
     private String[] splitInRows(String table) {
 194  171
         return table.split(ROW_SEPARATOR);
 195  
     }
 196  
 
 197  
     private List<String> columnsFor(String row, String separator) {
 198  2257
         List<String> columns = new ArrayList<String>();
 199  
         // use split limit -1 to ensure that empty strings will not be discarted
 200  26919
         for (String column : row.split(buildRegex(separator), -1)) {
 201  24662
             columns.add(valueOf(column));
 202  
         }
 203  
         // there may be a leading and a trailing empty column which we ignore
 204  2257
         if (StringUtils.isBlank(columns.get(0))) {
 205  2251
             columns.remove(0);
 206  
         }
 207  2257
         int lastIndex = columns.size() - 1;
 208  2257
         if (lastIndex != -1 && StringUtils.isBlank(columns.get(lastIndex))) {
 209  2128
             columns.remove(lastIndex);
 210  
         }
 211  2257
         return columns;
 212  
     }
 213  
 
 214  
     private String valueOf(String column) {
 215  24662
         return trim ? column.trim() : column;
 216  
     }
 217  
 
 218  
     private String buildRegex(String separator) {
 219  2257
         char[] chars = separator.toCharArray();
 220  2257
         StringBuffer sb = new StringBuffer();
 221  4516
         for (char c : chars) {
 222  2259
             sb.append("\\").append(c);
 223  
         }
 224  2257
         return sb.toString();
 225  
     }
 226  
 
 227  
     protected Map<String, String> createRowMap() {
 228  2086
         return new LinkedHashMap<String, String>();
 229  
     }
 230  
 
 231  
     public ExamplesTable withDefaults(Parameters defaults) {
 232  1
         return new ExamplesTable(this, new ChainedRow(defaults, this.defaults));
 233  
     }
 234  
 
 235  
     public ExamplesTable withNamedParameters(Map<String, String> namedParameters) {
 236  1
         this.namedParameters = namedParameters;
 237  1
         return this;
 238  
     }
 239  
 
 240  
     public ExamplesTable withRowValues(int row, Map<String, String> values) {
 241  2
         getRow(row).putAll(values);
 242  2
         for (String header : values.keySet()) {
 243  3
             if (!headers.contains(header)) {
 244  1
                 headers.add(header);
 245  
             }
 246  
         }
 247  2
         return this;
 248  
     }
 249  
 
 250  
     public ExamplesTable withRows(List<Map<String, String>> values) {
 251  1
         this.data.clear();
 252  1
         this.data.addAll(values);
 253  1
         this.headers.clear();
 254  1
         this.headers.addAll(values.get(0).keySet());
 255  1
         return this;
 256  
     }
 257  
 
 258  
     public Properties getProperties() {
 259  1
         return properties;
 260  
     }
 261  
 
 262  
     public List<String> getHeaders() {
 263  24
         return headers;
 264  
     }
 265  
 
 266  
     public Map<String, String> getRow(int row) {
 267  198
         if (row > data.size() - 1) {
 268  1
             throw new RowNotFound(row);
 269  
         }
 270  197
         Map<String, String> values = data.get(row);
 271  197
         if (headers.size() != values.keySet().size()) {
 272  1
             for (String header : headers) {
 273  3
                 if (!values.containsKey(header)) {
 274  1
                     values.put(header, EMPTY_VALUE);
 275  
                 }
 276  
             }
 277  
         }
 278  197
         return values;
 279  
     }
 280  
 
 281  
     public Parameters getRowAsParameters(int row) {
 282  9
         return getRowAsParameters(row, false);
 283  
     }
 284  
 
 285  
     public Parameters getRowAsParameters(int row, boolean replaceNamedParameters) {
 286  13
         Map<String, String> rowValues = getRow(row);
 287  12
         return createParameters((replaceNamedParameters ? replaceNamedParameters(rowValues) : rowValues));
 288  
     }
 289  
 
 290  
     private Map<String, String> replaceNamedParameters(Map<String, String> row) {
 291  1
         Map<String, String> replaced = new HashMap<String, String>();
 292  1
         for (String key : row.keySet()) {
 293  2
             String replacedValue = row.get(key);
 294  2
             for (String namedKey : namedParameters.keySet()) {
 295  2
                 String namedValue = namedParameters.get(namedKey);
 296  2
                 replacedValue = replacedValue.replaceAll(namedKey, namedValue);
 297  2
             }
 298  2
             replaced.put(key, replacedValue);
 299  2
         }
 300  1
         return replaced;
 301  
     }
 302  
 
 303  
     public int getRowCount() {
 304  176
         return data.size();
 305  
     }
 306  
 
 307  
     public List<Map<String, String>> getRows() {
 308  44
         List<Map<String, String>> rows = new ArrayList<Map<String, String>>();
 309  127
         for (int row = 0; row < getRowCount(); row++) {
 310  83
             rows.add(getRow(row));
 311  
         }
 312  44
         return rows;
 313  
     }
 314  
 
 315  
     public List<Parameters> getRowsAsParameters() {
 316  2
         return getRowsAsParameters(false);
 317  
     }
 318  
 
 319  
     public List<Parameters> getRowsAsParameters(boolean replaceNamedParameters) {
 320  3
         List<Parameters> rows = new ArrayList<Parameters>();
 321  
 
 322  7
         for (int row = 0; row < getRowCount(); row++) {
 323  4
             rows.add(getRowAsParameters(row, replaceNamedParameters));
 324  
         }
 325  
 
 326  3
         return rows;
 327  
     }
 328  
 
 329  
     private Parameters createParameters(Map<String, String> values) {
 330  12
         return new ConvertedParameters(new ChainedRow(new ConvertedParameters(values, parameterConverters), defaults),
 331  
                 parameterConverters);
 332  
     }
 333  
 
 334  
     public String getHeaderSeparator() {
 335  2
         return headerSeparator;
 336  
     }
 337  
 
 338  
     public String getValueSeparator() {
 339  2
         return valueSeparator;
 340  
     }
 341  
 
 342  
     public String asString() {
 343  50
         if (data.isEmpty()) {
 344  32
             return EMPTY_VALUE;
 345  
         }
 346  18
         return format();
 347  
     }
 348  
 
 349  
     public void outputTo(PrintStream output){
 350  1
         output.print(asString());
 351  1
     }
 352  
     
 353  
     private String format() {
 354  18
         StringBuffer sb = new StringBuffer();
 355  18
         for (String header : headers) {
 356  42
             sb.append(headerSeparator).append(header);
 357  
         }
 358  18
         sb.append(headerSeparator).append(ROW_SEPARATOR);
 359  18
         for (Map<String, String> row : getRows()) {
 360  34
             for (String header : headers) {
 361  80
                 sb.append(valueSeparator);
 362  80
                 sb.append(row.get(header));
 363  
             }
 364  34
             sb.append(valueSeparator).append(ROW_SEPARATOR);
 365  
         }
 366  18
         return sb.toString();
 367  
     }
 368  
 
 369  
     @Override
 370  
     public String toString() {
 371  31
         return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
 372  
     }
 373  
 
 374  
     @SuppressWarnings("serial")
 375  
     public static class RowNotFound extends RuntimeException {
 376  
 
 377  
         public RowNotFound(int row) {
 378  1
             super(Integer.toString(row));
 379  1
         }
 380  
 
 381  
     }
 382  
 }