001    /*
002     * Copyright 2002-2008 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.table;
017    
018    import java.awt.EventQueue;
019    import java.util.ArrayList;
020    import java.util.Arrays;
021    import java.util.Collections;
022    import java.util.Comparator;
023    import java.util.List;
024    import java.util.Map;
025    
026    import javax.swing.SwingUtilities;
027    import javax.swing.event.TableModelEvent;
028    import javax.swing.table.TableModel;
029    
030    import org.springframework.core.style.StylerUtils;
031    import org.springframework.util.Assert;
032    import org.springframework.util.comparator.NullSafeComparator;
033    
034    /**
035     * A sorter for TableModels. The sorter has a model (conforming to TableModel)
036     * and itself implements TableModel. TableSorter does not store or copy the
037     * model in the TableModel, instead it maintains an array of integers which it
038     * keeps the same size as the number of rows in its model. When the model
039     * changes it notifies the sorter that something has changed eg. "rowsAdded" so
040     * that its internal array of integers can be reallocated. As requests are made
041     * of the sorter (like getValueAt(row, col) it redirects them to its model via
042     * the mapping array. That way the TableSorter appears to hold another copy of
043     * the table with the rows in a different order. The sorting algorthm used is
044     * stable which means that it does not move around rows when its comparison
045     * function returns 0 to denote that they are equivalent.
046     */
047    public class ShuttleSortableTableModel extends AbstractTableModelFilter implements SortableTableModel {
048            private static final Comparator OBJECT_COMPARATOR = new NullSafeComparator(ToStringComparator.INSTANCE, true);
049    
050            private static final Comparator COMPARABLE_COMPARATOR = NullSafeComparator.NULLS_LOW;
051    
052            private Comparator[] columnComparators;
053    
054            private List columnsToSort = new ArrayList(4);
055    
056            private int[] indexes;
057    
058            private int compares;
059    
060            private boolean autoSortEnabled = true;
061    
062            private Runnable notifyTableRunnable = new Runnable() {
063                    public void run() {
064                            fireTableDataChanged();
065                    }
066            };
067    
068            public ShuttleSortableTableModel(TableModel model) {
069                    super(model);
070                    allocateIndexes();
071                    resetComparators();
072            }
073    
074            private void allocateIndexes() {
075                    int rowCount = filteredModel.getRowCount();
076                    // Set up a new array of indexes with the right number of elements
077                    // for the new model model.
078                    indexes = new int[rowCount];
079                    // Initialise with the identity mapping.
080                    for (int row = 0; row < rowCount; row++) {
081                            indexes[row] = row;
082                    }
083            }
084    
085            public void resetComparators() {
086                    resetComparators(Collections.EMPTY_MAP);
087            }
088    
089            /**
090             * Reset the <code>columnComparartos</code>.<br>
091             * Useful when the columns of the model were changed, and by consequence
092             * their comparators.
093             * @param comparators - map with comparators where the key is the column of
094             * the comparator.
095             */
096            public void resetComparators(Map comparators) {
097                    int colCount = filteredModel.getColumnCount();
098                    columnComparators = new Comparator[colCount];
099    
100                    Comparator newComparator;
101                    Class clazz;
102                    for (int i = 0; i < columnComparators.length; i++) {
103                            newComparator = (Comparator) comparators.get(Integer.valueOf(i));
104                            if (newComparator != null)
105                                    setComparator(i, newComparator);
106                            else {
107                                    clazz = filteredModel.getColumnClass(i);
108                                    if (clazz == Object.class || !Comparable.class.isAssignableFrom(clazz))
109                                            columnComparators[i] = OBJECT_COMPARATOR;
110                            }
111                    }
112            }
113    
114            public boolean isCellEditable(int row, int column) {
115                    return filteredModel.isCellEditable(indexes[row], column);
116            }
117    
118            public boolean isAutoSortEnabled() {
119                    return autoSortEnabled;
120            }
121    
122            public void setAutoSortEnabled(boolean autoSortEnabled) {
123                    this.autoSortEnabled = autoSortEnabled;
124            }
125    
126            public Comparator getComparator(int columnIndex) {
127                    return this.columnComparators[columnIndex];
128            }
129    
130            public void setComparator(int columnIndex, Comparator comparator) {
131                    Assert.notNull(comparator);
132                    this.columnComparators[columnIndex] = comparator;
133            }
134    
135            // The mapping only affects the contents of the model rows.
136            // Pass all requests to these rows through the mapping array: "indexes".
137            public Object getValueAt(int row, int column) {
138                    return filteredModel.getValueAt(indexes[row], column);
139            }
140    
141            public void setValueAt(Object value, int row, int column) {
142                    filteredModel.setValueAt(value, indexes[row], column);
143            }
144    
145            public void sortByColumn(ColumnToSort columnToSort) {
146                    columnsToSort.clear();
147                    columnsToSort.add(columnToSort);
148                    sort();
149                    SwingUtilities.invokeLater(notifyTableRunnable);
150            }
151    
152            public void sortByColumns(ColumnToSort[] columnsToSort) {
153                    this.columnsToSort = Arrays.asList(columnsToSort);
154                    sort();
155                    notifyTableChanged();
156            }
157    
158            public int[] sortByColumns(ColumnToSort[] columnsToSort, int[] preSortSelectedRows) {
159                    int[] modelIndexes = new int[preSortSelectedRows.length];
160                    if (logger.isDebugEnabled()) {
161                            logger.debug("Selected row indexes before sort" + StylerUtils.style(preSortSelectedRows));
162                    }
163                    for (int i = 0; i < preSortSelectedRows.length; i++) {
164                            modelIndexes[i] = convertSortedIndexToDataIndex(preSortSelectedRows[i]);
165                    }
166                    this.columnsToSort = Arrays.asList(columnsToSort);
167                    sort();
168                    int[] postSortSelectedRows = new int[modelIndexes.length];
169                    for (int i = 0; i < modelIndexes.length; i++) {
170                            postSortSelectedRows[i] = convertModelToRowIndex(modelIndexes[i]);
171                    }
172                    if (logger.isDebugEnabled()) {
173                            logger.debug("Selected row indexes after sort" + StylerUtils.style(postSortSelectedRows));
174                    }
175                    notifyTableChanged();
176                    return postSortSelectedRows;
177            }
178    
179            protected void notifyTableChanged() {
180                    if (!EventQueue.isDispatchThread()) {
181                            SwingUtilities.invokeLater(notifyTableRunnable);
182                    }
183                    else {
184                            notifyTableRunnable.run();
185                    }
186            }
187    
188            public int convertSortedIndexToDataIndex(int index) {
189                    return indexes[index];
190            }
191    
192            public int[] convertSortedIndexesToDataIndexes(int[] indexes) {
193                    int[] converted = new int[indexes.length];
194                    for (int i = 0; i < indexes.length; i++) {
195                            converted[i] = convertSortedIndexToDataIndex(indexes[i]);
196                    }
197                    return converted;
198            }
199    
200            /* this linear search is a bit slow -- we'll need to optimize later */
201            public int convertModelToRowIndex(int index) {
202                    for (int i = 0; i < indexes.length; i++) {
203                            if (index == indexes[i]) {
204                                    return i;
205                            }
206                    }
207                    return 0;
208            }
209    
210            public int[] convertDataIndexesToSortedIndexes(int[] indexes) {
211                    int[] converted = new int[indexes.length];
212                    for (int i = 0; i < indexes.length; i++) {
213                            converted[i] = convertModelToRowIndex(indexes[i]);
214                    }
215                    return converted;
216            }
217    
218            private void sort() {
219                    if (columnsToSort.size() > 0) {
220                            checkModel();
221                            compares = 0;
222                            doShuttleSort((int[]) indexes.clone(), indexes, 0, indexes.length);
223                    }
224            }
225    
226            private void checkModel() {
227                    if (indexes.length != filteredModel.getRowCount()) {
228                            throw new IllegalStateException("Sorter not informed of a change in model.");
229                    }
230            }
231    
232            // This is a home-grown implementation which we have not had time
233            // to research - it may perform poorly in some circumstances. It
234            // requires twice the space of an in-place algorithm and makes
235            // NlogN assigments shuttling the values between the two
236            // arrays. The number of compares appears to vary between N-1 and
237            // NlogN depending on the initial order but the main reason for
238            // using it here is that, unlike qsort, it is stable.
239            private void doShuttleSort(int from[], int to[], int low, int high) {
240                    if (high - low < 2) {
241                            return;
242                    }
243                    int middle = (low + high) / 2;
244                    doShuttleSort(to, from, low, middle);
245                    doShuttleSort(to, from, middle, high);
246    
247                    int p = low;
248                    int q = middle;
249    
250                    /*
251                     * This is an optional short-cut; at each recursive call, check to see
252                     * if the elements in this subset are already ordered. If so, no further
253                     * comparisons are needed; the sub-array can just be copied. The array
254                     * must be copied rather than assigned otherwise sister calls in the
255                     * recursion might get out of sinc. When the number of elements is three
256                     * they are partitioned so that the first set, [low, mid), has one
257                     * element and and the second, [mid, high), has two. We skip the
258                     * optimisation when the number of elements is three or less as the
259                     * first compare in the normal merge will produce the same sequence of
260                     * steps. This optimisation seems to be worthwhile for partially ordered
261                     * lists but some analysis is needed to find out how the performance
262                     * drops to Nlog(N) as the initial
263                     */
264                    if (high - low >= 4 && compare(from[middle - 1], from[middle]) <= 0) {
265                            for (int i = low; i < high; i++) {
266                                    to[i] = from[i];
267                            }
268                            return;
269                    }
270    
271                    // A normal merge.
272                    for (int i = low; i < high; i++) {
273                            if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
274                                    to[i] = from[p++];
275                            }
276                            else {
277                                    to[i] = from[q++];
278                            }
279                    }
280            }
281    
282            public int compare(int row1, int row2) {
283                    compares++;
284                    for (int level = 0; level < columnsToSort.size(); level++) {
285                            ColumnToSort column = (ColumnToSort) columnsToSort.get(level);
286                            int result = compareRowsByColumn(row1, row2, column.getColumnIndex());
287                            if (result != 0) {
288                                    return column.getSortOrder() == SortOrder.ASCENDING ? result : -result;
289                            }
290                    }
291                    return 0;
292            }
293    
294            private int compareRowsByColumn(int row1, int row2, int column) {
295                    Object o1 = filteredModel.getValueAt(row1, column);
296                    Object o2 = filteredModel.getValueAt(row2, column);
297    
298                    Comparator comparator = columnComparators[column];
299                    if (comparator != null) {
300                            return comparator.compare(o1, o2);
301                    }
302    
303                    return COMPARABLE_COMPARATOR.compare(o1, o2);
304            }
305    
306            public void tableChanged(final TableModelEvent e) {
307                    if (e.getType() == TableModelEvent.INSERT) {
308                            if (autoSortEnabled) {
309                                    reallocateIndexesOnInsert(e.getFirstRow(), e.getLastRow());
310                                    sort();
311                                    final int[] insertedRows = new int[e.getLastRow() - e.getFirstRow() + 1];
312                                    int row = e.getFirstRow();
313                                    for (int i = 0; i < insertedRows.length; i++) {
314                                            insertedRows[i] = convertModelToRowIndex(row++);
315                                    }
316                                    for (int i = 0; i < insertedRows.length; i++) {
317                                            fireTableRowsInserted(insertedRows[i], insertedRows[i]);
318                                    }
319                            }
320                            else {
321                                    reallocateIndexesOnInsert(e.getFirstRow(), e.getLastRow());
322                                    super.tableChanged(e);
323                            }
324                    }
325                    else if (e.getType() == TableModelEvent.DELETE) {
326                            final int[] deletedRows = new int[e.getLastRow() - e.getFirstRow() + 1];
327                            int row = e.getFirstRow();
328                            for (int i = 0; i < deletedRows.length; i++) {
329                                    deletedRows[i] = convertModelToRowIndex(row);
330                                    row++;
331                            }
332                            allocateIndexes();
333                            for (int i = 0; i < deletedRows.length; i++) {
334                                    fireTableRowsDeleted(deletedRows[i], deletedRows[i]);
335                            }
336                            sort();
337                    }
338                    else if (e.getType() == TableModelEvent.UPDATE) {
339                            allocateIndexes();
340                            sort();
341                            fireTableDataChanged();
342                    }
343                    else {
344                            logger.warn("Doing an unknown table change type: " + e.getType());
345                            allocateIndexes();
346                            sort();
347                            super.tableChanged(e);
348                    }
349            }
350    
351            private void reallocateIndexesOnInsert(int firstRow, int lastRow) {
352                    int rowCount = filteredModel.getRowCount();
353                    int[] newIndexes = new int[rowCount];
354                    for (int row = 0; row < indexes.length; row++) {
355                            newIndexes[row] = indexes[row];
356                    }
357                    for (int row = firstRow; row <= lastRow; row++) {
358                            newIndexes[row] = row;
359                    }
360                    indexes = newIndexes;
361            }
362    
363    }