SortedTable.java

/*
 *
 * The DbUnit Database Testing Framework
 * Copyright (C)2002-2004, DbUnit.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

package org.dbunit.dataset;

import java.util.Arrays;
import java.util.Comparator;

import org.dbunit.DatabaseUnitRuntimeException;
import org.dbunit.dataset.datatype.DataType;
import org.dbunit.dataset.datatype.TypeCastException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is a ITable decorator that provide a sorted view of the decorated table.
 * This implementation does not keep a separate copy of the decorated table
 * data.
 *
 * @author Manuel Laflamme
 * @author Last changed by: $Author$
 * @version $Revision$ $Date: 2009-05-01 02:56:07 -0500 (Fri, 01 May 2009) $
 * @since Feb 19, 2003
 */
public class SortedTable extends AbstractTable
{
    private static final Logger logger =
            LoggerFactory.getLogger(SortedTable.class);

    private final ITable _table;
    private final Column[] _columns;
    private Integer[] _indexes;

    /**
     * The row comparator which is used for sorting
     */
    private Comparator rowComparator;

    /**
     * Sort the decorated table by specified columns order. Resulting table uses
     * column definitions from the specified table's metadata, not the specified
     * columns.
     *
     * @param table
     *            decorated table
     * @param columns
     *            columns to be used for sorting
     * @throws DataSetException
     */
    public SortedTable(final ITable table, final Column[] columns)
            throws DataSetException
    {
        this(table, columns, false);
    }

    /**
     * Sort the decorated table by specified columns order. Resulting table uses
     * column definitions from the specified columns, not the ones from the
     * specified table's metadata.
     *
     * @param table
     *            decorated table
     * @param columns
     *            columns to be used for sorting
     * @param useSpecifiedColumns
     *            true to use the column definitions specified by the columns
     *            parameter, false to use the column definitions from the
     *            specified table's metadata.
     * @throws DataSetException
     */
    public SortedTable(final ITable table, final Column[] columns,
            final boolean useSpecifiedColumns) throws DataSetException
    {
        _table = table;

        final Column[] validatedColumns = validateAndResolveColumns(columns);
        if (useSpecifiedColumns)
        {
            _columns = columns;
        } else
        {
            _columns = validatedColumns;
        }

        initialize();
    }

    /**
     * Sort the decorated table by specified columns order. Resulting table uses
     * column definitions from the specified table's metadata.
     *
     * @param table
     *            decorated table
     * @param columnNames
     *            names of columns to be used for sorting
     * @throws DataSetException
     */
    public SortedTable(final ITable table, final String[] columnNames)
            throws DataSetException
    {
        _table = table;
        _columns = validateAndResolveColumns(columnNames);
        initialize();
    }

    /**
     * Sort the decorated table by specified metadata columns order. All
     * metadata columns will be used.
     *
     * @param table
     *            The decorated table
     * @param metaData
     *            The metadata used to retrieve all columns which in turn are
     *            used for sorting the table
     * @throws DataSetException
     */
    public SortedTable(final ITable table, final ITableMetaData metaData)
            throws DataSetException
    {
        this(table, metaData.getColumns());
    }

    /**
     * Sort the decorated table by its own columns order which is defined by
     * {@link ITable#getTableMetaData()}. All table columns will be used.
     *
     * @param table
     *            The decorated table
     * @throws DataSetException
     */
    public SortedTable(final ITable table) throws DataSetException
    {
        this(table, table.getTableMetaData());
    }

    /**
     * Verifies that all given columns really exist in the current table and
     * returns the physical {@link Column} objects from the table.
     *
     * @param columns
     * @return
     * @throws DataSetException
     */
    private Column[] validateAndResolveColumns(final Column[] columns)
            throws DataSetException
    {
        final ITableMetaData tableMetaData = _table.getTableMetaData();
        final Column[] resultColumns =
                Columns.findColumnsByName(columns, tableMetaData);
        return resultColumns;
    }

    /**
     * Verifies that all given columns really exist in the current table and
     * returns the physical {@link Column} objects from the table.
     *
     * @param columnNames
     * @return
     * @throws DataSetException
     */
    private Column[] validateAndResolveColumns(final String[] columnNames)
            throws DataSetException
    {
        final ITableMetaData tableMetaData = _table.getTableMetaData();
        final Column[] resultColumns =
                Columns.findColumnsByName(columnNames, tableMetaData);
        return resultColumns;
    }

    private void initialize()
    {
        logger.debug("initialize() - start");

        // The default comparator is the one that sorts by string - for
        // backwards compatibility
        this.rowComparator =
                new RowComparatorByString(this._table, this._columns);
    }

    /**
     * @return The columns that are used for sorting the table
     */
    public Column[] getSortColumns()
    {
        return this._columns;
    }

    private int getOriginalRowIndex(final int row) throws DataSetException
    {
        if (logger.isDebugEnabled())
        {
            logger.debug("getOriginalRowIndex(row={}) - start",
                    Integer.toString(row));
        }

        if (_indexes == null)
        {
            final Integer[] indexes = new Integer[getRowCount()];
            for (int i = 0; i < indexes.length; i++)
            {
                indexes[i] = new Integer(i);
            }

            try
            {
                Arrays.sort(indexes, rowComparator);
            } catch (final DatabaseUnitRuntimeException e)
            {
                throw (DataSetException) e.getCause();
            }

            _indexes = indexes;
        }

        return _indexes[row].intValue();
    }

    /**
     * Whether or not the comparable interface should be used of the compared
     * columns instead of the plain strings Default value is <code>false</code>
     * for backwards compatibility Set whether or not to use the Comparable
     * implementation of the corresponding column DataType for comparing values
     * or not. Default value is <code>false</code> which means that the old
     * string comparison is used. <br>
     *
     * @param useComparable
     * @since 2.3.0
     */
    public void setUseComparable(final boolean useComparable)
    {
        if (logger.isDebugEnabled())
        {
            logger.debug("setUseComparable(useComparable={}) - start",
                    Boolean.valueOf(useComparable));
        }

        if (useComparable)
        {
            setRowComparator(new RowComparator(this._table, this._columns));
        } else
        {
            setRowComparator(
                    new RowComparatorByString(this._table, this._columns));
        }
    }

    /**
     * Sets the comparator to be used for sorting the table rows.
     *
     * @param comparator
     *            that sorts the table rows
     * @since 2.4.2
     */
    public void setRowComparator(final Comparator comparator)
    {
        if (logger.isDebugEnabled())
        {
            logger.debug("setRowComparator(comparator={}) - start", comparator);
        }

        if (_indexes != null)
        {
            // TODO this is an ugly design to avoid increasing the number of
            // constructors from 4 to 8. To be discussed how to implement it the
            // best way.
            throw new IllegalStateException(
                    "Do not use this method after the table has been used (i.e. #getValue() has been called). "
                            + "Please invoke this method immediately after the intialization of this object.");
        }

        this.rowComparator = comparator;
    }

    // //////////////////////////////////////////////////////////////////////////
    // ITable interface

    @Override
    public ITableMetaData getTableMetaData()
    {
        logger.debug("getTableMetaData() - start");

        return _table.getTableMetaData();
    }

    @Override
    public int getRowCount()
    {
        logger.debug("getRowCount() - start");

        return _table.getRowCount();
    }

    @Override
    public Object getValue(final int row, final String columnName)
            throws DataSetException
    {
        if (logger.isDebugEnabled())
        {
            logger.debug("getValue(row={}, columnName={}) - start",
                    Integer.toString(row), columnName);
        }

        assertValidRowIndex(row);

        return _table.getValue(getOriginalRowIndex(row), columnName);
    }

    // //////////////////////////////////////////////////////////////////////////
    // Comparator interface

    /**
     * Abstract class for sorting the table rows of a given table in a specific
     * order
     */
    public static abstract class AbstractRowComparator implements Comparator
    {
        /**
         * Logger for this class
         */
        private final Logger logger =
                LoggerFactory.getLogger(AbstractRowComparator.class);
        private final ITable _table;
        private final Column[] _sortColumns;

        /**
         * @param table
         *            The wrapped table to be sorted
         * @param sortColumns
         *            The columns to be used for sorting in the given order
         */
        public AbstractRowComparator(final ITable table,
                final Column[] sortColumns)
        {
            this._table = table;
            this._sortColumns = sortColumns;
        }

        @Override
        public int compare(final Object o1, final Object o2)
        {
            logger.debug("compare(o1={}, o2={}) - start", o1, o2);

            final Integer i1 = (Integer) o1;
            final Integer i2 = (Integer) o2;

            try
            {
                for (int i = 0; i < _sortColumns.length; i++)
                {
                    final String columnName = _sortColumns[i].getColumnName();

                    final Object value1 =
                            _table.getValue(i1.intValue(), columnName);
                    final Object value2 =
                            _table.getValue(i2.intValue(), columnName);

                    if (value1 == null && value2 == null)
                    {
                        continue;
                    }

                    if (value1 == null && value2 != null)
                    {
                        return -1;
                    }

                    if (value1 != null && value2 == null)
                    {
                        return 1;
                    }

                    // Compare the two values with each other for sorting
                    final int result = compare(_sortColumns[i], value1, value2);

                    if (result != 0)
                    {
                        return result;
                    }
                }
            } catch (final DataSetException e)
            {
                throw new DatabaseUnitRuntimeException(e);
            }

            return 0;
        }

        /**
         * @param column
         *            The column to be compared
         * @param value1
         *            The first value of the given column
         * @param value2
         *            The second value of the given column
         * @return 0 if both values are considered equal.
         * @throws TypeCastException
         */
        protected abstract int compare(Column column, Object value1,
                Object value2) throws TypeCastException;

    }

    /**
     * Compares the rows with each other in order to sort them in the correct
     * order using the data type and the Comparable implementation the current
     * column has.
     */
    protected static class RowComparator extends AbstractRowComparator
    {
        /**
         * Logger for this class
         */
        private final Logger logger =
                LoggerFactory.getLogger(RowComparator.class);

        public RowComparator(final ITable table, final Column[] sortColumns)
        {
            super(table, sortColumns);
        }

        @Override
        protected int compare(final Column column, final Object value1,
                final Object value2) throws TypeCastException
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("compare(column={}, value1={}, value2={}) - start",
                        new Object[] {column, value1, value2});
            }

            final DataType dataType = column.getDataType();
            final int result = dataType.compare(value1, value2);
            return result;
        }

    }

    /**
     * Compares the rows with each other in order to sort them in the correct
     * order using the string value of both values for the comparison.
     */
    protected static class RowComparatorByString extends AbstractRowComparator
    {
        /**
         * Logger for this class
         */
        private final Logger logger =
                LoggerFactory.getLogger(RowComparatorByString.class);

        public RowComparatorByString(final ITable table,
                final Column[] sortColumns)
        {
            super(table, sortColumns);
        }

        @Override
        protected int compare(final Column column, final Object value1,
                final Object value2) throws TypeCastException
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("compare(column={}, value1={}, value2={}) - start",
                        new Object[] {column, value1, value2});
            }

            // Default behavior since ever
            final String stringValue1 = DataType.asString(value1);
            final String stringValue2 = DataType.asString(value2);
            final int result = stringValue1.compareTo(stringValue2);
            return result;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString()
    {
        final StringBuilder sb = new StringBuilder(2000);

        sb.append(getClass().getName()).append("[");
        sb.append("_columns=[").append(Arrays.toString(_columns)).append("], ");
        sb.append("_indexes=[").append(_indexes).append("], ");
        sb.append("_table=[").append(_table).append("]");
        sb.append("]");

        return sb.toString();
    }
}