001    package net.provision.soap;
002    
003    import java.awt.Color;
004    import java.awt.Component;
005    import java.awt.FontMetrics;
006    import java.awt.Graphics;
007    import java.awt.Polygon;
008    import java.awt.event.*;
009    
010    import java.util.ArrayList;
011    import java.util.Arrays;
012    import java.util.Comparator;
013    import java.util.Date;
014    import java.util.HashMap;
015    import java.util.Iterator;
016    import java.util.List;
017    import java.util.Map;
018    
019    import javax.swing.*;
020    import javax.swing.event.*;
021    import javax.swing.table.*;
022    
023    
024    /**
025     * TableSorter is a decorator for TableModels; adding sorting functionality to a
026     * supplied TableModel. TableSorter does not store or copy the data in its TableModel;
027     * instead it maintains a map from the row indexes of the view to the row indexes of
028     * the model. As requests are made of the sorter (like getValueAt(row, col)) they are
029     * passed to the underlying model after the row numbers have been translated via the
030     * internal mapping array. This way, the TableSorter appears to hold another copy of
031     * the table with the rows in a different order.  TableSorter registers itself as a
032     * listener to the underlying model, just as the JTable itself would. Events recieved
033     * from the model are examined, sometimes manipulated (typically widened), and then
034     * passed on to the TableSorter's listeners (typically the JTable). If a change to the
035     * model has invalidated the order of TableSorter's rows, a note of this is made and
036     * the sorter will resort the rows the next time a value is requested.  When the
037     * tableHeader property is set, either by using the setTableHeader() method or the two
038     * argument constructor, the table header may be used as a complete UI for
039     * TableSorter. The default renderer of the tableHeader is decorated with a renderer
040     * that indicates the sorting status of each column. In addition, a mouse listener is
041     * installed with the following behavior:
042     * 
043     * <ul>
044     * <li>
045     * Mouse-click: Clears the sorting status of all other columns and advances the sorting
046     * status of that column through three values: {NOT_SORTED, ASCENDING, DESCENDING}
047     * (then back to NOT_SORTED again).
048     * </li>
049     * <li>
050     * SHIFT-mouse-click: Clears the sorting status of all other columns and cycles the
051     * sorting status of the column through the same three values, in the opposite order:
052     * {NOT_SORTED, DESCENDING, ASCENDING}.
053     * </li>
054     * <li>
055     * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except that the changes
056     * to the column do not cancel the statuses of columns that are already sorting -
057     * giving a way to initiate a compound sort.
058     * </li>
059     * </ul>
060     * 
061     * This is a long overdue rewrite of a class of the same name that first appeared in
062     * the swing table demos in 1997.
063     *
064     * @author Philip Milne
065     * @author Brendon McLean
066     * @author Dan van Enckevort
067     * @author Parwinder Sekhon
068     * @version 2.0 02/27/04
069     */
070    public class TableSorter extends AbstractTableModel {
071       public static final int DESCENDING = -1;
072       public static final int NOT_SORTED = 0;
073       public static final int ASCENDING = 1;
074       private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);
075       public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
076             public int compare(Object o1, Object o2) {
077                return ((Comparable)o1).compareTo(o2);
078             }
079          };
080    
081       public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
082             public int compare(Object o1, Object o2) {
083                if(o1 instanceof Date) {
084                   return ((Date)o1).compareTo((Date)o2);
085                }
086    
087                return o1.toString().compareTo(o2.toString());
088             }
089          };
090    
091       protected TableModel tableModel;
092       private JTableHeader tableHeader;
093       private List sortingColumns = new ArrayList();
094       private Map columnComparators = new HashMap();
095       private MouseListener mouseListener;
096       private TableModelListener tableModelListener;
097       private int[] modelToView;
098       private Row[] viewToModel;
099    
100       public TableSorter() {
101          this.mouseListener = new MouseHandler();
102          this.tableModelListener = new TableModelHandler();
103       }
104    
105       public TableSorter(TableModel tableModel) {
106          this();
107          setTableModel(tableModel);
108       }
109    
110       public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
111          this();
112          setTableHeader(tableHeader);
113          setTableModel(tableModel);
114       }
115    
116       /**
117        * DOCUMENT ME!
118        *
119        * @param row DOCUMENT ME!
120        * @param column DOCUMENT ME!
121        *
122        * @return DOCUMENT ME!
123        */
124       public boolean isCellEditable(int row, int column) {
125          return tableModel.isCellEditable(modelIndex(row), column);
126       }
127    
128       /**
129        * DOCUMENT ME!
130        *
131        * @param column DOCUMENT ME!
132        *
133        * @return DOCUMENT ME!
134        */
135       public Class getColumnClass(int column) {
136          return tableModel.getColumnClass(column);
137       }
138    
139       /**
140        * DOCUMENT ME!
141        *
142        * @param type DOCUMENT ME!
143        * @param comparator DOCUMENT ME!
144        */
145       public void setColumnComparator(Class type, Comparator comparator) {
146          if(comparator == null) {
147             columnComparators.remove(type);
148          } else {
149             columnComparators.put(type, comparator);
150          }
151       }
152    
153       /**
154        * DOCUMENT ME!
155        *
156        * @return DOCUMENT ME!
157        */
158       public int getColumnCount() {
159          return (tableModel == null) ? 0 : tableModel.getColumnCount();
160       }
161    
162       /**
163        * DOCUMENT ME!
164        *
165        * @param column DOCUMENT ME!
166        *
167        * @return DOCUMENT ME!
168        */
169       public String getColumnName(int column) {
170          return tableModel.getColumnName(column);
171       }
172    
173       // TableModel interface methods 
174       public int getRowCount() {
175          return (tableModel == null) ? 0 : tableModel.getRowCount();
176       }
177    
178       /**
179        * DOCUMENT ME!
180        *
181        * @return DOCUMENT ME!
182        */
183       public boolean isSorting() {
184          return sortingColumns.size() != 0;
185       }
186    
187       /**
188        * DOCUMENT ME!
189        *
190        * @param column DOCUMENT ME!
191        * @param status DOCUMENT ME!
192        */
193       public void setSortingStatus(int column, int status) {
194          Directive directive = getDirective(column);
195    
196          if(directive != EMPTY_DIRECTIVE) {
197             sortingColumns.remove(directive);
198          }
199    
200          if(status != NOT_SORTED) {
201             sortingColumns.add(new Directive(column, status));
202          }
203    
204          sortingStatusChanged();
205       }
206    
207       /**
208        * DOCUMENT ME!
209        *
210        * @param column DOCUMENT ME!
211        *
212        * @return DOCUMENT ME!
213        */
214       public int getSortingStatus(int column) {
215          return getDirective(column).direction;
216       }
217    
218       /**
219        * DOCUMENT ME!
220        *
221        * @param tableHeader DOCUMENT ME!
222        */
223       public void setTableHeader(JTableHeader tableHeader) {
224          if(this.tableHeader != null) {
225             this.tableHeader.removeMouseListener(mouseListener);
226    
227             TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
228    
229             if(defaultRenderer instanceof SortableHeaderRenderer) {
230                this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer)defaultRenderer).tableCellRenderer);
231             }
232          }
233    
234          this.tableHeader = tableHeader;
235    
236          if(this.tableHeader != null) {
237             this.tableHeader.addMouseListener(mouseListener);
238             this.tableHeader.setDefaultRenderer(new SortableHeaderRenderer(
239                   this.tableHeader.getDefaultRenderer()));
240          }
241       }
242    
243       /**
244        * DOCUMENT ME!
245        *
246        * @return DOCUMENT ME!
247        */
248       public JTableHeader getTableHeader() {
249          return tableHeader;
250       }
251    
252       /**
253        * DOCUMENT ME!
254        *
255        * @param tableModel DOCUMENT ME!
256        */
257       public void setTableModel(TableModel tableModel) {
258          if(this.tableModel != null) {
259             this.tableModel.removeTableModelListener(tableModelListener);
260          }
261    
262          this.tableModel = tableModel;
263    
264          if(this.tableModel != null) {
265             this.tableModel.addTableModelListener(tableModelListener);
266          }
267    
268          clearSortingState();
269          fireTableStructureChanged();
270       }
271    
272       /**
273        * DOCUMENT ME!
274        *
275        * @return DOCUMENT ME!
276        */
277       public TableModel getTableModel() {
278          return tableModel;
279       }
280    
281       /**
282        * DOCUMENT ME!
283        *
284        * @param aValue DOCUMENT ME!
285        * @param row DOCUMENT ME!
286        * @param column DOCUMENT ME!
287        */
288       public void setValueAt(Object aValue, int row, int column) {
289          tableModel.setValueAt(aValue, modelIndex(row), column);
290       }
291    
292       /**
293        * DOCUMENT ME!
294        *
295        * @param row DOCUMENT ME!
296        * @param column DOCUMENT ME!
297        *
298        * @return DOCUMENT ME!
299        */
300       public Object getValueAt(int row, int column) {
301          return tableModel.getValueAt(modelIndex(row), column);
302       }
303    
304       /**
305        * DOCUMENT ME!
306        *
307        * @param viewIndex DOCUMENT ME!
308        *
309        * @return DOCUMENT ME!
310        */
311       public int modelIndex(int viewIndex) {
312          return getViewToModel()[viewIndex].modelIndex;
313       }
314    
315       /**
316        * DOCUMENT ME!
317        *
318        * @param column DOCUMENT ME!
319        *
320        * @return DOCUMENT ME!
321        */
322       protected Comparator getComparator(int column) {
323          Class columnType = tableModel.getColumnClass(column);
324          Comparator comparator = (Comparator)columnComparators.get(columnType);
325    
326          if(comparator != null) {
327             return comparator;
328          }
329    
330          if(Comparable.class.isAssignableFrom(columnType)) {
331             return COMPARABLE_COMAPRATOR;
332          }
333    
334          return LEXICAL_COMPARATOR;
335       }
336    
337       /**
338        * DOCUMENT ME!
339        *
340        * @param column DOCUMENT ME!
341        * @param size DOCUMENT ME!
342        *
343        * @return DOCUMENT ME!
344        */
345       protected Icon getHeaderRendererIcon(int column, int size) {
346          Directive directive = getDirective(column);
347    
348          if(directive == EMPTY_DIRECTIVE) {
349             return null;
350          }
351    
352          return new Arrow(directive.direction == DESCENDING, size,
353             sortingColumns.indexOf(directive));
354       }
355    
356       /**
357        * DOCUMENT ME!
358        *
359        * @param column DOCUMENT ME!
360        *
361        * @return DOCUMENT ME!
362        */
363       private Directive getDirective(int column) {
364          for(int i = 0; i < sortingColumns.size(); i++) {
365             Directive directive = (Directive)sortingColumns.get(i);
366    
367             if(directive.column == column) {
368                return directive;
369             }
370          }
371    
372          return EMPTY_DIRECTIVE;
373       }
374    
375       /**
376        * DOCUMENT ME!
377        *
378        * @return DOCUMENT ME!
379        */
380       private int[] getModelToView() {
381          if(modelToView == null) {
382             int n = getViewToModel().length;
383             modelToView = new int[n];
384    
385             for(int i = 0; i < n; i++) {
386                modelToView[modelIndex(i)] = i;
387             }
388          }
389    
390          return modelToView;
391       }
392    
393       /**
394        * DOCUMENT ME!
395        *
396        * @return DOCUMENT ME!
397        */
398       private Row[] getViewToModel() {
399          if(viewToModel == null) {
400             int tableModelRowCount = tableModel.getRowCount();
401             viewToModel = new Row[tableModelRowCount];
402    
403             for(int row = 0; row < tableModelRowCount; row++) {
404                viewToModel[row] = new Row(row);
405             }
406    
407             if(isSorting()) {
408                Arrays.sort(viewToModel);
409             }
410          }
411    
412          return viewToModel;
413       }
414    
415       /**
416        * DOCUMENT ME!
417        */
418       private void cancelSorting() {
419          sortingColumns.clear();
420          sortingStatusChanged();
421       }
422    
423       /**
424        * DOCUMENT ME!
425        */
426       private void clearSortingState() {
427          viewToModel = null;
428          modelToView = null;
429       }
430    
431       /**
432        * DOCUMENT ME!
433        */
434       private void sortingStatusChanged() {
435          clearSortingState();
436          fireTableDataChanged();
437    
438          if(tableHeader != null) {
439             tableHeader.repaint();
440          }
441       }
442    
443       /**
444        * DOCUMENT ME!
445        *
446        * @author $author$
447        * @version $Revision$
448        */
449       private static class Arrow implements Icon {
450          private boolean descending;
451          private int priority;
452          private int size;
453    
454          public Arrow(boolean descending, int size, int priority) {
455             this.descending = descending;
456             this.size = size;
457             this.priority = priority;
458          }
459    
460          /**
461           * DOCUMENT ME!
462           *
463           * @return DOCUMENT ME!
464           */
465          public int getIconHeight() {
466             return size;
467          }
468    
469          /**
470           * DOCUMENT ME!
471           *
472           * @return DOCUMENT ME!
473           */
474          public int getIconWidth() {
475             return size;
476          }
477    
478          /**
479           * DOCUMENT ME!
480           *
481           * @param c DOCUMENT ME!
482           * @param g DOCUMENT ME!
483           * @param x DOCUMENT ME!
484           * @param y DOCUMENT ME!
485           */
486          public void paintIcon(Component c, Graphics g, int x, int y) {
487             // modified by Brett on 7/14/2004 to make arrow a better color            
488             //            Color color = c == null ? Color.GRAY : c.getBackground();             
489             //            // In a compound sort, make each succesive triangle 20% 
490             //            // smaller than the previous one. 
491             //            int dx = (int)(size/2*Math.pow(0.8, priority));
492             //            int dy = descending ? dx : -dx;
493             //            // Align icon (roughly) with font baseline. 
494             //            y = y + 5*size/6 + (descending ? -dy : 0);
495             //            int shift = descending ? 1 : -1;
496             //            g.translate(x, y);
497             //
498             //            // Right diagonal. 
499             //            g.setColor(color.darker());
500             //            g.drawLine(dx / 2, dy, 0, 0);
501             //            g.drawLine(dx / 2, dy + shift, 0, shift);
502             //            
503             //            // Left diagonal. 
504             //            g.setColor(color.brighter());
505             //            g.drawLine(dx / 2, dy, dx, 0);
506             //            g.drawLine(dx / 2, dy + shift, dx, shift);
507             //            
508             //            // Horizontal line. 
509             //            if (descending) {
510             //                g.setColor(color.darker().darker());
511             //            } else {
512             //                g.setColor(color.brighter().brighter());
513             //            }
514             //            g.drawLine(dx, 0, 0, 0);
515             //
516             //            g.setColor(color);
517             //            g.translate(-x, -y);
518             // Override base size with a value calculated from the
519             // component's font.
520             updateSize(c);
521    
522             Color color = (c == null) ? Color.BLACK : c.getForeground();
523             g.setColor(color);
524    
525             int npoints = 3;
526             int[] xpoints = new int[]{ 0, size / 2, size };
527             int[] ypoints = descending ? new int[]{ 0, size, 0 } : new int[]{ size, 0, size };
528    
529             Polygon triangle = new Polygon(xpoints, ypoints, npoints);
530    
531             // Center icon vertically within the column heading label.
532             int dy = (c.getHeight() - size) / 2;
533    
534             g.translate(x, dy);
535             g.drawPolygon(triangle);
536             g.fillPolygon(triangle);
537             g.translate(-x, -dy);
538          }
539    
540          /**
541           * DOCUMENT ME!
542           *
543           * @param c DOCUMENT ME!
544           */
545          private void updateSize(Component c) {
546             if(c != null) {
547                FontMetrics fm = c.getFontMetrics(c.getFont());
548                int baseHeight = fm.getAscent();
549    
550                // In a compound sort, make each succesive triangle 20% 
551                // smaller than the previous one. 
552                size = (int)((baseHeight * 3) / 4 * Math.pow(0.8, priority));
553             }
554          }
555       }
556    
557       /**
558        * DOCUMENT ME!
559        *
560        * @author $author$
561        * @version $Revision$
562        */
563       private static class Directive {
564          private int column;
565          private int direction;
566    
567          public Directive(int column, int direction) {
568             this.column = column;
569             this.direction = direction;
570          }
571       }
572    
573       /**
574        * DOCUMENT ME!
575        *
576        * @author $author$
577        * @version $Revision$
578        */
579       private class MouseHandler extends MouseAdapter {
580          /**
581           * DOCUMENT ME!
582           *
583           * @param e DOCUMENT ME!
584           */
585          public void mouseClicked(MouseEvent e) {
586             JTableHeader h = (JTableHeader)e.getSource();
587             TableColumnModel columnModel = h.getColumnModel();
588             int viewColumn = columnModel.getColumnIndexAtX(e.getX());
589             int column = columnModel.getColumn(viewColumn).getModelIndex();
590    
591             if(column != -1) {
592                int status = getSortingStatus(column);
593    
594                if(!e.isControlDown()) {
595                   cancelSorting();
596                }
597    
598                // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or 
599                // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed. 
600                status = status + (e.isShiftDown() ? (-1) : 1);
601                status = ((status + 4) % 3) - 1; // signed mod, returning {-1, 0, 1}
602                setSortingStatus(column, status);
603             }
604          }
605       }
606    
607       // Helper classes
608       private class Row implements Comparable {
609          private int modelIndex;
610    
611          public Row(int index) {
612             this.modelIndex = index;
613          }
614    
615          /**
616           * DOCUMENT ME!
617           *
618           * @param o DOCUMENT ME!
619           *
620           * @return DOCUMENT ME!
621           */
622          public int compareTo(Object o) {
623             int row1 = modelIndex;
624             int row2 = ((Row)o).modelIndex;
625    
626             for(Iterator it = sortingColumns.iterator(); it.hasNext();) {
627                Directive directive = (Directive)it.next();
628                int column = directive.column;
629                Object o1 = tableModel.getValueAt(row1, column);
630                Object o2 = tableModel.getValueAt(row2, column);
631    
632                int comparison = 0;
633    
634                // Define null less than everything, except null.
635                if((o1 == null) && (o2 == null)) {
636                   comparison = 0;
637                } else if(o1 == null) {
638                   comparison = -1;
639                } else if(o2 == null) {
640                   comparison = 1;
641                } else {
642                   comparison = getComparator(column).compare(o1, o2);
643                }
644    
645                if(comparison != 0) {
646                   return (directive.direction == DESCENDING) ? (-comparison) : comparison;
647                }
648             }
649    
650             return 0;
651          }
652       }
653    
654       /**
655        * DOCUMENT ME!
656        *
657        * @author $author$
658        * @version $Revision$
659        */
660       private class SortableHeaderRenderer implements TableCellRenderer {
661          private TableCellRenderer tableCellRenderer;
662    
663          public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
664             this.tableCellRenderer = tableCellRenderer;
665          }
666    
667          /**
668           * DOCUMENT ME!
669           *
670           * @param table DOCUMENT ME!
671           * @param value DOCUMENT ME!
672           * @param isSelected DOCUMENT ME!
673           * @param hasFocus DOCUMENT ME!
674           * @param row DOCUMENT ME!
675           * @param column DOCUMENT ME!
676           *
677           * @return DOCUMENT ME!
678           */
679          public Component getTableCellRendererComponent(JTable table, Object value,
680             boolean isSelected, boolean hasFocus, int row, int column) {
681             Component c = tableCellRenderer.getTableCellRendererComponent(table, value,
682                   isSelected, hasFocus, row, column);
683    
684             if(c instanceof JLabel) {
685                JLabel l = (JLabel)c;
686                l.setHorizontalTextPosition(JLabel.LEFT);
687    
688                int modelColumn = table.convertColumnIndexToModel(column);
689                l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize()));
690             }
691    
692             return c;
693          }
694       }
695    
696       /**
697        * DOCUMENT ME!
698        *
699        * @author $author$
700        * @version $Revision$
701        */
702       private class TableModelHandler implements TableModelListener {
703          /**
704           * DOCUMENT ME!
705           *
706           * @param e DOCUMENT ME!
707           */
708          public void tableChanged(TableModelEvent e) {
709             // If we're not sorting by anything, just pass the event along.             
710             if(!isSorting()) {
711                clearSortingState();
712                fireTableChanged(e);
713    
714                return;
715             }
716    
717             // If the table structure has changed, cancel the sorting; the             
718             // sorting columns may have been either moved or deleted from             
719             // the model. 
720             if(e.getFirstRow() == TableModelEvent.HEADER_ROW) {
721                cancelSorting();
722                fireTableChanged(e);
723    
724                return;
725             }
726    
727             // We can map a cell event through to the view without widening             
728             // when the following conditions apply: 
729             // 
730             // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and, 
731             // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
732             // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and, 
733             // d) a reverse lookup will not trigger a sort (modelToView != null)
734             //
735             // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
736             // 
737             // The last check, for (modelToView != null) is to see if modelToView 
738             // is already allocated. If we don't do this check; sorting can become 
739             // a performance bottleneck for applications where cells  
740             // change rapidly in different parts of the table. If cells 
741             // change alternately in the sorting column and then outside of             
742             // it this class can end up re-sorting on alternate cell updates - 
743             // which can be a performance problem for large tables. The last 
744             // clause avoids this problem. 
745             int column = e.getColumn();
746    
747             if((e.getFirstRow() == e.getLastRow()) &&
748                   (column != TableModelEvent.ALL_COLUMNS) &&
749                   (getSortingStatus(column) == NOT_SORTED) && (modelToView != null)) {
750                int viewIndex = getModelToView()[e.getFirstRow()];
751                fireTableChanged(new TableModelEvent(TableSorter.this, viewIndex,
752                      viewIndex, column, e.getType()));
753    
754                return;
755             }
756    
757             // Something has happened to the data that may have invalidated the row order. 
758             clearSortingState();
759             fireTableDataChanged();
760    
761             return;
762          }
763       }
764    }