// Copyright (C) 2003-2009 by Object Mentor, Inc. All rights reserved. // Released under the terms of the CPL Common Public License version 1.0. package fitnesse.slimTables; import fitnesse.responders.run.slimResponder.SlimTestContext; import util.ListUtility; import java.util.*; public class QueryTable extends SlimTable { protected List<String> fieldNames = new ArrayList<String>(); private String queryId; protected QueryResults queryResults; private String tableInstruction; public QueryTable(Table table, String id, SlimTestContext testContext) { super(table, id, testContext); } protected String getTableType() { return "queryTable"; } public boolean matches(String actual, String expected) { if (actual == null || expected == null) return false; if (actual.equals(replaceSymbols(expected))) return true; Comparator c = new Comparator(actual, expected); c.evaluate(); return c.matches(); } public String matchMessage(String actual, String expected) { if (actual == null) return "NULL"; if (actual.equals(replaceSymbols(expected))) return expected; Comparator c = new Comparator(actual, expected); return c.evaluate(); } public void appendInstructions() { if (table.getRowCount() < 2) throw new SlimTable.SyntaxError("Query tables must have at least two rows."); assignColumns(); constructFixture(); tableInstruction = callFunction(getTableName(), "table", tableAsList()); queryId = callFunction(getTableName(), "query"); } public boolean shouldIgnoreException(String resultKey, String resultString) { boolean isTableInstruction = resultKey.equals(tableInstruction); boolean isNoMethodException = resultString.indexOf("NO_METHOD_IN_CLASS") != -1; return isTableInstruction && isNoMethodException; } private void assignColumns() { int cols = table.getColumnCountInRow(1); for (int col = 0; col < cols; col++) fieldNames.add(table.getCellContents(col, 1)); } public void evaluateReturnValues(Map<String, Object> returnValues) throws Exception { Object queryReturn = returnValues.get(queryId); if (queryId == null || queryReturn == null) { table.appendToCell(0, 0, error("query method did not return a list.")); return; } else if (queryReturn instanceof String) { appendQueryErrorMessage((String) queryReturn); } else { scanRowsForMatches(ListUtility.uncheckedCast(Object.class, queryReturn)); } } private void appendQueryErrorMessage(String message) { if (isExceptionMessage(message)) table.appendToCell(0, 0, makeExeptionMessage(message)); else table.appendToCell(0, 0, error(String.format("The query method returned: %s", message))); } protected void scanRowsForMatches(List<Object> queryResultList) throws Exception { queryResults = new QueryResults(queryResultList); int rows = table.getRowCount(); for (int tableRow = 2; tableRow < rows; tableRow++) scanRowForMatch(tableRow); markSurplusRows(); } private void markSurplusRows() throws Exception { List<Integer> unmatchedRows = queryResults.getUnmatchedRows(); for (int unmatchedRow : unmatchedRows) { List<String> surplusRow = queryResults.getList(fieldNames, unmatchedRow); int newTableRow = table.addRow(surplusRow); failMessage(0, newTableRow, "surplus"); markMissingFields(surplusRow, newTableRow); } } private void markMissingFields(List<String> surplusRow, int newTableRow) { for (int col = 0; col < surplusRow.size(); col++) { String surplusField = surplusRow.get(col); if (surplusField == null) { String fieldName = fieldNames.get(col); fail(col, newTableRow, String.format("field %s not present", fieldName)); } } } protected void scanRowForMatch(int tableRow) throws Exception { int matchedRow = queryResults.findBestMatch(tableRow); if (matchedRow == -1) { replaceAllvariablesInRow(tableRow); failMessage(0, tableRow, "missing"); } else { markFieldsInMatchedRow(tableRow, matchedRow); } } protected void replaceAllvariablesInRow(int tableRow) { int columns = table.getColumnCountInRow(tableRow); for (int col = 0; col < columns; col++) { String contents = table.getCellContents(col, tableRow); table.setCell(col, tableRow, replaceSymbolsWithFullExpansion(contents)); } } protected void markFieldsInMatchedRow(int tableRow, int matchedRow) { int columns = table.getColumnCountInRow(tableRow); for (int col = 0; col < columns; col++) { markField(tableRow, matchedRow, col); } } protected void markField(int tableRow, int matchedRow, int col) { if (col >= fieldNames.size()) return; // ignore strange table geometry. String fieldName = fieldNames.get(col); String actualValue = queryResults.getCell(fieldName, matchedRow); String expectedValue = table.getCellContents(col, tableRow); if (actualValue == null) failMessage(col, tableRow, String.format("field %s not present", fieldName)); else if (expectedValue == null || expectedValue.length() == 0) ignore(col, tableRow, actualValue); else { String message = matchMessage(actualValue, expectedValue); if (message != null) table.setCell(col, tableRow, replaceSymbolsWithFullExpansion(message)); else table.setCell(col, tableRow, replaceSymbolsWithFullExpansion(expectedValue)); if (matches(actualValue, expectedValue)) markMatch(tableRow, matchedRow, col); else expected(col, tableRow, actualValue); } } protected void markMatch(int tableRow, int matchedRow, int col) { pass(col, tableRow); } class QueryResults { private List<Map<String, String>> rows = new ArrayList<Map<String, String>>(); private List<Integer> unmatchedRows = new ArrayList<Integer>(); public QueryResults(List<Object> queryResultTable) { int rowNumber = 0; for (Object row : queryResultTable) { rows.add(makeRowMap(row)); unmatchedRows.add(rowNumber++); } } @SuppressWarnings("unchecked") private Map<String, String> makeRowMap(Object row) { Map<String, String> rowMap = new HashMap<String, String>(); for (List<Object> columnPair : (List<List<Object>>) row) { String fieldName = (String) columnPair.get(0); String fieldValue = (String) columnPair.get(1); rowMap.put(fieldName, fieldValue); } return rowMap; } public int findBestMatch(int tableRow) { return new QueryMatcher().findBestMatch(tableRow); } public List<String> getList(List<String> fieldNames, int row) { List<String> result = new ArrayList<String>(); for (String name : fieldNames) result.add(rows.get(row).get(name)); return result; } public String getCell(String name, int row) { return rows.get(row).get(name); } public List<Integer> getUnmatchedRows() { return unmatchedRows; } private class QueryMatcher { private int matchDepth; private int deepestRow; private List<Integer> matchCandidates; private QueryMatcher() { matchDepth = -1; deepestRow = -1; matchCandidates = new ArrayList<Integer>(unmatchedRows); } public int findBestMatch(int tableRow) { for (int fieldIndex = 0; fieldIndex < fieldNames.size(); fieldIndex++) new FieldMatcher(fieldIndex, tableRow).eliminateRowsThatDontMatchField(); if (deepestRow >= 0) unmatchedRows.remove(unmatchedRows.indexOf(deepestRow)); return deepestRow; } class FieldMatcher { private int fieldIndex; private int tableRow; FieldMatcher(int fieldIndex, int tableRow) { this.fieldIndex = fieldIndex; this.tableRow = tableRow; } private void eliminateRowsThatDontMatchField() { String fieldName = fieldNames.get(fieldIndex); Iterator<Integer> rowIterator = matchCandidates.iterator(); while (rowIterator.hasNext()) eliminateUnmatchingRow(rowIterator, fieldName); } private void eliminateUnmatchingRow(Iterator<Integer> rowIterator, String fieldName) { int row = rowIterator.next(); String actualValue = rows.get(row).get(fieldName); String expectedValue = table.getCellContents(fieldIndex, tableRow); if (matches(actualValue, expectedValue)) { recordMatch(row); } else { rowIterator.remove(); } } private void recordMatch(int row) { if (matchDepth < fieldIndex) { matchDepth = fieldIndex; deepestRow = row; } } } } } }