001/*
002 * $Id$
003 *
004 * Copyright 2009 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 *
021 */
022package org.jdesktop.swingx.sort;
023
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Comparator;
027import java.util.List;
028
029import javax.swing.DefaultRowSorter;
030import javax.swing.SortOrder;
031
032import org.jdesktop.swingx.renderer.StringValue;
033import org.jdesktop.swingx.renderer.StringValues;
034import org.jdesktop.swingx.util.Contract;
035
036/**
037 * A default SortController implementation used as parent class for concrete 
038 * SortControllers in SwingX.<p>
039 * 
040 * Additionally, this implementation contains a fix for core 
041 * <a href=http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6894632>Issue 6894632</a>.
042 * It guarantees to only touch the underlying model during sort/filter and during 
043 * processing the notification methods. This implies that the conversion and size query
044 * methods are valid at all times outside the internal updates, including the critical 
045 * period (in core with undefined behaviour) after the underlying model has changed and 
046 * before this sorter has been notified.
047 * 
048 * @author Jeanette Winzenburg
049 */
050public abstract class DefaultSortController<M> extends DefaultRowSorter<M, Integer> implements
051        SortController<M> {
052
053    /**
054     * Comparator that uses compareTo on the contents.
055     */
056    @SuppressWarnings({ "unchecked", "rawtypes" })
057    public static final Comparator COMPARABLE_COMPARATOR =
058            new ComparableComparator();
059
060    private final static SortOrder[] DEFAULT_CYCLE = new SortOrder[] {SortOrder.ASCENDING, SortOrder.DESCENDING};
061
062    private List<SortOrder> sortCycle;
063    
064    private boolean sortable;
065
066    private StringValueProvider stringValueProvider;
067
068    protected int cachedModelRowCount;
069    
070    public DefaultSortController() {
071        super();
072        setSortable(true);
073        setSortOrderCycle(DEFAULT_CYCLE);
074        setSortsOnUpdates(true);
075    }
076    /**
077     * {@inheritDoc} <p>
078     * 
079     */
080    @Override
081    public void setSortable(boolean sortable) {
082        this.sortable = sortable;
083    }
084    
085    /**
086     * {@inheritDoc} <p>
087     * 
088     */
089    @Override
090    public boolean isSortable() {
091        return sortable;
092    }
093    
094    /**
095     * {@inheritDoc} <p>
096     * 
097     */
098    @Override
099    public void setSortable(int column, boolean sortable) {
100        super.setSortable(column, sortable);
101    }
102    
103    /**
104     * {@inheritDoc} <p>
105     * 
106     */
107    @Override
108    public boolean isSortable(int column) {
109        if (!isSortable()) return false;
110        return super.isSortable(column);
111    }
112    
113    /**
114     * {@inheritDoc}
115     * <p>
116     * 
117     * Overridden - that is completely new implementation - to get first/next SortOrder
118     * from sort order cycle. Does nothing if the cycle is empty. 
119     */
120    @Override
121    public void toggleSortOrder(int column) {
122        checkColumn(column);
123        if (!isSortable(column))
124            return;
125        SortOrder firstInCycle = getFirstInCycle();
126        // nothing to toggle through
127        if (firstInCycle == null)
128            return;
129        List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
130        SortKey sortKey = SortUtils.getFirstSortKeyForColumn(keys, column);
131        if (keys.indexOf(sortKey) == 0)  {
132            //  primary key: in this case we'll use next sortorder in cylce
133            keys.set(0, new SortKey(column, getNextInCycle(sortKey.getSortOrder())));
134        } else {
135            // all others: make primary with first sortOrder in cycle
136            keys.remove(sortKey);
137            keys.add(0, new SortKey(column, getFirstInCycle()));
138        }
139        if (keys.size() > getMaxSortKeys()) {
140            keys = keys.subList(0, getMaxSortKeys());
141        }
142        setSortKeys(keys);
143    }
144    
145
146    /**
147     * Returns the next SortOrder relative to the current, or null
148     * if the sort order cycle is empty. 
149     * 
150     * @param current the current SortOrder
151     * @return the next SortOrder to use, may be null if the cycle is empty.
152     */
153    private SortOrder getNextInCycle(SortOrder current) {
154        int pos = sortCycle.indexOf(current);
155        if (pos < 0) {
156            // not in cycle ... what to do?
157            return getFirstInCycle();
158        }
159        pos++;
160        if (pos >= sortCycle.size()) {
161            pos = 0;
162        }
163        return sortCycle.get(pos);
164    }
165
166    /**
167     * Returns the first SortOrder in the sort order cycle, or null if empty.
168     * 
169     * @return the first SortOrder in the sort order cycle or null if empty.
170     */
171    private SortOrder getFirstInCycle() {
172        return sortCycle.size() > 0 ? sortCycle.get(0) : null;
173    }
174
175    private void checkColumn(int column) {
176        if (column < 0 || column >= getModelWrapper().getColumnCount()) {
177            throw new IndexOutOfBoundsException(
178                    "column beyond range of TableModel");
179        }
180    }
181
182    /**
183     * {@inheritDoc} <p>
184     * 
185     * PENDING JW: toggle has two effects: makes the column the primary sort column, 
186     * and cycle through. So here we something similar. Should we?
187     *   
188     */
189    @Override
190    public void setSortOrder(int column, SortOrder sortOrder) {
191        if (!isSortable(column)) return;
192        SortKey replace = new SortKey(column, sortOrder);
193        List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
194        SortUtils.removeFirstSortKeyForColumn(keys, column);
195        keys.add(0, replace);
196        // PENDING max sort keys, respect here?
197        setSortKeys(keys);
198    }
199    
200    /**
201     * {@inheritDoc} <p>
202     * 
203     */
204    @Override
205    public SortOrder getSortOrder(int column) {
206        SortKey key = SortUtils.getFirstSortKeyForColumn(getSortKeys(), column);
207        return key != null ? key.getSortOrder() : SortOrder.UNSORTED;
208    }
209
210    /**
211     * {@inheritDoc} <p>
212     * 
213     */
214    @Override
215    public void resetSortOrders() {
216        if (!isSortable()) return;
217        List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
218        for (int i = keys.size() -1; i >= 0; i--) {
219            SortKey sortKey = keys.get(i);
220            if (isSortable(sortKey.getColumn())) {
221                keys.remove(sortKey);
222            }
223            
224        }
225        setSortKeys(keys);
226        
227    }
228    
229
230    /**
231     * {@inheritDoc} <p>
232     */
233    @Override
234    public SortOrder[] getSortOrderCycle() {
235        return sortCycle.toArray(new SortOrder[0]);
236    }
237
238    /**
239     * {@inheritDoc} <p>
240     */
241    @Override
242    public void setSortOrderCycle(SortOrder... cycle) {
243        Contract.asNotNull(cycle, "Elements of SortOrderCycle must not be null");
244        // JW: not safe enough?
245        sortCycle = Arrays.asList(cycle);
246    }
247
248    /**
249     * Sets the registry of string values. If null, the default provider is used.
250     * 
251     * @param registry the registry to get StringValues for conversion.
252     */
253    @Override
254    public void setStringValueProvider(StringValueProvider registry) {
255        this.stringValueProvider = registry;
256//        updateStringConverter();
257    }
258    
259    /**
260     * Returns the registry of string values.
261     * 
262     * @return the registry of string converters, guaranteed to never be null.
263     */
264    @Override
265    public StringValueProvider getStringValueProvider() {
266        if (stringValueProvider == null) {
267            stringValueProvider = DEFAULT_PROVIDER;
268        }
269        return stringValueProvider;
270    }
271
272    /**
273     * Returns the default cycle.
274     * 
275     * @return default sort order cycle.
276     */
277    public static SortOrder[] getDefaultSortOrderCycle() {
278        return Arrays.copyOf(DEFAULT_CYCLE, DEFAULT_CYCLE.length);
279    }
280    
281    private static final StringValueProvider DEFAULT_PROVIDER = new StringValueProvider() {
282
283        @Override
284        public StringValue getStringValue(int row, int column) {
285            return StringValues.TO_STRING;
286        }
287        
288    };
289    
290
291    @SuppressWarnings({ "unchecked", "rawtypes" })
292    private static class ComparableComparator implements Comparator {
293        @Override
294        public int compare(Object o1, Object o2) {
295            return ((Comparable)o1).compareTo(o2);
296        }
297    }
298
299//-------------------------- replacing super for more consistent conversion/rowCount behaviour
300
301    /**
302     * {@inheritDoc} <p>
303     * 
304     * Overridden to use check against <code>getViewRowCount</code> for validity.
305     * 
306     * @see #getViewRowCount()
307     */
308    @Override
309    public int convertRowIndexToModel(int viewIndex) {
310        if ((viewIndex < 0) || viewIndex >= getViewRowCount()) 
311            throw new IndexOutOfBoundsException("valid viewIndex: 0 <= index < " 
312                    + getViewRowCount() 
313                    + " but was: " + viewIndex);
314        try {
315             return super.convertRowIndexToModel(viewIndex);
316        } catch (Exception e) {
317            // this will happen only if unsorted/-filtered and super
318            // incorrectly access the model while it had been changed
319            // under its feet
320        }
321        return viewIndex;
322    }
323    
324    
325    /**
326     * {@inheritDoc} <p>
327     * 
328     * Overridden to use check against <code>getModelRowCount</code> for validity.
329     * 
330     * @see #getModelRowCount()
331     */
332    @Override
333    public int convertRowIndexToView(int modelIndex) {
334        if ((modelIndex < 0) || modelIndex >= getModelRowCount()) 
335            throw new IndexOutOfBoundsException("valid modelIndex: 0 <= index < " 
336                    + getModelRowCount() 
337                    + " but was: " + modelIndex);
338        try {
339            return super.convertRowIndexToView(modelIndex);
340        } catch (Exception e) {
341            // this will happen only if unsorted/-filtered and super
342            // incorrectly access the model while it had been changed
343            // under its feet
344        }
345        return modelIndex;
346    }
347    
348    /**
349     * {@inheritDoc} <p>
350     * 
351     * Overridden to return the model row count which corresponds to the currently 
352     * mapped model instead of accessing the model directly (as super does).
353     * This may differ from the "real" current model row count if the model has changed
354     * but this sorter not yet notified.
355     * 
356     */
357    @Override
358    public int getModelRowCount() {
359        return cachedModelRowCount;
360    }
361
362    /**
363     * {@inheritDoc} <p>
364     * 
365     * Overridden to return the model row count if no filters installed, otherwise
366     * return super.
367     * 
368     * @see #getModelRowCount()
369     * 
370     */
371    @Override
372    public int getViewRowCount() {
373        if (hasRowFilter())
374            return super.getViewRowCount();
375        return getModelRowCount();
376    }
377    
378    /**
379     * @return
380     */
381    private boolean hasRowFilter() {
382        return getRowFilter() != null;
383    }
384    
385//------------------ overridden notification methods: cache model row count    
386    @Override
387    public void allRowsChanged() {
388        cachedModelRowCount = getModelWrapper().getRowCount();
389        super.allRowsChanged();
390    }
391    @Override
392    public void modelStructureChanged() {
393        super.modelStructureChanged();
394        cachedModelRowCount = getModelWrapper().getRowCount();
395    }
396    @Override
397    public void rowsDeleted(int firstRow, int endRow) {
398        cachedModelRowCount = getModelWrapper().getRowCount();
399        super.rowsDeleted(firstRow, endRow);
400    }
401    @Override
402    public void rowsInserted(int firstRow, int endRow) {
403        cachedModelRowCount = getModelWrapper().getRowCount();
404        super.rowsInserted(firstRow, endRow);
405    }
406    
407}