001/**
002 * The MIT License (MIT)
003 *
004 * Copyright (c) 2017 tools4j.org (Marco Terzer)
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024package org.tools4j.spockito;
025
026import java.lang.reflect.Array;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.Iterator;
030import java.util.List;
031
032/**
033 * Prepresents a table with header row and data rows as defined via the {@link org.tools4j.spockito.Spockito.Unroll}
034 * annotation.
035 */
036public class Table implements Iterable<TableRow> {
037
038    private static final Table EMPTY = new Table();
039
040    private final TableRow headers;
041    private final List<TableRow> data = new ArrayList<>();
042
043    private Table() {
044        this.headers = TableRow.empty(this);
045    }
046
047    private Table(final String headerString) {
048        this.headers = parseRow(this, 0, headerString);
049        if (headers.distinctCount() < headers.size()) {
050            throw new IllegalArgumentException("Duplicate column headers: " + headers);
051        }
052    }
053
054    public int getColumnCount() {
055        //null in constructor when parsing header row
056        return headers == null ? 0 : headers.size();
057    }
058
059    public int getColumnIndexByName(final String columnName) {
060        int columnIndex = headers.indexOf(columnName);
061        if (columnIndex < 0) {
062            if (columnName.length() > 0 && Character.isLowerCase(columnName.charAt(0))) {
063                columnIndex = headers.indexOf(Strings.firstCharToUpperCase(columnName));
064            }
065        }
066        if (columnIndex < 0) {
067            throw new IllegalArgumentException("No such column: " + columnName);
068        }
069        return columnIndex;
070    }
071
072    public boolean hasColumn(final String columnName) {
073        return 0 <= headers.indexOf(columnName);
074    }
075
076    public int getRowCount() {
077        return data.size();
078    }
079
080    public String getColumnName(final int index) {
081        return headers.get(index);
082    }
083
084    public TableRow getRow(final int rowIndex) {
085        return data.get(rowIndex);
086    }
087
088    public int getRowIndex(final TableRow row) {
089        return data.indexOf(row);
090    }
091
092    public String getValue(final int rowIndex, final int columnIndex) {
093        return data.get(rowIndex).get(columnIndex);
094    }
095
096    public String getValue(final int rowIndex, final String columnName) {
097        final int columnIndex = getColumnIndexByName(columnName);
098        return data.get(rowIndex).get(columnIndex);
099    }
100
101    @Override
102    public Iterator<TableRow> iterator() {
103        return Collections.unmodifiableList(data).iterator();
104    }
105
106    public static <T> T[] parse(final Class<T> rowType, final String[] headerAndRows) {
107        return parse(rowType, headerAndRows, new SpockitoValueConverter());
108    }
109
110    public static <T> T[] parse(final Class<T> rowType, final String[] headerAndRows, final ValueConverter valueConverter) {
111        final Table table = parse(headerAndRows);
112        final int rows = table.getRowCount();
113        final T[] result = (T[])Array.newInstance(rowType, rows);
114        for (int row = 0; row < rows; row++) {
115            result[row] = valueConverter.convert(rowType, rowType, table.getRow(row).asMap().toString());
116        }
117        return result;
118    }
119
120    public static Table parse(final String[] headerAndRows) {
121        if (headerAndRows.length > 0) {
122            final Table table = new Table(headerAndRows[0]);
123            for (int i = 1; i < headerAndRows.length; i++) {
124                final TableRow tableRow = parseRow(table, i, headerAndRows[i]);
125                if (!tableRow.isSeparatorRow()) {
126                    table.data.add(tableRow);
127                }
128            }
129            return table;
130        }
131        return Table.EMPTY;
132    }
133
134    private static TableRow parseRow(final Table table, final int row, final String rowString) {
135        final String trimmed = rowString.trim();
136        if (trimmed.length() < 2 || trimmed.charAt(0) != '|' || trimmed.charAt(trimmed.length() - 1) != '|') {
137            throw new IllegalArgumentException("Invalid table data: row " + row + " must start and end with '|'");
138        }
139        final TableRow tableRow = TableRow.parse(table, trimmed);
140        if (row != 0) {
141            if (tableRow.size() > table.getColumnCount()) {
142                throw new IllegalArgumentException("Invalid table data: row " + row + " has more columns than header row: " + tableRow.size() + " > " + table.getColumnCount());
143            }
144        }
145        return tableRow;
146    }
147
148}