rocket.widget.client.SortableTable.java Source code

Java tutorial

Introduction

Here is the source code for rocket.widget.client.SortableTable.java

Source

/*
 * Copyright Miroslav Pokorny
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package rocket.widget.client;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import rocket.event.client.MouseClickEvent;
import rocket.event.client.MouseEventAdapter;
import rocket.util.client.Checker;

import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.HTMLTable.RowFormatter;

/**
 * A powerful easy to use table that includes support for sorting individual
 * columns using Comparators provided by the user. This class uses the template
 * approach to facilitate mapping between value objects and columns.
 * 
 * The {@link #getValue(Object, int)} method is required to return the value for
 * a particular column for the given row. The {@link #getWidget(Object, int)}
 * method is required to return the Widget that will appear at the given column
 * for the given row.
 * 
 * Two additional methods must be implemented each of which fetches the
 * {@link #getAscendingSortImageSource() ascending }/
 * {@link #getDescendingSortImageSource() descending }sort images.
 * 
 * <h6>Gotchas</h6>
 * <ul>
 * 
 * <li>Column comparators should be set before adding any headers. Once headers
 * have been created it is possible to set the order (ascending/descending) for
 * any sortable column Refer to the bundle test that shows how simple it is to
 * subclass and use this implementation. </li>
 * 
 * <li> if autoRedraw (which may be set via {@link #setAutoRedraw(boolean)}) is
 * true the table will be redrawn each time the rows list is modified. </li>
 * 
 * <li> if autoRedraw (which may be set via {@link #setAutoRedraw(boolean)}) is
 * false the user must force redraws using {@link #redraw() } </li>
 * 
 * </ul>
 * {@link #redrawIfAutoEnabled()} and {@link #redrawIfAutoEnabled()} </li>
 * 
 * </ul>
 * 
 * @author Miroslav Pokorny (mP) ROCKET add generics to the list views
 */
public abstract class SortableTable<R> extends CompositeWidget {

    public SortableTable() {
        super();
    }

    @Override
    protected Widget createWidget() {
        return this.createGrid();
    }

    @Override
    protected void afterCreateWidget() {
        this.setColumnComparators(this.createColumnComparators());
        this.setRows(this.createRows());
        this.setAutoRedraw(true);

        final Grid grid = this.getGrid();
        grid.getRowFormatter().addStyleName(0, this.getHeaderRowStyle());
    }

    protected String getHeaderRowStyle() {
        return WidgetConstants.SORTABLE_TABLE_HEADER_ROW_STYLE;
    }

    @Override
    protected String getInitialStyleName() {
        return WidgetConstants.SORTABLE_TABLE_STYLE;
    }

    protected int getSunkEventsBitMask() {
        return 0;
    }

    protected Grid getGrid() {
        return (Grid) this.getWidget();
    }

    protected Grid createGrid() {
        final Grid grid = new Grid(1, this.getColumnCount());
        grid.setCellPadding(0);
        grid.setCellSpacing(0);
        return grid;
    }

    protected void setWidget(final int row, final int column, final Widget widget) {
        this.getGrid().setWidget(row, column, widget);
    }

    protected void setText(final int row, final int column, final String text) {
        this.getGrid().setText(row, column, text);
    }

    /**
     * A list of comparators one for each column or null if a column is not
     * sortable.
     */
    private ColumnSorting[] columnComparators;

    protected ColumnSorting[] getColumnComparators() {
        Checker.notNull("field:columnComparators", columnComparators);
        return columnComparators;
    }

    protected boolean hasColumnComparators() {
        return this.columnComparators != null;
    }

    protected void setColumnComparators(final ColumnSorting[] columnComparators) {
        Checker.notNull("parameter:columnComparators", columnComparators);
        this.columnComparators = columnComparators;
    }

    protected ColumnSorting[] createColumnComparators() {
        return new ColumnSorting[this.getColumnCount()];
    }

    /**
     * @param columnComparator
     * @param column
     * @param ascending
     */
    public void setColumnComparator(final Comparator columnComparator, final int column, final boolean ascending) {
        Checker.notNull("parameter:columnComparator", columnComparator);
        this.checkColumn("parameter:column", column);

        final ColumnSorting sorting = new ColumnSorting();
        sorting.setAscendingSort(ascending);
        sorting.setComparator(columnComparator);

        this.getColumnComparators()[column] = sorting;

        this.addColumnStyle(column, this.getSortableColumnStyle());
    }

    protected String getSortableColumnStyle() {
        return WidgetConstants.SORTABLE_TABLE_SORTABLE_COLUMN_STYLE;
    }

    protected int getColumnIndex(final Widget widget) {
        Checker.notNull("parameter:widget", widget);

        int columnIndex = -1;

        final Grid grid = this.getGrid();
        final int columnCount = this.getColumnCount();
        for (int i = 0; i < columnCount; i++) {
            final HorizontalPanel panel = (HorizontalPanel) grid.getWidget(0, i);
            final int index = panel.getWidgetIndex(widget);
            if (-1 != index) {
                columnIndex = i;
                break;
            }
        }

        return columnIndex;
    }

    protected void onColumnSortingClick(final Widget widget) {
        Checker.notNull("parameter:widget", widget);

        final Image image = (Image) widget;
        final int column = this.getColumnIndex(image);

        final ColumnSorting sorting = this.getColumnComparators()[column];
        final boolean newSortingOrder = !sorting.isAscendingSort();
        sorting.setAscendingSort(newSortingOrder);

        final String url = newSortingOrder ? this.getAscendingSortImageSource()
                : this.getDescendingSortImageSource();
        image.setUrl(url);

        this.setSortedColumn(column);
        this.redraw();
    }

    /**
     * Marks a particular column as not being sortable.
     * 
     * @param column
     */
    public void makeColumnUnsortable(final int column) {
        this.checkColumn("parameter:column", column);

        this.getColumnComparators()[column] = null;

        final HorizontalPanel panel = (HorizontalPanel) this.getGrid().getWidget(0, column);
        if (panel.getWidgetCount() > 1) {
            panel.remove(panel.getWidget(1));
        }
        this.removeColumnStyle(column, this.getSortableColumnStyle());
    }

    protected boolean isColumnSortable(final int column) {
        this.checkColumn("parameter:column", column);

        return null != this.getColumnComparators()[column];
    }

    /**
     * Returns a comparator which may be used to sort a particular column. It
     * also factors into whether the column is sorted in ascending or descending
     * mode.
     * 
     * @param column
     *            The column
     * @return The matching Comparator for the given column
     */
    protected Comparator getColumnComparator(final int column) {
        final ColumnSorting sorting = this.getColumnSorting(column);
        Comparator comparator = sorting.getComparator();

        if (false == sorting.isAscendingSort()) {
            final Comparator originalComparator = comparator;
            comparator = new Comparator() {
                public int compare(final Object first, final Object second) {
                    return originalComparator.compare(second, first);
                }
            };
        }

        return comparator;
    }

    protected boolean hasColumnComparator(final int column) {
        return null != this.getColumnComparators()[column];
    }

    protected boolean isAscending(final int column) {
        return this.getColumnSorting(column).isAscendingSort();
    }

    protected ColumnSorting getColumnSorting(final int column) {
        this.checkColumn("parameter:column", column);

        final ColumnSorting sorting = this.getColumnComparators()[column];
        Checker.notNull("sorting", sorting);
        return sorting;
    }

    /**
     * The currently selected sorted column
     */
    private int sortedColumn = -1;

    public int getSortedColumn() {
        Checker.greaterThanOrEqual("field:sortedColumn", 0, this.sortedColumn);
        return this.sortedColumn;
    }

    public boolean hasSortedColumn() {
        return sortedColumn != -1;
    }

    public void setSortedColumn(final int sortedColumn) {
        if (false == this.isColumnSortable(sortedColumn)) {
            Checker.fail("parameter:sortedColumn",
                    "The parameter:sortedColumn is not a sortable column. sortedColumn: " + sortedColumn);
        }

        if (this.isAutoRedraw()) {
            // remove the sorted image from the previous column...
            this.getRowsList().getSorted().setUnsorted(true);

            // remove style from the previous column
            if (this.hasSortedColumn()) {
                final int previousSortedColumn = this.getSortedColumn();
                this.removeColumnStyle(previousSortedColumn, WidgetConstants.SORTABLE_TABLE_SORTED_COLUMN_STYLE);
                this.removeSortDirectionImage(previousSortedColumn);
            }
            // add style to highlight the new sorted column.
            final boolean ascending = isAscending(sortedColumn);
            this.addSortDirectionImage(sortedColumn, ascending);

            this.addColumnStyle(sortedColumn, this.getSortedColumnStyle());
        }
        this.sortedColumn = sortedColumn;
    }

    protected String getSortedColumnStyle() {
        return WidgetConstants.SORTABLE_TABLE_SORTED_COLUMN_STYLE;
    }

    protected void removeSortDirectionImage(final int column) {
        final HorizontalPanel panel = (HorizontalPanel) this.getGrid().getWidget(0, column);
        panel.remove(1);
    }

    protected void addSortDirectionImage(final int column, final boolean ascending) {
        final String url = ascending ? this.getAscendingSortImageSource() : this.getDescendingSortImageSource();
        final Image image = new Image(url);
        image.addMouseEventListener(new MouseEventAdapter() {
            public void onClick(final MouseClickEvent mouseEvent) {
                SortableTable.this.onColumnSortingClick(mouseEvent.getWidget());
            }
        });

        final HorizontalPanel panel = (HorizontalPanel) this.getGrid().getWidget(0, column);
        panel.add(image);
    }

    protected void addColumnStyle(final int column, final String style) {
        this.checkColumn("parameter:column", column);

        final Grid table = this.getGrid();
        final CellFormatter cellFormatter = table.getCellFormatter();
        final int rows = table.getRowCount();
        for (int row = 0; row < rows; row++) {
            cellFormatter.addStyleName(row, column, style);
        }
    }

    protected void removeColumnStyle(final int column, final String style) {
        this.checkColumn("parameter:column", column);

        final Grid table = this.getGrid();
        final CellFormatter cellFormatter = table.getCellFormatter();
        final int rows = table.getRowCount();
        for (int row = 0; row < rows; row++) {
            cellFormatter.removeStyleName(row, column, style);
        }
    }

    /**
     * Rows of value objects one for each row of the table.
     */
    private RowList rows;

    public List getRows() {
        Checker.notNull("field:rows", rows);
        return rows;
    }

    public void setRows(final RowList rows) {
        Checker.notNull("parameter:rows", rows);
        this.rows = rows;
    }

    protected RowList getRowsList() {
        Checker.notNull("field:rows", rows);
        return this.rows;
    }

    /**
     * This factory returns a list that automatically takes care of updating the
     * table as rows are added, removed etc.
     */
    protected RowList createRows() {
        return new RowList();
    };

    /**
     * Returns a list view of the sorted rows. The indexes match the display.
     * 
     * This list takes a lazy approach to sorting the list of rows.
     * 
     * @return
     */
    public List getTableRows() {
        final RowList rowList = this.getRowsList();
        final SortedRowList sortedRowList = rowList.getSorted();

        return new AbstractList() {
            @Override
            public int size() {
                return rowList.size();
            }

            @Override
            public boolean add(final Object element) {
                rowList.add(element);
                return true;
            }

            @Override
            public void add(final int index, final Object element) {
                throw new UnsupportedOperationException(
                        "Adding in a specific slot is not supported, add with add( Object )");
            }

            @Override
            public Object get(final int index) {
                return sortedRowList.get(index);
            }

            @Override
            public boolean remove(final Object row) {
                return rowList.remove(row);
            }

            @Override
            public Object remove(final int index) {
                final Object removing = this.get(index);
                rowList.remove(removing);
                return removing;
            }

            // TODO make sure this only redraws the row.
            @Override
            public Object set(final int index, final Object element) {
                final Object replaced = sortedRowList.set(index, element);

                final SortedRowListElement sortedRowListElement = sortedRowList.getSortedRowListElement(index);
                sortedRowListElement.clear();

                SortableTable.this.redraw(sortedRowListElement, index);

                return replaced;
            }
        };
    }

    /**
     * This method must be implemented by sub-classes. It provides a method of
     * addressing properties for an object using an index. These details are
     * implemented by the sub-class.
     * 
     * @param row
     * @param column
     * @return The value
     */
    protected abstract Object getValue(final Object row, final int column);

    /**
     * This method returns the appropriate widget for the given value using its
     * column to distinguish the type of widget etc.
     * 
     * @param row
     * @param column
     * @return The widget at the given row/column
     */
    protected abstract Widget getWidget(final Object row, final int column);

    /**
     * Sub-classes must override this method and return the number of columns
     * the table will display.
     * 
     * @return The number of columns.
     */
    protected abstract int getColumnCount();

    /**
     * Simply asserts that the column value is valid. If not an exception is
     * thrown.
     * 
     * @param name
     * @param column
     */
    protected void checkColumn(final String name, final int column) {
        Checker.between(name, column, 0, this.getColumnCount());
    }

    /**
     * This flag controls whether this table is repainted each time a new row is
     * added or removed etc. if set to false the user must call {@link #redraw}
     * themselves.
     */
    private boolean autoRedraw;

    public boolean isAutoRedraw() {
        return this.autoRedraw;
    }

    public void setAutoRedraw(final boolean autoRedraw) {
        this.autoRedraw = autoRedraw;
    }

    protected void redrawIfAutoEnabled() {
        if (this.isAutoRedraw()) {
            this.redraw();
        }
    }

    public void redraw() {
        final SortedRowList sorted = this.getRowsList().getSorted();
        sorted.sort();
        this.redraw(sorted);
    }

    /**
     * This method does the actual translation or painting of the sorted rows.
     * 
     * @param rows
     *            A sorted list ready to be painted.
     */
    protected void redraw(final SortedRowList rows) {
        Checker.notNull("parameter:rows", rows);

        final Grid table = this.getGrid();
        final int columnCount = this.getColumnCount();
        int rowIndex = 1;
        final int rowSize = rows.size();
        final int gridRowCount = table.getRowCount();
        final int requiredGridCount = rowSize + 1;

        // update grid to match number of rows...
        table.resizeRows(requiredGridCount);

        // if grid had a few rows added add even/odd styles to them...

        final RowFormatter rowFormatter = table.getRowFormatter();
        final String evenRowStyle = this.getEvenRowStyle();
        final String oddRowStyle = this.getOddRowStyle();
        final String sortableColumnStyle = this.getSortableColumnStyle();
        final String sortedColumnStyle = this.getSortedColumnStyle();

        for (int row = gridRowCount; row < requiredGridCount; row++) {
            final String style = ((row & 1) == 1) ? evenRowStyle : oddRowStyle;
            rowFormatter.addStyleName(row, style);

            final CellFormatter cellFormatter = table.getCellFormatter();
            final int sortedColumn = this.getSortedColumn();

            for (int column = 0; column < columnCount; column++) {
                if (this.isColumnSortable(column)) {
                    cellFormatter.setStyleName(row, column, sortableColumnStyle);

                    if (sortedColumn == column) {
                        cellFormatter.addStyleName(row, column, sortedColumnStyle);
                    }
                }
            }
        }

        for (int row = 0; row < rowSize; row++) {
            final SortedRowListElement rowObject = (SortedRowListElement) rows.getSortedRowListElement(row);
            this.redraw(rowObject, rowIndex);
            rowIndex++;
        }
    }

    protected void redraw(final SortedRowListElement rowData, final int rowIndex) {
        Checker.notNull("parameter:rowData", rowData);
        Checker.greaterThanOrEqual("parameter:rowIndex", 0, rowIndex);

        final Grid table = this.getGrid();
        final int columnCount = this.getColumnCount();
        for (int column = 0; column < columnCount; column++) {
            final Widget cell = rowData.getWidget(column);
            table.setWidget(rowIndex, column, cell);
        }
    }

    protected String getEvenRowStyle() {
        return WidgetConstants.SORTABLE_TABLE_EVEN_ROW_STYLE;
    }

    protected String getOddRowStyle() {
        return WidgetConstants.SORTABLE_TABLE_ODD_ROW_STYLE;
    }

    /**
     * Creates the widget that will house the header cell
     * 
     * @param text
     *            The header text
     * @param index
     *            The column
     * @return The new widget
     */
    protected Widget createHeader(final String text, final int index) {
        final HorizontalPanel panel = new HorizontalPanel();
        panel.add(this.createLabel(text));
        return panel;
    }

    protected Label createLabel(final String text) {
        Checker.notEmpty("parameter:text", text);

        final Label label = new Label();
        label.setText(text);
        label.addMouseEventListener(new MouseEventAdapter() {
            public void onClick(final MouseClickEvent mouseEvent) {
                onHeaderClick(mouseEvent.getWidget());
            }
        });
        return label;
    }

    /**
     * Handles whenever a header is clicked (this should only be possible with
     * sortable headers).
     * 
     * @param widget
     */
    protected void onHeaderClick(final Widget widget) {
        final int column = this.getColumnIndex(widget);
        if (this.isColumnSortable(column)) {
            this.setSortedColumn(column);
            this.redraw();
        }
    }

    protected Image createSortDirectionImage() {
        final Image image = new Image();
        image.addStyleName(this.getSortDirectionArrowStyle());
        image.setUrl(this.getAscendingSortImageSource());
        return image;
    }

    protected String getSortDirectionArrowStyle() {
        return WidgetConstants.SORTABLE_TABLE_SORT_DIRECTIONS_ARROWS_STYLE;
    }

    abstract protected String getAscendingSortImageSource();

    abstract protected String getDescendingSortImageSource();

    public String toString() {
        return super.toString() + ", columnComparators: " + columnComparators + ", rows: " + rows
                + ", sortedColumn: " + sortedColumn;
    }

    /**
     * This object contains both the comparator and sorting option for a
     * particular column
     */
    static private class ColumnSorting {
        /**
         * The comparator that will be used to sort this column
         */
        private Comparator comparator;

        public Comparator getComparator() {
            Checker.notNull("field:comparator", this.comparator);
            return this.comparator;
        }

        public void setComparator(final Comparator comparator) {
            Checker.notNull("parameter:comparator", comparator);
            this.comparator = comparator;
        }

        /**
         * This flag when true indicates that this column is in ascending sort
         * mode.
         */
        private boolean ascendingSort;

        public boolean isAscendingSort() {
            return this.ascendingSort;
        }

        public void setAscendingSort(final boolean ascendingSort) {
            this.ascendingSort = ascendingSort;
        }

        @Override
        public String toString() {
            return super.toString() + ", comparator:" + comparator + ", ascendingSort: " + ascendingSort;
        }
    }

    /**
     * This list automatically takes care of refreshing of the parent
     * SortableTable
     * 
     * @author Miroslav Pokorny (mP)
     */
    class RowList extends ArrayList {

        /**
         * This list maintains a a list of sorted rows along with their widgets.
         */
        private SortedRowList sorted = new SortedRowList();

        SortedRowList getSorted() {
            Checker.notNull("field:sorted", sorted);
            return sorted;
        }

        void setSorted(final SortedRowList sorted) {
            Checker.notNull("parameter:sorted", sorted);
            this.sorted = sorted;
        }

        @Override
        public boolean add(final Object element) {
            Checker.notNull("parameter:element", element);

            if (super.contains(element)) {
                Checker.fail("parameter:element",
                        "The given element may only be added once to this list, element: " + element);
            }
            super.add(element);

            this.getSorted().add(element);

            SortableTable.this.redrawIfAutoEnabled();
            return true;
        }

        @Override
        public void add(final int index, final Object element) {
            Checker.notNull("parameter:element", element);

            if (super.contains(element)) {
                Checker.fail("parameter:element",
                        "The given element may only be added once to this list, element: " + element);
            }
            super.add(index, element);

            this.getSorted().add(element);

            SortableTable.this.redrawIfAutoEnabled();
        }

        @Override
        public boolean addAll(final Collection collection) {
            final boolean redraw = SortableTable.this.isAutoRedraw();
            SortableTable.this.setAutoRedraw(false);

            boolean modified = false;
            final Iterator iterator = collection.iterator();
            while (iterator.hasNext()) {
                this.add(iterator.next());
                modified = true;
            }

            SortableTable.this.setAutoRedraw(redraw);
            if (modified) {
                SortableTable.this.redrawIfAutoEnabled();
            }

            return modified;
        }

        @Override
        public boolean addAll(final int index, final Collection collection) {
            final boolean redraw = SortableTable.this.isAutoRedraw();
            SortableTable.this.setAutoRedraw(false);

            int index0 = index;
            final Iterator iterator = collection.iterator();
            while (iterator.hasNext()) {
                this.add(index, iterator.next());
                index0++;
            }

            SortableTable.this.setAutoRedraw(redraw);

            final boolean modified = index != index0;
            if (modified) {
                SortableTable.this.redrawIfAutoEnabled();
            }

            return modified;
        }

        @Override
        public void clear() {
            // backup autoRedraw flag
            final boolean autoRedraw = SortableTable.this.isAutoRedraw();
            SortableTable.this.setAutoRedraw(false);

            super.clear();
            this.getSorted().clear();

            // restore autoRedraw
            SortableTable.this.setAutoRedraw(autoRedraw);
            SortableTable.this.redrawIfAutoEnabled();
        }

        @Override
        public Object remove(final int index) {
            final Object removed = super.remove(index);

            // wasnt found skip updating sorted list...
            if (null != removed) {
                final SortedRowList sorted = this.getSorted();
                sorted.remove(removed);

                // redraw if necessary
                SortableTable.this.redrawIfAutoEnabled();
            }
            return removed;
        }

        @Override
        public boolean remove(final Object row) {
            final boolean removed = super.remove(row);

            if (removed) {
                final SortedRowList sorted = this.getSorted();
                sorted.remove(row);

                // redraw if necessary
                SortableTable.this.redrawIfAutoEnabled();
            }

            return removed;
        }

        @Override
        public Object set(final int index, final Object element) {
            final Object replaced = super.set(index, element);
            this.getSorted().set(index, element);
            return replaced;
        }
    };

    /**
     * This list is a sorted view of the rows that belong to RowList. Each row
     * is actually wrapped inside a SortedRowListElement, therefore all the
     * public methods wrap/unwrap the row, whilst a few additional methods are
     * available to get at the wrapper itself. This class also includes a method
     * to sort if necessary.
     */
    class SortedRowList extends ArrayList {
        /**
         * This flag indicates that the sorted list is out of sync with the rows
         * belonging to this list.
         */
        boolean unsorted = true;

        boolean isUnsorted() {
            return this.unsorted;
        }

        void setUnsorted(final boolean unsorted) {
            this.unsorted = unsorted;
        }

        void sort() {
            if (this.isUnsorted()) {
                final int sortedColumn = SortableTable.this.getSortedColumn();
                final Comparator columnComparator = SortableTable.this.getColumnComparator(sortedColumn);
                Collections.sort(
                        /**
                         * THis list returns SortedRowListElement rather than the
                         * default behaviour which unwraps the row object.
                         */
                        new AbstractList() {

                            @Override
                            public Object get(final int index) {
                                return SortedRowList.this.getSortedRowListElement(index);
                            }

                            @Override
                            public Object set(final int index, final Object element) {
                                return SortedRowList.this.setSortedRowListElement(index,
                                        (SortedRowListElement) element);
                            }

                            @Override
                            public int size() {
                                return SortedRowList.this.size();
                            }
                        }, new Comparator() {
                            /**
                             * Retrieve the row property from each SortedRowListElement
                             * and then pass that to the comparator.
                             */
                            public int compare(final Object first, final Object second) {
                                final SortedRowListElement firstElement = (SortedRowListElement) first;
                                final SortedRowListElement secondElement = (SortedRowListElement) second;
                                final Object firstValue = SortableTable.this.getValue(firstElement.getRow(),
                                        sortedColumn);
                                final Object secondValue = SortableTable.this.getValue(secondElement.getRow(),
                                        sortedColumn);
                                return columnComparator.compare(firstValue, secondValue);
                            }
                        });
            }
        }

        public boolean add(final Object row) {
            final SortedRowListElement sortedRowListElement = new SortedRowListElement(
                    SortableTable.this.getColumnCount());
            sortedRowListElement.setRow((R) row);

            super.add(sortedRowListElement);
            this.setUnsorted(true);

            return true;
        }

        @Override
        public void add(final int index, final Object row) {
            throw new UnsupportedOperationException("Adding an element in a specific slot is not supported.");
        }

        public void add(final int index, final Collection collection) {
            throw new UnsupportedOperationException("Adding a collection in a specific slot is not supported.");
        }

        @Override
        public Object get(final int index) {
            final SortedRowListElement sortedRowListElement = (SortedRowListElement) super.get(index);
            return null == sortedRowListElement ? null : sortedRowListElement.getRow();
        }

        public SortedRowListElement getSortedRowListElement(final int index) {
            return (SortedRowListElement) super.get(index);
        }

        /**
         * Assumes that the list is sorted so be careful when removing...
         */
        @Override
        public Object remove(final int index) {
            final SortedRowListElement sortedRowListElement = (SortedRowListElement) super.remove(index);
            sortedRowListElement.clear();
            return sortedRowListElement.getRow();
        }

        @Override
        public boolean remove(final Object row) {

            int index = -1;
            final int size = this.size();
            for (int i = 0; i < size; i++) {
                final SortedRowListElement element = this.getSortedRowListElement(i);
                if (row.equals(element.getRow())) {
                    index = i;
                    super.remove(index);
                    element.clear();
                    break;
                }
            }

            return -1 != index;
        }

        @Override
        public Object set(final int index, final Object row) {
            throw new UnsupportedOperationException("Replacing an element in a specific slot is not supported.");
        }

        public Object setSortedRowListElement(final int index, final SortedRowListElement element) {
            return super.set(index, element);
        }
    }

    /**
     * This object maintains a cache between a row, individual values for each
     * column, and widgets for the same column.
     * 
     * This means that the widgets for a particular row mapped for a particular
     * value object are only ever created once.
     */
    class SortedRowListElement {

        public SortedRowListElement(int columnCount) {
            super();

            this.setWidgets(new Widget[columnCount]);
        }

        /**
         * Lazily creates the widget when requested and caches for future
         * requests.
         * 
         * @param column
         *            The column
         * @return The widget
         */
        Widget getWidget(final int column) {
            final Widget[] widgets = this.getWidgets();
            Widget widget = widgets[column];
            if (null == widget) {
                widget = SortableTable.this.getWidget(this.getRow(), column);
                widgets[column] = widget;
            }
            return widget;
        }

        /**
         * This method should be called when this row is removed from the table.
         */
        void clear() {
            final Widget[] widgets = this.getWidgets();
            for (int i = 0; i < widgets.length; i++) {
                widgets[i] = null;
            }
        }

        /**
         * The source value object
         */
        private R row;

        R getRow() {
            Checker.notNull("field:row", row);
            return this.row;
        }

        void setRow(final R row) {
            Checker.notNull("parameter:row", row);
            this.row = row;
        }

        /**
         * This array holds a cache of widgets previously created for this row.
         * The SortableTable will reuse these until the row is removed. If the
         * array contains null elements it means the table has not yet been
         * redrawn.
         */
        private Widget[] widgets;

        Widget[] getWidgets() {
            Checker.notNull("field:widgets", widgets);
            return this.widgets;
        }

        void setWidgets(final Widget[] widgets) {
            Checker.notNull("parameter:widgets", widgets);
            this.widgets = widgets;
        }
    }
}