ExamplesTable.java

  1. package org.jbehave.core.model;

  2. import static java.lang.Boolean.parseBoolean;
  3. import static java.util.regex.Pattern.DOTALL;
  4. import static java.util.stream.Collectors.toList;
  5. import static org.apache.commons.lang3.Validate.isTrue;
  6. import static org.apache.commons.lang3.Validate.notBlank;

  7. import java.io.PrintStream;
  8. import java.lang.reflect.Type;
  9. import java.util.ArrayList;
  10. import java.util.Collections;
  11. import java.util.Deque;
  12. import java.util.LinkedHashMap;
  13. import java.util.LinkedList;
  14. import java.util.List;
  15. import java.util.Locale;
  16. import java.util.Map;
  17. import java.util.Optional;
  18. import java.util.Properties;
  19. import java.util.regex.Matcher;
  20. import java.util.regex.Pattern;
  21. import java.util.stream.Collectors;
  22. import java.util.stream.IntStream;
  23. import java.util.stream.Stream;

  24. import org.apache.commons.lang3.StringUtils;
  25. import org.apache.commons.lang3.builder.ToStringBuilder;
  26. import org.apache.commons.lang3.builder.ToStringStyle;
  27. import org.jbehave.core.configuration.Keywords;
  28. import org.jbehave.core.i18n.LocalizedKeywords;
  29. import org.jbehave.core.io.LoadFromClasspath;
  30. import org.jbehave.core.model.TableTransformers.TableTransformer;
  31. import org.jbehave.core.steps.ChainedRow;
  32. import org.jbehave.core.steps.ConvertedParameters;
  33. import org.jbehave.core.steps.ParameterControls;
  34. import org.jbehave.core.steps.ParameterConverters;
  35. import org.jbehave.core.steps.Parameters;
  36. import org.jbehave.core.steps.Row;

  37. /**
  38.  * Represents a tabular structure that holds rows of example data for parameters named via the column headers:
  39.  * <pre>
  40.  * |header 1|header 2| .... |header n|
  41.  * |value 11|value 12| .... |value 1n|
  42.  * ...
  43.  * |value m1|value m2| .... |value mn|
  44.  * </pre>
  45.  *
  46.  * <p>Different header and value column separators can be specified to replace the default separator "|":
  47.  * <pre>
  48.  * !!header 1!!header 2!! .... !!header n!!
  49.  * !value 11!value 12! .... !value 1n!
  50.  * ...
  51.  * !value m1!value m2| .... !value mn!
  52.  * </pre>
  53.  * </p>
  54.  *
  55.  * <p>Rows starting with an ignorable separator are allowed and ignored:
  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.  *
  66.  * <p>Ignorable separator is configurable and defaults to "|--".</p>
  67.  * <p>The separators are also configurable via inlined properties:
  68.  * <pre>
  69.  * {ignorableSeparator=!--,headerSeparator=!,valueSeparator=!}
  70.  * !header 1!header 2! .... !header n!
  71.  * !-- A commented row --!
  72.  * !value 11!value 12! .... !value 1n!
  73.  * ...
  74.  * !-- Another commented row --!
  75.  * !value m1!value m2! .... !value mn!
  76.  * </pre>
  77.  * </p>
  78.  *
  79.  * <p>By default all column values are trimmed. To avoid trimming the values, use the "trim" inlined property:
  80.  * <pre>
  81.  * {trim=false}
  82.  * | header 1 | header 2 | .... | header n |
  83.  * | value 11 | value 12 | .... | value 1n |
  84.  * </pre>
  85.  * </p>
  86.  *
  87.  * <p>Comments is column values are supported via the "commentSeparator" inlined property:
  88.  * <pre>
  89.  * {commentSeparator=#}
  90.  * | header 1#comment | header 2 | .... | header n |
  91.  * | value 11#comment | value 12 | .... | value 1n |
  92.  * </pre>
  93.  * Comments including the separator are stripped.</p>
  94.  *
  95.  * <p>Line break is a default separator for rows in ExamplesTable, that's why they can't be added as is
  96.  * to the data. In order to put the value with line breaks to ExamplesTable escape sequences (a character preceded by a
  97.  * backslash (\) is an escape sequence) must be used.
  98.  * <table>
  99.  *  <thead><tr><th>Escape Sequence</th><th>Description</th></tr></thead>
  100.  *  <tbody>
  101.  *   <tr><td>\n</td><td>Insert a newline in the value at this point.</td></tr>
  102.  *   <tr><td>\r</td><td>Insert a carriage return in the text at this point.</td></tr>
  103.  *   <tr><td>\\</td><td>Insert a backslash character in the text at this point.</td></tr>
  104.  *  </tbody>
  105.  * </table>
  106.  * Inlined property <code>processEscapeSequences</code> defines whether escape sequences should be replaced in the data.
  107.  * It’s <code>false</code> by default (no property is declared explicitly). The allowed values are <code>true</code> and
  108.  * <code>false</code>, any other values are considered invalid and will lead to exception thrown at parsing.
  109.  * <pre>
  110.  * {processEscapeSequences=true, commentSeparator=#}
  111.  * |header          |
  112.  * |line 1\nline 2  |# The value with a newline
  113.  * |line 1\r\nline 2|# The value with a carriage return and a newline
  114.  * |line 1\\nline 2 |# The value with an escaped escape sequence, the result will be "line 1\nline 2"
  115.  * </pre>
  116.  * </p>
  117.  *
  118.  * <p>The table allows the retrieval of row values as converted parameters. Use {@link #getRowAsParameters(int)} and
  119.  * invoke {@link Parameters#valueAs(String, Type)} specifying the header and the class type of the parameter.</p>
  120.  *
  121.  * <p>The table allows the transformation of its string representation via the "transformer" inlined property
  122.  * (multiple transformers will be applied in chain-mode):
  123.  * <pre>
  124.  * {transformer=myTransformerName}
  125.  * {transformer=myOtherTransformerName}
  126.  * |header 1|header 2| .... |header n|
  127.  * |value 11|value 12| .... |value 1n|
  128.  * ...
  129.  * |value m1|value m2| .... |value mn|
  130.  * </pre>
  131.  * The transformers need to be registered by name via the
  132.  * {@link TableTransformers#useTransformer(String, TableTransformer)}. A few transformers are already registered by
  133.  * default in {@link TableTransformers}.</p>
  134.  *
  135.  * <p>The table allow filtering on meta by row via the "metaByRow" inlined property:
  136.  * <pre>
  137.  * {metaByRow=true}
  138.  * | Meta:       | header 1 | .... | header n |
  139.  * | @name=value | value 11 | .... | value 1n |
  140.  * </pre>
  141.  * </p>
  142.  *
  143.  * <p>Once created, the table row can be modified, via the {@link #withRowValues(int, Map)} method, by specifying the
  144.  * map of row values to be changed.</p>
  145.  *
  146.  * <p>A table can also be created by providing the entire data content, via the {@link #withRows(List)} method.</p>
  147.  *
  148.  * <p>The parsing code assumes that the number of columns for data rows is the same as in the header, if a row has
  149.  * less fields, the remaining are filled with empty values, if it has more, the fields are ignored.</p>
  150.  */
  151. public class ExamplesTable {
  152.     private static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
  153.     private static final String EMPTY_VALUE = "";

  154.     public static final Pattern INLINED_PROPERTIES_PATTERN = Pattern.compile("\\{(.*?[^\\\\])\\}\\s*(.*)", DOTALL);
  155.     public static final ExamplesTable EMPTY = new ImmutableExamplesTable(EMPTY_VALUE);

  156.     private final ParameterConverters parameterConverters;
  157.     private final Row defaults;
  158.     private final TableRows tableRows;
  159.     private final Deque<TableProperties> tablePropertiesQueue = new LinkedList<>();

  160.     private Map<String, String> namedParameters = Collections.emptyMap();
  161.     private ParameterControls parameterControls;
  162.     private TableTransformerMonitor tableTransformerMonitor;

  163.     public ExamplesTable(String tableAsString) {
  164.         this(tableAsString, new TableTransformers());
  165.     }

  166.     private ExamplesTable(String tableAsString, TableTransformers tableTransformers) {
  167.         this(tableAsString, new ParameterConverters(new LoadFromClasspath(), tableTransformers), tableTransformers);
  168.     }

  169.     private ExamplesTable(String tableAsString, ParameterConverters parameterConverters,
  170.             TableTransformers tableTransformers) {
  171.         this(tableAsString, parameterConverters, new TableParsers(new LocalizedKeywords(), parameterConverters),
  172.                 tableTransformers);
  173.     }

  174.     private ExamplesTable(String tableAsString, ParameterConverters parameterConverters, TableParsers tableParsers,
  175.             TableTransformers tableTransformers) {
  176.         this(tableParsers.parseProperties(tableAsString), parameterConverters, new ParameterControls(), tableParsers,
  177.                 tableTransformers, new NullTableTransformerMonitor());
  178.     }

  179.     ExamplesTable(TablePropertiesQueue tablePropertiesQueue, ParameterConverters parameterConverters,
  180.                   ParameterControls parameterControls, TableParsers tableParsers, TableTransformers tableTransformers,
  181.                   TableTransformerMonitor tableTransformerMonitor) {
  182.         this.parameterConverters = parameterConverters;
  183.         this.parameterControls = parameterControls;
  184.         this.defaults = new ConvertedParameters(EMPTY_MAP, parameterConverters);
  185.         this.tablePropertiesQueue.addAll(tablePropertiesQueue.getProperties());
  186.         this.tableTransformerMonitor = tableTransformerMonitor;
  187.         String transformedTable = applyTransformers(tableTransformers, tablePropertiesQueue.getTable(), tableParsers);
  188.         this.tableRows = tableParsers.parseRows(transformedTable, lastTableProperties());
  189.     }

  190.     private TableProperties lastTableProperties() {
  191.         return tablePropertiesQueue.getLast();
  192.     }

  193.     private ExamplesTable(ExamplesTable other, Row defaults) {
  194.         this.tableRows = new TableRows(other.tableRows.getHeaders(),
  195.                 other.tableRows.getRows());
  196.         this.parameterConverters = other.parameterConverters;
  197.         this.tablePropertiesQueue.addAll(other.tablePropertiesQueue);
  198.         this.defaults = defaults;
  199.     }

  200.     private String applyTransformers(TableTransformers tableTransformers, String tableAsString,
  201.             TableParsers tableParsers) {
  202.         String transformedTable = tableAsString;
  203.         TableProperties previousProperties = null;
  204.         for (TableProperties properties : tablePropertiesQueue) {
  205.             String transformer = properties.getTransformer();
  206.             if (transformer != null) {
  207.                 if (previousProperties != null) {
  208.                     properties.overrideSeparatorsFrom(previousProperties);
  209.                 }
  210.                 tableTransformerMonitor.beforeTransformerApplying(transformer, properties, tableAsString);
  211.                 transformedTable = tableTransformers.transform(transformer, transformedTable, tableParsers, properties);
  212.                 tableTransformerMonitor.afterTransformerApplying(transformer, properties, transformedTable);
  213.             }
  214.             previousProperties = properties;
  215.         }
  216.         return transformedTable;
  217.     }

  218.     public ExamplesTable withDefaults(Parameters defaults) {
  219.         return new ExamplesTable(this, new ChainedRow(defaults, this.defaults));
  220.     }

  221.     public ExamplesTable withNamedParameters(Map<String, String> namedParameters) {
  222.         this.namedParameters = namedParameters;
  223.         return this;
  224.     }

  225.     public ExamplesTable withRowValues(int rowIndex, Map<String, String> values) {
  226.         for (String header : values.keySet()) {
  227.             if (!getHeaders().contains(header)) {
  228.                 getHeaders().add(header);
  229.             }
  230.         }
  231.         List<String> row = getRowValues(rowIndex);
  232.         List<String> headers = getHeaders();
  233.         for (int i = 0, headersSize = headers.size(); i < headersSize; i++) {
  234.             String value = values.get(headers.get(i));

  235.             if (i >= row.size()) {
  236.                 row.add(Optional.ofNullable(value).orElse(EMPTY_VALUE));
  237.             } else if (value != null) {
  238.                 row.set(i, value);
  239.             }
  240.         }
  241.         return this;
  242.     }

  243.     public ExamplesTable withRows(List<Map<String, String>> rows) {
  244.         tableRows.clear();
  245.         if (!rows.isEmpty()) {
  246.             getHeaders().addAll(rows.get(0).keySet());
  247.             rows.stream().map(Map::values).map(ArrayList::new).forEach(tableRows.rows::add);
  248.         }
  249.         return this;
  250.     }

  251.     public Properties getProperties() {
  252.         return lastTableProperties().getProperties();
  253.     }

  254.     public String getPropertiesAsString() {
  255.         return lastTableProperties().getPropertiesAsString();
  256.     }

  257.     private String replaceNamedParameters(String text) {
  258.         return parameterControls.replaceAllDelimitedNames(text, namedParameters);
  259.     }

  260.     private List<String> replaceNamedParameters(List<String> values, boolean replaceNamedParameters) {
  261.         return replaceNamedParameters ? values.stream().map(this::replaceNamedParameters).collect(toList()) : values;
  262.     }

  263.     public List<String> getHeaders() {
  264.         return tableRows.getHeaders();
  265.     }

  266.     public Map<String, String> getRow(int rowIndex) {
  267.         return getRow(rowIndex, false);
  268.     }

  269.     public Map<String, String> getRow(int rowIndex, boolean replaceNamedParameters) {
  270.         List<String> values = getRowValues(rowIndex, replaceNamedParameters);

  271.         if (!tableRows.areAllColumnsDistinct()) {
  272.             String exceptionMessage = "ExamplesTable contains non-distinct columns, all columns are: "
  273.                     + String.join(", ", getHeaders());
  274.             throw new NonDistinctColumnFound(exceptionMessage);
  275.         }

  276.         Map<String, String> result = new LinkedHashMap<>();
  277.         List<String> headers = getHeaders();
  278.         for (int i = 0, headersSize = headers.size(); i < headersSize; i++) {
  279.             result.put(headers.get(i), i < values.size() ? values.get(i) : EMPTY_VALUE);
  280.         }
  281.         return result;
  282.     }

  283.     public List<String> getRowValues(int rowIndex) {
  284.         return getRowValues(rowIndex, false);
  285.     }

  286.     public List<String> getRowValues(int rowIndex, boolean replaceNamedParameters) {
  287.         return replaceNamedParameters(tableRows.getRow(rowIndex), replaceNamedParameters);
  288.     }

  289.     public Parameters getRowAsParameters(int rowIndex) {
  290.         return getRowAsParameters(rowIndex, false);
  291.     }

  292.     public Parameters getRowAsParameters(int rowIndex, boolean replaceNamedParameters) {
  293.         Map<String, String> row = getRow(rowIndex, replaceNamedParameters);
  294.         return new ConvertedParameters(new ChainedRow(new ConvertedParameters(row, parameterConverters), defaults),
  295.                 parameterConverters);
  296.     }

  297.     public int getRowCount() {
  298.         return tableRows.getRows().size();
  299.     }

  300.     public boolean metaByRow() {
  301.         return lastTableProperties().isMetaByRow();
  302.     }

  303.     public List<Map<String, String>> getRows() {
  304.         List<Map<String, String>> rows = new ArrayList<>();
  305.         for (int row = 0; row < getRowCount(); row++) {
  306.             rows.add(getRow(row));
  307.         }
  308.         return rows;
  309.     }

  310.     public List<Parameters> getRowsAsParameters() {
  311.         return getRowsAsParameters(false);
  312.     }

  313.     public List<Parameters> getRowsAsParameters(boolean replaceNamedParameters) {
  314.         List<Parameters> rows = new ArrayList<>();

  315.         for (int row = 0; row < getRowCount(); row++) {
  316.             rows.add(getRowAsParameters(row, replaceNamedParameters));
  317.         }

  318.         return rows;
  319.     }

  320.     public <T> List<T> getRowsAs(Class<T> type) {
  321.         return getRowsAs(type, Collections.emptyMap());
  322.     }

  323.     public <T> List<T> getRowsAs(Class<T> type, Map<String, String> fieldNameMapping) {
  324.         return getRowsAsParameters().stream()
  325.                                     .map(p -> p.mapTo(type, fieldNameMapping))
  326.                                     .collect(toList());
  327.     }

  328.     public List<String> getColumn(String columnName) {
  329.         return getColumn(columnName, false);
  330.     }

  331.     public List<String> getColumn(String columnName, boolean replaceNamedParameters) {
  332.         return replaceNamedParameters(tableRows.getColumn(columnName), replaceNamedParameters);
  333.     }

  334.     public String getHeaderSeparator() {
  335.         return lastTableProperties().getHeaderSeparator();
  336.     }

  337.     public String getValueSeparator() {
  338.         return lastTableProperties().getValueSeparator();
  339.     }

  340.     public String asString() {
  341.         if (tableRows.getRows().isEmpty()) {
  342.             return EMPTY_VALUE;
  343.         }
  344.         StringBuilder sb = new StringBuilder();
  345.         for (TableProperties properties : tablePropertiesQueue) {
  346.             String propertiesAsString = properties.getPropertiesAsString();
  347.             if (!propertiesAsString.isEmpty()) {
  348.                 sb.append("{").append(propertiesAsString).append("}").append(lastTableProperties().getRowSeparator());
  349.             }
  350.         }
  351.         sb.append(ExamplesTableStringBuilder.buildExamplesTableString(lastTableProperties(), getHeaders(),
  352.                 tableRows.getRows()));

  353.         return sb.toString();
  354.     }

  355.     public boolean isEmpty() {
  356.         return getHeaders().isEmpty();
  357.     }

  358.     public void outputTo(PrintStream output) {
  359.         output.print(asString());
  360.     }

  361.     public static ExamplesTable empty() {
  362.         return new ExamplesTable("");
  363.     }

  364.     @Override
  365.     public String toString() {
  366.         return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  367.     }

  368.     public static final class TableProperties {

  369.         public enum Decorator {
  370.             LOWERCASE, UPPERCASE, VERBATIM, TRIM
  371.         }

  372.         private static final String COMMA = ",";
  373.         private static final String COMMA_REGEX = "\\,";
  374.         private static final String EQUAL = "=";
  375.         private static final String PIPE_REGEX = "\\|";

  376.         private static final String DECORATORS_REGEX = Stream.of(Decorator.values())
  377.                 .map(Decorator::name)
  378.                 .collect(Collectors.joining("|", "(", ")"));

  379.         private static final Pattern DECORATED_PROPERTY_PATTERN = Pattern.compile(
  380.                 "\\s*\\{([^=,\\s]+(\\|" + DECORATORS_REGEX + ")*)}\\s*", Pattern.CASE_INSENSITIVE);

  381.         private static final String HEADER_SEPARATOR_KEY = "headerSeparator";
  382.         private static final String VALUE_SEPARATOR_KEY = "valueSeparator";
  383.         private static final String IGNORABLE_SEPARATOR_KEY = "ignorableSeparator";
  384.         private static final String COMMENT_SEPARATOR_KEY = "commentSeparator";
  385.         private static final String NULL_PLACEHOLDER_KEY = "nullPlaceholder";
  386.         private static final String PROCESS_ESCAPE_SEQUENCES_KEY = "processEscapeSequences";

  387.         private static final String ROW_SEPARATOR = "\n";

  388.         private final Properties properties = new Properties();
  389.         private final ParameterConverters parameterConverters;
  390.         private final String propertiesAsString;

  391.         public TableProperties(String propertiesAsString, Keywords keywords, ParameterConverters parameterConverters) {
  392.             this.propertiesAsString = propertiesAsString;
  393.             this.parameterConverters = parameterConverters;
  394.             properties.setProperty(HEADER_SEPARATOR_KEY, keywords.examplesTableHeaderSeparator());
  395.             properties.setProperty(VALUE_SEPARATOR_KEY, keywords.examplesTableValueSeparator());
  396.             properties.setProperty(IGNORABLE_SEPARATOR_KEY, keywords.examplesTableIgnorableSeparator());
  397.             properties.putAll(parseProperties(propertiesAsString));
  398.         }

  399.         private Map<String, String> parseProperties(String propertiesAsString) {
  400.             Map<String, String> result = new LinkedHashMap<>();
  401.             if (!StringUtils.isEmpty(propertiesAsString)) {
  402.                 for (String propertyAsString : propertiesAsString.split("(?<!\\\\),")) {
  403.                     String[] property = StringUtils.split(propertyAsString, EQUAL, 2);
  404.                     String propertyName = property[0];
  405.                     String propertyValue = property[1];
  406.                     Matcher decoratedPropertyMatcher = DECORATED_PROPERTY_PATTERN.matcher(propertyName);
  407.                     if (decoratedPropertyMatcher.matches()) {
  408.                         String[] propertyWithDecorators = decoratedPropertyMatcher.group(1).split(PIPE_REGEX);
  409.                         propertyName = propertyWithDecorators[0];
  410.                         for (int i = 1; i < propertyWithDecorators.length; i++) {
  411.                             String decorator = propertyWithDecorators[i].toUpperCase();
  412.                             propertyValue = decoratePropertyValue(propertyValue, Decorator.valueOf(decorator));
  413.                         }
  414.                     } else {
  415.                         propertyValue = propertyValue.trim();
  416.                     }
  417.                     result.put(propertyName.trim(), StringUtils.replace(propertyValue, COMMA_REGEX, COMMA));
  418.                 }
  419.             }
  420.             return result;
  421.         }

  422.         private String decoratePropertyValue(String value, Decorator decorator) {
  423.             switch (decorator) {
  424.                 case VERBATIM:
  425.                     return value;
  426.                 case LOWERCASE:
  427.                     return value.toLowerCase();
  428.                 case UPPERCASE:
  429.                     return value.toUpperCase();
  430.                 case TRIM:
  431.                 default:
  432.                     return value.trim();
  433.             }
  434.         }

  435.         @SuppressWarnings("unchecked")
  436.         public <T> T getMandatoryNonBlankProperty(String propertyName, Type type) {
  437.             String propertyValue = properties.getProperty(propertyName);
  438.             isTrue(propertyValue != null, "'%s' is not set in ExamplesTable properties", propertyName);
  439.             notBlank(propertyValue, "ExamplesTable property '%s' is blank", propertyName);
  440.             return (T) parameterConverters.convert(propertyValue, type);
  441.         }

  442.         public String getRowSeparator() {
  443.             return ROW_SEPARATOR;
  444.         }

  445.         public String getHeaderSeparator() {
  446.             return properties.getProperty(HEADER_SEPARATOR_KEY);
  447.         }

  448.         public String getValueSeparator() {
  449.             return properties.getProperty(VALUE_SEPARATOR_KEY);
  450.         }

  451.         public String getIgnorableSeparator() {
  452.             return properties.getProperty(IGNORABLE_SEPARATOR_KEY);
  453.         }

  454.         public String getCommentSeparator() {
  455.             return properties.getProperty(COMMENT_SEPARATOR_KEY);
  456.         }

  457.         public Optional<String> getNullPlaceholder() {
  458.             return Optional.ofNullable(properties.getProperty(NULL_PLACEHOLDER_KEY));
  459.         }

  460.         public boolean isProcessEscapeSequences() {
  461.             String processEscapeSequences = properties.getProperty(PROCESS_ESCAPE_SEQUENCES_KEY);
  462.             if (processEscapeSequences != null) {
  463.                 String processEscapeSequencesTrimmed = processEscapeSequences.trim().toLowerCase(Locale.ENGLISH);
  464.                 if ("true".equals(processEscapeSequencesTrimmed)) {
  465.                     return true;
  466.                 }
  467.                 if ("false".equals(processEscapeSequencesTrimmed)) {
  468.                     return false;
  469.                 }
  470.                 throw new IllegalArgumentException(
  471.                         "ExamplesTable property 'processEscapeSequences' contains invalid value: '"
  472.                                 + processEscapeSequences + "', but allowed values are 'true' and 'false'");
  473.             }
  474.             return false;
  475.         }

  476.         void overrideSeparatorsFrom(TableProperties properties) {
  477.             Stream.of(HEADER_SEPARATOR_KEY, VALUE_SEPARATOR_KEY, IGNORABLE_SEPARATOR_KEY, COMMENT_SEPARATOR_KEY)
  478.                   .forEach(key -> {
  479.                       String value = properties.properties.getProperty(key);
  480.                       if (value != null) {
  481.                           this.properties.setProperty(key, value);
  482.                       }
  483.                   });
  484.         }

  485.         public boolean isTrim() {
  486.             return parseBoolean(properties.getProperty("trim", "true"));
  487.         }

  488.         public boolean isMetaByRow() {
  489.             return parseBoolean(properties.getProperty("metaByRow", "false"));
  490.         }

  491.         public String getTransformer() {
  492.             return properties.getProperty("transformer");
  493.         }

  494.         public Properties getProperties() {
  495.             return properties;
  496.         }

  497.         public String getPropertiesAsString() {
  498.             return propertiesAsString;
  499.         }
  500.     }

  501.     static final class TablePropertiesQueue {
  502.         private final String table;
  503.         private final Deque<TableProperties> properties;

  504.         TablePropertiesQueue(String table, Deque<TableProperties> properties) {
  505.             this.table = table;
  506.             this.properties = properties;
  507.         }

  508.         String getTable() {
  509.             return table;
  510.         }

  511.         Deque<TableProperties> getProperties() {
  512.             return properties;
  513.         }
  514.     }

  515.     public static final class TableRows {
  516.         private final List<String> headers;
  517.         private final List<List<String>> rows;
  518.         private final boolean allColumnsDistinct;

  519.         public TableRows(List<String> headers, List<List<String>> rows) {
  520.             this.headers = headers;
  521.             this.rows = rows;
  522.             this.allColumnsDistinct = headers.stream().distinct().count() == headers.size();
  523.         }

  524.         public List<String> getHeaders() {
  525.             return headers;
  526.         }

  527.         public List<List<String>> getRows() {
  528.             return rows;
  529.         }

  530.         private boolean areAllColumnsDistinct() {
  531.             return allColumnsDistinct;
  532.         }

  533.         private void clear() {
  534.             headers.clear();
  535.             rows.clear();
  536.         }

  537.         private List<String> getRow(int rowIndex) {
  538.             if (rowIndex > rows.size() - 1) {
  539.                 throw new RowNotFound(rowIndex);
  540.             }
  541.             return rows.get(rowIndex);
  542.         }

  543.         private List<String> getColumn(int columnIndex) {
  544.             return rows.stream().map(row -> row.get(columnIndex)).collect(toList());
  545.         }

  546.         private List<String> getColumn(String columnName) {
  547.             int[] headerIndexes = IntStream.range(0, headers.size()).filter(i -> headers.get(i).equals(columnName))
  548.                     .toArray();
  549.             long headerCount = headerIndexes.length;
  550.             if (headerCount == 0) {
  551.                 throw new ColumnNotFound(columnName);
  552.             } else if (headerCount > 1) {
  553.                 throw new NonDistinctColumnFound(columnName, headerCount);
  554.             }
  555.             return getColumn(headerIndexes[0]);
  556.         }
  557.     }

  558.     public static class RowNotFound extends RuntimeException {
  559.         static final long serialVersionUID = 6577709350720827070L;

  560.         public RowNotFound(int rowIndex) {
  561.             super(Integer.toString(rowIndex));
  562.         }
  563.     }

  564.     public static class ColumnNotFound extends RuntimeException {
  565.         static final long serialVersionUID = -6008855238823273059L;

  566.         public ColumnNotFound(String columnName) {
  567.             super(String.format("The '%s' column does not exist", columnName));
  568.         }
  569.     }

  570.     public static class NonDistinctColumnFound extends RuntimeException {
  571.         static final long serialVersionUID = -6898791308443992005L;

  572.         public NonDistinctColumnFound(String columnName, long totalColumnCount) {
  573.             super(String.format("There are %d columns with the name '%s'", totalColumnCount, columnName));
  574.         }

  575.         public NonDistinctColumnFound(String message) {
  576.             super(message);
  577.         }
  578.     }
  579. }