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.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.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>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 = new HashMap<>();
private ParameterControls parameterControls;
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);
}
ExamplesTable(TablePropertiesQueue tablePropertiesQueue, ParameterConverters parameterConverters,
ParameterControls parameterControls, TableParsers tableParsers, TableTransformers tableTransformers) {
this.parameterConverters = parameterConverters;
this.parameterControls = parameterControls;
this.defaults = new ConvertedParameters(EMPTY_MAP, parameterConverters);
this.tablePropertiesQueue.addAll(tablePropertiesQueue.getProperties());
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);
}
transformedTable = tableTransformers.transform(transformer, transformedTable, tableParsers, properties);
}
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 row, Map<String, String> values) {
getRow(row).putAll(values);
for (String header : values.keySet()) {
if (!getHeaders().contains(header)) {
getHeaders().add(header);
}
}
return this;
}
public ExamplesTable withRows(List<Map<String, String>> values) {
getHeaders().clear();
tableRows.getRows().clear();
if (!values.isEmpty()) {
getHeaders().addAll(values.get(0).keySet());
tableRows.getRows().addAll(values);
}
return this;
}
public Properties getProperties() {
return lastTableProperties().getProperties();
}
public String getPropertiesAsString() {
return lastTableProperties().getPropertiesAsString();
}
public List<String> getHeaders() {
return tableRows.getHeaders();
}
public Map<String, String> getRow(int row) {
if (row > tableRows.getRows().size() - 1) {
throw new RowNotFound(row);
}
Map<String, String> values = tableRows.getRows().get(row);
if (getHeaders().size() != values.keySet().size()) {
for (String header : getHeaders()) {
if (!values.containsKey(header)) {
values.put(header, EMPTY_VALUE);
}
}
}
return values;
}
public Parameters getRowAsParameters(int row) {
return getRowAsParameters(row, false);
}
public Parameters getRowAsParameters(int row, boolean replaceNamedParameters) {
Map<String, String> rowValues = getRow(row);
return createParameters(replaceNamedParameters ? replaceNamedParameters(rowValues) : rowValues);
}
private Map<String, String> replaceNamedParameters(Map<String, String> row) {
Map<String, String> replaced = new LinkedHashMap<>();
for (Entry<String, String> cell : row.entrySet()) {
replaced.put(cell.getKey(), replaceNamedParameters(cell.getValue()));
}
return replaced;
}
private String replaceNamedParameters(String text) {
return parameterControls.replaceAllDelimitedNames(text, namedParameters);
}
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, new HashMap<String, String>());
}
public <T> List<T> getRowsAs(Class<T> type, Map<String, String> fieldNameMapping) {
return getRowsAsParameters().stream()
.map(p -> p.mapTo(type, fieldNameMapping))
.collect(Collectors.toList());
}
public List<String> getColumn(String columnName) {
return getColumn(columnName, false);
}
public List<String> getColumn(String columnName, boolean replaceNamedParameters) {
if (!getHeaders().contains(columnName)) {
throw new ColumnNotFound(columnName);
}
List<String> column = tableRows.getRows().stream()
.map(row -> row.get(columnName))
.collect(toList());
return replaceNamedParameters ? column.stream().map(this::replaceNamedParameters).collect(toList()) : column;
}
private Parameters createParameters(Map<String, String> values) {
return new ConvertedParameters(new ChainedRow(new ConvertedParameters(values, parameterConverters), defaults),
parameterConverters);
}
public String getHeaderSeparator() {
return lastTableProperties().getHeaderSeparator();
}
public String getValueSeparator() {
return lastTableProperties().getValueSeparator();
}
public String asString() {
if (tableRows.getRows().isEmpty()) {
return EMPTY_VALUE;
}
return format();
}
public boolean isEmpty() {
return getHeaders().isEmpty();
}
public void outputTo(PrintStream output) {
output.print(asString());
}
private String format() {
StringBuilder sb = new StringBuilder();
for (TableProperties properties : tablePropertiesQueue) {
String propertiesAsString = properties.getPropertiesAsString();
if (!propertiesAsString.isEmpty()) {
sb.append("{").append(propertiesAsString).append("}").append(lastTableProperties().getRowSeparator());
}
}
for (String header : getHeaders()) {
sb.append(getHeaderSeparator()).append(header);
}
sb.append(getHeaderSeparator()).append(lastTableProperties().getRowSeparator());
for (Map<String, String> row : getRows()) {
for (String header : getHeaders()) {
sb.append(getValueSeparator()).append(row.get(header));
}
sb.append(getValueSeparator()).append(lastTableProperties().getRowSeparator());
}
return sb.toString();
}
public static ExamplesTable empty() {
return new ExamplesTable("");
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
@SuppressWarnings("serial")
public static class RowNotFound extends RuntimeException {
public RowNotFound(int row) {
super(Integer.toString(row));
}
}
@SuppressWarnings("serial")
public static class ColumnNotFound extends RuntimeException {
public ColumnNotFound(String columnName) {
super(String.format("The '%s' column does not exist", columnName));
}
}
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 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));
}
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<Map<String, String>> rows;
public TableRows(List<String> headers, List<Map<String, String>> rows) {
this.headers = headers;
this.rows = rows;
}
public List<String> getHeaders() {
return headers;
}
public List<Map<String, String>> getRows() {
return rows;
}
}
}