View Javadoc

1   package org.apache.velocity.tools.generic;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Comparator;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Map;
29  import org.apache.commons.beanutils.PropertyUtils;
30  import org.apache.velocity.tools.config.DefaultKey;
31  
32  /**
33   * SortTool allows a user to sort a collection (or array, iterator, etc)
34   * on any arbitary set of properties exposed by the objects contained
35   * within the collection.
36   *
37   * <p>The sort tool is specifically designed to use within a #foreach
38   * but you may find other uses for it.</p>
39   *
40   * <p>The sort tool can handle all of the collection types supported by
41   * #foreach and the same constraints apply as well as the following.
42   * Every object in the collection must support the set of properties
43   * selected to sort on. Each property which is to be sorted on must
44   * return one of the follow:
45   * <ul>
46   *   <li>Primitive type: e.g. int, char, long etc</li>
47   *   <li>Standard Object: e.g. String, Integer, Long etc</li>
48   *   <li>Object which implements the Comparable interface.</li>
49   * </ul>
50   * </p>
51   *
52   * <p>During the sort operation all properties are compared by calling
53   * compareTo() with the exception of Strings for which
54   * compareToIgnoreCase() is called.</p>
55   *
56   * <p>The sort is performed by calling Collections.sort() after
57   * marshalling the collection to sort into an appropriate collection type.
58   * The original collection will not be re-ordered; a new list containing
59   * the sorted elements will always be returned.</p>
60   *
61   * <p>The tool is used as follows:
62   * <pre>
63   * Single Property Sort
64   * #foreach($obj in $sorter.sort($objects, "name"))
65   *   $obj.name Ordinal= $obj.ordinal
66   * #end
67   * End
68   *
69   * Multiple Property Sort
70   * #foreach($obj in $sorter.sort($objects, ["name", "ordinal"]))
71   *   $obj.name, $obj.ordinal
72   * #end
73   * End
74   * </pre>
75   *
76   * The sort method takes two parameters a collection and a property name
77   * or an array of property names. The property names and corresponding
78   * methods must conform to java bean standards since commons-beanutils
79   * is used to extract the property values.</p>
80   *
81   * <p>By default the sort tool sorts ascending, you can override this by
82   * adding a sort type suffix to any property name.</p>
83   *
84   * <p>The supported suffixes are:
85   * <pre>
86   * For ascending
87   * :asc
88   * For descending
89   * :desc
90   *
91   * Example
92   * #foreach($obj in $sorter.sort($objects, ["name:asc", "ordinal:desc"]))
93   *   $obj.name, $obj.ordinal
94   * #end
95   * </pre><p>
96   *
97   * <p>This will sort first by Name in ascending order and then by Ordinal
98   * in descending order, of course you could have left the :asc off of the
99   * 'Name' property as ascending is always the default.</p>
100  *
101  * <p><pre>
102  * Example tools.xml config (if you want to use this with VelocityView):
103  * &lt;tools&gt;
104  *   &lt;toolbox scope="application"&gt;
105  *     &lt;tool class="org.apache.velocity.tools.generic.SortTool"/&gt;
106  *   &lt;/toolbox&gt;
107  * &lt;/tools&gt;
108  * </pre></p>
109  *
110  * @author S. Brett Sutton
111  * @author Nathan Bubna
112  * @since VelocityTools 1.2
113  * @version $Id: SortTool.java 564672 2007-08-10 16:50:04Z nbubna $
114  */
115 @DefaultKey("sorter")
116 public class SortTool
117 {
118 
119     public Collection sort(Collection collection)
120     {
121         return sort(collection, (List)null);
122     }
123 
124     public Collection sort(Object[] array)
125     {
126         return sort(array, (List)null);
127     }
128 
129     public Collection sort(Map map)
130     {
131         return sort(map, (List)null);
132     }
133 
134     /**
135      * Sorts the collection on a single property.
136      *
137      * @param object the collection to be sorted.
138      * @param property the property to sort on.
139      */
140     public Collection sort(Object object, String property)
141     {
142         List properties = new ArrayList(1);
143         properties.add(property);
144 
145         if (object instanceof Collection)
146         {
147             return sort((Collection)object, properties);
148         }
149         else if (object instanceof Object[])
150         {
151             return sort((Object[])object, properties);
152         }
153         else if (object instanceof Map)
154         {
155             return sort((Map)object, properties);
156         }
157         // the object type is not supported
158         return null;
159     }
160 
161     public Collection sort(Collection collection, List properties)
162     {
163         List list = new ArrayList(collection.size());
164         list.addAll(collection);
165         return internalSort(list, properties);
166     }
167 
168     public Collection sort(Map map, List properties)
169     {
170         return sort(map.values(), properties);
171     }
172 
173     public Collection sort(Object[] array, List properties)
174     {
175         return internalSort(Arrays.asList(array), properties);
176     }
177 
178     protected Collection internalSort(List list, List properties)
179     {
180         try
181         {
182             if (properties == null)
183             {
184                 Collections.sort(list);
185             } else {
186                 Collections.sort(list, new PropertiesComparator(properties));
187             }
188             return list;
189         }
190         catch (Exception e)
191         {
192             //TODO: log this
193             return null;
194         }
195     }
196 
197 
198     /**
199      * Does all of the comparisons
200      */
201     public static class PropertiesComparator
202         implements Comparator, java.io.Serializable
203     {
204         private static final int TYPE_ASCENDING = 1;
205         private static final int TYPE_DESCENDING = -1;
206 
207         public static final String TYPE_ASCENDING_SHORT = "asc";
208         public static final String TYPE_DESCENDING_SHORT = "desc";
209 
210         List properties;
211         int[] sortTypes;
212 
213         public PropertiesComparator(List props)
214         {
215             // copy the list so we can safely drop :asc and :desc suffixes
216             this.properties = new ArrayList(props.size());
217             this.properties.addAll(props);
218 
219             // determine ascending/descending
220             sortTypes = new int[properties.size()];
221 
222             for (int i = 0; i < properties.size(); i++)
223             {
224                 if (properties.get(i) == null)
225                 {
226                     throw new IllegalArgumentException("Property " + i
227                             + "is null, sort properties may not be null.");
228                 }
229 
230                 // determine if the property contains a sort type
231                 // e.g "Name:asc" means sort by property Name ascending
232                 String prop = properties.get(i).toString();
233                 int colonIndex = prop.indexOf(':');
234                 if (colonIndex != -1)
235                 {
236                     String sortType = prop.substring(colonIndex + 1);
237                     properties.set(i, prop.substring(0, colonIndex));
238 
239                     if (TYPE_ASCENDING_SHORT.equalsIgnoreCase(sortType))
240                     {
241                         sortTypes[i] = TYPE_ASCENDING;
242                     }
243                     else if (TYPE_DESCENDING_SHORT.equalsIgnoreCase(sortType))
244                     {
245                         sortTypes[i] = TYPE_DESCENDING;
246                     }
247                     else
248                     {
249                         //FIXME: log this
250                         // invalide property sort type. use default instead.
251                         sortTypes[i] = TYPE_ASCENDING;
252                     }
253                 }
254                 else
255                 {
256                     // default sort type is ascending.
257                     sortTypes[i] = TYPE_ASCENDING;
258                 }
259             }
260         }
261 
262         public int compare(Object lhs, Object rhs)
263         {
264             for (int i = 0; i < properties.size(); i++)
265             {
266                 int comparison = 0;
267                 String property = (String)properties.get(i);
268 
269                 // properties must be comparable
270                 Comparable left = getComparable(lhs, property);
271                 Comparable right = getComparable(rhs, property);
272 
273                 if (left == null && right != null)
274                 {
275                     // find out how right feels about left being null
276                     comparison = right.compareTo(null);
277                     // and reverse that (if it works)
278                     comparison *= -1;
279                 }
280                 else if (left instanceof String)
281                 {
282                     //TODO: make it optional whether or not case is ignored
283                     comparison = ((String)left).compareToIgnoreCase((String)right);
284                 }
285                 else if (left != null)
286                 {
287                     comparison = left.compareTo(right);
288                 }
289 
290                 // return the first difference we find
291                 if (comparison != 0)
292                 {
293                     // multiplied by the sort direction, of course
294                     return comparison * sortTypes[i];
295                 }
296             }
297             return 0;
298         }
299     }
300 
301     /**
302      * Safely retrieves the comparable value for the specified property
303      * from the specified object. Subclasses that wish to perform more
304      * advanced, efficient, or just different property retrieval methods
305      * should override this method to do so.
306      */
307     protected static Comparable getComparable(Object object, String property)
308     {
309         try
310         {
311             return (Comparable)PropertyUtils.getProperty(object, property);
312         }
313         catch (Exception e)
314         {
315             throw new IllegalArgumentException("Could not retrieve comparable value for '"
316                                                + property + "' from " + object + ": " + e);
317         }
318     }
319 
320 }