package com.onaro.util.jfc; import com.onaro.util.jfc.tables.SortingInfo; import javax.swing.table.TableModel; import javax.swing.event.*; import java.io.Serializable; import java.text.Collator; import java.util.Arrays; import java.util.Comparator; import java.util.EventListener; public class SortedTableModel implements TableModel, TableModelListener, SortingInfo { /** * The table model that is wrapped by this one and provide the actual model's * DATA. */ private TableModel sourceModel; /** * List of listeners which are notified whenever the source table model changes * or the sorting has changed. */ protected EventListenerList listenerList = new EventListenerList(); /** * Sorting column number to eliminate all sorting. */ public final static int NO_SORTING_COLUMN = -1; /** * The column number by which the table is sorted. */ private int sortingColumn = NO_SORTING_COLUMN; /** * When */ public final int BEFORE_SORTING = 1; /** * Ascending sort direction. */ private boolean ascending = true; /** * Maps sorted row numbers to rows in the source model. The index is * the index in this sorted model and the value contains the index in the * source model and the value of the sorting column at that row. * Cells beyod the table's last row contain null. */ private MapEntry mapToSource[]; /** * Compares the values of two map entries ({@link MapEntry}). */ private MapEntryComparator mapEntryComparator = new MapEntryComparator(); /** * Compare two values. The mapEntryComparator delegates the * values to this comparator. */ private Comparator valueComparator = new DefaultValueComparator(); private boolean sortingEnabled = true; /** * Constructs a table model that sorts another table model. * @param source source the table model that is sorted */ public SortedTableModel(TableModel source) { sourceModel = source; sourceModel.addTableModelListener(this); } /** * Adds a listener to the list that is notified each time a change * to the source DATA model occurs or the sorting has changed. * * @param l the listener */ public void addTableModelListener(TableModelListener l) { listenerList.add(TableModelListener.class, l); } /** * Adds a listener to the model that is notified each the sorting has changed. * * @param l the listener */ public void addSortingListener(SortingListener l) { listenerList.add(SortingListener.class, l); } /** * Returns the column class as returned by the source model. * * @param columnIndex the index of the column * @return the class returned from the source model */ public Class getColumnClass(int columnIndex) { return sourceModel.getColumnClass(columnIndex); } /** * Returns the number of columns as returned by the source model. * * @return the number of columns of the source model */ public int getColumnCount() { return sourceModel.getColumnCount(); } /** * Returns the name of the column at columnIndex as returned by * the source model. * * @param columnIndex the index of the column * @return the name of the column in the source model */ public String getColumnName(int columnIndex) { return sourceModel.getColumnName(columnIndex); } /** * Returns the number of rows in the source model. * * @return the number of rows in the source model */ public int getRowCount() { return sourceModel.getRowCount(); } /** * Forwards the query to the source model. * * @param rowIndex the row whose value is to be queried * @param columnIndex the column whose value is to be queried * @return the value Object at the specified cell */ public Object getValueAt(int rowIndex, int columnIndex) { return sourceModel.getValueAt(translateSortedToSource(rowIndex), columnIndex); } /** * Forwards the query to the source model. * * @param rowIndex the row whose value to be queried * @param columnIndex the column whose value to be queried * @return true if the cell is editable */ public boolean isCellEditable(int rowIndex, int columnIndex) { return sourceModel.isCellEditable(translateSortedToSource(rowIndex), columnIndex); } /** * Removes a listener from the list that is notified each time a * change to the source DATA model occurs or the sorting had changed. * * @param l the listener */ public void removeTableModelListener(TableModelListener l) { listenerList.remove(TableModelListener.class, l); } /** * Removes a listener from the list that is notified each the sorting had changed. * * @param l the listener */ public void removeSortingListener(SortingListener l) { listenerList.remove(SortingListener.class, l); } /** * Forward the set request to the source model. * * @param aValue the new value * @param rowIndex the row whose value is to be changed * @param columnIndex the column whose value is to be changed */ public void setValueAt(Object aValue, int rowIndex, int columnIndex) { /** * If the table is sorted and the update is in the sorting column, update * the value that is saved in the map. The sort will happen when handling * the event resulting the update that follows. */ if (sortingColumn != NO_SORTING_COLUMN && sortingColumn == columnIndex) { mapToSource[rowIndex].val = aValue; } sourceModel.setValueAt(aValue, translateSortedToSource(rowIndex), columnIndex); } /** * This fine grain notification tells listeners the exact range * of cells, rows, or columns that changed. */ public void tableChanged(TableModelEvent e) { if (sortingColumn == NO_SORTING_COLUMN) { fireTableChanged(e); } else if (sortingEnabled) { doSort(); } else if( e.getType() == TableModelEvent.INSERT ) { addRowToMapping( e.getLastRow()-e.getFirstRow() + 1); fireTableChanged(e); } } private void addRowToMapping( int number ) { MapEntry[] tmp = mapToSource; mapToSource = new MapEntry[tmp.length+number]; System.arraycopy(tmp, 0, mapToSource, 0, tmp.length); for( int cnt = 0; cnt < number; cnt++ ) { int row = tmp.length+cnt; mapToSource[row] = new MapEntry(); mapToSource[row].row = row; mapToSource[row].val = sourceModel.getValueAt(row, sortingColumn); } } /** * Gets the model that is encapsulated by this sorted-model * @return the encapsulated model */ public TableModel getSourceModel() { return sourceModel; } /** * Forwards the given notification event to all * TableModelListeners that registered * themselves as listeners for this table model. * * @param e the event to be forwarded */ public void fireTableChanged(TableModelEvent e) { /** Guaranteed to return a non-null array */ Object[] listeners = listenerList.getListenerList(); /** * Process the listeners last to first, notifying those that are * interested in this event */ for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == TableModelListener.class) { ((TableModelListener) listeners[i + 1]).tableChanged(e); } } } /** * Notify all sorting listeners that the sorting had changed. * @param afterSort distinguishes between an event fired before or after the model had * been sorted * @param col the new sorting column * @param isAscending the new sorting order */ public void fireSortingChanged(boolean afterSort, int col, boolean isAscending) { /** Guaranteed to return a non-null array */ Object[] listeners = listenerList.getListenerList(); /** * Process the listeners last to first, notifying those that are * interested in this event */ for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == SortingListener.class) { if (afterSort) { ((SortingListener) listeners[i + 1]).sortingChanged(col, isAscending); } else { ((SortingListener) listeners[i + 1]).sortingWillChange(); } } } } /** * Sets the sorting column. * @param sortingColumn the column the table is sorted by or NO_SORTING_COLUMN * to disable sorting */ public void setSortingColumn(int sortingColumn) { setSortingColumn(sortingColumn, getAscending()); } /** * Returns the index of the sorting column, or NO_SORTING_COLUMN * if the DATA is not sorted on any column. * @return the column used for sorting */ public int getSortingColumn() { return sortingColumn; } /** * Returns true if the DATA is sorted in ascending order, and false otherwise. * @return true if the DATA is sorted in ascending order, and false otherwise */ public boolean getAscending() { return this.ascending; } /** * Tells the sorting order of a given column. * @param column the column, must be the sorting column * @return true if the order is ascending */ public boolean isAscendingSorting(int column) { assert sortingColumn == column : "Column " + column + " is different than the sorting column " + sortingColumn; //$NON-NLS-1$ //$NON-NLS-2$ return ascending; } /** * Sets the flag that determines whether the sort order is ascending or descending. * @param ascending true to set ascending sorting order */ public void setAscending(boolean ascending) { setSortingColumn(getSortingColumn(), ascending); } public boolean isSortingEnabled() { return sortingEnabled; } /** * Enabling the sorting will cause sorting of the table, if the previous state was disabled. * Disabling the sorting will have the effect of freezing the current state. * @param sortingEnabled */ public void setSortingEnabled(boolean sortingEnabled) { boolean wasEnabled = this.sortingEnabled; this.sortingEnabled = sortingEnabled; if( !wasEnabled && sortingEnabled ) { doSort(); } } /** * Sets the sorting column and order. * * @param sortingColumn the new sorting column * @param ascending true id the sorting order is ascending */ public void setSortingColumn(int sortingColumn, boolean ascending) { int oldSortingColumn = this.sortingColumn; boolean oldAscending = this.ascending; if( oldSortingColumn == sortingColumn && oldAscending == ascending && sortingEnabled ) { return; } setSortingEnabled(true); fireSortingChanged(false, sortingColumn, ascending); this.sortingColumn = sortingColumn; this.ascending = ascending; if (oldSortingColumn == sortingColumn && sortingColumn != NO_SORTING_COLUMN && oldAscending != ascending) { doReverseOrder(); } else if (oldSortingColumn != sortingColumn && sortingColumn != NO_SORTING_COLUMN ) { doSort(); } fireSortingChanged(true, sortingColumn, ascending); } /** * Translates a row in this sorted model to a row in the source model. * @param row index in this model * @return row index in the source model */ public int translateSortedToSource(int row) { if (sortingColumn != NO_SORTING_COLUMN) { assert mapToSource != null : "sorting map is null when sorted by " + sortingColumn; //$NON-NLS-1$ assert row < mapToSource.length : "row " + row + " is out of bounds of the map (length " + mapToSource.length + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ assert mapToSource[row] != null : "row " + row + " is not mapped"; //$NON-NLS-1$ //$NON-NLS-2$ return mapToSource[row].row; } return row; } /** * Translates a row in the source, non-sorted model, to a row in this sorted * model. * @param row index in the source, non-sorted model * @return row index in this model */ public int translateSourceToSort(int row) { if (sortingColumn != NO_SORTING_COLUMN) { assert mapToSource != null : "sorting map is null when sorted by " + sortingColumn; //$NON-NLS-1$ assert row < mapToSource.length : "row " + row + " is out of bounds of the map (length " + mapToSource.length + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ for (int i = 0 ; i < mapToSource.length && mapToSource[i] != null; ++i) { if (mapToSource[i].row == row) { return i; } } assert false : "row " + row + " is not mapped"; //$NON-NLS-1$ //$NON-NLS-2$ return -1; } return row; } /** * Sets a comparator that will be used for the sorting. * @param valueComparator the comparator */ public void setValueComparator(Comparator valueComparator) { assert valueComparator != null : "The valueComparator mustn't be null"; //$NON-NLS-1$ this.valueComparator = valueComparator; } /** * When the table is already sorted but the order need to change, it reverse * the map array. */ private void doReverseOrder() { assert mapToSource != null : "sorting map is null when sorted by " + sortingColumn; //$NON-NLS-1$ int rows = sourceModel.getRowCount(); if (rows <= 1) { return; } int first = 0; int last = rows - 1; assert last < mapToSource.length && last >= 0 : "last row " + last + ", is out of bounds of the map (length " + mapToSource.length + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ assert mapToSource[last] != null : "row " + last + " is not mapped"; //$NON-NLS-1$ //$NON-NLS-2$ MapEntry tmp; while (last > first) { tmp = mapToSource[first]; mapToSource[first] = mapToSource[last]; mapToSource[last] = tmp; --last; ++first; } /** * Notify all the listeners that the table changed. */ fireTableChanged(new TableModelEvent(this)); } /** * Sort the source model's rows by the current sorting column in the current * order. The sorting column's values are copied into the map and the map * is than sorted. *

* The map's array is reallocated if needed and its size changes by power of 2. */ private void doSort() { /** * Make sure the map array match the source model's DATA */ int rows = sourceModel.getRowCount(); int oldCapacity = (mapToSource != null) ? mapToSource.length : 0; int newCapacity = rows; if (rows > oldCapacity) { newCapacity = (oldCapacity * 3) / 2 + 1; if (newCapacity < rows) newCapacity = rows; } else { int smallerCapacity = oldCapacity / 2; if (rows < smallerCapacity) { newCapacity = smallerCapacity; } } if (mapToSource == null || mapToSource.length != newCapacity) { mapToSource = new MapEntry[newCapacity]; } /** * Initialize the map - not sorted */ for (int row = 0; row < rows; ++row) { if (mapToSource[row] == null) { mapToSource[row] = new MapEntry(); } mapToSource[row].row = row; mapToSource[row].val = sourceModel.getValueAt(row, sortingColumn); } for (int row = rows; row < mapToSource.length; ++row) { mapToSource[row] = null; } /** * Sort the map. */ Arrays.sort(mapToSource, 0, rows, mapEntryComparator); /** * Notify all the listeners that the table changed. */ fireTableChanged(new TableModelEvent(this)); } /** * An entry in the map contains the source's row number the map is pointing * to and the sorting column's value at that row. */ static class MapEntry { int row; Object val; } /** * Compares the values of two map entries by delegating to the * valueComparator and enforces the sorting order depending on * the ascending selection. * null are regarded as the "largest". */ class MapEntryComparator implements Comparator { public int compare(MapEntry v1, MapEntry v2) { if (ascending) { return valueComparator.compare(v1.val, v2.val); } else { return valueComparator.compare(v2.val, v1.val); } } } /** * The default comparator compares the string values of the two values. */ public static class DefaultValueComparator implements Comparator, Serializable { private static final long serialVersionUID = 1L; @SuppressWarnings({"rawtypes", "unchecked"}) public int compare(Object v1, Object v2) { if (v1 == v2) return 0; if (v1 == null) return -1; if (v2 == null) return 1; assert v1 instanceof Comparable : "v1 ('" + v1 + "') isn't comparable"; //$NON-NLS-1$ //$NON-NLS-2$ assert v2 instanceof Comparable : "v2 ('" + v2 + "') isn't comparable"; //$NON-NLS-1$ //$NON-NLS-2$ Comparable c1 = (Comparable) v1; Comparable c2 = (Comparable) v2; if (!c2.getClass().equals(c1.getClass())){ c2 = c2.toString(); c1 = c1.toString(); } if (c1 instanceof String && c2 instanceof String) { // Use natural language sort order. return Collator.getInstance().compare((String)c1, (String)c2); } return c1.compareTo(c2); } } /** * Allows user to trace sorting changes. */ public interface SortingListener extends EventListener { /** * When sorting changes, tells the new sorting column and order. * @param column the number of the sorting column * @param ascending true if the sorting is ascending */ public void sortingChanged(int column, boolean ascending); /** * When the sorting is about to change, allows the table to * perform some preparations like saving the selection. */ public void sortingWillChange(); } }