/*
* Copyright (c) 2002 Cunningham & Cunningham, Inc.
* Copyright (c) 2009-2015 by Jochen Wierum & Cologne Intelligence
*
* This file is part of FitGoodies.
*
* FitGoodies is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitGoodies is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FitGoodies. If not, see <http://www.gnu.org/licenses/>.
*/
package de.cologneintelligence.fitgoodies;
import de.cologneintelligence.fitgoodies.htmlparser.FitCell;
import de.cologneintelligence.fitgoodies.htmlparser.FitRow;
import de.cologneintelligence.fitgoodies.htmlparser.FitTable;
import de.cologneintelligence.fitgoodies.typehandler.TypeHandler;
import de.cologneintelligence.fitgoodies.util.FitUtils;
import de.cologneintelligence.fitgoodies.valuereceivers.ValueReceiver;
import de.cologneintelligence.fitgoodies.valuereceivers.ValueReceiverFactory;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.util.*;
import java.util.regex.Matcher;
abstract public class RowFixture extends Fixture {
protected List<FitRow> missing = new LinkedList<>();
protected List<Object> surplus = new LinkedList<>();
private String[] columnParameters;
private String[] columnNames;
private FitTable table;
/**
* get rows to be compared
* @return Actual values to be compared with the HTML content.
* @throws Exception when querying for object fails.
*/
abstract protected Object[] query() throws Exception;
/**
* get expected type of row
*
* @return Class which is processed by this RowFixture.
*/
abstract protected Class getTargetClass();
@Override
public void doTable(FitTable table) {
this.table = table;
super.doTable(table);
}
protected void doRows(List<FitRow> rows) throws Exception {
FitRow header = rows.get(0);
columnParameters = extractColumnParameters(header);
columnNames = findColumnNames(header);
List<FitRow> expected = new LinkedList<>(rows.subList(1, rows.size()));
List<Object> computed = new LinkedList<>(Arrays.asList(query()));
multiLevelMatch(expected, computed, 0);
appendSurplusRows();
markMissingRows();
}
protected String[] findColumnNames(FitRow heads) {
String[] names = new String[heads.size()];
int i = 0;
for (FitCell fitCell : heads.cells()) {
names[i++] = validator.preProcess(fitCell);
}
return names;
}
/**
* Sorts and compare two lists.
* It is assumed that all items in {@code expected} and {@code computed}
* are equal on the {@code col - 1} columns.
*/
private void multiLevelMatch(List<FitRow> expected, List<Object> computed, int col) {
boolean cantGoDeeper = col >= columnNames.length;
if (cantGoDeeper) {
check(expected, computed);
} else {
boolean isComment = isComment(col);
if (isComment) {
multiLevelMatch(expected, computed, col + 1);
} else {
groupAndMatch(expected, computed, col);
}
}
}
private boolean isComment(int col) {
return columnNames[col] == null || columnNames[col].isEmpty();
}
/**
* Groups both lists by column {@code col}. For every resulting group check if
* there is a 1:1 or a 1:0 match. Otherwise, group further.
*/
private void groupAndMatch(List<FitRow> expected, List<Object> computed, int col) {
Map<Object, List<FitRow>> expectedMap = groupExpectedByColumn(expected, col);
Map<Object, List<Object>> computedMap = groupComputedByColumn(computed, col);
Set keys = union(expectedMap.keySet(), computedMap.keySet());
for (Object key : keys) {
List<FitRow> expectedList = expectedMap.get(key);
List<Object> computedList = computedMap.get(key);
boolean isAmbiguous = hasMultipleEntries(expectedList) && hasMultipleEntries(computedList);
if (isAmbiguous) {
multiLevelMatch(expectedList, computedList, col + 1);
} else {
check(expectedList, computedList);
}
}
}
private boolean hasMultipleEntries(List<?> expectedList) {
return expectedList != null && expectedList.size() > 1;
}
private Map<Object, List<FitRow>> groupExpectedByColumn(List<FitRow> list, int col) {
Map<Object, List<FitRow>> result = new HashMap<>(list.size());
for (FitRow row : list) {
List<FitCell> cells = row.cells();
FitCell keyCell = cells.get(col);
try {
Object key = parseCell(col, keyCell);
addToMap(result, key, row);
} catch (Exception e) {
keyCell.exception(e);
for (int i = col + 1; i < cells.size(); i++) {
cells.get(i).ignore();
}
}
}
return result;
}
private Object parseCell(int col, FitCell cell) throws NoSuchMethodException, NoSuchFieldException, ParseException {
String preprocessedText = validator.preProcess(cell);
String parameter = FitUtils.saveGet(col, columnParameters);
Class<?> columnType = getColumnType(columnNames[col]);
TypeHandler typeHandler = typeHandlerFactory.getHandler(columnType, parameter);
return typeHandler.parse(preprocessedText);
}
private Map<Object, List<Object>> groupComputedByColumn(List<Object> list, int col) {
Map<Object, List<Object>> result = new HashMap<>(list.size());
for (Object row : list) {
try {
Object key = createReceiver(row, columnNames[col]).get();
addToMap(result, key, row);
} catch (Exception e) {
// surplus anything with bad keys, including null
surplus.add(row);
}
}
return result;
}
private <T> void addToMap(Map<Object, List<T>> map, Object key, T row) {
if (key.getClass().isArray()) {
key = Arrays.asList((Object[]) key);
}
if (map.containsKey(key)) {
map.get(key).add(row);
} else {
List<T> list = new LinkedList<>();
list.add(row);
map.put(key, list);
}
}
private <T> Set<T> union(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<>(a);
result.addAll(b);
return result;
}
/**
* Compares two lists.
* <ul>
* <li>if {@code expectedList} is empty, all {@code computedList} items are surplus</li>
* <li>if {@code computedList} is empty, all {@code expectedList} items are missing</li>
* <li>otherwise, match the first rows and compare the rest recursively</li>
* </ul>
*
* @param computedList Objects from {@link #query()}.
* @param expectedList Objects from HTML.
*/
protected void check(List<FitRow> expectedList, List<Object> computedList) {
if (expectedList == null || expectedList.size() == 0) {
surplus.addAll(computedList);
} else if (computedList == null || computedList.size() == 0) {
missing.addAll(expectedList);
} else {
Object computedRow = computedList.remove(0);
List<FitCell> expectedRow = expectedList.remove(0).cells();
compareRow(computedRow, expectedRow);
check(expectedList, computedList);
}
}
/**
* Compares two rows item by item using {@link Fixture#check(FitCell, ValueReceiver, String)}.
* Each cell will be marked as right, wrong, ignored or exception
*/
private void compareRow(Object computedRow, List<FitCell> expectedCells) {
for (int i = 0; i < columnNames.length && expectedCells != null; i++) {
try {
ValueReceiver valueReceiver;
if (isComment(i)) {
valueReceiver = null;
} else {
valueReceiver = createReceiver(computedRow, columnNames[i]);
}
String columnParameter = FitUtils.saveGet(i, columnParameters);
check(expectedCells.get(i), valueReceiver, columnParameter);
} catch (NoSuchMethodException | NoSuchFieldException e) {
expectedCells.indexOf(e);
}
}
}
private Class<?> getColumnType(String name) throws NoSuchMethodException, NoSuchFieldException {
Matcher matcher = ValueReceiverFactory.METHOD_PATTERN.matcher(name);
if (matcher.find()) {
final String methodName = FitUtils.camel(matcher.group(1));
return getTargetClass().getMethod(methodName).getReturnType();
} else {
final Field field = getTargetClass().getField(FitUtils.camel(name));
return field.getType();
}
}
private void appendSurplusRows() {
for (Object row : surplus) {
FitRow fitRow = table.appendRow();
buildRow(fitRow, row, "surplus");
}
}
private void buildRow(FitRow fitRow, Object row, String info) {
if (row == null) {
fitRow.insert(0).blank("null", columnNames.length);
fitRow.wrong(info);
} else {
fitRow.wrong(info);
for (int i = 0; i < columnNames.length; i++) {
FitCell cell = fitRow.insert(i);
buildCell(cell, row, i);
}
}
}
private void buildCell(FitCell cell, Object row, int i) {
if (columnNames[i] == null) {
cell.ignore();
} else {
try {
ValueReceiver receiver = createReceiver(row, columnNames[i]);
String parameter = FitUtils.saveGet(i, columnParameters);
TypeHandler typeHandler = createTypeHandler(receiver, parameter);
cell.setDisplayValue(typeHandler.toString(receiver.get()));
//cell.ignore();
} catch (Exception e) {
cell.exception(e);
}
}
}
private void markMissingRows() {
markRowsAs(missing.iterator(), "missing");
}
private void markRowsAs(Iterator<FitRow> rows, String message) {
while (rows.hasNext()) {
FitRow row = rows.next();
preprocessMissingCells(row);
row.wrong(message);
}
}
private void preprocessMissingCells(FitRow row) {
for (int i = 0; i < row.size(); i++) {
FitCell cell = row.cells().get(i);
Object preprocessed = validator.preProcess(cell);
if (isComment(i)) {
cell.setDisplayValue(Objects.toString(preprocessed));
} else {
String parameter = FitUtils.saveGet(i, columnParameters);
try {
Class<?> columnType = getColumnType(columnNames[i]);
TypeHandler handler = typeHandlerFactory.getHandler(columnType, parameter);
cell.setDisplayValue(handler.toString(preprocessed));
} catch (NoSuchMethodException | NoSuchFieldException e) {
cell.setDisplayValue(Objects.toString(preprocessed));
}
}
}
}
}