/* * $Id: FilterPipeline.java,v 1.26 2009/05/25 01:52:13 kschaefe Exp $ * * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jdesktop.swingx.decorator; import java.util.BitSet; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Vector; import javax.swing.event.EventListenerList; /** * <p>A <b><code>FilterPipeline</code></b> is used to define the set of * {@link org.jdesktop.swingx.decorator.Filter filters} * for a data-aware component such as a {@link org.jdesktop.swingx.JXList} or a * {@link org.jdesktop.swingx.JXTable}. Filtering involves interposing one or * more filters in a {@link org.jdesktop.swingx.decorator.FilterPipeline} between * a data model and a view to change the apparent order and/or number of records * in the data model. The order of filters in the filter pipeline determines the * order in which each filter is applied. The output from one filter in the * pipeline is piped as the input to the next filter in the pipeline.</p> * * <pre> * {@link org.jdesktop.swingx.decorator.Filter}[] filters = new {@link org.jdesktop.swingx.decorator.Filter}[] { * new {@link org.jdesktop.swingx.decorator.PatternFilter}("S.*", 0, 1), // regex, matchflags, column * new {@link org.jdesktop.swingx.decorator.ShuttleSorter}(1, false), // column 1, descending * new {@link org.jdesktop.swingx.decorator.ShuttleSorter}(0, true), // column 0, ascending * }; * {@link org.jdesktop.swingx.decorator.FilterPipeline} pipeline = new {@link org.jdesktop.swingx.decorator.FilterPipeline}(filters); * {@link org.jdesktop.swingx.JXTable} table = new {@link org.jdesktop.swingx.JXTable}(model); * table.setFilters(pipeline); * </pre> * * This is all you need to do in order to use <code>FilterPipeline</code>. Most * of the methods in this class are only for advanced developers who want to write * their own filter subclasses and want to override the way a filter pipeline works. * * @author Ramesh Gupta * @see org.jdesktop.swingx.decorator.Filter */ public class FilterPipeline { protected EventListenerList listenerList = new EventListenerList(); private ComponentAdapter adapter = null; private Sorter sorter = null; private final Filter[] filters; private SortController sortController; /** * Creates an empty open pipeline. * */ public FilterPipeline() { this(new Filter[] {}); } /** * Constructs a new <code>FilterPipeline</code> populated with the specified * filters that are applied in the order they appear in the list. Since filters * maintain state about the view to which they are attached, an instance of * a filter may not ever be used in more than one pipeline. * * @param inList array of filters */ public FilterPipeline(Filter... inList) { filters = reorderSorters(inList, locateSorters(inList)); assignFilters(); } /* * JW let each contained filter assign both order and pipeline. * Now we have a invariant * * (containedFilter.order >= 0) && (containedFilter.pipeline != null) * * which simplifies access logic (IMO) * */ private void assignFilters() { for (int i = 0; i < filters.length; i++) { // JW: changed to bind early and move // binding responsibility to filter // instead of fiddling around in other's bowels filters[i].assign(this, i); } } /** * Sets the sorter that the output of the filter pipeline is piped through. * This is the sorter that is installed interactively on a view by a user * action. * * This method is responsible for doing all the bookkeeping to assign/cleanup * pipeline/adapter assignments. * * @param sorter the interactive sorter, if any; null otherwise. */ protected void setSorter(Sorter sorter) { Sorter oldSorter = getSorter(); if (oldSorter == sorter) return; if (oldSorter != null) { oldSorter.assign((FilterPipeline) null); } this.sorter = sorter; if (sorter != null) { sorter.assign((FilterPipeline) null); sorter.assign(this); if (adapter != null) { sorter.assign(adapter); sorter.refresh(); } } if ((sorter == null) && isAssigned()) { fireContentsChanged(); } } /** * Returns the sorter that the output of the filter pipeline is piped through. * This is the sorter that is installed interactively on a view by a user * action. * * @return the interactive sorter, if any; null otherwise. */ protected Sorter getSorter() { return sorter; } public SortController getSortController() { if (sortController == null) { sortController = createDefaultSortController(); } return sortController; } protected SortController createDefaultSortController() { return new SorterBasedSortController(); } protected class SorterBasedSortController implements SortController { public void toggleSortOrder(int column) { toggleSortOrder(column, null); } public void toggleSortOrder(int column, Comparator comparator) { Sorter currentSorter = getSorter(); if ((currentSorter != null) && (currentSorter.getColumnIndex() == column)) { // JW: think about logic - need to update comparator? currentSorter.toggle(); } else { setSorter(createDefaultSorter(new SortKey(SortOrder.ASCENDING, column, comparator))); } } public void setSortKeys(List<? extends SortKey> keys) { if ((keys == null) || keys.isEmpty()) { setSorter(null); return; } SortKey sortKey = SortKey.getFirstSortingKey(keys); // only crappy unsorted... if (sortKey == null) return; Sorter sorter = getSorter(); if (sorter == null) { sorter = createDefaultSorter(); } sorter.setSortKey(sortKey); // technically, we could re-use the sorter // and only reset column, comparator and direction // need to detangle from TableColumn before going there... // so for now we only change the order if we have a sorter // for the given column, create a new default sorter if not // if ((currentSorter == null) || // (currentSorter.getColumnIndex() != sortKey.getColumn())) { // currentSorter = createDefaultSorter(sortKey); // } // if (currentSorter.isAscending() != sortKey.getSortOrder().isAscending()) { // currentSorter.setAscending(sortKey.getSortOrder().isAscending()); // } setSorter(sorter); } /** * creates a Sorter initialized with sortKey * @param sortKey the properties to use * @return <code>Sorter</code> initialized with the specified <code>sortKey</code> */ protected Sorter createDefaultSorter(SortKey sortKey) { Sorter sorter = createDefaultSorter(); sorter.setSortKey(sortKey); return sorter; } protected Sorter createDefaultSorter() { return new ShuttleSorter(); } @SuppressWarnings("unchecked") public List<? extends SortKey> getSortKeys() { Sorter sorter = getSorter(); if (sorter == null) { return Collections.EMPTY_LIST; } return Collections.singletonList(sorter.getSortKey()); } public SortOrder getSortOrder(int column) { Sorter sorter = getSorter(); if ((sorter == null) || (sorter.getColumnIndex() != column)) { return SortOrder.UNSORTED; } return sorter.getSortOrder(); } } /** * Assigns a {@link org.jdesktop.swingx.decorator.ComponentAdapter} to this * pipeline if no adapter has previously been assigned to the pipeline. Once an * adapter has been assigned to this pipeline, any attempt to change that will * cause an exception to be thrown. * * @param adapter the <code>ComponentAdapter</code> to assign * @throws IllegalArgumentException if adapter is null * @throws IllegalStateException if an adapter is already assigned to this * pipeline and the new adapter is not the same the existing adapter */ public final void assign(ComponentAdapter adapter) { if (adapter == null) { throw new IllegalArgumentException("null adapter"); } // also assign individual filters when adapter is bound if (this.adapter == null) { this.adapter = adapter; for (int i = 0; i < filters.length; i++) { filters[i].assign(adapter); } if (sorter != null) { sorter.assign(adapter); } flush(); } else if (this.adapter != adapter){ throw new IllegalStateException("Can't bind to a different adapter"); } } /** * * @return true if an adapter has been assigned, false otherwise */ public boolean isAssigned() { return adapter != null; } /** * Returns true if this pipeline contains the specified filter; * otherwise it returns false. * * @param filter filter whose membership in this pipeline is tested * @return true if this pipeline contains the specified filter; * otherwise it returns false * @throws NullPointerException if filter == null * */ boolean contains(Filter filter) { return (filter.equals(sorter)) || (filter.order >= 0) && (filters.length > 0) && (filters[filter.order] == filter); } /** * Returns the first filter, if any, in this pipeline, or null, if there are * no filters in this pipeline. * * @return the first filter, if any, in this pipeline, or null, if there are * no filters in this pipeline */ Filter first() { return (filters.length > 0) ? filters[0] : null; } /** * Returns the last filter, if any, in this pipeline, or null, if there are * no filters in this pipeline. * * @return the last filter, if any, in this pipeline, or null, if there are * no filters in this pipeline */ Filter last() { if (sorter != null) return sorter; return (filters.length > 0) ? filters[filters.length - 1] : null; } /** * Returns the filter after the supplied filter in this pipeline, or null, * if there aren't any filters after the supplied filter. * * @param filter a filter in this pipeline * @return the filter after the supplied filter in this pipeline, or null, * if there aren't any filters after the supplied filter */ Filter next(Filter filter) { if (last().equals(filter)) return null; return filter.order + 1 < filters.length ? filters[filter.order + 1] : getSorter(); } /** * Returns the filter before the supplied filter in this pipeline, * or null, if there aren't any filters before the supplied filter. * * @param filter a filter in this pipeline * @return the filter before the supplied filter in this pipeline, * or null, if there aren't any filters before the supplied filter */ Filter previous(Filter filter) { if (filter.equals(sorter)) return filters.length > 0 ? filters[filters.length - 1] : null; return first().equals(filter) ? null : filters[filter.order - 1]; } /** * Called when the specified filter has changed. * Cascades <b><code>filterChanged</code></b> notifications to the next * filter in the pipeline after the specified filter. If the specified filter * is the last filter in the pipeline, this method broadcasts a * <b><code>filterChanged</code></b> notification to all * <b><code>PipelineListener</code></b> objects registered with this pipeline. * * @param filter a filter in this pipeline that has changed in any way */ protected void filterChanged(Filter filter) { // JW: quick partial fix for #370-swingx: don't // fire if there are no active filters/and no sorter. // can't do anything if we have filters/sorters // because we have no notion to turn "auto-flush on model update" off // (Mustang does - and has it off by default) // JW: reverted - wrong place, depends on a specific // sortController implementation and "ripples" // (JXList selection not correctly updated) // if ((filter instanceof IdentityFilter) && (getSorter() == null)) return; Filter next = next(filter); if (next == null) { // prepared for additional event type // if (filter == getSorter()) { // fireSortOrderChanged(); // } fireContentsChanged(); } else { next.refresh(); // Cascade to next filter } } /** * returns the unfiltered data adapter size or 0 if unassigned. * * @return the unfiltered data adapter size or 0 if unassigned */ public int getInputSize() { return isAssigned() ? adapter.getRowCount() : 0; } /** * @param filter * @return returns the unfiltered data adapter size or 0 if unassigned. */ int getInputSize(Filter filter) { Filter previous = previous(filter); if (previous != null) { return previous.getSize(); } // fixed issue #64-swingx - removed precondition... (was: isAssigned()) return getInputSize(); } /** * Returns the number of records in the filtered view. * * @return the number of records in the filtered view */ public int getOutputSize() { // JW: don't need to check - but that's heavily dependent on the // implementation detail that there's always the identityFilter // (which might change any time) if (!isAssigned()) return 0; Filter last = last(); return (last == null) ? adapter.getRowCount() : last.getSize(); } /** * Convert row index from view coordinates to model coordinates * accounting for the presence of sorters and filters. This is essentially * a pass-through to the {@link org.jdesktop.swingx.decorator.Filter#convertRowIndexToModel(int) convertRowIndexToModel} * method of the <em>last</em> {@link org.jdesktop.swingx.decorator.Filter}, * if any, in this pipeline. * * @param row row index in view coordinates * @return row index in model coordinates */ public int convertRowIndexToModel(int row) { Filter last = last(); return (last == null) ? row : last.convertRowIndexToModel(row); } /** * Convert row index from model coordinates to view coordinates * accounting for the presence of sorters and filters. This is essentially * a pass-through to the {@link org.jdesktop.swingx.decorator.Filter#convertRowIndexToView(int) convertRowIndexToModel} * method of the <em>last</em> {@link org.jdesktop.swingx.decorator.Filter}, * if any, in this pipeline. * * @param row row index in model coordinates * @return row index in view coordinates */ public int convertRowIndexToView(int row) { Filter last = last(); return (last == null) ? row : last.convertRowIndexToView(row); } /** * Returns the value of the cell at the specified coordinates. * * * @param row in view coordinates * @param column in model coordinates * @return the value of the cell at the specified coordinates */ public Object getValueAt(int row, int column) { // JW: this impl relies on the fact that there's always the // identity filter installed // should use adapter if assigned and no filter Filter last = last(); return (last == null) ? null : last.getValueAt(row, column); } public void setValueAt(Object aValue, int row, int column) { // JW: this impl relies on the fact that there's always the // identity filter installed // should use adapter if assigned and no filter Filter last = last(); if (last != null) { last.setValueAt(aValue, row, column); } } public boolean isCellEditable(int row, int column) { // JW: this impl relies on the fact that there's always the // identity filter installed // should use adapter if assigned and no filter Filter last = last(); return (last == null) ? false : last.isCellEditable(row, column); } /** * Flushes the pipeline by initiating a {@link org.jdesktop.swingx.decorator.Filter#refresh() refresh} * on the <em>first</em> {@link org.jdesktop.swingx.decorator.Filter filter}, * if any, in this pipeline. After that filter has refreshed itself, it sends a * {@link #filterChanged(org.jdesktop.swingx.decorator.Filter) filterChanged} * notification to this pipeline, and the pipeline responds by initiating a * {@link org.jdesktop.swingx.decorator.Filter#refresh() refresh} * on the <em>next</em> {@link org.jdesktop.swingx.decorator.Filter filter}, * if any, in this pipeline. Eventualy, when there are no more filters left * in the pipeline, it broadcasts a {@link org.jdesktop.swingx.decorator.PipelineEvent} * signaling a {@link org.jdesktop.swingx.decorator.PipelineEvent#CONTENTS_CHANGED} * message to all {@link org.jdesktop.swingx.decorator.PipelineListener} objects * registered with this pipeline. */ public void flush() { // JW PENDING: use first! if ((filters != null) && (filters.length > 0)) { filters[0].refresh(); } else if (sorter != null) { sorter.refresh(); } } /** * Adds a listener to the list that's notified each time there is a change * to this pipeline. * * @param l the <code>PipelineListener</code> to be added */ public void addPipelineListener(PipelineListener l) { listenerList.add(PipelineListener.class, l); } /** * Removes a listener from the list that's notified each time there is a change * to this pipeline. * * @param l the <code>PipelineListener</code> to be removed */ public void removePipelineListener(PipelineListener l) { listenerList.remove(PipelineListener.class, l); } /** * Returns an array of all the pipeline listeners * registered on this <code>FilterPipeline</code>. * * @return all of this pipeline's <code>PipelineListener</code>s, * or an empty array if no pipeline listeners * are currently registered * * @see #addPipelineListener * @see #removePipelineListener */ public PipelineListener[] getPipelineListeners() { return (PipelineListener[]) listenerList.getListeners( PipelineListener.class); } /** * Notifies all registered {@link org.jdesktop.swingx.decorator.PipelineListener} * objects that the contents of this pipeline has changed. The event instance * is lazily created. */ protected void fireContentsChanged() { Object[] listeners = listenerList.getListenerList(); PipelineEvent e = null; for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == PipelineListener.class) { if (e == null) { e = new PipelineEvent(this, PipelineEvent.CONTENTS_CHANGED); } ( (PipelineListener) listeners[i + 1]).contentsChanged(e); } } } /** * Notifies all registered {@link org.jdesktop.swingx.decorator.PipelineListener} * objects that the contents of this pipeline has changed. */ protected void fireSortOrderChanged() { Object[] listeners = listenerList.getListenerList(); PipelineEvent e = null; for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == PipelineListener.class) { if (e == null) { e = new PipelineEvent(this, PipelineEvent.SORT_ORDER_CHANGED); } ( (PipelineListener) listeners[i + 1]).contentsChanged(e); } } } private List locateSorters(Filter[] inList) { BitSet sortableColumns = new BitSet(); // temporary structure for checking List<Integer> sorterLocations = new Vector<Integer>(); for (int i = 0; i < inList.length; i++) { if (inList[i] instanceof Sorter) { int columnIndex = inList[i].getColumnIndex(); if (columnIndex < 0) { throw new IndexOutOfBoundsException( "Negative column index for filter: " + inList[i]); } if (sortableColumns.get(columnIndex)) { throw new IllegalArgumentException( "Filter "+ i +" attempting to overwrite sorter for column " + columnIndex); } sortableColumns.set(columnIndex); // mark column index sorterLocations.add(i); // mark sorter index //columnSorterMap.put(new Integer(columnIndex), inList[i]); } } return sorterLocations; } private Filter[] reorderSorters(Filter[] inList, List sorterLocations) { // quick hack for issue #46-swingx: make sure we are open // without filter. if (inList.length == 0) { return new Filter[] {new IdentityFilter()}; } // always returns a new copy of inList Filter[] outList = (Filter[]) inList.clone(); // Invert the order of sorters, if any, in outList int max = sorterLocations.size() - 1; for (int i = 0; i <= max; i++) { int orig = ((Integer) sorterLocations.get(max - i)).intValue(); int copy = ((Integer) sorterLocations.get(i)).intValue(); outList[copy] = inList[orig]; } return outList; } public static class IdentityFilter extends Filter { /** * PENDING JW: fires always, even without sorter .. * Could do better - but will break behaviour of apps which relied on * the (buggy) side-effect of repainting on each change. * */ @Override public void refresh() { // if ((sortController == null) // || (sortController.getSortKeys().size() == 0)) return; super.refresh(); } @Override protected void init() { } @Override protected void reset() { } @Override protected void filter() { } @Override public int getSize() { return this.getInputSize(); } @Override protected int mapTowardModel(int row) { return row; } @Override protected int mapTowardView(int row) { return row; } } }