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 }