TableTransformers.java

  1. package org.jbehave.core.model;

  2. import static java.util.stream.Collectors.joining;

  3. import java.util.ArrayList;
  4. import java.util.HashMap;
  5. import java.util.LinkedHashMap;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.regex.Matcher;
  9. import java.util.regex.Pattern;
  10. import java.util.stream.Collectors;
  11. import java.util.stream.IntStream;

  12. import org.apache.commons.lang3.StringUtils;
  13. import org.apache.commons.lang3.Validate;
  14. import org.jbehave.core.model.ExamplesTable.TableProperties;
  15. import org.jbehave.core.model.ExamplesTable.TableRows;
  16. import org.jbehave.core.steps.ParameterControls;

  17. /**
  18.  * <p>
  19.  * Facade responsible for transforming table string representations. It allows
  20.  * the registration of several {@link TableTransformer} instances by name.
  21.  * </p>
  22.  * <p>
  23.  * Some Transformers are provided out-of-the-box:
  24.  * <ul>
  25.  * <li>{@link TableTransformers.FromLandscape FromLandscape}: registered under
  26.  * name {@link TableTransformers#FROM_LANDSCAPE}</li>
  27.  * <li>{@link TableTransformers.Formatting Formatting}: registered under name
  28.  * {@link TableTransformers#FORMATTING}</li>
  29.  * <li>{@link TableTransformers.Replacing Replacing}: registered under name
  30.  * {@link TableTransformers#REPLACING}</li>
  31.  * </ul>
  32.  * Out-of-the-box transformers that must be registered:
  33.  * <ul>
  34.  * <li>{@link ResolvingSelfReferences ResolvingSelfReferences}
  35.  * </ul>
  36.  * </p>
  37.  */
  38. public class TableTransformers {

  39.     public static final String FROM_LANDSCAPE = "FROM_LANDSCAPE";
  40.     public static final String FORMATTING = "FORMATTING";
  41.     public static final String REPLACING = "REPLACING";

  42.     private final Map<String, TableTransformer> transformers = new HashMap<>();

  43.     public TableTransformers() {
  44.         useTransformer(FROM_LANDSCAPE, new FromLandscape());
  45.         useTransformer(FORMATTING, new Formatting());
  46.         useTransformer(REPLACING, new Replacing());
  47.     }

  48.     public String transform(String transformerName, String tableAsString, TableParsers tableParsers,
  49.             TableProperties properties) {
  50.         TableTransformer tableTransformer = transformers.get(transformerName);
  51.         if (tableTransformer == null) {
  52.             throw new TransformerNotFoundException(transformerName);
  53.         }
  54.         String result = tableTransformer.transform(tableAsString, tableParsers, properties);
  55.         if (result == null) {
  56.             throw new InvalidTransformationResultException(
  57.                     String.format("Table transformation using transformer '%s' resulted in 'null'", transformerName));
  58.         }
  59.         return result;
  60.     }

  61.     public void useTransformer(String name, TableTransformer transformer) {
  62.         transformers.put(name, transformer);
  63.     }

  64.     public interface TableTransformer {
  65.         String transform(String tableAsString, TableParsers tableParsers, TableProperties properties);
  66.     }

  67.     public static class FromLandscape implements TableTransformer {

  68.         @Override
  69.         public String transform(String tableAsString, TableParsers tableParsers, TableProperties properties) {
  70.             Map<String, List<String>> data = new LinkedHashMap<>();
  71.             for (String rowAsString : tableAsString.split(properties.getRowSeparator())) {
  72.                 if (ignoreRow(rowAsString, properties.getIgnorableSeparator())) {
  73.                     continue;
  74.                 }
  75.                 List<String> values = tableParsers.parseRow(rowAsString, false, properties);
  76.                 String header = values.get(0);
  77.                 List<String> rowValues = new ArrayList<>(values);
  78.                 rowValues.remove(0);
  79.                 data.put(header, rowValues);
  80.             }

  81.             if (data.values().stream().mapToInt(List::size).distinct().count() != 1) {
  82.                 String errorMessage = data.entrySet()
  83.                         .stream()
  84.                         .map(e -> {
  85.                             int numberOfCells = e.getValue().size();
  86.                             StringBuilder rowDescription = new StringBuilder(e.getKey())
  87.                                     .append(" -> ")
  88.                                     .append(numberOfCells)
  89.                                     .append(" cell");
  90.                             if (numberOfCells > 1) {
  91.                                 rowDescription.append('s');
  92.                             }
  93.                             return rowDescription.toString();
  94.                         })
  95.                         .collect(joining(", ", "The table rows have unequal numbers of cells: ", ""));
  96.                 throw new IllegalArgumentException(errorMessage);
  97.             }

  98.             StringBuilder builder = new StringBuilder();
  99.             builder.append(properties.getHeaderSeparator());
  100.             for (String header : data.keySet()) {
  101.                 builder.append(header).append(properties.getHeaderSeparator());
  102.             }
  103.             builder.append(properties.getRowSeparator());
  104.             int numberOfCells = data.values().iterator().next().size();
  105.             for (int c = 0; c < numberOfCells; c++) {
  106.                 builder.append(properties.getValueSeparator());
  107.                 for (List<String> row : data.values()) {
  108.                     builder.append(row.get(c)).append(properties.getValueSeparator());
  109.                 }
  110.                 builder.append(properties.getRowSeparator());
  111.             }
  112.             return builder.toString();
  113.         }

  114.         private boolean ignoreRow(String rowAsString, String ignorableSeparator) {
  115.             return rowAsString.startsWith(ignorableSeparator)
  116.                     || rowAsString.length() == 0;
  117.         }

  118.     }

  119.     public static class Formatting implements TableTransformer {

  120.         @Override
  121.         public String transform(String tableAsString, TableParsers tableParsers, TableProperties properties) {
  122.             List<List<String>> data = new ArrayList<>();
  123.             for (String rowAsString : tableAsString.split(properties.getRowSeparator())) {
  124.                 if (ignoreRow(rowAsString, properties.getIgnorableSeparator())) {
  125.                     continue;
  126.                 }
  127.                 data.add(tableParsers.parseRow(rowAsString, rowAsString.contains(properties.getHeaderSeparator()),
  128.                         properties));
  129.             }

  130.             StringBuilder builder = new StringBuilder();
  131.             Map<Integer, Integer> maxWidths = maxWidth(data);
  132.             for (int r = 0; r < data.size(); r++) {
  133.                 String formattedRow = formatRow(data.get(r), maxWidths,
  134.                         r == 0 ? properties.getHeaderSeparator() : properties.getValueSeparator());
  135.                 builder.append(formattedRow).append(properties.getRowSeparator());
  136.             }
  137.             return builder.toString();
  138.         }

  139.         private boolean ignoreRow(String rowAsString, String ignorableSeparator) {
  140.             return rowAsString.startsWith(ignorableSeparator)
  141.                     || rowAsString.length() == 0;
  142.         }

  143.         private Map<Integer, Integer> maxWidth(List<List<String>> data) {
  144.             Map<Integer, Integer> maxWidths = new HashMap<>();
  145.             for (List<String> row : data) {
  146.                 for (int c = 0; c < row.size(); c++) {
  147.                     String cell = row.get(c).trim();
  148.                     Integer width = maxWidths.get(c);
  149.                     int length = cell.length();
  150.                     if (width == null || length > width) {
  151.                         width = length;
  152.                         maxWidths.put(c, width);
  153.                     }
  154.                 }
  155.             }

  156.             return maxWidths;
  157.         }

  158.         private String formatRow(List<String> row,
  159.                 Map<Integer, Integer> maxWidths, String separator) {
  160.             StringBuilder builder = new StringBuilder();
  161.             builder.append(separator);
  162.             for (int c = 0; c < row.size(); c++) {
  163.                 builder.append(formatValue(row.get(c).trim(), maxWidths.get(c)))
  164.                         .append(separator);
  165.             }
  166.             return builder.toString();
  167.         }

  168.         private String formatValue(String value, int width) {
  169.             if (value.length() < width) {
  170.                 return value + padding(width - value.length());
  171.             }
  172.             return value;
  173.         }

  174.         private String padding(int size) {
  175.             StringBuilder builder = new StringBuilder();
  176.             for (int i = 0; i < size; i++) {
  177.                 builder.append(' ');
  178.             }
  179.             return builder.toString();
  180.         }

  181.     }

  182.     public static class Replacing implements TableTransformer {

  183.         @Override
  184.         public String transform(String tableAsString, TableParsers tableParsers, TableProperties properties) {
  185.             String replacing = properties.getProperties().getProperty("replacing");
  186.             String replacement = properties.getProperties().getProperty("replacement");
  187.             if (replacing == null || replacement == null) {
  188.                 return tableAsString;
  189.             }
  190.             return tableAsString.replace(replacing, replacement);
  191.         }
  192.     }

  193.     public static class ResolvingSelfReferences implements TableTransformer {
  194.         private final ParameterControls parameterControls;
  195.         private final Pattern placeholderPattern;

  196.         public ResolvingSelfReferences(ParameterControls parameterControls) {
  197.             this.parameterControls = parameterControls;
  198.             placeholderPattern = Pattern.compile(
  199.                     parameterControls.nameDelimiterLeft() + "(.*?)" + parameterControls.nameDelimiterRight());
  200.         }

  201.         @Override
  202.         public String transform(String tableAsString, TableParsers tableParsers, TableProperties properties) {
  203.             TableRows rows = tableParsers.parseRows(tableAsString, properties);
  204.             List<String> headers = rows.getHeaders();

  205.             List<List<String>> resolvedRows = getNamedRows(rows.getRows(), headers).stream()
  206.                     .map(this::resolveRow)
  207.                     .collect(Collectors.toList());

  208.             return ExamplesTableStringBuilder.buildExamplesTableString(properties, headers, resolvedRows);
  209.         }

  210.         private List<String> resolveRow(Map<String, String> unresolvedRow) {
  211.             Map<String, String> resolvedRow = new HashMap<>(unresolvedRow.size(), 1);
  212.             return unresolvedRow.keySet().stream()
  213.                     .map(name -> resolveCell(name, resolvedRow, unresolvedRow))
  214.                     .collect(Collectors.toList());
  215.         }

  216.         private String resolveCell(String name, Map<String, String> resolvedRow, Map<String, String> unresolvedRow) {
  217.             return resolveCell(name, new ArrayList<>(), resolvedRow, unresolvedRow);
  218.         }

  219.         private String resolveCell(String name, List<String> resolutionChain, Map<String, String> resolvedRow,
  220.                                    Map<String, String> unresolvedRow) {
  221.             if (resolvedRow.containsKey(name)) {
  222.                 return resolvedRow.get(name);
  223.             }
  224.             resolutionChain.add(name);
  225.             String result = unresolvedRow.get(name);
  226.             Matcher matcher = placeholderPattern.matcher(result);
  227.             while (matcher.find()) {
  228.                 String nestedName = matcher.group(1);
  229.                 Validate.validState(!name.equals(nestedName), "Circular self reference is found in column '%s'", name);
  230.                 checkForCircularChainOfReferences(resolutionChain, nestedName);
  231.                 if (unresolvedRow.containsKey(nestedName)) {
  232.                     resolveCell(nestedName, resolutionChain, resolvedRow, unresolvedRow);
  233.                 }
  234.                 result = StringUtils.replace(result,
  235.                         parameterControls.nameDelimiterLeft() + nestedName + parameterControls.nameDelimiterRight(),
  236.                         resolvedRow.get(nestedName));
  237.             }
  238.             resolvedRow.put(name, result);
  239.             return result;
  240.         }

  241.         private void checkForCircularChainOfReferences(List<String> resolutionChain, String name) {
  242.             int index = resolutionChain.indexOf(name);
  243.             if (index >= 0) {
  244.                 String delimiter = " -> ";
  245.                 String truncatedChain = resolutionChain.stream().skip(index).collect(Collectors.joining(delimiter));
  246.                 throw new IllegalStateException(
  247.                         "Circular chain of references is found: " + truncatedChain + delimiter + name);
  248.             }
  249.         }

  250.         private List<Map<String, String>> getNamedRows(List<List<String>> tableRows, List<String> tableHeaders) {
  251.             List<Map<String, String>> namedRows = new ArrayList<>();
  252.             for (List<String> row : tableRows) {
  253.                 Map<String, String> namedRow = new LinkedHashMap<>();
  254.                 IntStream.range(0, row.size()).forEach(i -> namedRow.put(tableHeaders.get(i), row.get(i)));
  255.                 namedRows.add(namedRow);
  256.             }
  257.             return namedRows;
  258.         }
  259.     }

  260.     public static class TransformerNotFoundException extends RuntimeException {
  261.         private static final long serialVersionUID = 3742264745351414296L;

  262.         public TransformerNotFoundException(String transformerName) {
  263.             super(String.format("Table transformer '%s' does not exist", transformerName));
  264.         }
  265.     }

  266.     public static class InvalidTransformationResultException extends RuntimeException {
  267.         private static final long serialVersionUID = -1242448321325167977L;

  268.         public InvalidTransformationResultException(String message) {
  269.             super(message);
  270.         }
  271.     }
  272. }