View Javadoc

1   /*
2    * $Id: SortableFilterableTableDataModel.java 178 2010-10-31 18:01:20Z roland $
3    * Copyright (C) 2007-2010 Roland Krueger
4    * Created on 03.02.2010
5    * 
6    * Author: Roland Krueger (www.rolandkrueger.info)
7    *
8    * This file is part of RoKlib.
9    *
10   * This library is free software; you can redistribute it and/or
11   * modify it under the terms of the GNU Lesser General Public License
12   * as published by the Free Software Foundation; either version 2.1 of
13   * the License, or (at your option) any later version.
14   *
15   * This library is distributed in the hope that it will be useful, but
16   * WITHOUT ANY WARRANTY; without even the implied warranty of
17   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18   * Lesser General Public License for more details.
19   *
20   * You should have received a copy of the GNU Lesser General Public
21   * License along with this library; if not, write to the Free Software
22   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
23   * USA
24   */
25  package info.rolandkrueger.roklib.util.tables.filtertable;
26  
27  import info.rolandkrueger.roklib.util.helper.CheckForNull;
28  
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.Comparator;
32  import java.util.List;
33  
34  import javax.swing.SortOrder;
35  import javax.swing.event.TableModelEvent;
36  import javax.swing.event.TableModelListener;
37  import javax.swing.table.AbstractTableModel;
38  import javax.swing.table.TableModel;
39  
40  public class SortableFilterableTableDataModel<T, H extends ITableDataColumnHeader> extends
41      AbstractTableModel implements TableModelListener
42  {
43    private static final long serialVersionUID = 2016102798175233234L;
44  
45    public enum SearchMode
46    {
47      NONE, PREFIX_SIMILARITY, INFIX, BOTH
48    };
49  
50    private List<TableDataRow<T>> mTableRows;
51    private List<TableDataColumn<T, H>> mTableColumns;
52    private List<TableDataRow<T>> mVisibleRows;
53    private int mColumnCount;
54    private List<Comparator<T>> mColumnComparators;
55    private List<Class<?>> mColumnClasses;
56    private int mCurrentAddColumnIndex = 0;
57    private TableDataRow<T> mCurrentRow;
58    private int mSortColumnIndex = - 1;
59    private SortOrder mSortOrder;
60    private final StringComparator mStringComparator = new StringComparator ();
61    private final NumberComparator mnNumberComparator = new NumberComparator ();
62    private TableModel mDataFromTableModel;
63    private List<ISortableFilterableTableListener> mListeners;
64  
65    public SortableFilterableTableDataModel (TableModel tableModel, SearchMode searchCapability)
66    {
67      this (tableModel.getColumnCount (), searchCapability);
68      mDataFromTableModel = tableModel;
69      mDataFromTableModel.addTableModelListener (this);
70      buildFromTableModel ();
71    }
72  
73    public SortableFilterableTableDataModel (int columnCount, SearchMode searchCapability)
74    {
75      if (columnCount < 0)
76        throw new IllegalArgumentException ("Column count must be a positive integer.");
77      mTableColumns = new ArrayList<TableDataColumn<T, H>> (columnCount);
78      mColumnClasses = new ArrayList<Class<?>> (mColumnCount);
79      for (int i = 0; i < columnCount; ++i)
80      {
81        mTableColumns.add (new TableDataColumn<T, H> (searchCapability));
82        mColumnClasses.add (null);
83      }
84      mTableRows = new ArrayList<TableDataRow<T>> ();
85      mVisibleRows = new ArrayList<TableDataRow<T>> ();
86      mListeners = new ArrayList<ISortableFilterableTableListener> ();
87      mColumnCount = columnCount;
88    }
89  
90    public void addSortableFilterableTableListener (ISortableFilterableTableListener listener)
91    {
92      mListeners.add (listener);
93    }
94  
95    public void removeSortableFilterableTableListener (ISortableFilterableTableListener listener)
96    {
97      mListeners.remove (listener);
98    }
99  
100   public synchronized SortableFilterableTableDataModel<T, H> addStringData (String data)
101   {
102     updateInsertData (addCellLabel (data));
103     return this;
104   }
105 
106   public synchronized SortableFilterableTableDataModel<T, H> addData (String data, T additional)
107   {
108     updateInsertData (addCellLabel (data).setAdditionalData (additional));
109     return this;
110   }
111 
112   public synchronized SortableFilterableTableDataModel<T, H> addData (T data)
113   {
114     if (data != null)
115       addCellLabel (data.toString ());
116     else
117       addCellLabel ("");
118     updateInsertData (mCurrentRow.getRowData (mCurrentAddColumnIndex).setAdditionalData (data));
119     return this;
120   }
121 
122   public synchronized SortableFilterableTableDataModel<T, H> addData (int data)
123   {
124     addData (data, null);
125     return this;
126   }
127 
128   public synchronized SortableFilterableTableDataModel<T, H> addData (long data)
129   {
130     addData (data, null);
131     return this;
132   }
133 
134   public synchronized SortableFilterableTableDataModel<T, H> addData (float data)
135   {
136     addData (data, null);
137     return this;
138   }
139 
140   public synchronized SortableFilterableTableDataModel<T, H> addData (double data)
141   {
142     addData (data, null);
143     return this;
144   }
145 
146   public synchronized SortableFilterableTableDataModel<T, H> addData (byte data)
147   {
148     addData (data, null);
149     return this;
150   }
151 
152   public synchronized SortableFilterableTableDataModel<T, H> addData (short data)
153   {
154     addData (data, null);
155     return this;
156   }
157 
158   public synchronized SortableFilterableTableDataModel<T, H> addData (char data)
159   {
160     addData (data, null);
161     return this;
162   }
163 
164   public synchronized SortableFilterableTableDataModel<T, H> addData (int data, T additional)
165   {
166     updateInsertData (getCurrentAddRow ().addCellData (mCurrentAddColumnIndex, data)
167         .setAdditionalData (additional));
168     return this;
169   }
170 
171   public synchronized SortableFilterableTableDataModel<T, H> addData (long data, T additional)
172   {
173     updateInsertData (getCurrentAddRow ().addCellData (mCurrentAddColumnIndex, data)
174         .setAdditionalData (additional));
175     return this;
176   }
177 
178   public synchronized SortableFilterableTableDataModel<T, H> addData (double data, T additional)
179   {
180     updateInsertData (getCurrentAddRow ().addCellData (mCurrentAddColumnIndex, data)
181         .setAdditionalData (additional));
182     return this;
183   }
184 
185   public synchronized SortableFilterableTableDataModel<T, H> addData (float data, T additional)
186   {
187     updateInsertData (getCurrentAddRow ().addCellData (mCurrentAddColumnIndex, data)
188         .setAdditionalData (additional));
189     return this;
190   }
191 
192   public synchronized SortableFilterableTableDataModel<T, H> addData (byte data, T additional)
193   {
194     updateInsertData (getCurrentAddRow ().addCellData (mCurrentAddColumnIndex, data)
195         .setAdditionalData (additional));
196     return this;
197   }
198 
199   public synchronized SortableFilterableTableDataModel<T, H> addData (char data, T additional)
200   {
201     updateInsertData (getCurrentAddRow ().addCellData (mCurrentAddColumnIndex, data)
202         .setAdditionalData (additional));
203     return this;
204   }
205 
206   private TableCellData<T> addCellLabel (String label)
207   {
208     TableDataRow<T> row = getCurrentAddRow ();
209     TableDataColumn<T, H> col = mTableColumns.get (mCurrentAddColumnIndex);
210     TableCellData<T> cellData = row.addCellData (mCurrentAddColumnIndex, label, col.isNumeric ());
211     if (col.isNumeric () && ! cellData.isNumeric ()) col.setNumeric (false);
212     return cellData;
213   }
214 
215   private TableDataRow<T> getCurrentAddRow ()
216   {
217     if (mDataFromTableModel != null)
218       throw new IllegalStateException (
219           "Cannot add data. Data is already defined through a TableModel.");
220     if (mCurrentRow == null)
221     {
222       mCurrentRow = new TableDataRow<T> (mColumnCount, mTableRows.size ());
223       mTableRows.add (mCurrentRow);
224       mVisibleRows.add (mCurrentRow);
225       // fireTableRowsInserted (mTableRows.size () - 1, mTableRows.size () - 1);
226       updateVisibleRows ();
227     }
228     return mCurrentRow;
229   }
230 
231   private void updateInsertData (TableCellData<T> cellData)
232   {
233     mTableColumns.get (mCurrentAddColumnIndex).addToSearchTree (cellData.getStringValue (),
234         mCurrentRow);
235     if (mCurrentAddColumnIndex == mColumnCount - 1)
236     {
237       mCurrentAddColumnIndex = 0;
238       mCurrentRow = null;
239     } else
240       ++mCurrentAddColumnIndex;
241   }
242 
243   public synchronized void refreshSearchIndices ()
244   {
245     int index = 0;
246     for (TableDataColumn<T, H> column : mTableColumns)
247     {
248       column.clearSearchIndices ();
249       for (TableDataRow<T> row : mTableRows)
250       {
251         column.addToSearchTree (row.getRowData (index).getStringValue (), row);
252       }
253     }
254     ++index;
255   }
256 
257   public synchronized void applyFilters ()
258   {
259     for (TableDataRow<T> row : mTableRows)
260     {
261       row.setVisible (true);
262     }
263     for (TableDataColumn<T, H> column : mTableColumns)
264     {
265       column.filterWithPrefix ();
266       column.filterWithInfix ();
267       column.filterWithSimilarContent ();
268     }
269     updateVisibleRows ();
270   }
271 
272   private void updateVisibleRows ()
273   {
274     mVisibleRows.clear ();
275     for (TableDataRow<T> row : mTableRows)
276     {
277       if (row.isVisible ())
278       {
279         mVisibleRows.add (row);
280       }
281     }
282     // fireTableStructureChanged ();
283     // fireTableDataChanged ();
284   }
285 
286   public String getInfixFilter (int columnIndex)
287   {
288     checkColumnIndex (columnIndex);
289     return mTableColumns.get (columnIndex).getCurrentInfixFilter ();
290   }
291 
292   public String getPrefixFilter (int columnIndex)
293   {
294     checkColumnIndex (columnIndex);
295     return mTableColumns.get (columnIndex).getCurrentPrefixFilter ();
296   }
297 
298   public String getMatchSimilarFilter (int columnIndex)
299   {
300     checkColumnIndex (columnIndex);
301     return mTableColumns.get (columnIndex).getCurrentMatchSimilarFilter ();
302   }
303 
304   public synchronized void setInfixFilter (String infix, int columnIndex, boolean applyFilter)
305   {
306     checkColumnIndex (columnIndex);
307     mTableColumns.get (columnIndex).setInfixFilter (infix);
308     if (applyFilter)
309     {
310       mTableColumns.get (columnIndex).filterWithInfix ();
311       updateVisibleRows ();
312     }
313   }
314 
315   public synchronized void setPrefixFilter (String prefix, int columnIndex, boolean applyFilter)
316   {
317     checkColumnIndex (columnIndex);
318     mTableColumns.get (columnIndex).setPrefixFilter (prefix);
319     if (applyFilter)
320     {
321       mTableColumns.get (columnIndex).filterWithPrefix ();
322       updateVisibleRows ();
323     }
324   }
325 
326   public synchronized void setMatchSimilarFilter (String filter, int distance, int lengthTolerance,
327       int columnIndex, boolean applyFilter)
328   {
329     checkColumnIndex (columnIndex);
330     mTableColumns.get (columnIndex).setMatchSimilarFilter (filter, distance, lengthTolerance);
331     if (applyFilter)
332     {
333       mTableColumns.get (columnIndex).filterWithSimilarContent ();
334       updateVisibleRows ();
335     }
336   }
337 
338   public synchronized void clear ()
339   {
340     mTableColumns.clear ();
341     mTableRows.clear ();
342     mVisibleRows.clear ();
343     mCurrentAddColumnIndex = 0;
344     mCurrentRow = null;
345     fireTableDataChanged ();
346   }
347 
348   public synchronized void setColumnHeader (H header, int columnIndex)
349   {
350     checkColumnIndex (columnIndex);
351     mTableColumns.get (columnIndex).setHeader (header);
352   }
353 
354   public H getColumnHeader (int columnIndex)
355   {
356     checkColumnIndex (columnIndex);
357     return mTableColumns.get (columnIndex).getHeader ();
358   }
359 
360   public synchronized Comparator<T> setColumnSortingComparator (Comparator<T> comparator,
361       int columnIndex)
362   {
363     checkColumnIndex (columnIndex);
364     if (mColumnComparators == null)
365     {
366       mColumnComparators = new ArrayList<Comparator<T>> (mColumnCount);
367       for (int i = 0; i < mColumnCount; ++i)
368         mColumnComparators.add (null);
369     }
370     Comparator<T> old = mColumnComparators.get (columnIndex);
371     mColumnComparators.set (columnIndex, comparator);
372     return old;
373   }
374 
375   public synchronized void setColumnClass (Class<?> clazz, int columnIndex)
376   {
377     checkColumnIndex (columnIndex);
378     mColumnClasses.set (columnIndex, clazz);
379   }
380 
381   public synchronized void setCaseSensitiveFiltering (boolean caseSensitive, int columnIndex)
382   {
383     checkColumnIndex (columnIndex);
384     mTableColumns.get (columnIndex).setCaseSensitiveFiltering (caseSensitive);
385     refreshSearchIndices ();
386   }
387 
388   public synchronized void resetFilters ()
389   {
390     for (TableDataColumn<T, H> column : mTableColumns)
391     {
392       column.resetFilters ();
393     }
394     for (TableDataRow<T> row : mTableRows)
395     {
396       row.setVisible (true);
397     }
398     mVisibleRows.clear ();
399     mVisibleRows.addAll (mTableRows);
400     fireResetAllFilters ();
401   }
402 
403   public Class<?> getColumnClass (int columnIndex)
404   {
405     Class<?> result = mColumnClasses.get (columnIndex);
406     if (result == null) return String.class;
407     return result;
408   }
409 
410   public int getColumnCount ()
411   {
412     return mColumnCount;
413   }
414 
415   public String getColumnName (int columnIndex)
416   {
417     checkColumnIndex (columnIndex);
418     H header = mTableColumns.get (columnIndex).getHeader ();
419     if (header == null) return "";
420     return header.getHeadline ();
421   }
422 
423   public int getRowCount ()
424   {
425     return mVisibleRows.size ();
426   }
427 
428   public Object getValueAt (int rowIndex, int columnIndex)
429   {
430     return mVisibleRows.get (rowIndex).getRowData (columnIndex).getStringValue ();
431   }
432 
433   public TableCellData<T> getCellDataAt (int rowIndex, int columnIndex)
434   {
435     return mVisibleRows.get (rowIndex).getRowData (columnIndex);
436   }
437 
438   public boolean isCellEditable (int rowIndex, int columnIndex)
439   {
440     return false;
441   }
442 
443   public synchronized void setValueAt (Object aValue, int rowIndex, int columnIndex)
444   {
445     TableDataColumn<T, H> column = mTableColumns.get (columnIndex);
446     TableDataRow<T> row = mVisibleRows.get (rowIndex);
447     TableCellData<T> cellData = row.getRowData (columnIndex);
448     String oldValue = cellData.getStringValue ();
449     String newValue = aValue.toString ();
450     cellData.setStringValue (newValue, column.isNumeric ());
451     if (column.isNumeric () && ! cellData.isNumeric ()) column.setNumeric (false);
452     column.updateSearchTree (oldValue, newValue, row);
453     fireTableCellUpdated (rowIndex, columnIndex);
454   }
455 
456   public int getMaximumCellValueLength (int forColumnIndex)
457   {
458     checkColumnIndex (forColumnIndex);
459     return mTableColumns.get (forColumnIndex).getMaximumCellValueLength ();
460   }
461 
462   private void checkColumnIndex (int index)
463   {
464     if (index < 0)
465       throw new ArrayIndexOutOfBoundsException (String.format ("index %d < 0", index));
466     if (index >= mColumnCount)
467       throw new ArrayIndexOutOfBoundsException (String.format ("index %d >= %d", index,
468           mColumnCount));
469   }
470 
471   public synchronized void restoreOriginalOrdering ()
472   {
473     mSortOrder = SortOrder.UNSORTED;
474     mSortColumnIndex = - 1;
475     Collections.sort (mVisibleRows, mStringComparator); // it doesn't matter
476                                                         // which comparator is
477                                                         // used to restore the
478                                                         // original ordering
479   }
480 
481   public synchronized void sortColumn (SortOrder order, int columnIndex)
482   {
483     checkColumnIndex (columnIndex);
484     mSortOrder = order;
485     TableDataColumn<T, H> column = mTableColumns.get (columnIndex);
486     Comparator<TableDataRow<T>> comparator = mStringComparator;
487     if (column.isNumeric ()) comparator = mnNumberComparator;
488     Comparator<T> tComparator = mColumnComparators.get (columnIndex);
489     if (tComparator != null) comparator = new TComparator (tComparator);
490 
491     mSortColumnIndex = columnIndex;
492     Collections.sort (mVisibleRows, comparator);
493     Collections.sort (mTableRows, comparator);
494     fireTableDataChanged ();
495   }
496 
497   private class TComparator implements Comparator<TableDataRow<T>>
498   {
499     private Comparator<T> mComparator;
500 
501     public TComparator (Comparator<T> comparator)
502     {
503       assert comparator != null;
504       mComparator = comparator;
505     }
506 
507     public int compare (TableDataRow<T> o1, TableDataRow<T> o2)
508     {
509       if (mSortOrder == SortOrder.UNSORTED)
510         return o1.getOriginalIndexInTable ().compareTo (o2.getOriginalIndexInTable ());
511       if (mSortOrder == SortOrder.ASCENDING)
512         return mComparator.compare (o1.getRowData (mSortColumnIndex).getAdditionalData (), o2
513             .getRowData (mSortColumnIndex).getAdditionalData ());
514       else if (mSortOrder == SortOrder.DESCENDING)
515         return mComparator.compare (o2.getRowData (mSortColumnIndex).getAdditionalData (), o1
516             .getRowData (mSortColumnIndex).getAdditionalData ());
517       return 0;
518     }
519   }
520 
521   private class StringComparator implements Comparator<TableDataRow<T>>
522   {
523     public int compare (TableDataRow<T> o1, TableDataRow<T> o2)
524     {
525       if (mSortOrder == SortOrder.UNSORTED)
526         return o1.getOriginalIndexInTable ().compareTo (o2.getOriginalIndexInTable ());
527       if (mSortOrder == SortOrder.ASCENDING)
528         return o1.getRowData (mSortColumnIndex).getStringValue ()
529             .compareTo (o2.getRowData (mSortColumnIndex).getStringValue ());
530       else if (mSortOrder == SortOrder.DESCENDING)
531         return o2.getRowData (mSortColumnIndex).getStringValue ()
532             .compareTo (o1.getRowData (mSortColumnIndex).getStringValue ());
533       return 0;
534     }
535   }
536 
537   private class NumberComparator implements Comparator<TableDataRow<T>>
538   {
539     public int compare (TableDataRow<T> o1, TableDataRow<T> o2)
540     {
541       if (mSortOrder == SortOrder.UNSORTED)
542         return o1.getOriginalIndexInTable ().compareTo (o2.getOriginalIndexInTable ());
543       if (mSortOrder == SortOrder.ASCENDING)
544         return o1.getRowData (mSortColumnIndex).getNumericValue ()
545             .compareTo (o2.getRowData (mSortColumnIndex).getNumericValue ());
546       else if (mSortOrder == SortOrder.DESCENDING)
547         return o2.getRowData (mSortColumnIndex).getNumericValue ()
548             .compareTo (o1.getRowData (mSortColumnIndex).getNumericValue ());
549       return 0;
550     }
551   }
552 
553   public void setTableModel (TableModel model)
554   {
555     CheckForNull.check (model);
556     mDataFromTableModel = model;
557     clear ();
558     buildFromTableModel ();
559   }
560 
561   private void buildFromTableModel ()
562   {
563     TableModel model = mDataFromTableModel;
564     mDataFromTableModel = null;
565     for (int r = 0; r < model.getRowCount (); ++r)
566     {
567       for (int c = 0; c < model.getColumnCount (); ++c)
568       {
569         addStringData (model.getValueAt (r, c).toString ());
570       }
571     }
572     mDataFromTableModel = model;
573   }
574 
575   public void tableChanged (TableModelEvent e)
576   {
577     if (mDataFromTableModel == null) return;
578     // TODO: make this more performant by evaluating the TableModelEvent
579     clear ();
580     buildFromTableModel ();
581   }
582 
583   private void fireResetAllFilters ()
584   {
585     for (ISortableFilterableTableListener l : new ArrayList<ISortableFilterableTableListener> (
586         mListeners))
587     {
588       l.resetAllFilters ();
589     }
590   }
591 }