View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.collections4.comparators;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.BitSet;
22  import java.util.Comparator;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Objects;
26  
27  /**
28   * A ComparatorChain is a Comparator that wraps one or more Comparators in
29   * sequence. The ComparatorChain calls each Comparator in sequence until either
30   * 1) any single Comparator returns a non-zero result (and that result is then
31   * returned), or 2) the ComparatorChain is exhausted (and zero is returned).
32   * This type of sorting is very similar to multi-column sorting in SQL, and this
33   * class allows Java classes to emulate that kind of behavior when sorting a
34   * List.
35   * <p>
36   * To further facilitate SQL-like sorting, the order of any single Comparator in
37   * the list can be reversed.
38   * </p>
39   * <p>
40   * Calling a method that adds new Comparators or changes the ascend/descend sort
41   * <i>after compare(Object, Object) has been called</i> will result in an
42   * UnsupportedOperationException. However, <i>take care</i> to not alter the
43   * underlying List of Comparators or the BitSet that defines the sort order.
44   * </p>
45   * <p>
46   * Instances of ComparatorChain are not synchronized. The class is not
47   * thread-safe at construction time, but it <i>is</i> thread-safe to perform
48   * multiple comparisons after all the setup operations are complete.
49   * </p>
50   *
51   * @param <E> the type of objects compared by this comparator
52   * @since 2.0
53   */
54  public class ComparatorChain<E> implements Comparator<E>, Serializable {
55  
56      /** Serialization version from Collections 2.0. */
57      private static final long serialVersionUID = -721644942746081630L;
58  
59      /** The list of comparators in the chain. */
60      private final List<Comparator<E>> comparatorChain;
61      /** Order - false (clear) = ascend; true (set) = descend. */
62      private final BitSet orderingBits;
63     /** Whether the chain has been "locked". */
64      private boolean isLocked;
65  
66      /**
67       * Constructs a ComparatorChain with no Comparators.
68       * You must add at least one Comparator before calling
69       * the compare(Object,Object) method, or an
70       * UnsupportedOperationException is thrown
71       */
72      public ComparatorChain() {
73          this(new ArrayList<>(), new BitSet());
74      }
75  
76      /**
77       * Constructs a ComparatorChain with a single Comparator,
78       * sorting in the forward order
79       *
80       * @param comparator First comparator in the Comparator chain
81       */
82      public ComparatorChain(final Comparator<E> comparator) {
83          this(comparator, false);
84      }
85  
86      /**
87       * Constructs a Comparator chain with a single Comparator,
88       * sorting in the given order
89       *
90       * @param comparator First Comparator in the ComparatorChain
91       * @param reverse    false = forward sort; true = reverse sort
92       */
93      public ComparatorChain(final Comparator<E> comparator, final boolean reverse) {
94          comparatorChain = new ArrayList<>(1);
95          comparatorChain.add(comparator);
96          orderingBits = new BitSet(1);
97          if (reverse) {
98              orderingBits.set(0);
99          }
100     }
101 
102     /**
103      * Constructs a ComparatorChain from the Comparators in the
104      * List.  All Comparators will default to the forward
105      * sort order.
106      *
107      * @param list   List of Comparators
108      * @see #ComparatorChain(List,BitSet)
109      */
110     public ComparatorChain(final List<Comparator<E>> list) {
111         this(list, new BitSet(list.size()));
112     }
113 
114     /**
115      * Constructs a ComparatorChain from the Comparators in the
116      * given List.  The sort order of each column will be
117      * drawn from the given BitSet.  When determining the sort
118      * order for Comparator at index <i>i</i> in the List,
119      * the ComparatorChain will call BitSet.get(<i>i</i>).
120      * If that method returns <i>false</i>, the forward
121      * sort order is used; a return value of <i>true</i>
122      * indicates reverse sort order.
123      *
124      * @param list   List of Comparators.  NOTE: This constructor does not perform a
125      *               defensive copy of the list
126      * @param bits   Sort order for each Comparator.  Extra bits are ignored,
127      *               unless extra Comparators are added by another method.
128      */
129     public ComparatorChain(final List<Comparator<E>> list, final BitSet bits) {
130         comparatorChain = list;
131         orderingBits = bits;
132     }
133 
134     /**
135      * Add a Comparator to the end of the chain using the
136      * forward sort order
137      *
138      * @param comparator Comparator with the forward sort order
139      */
140     public void addComparator(final Comparator<E> comparator) {
141         addComparator(comparator, false);
142     }
143 
144     /**
145      * Add a Comparator to the end of the chain using the
146      * given sort order
147      *
148      * @param comparator Comparator to add to the end of the chain
149      * @param reverse    false = forward sort order; true = reverse sort order
150      */
151     public void addComparator(final Comparator<E> comparator, final boolean reverse) {
152         checkLocked();
153 
154         comparatorChain.add(comparator);
155         if (reverse) {
156             orderingBits.set(comparatorChain.size() - 1);
157         }
158     }
159 
160     /**
161      * Throws an exception if the {@link ComparatorChain} is empty.
162      *
163      * @throws UnsupportedOperationException if the {@link ComparatorChain} is empty
164      */
165     private void checkChainIntegrity() {
166         if (comparatorChain.isEmpty()) {
167             throw new UnsupportedOperationException("ComparatorChains must contain at least one Comparator");
168         }
169     }
170 
171     /**
172      * Throws an exception if the {@link ComparatorChain} is locked.
173      *
174      * @throws UnsupportedOperationException if the {@link ComparatorChain} is locked
175      */
176     private void checkLocked() {
177         if (isLocked) {
178             throw new UnsupportedOperationException(
179                     "Comparator ordering cannot be changed after the first comparison is performed");
180         }
181     }
182 
183     /**
184      * Perform comparisons on the Objects as per
185      * Comparator.compare(o1,o2).
186      *
187      * @param o1  the first object to compare
188      * @param o2  the second object to compare
189      * @return -1, 0, or 1
190      * @throws UnsupportedOperationException if the ComparatorChain does not contain at least one Comparator
191      */
192     @Override
193     public int compare(final E o1, final E o2) throws UnsupportedOperationException {
194         if (!isLocked) {
195             checkChainIntegrity();
196             isLocked = true;
197         }
198 
199         // iterate over all comparators in the chain
200         final Iterator<Comparator<E>> comparators = comparatorChain.iterator();
201         for (int comparatorIndex = 0; comparators.hasNext(); ++comparatorIndex) {
202 
203             final Comparator<? super E> comparator = comparators.next();
204             int retval = comparator.compare(o1, o2);
205             if (retval != 0) {
206                 // invert the order if it is a reverse sort
207                 if (orderingBits.get(comparatorIndex)) {
208                     if (retval > 0) {
209                         retval = -1;
210                     } else {
211                         retval = 1;
212                     }
213                 }
214                 return retval;
215             }
216         }
217 
218         // if comparators are exhausted, return 0
219         return 0;
220     }
221 
222     /**
223      * Returns {@code true} iff <i>that</i> Object is
224      * a {@link Comparator} whose ordering is known to be
225      * equivalent to mine.
226      * <p>
227      * This implementation returns {@code true}
228      * iff {@code <i>object</i>.{@link Object#getClass() getClass()}}
229      * equals {@code this.getClass()}, and the underlying
230      * comparators and order bits are equal.
231      * Subclasses may want to override this behavior to remain consistent
232      * with the {@link Comparator#equals(Object)} contract.
233      *
234      * @param object  the object to compare with
235      * @return true if equal
236      * @since 3.0
237      */
238     @Override
239     public boolean equals(final Object object) {
240         if (this == object) {
241             return true;
242         }
243         if (null == object) {
244             return false;
245         }
246         if (object.getClass().equals(this.getClass())) {
247             final ComparatorChain<?> chain = (ComparatorChain<?>) object;
248             return Objects.equals(orderingBits, chain.orderingBits) &&
249                    Objects.equals(comparatorChain, chain.comparatorChain);
250         }
251         return false;
252     }
253 
254     /**
255      * Implement a hash code for this comparator that is consistent with
256      * {@link #equals(Object) equals}.
257      *
258      * @return a suitable hash code
259      * @since 3.0
260      */
261     @Override
262     public int hashCode() {
263         int hash = 0;
264         if (null != comparatorChain) {
265             hash ^= comparatorChain.hashCode();
266         }
267         if (null != orderingBits) {
268             hash ^= orderingBits.hashCode();
269         }
270         return hash;
271     }
272 
273     /**
274      * Determine if modifications can still be made to the
275      * ComparatorChain.  ComparatorChains cannot be modified
276      * once they have performed a comparison.
277      *
278      * @return true = ComparatorChain cannot be modified; false =
279      *         ComparatorChain can still be modified.
280      */
281     public boolean isLocked() {
282         return isLocked;
283     }
284 
285     /**
286      * Replace the Comparator at the given index, maintaining
287      * the existing sort order.
288      *
289      * @param index      index of the Comparator to replace
290      * @param comparator Comparator to place at the given index
291      * @throws IndexOutOfBoundsException
292      *                   if index &lt; 0 or index &gt;= size()
293      */
294     public void setComparator(final int index, final Comparator<E> comparator) throws IndexOutOfBoundsException {
295         setComparator(index, comparator, false);
296     }
297 
298     /**
299      * Replace the Comparator at the given index in the
300      * ComparatorChain, using the given sort order
301      *
302      * @param index      index of the Comparator to replace
303      * @param comparator Comparator to set
304      * @param reverse    false = forward sort order; true = reverse sort order
305      */
306     public void setComparator(final int index, final Comparator<E> comparator, final boolean reverse) {
307         checkLocked();
308 
309         comparatorChain.set(index, comparator);
310         if (reverse) {
311             orderingBits.set(index);
312         } else {
313             orderingBits.clear(index);
314         }
315     }
316 
317     /**
318      * Change the sort order at the given index in the
319      * ComparatorChain to a forward sort.
320      *
321      * @param index  Index of the ComparatorChain
322      */
323     public void setForwardSort(final int index) {
324         checkLocked();
325         orderingBits.clear(index);
326     }
327 
328     /**
329      * Change the sort order at the given index in the
330      * ComparatorChain to a reverse sort.
331      *
332      * @param index  Index of the ComparatorChain
333      */
334     public void setReverseSort(final int index) {
335         checkLocked();
336         orderingBits.set(index);
337     }
338 
339     /**
340      * Number of Comparators in the current ComparatorChain.
341      *
342      * @return Comparator count
343      */
344     public int size() {
345         return comparatorChain.size();
346     }
347 
348 }