1 /*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License, Version 1.0 only
6 * (the "License"). You may not use this file except in compliance
7 * with the License.
8 *
9 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 * or http://www.opensolaris.org/os/licensing.
11 * See the License for the specific language governing permissions
12 * and limitations under the License.
13 *
14 * When distributing Covered Code, include this CDDL HEADER in each
15 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 * If applicable, add the following below this CDDL HEADER, with the
17 * fields enclosed by brackets "[]" replaced with your own identifying
18 * information: Portions Copyright [yyyy] [name of copyright owner]
19 *
20 * CDDL HEADER END
21 */
22 /*
23 * Copyright 2000 Sun Microsystems, Inc. All rights reserved.
24 * Use is subject to license terms.
25 *
26 * ident "%Z%%M% %I% %E% SMI"
27 */
28 package com.sun.dhcpmgr.ui;
29
30 /*
31 * @(#)TableSorter.java 1.5 97/12/17
32 */
33
34 /**
35 * A sorter for TableModels. The sorter has a model (conforming to TableModel)
36 * and itself implements TableModel. TableSorter does not store or copy
37 * the data in the TableModel, instead it maintains an array of
38 * integers which it keeps the same size as the number of rows in its
39 * model. When the model changes it notifies the sorter that something
40 * has changed eg. "rowsAdded" so that its internal array of integers
41 * can be reallocated. As requests are made of the sorter (like
42 * getValueAt(row, col) it redirects them to its model via the mapping
43 * array. That way the TableSorter appears to hold another copy of the table
44 * with the rows in a different order. The sorting algorthm used is stable
45 * which means that it does not move around rows when its comparison
46 * function returns 0 to denote that they are equivalent.
47 *
48 * @version 1.5 12/17/97
49 * @author Philip Milne
50 */
51
52 import java.util.*;
53
54 import javax.swing.table.TableModel;
55 import javax.swing.event.TableModelEvent;
56
57 import com.sun.dhcpmgr.data.IPAddress;
58
59 // Imports for picking up mouse events from the JTable.
60
61 import java.awt.event.MouseAdapter;
62 import java.awt.event.MouseEvent;
63 import java.awt.event.InputEvent;
64 import java.awt.event.ActionEvent;
65 import java.awt.event.ActionListener;
66 import javax.swing.JTable;
67 import javax.swing.table.JTableHeader;
68 import javax.swing.table.TableColumn;
69 import javax.swing.table.TableColumnModel;
70
71 public class TableSorter extends TableMap
72 {
73 int indexes[] = new int[0];
74 Vector sortingColumns = new Vector();
75 boolean ascending = true;
76 int compares;
77 Vector listeners = new Vector();
78
79 public TableSorter() {
80 indexes = new int[0]; // For consistency.
81 }
82
83 public TableSorter(TableModel model) {
84 setModel(model);
85 }
86
87 public void setModel(TableModel model) {
88 super.setModel(model);
89 reallocateIndexes();
90 }
91
92 public int compareRowsByColumn(int row1, int row2, int column) {
93 Class type = model.getColumnClass(column);
94 TableModel data = model;
95
96 // Check for nulls
97
98 Object o1 = data.getValueAt(row1, column);
99 Object o2 = data.getValueAt(row2, column);
100
101 // If both values are null return 0
102 if (o1 == null && o2 == null) {
103 return 0;
104 } else if (o1 == null) { // Define null less than everything.
105 return -1;
106 } else if (o2 == null) {
107 return 1;
108 }
109
110 /*
111 * We copy all returned values from the getValue call in case
112 * an optimised model is reusing one object to return many values.
113 * The Number subclasses in the JDK are immutable and so will not be
114 * used in this way but other subclasses of Number might want to do
115 * this to save space and avoid unnecessary heap allocation.
116 */
117 if (type.getSuperclass() == java.lang.Number.class) {
118 Number n1 = (Number)data.getValueAt(row1, column);
119 double d1 = n1.doubleValue();
120 Number n2 = (Number)data.getValueAt(row2, column);
121 double d2 = n2.doubleValue();
122
123 if (d1 < d2)
124 return -1;
125 else if (d1 > d2)
126 return 1;
127 else
128 return 0;
129 } else if (type == java.util.Date.class) {
130 Date d1 = (Date)data.getValueAt(row1, column);
131 long n1 = d1.getTime();
132 Date d2 = (Date)data.getValueAt(row2, column);
133 long n2 = d2.getTime();
134
135 // Handle negatives specially
136 if (n1 < 0) {
137 return 1;
138 } else if (n2 < 0) {
139 return -1;
140 }
141 if (n1 < n2)
142 return -1;
143 else if (n1 > n2)
144 return 1;
145 else
146 return 0;
147 } else if (type == String.class) {
148 String s1 = (String)data.getValueAt(row1, column);
149 String s2 = (String)data.getValueAt(row2, column);
150 int result = s1.compareTo(s2);
151
152 if (result < 0)
153 return -1;
154 else if (result > 0)
155 return 1;
156 else return 0;
157 } else if (type == Boolean.class) {
158 Boolean bool1 = (Boolean)data.getValueAt(row1, column);
159 boolean b1 = bool1.booleanValue();
160 Boolean bool2 = (Boolean)data.getValueAt(row2, column);
161 boolean b2 = bool2.booleanValue();
162
163 if (b1 == b2)
164 return 0;
165 else if (b1) // Define false < true
166 return 1;
167 else
168 return -1;
169 } else if (type == IPAddress.class) {
170 IPAddress addr1 = (IPAddress)data.getValueAt(row1, column);
171 IPAddress addr2 = (IPAddress)data.getValueAt(row2, column);
172 byte [] a1 = addr1.getAddress();
173 byte [] a2 = addr2.getAddress();
174 int c1, c2;
175 for (int i = 0; i < a1.length; ++i) {
176 /*
177 * Promote and mask because bytes are signed and 128-255
178 * will be done wrong
179 */
180 c1 = a1[i] & 0xff;
181 c2 = a2[i] & 0xff;
182 if (c1 < c2) {
183 return -1;
184 } else if (c1 > c2) {
185 return 1;
186 }
187 }
188 return 0;
189 } else {
190 Object v1 = data.getValueAt(row1, column);
191 String s1 = v1.toString();
192 Object v2 = data.getValueAt(row2, column);
193 String s2 = v2.toString();
194 int result = s1.compareTo(s2);
195
196 if (result < 0)
197 return -1;
198 else if (result > 0)
199 return 1;
200 else return 0;
201 }
202 }
203
204 public int compare(int row1, int row2) {
205 compares++;
206 for (int level = 0; level < sortingColumns.size(); level++) {
207 Integer column = (Integer)sortingColumns.elementAt(level);
208 int result = compareRowsByColumn(row1, row2, column.intValue());
209 if (result != 0)
210 return ascending ? result : -result;
211 }
212 return 0;
213 }
214
215 public void reallocateIndexes() {
216 int rowCount = model.getRowCount();
217
218 // Set up a new array of indexes with the right number of elements
219 // for the new data model.
220 indexes = new int[rowCount];
221
222 // Initialise with the identity mapping.
223 for (int row = 0; row < rowCount; row++)
224 indexes[row] = row;
225 }
226
227 public void tableChanged(TableModelEvent e) {
228 // System.out.println("Sorter: tableChanged");
229 reallocateIndexes();
230 sort(this);
231 super.tableChanged(e);
232 }
233
234 public void checkModel() {
235 if (indexes.length != model.getRowCount()) {
236 System.err.println("Sorter not informed of a change in model.");
237 }
238 }
239
240 public void sort(Object sender) {
241 checkModel();
242
243 compares = 0;
244 // n2sort();
245 // qsort(0, indexes.length-1);
246 shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
247 // System.out.println("Compares: "+compares);
248 }
249
250 public void n2sort() {
251 for (int i = 0; i < getRowCount(); i++) {
252 for (int j = i+1; j < getRowCount(); j++) {
253 if (compare(indexes[i], indexes[j]) == -1) {
254 swap(i, j);
255 }
256 }
257 }
258 }
259
260 /*
261 * This is a home-grown implementation which we have not had time
262 * to research - it may perform poorly in some circumstances. It
263 * requires twice the space of an in-place algorithm and makes
264 * NlogN assigments shuttling the values between the two
265 * arrays. The number of compares appears to vary between N-1 and
266 * NlogN depending on the initial order but the main reason for
267 * using it here is that, unlike qsort, it is stable.
268 */
269 public void shuttlesort(int from[], int to[], int low, int high) {
270 if (high - low < 2) {
271 return;
272 }
273 int middle = (low + high)/2;
274 shuttlesort(to, from, low, middle);
275 shuttlesort(to, from, middle, high);
276
277 int p = low;
278 int q = middle;
279
280 /*
281 * This is an optional short-cut; at each recursive call,
282 * check to see if the elements in this subset are already
283 * ordered. If so, no further comparisons are needed; the
284 * sub-array can just be copied. The array must be copied rather
285 * than assigned otherwise sister calls in the recursion might
286 * get out of sinc. When the number of elements is three they
287 * are partitioned so that the first set, [low, mid), has one
288 * element and and the second, [mid, high), has two. We skip the
289 * optimisation when the number of elements is three or less as
290 * the first compare in the normal merge will produce the same
291 * sequence of steps. This optimisation seems to be worthwhile
292 * for partially ordered lists but some analysis is needed to
293 * find out how the performance drops to Nlog(N) as the initial
294 * order diminishes - it may drop very quickly.
295 */
296
297 if (high - low >= 4 && compare(from[middle-1], from[middle]) <= 0) {
298 for (int i = low; i < high; i++) {
299 to[i] = from[i];
300 }
301 return;
302 }
303
304 // A normal merge.
305
306 for (int i = low; i < high; i++) {
307 if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
308 to[i] = from[p++];
309 } else {
310 to[i] = from[q++];
311 }
312 }
313 }
314
315 public void swap(int i, int j) {
316 int tmp = indexes[i];
317 indexes[i] = indexes[j];
318 indexes[j] = tmp;
319 }
320
321 /*
322 * The mapping only affects the contents of the data rows.
323 * Pass all requests to these rows through the mapping array: "indexes".
324 */
325
326 public Object getValueAt(int aRow, int aColumn)
327 {
328 checkModel();
329 return model.getValueAt(indexes[aRow], aColumn);
330 }
331
332 public int mapRowAt(int aRow) {
333 checkModel();
334 if (aRow < indexes.length) {
335 return indexes[aRow];
336 } else {
337 return -1;
338 }
339 }
340
341 public void setValueAt(Object aValue, int aRow, int aColumn) {
342 checkModel();
343 model.setValueAt(aValue, indexes[aRow], aColumn);
344 }
345
346 public void sortByColumn(int column) {
347 // Re-sort on this column, but don't change sort order
348 sortByColumn(column, this.ascending);
349 }
350
351 public void sortByColumn(int column, boolean ascending) {
352 this.ascending = ascending;
353 sortingColumns.removeAllElements();
354 sortingColumns.addElement(new Integer(column));
355 sort(this);
356 super.tableChanged(new TableModelEvent(this));
357 fireActionPerformed();
358 }
359
360 /*
361 * There is no-where else to put this.
362 * Add a mouse listener to the Table to trigger a table sort
363 * when a column heading is clicked in the JTable.
364 */
365 public void addMouseListenerToHeaderInTable(JTable table) {
366 final TableSorter sorter = this;
367 final JTable tableView = table;
368 tableView.setColumnSelectionAllowed(false);
369 MouseAdapter listMouseListener = new MouseAdapter() {
370 public void mouseClicked(MouseEvent e) {
371 TableColumnModel columnModel = tableView.getColumnModel();
372 int viewColumn = columnModel.getColumnIndexAtX(e.getX());
373 int column = tableView.convertColumnIndexToModel(viewColumn);
374 if (e.getClickCount() == 1 && column != -1) {
375 // System.out.println("Sorting ...");
376 int shiftPressed = e.getModifiers()&InputEvent.SHIFT_MASK;
377 boolean ascending = (shiftPressed == 0);
378 sorter.sortByColumn(column, ascending);
379 }
380 }
381 };
382 JTableHeader th = tableView.getTableHeader();
383 th.addMouseListener(listMouseListener);
384 }
385
386 // Allow others to be notified when re-sorting is done
387 public void addActionListener(ActionListener l) {
388 listeners.addElement(l);
389 }
390
391 // Take me off the notify list
392 public void removeActionListener(ActionListener l) {
393 listeners.removeElement(l);
394 }
395
396 /*
397 * Notify listeners of sort events; we just use ActionEvent as it's a
398 * good all-purpose event
399 */
400 protected void fireActionPerformed() {
401 ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
402 sortingColumns.firstElement().toString());
403 Enumeration en = listeners.elements();
404 while (en.hasMoreElements()) {
405 ActionListener l = (ActionListener)en.nextElement();
406 l.actionPerformed(e);
407 }
408 }
409 }