package com.onaro.util.jfc.tables.filter; import java.util.NoSuchElementException; import javax.swing.event.TableModelListener; import javax.swing.table.TableModel; import org.apache.commons.lang3.StringUtils; import com.onaro.commons.swing.table.SnapshotTableModel; import com.onaro.util.jfc.EncapsulatingTable; import com.onaro.util.jfc.grouping.GroupingTable; /** * A TableModel implementation that provides an immutable filtered view of a snapshot table model. * The filtering operation is done once at construction time and the filter results do not change afterwards * (based on the reliance on {@link SnapshotTableModel}, it is guaranteed that the underlying table data will not * change). * * Calls to {@link #addTableModelListener(TableModelListener)} throws an {@link UnsupportedOperationException} * because the data in this model is never modified (so would never fire a change event). * * @author jmyers * */ public final class SnapshotFilteredTableModel implements TableModel, EncapsulatingTable { /** * Constant marker used for the acceptedRowCount value when there is no filtering active on the table model */ private static final int UNFILTERED_ACCEPTED_ROW_COUNT = -1; private final SnapshotTableModel snapshotModel; /** * Maps number of visible (accepted) rows to row numbers in the encapsulated model.

* The array is indexed by numbers visible in the JTable and the value is the number in the encapsulated model.

* In order to improve performance, the array is kept empty if no filter is set. */ private final int[] acceptedRows; private final int acceptedRowCount; private final Filter[] filters; public SnapshotFilteredTableModel(final SnapshotTableModel snapshotModel, final Filter[] filters) { this.snapshotModel = snapshotModel; if (filters == null || filters.length == 0) { // If there's no filters specified, simply initialize the model to ignore all filtering acceptedRows = null; acceptedRowCount = UNFILTERED_ACCEPTED_ROW_COUNT; this.filters = new Filter[0]; } else { // Perform the filtering int columnCount = snapshotModel.getColumnCount(); int[] activeFilterColumns = new int[filters.length]; int activeFilterColumnsCount = 0; activeFilterColumnsCount = 0; this.filters = new Filter[columnCount]; for (int col = 0; col < filters.length; col++) { Filter sourceFilter = filters[col]; // Skip non-existent filters if (sourceFilter == null) { continue; } Filter filter = sourceFilter.clone(); this.filters[col] = filter; if (filter.isActive() && filter.isValid()) { activeFilterColumns[activeFilterColumnsCount++] = col; } } if (activeFilterColumnsCount > 0) { // Temporary variable that will be used to fill in acceptedRowCount when the filtering is complete int tempAcceptedRowCount = 0; // The number of rows in the encapsulated model int encRowCount = this.snapshotModel.getRowCount(); // Array to keep track of the mapping between filtered rows and the source row index acceptedRows = new int[encRowCount]; // Iterate over each row in the encapsulated model for (int encRow = 0; encRow < encRowCount; ++encRow) { boolean accepted = true; // Iterate over the active filters for (int i = 0; i < activeFilterColumnsCount; i++) { int activeFilterColumn = activeFilterColumns[i]; Filter filter = filters[activeFilterColumn]; // Get the value from the encapsulated model Object value = this.snapshotModel.getValueAt(encRow, activeFilterColumn); // Check whether the value is accepted by the filter accepted = filter.isAccepted(value); // No need to continue checking if this row is filtered out if (!accepted) { break; } } // If the row isn't filtered out if (accepted) { // Add the row to the mapping of accepted rows acceptedRows[tempAcceptedRowCount++] = encRow; } } // Set the final accepted row count this.acceptedRowCount = tempAcceptedRowCount; } else { // If there's no active filters, simply initialize the model to ignore all filtering acceptedRows = null; acceptedRowCount = UNFILTERED_ACCEPTED_ROW_COUNT; } } } public Object getFilterPattern(int column) { return filters[column] != null ? filters[column].getPattern() : null; } /** * Retrieve a copy of the filter at the given column index * @param column the column index * @return a clone of the filter, null if the column doesn't have a filter. */ public Filter getFilter(int column) { return filters[column] != null ? filters[column].clone() : null; } /** * Retrieve a copy of the filter array of the filters used by this model. Modifying * the Filters returned by this call will not modify the snapshot's filter. * @return a copy of the filter array */ public Filter[] getFilters() { Filter[] copyOfFilters = new Filter[filters.length]; for (int i = 0; i < filters.length; i++) { Filter source = filters[i]; copyOfFilters[i] = (source == null) ? null : source.clone(); } return copyOfFilters; } public boolean isFilterable(int column) { return column >= 0 && column < filters.length && filters[column] != null; } public boolean isFilterActive(int column) { return column >= 0 && column < filters.length && filters[column] != null && filters[column].isActive(); } public boolean isFilterValid(int column) { return filters[column] != null && filters[column].isValid(); } public String getFilterTooltip(int column) { if (column < 0 || column > filters.length - 1) { return null; } Filter filter = filters[column]; if (column >= 0 && column < filters.length && filter != null) { if (filter.isActive()) { Object pattern = filter.getPattern(); String patternStr = filter.formatPatternForTooltip(pattern); if (patternStr == null) { patternStr = String.valueOf(pattern); } // Strip out existing html elements patternStr = patternStr.replaceAll("(<.+?>)", StringUtils.EMPTY); //$NON-NLS-1$ patternStr = patternStr.replaceAll("&", "&qout;").replaceAll("<", "<").replaceAll(">", ">"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ StringBuilder tooltip = new StringBuilder(""); //$NON-NLS-1$ tooltip.append(Messages.INSTANCE.getFilter()).append(" "); //$NON-NLS-1$ if (!filter.isValid()) tooltip.append(""); //$NON-NLS-1$ else tooltip.append(""); //$NON-NLS-1$ tooltip.append(patternStr); if (filter.isValid()) tooltip.append(StringUtils.EMPTY); tooltip.append(""); //$NON-NLS-1$ return tooltip.toString(); } } return null; } @Override public int getRowInEncapsulatedModel(int row) { // Ensure snapshot is in sync with encapsulated model. // IBG-6297: Row in encapsulated model might have been deleted. if (! snapshotModel.isInSync()) { return UNMAPPED_ROW; } if (acceptedRowCount == UNFILTERED_ACCEPTED_ROW_COUNT) { return row; } else { return acceptedRows[row]; } } @Override public TableModel getEncapsulatedTableModel() { return snapshotModel.getSourceModel(); } @Override public int getRowCount() { if (acceptedRowCount == UNFILTERED_ACCEPTED_ROW_COUNT) { return snapshotModel.getRowCount(); } else { return acceptedRowCount; } } // Methods that need to translate row indices and delegate to source model @Override public Object getValueAt(int rowIndex, int columnIndex) { int encapsulatedRow = getRowInEncapsulatedModel(rowIndex); return GroupingTable.isMappedRow(encapsulatedRow)? snapshotModel.getValueAt(encapsulatedRow, columnIndex) : null; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { int encapsulatedRow = getRowInEncapsulatedModel(rowIndex); return GroupingTable.isMappedRow(encapsulatedRow)? snapshotModel.isCellEditable(encapsulatedRow, columnIndex) : false; } @Override public void setValueAt(Object value, int rowIndex, int columnIndex) { int encapsulatedRow = getRowInEncapsulatedModel(rowIndex); if (! GroupingTable.isMappedRow(encapsulatedRow)) { throw new NoSuchElementException("Row " + rowIndex + " not currently in encapsulated table"); //$NON-NLS-1$ //$NON-NLS-2$ } snapshotModel.setValueAt(value, encapsulatedRow, columnIndex); } // Methods that direct delegate to source model @Override public Class getColumnClass(int columnIndex) { return snapshotModel.getColumnClass(columnIndex); } @Override public int getColumnCount() { return snapshotModel.getColumnCount(); } @Override public String getColumnName(int columnIndex) { return snapshotModel.getColumnName(columnIndex); } // Unsupported methods @Override public void addTableModelListener(TableModelListener l) { // No-op, this model will never change throw new UnsupportedOperationException(); } @Override public void removeTableModelListener(TableModelListener l) { // No-op, this model will never change throw new UnsupportedOperationException(); } }