DbUnitAssertBase.java

package org.dbunit.assertion;

import java.util.Arrays;
import java.util.Map;

import org.dbunit.DatabaseUnitException;
import org.dbunit.assertion.DbUnitAssert.ComparisonColumn;
import org.dbunit.assertion.comparer.value.DefaultValueComparerDefaults;
import org.dbunit.assertion.comparer.value.ValueComparer;
import org.dbunit.assertion.comparer.value.ValueComparerDefaults;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.Columns;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ITableMetaData;
import org.dbunit.dataset.datatype.DataType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base class for DbUnit assert classes containing common methods.
 *
 * @author Jeff Jensen
 * @since 2.6.0
 */
public class DbUnitAssertBase
{
    private final Logger log = LoggerFactory.getLogger(DbUnitAssertBase.class);

    private FailureFactory junitFailureFactory = getJUnitFailureFactory();

    protected ValueComparerDefaults valueComparerDefaults =
            new DefaultValueComparerDefaults();

    /**
     * @return The default failure handler
     * @since 2.4
     */
    protected FailureHandler getDefaultFailureHandler()
    {
        return getDefaultFailureHandler(null);
    }

    /**
     * @return The default failure handler
     * @since 2.4
     */
    protected FailureHandler getDefaultFailureHandler(
            final Column[] additionalColumnInfo)
    {
        final DefaultFailureHandler failureHandler =
                new DefaultFailureHandler(additionalColumnInfo);
        if (junitFailureFactory != null)
        {
            failureHandler.setFailureFactory(junitFailureFactory);
        }
        return failureHandler;
    }

    /**
     * @return the JUnitFailureFactory if JUnit is on the classpath or
     *         <code>null</code> if JUnit is not on the classpath.
     */
    private FailureFactory getJUnitFailureFactory()
    {
        try
        {
            Class.forName("junit.framework.Assert");
            // JUnit available
            return new JUnitFailureFactory();
        } catch (final ClassNotFoundException e)
        {
            // JUnit not available on the classpath return null
            log.debug("JUnit does not seem to be on the classpath. " + e);
        }
        return null;
    }

    /**
     * @param expectedTableName
     * @param expectedColumns
     * @param actualColumns
     * @param failureHandler
     *            The {@link FailureHandler} to be used when no datatype can be
     *            determined
     * @return The columns to be used for the assertion, including the correct
     *         datatype
     * @since 2.4
     */
    protected ComparisonColumn[] getComparisonColumns(
            final String expectedTableName, final Column[] expectedColumns,
            final Column[] actualColumns, final FailureHandler failureHandler)
    {
        final ComparisonColumn[] result =
                new ComparisonColumn[expectedColumns.length];

        for (int j = 0; j < expectedColumns.length; j++)
        {
            final Column expectedColumn = expectedColumns[j];
            final Column actualColumn = actualColumns[j];
            result[j] = new ComparisonColumn(expectedTableName, expectedColumn,
                    actualColumn, failureHandler);
        }
        return result;
    }

    /**
     * Method to last-minute intercept the comparison of a single expected and
     * actual value. Designed to be overridden in order to skip cell comparison
     * by specific cell values.
     *
     * @param columnName
     *            The column being compared
     * @param expectedValue
     *            The expected value to be compared
     * @param actualValue
     *            The actual value to be compared
     * @return <code>false</code> always so that the comparison is never skipped
     * @since 2.4
     */
    protected boolean skipCompare(final String columnName,
            final Object expectedValue, final Object actualValue)
    {
        return false;
    }

    protected FailureHandler determineFailureHandler(
            final FailureHandler failureHandler)
    {
        final FailureHandler validFailureHandler;

        if (failureHandler == null)
        {
            log.debug("FailureHandler is null. Using default implementation");
            validFailureHandler = getDefaultFailureHandler();
        } else
        {
            validFailureHandler = failureHandler;
        }

        return validFailureHandler;
    }

    protected boolean compareRowCounts(final ITable expectedTable,
            final ITable actualTable, final FailureHandler failureHandler,
            final String expectedTableName) throws Error
    {
        boolean isTablesEmpty;

        final int expectedRowsCount = expectedTable.getRowCount();
        int actualRowsCount = 0;
        boolean skipRowComparison = false;
        try
        {
            actualRowsCount = actualTable.getRowCount();
        } catch (final UnsupportedOperationException exception)
        {
            skipRowComparison = true;
        }

        if (skipRowComparison)
        {
            isTablesEmpty = false;
        } else
        {
            if (expectedRowsCount != actualRowsCount)
            {
                final String msg =
                        "row count (table=" + expectedTableName + ")";
                final Error error = failureHandler.createFailure(msg,
                        String.valueOf(expectedRowsCount),
                        String.valueOf(actualRowsCount));
                log.error(error.toString());
                throw error;
            }

            // if both tables are empty, it is not necessary to compare columns,
            // as such comparison can fail if column metadata is different
            // (which could occurs when comparing empty tables)
            if (expectedRowsCount == 0 && actualRowsCount == 0)
            {
                log.debug("Tables are empty, hence equals.");
                isTablesEmpty = true;
            } else
            {
                isTablesEmpty = false;
            }
        }

        return isTablesEmpty;
    }

    protected void compareColumns(final Column[] expectedColumns,
            final Column[] actualColumns, final ITableMetaData expectedMetaData,
            final ITableMetaData actualMetaData,
            final FailureHandler failureHandler) throws DataSetException, Error
    {
        final Columns.ColumnDiff columnDiff =
                Columns.getColumnDiff(expectedMetaData, actualMetaData);
        if (columnDiff.hasDifference())
        {
            final String message = columnDiff.getMessage();
            final Error error = failureHandler.createFailure(message,
                    Columns.getColumnNamesAsString(expectedColumns),
                    Columns.getColumnNamesAsString(actualColumns));
            log.error(error.toString());
            throw error;
        }
    }

    protected void compareTableCounts(final String[] expectedNames,
            final String[] actualNames, final FailureHandler failureHandler)
            throws Error
    {
        if (expectedNames.length != actualNames.length)
        {
            throw failureHandler.createFailure("table count",
                    String.valueOf(expectedNames.length),
                    String.valueOf(actualNames.length));
        }
    }

    protected void compareTableNames(final String[] expectedNames,
            final String[] actualNames, final FailureHandler failureHandler)
            throws Error
    {
        for (int i = 0; i < expectedNames.length; i++)
        {
            if (!actualNames[i].equals(expectedNames[i]))
            {
                throw failureHandler.createFailure("tables",
                        Arrays.asList(expectedNames).toString(),
                        Arrays.asList(actualNames).toString());
            }
        }
    }

    protected String[] getSortedTableNames(final IDataSet dataSet)
            throws DataSetException
    {
        log.debug("getSortedTableNames(dataSet={}) - start", dataSet);

        final String[] names = dataSet.getTableNames();
        if (!dataSet.isCaseSensitiveTableNames())
        {
            for (int i = 0; i < names.length; i++)
            {
                names[i] = names[i].toUpperCase();
            }
        }
        Arrays.sort(names);
        return names;
    }

    /**
     * Asserts the two specified {@link IDataSet}s comparing their columns using
     * the specified columnValueComparers or defaultValueComparer and handles
     * failures using the specified failureHandler. This method ignores the
     * table names, the columns order, the columns data type, and which columns
     * are composing the primary keys.
     *
     * @param expectedDataSet
     *            {@link IDataSet} containing all expected results.
     * @param actualDataSet
     *            {@link IDataSet} containing all actual results.
     * @param failureHandler
     *            The failure handler used if the assert fails because of a data
     *            mismatch. Provides some additional information that may be
     *            useful to quickly identify the rows for which the mismatch
     *            occurred (for example by printing an additional primary key
     *            column). Can be <code>null</code>.
     * @param defaultValueComparer
     *            {@link ValueComparer} to use with column value comparisons
     *            when the column name for the table is not in the
     *            tableColumnValueComparers {@link Map}. Can be
     *            <code>null</code> and will default to
     *            {@link #getDefaultValueComparer()}.
     * @param tableColumnValueComparers
     *            {@link Map} of {@link ValueComparer}s to use for specific
     *            tables and columns. Key is table name, value is {@link Map} of
     *            column name in the table to {@link ValueComparer}s. Can be
     *            <code>null</code> and will default to using
     *            {@link #getDefaultColumnValueComparerMapForTable(String)} or,
     *            if that is empty, defaultValueComparer for all columns in all
     *            tables.
     * @throws DatabaseUnitException
     */
    public void assertWithValueComparer(final IDataSet expectedDataSet,
            final IDataSet actualDataSet, final FailureHandler failureHandler,
            final ValueComparer defaultValueComparer,
            final Map<String, Map<String, ValueComparer>> tableColumnValueComparers)
            throws DatabaseUnitException
    {
        log.debug(
                "assertWithValueComparer(expectedDataSet={}, actualDataSet={},"
                        + " failureHandler={}, defaultValueComparer={},"
                        + " tableColumnValueComparers={}) - start",
                expectedDataSet, actualDataSet, failureHandler,
                defaultValueComparer, tableColumnValueComparers);

        // do not continue if same instance
        if (expectedDataSet == actualDataSet)
        {
            log.debug("The given datasets reference the same object."
                    + " Skipping comparisons.");
            return;
        }

        final FailureHandler validFailureHandler =
                determineFailureHandler(failureHandler);

        final String[] expectedNames = getSortedTableNames(expectedDataSet);
        final String[] actualNames = getSortedTableNames(actualDataSet);

        compareTableCounts(expectedNames, actualNames, validFailureHandler);

        // table names in no specific order
        compareTableNames(expectedNames, actualNames, validFailureHandler);

        compareTables(expectedDataSet, actualDataSet, expectedNames,
                validFailureHandler, defaultValueComparer,
                tableColumnValueComparers);
    }

    protected void compareTables(final IDataSet expectedDataSet,
            final IDataSet actualDataSet, final String[] expectedNames,
            final FailureHandler failureHandler,
            final ValueComparer defaultValueComparer,
            final Map<String, Map<String, ValueComparer>> tableColumnValueComparers)
            throws DatabaseUnitException
    {
        final Map<String, Map<String, ValueComparer>> validTableColumnValueComparers =
                determineValidTableColumnValueComparers(
                        tableColumnValueComparers);

        for (int i = 0; i < expectedNames.length; i++)
        {
            final String tableName = expectedNames[i];

            final ITable expectedTable = expectedDataSet.getTable(tableName);
            final ITable actualTable = actualDataSet.getTable(tableName);
            final Map<String, ValueComparer> columnValueComparers =
                    validTableColumnValueComparers.get(tableName);

            assertWithValueComparer(expectedTable, actualTable, failureHandler,
                    defaultValueComparer, columnValueComparers);
        }
    }

    /**
     * Asserts the two specified {@link ITable}s comparing their columns using
     * the specified columnValueComparers or defaultValueComparer and handles
     * failures using the specified failureHandler. This method ignores the
     * table names, the columns order, the columns data type, and which columns
     * are composing the primary keys.
     *
     * @param expectedTable
     *            {@link ITable} containing all expected results.
     * @param actualTable
     *            {@link ITable} containing all actual results.
     * @param failureHandler
     *            The failure handler used if the assert fails because of a data
     *            mismatch. Provides some additional information that may be
     *            useful to quickly identify the rows for which the mismatch
     *            occurred (for example by printing an additional primary key
     *            column). Can be <code>null</code>.
     * @param defaultValueComparer
     *            {@link ValueComparer} to use with column value comparisons
     *            when the column name for the table is not in the
     *            columnValueComparers {@link Map}. Can be <code>null</code> and
     *            will default to {@link #getDefaultValueComparer()}.
     * @param columnValueComparers
     *            {@link Map} of {@link ValueComparer}s to use for specific
     *            columns. Key is column name in the table, value is
     *            {@link ValueComparer} to use in comparing expected to actual
     *            column values. Can be <code>null</code> and will default to
     *            using
     *            {@link #getDefaultColumnValueComparerMapForTable(String)} or,
     *            if that is empty, defaultValueComparer for all columns in the
     *            table.
     * @throws DatabaseUnitException
     */
    public void assertWithValueComparer(final ITable expectedTable,
            final ITable actualTable, final FailureHandler failureHandler,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
            throws DatabaseUnitException
    {
        log.trace("assertWithValueComparer(expectedTable, actualTable,"
                + " failureHandler, defaultValueComparer,"
                + " columnValueComparers) - start");
        log.debug("assertWithValueComparer: expectedTable={}", expectedTable);
        log.debug("assertWithValueComparer: actualTable={}", actualTable);
        log.debug("assertWithValueComparer: failureHandler={}", failureHandler);
        log.debug("assertWithValueComparer: defaultValueComparer={}",
                defaultValueComparer);
        log.debug("assertWithValueComparer: columnValueComparers={}",
                columnValueComparers);

        // Do not continue if same instance
        if (expectedTable == actualTable)
        {
            log.debug("The given tables reference the same object."
                    + " Skipping comparisons.");
            return;
        }

        final FailureHandler validFailureHandler =
                determineFailureHandler(failureHandler);

        final ITableMetaData expectedMetaData =
                expectedTable.getTableMetaData();
        final ITableMetaData actualMetaData = actualTable.getTableMetaData();
        final String expectedTableName = expectedMetaData.getTableName();

        final boolean isTablesEmpty = compareRowCounts(expectedTable,
                actualTable, validFailureHandler, expectedTableName);
        if (isTablesEmpty)
        {
            return;
        }

        // Put the columns into the same order
        final Column[] expectedColumns =
                Columns.getSortedColumns(expectedMetaData);
        final Column[] actualColumns = Columns.getSortedColumns(actualMetaData);

        // Verify columns
        compareColumns(expectedColumns, actualColumns, expectedMetaData,
                actualMetaData, validFailureHandler);

        // Get the datatypes to be used for comparing the sorted columns
        final ComparisonColumn[] comparisonCols =
                getComparisonColumns(expectedTableName, expectedColumns,
                        actualColumns, validFailureHandler);

        // Finally compare the data
        compareData(expectedTable, actualTable, comparisonCols,
                validFailureHandler, defaultValueComparer,
                columnValueComparers);
    }

    /**
     * @param expectedTable
     *            Table containing all expected results.
     * @param actualTable
     *            Table containing all actual results.
     * @param comparisonCols
     *            The columns to be compared, also including the correct
     *            {@link DataType}s for comparison
     * @param failureHandler
     *            The failure handler used if the assert fails because of a data
     *            mismatch. Provides some additional information that may be
     *            useful to quickly identify the rows for which the mismatch
     *            occurred (for example by printing an additional primary key
     *            column). Must not be <code>null</code> at this stage
     * @throws DataSetException
     * @since 2.4
     */
    protected void compareData(final ITable expectedTable,
            final ITable actualTable, final ComparisonColumn[] comparisonCols,
            final FailureHandler failureHandler) throws DataSetException
    {
        final ValueComparer defaultValueComparer = null;
        final Map<String, ValueComparer> columnValueComparers = null;
        try
        {
            compareData(expectedTable, actualTable, comparisonCols,
                    failureHandler, defaultValueComparer, columnValueComparers);
        } catch (final DatabaseUnitException e)
        {
            // not-private method, signature change breaks compatability
            throw new DataSetException(e);
        }
    }

    /**
     * @param expectedTable
     *            {@link ITable} containing all expected results.
     * @param actualTable
     *            {@link ITable} containing all actual results.
     * @param comparisonCols
     *            The columns to be compared, also including the correct
     *            {@link DataType}s for comparison
     * @param failureHandler
     *            The failure handler used if the assert fails because of a data
     *            mismatch. Provides some additional information that may be
     *            useful to quickly identify the rows for which the mismatch
     *            occurred (for example by printing an additional primary key
     *            column). Must not be <code>null</code> at this stage.
     * @param defaultValueComparer
     *            {@link ValueComparer} to use with column value comparisons
     *            when the column name for the table is not in the
     *            columnValueComparers {@link Map}. Can be <code>null</code> and
     *            will default to {@link #getDefaultValueComparer()}.
     * @param columnValueComparers
     *            {@link Map} of {@link ValueComparer}s to use for specific
     *            columns. Key is column name in the table, value is
     *            {@link ValueComparer} to use in comparing expected to actual
     *            column values. Can be <code>null</code> and will default to
     *            using
     *            {@link #getDefaultColumnValueComparerMapForTable(String)} or,
     *            if that is empty, defaultValueComparer for all columns in the
     *            table.
     * @throws DataSetException
     * @since 2.4
     * @since 2.6.0
     */
    protected void compareData(final ITable expectedTable,
            final ITable actualTable, final ComparisonColumn[] comparisonCols,
            final FailureHandler failureHandler,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
            throws DatabaseUnitException
    {
        log.debug(
                "compareData(expectedTable={}, actualTable={}, "
                        + "comparisonCols={}, failureHandler={},"
                        + " defaultValueComparer={}, columnValueComparers={})"
                        + " - start",
                expectedTable, actualTable, comparisonCols, failureHandler,
                defaultValueComparer, columnValueComparers);

        if (expectedTable == null)
        {
            throw new IllegalArgumentException(
                    "The parameter 'expectedTable' is null");
        }
        if (actualTable == null)
        {
            throw new IllegalArgumentException(
                    "The parameter 'actualTable' is null");
        }
        if (comparisonCols == null)
        {
            throw new IllegalArgumentException(
                    "The parameter 'comparisonCols' is null");
        }
        if (failureHandler == null)
        {
            throw new IllegalArgumentException(
                    "The parameter 'failureHandler' is null");
        }

        final ValueComparer validDefaultValueComparer =
                determineValidDefaultValueComparer(defaultValueComparer);
        final String expectedTableName =
                expectedTable.getTableMetaData().getTableName();
        final Map<String, ValueComparer> validColumnValueComparers =
                determineValidColumnValueComparers(columnValueComparers,
                        expectedTableName);

        // iterate over all rows
        for (int rowNum = 0; rowNum < expectedTable.getRowCount(); rowNum++)
        {
            // iterate over all columns of the current row
            final int columnCount = comparisonCols.length;
            for (int columnNum = 0; columnNum < columnCount; columnNum++)
            {
                compareData(expectedTable, actualTable, comparisonCols,
                        failureHandler, validDefaultValueComparer,
                        validColumnValueComparers, rowNum, columnNum);
            }
        }
    }

    protected void compareData(final ITable expectedTable,
            final ITable actualTable, final ComparisonColumn[] comparisonCols,
            final FailureHandler failureHandler,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers,
            final int rowNum, final int columnNum) throws DatabaseUnitException
    {
        final ComparisonColumn compareColumn = comparisonCols[columnNum];

        final String columnName = compareColumn.getColumnName();
        final DataType dataType = compareColumn.getDataType();

        final Object expectedValue = expectedTable.getValue(rowNum, columnName);
        final Object actualValue = actualTable.getValue(rowNum, columnName);

        // Compare the values
        if (skipCompare(columnName, expectedValue, actualValue))
        {
            log.trace(
                    "skipCompare: ignoring comparison" + " {}={} on column={}",
                    expectedValue, actualValue, columnName);
        } else
        {
            final ValueComparer valueComparer = determineValueComparer(
                    columnName, defaultValueComparer, columnValueComparers);

            log.debug(
                    "compareData: comparing actualValue={}"
                            + " to expectedValue={} with valueComparer={}",
                    actualValue, expectedValue, valueComparer);
            final String failMessage =
                    valueComparer.compare(expectedTable, actualTable, rowNum,
                            columnName, dataType, expectedValue, actualValue);

            failIfNecessary(expectedTable, actualTable, failureHandler, rowNum,
                    columnName, expectedValue, actualValue, failMessage);
        }
    }

    protected void failIfNecessary(final ITable expectedTable,
            final ITable actualTable, final FailureHandler failureHandler,
            final int rowNum, final String columnName,
            final Object expectedValue, final Object actualValue,
            final String failMessage)
    {
        if (failMessage != null)
        {
            final Difference diff = new Difference(expectedTable, actualTable,
                    rowNum, columnName, expectedValue, actualValue,
                    failMessage);

            failureHandler.handle(diff);
        }
    }

    protected ValueComparer determineValueComparer(final String columnName,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
    {
        ValueComparer valueComparer = columnValueComparers.get(columnName);
        if (valueComparer == null)
        {
            log.debug(
                    "determineValueComparer: using defaultValueComparer='{}'"
                            + " as columnName='{}' not found"
                            + " in columnValueComparers='{}'",
                    defaultValueComparer, columnName, columnValueComparers);
            valueComparer = defaultValueComparer;
        }

        return valueComparer;
    }

    protected ValueComparer determineValidDefaultValueComparer(
            final ValueComparer defaultValueComparer)
    {
        final ValueComparer validValueComparer;

        if (defaultValueComparer == null)
        {
            validValueComparer =
                    valueComparerDefaults.getDefaultValueComparer();
            log.debug(
                    "determineValidDefaultValueComparer:"
                            + " using getDefaultValueComparer()={}"
                            + " as defaultValueComparer={}",
                    validValueComparer, defaultValueComparer);
        } else
        {
            validValueComparer = defaultValueComparer;
        }

        return validValueComparer;
    }

    protected Map<String, Map<String, ValueComparer>> determineValidTableColumnValueComparers(
            final Map<String, Map<String, ValueComparer>> tableColumnValueComparers)
    {
        final Map<String, Map<String, ValueComparer>> validMap;

        if (tableColumnValueComparers == null)
        {
            validMap = valueComparerDefaults
                    .getDefaultTableColumnValueComparerMap();
            log.debug(
                    "determineValidTableColumnValueComparers:"
                            + " using getDefaultTableColumnValueComparerMap()={}"
                            + " as tableColumnValueComparers={}",
                    validMap, tableColumnValueComparers);
        } else
        {
            validMap = tableColumnValueComparers;
        }

        return validMap;
    }

    protected Map<String, ValueComparer> determineValidColumnValueComparers(
            final Map<String, ValueComparer> columnValueComparers,
            final String tableName)
    {
        final Map<String, ValueComparer> validMap;

        if (columnValueComparers == null)
        {
            validMap = valueComparerDefaults
                    .getDefaultColumnValueComparerMapForTable(tableName);
            log.debug(
                    "determineValidColumnValueComparers:"
                            + " using getDefaultValueComparerMap()={}"
                            + " as columnValueComparers={} for tableName={}",
                    validMap, columnValueComparers, tableName);
        } else
        {
            validMap = columnValueComparers;
        }

        return validMap;
    }

    public void setValueComparerDefaults(
            final ValueComparerDefaults valueComparerDefaults)
    {
        this.valueComparerDefaults = valueComparerDefaults;
    }
}