package com.towel.swing.table; import java.io.IOException; import java.io.InputStream; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; import javax.swing.JOptionPane; import javax.swing.JTable; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableModel; import com.towel.cfg.TowelConfig; import com.towel.swing.GuiUtils; import com.towel.swing.TextUtils; import com.towel.swing.table.adapter.TableColumnModelAdapter; import com.towel.swing.table.headerpopup.HeaderButtonListener; import com.towel.swing.table.headerpopup.HeaderPopupEvent; import com.towel.swing.table.headerpopup.HeaderPopupListener; import com.towel.swing.table.headerpopup.TableHeaderPopup; /** * TableFilter is a decorator for TableModel adding auto-filter functionality to * a supplied TableModel. TODO javadoc me * * @author Vinicius Godoy */ public class TableFilter extends AbstractTableModel { private static final String POPUP_ITM_SORT_DESC_ATTR = "popup_itm_sort_desc_attr"; private static final String POPUP_ITM_SORT_ASC_ATTR = "popup_itm_sort_asc_attr"; private static final String POPUP_CUSTOMIZE_ATTR = "popup_customize_attr"; private static final String POPUP_EMPTY_ATTR = "popup_empty_attr"; private static final String POPUP_ITM_ALL_ATTR = "popup_itm_all_attr"; private static final String POPUP_TEXT_ATTR = "popup_text"; private String popup_itm_sort_desc; private String popup_itm_sort_asc; private String popup_customize; private String popup_empty; private String popup_itm_all; private String popup_text; private static final int NO_COLUMN = -1; private Map<Integer, Filter> filters = null; private Map<Integer, List<Integer>> filterByColumn = null; private TableModel tableModel; private List<Integer> filteredRows; private TableHeaderPopup tableHeaderPopup; private HeaderPopupListener listener; private Set<Integer> disableColumns; private Set<Integer> sortedOnlyColumn; private Set<Integer> upToDateColumns; private Integer sortingColumn = NO_COLUMN; private Sorting order = Sorting.NONE; private JTableHeader header; /** * Table filter constructor. This is the only way to set a TableHeader and a * TableModel to this TableFilter.The class will use TableHeader to create a * PopUp menu and draw a Button on Table, and the TableModel to get the * table data. * * @param table * JTable to create the Filter on. */ public TableFilter(JTable table) { this(table.getTableHeader(), table.getModel()); table.setModel(this); } /** * Table filter constructor. This is the only way to set a TableHeader and a * TableModel to this TableFilter. The class will use TableHeader to create * a PopUp menu and draw a Button on Table, and the TableModel to get the * table data. * * @param tableHeader * Header from JTable where will be draw a Button and display the * PopUp menu. * @param tableModel * TableModel that return the data to display on table. */ public TableFilter(JTableHeader tableHeader, TableModel tableModel) { this.filters = new HashMap<Integer, Filter>(); this.filteredRows = new ArrayList<Integer>(); this.disableColumns = new TreeSet<Integer>(); this.sortedOnlyColumn = new TreeSet<Integer>(); this.upToDateColumns = new HashSet<Integer>(); this.filterByColumn = new HashMap<Integer, List<Integer>>(); this.header = tableHeader; tableHeader.getColumnModel().addColumnModelListener( new TableColumnModelAdapter() { @Override public void columnAdded(TableColumnModelEvent e) { int modelIndex = header.getColumnModel() .getColumn(e.getToIndex()).getModelIndex(); refreshHeader(modelIndex); } }); setTableValues(tableHeader, tableModel); setLocale(TowelConfig.getInstance().getDefaultLocale()); } /** * @param modelIndex */ private void refreshHeader(int column) { tableHeaderPopup.getPopup(column).removeAllElements(); if (disableColumns.contains(column)) return; tableHeaderPopup.getPopup(column).addElement(0, null); } /** * Sets if the column auto-filter is enabled or not. * * @param column * Index from column that will be disabled. * @param enabled * True if the column auto-filter is enabled. */ public void setColumnFilterEnabled(Integer column, boolean enabled) { if (!enabled) disableColumns.add(column); else disableColumns.remove(column); refreshHeader(column); updateFilter(); } public void setColumnSortedOnly(Integer column, boolean onlySorted) { if (onlySorted) sortedOnlyColumn.add(column); else sortedOnlyColumn.remove(column); updateFilter(); } /** * Return all filter options from a specific column. * * @param columnIndex * @return All possible values of the column. */ public Set<Object> getFilterOptions(int columnIndex) { Set<Object> set = new TreeSet<Object>(getColumnComparator(columnIndex)); if (!isFiltering()) { for (int i = 0; i < getRowCount(); i++) set.add(getValueAt(i, columnIndex)); return set; } List<Integer> itens = new ArrayList<Integer>(); processFilter(itens, columnIndex); for (int row : itens) set.add(tableModel.getValueAt(row, columnIndex)); return set; } /** * Sets a filter a table column. Only row with the value equals from filter * will be shown. * * @param columnIndex * @param filter * Filter value. */ public void setFilter(int columnIndex, Filter filter) { filters.put(columnIndex, filter); tableHeaderPopup.setModified(columnIndex, true); updateFilter(); } public String getFilterString(int columnIndex) { if (filters.containsKey(columnIndex)) return filters.get(columnIndex).toString(); return null; } public Filter getFilter(int columnIndex) { return filters.get(columnIndex); } public void setFilterByString(int columnIndex, String filter) { setFilter(columnIndex, new StringFilter(filter)); } public void setFilterByRegex(int columnIndex, String filter) { setFilter(columnIndex, new RegexFilter(filter)); } /** * Removes a filter from column. * * @param columnIndex */ public void removeFilter(int columnIndex) { filters.remove(columnIndex); updateFilter(); } private void updateFilter() { updateFilter(true); } /** * Updates the table using the filter values. Only row filtered will be * shown. */ private void updateFilter(boolean fireDataChanged) { generateColumnsIndices(); processFilter(); sortColumn(); upToDateColumns.clear(); if (fireDataChanged) fireTableDataChanged(); } private void generateColumnsIndices() { filterByColumn.clear(); for (int column = 0; column < tableModel.getColumnCount(); column++) { List<Integer> columnFilter = new ArrayList<Integer>(); for (int i = 0; i < tableModel.getRowCount(); i++) columnFilter.add(i); filterByColumn.put(column, columnFilter); if (filters.get(column) == null) continue; Iterator<Integer> it = columnFilter.iterator(); while (it.hasNext()) { int row = it.next(); Object obj = tableModel.getValueAt(row, column); if (!filters.get(column).doFilter(obj)) it.remove(); } } } private void processFilter() { if (!isFiltering()) return; processFilter(filteredRows, NO_COLUMN); } private void processFilter(List<Integer> filter, int except) { filter.clear(); for (int i = 0; i < tableModel.getRowCount(); i++) filter.add(i); for (int i = 0; i < filterByColumn.size(); i++) { if (i != except) filter.retainAll(filterByColumn.get(i)); } } /** * Sorts a column by descending or ascending order. */ private void sortColumn() { if (!isSorting()) return; Collections.sort(filteredRows, new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { Object obj1 = tableModel.getValueAt(o1, sortingColumn); Object obj2 = tableModel.getValueAt(o2, sortingColumn); if (order == Sorting.ASCENDING) return getColumnComparator(sortingColumn).compare(obj1, obj2); return getColumnComparator(sortingColumn).compare(obj2, obj1); } }); } /** * Returns the comparator from column. The comparator must be * <code>COMPARABLE_COMPARATOR</code> or <code>LEXICAL_COMPARATOR</code>. * * @param column * @return */ private Comparator<Object> getColumnComparator(Integer column) { if (Comparable.class .isAssignableFrom(tableModel.getColumnClass(column))) return COMPARABLE_COMPARATOR; return LEXICAL_COMPARATOR; } /** * Update the specific column popup menu from table header. Will be inserted * into popup menu the sorting itens and the possible values for filter. * * @param column * Column index. */ private void updateColumnPopup(int column) { if (upToDateColumns.contains(column)) return; upToDateColumns.add(column); tableHeaderPopup.getPopup(column).removeAllElements(); if (disableColumns.contains(column)) return; tableHeaderPopup.getPopup(column).addElement(popup_itm_sort_asc, getHeaderPopupListener()); tableHeaderPopup.getPopup(column).addElement(popup_itm_sort_desc, getHeaderPopupListener()); tableHeaderPopup.getPopup(column).addElement(popup_customize, getHeaderPopupListener()); tableHeaderPopup.getPopup(column).addElement(popup_empty, getHeaderPopupListener()); tableHeaderPopup.getPopup(column).addListSeparator(); tableHeaderPopup.getPopup(column).addElement(popup_itm_all, getHeaderPopupListener()); if (sortedOnlyColumn.contains(column)) return; Set<Object> filterOptions = getFilterOptions(column); for (Object obj : filterOptions) tableHeaderPopup.getPopup(column).addElement(obj, getHeaderPopupListener()); } public void setSorting(int index, Sorting order) { if (order == Sorting.NONE) { if (sortingColumn == index) { sortingColumn = NO_COLUMN; this.order = Sorting.NONE; } return; } if (sortingColumn != NO_COLUMN && !filters.containsKey(sortingColumn)) tableHeaderPopup.setModified(sortingColumn, false); sortingColumn = index; this.order = order; tableHeaderPopup.setModified(index, true); updateFilter(); } /** * Creates by lazy creation and return a listener for Header Popup. * * @return The created listener. */ private HeaderPopupListener getHeaderPopupListener() { if (listener == null) { listener = new HeaderPopupListener() { public void elementSelected(HeaderPopupEvent e) { if (e.getSource().equals(popup_itm_sort_asc)) { setSorting(e.getModelIndex(), Sorting.ASCENDING); } else if (e.getSource().equals(popup_itm_sort_desc)) { setSorting(e.getModelIndex(), Sorting.DESCENDING); } else if (e.getSource().equals(popup_itm_all)) { setSorting(e.getModelIndex(), Sorting.NONE); removeFilter(e.getModelIndex()); tableHeaderPopup.setModified(e.getModelIndex(), false); } else if (e.getSource().equals(popup_customize)) { String text = ""; if (filters.get(e.getModelIndex()) instanceof RegexFilter) text = ((RegexFilter) filters .get(e.getModelIndex())).getRegex(); String value = JOptionPane.showInputDialog( GuiUtils.getOwnerWindow(header), popup_text, text); if (value == null) return; setFilterByRegex(e.getModelIndex(), value); } else if (e.getSource().equals(popup_empty)) { setFilterByString(e.getModelIndex(), ""); } else { setFilterByString(e.getModelIndex(), e.getSource() .toString()); } } }; } return listener; } public int getColumnCount() { return tableModel.getColumnCount(); } public int getRowCount() { if (isFiltering()) return filteredRows.size(); return tableModel.getRowCount(); } public Object getValueAt(int rowIndex, int columnIndex) { return tableModel.getValueAt(getModelRow(rowIndex), columnIndex); } /** * Sets the TableHeader and TableModel on this TableFilter. This method must * be used only on contructor. * * @param header * @param tableModel */ private void setTableValues(JTableHeader header, TableModel tableModel) { this.tableModel = tableModel; this.tableHeaderPopup = new TableHeaderPopup(header, tableModel); tableHeaderPopup.addButtonListener(new HeaderButtonListener() { public void buttonClicked(HeaderPopupEvent e) { updateColumnPopup(e.getModelIndex()); } }); tableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { onTableChanged(e); } }); updateFilter(); } /** * Method called on TableChanged event generated from TableModel. This is a * very important method the TableModel is adding or removing an object from * table. If the TableModel is adding an object to table, this object will * be shown independent of the filter values. * * @param e */ private void onTableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.INSERT) { int first = filteredRows.size(); int last = filteredRows.size(); for (int row = e.getFirstRow(); row <= e.getLastRow(); row++) { filteredRows.add(row); last++; } fireTableRowsInserted(first, last - 1); upToDateColumns.clear(); // invalidate header popup } else if (e.getType() == TableModelEvent.DELETE) { if (!isFiltering()) { fireTableRowsDeleted(e.getFirstRow(), e.getLastRow()); upToDateColumns.clear(); // invalidate header popup return; } for (int row = e.getLastRow(); row >= e.getFirstRow(); row--) { int index = filteredRows.indexOf(row); if (index != -1) { filteredRows.remove(index); fireTableRowsDeleted(index, index); } } // shift up nRemoved times the index of filteredRows int nRemoved = e.getLastRow() - e.getFirstRow() + 1; for (int i = 0; i < filteredRows.size(); i++) { if (filteredRows.get(i) > e.getLastRow()) filteredRows.set(i, filteredRows.get(i) - nRemoved); } upToDateColumns.clear(); // invalidate header popup } else if (e.getType() == TableModelEvent.UPDATE) { if (e.getColumn() == TableModelEvent.ALL_COLUMNS) { if (!isFiltering()) { fireTableDataChanged(); upToDateColumns.clear(); // invalidate header popup return; } if (e.getLastRow() == Integer.MAX_VALUE) { // TableDataChanged! Integer currentSortingColumn = sortingColumn; Sorting currentOrder = order; Map<Integer, Filter> currentFilters = new HashMap<Integer, Filter>( filters); sortingColumn = NO_COLUMN; order = Sorting.NONE; filters.clear(); updateFilter(false); sortingColumn = currentSortingColumn; order = currentOrder; filters.putAll(currentFilters); updateFilter(); return; } for (int row = e.getFirstRow(); row <= e.getLastRow(); row++) { int index = filteredRows.indexOf(row); if (index != -1) fireTableRowsUpdated(index, index); } upToDateColumns.clear(); // invalidate header popup } else { fireTableCellUpdated(e.getFirstRow(), e.getColumn()); // invalidate header popup from specific column upToDateColumns.remove(e.getColumn()); } } } /** * Converts the view index to table model index. * * @param viewRow * @return Table model index. */ public int getModelRow(int viewRow) { if (viewRow == -1) return -1; if (!isFiltering()) return viewRow; return filteredRows.get(viewRow); } public int[] getModelRows(int[] viewRows) { int[] modelRows = new int[viewRows.length]; for (int i = 0; i < viewRows.length; i++) modelRows[i] = getModelRow(viewRows[i]); return modelRows; } public int getViewRow(int modelRow) { if (modelRow == -1) return -1; if (!isFiltering()) return modelRow; for (int i = 0; i < filteredRows.size(); i++) if (modelRow == filteredRows.get(i).intValue()) return i; return -1; } @Override public Class<?> getColumnClass(int columnIndex) { return tableModel.getColumnClass(columnIndex); } @Override public String getColumnName(int column) { return tableModel.getColumnName(column); } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return tableModel.isCellEditable(getModelRow(rowIndex), columnIndex); } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { tableModel.setValueAt(aValue, getModelRow(rowIndex), columnIndex); } /** * @return the Table Model. */ public TableModel getTableModel() { return tableModel; } /** * Get filtered rows. * * @return A list of rows. */ public List<Integer> getFilteredRows() { return Collections.unmodifiableList(filteredRows); } public void setLocale(Locale locale) { InputStream is = getClass().getResourceAsStream( "/res/strings_" + locale.toString() + ".properties"); Properties props = new Properties(); try { props.load(is); is.close(); setOptions(props); } catch (IOException e) { e.printStackTrace(); } } private void setOptions(Properties props) { popup_itm_sort_desc = props.getProperty(POPUP_ITM_SORT_DESC_ATTR); popup_itm_sort_asc = props.getProperty(POPUP_ITM_SORT_ASC_ATTR); popup_customize = props.getProperty(POPUP_CUSTOMIZE_ATTR); popup_empty = props.getProperty(POPUP_EMPTY_ATTR); popup_itm_all = props.getProperty(POPUP_ITM_ALL_ATTR); popup_text = props.getProperty(POPUP_TEXT_ATTR); } public boolean isFiltering() { return !filters.isEmpty() || isSorting(); } public boolean isSorting() { return sortingColumn != NO_COLUMN && order != Sorting.NONE; } public Sorting getOrder() { return order; } public Integer getSortingColumn() { return sortingColumn; } public static interface Filter { boolean doFilter(Object obj); } public static class StringFilter implements Filter { private String string = ""; public StringFilter() { } public StringFilter(String str) { this.string = str; } public boolean doFilter(Object obj) { String objStr = obj == null ? "" : obj.toString(); return string.equals(objStr); } public String getString() { return string; } public void setString(String string) { this.string = string; } } public static class RegexFilter implements Filter { private String regex = ""; public RegexFilter() { } public RegexFilter(String regex) { this.regex = regex; } public boolean doFilter(Object obj) { String regex = TextUtils.generateEscapeRegex(this.regex .toLowerCase()); regex = regex.replaceAll("\\\\\\*", ".*"); regex = regex.replaceAll("\\\\\\?", "."); String objStr = obj == null ? "" : obj.toString().toLowerCase(); return Pattern.matches(regex, objStr); } public String getRegex() { return regex; } public void setRegex(String regex) { this.regex = regex; } } public enum Sorting { NONE, ASCENDING, DESCENDING }; /** * Default comparator using objects that implements <code>Comparable</code> * interface */ private static final Comparator<Object> COMPARABLE_COMPARATOR = new Comparator<Object>() { @SuppressWarnings("unchecked") public int compare(Object o1, Object o2) { if (o1 == o2) return 0; if (o1 == null) return 1; if (o2 == null) return -1; if (o1 instanceof String) return Collator.getInstance().compare(o1, o2); return ((Comparable<Object>) o1).compareTo(o2); } }; /** Default comparator using <code>toString</code> method from objects */ private static final Comparator<Object> LEXICAL_COMPARATOR = new Comparator<Object>() { public int compare(Object o1, Object o2) { if (o1 == o2) return 0; if (o1 == null) return 1; if (o2 == null) return -1; return Collator.getInstance().compare(o1.toString(), o2.toString()); } }; }