001    package org.objectweb.jac.aspects.gui;
002    
003    import java.util.Date;
004    import java.util.HashMap;
005    import java.util.Vector;
006    import javax.swing.event.TableModelEvent;
007    import javax.swing.event.TableModelListener;
008    import javax.swing.table.AbstractTableModel;
009    import org.apache.log4j.Logger;
010    import org.objectweb.jac.core.Collaboration;
011    import org.objectweb.jac.core.rtti.CollectionItem;
012    import org.objectweb.jac.core.rtti.MemberItem;
013    
014    
015    /**
016     * A sorter for TableModels. The sorter has a model (conforming to TableModel) 
017     * and itself implements TableModel. TableSorter does not store or copy 
018     * the data in the TableModel, instead it maintains an array of 
019     * integers which it keeps the same size as the number of rows in its 
020     * model. When the model changes it notifies the sorter that something 
021     * has changed eg. "rowsAdded" so that its internal array of integers 
022     * can be reallocated. As requests are made of the sorter (like 
023     * getValueAt(row, col) it redirects them to its model via the mapping 
024     * array. That way the TableSorter appears to hold another copy of the table 
025     * with the rows in a different order. The sorting algorthm used is stable 
026     * which means that it does not move around rows when its comparison 
027     * function returns 0 to denote that they are equivalent. 
028     *
029     * @version 1.5 12/17/97
030     * @author Philip Milne
031     */
032    
033    public class TableSorter extends TableMap  {
034        static Logger logger = Logger.getLogger("gui.sort");
035    
036        int indexes[];
037        /** list of SortCriteria */
038        Vector sortingColumns = new Vector();
039        int compares;
040       
041        public TableSorter() {
042            indexes = new int[0]; // for consistency
043        }
044       
045        public TableSorter(ExtendedTableModel model) {
046            setModel(model);
047        }
048    
049        /**
050         * Gets the sort criteria for a column.
051         * @return the SortCriteria of the given column, or null.
052         */
053        public SortCriteria getSortCriteria(int column) {
054            for (int i=0; i<sortingColumns.size(); i++) {
055                if (((SortCriteria)sortingColumns.get(i)).column==column) {
056                    return (SortCriteria)sortingColumns.get(i);
057                }
058            }
059            return null;
060        }
061    
062        public void setModel(ExtendedTableModel model) {
063            super.setModel(model); 
064            reallocateIndexes(); 
065            defaultSortOrder();
066        }
067    
068        /**
069         * Sets the sort column for the collection from the context or from
070         * RTTI configuration. 
071         */
072        public void defaultSortOrder() {
073            // First see if there's a config in the context
074            HashMap map = (HashMap)Collaboration.get().getAttribute(GuiAC.SORT_COLUMN);
075            if (map != null) {
076                SortCriteria criteria = (SortCriteria)map.get(getCollection());
077                if (criteria != null) {
078                    logger.debug("Using sort criteria from context: "+criteria);
079                    sortByColumn(criteria.column,criteria.ascending);
080                    return;
081                }
082            }
083    
084            // Then see if the collection has a default sort order
085            String column = GuiAC.getDefaultSortedColumn(getCollection());
086            if (column!=null) {
087                logger.debug("Using default sort order : "+column);
088                boolean ascending = true;
089                if (column.startsWith("-")) {
090                    ascending = false;
091                    column = column.substring(1);
092                } else if (column.startsWith("+")) {
093                    ascending = true;
094                    column = column.substring(1);
095                }
096                // find the column with that name
097                MemberItem[] members = getMembers();
098                for (int i=0; i<members.length; i++) {
099                    if (members[i].getName().equals(column)) {
100                        sortByColumn(i,ascending);
101                        return;
102                    }
103                }
104            }
105        }
106    
107        public int compareRowsByColumn(int row1, int row2, int column) {
108            Class type = model.getColumnClass(column);
109            ExtendedTableModel data = model;
110    
111            // Check for nulls.
112          
113            Object o1 = data.getValueAt(row1, column);
114            Object o2 = data.getValueAt(row2, column); 
115                
116            // If both values are null, return 0.
117            if (o1 == null && o2 == null) {
118                return 0; 
119            } else if (o1 == null) { // Define null less than everything. 
120                return -1; 
121            } else if (o2 == null) { 
122                return 1; 
123            }
124    
125            /*
126             * We copy all returned values from the getValue call in case
127             * an optimised model is reusing one object to return many
128             * values.  The Number subclasses in the JDK are immutable and
129             * so will not be used in this way but other subclasses of
130             * Number might want to do this to save space and avoid
131             * unnecessary heap allocation.
132             */
133          
134            if (type.getSuperclass() == java.lang.Number.class) {
135                double d1 = ((Number)o1).doubleValue();
136                double d2 = ((Number)o2).doubleValue();
137             
138                if (d1 < d2) {
139                    return -1;
140                } else if (d1 > d2) {
141                    return 1;
142                } else {
143                    return 0;
144                }
145            } else if (type == java.util.Date.class) {
146                long n1 = ((Date)o1).getTime();
147                long n2 = ((Date)o2).getTime();
148             
149                if (n1 < n2) {
150                    return -1;
151                } else if (n1 > n2) {
152                    return 1;
153                } else {
154                    return 0;
155                }
156            } else if (type == String.class) {
157                return ((String)o1).compareToIgnoreCase((String)o2);
158            } else if (type == Boolean.class) {
159                boolean b1 = ((Boolean)o1).booleanValue();
160                boolean b2 = ((Boolean)o2).booleanValue();
161             
162                if (b1 == b2) {
163                    return 0;
164                } else if (b1) { // Define false < true
165                    return 1;
166                } else {
167                    return -1;
168                }
169            } else {
170                return GuiAC.toString(data.getValueAt(row1, column)).
171                    compareToIgnoreCase(GuiAC.toString(data.getValueAt(row2, column)));
172            }
173        }
174       
175        public int compare(int row1, int row2) {
176            compares++;
177            for (int level=0; level<sortingColumns.size(); level++) {
178                SortCriteria criteria = (SortCriteria)sortingColumns.get(level);
179                int result = compareRowsByColumn(row1, row2, criteria.column);
180                if (result != 0) {
181                    return criteria.ascending ? result : -result;
182                }
183            }
184            return 0;
185        }
186       
187        /**
188         * Reset to default unsorted order of the model.
189         */
190        public void reallocateIndexes() {
191            int rowCount = model.getRowCount();
192          
193            logger.debug(this+".reallocateIndexes "+rowCount);
194    
195            // Set up a new array of indexes with the right number of elements
196            // for the new data model.
197            indexes = new int[rowCount];
198          
199            // Initialise with the identity mapping.
200            for (int row = 0; row < rowCount; row++) {
201                indexes[row] = row;
202            }
203        }
204    
205        public int getActualIndex(int row) {
206            return indexes[row];
207        }
208       
209        public void tableChanged(TableModelEvent e) {
210            if (e.getType()==TableModelEvent.INSERT || 
211                e.getType()==TableModelEvent.DELETE) {
212                logger.debug(this+".tableChanged "+
213                          (e.getType()==TableModelEvent.DELETE?"DELETE":"INSERT")+
214                          " "+e.getFirstRow()+"-"+e.getLastRow());
215                reallocateIndexes();
216                sort(this);
217                super.tableChanged(e);
218            } else {
219                logger.debug(this+".tableChanged UPDATE "+
220                          e.getFirstRow()+"-"+e.getLastRow());
221                reallocateIndexes();
222                sort(this);
223                super.tableChanged(e);
224            }
225        }
226       
227        public void checkModel() {
228            if (indexes.length != model.getRowCount()) {
229                logger.warn(this+" not informed of a change in model for collection "+
230                            getCollection().getName()+": "+indexes.length+"!="+model.getRowCount());
231            }
232        }
233       
234        public void sort(Object sender) {
235            if (sortingColumns.size()>0) {
236                logger.debug("sorting "+getCollection()+" by "+sortingColumns);
237                checkModel();
238             
239                compares = 0;
240                // n2sort();
241                // qsort(0, indexes.length-1);
242                shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
243                //System.out.println("Compares: "+compares);
244            } else {
245                reallocateIndexes();
246            }
247        }
248    
249        public void n2sort() {
250            for (int i=0; i<getRowCount(); i++) {
251                for (int j=i+1; j<getRowCount(); j++) {
252                    if (compare(indexes[i], indexes[j]) < 0) {
253                        swap(i, j);
254                    }
255                }
256            }
257        }
258       
259        // This is a home-grown implementation which we have not had time
260        // to research - it may perform poorly in some circumstances. It
261        // requires twice the space of an in-place algorithm and makes
262        // NlogN assigments shuttling the values between the two
263        // arrays. The number of compares appears to vary between N-1 and
264        // NlogN depending on the initial order but the main reason for
265        // using it here is that, unlike qsort, it is stable.
266        public void shuttlesort(int from[], int to[], int low, int high) {
267            if (high - low < 2) {
268                return;
269            }
270            int middle = (low + high)/2;
271            shuttlesort(to, from, low, middle);
272            shuttlesort(to, from, middle, high);
273          
274            int p = low;
275            int q = middle;
276          
277            /* This is an optional short-cut; at each recursive call,
278               check to see if the elements in this subset are already
279               ordered.  If so, no further comparisons are needed; the
280               sub-array can just be copied.  The array must be copied rather
281               than assigned otherwise sister calls in the recursion might
282               get out of sinc.  When the number of elements is three they
283               are partitioned so that the first set, [low, mid), has one
284               element and and the second, [mid, high), has two. We skip the
285               optimisation when the number of elements is three or less as
286               the first compare in the normal merge will produce the same
287               sequence of steps. This optimisation seems to be worthwhile
288               for partially ordered lists but some analysis is needed to
289               find out how the performance drops to Nlog(N) as the initial
290               order diminishes - it may drop very quickly.  */
291          
292            if (high - low >= 4 && compare(from[middle-1], from[middle]) <= 0) {
293                for (int i = low; i < high; i++) {
294                    to[i] = from[i];
295                }
296                return;
297            }
298          
299            // A normal merge. 
300          
301            for (int i = low; i < high; i++) {
302                if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
303                    to[i] = from[p++];
304                }
305                else {
306                    to[i] = from[q++];
307                }
308            }
309        }
310       
311        public void swap(int i, int j) {
312            int tmp = indexes[i];
313            indexes[i] = indexes[j];
314            indexes[j] = tmp;
315        }
316       
317        // The mapping only affects the contents of the data rows.
318        // Pass all requests to these rows through the mapping array: "indexes".
319       
320        public Object getValueAt(int aRow, int aColumn) {
321            checkModel();
322            if(indexes.length>aRow) {
323                logger.debug("getValueAt("+aRow+","+aColumn+") -> "+
324                          model.getValueAt(indexes[aRow], aColumn));
325                return model.getValueAt(indexes[aRow], aColumn);
326            } else {
327                return null;
328            }
329        }
330       
331        public Object getObject(int row) {
332            checkModel();
333            return model.getObject(indexes[row]);
334        }
335    
336        public int indexOf(Object object) {
337            checkModel();
338            return indexes[model.indexOf(object)];
339        }
340    
341        public Object getObject(int row, int column) {
342            checkModel();
343            return model.getObject(indexes[row],column);
344        }
345    
346        public void setValueAt(Object aValue, int aRow, int aColumn) {
347            checkModel();
348            model.setValueAt(aValue, indexes[aRow], aColumn);
349        }
350       
351        /**
352         * Sorts using values of a column
353         * @param column index of column to sort by
354         */
355        public void sortByColumn(int column) {
356            sortByColumn(column, true);
357        }
358    
359        /**
360         * Sorts using values of a column. Reverse the order if the table
361         * was already sorted by this column.
362         * @param column index of column to sort by 
363         */
364        public void toggleSortByColumn(int column) {
365            SortCriteria criteria = getSortCriteria(column);
366            SortCriteria savedCriteria;
367            if (criteria!=null) {
368                criteria.toggleAscending();
369                savedCriteria = new SortCriteria(column,criteria.isAscending());
370            } else {
371                sortingColumns.clear();
372                sortingColumns.add(new SortCriteria(column,true));
373                savedCriteria = new SortCriteria(column,true);
374            }
375            sort(this);
376            saveSortCriteria(savedCriteria);
377            super.tableChanged(new TableModelEvent(this)); 
378        }
379    
380        public void sortByColumn(int column, boolean ascending) {
381            logger.debug("sortByColumn "+column+
382                         "("+(ascending?"":"-")+(column>=0?getHeaders()[column]:"none")+")");
383            sortingColumns.clear();
384    
385            if (column>=0) {
386                sortingColumns.add(new SortCriteria(column,ascending));
387            }
388            sort(this);
389    
390            saveSortCriteria(column>=0?new SortCriteria(column,ascending):null);
391    
392            super.tableChanged(new TableModelEvent(this)); 
393        }
394    
395        /**
396         * Save the sort criteria in the context
397         * @param criteria the sort criteria to save
398         */
399        protected void saveSortCriteria(SortCriteria criteria) {
400            //      new Exception().printStackTrace();
401            HashMap map = (HashMap)Collaboration.get().getAttribute(GuiAC.SORT_COLUMN);
402            if (map == null) {
403                map = new HashMap();
404                Collaboration.get().addAttribute(GuiAC.SORT_COLUMN, map);
405            }
406            map.put(getCollection(), criteria);
407        }
408    }