ExamplesTable.java
- package org.jbehave.core.model;
- import static java.lang.Boolean.parseBoolean;
- import static java.util.regex.Pattern.DOTALL;
- import static java.util.stream.Collectors.toList;
- import static org.apache.commons.lang3.Validate.isTrue;
- import static org.apache.commons.lang3.Validate.notBlank;
- import java.io.PrintStream;
- import java.lang.reflect.Type;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.Deque;
- import java.util.LinkedHashMap;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.Optional;
- import java.util.Properties;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
- import java.util.stream.Collectors;
- import java.util.stream.IntStream;
- import java.util.stream.Stream;
- import org.apache.commons.lang3.StringUtils;
- import org.apache.commons.lang3.builder.ToStringBuilder;
- import org.apache.commons.lang3.builder.ToStringStyle;
- import org.jbehave.core.configuration.Keywords;
- import org.jbehave.core.i18n.LocalizedKeywords;
- import org.jbehave.core.io.LoadFromClasspath;
- import org.jbehave.core.model.TableTransformers.TableTransformer;
- import org.jbehave.core.steps.ChainedRow;
- import org.jbehave.core.steps.ConvertedParameters;
- import org.jbehave.core.steps.ParameterControls;
- import org.jbehave.core.steps.ParameterConverters;
- import org.jbehave.core.steps.Parameters;
- import org.jbehave.core.steps.Row;
- /**
- * Represents a tabular structure that holds rows of example data for parameters named via the column headers:
- * <pre>
- * |header 1|header 2| .... |header n|
- * |value 11|value 12| .... |value 1n|
- * ...
- * |value m1|value m2| .... |value mn|
- * </pre>
- *
- * <p>Different header and value column separators can be specified to replace the default separator "|":
- * <pre>
- * !!header 1!!header 2!! .... !!header n!!
- * !value 11!value 12! .... !value 1n!
- * ...
- * !value m1!value m2| .... !value mn!
- * </pre>
- * </p>
- *
- * <p>Rows starting with an ignorable separator are allowed and ignored:
- * <pre>
- * |header 1|header 2| .... |header n|
- * |-- A commented row --|
- * |value 11|value 12| .... |value 1n|
- * ...
- * |-- Another commented row --|
- * |value m1|value m2| .... |value mn|
- * </pre>
- * </p>
- *
- * <p>Ignorable separator is configurable and defaults to "|--".</p>
- * <p>The separators are also configurable via inlined properties:
- * <pre>
- * {ignorableSeparator=!--,headerSeparator=!,valueSeparator=!}
- * !header 1!header 2! .... !header n!
- * !-- A commented row --!
- * !value 11!value 12! .... !value 1n!
- * ...
- * !-- Another commented row --!
- * !value m1!value m2! .... !value mn!
- * </pre>
- * </p>
- *
- * <p>By default all column values are trimmed. To avoid trimming the values, use the "trim" inlined property:
- * <pre>
- * {trim=false}
- * | header 1 | header 2 | .... | header n |
- * | value 11 | value 12 | .... | value 1n |
- * </pre>
- * </p>
- *
- * <p>Comments is column values are supported via the "commentSeparator" inlined property:
- * <pre>
- * {commentSeparator=#}
- * | header 1#comment | header 2 | .... | header n |
- * | value 11#comment | value 12 | .... | value 1n |
- * </pre>
- * Comments including the separator are stripped.</p>
- *
- * <p>Line break is a default separator for rows in ExamplesTable, that's why they can't be added as is
- * to the data. In order to put the value with line breaks to ExamplesTable escape sequences (a character preceded by a
- * backslash (\) is an escape sequence) must be used.
- * <table>
- * <thead><tr><th>Escape Sequence</th><th>Description</th></tr></thead>
- * <tbody>
- * <tr><td>\n</td><td>Insert a newline in the value at this point.</td></tr>
- * <tr><td>\r</td><td>Insert a carriage return in the text at this point.</td></tr>
- * <tr><td>\\</td><td>Insert a backslash character in the text at this point.</td></tr>
- * </tbody>
- * </table>
- * Inlined property <code>processEscapeSequences</code> defines whether escape sequences should be replaced in the data.
- * It’s <code>false</code> by default (no property is declared explicitly). The allowed values are <code>true</code> and
- * <code>false</code>, any other values are considered invalid and will lead to exception thrown at parsing.
- * <pre>
- * {processEscapeSequences=true, commentSeparator=#}
- * |header |
- * |line 1\nline 2 |# The value with a newline
- * |line 1\r\nline 2|# The value with a carriage return and a newline
- * |line 1\\nline 2 |# The value with an escaped escape sequence, the result will be "line 1\nline 2"
- * </pre>
- * </p>
- *
- * <p>The table allows the retrieval of row values as converted parameters. Use {@link #getRowAsParameters(int)} and
- * invoke {@link Parameters#valueAs(String, Type)} specifying the header and the class type of the parameter.</p>
- *
- * <p>The table allows the transformation of its string representation via the "transformer" inlined property
- * (multiple transformers will be applied in chain-mode):
- * <pre>
- * {transformer=myTransformerName}
- * {transformer=myOtherTransformerName}
- * |header 1|header 2| .... |header n|
- * |value 11|value 12| .... |value 1n|
- * ...
- * |value m1|value m2| .... |value mn|
- * </pre>
- * The transformers need to be registered by name via the
- * {@link TableTransformers#useTransformer(String, TableTransformer)}. A few transformers are already registered by
- * default in {@link TableTransformers}.</p>
- *
- * <p>The table allow filtering on meta by row via the "metaByRow" inlined property:
- * <pre>
- * {metaByRow=true}
- * | Meta: | header 1 | .... | header n |
- * | @name=value | value 11 | .... | value 1n |
- * </pre>
- * </p>
- *
- * <p>Once created, the table row can be modified, via the {@link #withRowValues(int, Map)} method, by specifying the
- * map of row values to be changed.</p>
- *
- * <p>A table can also be created by providing the entire data content, via the {@link #withRows(List)} method.</p>
- *
- * <p>The parsing code assumes that the number of columns for data rows is the same as in the header, if a row has
- * less fields, the remaining are filled with empty values, if it has more, the fields are ignored.</p>
- */
- public class ExamplesTable {
- private static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
- private static final String EMPTY_VALUE = "";
- public static final Pattern INLINED_PROPERTIES_PATTERN = Pattern.compile("\\{(.*?[^\\\\])\\}\\s*(.*)", DOTALL);
- public static final ExamplesTable EMPTY = new ImmutableExamplesTable(EMPTY_VALUE);
- private final ParameterConverters parameterConverters;
- private final Row defaults;
- private final TableRows tableRows;
- private final Deque<TableProperties> tablePropertiesQueue = new LinkedList<>();
- private Map<String, String> namedParameters = Collections.emptyMap();
- private ParameterControls parameterControls;
- private TableTransformerMonitor tableTransformerMonitor;
- public ExamplesTable(String tableAsString) {
- this(tableAsString, new TableTransformers());
- }
- private ExamplesTable(String tableAsString, TableTransformers tableTransformers) {
- this(tableAsString, new ParameterConverters(new LoadFromClasspath(), tableTransformers), tableTransformers);
- }
- private ExamplesTable(String tableAsString, ParameterConverters parameterConverters,
- TableTransformers tableTransformers) {
- this(tableAsString, parameterConverters, new TableParsers(new LocalizedKeywords(), parameterConverters),
- tableTransformers);
- }
- private ExamplesTable(String tableAsString, ParameterConverters parameterConverters, TableParsers tableParsers,
- TableTransformers tableTransformers) {
- this(tableParsers.parseProperties(tableAsString), parameterConverters, new ParameterControls(), tableParsers,
- tableTransformers, new NullTableTransformerMonitor());
- }
- ExamplesTable(TablePropertiesQueue tablePropertiesQueue, ParameterConverters parameterConverters,
- ParameterControls parameterControls, TableParsers tableParsers, TableTransformers tableTransformers,
- TableTransformerMonitor tableTransformerMonitor) {
- this.parameterConverters = parameterConverters;
- this.parameterControls = parameterControls;
- this.defaults = new ConvertedParameters(EMPTY_MAP, parameterConverters);
- this.tablePropertiesQueue.addAll(tablePropertiesQueue.getProperties());
- this.tableTransformerMonitor = tableTransformerMonitor;
- String transformedTable = applyTransformers(tableTransformers, tablePropertiesQueue.getTable(), tableParsers);
- this.tableRows = tableParsers.parseRows(transformedTable, lastTableProperties());
- }
- private TableProperties lastTableProperties() {
- return tablePropertiesQueue.getLast();
- }
- private ExamplesTable(ExamplesTable other, Row defaults) {
- this.tableRows = new TableRows(other.tableRows.getHeaders(),
- other.tableRows.getRows());
- this.parameterConverters = other.parameterConverters;
- this.tablePropertiesQueue.addAll(other.tablePropertiesQueue);
- this.defaults = defaults;
- }
- private String applyTransformers(TableTransformers tableTransformers, String tableAsString,
- TableParsers tableParsers) {
- String transformedTable = tableAsString;
- TableProperties previousProperties = null;
- for (TableProperties properties : tablePropertiesQueue) {
- String transformer = properties.getTransformer();
- if (transformer != null) {
- if (previousProperties != null) {
- properties.overrideSeparatorsFrom(previousProperties);
- }
- tableTransformerMonitor.beforeTransformerApplying(transformer, properties, tableAsString);
- transformedTable = tableTransformers.transform(transformer, transformedTable, tableParsers, properties);
- tableTransformerMonitor.afterTransformerApplying(transformer, properties, transformedTable);
- }
- previousProperties = properties;
- }
- return transformedTable;
- }
- public ExamplesTable withDefaults(Parameters defaults) {
- return new ExamplesTable(this, new ChainedRow(defaults, this.defaults));
- }
- public ExamplesTable withNamedParameters(Map<String, String> namedParameters) {
- this.namedParameters = namedParameters;
- return this;
- }
- public ExamplesTable withRowValues(int rowIndex, Map<String, String> values) {
- for (String header : values.keySet()) {
- if (!getHeaders().contains(header)) {
- getHeaders().add(header);
- }
- }
- List<String> row = getRowValues(rowIndex);
- List<String> headers = getHeaders();
- for (int i = 0, headersSize = headers.size(); i < headersSize; i++) {
- String value = values.get(headers.get(i));
- if (i >= row.size()) {
- row.add(Optional.ofNullable(value).orElse(EMPTY_VALUE));
- } else if (value != null) {
- row.set(i, value);
- }
- }
- return this;
- }
- public ExamplesTable withRows(List<Map<String, String>> rows) {
- tableRows.clear();
- if (!rows.isEmpty()) {
- getHeaders().addAll(rows.get(0).keySet());
- rows.stream().map(Map::values).map(ArrayList::new).forEach(tableRows.rows::add);
- }
- return this;
- }
- public Properties getProperties() {
- return lastTableProperties().getProperties();
- }
- public String getPropertiesAsString() {
- return lastTableProperties().getPropertiesAsString();
- }
- private String replaceNamedParameters(String text) {
- return parameterControls.replaceAllDelimitedNames(text, namedParameters);
- }
- private List<String> replaceNamedParameters(List<String> values, boolean replaceNamedParameters) {
- return replaceNamedParameters ? values.stream().map(this::replaceNamedParameters).collect(toList()) : values;
- }
- public List<String> getHeaders() {
- return tableRows.getHeaders();
- }
- public Map<String, String> getRow(int rowIndex) {
- return getRow(rowIndex, false);
- }
- public Map<String, String> getRow(int rowIndex, boolean replaceNamedParameters) {
- List<String> values = getRowValues(rowIndex, replaceNamedParameters);
- if (!tableRows.areAllColumnsDistinct()) {
- String exceptionMessage = "ExamplesTable contains non-distinct columns, all columns are: "
- + String.join(", ", getHeaders());
- throw new NonDistinctColumnFound(exceptionMessage);
- }
- Map<String, String> result = new LinkedHashMap<>();
- List<String> headers = getHeaders();
- for (int i = 0, headersSize = headers.size(); i < headersSize; i++) {
- result.put(headers.get(i), i < values.size() ? values.get(i) : EMPTY_VALUE);
- }
- return result;
- }
- public List<String> getRowValues(int rowIndex) {
- return getRowValues(rowIndex, false);
- }
- public List<String> getRowValues(int rowIndex, boolean replaceNamedParameters) {
- return replaceNamedParameters(tableRows.getRow(rowIndex), replaceNamedParameters);
- }
- public Parameters getRowAsParameters(int rowIndex) {
- return getRowAsParameters(rowIndex, false);
- }
- public Parameters getRowAsParameters(int rowIndex, boolean replaceNamedParameters) {
- Map<String, String> row = getRow(rowIndex, replaceNamedParameters);
- return new ConvertedParameters(new ChainedRow(new ConvertedParameters(row, parameterConverters), defaults),
- parameterConverters);
- }
- public int getRowCount() {
- return tableRows.getRows().size();
- }
- public boolean metaByRow() {
- return lastTableProperties().isMetaByRow();
- }
- public List<Map<String, String>> getRows() {
- List<Map<String, String>> rows = new ArrayList<>();
- for (int row = 0; row < getRowCount(); row++) {
- rows.add(getRow(row));
- }
- return rows;
- }
- public List<Parameters> getRowsAsParameters() {
- return getRowsAsParameters(false);
- }
- public List<Parameters> getRowsAsParameters(boolean replaceNamedParameters) {
- List<Parameters> rows = new ArrayList<>();
- for (int row = 0; row < getRowCount(); row++) {
- rows.add(getRowAsParameters(row, replaceNamedParameters));
- }
- return rows;
- }
- public <T> List<T> getRowsAs(Class<T> type) {
- return getRowsAs(type, Collections.emptyMap());
- }
- public <T> List<T> getRowsAs(Class<T> type, Map<String, String> fieldNameMapping) {
- return getRowsAsParameters().stream()
- .map(p -> p.mapTo(type, fieldNameMapping))
- .collect(toList());
- }
- public List<String> getColumn(String columnName) {
- return getColumn(columnName, false);
- }
- public List<String> getColumn(String columnName, boolean replaceNamedParameters) {
- return replaceNamedParameters(tableRows.getColumn(columnName), replaceNamedParameters);
- }
- public String getHeaderSeparator() {
- return lastTableProperties().getHeaderSeparator();
- }
- public String getValueSeparator() {
- return lastTableProperties().getValueSeparator();
- }
- public String asString() {
- if (tableRows.getRows().isEmpty()) {
- return EMPTY_VALUE;
- }
- StringBuilder sb = new StringBuilder();
- for (TableProperties properties : tablePropertiesQueue) {
- String propertiesAsString = properties.getPropertiesAsString();
- if (!propertiesAsString.isEmpty()) {
- sb.append("{").append(propertiesAsString).append("}").append(lastTableProperties().getRowSeparator());
- }
- }
- sb.append(ExamplesTableStringBuilder.buildExamplesTableString(lastTableProperties(), getHeaders(),
- tableRows.getRows()));
- return sb.toString();
- }
- public boolean isEmpty() {
- return getHeaders().isEmpty();
- }
- public void outputTo(PrintStream output) {
- output.print(asString());
- }
- public static ExamplesTable empty() {
- return new ExamplesTable("");
- }
- @Override
- public String toString() {
- return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
- }
- public static final class TableProperties {
- public enum Decorator {
- LOWERCASE, UPPERCASE, VERBATIM, TRIM
- }
- private static final String COMMA = ",";
- private static final String COMMA_REGEX = "\\,";
- private static final String EQUAL = "=";
- private static final String PIPE_REGEX = "\\|";
- private static final String DECORATORS_REGEX = Stream.of(Decorator.values())
- .map(Decorator::name)
- .collect(Collectors.joining("|", "(", ")"));
- private static final Pattern DECORATED_PROPERTY_PATTERN = Pattern.compile(
- "\\s*\\{([^=,\\s]+(\\|" + DECORATORS_REGEX + ")*)}\\s*", Pattern.CASE_INSENSITIVE);
- private static final String HEADER_SEPARATOR_KEY = "headerSeparator";
- private static final String VALUE_SEPARATOR_KEY = "valueSeparator";
- private static final String IGNORABLE_SEPARATOR_KEY = "ignorableSeparator";
- private static final String COMMENT_SEPARATOR_KEY = "commentSeparator";
- private static final String NULL_PLACEHOLDER_KEY = "nullPlaceholder";
- private static final String PROCESS_ESCAPE_SEQUENCES_KEY = "processEscapeSequences";
- private static final String ROW_SEPARATOR = "\n";
- private final Properties properties = new Properties();
- private final ParameterConverters parameterConverters;
- private final String propertiesAsString;
- public TableProperties(String propertiesAsString, Keywords keywords, ParameterConverters parameterConverters) {
- this.propertiesAsString = propertiesAsString;
- this.parameterConverters = parameterConverters;
- properties.setProperty(HEADER_SEPARATOR_KEY, keywords.examplesTableHeaderSeparator());
- properties.setProperty(VALUE_SEPARATOR_KEY, keywords.examplesTableValueSeparator());
- properties.setProperty(IGNORABLE_SEPARATOR_KEY, keywords.examplesTableIgnorableSeparator());
- properties.putAll(parseProperties(propertiesAsString));
- }
- private Map<String, String> parseProperties(String propertiesAsString) {
- Map<String, String> result = new LinkedHashMap<>();
- if (!StringUtils.isEmpty(propertiesAsString)) {
- for (String propertyAsString : propertiesAsString.split("(?<!\\\\),")) {
- String[] property = StringUtils.split(propertyAsString, EQUAL, 2);
- String propertyName = property[0];
- String propertyValue = property[1];
- Matcher decoratedPropertyMatcher = DECORATED_PROPERTY_PATTERN.matcher(propertyName);
- if (decoratedPropertyMatcher.matches()) {
- String[] propertyWithDecorators = decoratedPropertyMatcher.group(1).split(PIPE_REGEX);
- propertyName = propertyWithDecorators[0];
- for (int i = 1; i < propertyWithDecorators.length; i++) {
- String decorator = propertyWithDecorators[i].toUpperCase();
- propertyValue = decoratePropertyValue(propertyValue, Decorator.valueOf(decorator));
- }
- } else {
- propertyValue = propertyValue.trim();
- }
- result.put(propertyName.trim(), StringUtils.replace(propertyValue, COMMA_REGEX, COMMA));
- }
- }
- return result;
- }
- private String decoratePropertyValue(String value, Decorator decorator) {
- switch (decorator) {
- case VERBATIM:
- return value;
- case LOWERCASE:
- return value.toLowerCase();
- case UPPERCASE:
- return value.toUpperCase();
- case TRIM:
- default:
- return value.trim();
- }
- }
- @SuppressWarnings("unchecked")
- public <T> T getMandatoryNonBlankProperty(String propertyName, Type type) {
- String propertyValue = properties.getProperty(propertyName);
- isTrue(propertyValue != null, "'%s' is not set in ExamplesTable properties", propertyName);
- notBlank(propertyValue, "ExamplesTable property '%s' is blank", propertyName);
- return (T) parameterConverters.convert(propertyValue, type);
- }
- public String getRowSeparator() {
- return ROW_SEPARATOR;
- }
- public String getHeaderSeparator() {
- return properties.getProperty(HEADER_SEPARATOR_KEY);
- }
- public String getValueSeparator() {
- return properties.getProperty(VALUE_SEPARATOR_KEY);
- }
- public String getIgnorableSeparator() {
- return properties.getProperty(IGNORABLE_SEPARATOR_KEY);
- }
- public String getCommentSeparator() {
- return properties.getProperty(COMMENT_SEPARATOR_KEY);
- }
- public Optional<String> getNullPlaceholder() {
- return Optional.ofNullable(properties.getProperty(NULL_PLACEHOLDER_KEY));
- }
- public boolean isProcessEscapeSequences() {
- String processEscapeSequences = properties.getProperty(PROCESS_ESCAPE_SEQUENCES_KEY);
- if (processEscapeSequences != null) {
- String processEscapeSequencesTrimmed = processEscapeSequences.trim().toLowerCase(Locale.ENGLISH);
- if ("true".equals(processEscapeSequencesTrimmed)) {
- return true;
- }
- if ("false".equals(processEscapeSequencesTrimmed)) {
- return false;
- }
- throw new IllegalArgumentException(
- "ExamplesTable property 'processEscapeSequences' contains invalid value: '"
- + processEscapeSequences + "', but allowed values are 'true' and 'false'");
- }
- return false;
- }
- void overrideSeparatorsFrom(TableProperties properties) {
- Stream.of(HEADER_SEPARATOR_KEY, VALUE_SEPARATOR_KEY, IGNORABLE_SEPARATOR_KEY, COMMENT_SEPARATOR_KEY)
- .forEach(key -> {
- String value = properties.properties.getProperty(key);
- if (value != null) {
- this.properties.setProperty(key, value);
- }
- });
- }
- public boolean isTrim() {
- return parseBoolean(properties.getProperty("trim", "true"));
- }
- public boolean isMetaByRow() {
- return parseBoolean(properties.getProperty("metaByRow", "false"));
- }
- public String getTransformer() {
- return properties.getProperty("transformer");
- }
- public Properties getProperties() {
- return properties;
- }
- public String getPropertiesAsString() {
- return propertiesAsString;
- }
- }
- static final class TablePropertiesQueue {
- private final String table;
- private final Deque<TableProperties> properties;
- TablePropertiesQueue(String table, Deque<TableProperties> properties) {
- this.table = table;
- this.properties = properties;
- }
- String getTable() {
- return table;
- }
- Deque<TableProperties> getProperties() {
- return properties;
- }
- }
- public static final class TableRows {
- private final List<String> headers;
- private final List<List<String>> rows;
- private final boolean allColumnsDistinct;
- public TableRows(List<String> headers, List<List<String>> rows) {
- this.headers = headers;
- this.rows = rows;
- this.allColumnsDistinct = headers.stream().distinct().count() == headers.size();
- }
- public List<String> getHeaders() {
- return headers;
- }
- public List<List<String>> getRows() {
- return rows;
- }
- private boolean areAllColumnsDistinct() {
- return allColumnsDistinct;
- }
- private void clear() {
- headers.clear();
- rows.clear();
- }
- private List<String> getRow(int rowIndex) {
- if (rowIndex > rows.size() - 1) {
- throw new RowNotFound(rowIndex);
- }
- return rows.get(rowIndex);
- }
- private List<String> getColumn(int columnIndex) {
- return rows.stream().map(row -> row.get(columnIndex)).collect(toList());
- }
- private List<String> getColumn(String columnName) {
- int[] headerIndexes = IntStream.range(0, headers.size()).filter(i -> headers.get(i).equals(columnName))
- .toArray();
- long headerCount = headerIndexes.length;
- if (headerCount == 0) {
- throw new ColumnNotFound(columnName);
- } else if (headerCount > 1) {
- throw new NonDistinctColumnFound(columnName, headerCount);
- }
- return getColumn(headerIndexes[0]);
- }
- }
- public static class RowNotFound extends RuntimeException {
- static final long serialVersionUID = 6577709350720827070L;
- public RowNotFound(int rowIndex) {
- super(Integer.toString(rowIndex));
- }
- }
- public static class ColumnNotFound extends RuntimeException {
- static final long serialVersionUID = -6008855238823273059L;
- public ColumnNotFound(String columnName) {
- super(String.format("The '%s' column does not exist", columnName));
- }
- }
- public static class NonDistinctColumnFound extends RuntimeException {
- static final long serialVersionUID = -6898791308443992005L;
- public NonDistinctColumnFound(String columnName, long totalColumnCount) {
- super(String.format("There are %d columns with the name '%s'", totalColumnCount, columnName));
- }
- public NonDistinctColumnFound(String message) {
- super(message);
- }
- }
- }