Coverage Report - us.daveread.basicquery.util.TableSorter
 
Classes in this File Line Coverage Branch Coverage Complexity
TableSorter
99%
151/152
100%
31/31
0
TableSorter$1
100%
10/10
100%
2/2
0
 
 1  
 package us.daveread.basicquery.util;
 2  
 
 3  
 import javax.swing.table.TableModel;
 4  
 import javax.swing.event.TableModelEvent;
 5  
 import java.awt.event.MouseAdapter;
 6  
 import java.awt.event.MouseEvent;
 7  
 import java.awt.event.InputEvent;
 8  
 import javax.swing.JTable;
 9  
 import javax.swing.table.JTableHeader;
 10  
 import javax.swing.table.TableColumnModel;
 11  
 import java.awt.event.MouseListener;
 12  
 import java.util.ArrayList;
 13  
 import java.util.Date;
 14  
 import java.util.List;
 15  
 import java.util.Vector;
 16  
 
 17  
 import org.apache.log4j.Logger;
 18  
 
 19  
 /**
 20  
  * A sorter for TableModels. The sorter has a model (conforming to TableModel)
 21  
  * and itself implements TableModel. TableSorter does not store or copy
 22  
  * the data in the TableModel, instead it maintains an array of
 23  
  * integers which it keeps the same size as the number of rows in its
 24  
  * model. When the model changes it notifies the sorter that something
 25  
  * has changed eg. "rowsAdded" so that its internal array of integers
 26  
  * can be reallocated. As requests are made of the sorter (like
 27  
  * getValueAt(row, col) it redirects them to its model via the mapping
 28  
  * array. That way the TableSorter appears to hold another copy of the table
 29  
  * with the rows in a different order. The sorting algorithm used is stable
 30  
  * which means that it does not move around rows when its comparison
 31  
  * function returns 0 to denote that they are equivalent.
 32  
  * 
 33  
  * <p>
 34  
  * Original version 1.5 12/17/97
 35  
  * </p>
 36  
  * 
 37  
  * @author Philip Milne
 38  
  */
 39  
 
 40  2
 public class TableSorter extends TableMap {
 41  
   /**
 42  
    * Serial UID
 43  
    */
 44  
   private static final long serialVersionUID = 4178393865264618734L;
 45  
 
 46  
   /**
 47  
    * Logger
 48  
    */
 49  1
   private static final Logger LOGGER = Logger.getLogger(TableSorter.class);
 50  
 
 51  
   /**
 52  
    * Sorted indexes
 53  
    */
 54  
   private int[] indexes;
 55  
 
 56  
   /**
 57  
    * Columns used to sort the model
 58  
    */
 59  14
   private Vector<Integer> sortingColumns = new Vector<Integer>();
 60  
 
 61  
   /**
 62  
    * Whether sort is ascending
 63  
    */
 64  14
   private boolean ascending = true;
 65  
 
 66  
   /**
 67  
    * Track the comparison result of two rows
 68  
    */
 69  
   private int compares;
 70  
 
 71  
   /**
 72  
    * The listeners registered for mouse events on the table
 73  
    * 
 74  
    * DSR - So I can remove them
 75  
    */
 76  14
   private List<MouseAdapter> myMouseListeners = new ArrayList<MouseAdapter>();
 77  
 
 78  
   /**
 79  
    * Constructs a Table Sorter
 80  
    * Creates an integer array for consistency
 81  
    */
 82  13
   public TableSorter() {
 83  13
     indexes = new int[0];
 84  13
   }
 85  
 
 86  
   /**
 87  
    * Constructs a Table Sorter
 88  
    * 
 89  
    * @param model
 90  
    *          The Table Model Object
 91  
    */
 92  1
   public TableSorter(TableModel model) {
 93  1
     setModel(model);
 94  1
   }
 95  
 
 96  
   /**
 97  
    * Sets the data model for this table to model and
 98  
    * registers with it for listener notifications from
 99  
    * the new data model.
 100  
    * 
 101  
    * @param model
 102  
    *          The TableModel Object
 103  
    */
 104  
   public void setModel(TableModel model) {
 105  15
     super.setModel(model);
 106  15
     reallocateIndexes();
 107  15
   }
 108  
 
 109  
   /**
 110  
    * Remove the Model model
 111  
    * 
 112  
    * @param model
 113  
    *          The Table Model object to be removed
 114  
    */
 115  
   public void removeModel(TableModel model) {
 116  1
     super.removeModel(model);
 117  1
   }
 118  
 
 119  
   /**
 120  
    * Compares the values of two rows in a column
 121  
    * 
 122  
    * @param row1
 123  
    *          row index
 124  
    * @param row2
 125  
    *          row index
 126  
    * @param column
 127  
    *          column index
 128  
    * 
 129  
    * @return int Returns values -1,0,1 depending on the sort results
 130  
    */
 131  
   private int compareRowsByColumn(int row1, int row2, int column) {
 132  87
     final TableModel data = getModel();
 133  
 
 134  
     // Check for nulls.
 135  
 
 136  87
     final Object o1 = data.getValueAt(row1, column);
 137  87
     final Object o2 = data.getValueAt(row2, column);
 138  
 
 139  
     // If both values are null, return 0.
 140  87
     if (o1 == null && o2 == null) {
 141  3
       return 0;
 142  84
     } else if (o1 == null) { // Define null less than everything.
 143  4
       return -1;
 144  80
     } else if (o2 == null) {
 145  24
       return 1;
 146  
     }
 147  
 
 148  
     /*
 149  
      * We copy all returned values from the getValue call in case
 150  
      * an optimised model is reusing one object to return many
 151  
      * values. The Number subclasses in the JDK are immutable and
 152  
      * so will not be used in this way but other subclasses of
 153  
      * Number might want to do this to save space and avoid
 154  
      * unnecessary heap allocation.
 155  
      */
 156  
 
 157  
     // if (type.getSuperclass() == java.lang.Number.class) {
 158  56
     if (o1 instanceof java.lang.Number) {
 159  9
       final Number n1 = (Number) data.getValueAt(row1, column);
 160  9
       final double d1 = n1.doubleValue();
 161  9
       final Number n2 = (Number) data.getValueAt(row2, column);
 162  9
       final double d2 = n2.doubleValue();
 163  
 
 164  9
       if (d1 < d2) {
 165  4
         return -1;
 166  5
       } else if (d1 > d2) {
 167  3
         return 1;
 168  
       } else {
 169  2
         return 0;
 170  
       }
 171  
       // } else if (type == java.util.Date.class) {
 172  47
     } else if (o1 instanceof java.util.Date) {
 173  12
       final Date d1 = (Date) data.getValueAt(row1, column);
 174  12
       final long n1 = d1.getTime();
 175  12
       final Date d2 = (Date) data.getValueAt(row2, column);
 176  12
       final long n2 = d2.getTime();
 177  
 
 178  12
       if (n1 < n2) {
 179  4
         return -1;
 180  8
       } else if (n1 > n2) {
 181  6
         return 1;
 182  
       } else {
 183  2
         return 0;
 184  
       }
 185  
       // } else if (type == String.class) {
 186  35
     } else if (o1 instanceof java.lang.String) {
 187  21
       final String s1 = (String) data.getValueAt(row1, column);
 188  21
       final String s2 = (String) data.getValueAt(row2, column);
 189  21
       final int result = s1.compareTo(s2);
 190  
 
 191  21
       if (result < 0) {
 192  16
         return -1;
 193  5
       } else if (result > 0) {
 194  2
         return 1;
 195  
       } else {
 196  3
         return 0;
 197  
       }
 198  
       // } else if (type == Boolean.class) {
 199  14
     } else if (o1 instanceof java.lang.Boolean) {
 200  6
       final Boolean bool1 = (Boolean) data.getValueAt(row1, column);
 201  6
       final boolean b1 = bool1.booleanValue();
 202  6
       final Boolean bool2 = (Boolean) data.getValueAt(row2, column);
 203  6
       final boolean b2 = bool2.booleanValue();
 204  
 
 205  6
       if (b1 == b2) {
 206  3
         return 0;
 207  3
       } else if (b1) { // Define false < true
 208  2
         return 1;
 209  
       } else {
 210  1
         return -1;
 211  
       }
 212  
     } else {
 213  8
       final Object v1 = data.getValueAt(row1, column);
 214  8
       final String s1 = v1.toString();
 215  8
       final Object v2 = data.getValueAt(row2, column);
 216  8
       final String s2 = v2.toString();
 217  8
       final int result = s1.compareTo(s2);
 218  
 
 219  8
       if (result < 0) {
 220  1
         return -1;
 221  7
       } else if (result > 0) {
 222  3
         return 1;
 223  
       } else {
 224  4
         return 0;
 225  
       }
 226  
     }
 227  
   }
 228  
 
 229  
   /**
 230  
    * Compares values in row1 and row2
 231  
    * 
 232  
    * @param row1
 233  
    *          row index
 234  
    * @param row2
 235  
    *          row index
 236  
    * 
 237  
    * @return int
 238  
    */
 239  
   private int compare(int row1, int row2) {
 240  77
     compares++;
 241  94
     for (int level = 0; level < sortingColumns.size(); level++) {
 242  87
       final Integer column = (Integer) sortingColumns.elementAt(level);
 243  87
       final int result = compareRowsByColumn(row1, row2, column.intValue());
 244  87
       if (result != 0) {
 245  70
         return ascending ? result : -result;
 246  
       }
 247  
     }
 248  7
     return 0;
 249  
   }
 250  
 
 251  
   /**
 252  
    * Set up a new mapping array of indexes with the right number of
 253  
    * elements for the new data model
 254  
    */
 255  
   private void reallocateIndexes() {
 256  16
     final int rowCount = getModel().getRowCount();
 257  
 
 258  16
     indexes = new int[rowCount];
 259  
 
 260  
     // Initialise with the identity mapping.
 261  130
     for (int row = 0; row < rowCount; row++) {
 262  114
       indexes[row] = row;
 263  
     }
 264  16
   }
 265  
 
 266  
   /**
 267  
    * The tableChanged Method is called when any change is made to the table
 268  
    * 
 269  
    * @param e
 270  
    *          The Table ModelEvent
 271  
    */
 272  
   public void tableChanged(TableModelEvent e) {
 273  1
     LOGGER.debug("tableChanged");
 274  
 
 275  1
     reallocateIndexes();
 276  
 
 277  1
     super.tableChanged(e);
 278  1
   }
 279  
 
 280  
   /**
 281  
    * Checks if there has been any uninformed change in the model
 282  
    */
 283  
   private void checkModel() {
 284  34
     if (indexes.length != getModel().getRowCount()) {
 285  0
       LOGGER.warn("Sorter not informed of a change in model.");
 286  
     }
 287  34
   }
 288  
 
 289  
   /**
 290  
    * Performs sort operation on the sender
 291  
    * 
 292  
    * @param sender
 293  
    *          Object that is to sorted
 294  
    */
 295  
   private void sort(Object sender) {
 296  6
     checkModel();
 297  
 
 298  6
     compares = 0;
 299  
     // n2sort();
 300  
     // qsort(0, indexes.length-1);
 301  6
     shuttlesort((int[]) indexes.clone(), indexes, 0, indexes.length);
 302  6
     LOGGER.debug("Compares: " + compares);
 303  6
   }
 304  
 
 305  
   /**
 306  
    * This is a home-grown implementation which we have not had time
 307  
    * to research - it may perform poorly in some circumstances. It
 308  
    * requires twice the space of an in-place algorithm and makes
 309  
    * NlogN assigments shuttling the values between the two
 310  
    * arrays. The number of compares appears to vary between N-1 and
 311  
    * NlogN depending on the initial order but the main reason for
 312  
    * using it here is that, unlike qsort, it is stable.
 313  
    * 
 314  
    * @param from
 315  
    *          [] array that has to be sorted
 316  
    * @param to
 317  
    *          [] array that has the sorted result
 318  
    * @param low
 319  
    *          Starting row index 1
 320  
    * @param high
 321  
    *          Maximum number of rows
 322  
    * 
 323  
    */
 324  
 
 325  
   private void shuttlesort(int[] from, int[] to, int low, int high) {
 326  70
     if (high - low < 2) {
 327  38
       return;
 328  
     }
 329  32
     final int middle = (low + high) / 2;
 330  32
     shuttlesort(to, from, low, middle);
 331  32
     shuttlesort(to, from, middle, high);
 332  
 
 333  32
     int p = low;
 334  32
     int q = middle;
 335  
 
 336  
     /*
 337  
      * This is an optional short-cut; at each recursive call,
 338  
      * check to see if the elements in this subset are already
 339  
      * ordered. If so, no further comparisons are needed; the
 340  
      * sub-array can just be copied. The array must be copied rather
 341  
      * than assigned otherwise sister calls in the recursion might
 342  
      * get out of sync. When the number of elements is three they
 343  
      * are partitioned so that the first set, [low, mid), has one
 344  
      * element and and the second, [mid, high), has two. We skip the
 345  
      * optimisation when the number of elements is three or less as
 346  
      * the first compare in the normal merge will produce the same
 347  
      * sequence of steps. This optimisation seems to be worthwhile
 348  
      * for partially ordered lists but some analysis is needed to
 349  
      * find out how the performance drops to Nlog(N) as the initial
 350  
      * order diminishes - it may drop very quickly.
 351  
      */
 352  
 
 353  32
     if (high - low >= 4 && compare(from[middle - 1], from[middle]) <= 0) {
 354  6
       for (int i = low; i < high; i++) {
 355  5
         to[i] = from[i];
 356  
       }
 357  1
       return;
 358  
     }
 359  
 
 360  
     // A normal merge.
 361  
 
 362  130
     for (int i = low; i < high; i++) {
 363  99
       if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
 364  47
         to[i] = from[p++];
 365  
       } else {
 366  52
         to[i] = from[q++];
 367  
       }
 368  
     }
 369  31
   }
 370  
 
 371  
   // The mapping only affects the contents of the data rows.
 372  
   // Pass all requests to these rows through the mapping array: "indexes".
 373  
 
 374  
   /**
 375  
    * Gets the value of the model at indexes[aRow] and aColumn
 376  
    * 
 377  
    * @param aRow
 378  
    *          row index
 379  
    * @param aColumn
 380  
    *          column index
 381  
    * 
 382  
    * @return model The tableModel with its value at indexes[aRow] and
 383  
    *         aColumn
 384  
    */
 385  
   public Object getValueAt(int aRow, int aColumn) {
 386  27
     checkModel();
 387  
 
 388  27
     LOGGER.debug("aRow[" + aRow + "] aColumn[" + aColumn + "] indexes[aRow]["
 389  
         + indexes[aRow] + "]");
 390  
 
 391  27
     return getModel().getValueAt(indexes[aRow], aColumn);
 392  
   }
 393  
 
 394  
   /**
 395  
    * 
 396  
    * Sets the value of the model at aRow and aColumn to aValue
 397  
    * 
 398  
    * @param aValue
 399  
    *          The value to assign
 400  
    * @param aRow
 401  
    *          row index
 402  
    * @param aColumn
 403  
    *          column index
 404  
    */
 405  
   public void setValueAt(Object aValue, int aRow, int aColumn) {
 406  1
     checkModel();
 407  1
     getModel().setValueAt(aValue, indexes[aRow], aColumn);
 408  1
   }
 409  
 
 410  
   /**
 411  
    * Sorts the column in an ascending order
 412  
    * 
 413  
    * @param column
 414  
    *          column index
 415  
    * @param pAscending
 416  
    *          the column is sorted in an ascending order
 417  
    */
 418  
   private void sortByColumn(int column, boolean pAscending) {
 419  
     int[] columns;
 420  
 
 421  1
     columns = new int[1];
 422  1
     columns[0] = column;
 423  1
     sortByColumns(columns, pAscending);
 424  1
   }
 425  
 
 426  
   /**
 427  
    * Calls the method sortByColumns(int,boolean) after setting the sorting
 428  
    * option as ascending
 429  
    * 
 430  
    * @param columns
 431  
    *          The column values
 432  
    */
 433  
   public void sortByColumns(int[] columns) {
 434  5
     sortByColumns(columns, true);
 435  5
   }
 436  
 
 437  
   /**
 438  
    * Sort the data by the selected columns
 439  
    * 
 440  
    * @param columns
 441  
    *          The columns to sort the data by
 442  
    * @param pAscending
 443  
    *          A boolean value that determines whether the column
 444  
    *          is sorted in an ascending or descending order
 445  
    */
 446  
   public void sortByColumns(int[] columns, boolean pAscending) {
 447  6
     ascending = pAscending;
 448  6
     sortingColumns.removeAllElements();
 449  
 
 450  17
     for (int column = 0; column < columns.length; ++column) {
 451  11
       sortingColumns.addElement(new Integer(columns[column]));
 452  
     }
 453  6
     sort(this);
 454  6
     super.tableChanged(new TableModelEvent(this));
 455  6
   }
 456  
 
 457  
   /**
 458  
    * Adds a mouse listener to the Table to trigger a table sort
 459  
    * when a column heading is clicked in the JTable.
 460  
    * 
 461  
    * @param table
 462  
    *          The JTabel object
 463  
    */
 464  
 
 465  
   public void addMouseListenerToHeaderInTable(JTable table) {
 466  3
     final TableSorter sorter = this;
 467  3
     final JTable tableView = table;
 468  3
     tableView.setColumnSelectionAllowed(false);
 469  3
     final MouseAdapter listMouseListener = new MouseAdapter() {
 470  
       public void mouseClicked(MouseEvent e) {
 471  1
         final TableColumnModel columnModel = tableView.getColumnModel();
 472  1
         final int viewColumn = columnModel.getColumnIndexAtX(e.getX());
 473  1
         final int column = tableView.convertColumnIndexToModel(viewColumn);
 474  1
         if (e.getClickCount() == 1 && column != -1) {
 475  1
           LOGGER.info("Sorting ...");
 476  1
           final int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK;
 477  1
           final boolean lAscending = shiftPressed == 0;
 478  1
           sorter.sortByColumn(column, lAscending);
 479  
         }
 480  1
       }
 481  
     };
 482  3
     final JTableHeader th = tableView.getTableHeader();
 483  3
     th.addMouseListener(listMouseListener);
 484  3
     myMouseListeners.add(listMouseListener);
 485  3
   }
 486  
 
 487  
   /**
 488  
    * Need ability to remove mouselistener
 489  
    * Otherwise creation of new sorter leaks memory
 490  
    * 
 491  
    * @param table
 492  
    *          The JTable object
 493  
    */
 494  
   public void removeMouseListenerFromHeaderInTable(JTable table) {
 495  
     MouseListener[] mouseListeners;
 496  
 
 497  1
     mouseListeners = table.getTableHeader().getMouseListeners();
 498  
 
 499  4
     for (int tableLsnr = 0; tableLsnr < mouseListeners.length; ++tableLsnr) {
 500  3
       LOGGER.debug("Checking Registered Listener:" + mouseListeners[tableLsnr]);
 501  5
       for (int myLsnr = 0; myLsnr < myMouseListeners.size(); ++myLsnr) {
 502  3
         LOGGER.debug("Matching to Local Listener:" 
 503  
             + myMouseListeners.get(myLsnr));
 504  3
         if (mouseListeners[tableLsnr] == myMouseListeners.get(myLsnr)) {
 505  1
           table.getTableHeader().removeMouseListener(mouseListeners[tableLsnr]);
 506  1
           myMouseListeners.remove(myLsnr);
 507  1
           LOGGER.debug("Removed Mouse Listener:" + mouseListeners[tableLsnr]);
 508  1
           break;
 509  
         }
 510  
       }
 511  
     }
 512  1
   }
 513  
 }