package dbfit.fixture; import dbfit.api.DbObject; import dbfit.util.*; import fit.Binding; import fit.Fixture; import fit.Parse; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static dbfit.util.Direction.*; /** * this class handles all cases where a statement should be executed for each row with * given inputs and verifying optional outputs or exceptions. it also handles a special case * when just a single statement is executed without binding parameters to columns. Examples are * - Inserting data into tables/views * - Executing statements * - Updates * - Stored procedures/functions * <p/> * the object under test is defined by overriding getTargetObject. Unfortunately, because of the way FIT * instantiates fixtures, passing in an object using a constructor and aggregation simply doesn't do the trick * so users have to extend this fixture. */ public abstract class DbObjectExecutionFixture extends Fixture { private DbParameterAccessors accessors = new DbParameterAccessors(); private Map<DbParameterAccessor, Binding> columnBindings; private StatementExecution execution; private DbObject dbObject; // intentionally private, subclasses should extend getTargetObject /** * override this method to control whether an exception is expected or not. By default, expects no exception to happen */ protected ExpectedBehaviour getExpectedBehaviour() { return ExpectedBehaviour.NO_EXCEPTION; } /** * override this method and supply the expected exception number, if one is expected */ protected int getExpectedErrorCode() { return 0; } /** * override this method and supply the dbObject implementation that will be executed for each row */ protected abstract DbObject getTargetDbObject() throws SQLException; /** * executes the target dbObject for all rows of the table. if no rows are specified, executes * the target object only once */ public void doRows(Parse rows) { try { dbObject = getTargetDbObject(); if (dbObject == null) throw new Error("DB Object not specified!"); if (rows == null) {//single execution, no args try (StatementExecution preparedStatement = dbObject.buildPreparedStatement(accessors.toArray())) { preparedStatement.run(); return; } } List<String> columnNames = getColumnNamesFrom(rows.parts); accessors = getAccessors(rows.parts, columnNames); if (accessors == null) return;// error reading args columnBindings = getColumnBindings(); try (StatementExecution preparedStatement = dbObject.buildPreparedStatement(accessors.toArray())) { execution = preparedStatement; Parse row = rows; while ((row = row.more) != null) { runRow(row); } } } catch (Throwable e) { e.printStackTrace(); if (rows == null) { throw new Error(e); } exception(rows.parts, e); } } /** * does the column name map to an output argument */ private static boolean isOutput(String name) { return name.endsWith("?"); } private List<String> getColumnNamesFrom(Parse headerCells) { List<String> columnNames = new ArrayList<String>(); for (; headerCells != null; headerCells = headerCells.more) { columnNames.add(headerCells.text()); } return columnNames; } private DbParameterAccessors getAccessors(Parse headerRow, List<String> columnNames) throws SQLException { try { return getAccessors(columnNames); } catch (IllegalArgumentException e) { exception(headerRow, e); return null; } } /** * initialise db parameters for the dbObject based on table header cells */ private DbParameterAccessors getAccessors(List<String> columnNames) throws SQLException { DbParameterAccessors accessors = new DbParameterAccessors(); for (String name : columnNames) { DbParameterAccessor accessor = dbObject.getDbParameterAccessor(name, isOutput(name) ? OUTPUT : INPUT); if (accessor == null) { throw new IllegalArgumentException("Parameter/column " + name + " not found"); } accessors.add(accessor); } return accessors; } /** * bind db accessors to columns based on the text in the header */ private Map<DbParameterAccessor, Binding> getColumnBindings() throws Exception { Map<DbParameterAccessor, Binding> bindings = new HashMap<DbParameterAccessor, Binding>(); for (DbParameterAccessor accessor : accessors.toArray()) { Binding binding = (accessor.hasDirection(INPUT) ? new SymbolAccessSetBinding() : new SymbolAccessQueryBinding()); binding.adapter = new DbParameterAccessorTypeAdapter(accessor, this); bindings.put(accessor, binding); } return bindings; } /** * execute a single row */ private void runRow(Parse row) throws Throwable { //first set input params Map<DbParameterAccessor, Parse> cellMap = accessors.zipWith(asCellList(row)); for (DbParameterAccessor inputAccessor : accessors.getInputAccessors()) { Parse cell = cellMap.get(inputAccessor); columnBindings.get(inputAccessor).doCell(this, cell); } if (getExpectedBehaviour() == ExpectedBehaviour.NO_EXCEPTION) { executeStatementAndEvaluateOutputs(row); } else { executeStatementExpectingException(row); } } private void executeStatementExpectingException(Parse row) throws Exception { try { execution.run(); wrong(row); } catch (SQLException e) { e.printStackTrace(); // all good, exception expected if (getExpectedBehaviour() == ExpectedBehaviour.ANY_EXCEPTION) { right(row); } else { int realError = e.getErrorCode(); if (realError == getExpectedErrorCode()) right(row); else { wrong(row); row.parts.addToBody(fit.Fixture.gray(" got error code " + realError)); } } } } private void executeStatementAndEvaluateOutputs(Parse row) throws SQLException, Throwable { execution.run(); Map<DbParameterAccessor, Parse> cellMap = accessors.zipWith(asCellList(row)); for (DbParameterAccessor outputAccessor : accessors.getOutputAccessors()) { Parse cell = cellMap.get(outputAccessor); columnBindings.get(outputAccessor).doCell(this, cell); } } private List<Parse> asCellList(Parse row) { List<Parse> cells = new ArrayList<Parse>(); for (Parse cell = row.parts; cell != null; cell = cell.more) { cells.add(cell); } return cells; } }