1 /*
   2  * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
   3  *
   4  * Redistribution and use in source and binary forms, with or without
   5  * modification, are permitted provided that the following conditions
   6  * are met:
   7  *
   8  *   - Redistributions of source code must retain the above copyright
   9  *     notice, this list of conditions and the following disclaimer.
  10  *
  11  *   - Redistributions in binary form must reproduce the above copyright
  12  *     notice, this list of conditions and the following disclaimer in the
  13  *     documentation and/or other materials provided with the distribution.
  14  *
  15  *   - Neither the name of Oracle nor the names of its
  16  *     contributors may be used to endorse or promote products derived
  17  *     from this software without specific prior written permission.
  18  *
  19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  20  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  21  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  22  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  23  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  24  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  25  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  26  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  27  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  28  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  29  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30  */
  31 
  32 /*
  33  * This source code is provided to illustrate the usage of a given feature
  34  * or technique and has been deliberately simplified. Additional steps
  35  * required for a production-quality application, such as security checks,
  36  * input validation and proper error handling, might not be present in
  37  * this sample code.
  38  */
  39 
  40 
  41 
  42 import javax.swing.table.TableModel;
  43 import javax.swing.event.TableModelEvent;
  44 import java.awt.event.MouseAdapter;
  45 import java.awt.event.MouseEvent;
  46 import java.awt.event.InputEvent;
  47 import java.util.ArrayList;
  48 import java.util.Date;
  49 import java.util.List;
  50 import javax.swing.JTable;
  51 import javax.swing.table.JTableHeader;
  52 import javax.swing.table.TableColumnModel;
  53 
  54 
  55 /**
  56  * A sorter for TableModels. The sorter has a model (conforming to TableModel)
  57  * and itself implements TableModel. TableSorter does not store or copy
  58  * the data in the TableModel, instead it maintains an array of
  59  * integers which it keeps the same size as the number of rows in its
  60  * model. When the model changes it notifies the sorter that something
  61  * has changed eg. "rowsAdded" so that its internal array of integers
  62  * can be reallocated. As requests are made of the sorter (like
  63  * getValueAt(row, col) it redirects them to its model via the mapping
  64  * array. That way the TableSorter appears to hold another copy of the table
  65  * with the rows in a different order. The sorting algorthm used is stable
  66  * which means that it does not move around rows when its comparison
  67  * function returns 0 to denote that they are equivalent.
  68  *
  69  * @author Philip Milne
  70  */
  71 @SuppressWarnings("serial")
  72 public final class TableSorter extends TableMap {
  73 
  74     int[] indexes;
  75     List<Integer> sortingColumns = new ArrayList<Integer>();
  76     boolean ascending = true;
  77     int compares;
  78 
  79     public TableSorter() {
  80         indexes = new int[0]; // For consistency.
  81     }
  82 
  83     public TableSorter(TableModel model) {
  84         setModel(model);
  85     }
  86 
  87     @Override
  88     public void setModel(TableModel model) {
  89         super.setModel(model);
  90         reallocateIndexes();
  91     }
  92 
  93     public int compareRowsByColumn(int row1, int row2, int column) {
  94         Class type = model.getColumnClass(column);
  95         TableModel data = model;
  96 
  97         // Check for nulls
  98 
  99         Object o1 = data.getValueAt(row1, column);
 100         Object o2 = data.getValueAt(row2, column);
 101 
 102         // If both values are null return 0
 103         if (o1 == null && o2 == null) {
 104             return 0;
 105         } else if (o1 == null) { // Define null less than everything.
 106             return -1;
 107         } else if (o2 == null) {
 108             return 1;
 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 used
 114         in this way but other subclasses of Number might want to do this to save
 115         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             }
 130         } else if (type == java.util.Date.class) {
 131             Date d1 = (Date) data.getValueAt(row1, column);
 132             long n1 = d1.getTime();
 133             Date d2 = (Date) data.getValueAt(row2, column);
 134             long n2 = d2.getTime();
 135 
 136             if (n1 < n2) {
 137                 return -1;
 138             } else if (n1 > n2) {
 139                 return 1;
 140             } else {
 141                 return 0;
 142             }
 143         } else if (type == String.class) {
 144             String s1 = (String) data.getValueAt(row1, column);
 145             String s2 = (String) data.getValueAt(row2, column);
 146             int result = s1.compareTo(s2);
 147 
 148             if (result < 0) {
 149                 return -1;
 150             } else if (result > 0) {
 151                 return 1;
 152             } else {
 153                 return 0;
 154             }
 155         } else if (type == Boolean.class) {
 156             Boolean bool1 = (Boolean) data.getValueAt(row1, column);
 157             boolean b1 = bool1.booleanValue();
 158             Boolean bool2 = (Boolean) data.getValueAt(row2, column);
 159             boolean b2 = bool2.booleanValue();
 160 
 161             if (b1 == b2) {
 162                 return 0;
 163             } else if (b1) // Define false < true
 164             {
 165                 return 1;
 166             } else {
 167                 return -1;
 168             }
 169         } else {
 170             Object v1 = data.getValueAt(row1, column);
 171             String s1 = v1.toString();
 172             Object v2 = data.getValueAt(row2, column);
 173             String s2 = v2.toString();
 174             int result = s1.compareTo(s2);
 175 
 176             if (result < 0) {
 177                 return -1;
 178             } else if (result > 0) {
 179                 return 1;
 180             } else {
 181                 return 0;
 182             }
 183         }
 184     }
 185 
 186     public int compare(int row1, int row2) {
 187         compares++;
 188         for (int level = 0; level < sortingColumns.size(); level++) {
 189             Integer column = sortingColumns.get(level);
 190             int result = compareRowsByColumn(row1, row2, column.intValue());
 191             if (result != 0) {
 192                 return ascending ? result : -result;
 193             }
 194         }
 195         return 0;
 196     }
 197 
 198     public void reallocateIndexes() {
 199         int rowCount = model.getRowCount();
 200 
 201         // Set up a new array of indexes with the right number of elements
 202         // for the new data model.
 203         indexes = new int[rowCount];
 204 
 205         // Initialise with the identity mapping.
 206         for (int row = 0; row < rowCount; row++) {
 207             indexes[row] = row;
 208         }
 209     }
 210 
 211     @Override
 212     public void tableChanged(TableModelEvent e) {
 213         System.out.println("Sorter: tableChanged");
 214         reallocateIndexes();
 215 
 216         super.tableChanged(e);
 217     }
 218 
 219     public void checkModel() {
 220         if (indexes.length != model.getRowCount()) {
 221             System.err.println("Sorter not informed of a change in model.");
 222         }
 223     }
 224 
 225     public void sort(Object sender) {
 226         checkModel();
 227 
 228         compares = 0;
 229         // n2sort();
 230         // qsort(0, indexes.length-1);
 231         shuttlesort(indexes.clone(), indexes, 0, indexes.length);
 232         System.out.println("Compares: " + compares);
 233     }
 234 
 235     public void n2sort() {
 236         for (int i = 0; i < getRowCount(); i++) {
 237             for (int j = i + 1; j < getRowCount(); j++) {
 238                 if (compare(indexes[i], indexes[j]) == -1) {
 239                     swap(i, j);
 240                 }
 241             }
 242         }
 243     }
 244 
 245     // This is a home-grown implementation which we have not had time
 246     // to research - it may perform poorly in some circumstances. It
 247     // requires twice the space of an in-place algorithm and makes
 248     // NlogN assigments shuttling the values between the two
 249     // arrays. The number of compares appears to vary between N-1 and
 250     // NlogN depending on the initial order but the main reason for
 251     // using it here is that, unlike qsort, it is stable.
 252     public void shuttlesort(int[] from, int[] to, int low, int high) {
 253         if (high - low < 2) {
 254             return;
 255         }
 256         int middle = (low + high) / 2;
 257         shuttlesort(to, from, low, middle);
 258         shuttlesort(to, from, middle, high);
 259 
 260         int p = low;
 261         int q = middle;
 262 
 263         /* This is an optional short-cut; at each recursive call,
 264         check to see if the elements in this subset are already
 265         ordered.  If so, no further comparisons are needed; the
 266         sub-array can just be copied.  The array must be copied rather
 267         than assigned otherwise sister calls in the recursion might
 268         get out of sinc.  When the number of elements is three they
 269         are partitioned so that the first set, [low, mid), has one
 270         element and the second, [mid, high), has two. We skip the
 271         optimisation when the number of elements is three or less as
 272         the first compare in the normal merge will produce the same
 273         sequence of steps. This optimisation seems to be worthwhile
 274         for partially ordered lists but some analysis is needed to
 275         find out how the performance drops to Nlog(N) as the initial
 276         order diminishes - it may drop very quickly.  */
 277 
 278         if (high - low >= 4 && compare(from[middle - 1], from[middle]) <= 0) {
 279             System.arraycopy(from, low, to, low, high - low);
 280             return;
 281         }
 282 
 283         // A normal merge.
 284 
 285         for (int i = low; i < high; i++) {
 286             if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
 287                 to[i] = from[p++];
 288             } else {
 289                 to[i] = from[q++];
 290             }
 291         }
 292     }
 293 
 294     public void swap(int i, int j) {
 295         int tmp = indexes[i];
 296         indexes[i] = indexes[j];
 297         indexes[j] = tmp;
 298     }
 299 
 300     // The mapping only affects the contents of the data rows.
 301     // Pass all requests to these rows through the mapping array: "indexes".
 302     @Override
 303     public Object getValueAt(int aRow, int aColumn) {
 304         checkModel();
 305         return model.getValueAt(indexes[aRow], aColumn);
 306     }
 307 
 308     @Override
 309     public void setValueAt(Object aValue, int aRow, int aColumn) {
 310         checkModel();
 311         model.setValueAt(aValue, indexes[aRow], aColumn);
 312     }
 313 
 314     public void sortByColumn(int column) {
 315         sortByColumn(column, true);
 316     }
 317 
 318     public void sortByColumn(int column, boolean ascending) {
 319         this.ascending = ascending;
 320         sortingColumns.clear();
 321         sortingColumns.add(column);
 322         sort(this);
 323         super.tableChanged(new TableModelEvent(this));
 324     }
 325 
 326     // There is no-where else to put this.
 327     // Add a mouse listener to the Table to trigger a table sort
 328     // when a column heading is clicked in the JTable.
 329     public void addMouseListenerToHeaderInTable(JTable table) {
 330         final TableSorter sorter = this;
 331         final JTable tableView = table;
 332         tableView.setColumnSelectionAllowed(false);
 333         MouseAdapter listMouseListener = new MouseAdapter() {
 334 
 335             @Override
 336             public void mouseClicked(MouseEvent e) {
 337                 TableColumnModel columnModel = tableView.getColumnModel();
 338                 int viewColumn = columnModel.getColumnIndexAtX(e.getX());
 339                 int column = tableView.convertColumnIndexToModel(viewColumn);
 340                 if (e.getClickCount() == 1 && column != -1) {
 341                     System.out.println("Sorting ...");
 342                     int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK;
 343                     boolean ascending = (shiftPressed == 0);
 344                     sorter.sortByColumn(column, ascending);
 345                 }
 346             }
 347         };
 348         JTableHeader th = tableView.getTableHeader();
 349         th.addMouseListener(listMouseListener);
 350     }
 351 }