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 }