View Javadoc

1   /*
2    * @(#)TableSorter.java	1.8 99/04/23
3    *
4    * Copyright (c) 1997-1999 by Sun Microsystems, Inc. All Rights Reserved.
5    *
6    * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use,
7    * modify and redistribute this software in source and binary code form,
8    * provided that i) this copyright notice and license appear on all copies of
9    * the software; and ii) Licensee does not utilize the software in a manner
10   * which is disparaging to Sun.
11   *
12   * This software is provided "AS IS," without a warranty of any kind. ALL
13   * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
14   * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
15   * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
16   * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
17   * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
18   * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
19   * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
20   * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
21   * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
22   * POSSIBILITY OF SUCH DAMAGES.
23   *
24   * This software is not designed or intended for use in on-line control of
25   * aircraft, air traffic, aircraft navigation or aircraft communications; or in
26   * the design, construction, operation or maintenance of any nuclear
27   * facility. Licensee represents and warrants that it will not use or
28   * redistribute the Software for such purposes.
29   */
30  
31  /***
32   * A sorter for TableModels. The sorter has a model (conforming to TableModel)
33   * and itself implements TableModel. TableSorter does not store or copy
34   * the data in the TableModel, instead it maintains an array of
35   * integers which it keeps the same size as the number of rows in its
36   * model. When the model changes it notifies the sorter that something
37   * has changed eg. "rowsAdded" so that its internal array of integers
38   * can be reallocated. As requests are made of the sorter (like
39   * getValueAt(row, col) it redirects them to its model via the mapping
40   * array. That way the TableSorter appears to hold another copy of the table
41   * with the rows in a different order. The sorting algorthm used is stable
42   * which means that it does not move around rows when its comparison
43   * function returns 0 to denote that they are equivalent.
44   *
45   * @version 1.8 04/23/99
46   * @author Philip Milne
47   */
48  package org.xnap.commons.gui.table;
49  
50  import java.util.ArrayList;
51  import javax.swing.event.TableModelEvent;
52  import javax.swing.event.TableModelListener;
53  import javax.swing.table.AbstractTableModel;
54  import javax.swing.table.TableModel;
55  
56  public class TableSorter extends AbstractTableModel implements SortableModel
57  {
58  
59      protected int indexes[] = new int[0];
60      protected int revIndexes[] = new int[0];
61      protected ArrayList<Integer> sortingColumns = new ArrayList<Integer>();
62      protected Order sortOrder = Order.UNSORTED;
63      /***
64       * Counts number of compares.
65       */
66      protected int compares;
67      protected int lastSortedColumn = -1;
68      protected boolean maintainSortOrder;
69  	private TableModel tableModel;
70  	private TableModelListener tableModelListener;
71  	
72      public TableSorter()
73      {
74  		this.tableModelListener = new TableModelHandler();
75      }
76  
77      public TableSorter(TableModel tableModel)
78      {
79  		this();
80  		
81  		setTableModel(tableModel);
82      }
83  
84      public TableModel getTableModel() {
85          return tableModel;
86      }
87  
88      // TableModel interface methods 
89  
90      public int getRowCount() {
91          return (tableModel == null) ? 0 : tableModel.getRowCount();
92      }
93  
94      public int getColumnCount() {
95          return (tableModel == null) ? 0 : tableModel.getColumnCount();
96      }
97  
98      public String getColumnName(int column) {
99          return tableModel.getColumnName(column);
100     }
101 
102     public Class<?> getColumnClass(int column) {
103         return tableModel.getColumnClass(column);
104     }
105 
106     public boolean isCellEditable(int row, int column) {
107         return tableModel.isCellEditable(mapToIndex(row), column);
108     }
109 
110     public Object getValueAt(int row, int column) {
111 		synchronized (indexes) {
112 			return tableModel.getValueAt(mapToIndex(row), column);
113 		}
114     }
115 
116     public void setValueAt(Object aValue, int row, int column) {
117 		synchronized (indexes) {
118 			tableModel.setValueAt(aValue, mapToIndex(row), column);
119 		}
120     }
121 
122     public int getSortedColumn()
123     {
124 		return lastSortedColumn;
125     }
126 
127     public Order getSortOrder()
128     {
129 		return sortOrder;
130     }
131 
132     /***
133      * Returns the mapped row index.
134      */
135     public int mapToIndex(int i) 
136     {
137         return indexes[i];
138     }
139 
140     public void setMaintainSortOrder(boolean newValue)
141     {
142 		maintainSortOrder = newValue;
143     }
144 
145     public void setSortOrder(Order newValue)
146     {
147     	if (newValue == null) {
148     		throw new IllegalArgumentException();
149     	}
150     	
151 		sortOrder = newValue;
152     }
153 
154     public void setTableModel(TableModel tableModel) {
155         if (this.tableModel != null) {
156             this.tableModel.removeTableModelListener(tableModelListener);
157         }
158 
159         this.tableModel = tableModel;
160         if (this.tableModel != null) {
161             this.tableModel.addTableModelListener(tableModelListener);
162         }
163 
164 		reallocateIndexes();
165         fireTableStructureChanged();
166     }
167 
168     /***
169      * Sorts the table.
170 	 *
171 	 * @return true, if table is sorted ascending; false, if descending
172      */
173     public Order sortByColumn(int column, Order sortOrder, boolean revert) 
174     {
175 		// add column
176 		if (column < 0 || column >= getColumnCount()) {
177 			throw new IllegalArgumentException("Column is invalid");
178 		}
179 
180 		sortingColumns.clear();
181 		sortingColumns.add(new Integer(column));
182 
183 		setSortOrder(sortOrder);
184 
185 		if (!sort() && revert) {
186 			setSortOrder(sortOrder.next());
187 			sort();
188 		}
189 		lastSortedColumn = column;
190 
191 		return getSortOrder();
192     }
193 
194     public void resort()
195     {
196 		if (lastSortedColumn != -1) {
197 			sortByColumn(lastSortedColumn, getSortOrder(), false);
198 		}
199     }
200 
201     /***
202      * Compares two rows.
203      */
204     protected int compare(int row1, int row2) 
205     {
206         compares++;
207         for(int level = 0; level < sortingColumns.size(); level++) {
208             int column = sortingColumns.get(level);
209             int result = compareRowsByColumn(row1, row2, column);
210             if (result != 0) {
211                 return (sortOrder == Order.ASCENDING) ? result : -result; 
212             }
213         }
214         return 0;
215     }
216 
217     /***
218      * Compares two rows by a column.
219      */
220     @SuppressWarnings("unchecked")
221 	protected int compareRowsByColumn(int row1, int row2, int column) 
222     {
223         Class type = getColumnClass(column);
224 
225         // check for nulls
226         Object o1 = tableModel.getValueAt(row1, column);
227         Object o2 = tableModel.getValueAt(row2, column);
228 
229         // If both values are null return 0
230 		// define: null is less than anything
231         if (o1 == null && o2 == null) {
232             return 0;
233         } else if (o1 == null) { 
234             return -1;
235         } else if (o2 == null) {
236             return 1;
237         }
238 
239         /* We copy all returned values from the getValue call in case
240 		   an optimised model is reusing one object to return many values.  The
241 		   Number subclasses in the JDK are immutable and so will not be used in
242 		   this way but other subclasses of Number might want to do this to save
243 		   space and avoid unnecessary heap allocation.
244 		*/
245 		if (type == String.class) {
246             String s1  = (String)o1;
247             String s2  = (String)o2;
248 			int result = s1.compareToIgnoreCase(s2);
249 
250 			// empty strings come last
251 			if (s1.length() == 0 ^ s2.length() == 0) {
252 				result = -result;
253 			}
254 
255 			return result;
256 		}
257 		else if (o1 instanceof Comparable) {
258 			return ((Comparable)o1).compareTo(o2);
259 		}
260 		/* Boolean implements Comparable, so there is no need for extra
261 		 * handling code
262 		else if (type == Boolean.class) {
263             boolean b1 = ((Boolean)o1).booleanValue();
264             boolean b2 = ((Boolean)o2).booleanValue();
265 	    
266 			// define: false < true
267             if (b1 == b2) {
268                 return 0;
269             } else if (b1) {
270                 return 1;
271             } else {
272                 return -1;
273             }
274         } 
275         */
276 		else {
277 			// now what?
278 			return o1.toString().compareTo(o2.toString());
279         }
280     }
281 
282     protected void reallocateIndexes(TableModelEvent e) 
283     {
284         int rowCount = getRowCount();
285 
286         if (rowCount == indexes.length)
287             return;
288 
289         // Set up a new array of indexes with the right number of elements
290         // for the new data model.
291         int newIndexes[] = new int[rowCount];
292         int newRevIndexes[] = new int[rowCount];
293 
294         synchronized (indexes) {
295             int row = 0;
296 
297 			if (e != null && e.getType() == TableModelEvent.DELETE) {
298 				// we need to remove the deleted lines from the index
299 				// and decrease all row numbers by the appropriate
300 				// amount
301 				int skipped = 0;
302 				for(; row < indexes.length; row++) {
303 					int dec = 0;
304 					boolean skip = false;
305 					for (int i = e.getFirstRow(); i <= e.getLastRow(); i++) {
306 						if (i < indexes[row])
307 							dec++;
308 						else if (i == indexes[row])
309 							skip = true;
310 					}
311 					if (skip) {
312 						skipped++;
313 						continue;
314 					}
315 		    
316 					newIndexes[row - skipped] = indexes[row] - dec;
317 					newRevIndexes[indexes[row] - dec] = row - skipped;
318 				}
319 			} 
320 			else if (e != null && (e.getType() == TableModelEvent.UPDATE
321 								   && e.getLastRow() >= indexes.length)) {
322 				// start from scratch
323 				for (int i = 0; i < rowCount; i++) {
324 					newIndexes[i] = i;
325 					newRevIndexes[i] = i;
326 				}
327 			}
328 			else {
329 				// Add the new rows to the end of the map, but leave the
330 				// beginning intact.
331 				for(; row < indexes.length && row < rowCount; row++) {
332 					newIndexes[row] = indexes[row];
333 					newRevIndexes[row] = revIndexes[row];
334 				}
335 				for(; row < rowCount; row++) {
336 					newIndexes[row] = row;
337 					newRevIndexes[row] = row;
338 				}
339 		
340 			}
341 	    
342 			indexes = newIndexes;
343 			revIndexes = newRevIndexes;
344 		}
345     }
346 
347     protected void reallocateIndexes() 
348     {
349 		reallocateIndexes(null);
350     }
351     
352     /***
353      * Returns false if nothing has changed.
354      */
355     protected boolean sort() 
356     {
357         synchronized (indexes) {
358         	if (sortOrder == Order.UNSORTED) {
359         		for(int i = 0; i < indexes.length; i++) {
360         			indexes[i] = i;
361         			revIndexes[i] = i;
362         		}
363         		return false;
364         	}
365         	
366 			compares = 0;
367 			//long startTime = System.currentTimeMillis();
368 
369 			// copy indexes for sorting
370 			int[] oldIndexes = new int[indexes.length];
371 			System.arraycopy(indexes, 0, oldIndexes, 0, indexes.length);
372 	    
373 			if (shuttlesort(oldIndexes, indexes, 0, indexes.length)) {
374 	    
375 				//long elapsedTime = System.currentTimeMillis() - startTime;
376 				//System.out.println("sorted with " + compares 
377 				//+ " compares in " + elapsedTime + " ms.");
378 		
379 				// update reverse indexes
380 				for(int i = 0; i < indexes.length; i++) {
381 					revIndexes[indexes[i]] = i;
382 				}
383 		
384 				fireTableChanged(new TableModelEvent(this));
385 
386 				return true;
387 			}
388 			else {
389 				return false;
390 			}
391 		}
392     }
393 
394     /***
395      * Returns false if nothing has changed.
396      */
397     // This is a home-grown implementation which we have not had time
398     // to research - it may perform poorly in some circumstances. It
399     // requires twice the space of an in-place algorithm and makes
400     // NlogN assigments shuttling the values between the two
401     // arrays. The number of compares appears to vary between N-1 and
402     // NlogN depending on the initial order but the main reason for
403     // using it here is that, unlike qsort, it is stable.
404     public boolean shuttlesort(int from[], int to[], int low, int high) 
405     {
406         if (high - low < 2) {
407             return false;
408         }
409 
410 		boolean changed = false;
411         int middle = (low + high) / 2;
412         changed |= shuttlesort(to, from, low, middle);
413         changed |= shuttlesort(to, from, middle, high);
414 
415         int p = low;
416         int q = middle;
417 
418         /* This is an optional short-cut; at each recursive call,
419 		   check to see if the elements in this subset are already
420 		   ordered.  If so, no further comparisons are needed; the
421 		   sub-array can just be copied.  The array must be copied rather
422 		   than assigned otherwise sister calls in the recursion might
423 		   get out of sinc.  When the number of elements is three they
424 		   are partitioned so that the first set, [low, mid), has one
425 		   element and and the second, [mid, high), has two. We skip the
426 		   optimisation when the number of elements is three or less as
427 		   the first compare in the normal merge will produce the same
428 		   sequence of steps. This optimisation seems to be worthwhile
429 		   for partially ordered lists but some analysis is needed to
430 		   find out how the performance drops to Nlog(N) as the initial
431 		   order diminishes - it may drop very quickly.  */
432 
433         if (high - low >= 4) {
434             if (compare(from[middle-1], from[middle]) <= 0) {
435                 for (int i = low; i < high; i++) {
436                     to[i] = from[i];
437                 }
438                 return changed;
439             } else {
440                 // an optimisation for lists with reverse order -- j.a.
441                 if (compare(from[high-1], from[low]) < 0) {
442                     int i = low;
443                     for( ; q < high; q++) {
444                         to[i++] = from[q];
445                     }
446                     for( ; i < high; i++) {
447                         to[i] = from[p++];
448                     }
449                     return changed;
450                 }
451             }
452         }
453 
454         // standard merge
455         for (int i = low; i < high; i++) {
456             if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
457                 to[i] = from[p++];
458             } 
459 			else {
460 				changed |= (p < middle);
461                 to[i] = from[q++];
462             }
463         }
464 
465 		return changed;
466     }
467 
468 	private class TableModelHandler implements TableModelListener 
469 	{
470 	    /***
471 	     * We need to mangle these events to fire the right indicies.
472 	     */
473 	    public void tableChanged(TableModelEvent e) 
474 	    {
475 			reallocateIndexes(e);
476 	
477 			if (maintainSortOrder) {
478 				// this will reset the user selection
479 				if (sort()) {
480 					// sort has already notified the super class
481 					return;
482 				}
483 			}
484 	
485 			if (e.getType() == TableModelEvent.DELETE
486 				|| (e.getType() == TableModelEvent.UPDATE && 
487 					e.getLastRow() >= revIndexes.length)) {
488 				// we don't know anymore which rows have been deleted
489 				fireTableChanged(new TableModelEvent(TableSorter.this));
490 			}
491 			else {
492 				for (int i = e.getFirstRow(); i <= e.getLastRow(); i++) {
493 					TableModelEvent t = new TableModelEvent
494 						(TableSorter.this, revIndexes[i], revIndexes[i], e.getColumn(),
495 						 e.getType());
496 					fireTableChanged(t);
497 				}
498 			}
499 	    }
500 	}
501 
502 	public boolean getMaintainSortOrder()
503 	{
504 		return maintainSortOrder;
505 	}
506 
507 }