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    *      https://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   * <em>after compare(Object, Object) has been called</em> will result in an
42   * UnsupportedOperationException. However, <em>take care</em> 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 <em>is</em> 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  
62      /** Order - false (clear) = ascend; true (set) = descend. */
63      private final BitSet orderingBits;
64  
65      /** Whether the chain has been "locked". */
66      private boolean isLocked;
67  
68      /**
69       * Constructs a ComparatorChain with no Comparators.
70       * You must add at least one Comparator before calling
71       * the compare(Object, Object) method, or an
72       * UnsupportedOperationException is thrown
73       */
74      public ComparatorChain() {
75          this(new ArrayList<>(), new BitSet());
76      }
77  
78      /**
79       * Constructs a ComparatorChain with a single Comparator,
80       * sorting in the forward order
81       *
82       * @param comparator First comparator in the Comparator chain
83       */
84      public ComparatorChain(final Comparator<E> comparator) {
85          this(comparator, false);
86      }
87  
88      /**
89       * Constructs a Comparator chain with a single Comparator,
90       * sorting in the given order
91       *
92       * @param comparator First Comparator in the ComparatorChain
93       * @param reverse    false = forward sort; true = reverse sort
94       */
95      public ComparatorChain(final Comparator<E> comparator, final boolean reverse) {
96          comparatorChain = new ArrayList<>(1);
97          comparatorChain.add(comparator);
98          orderingBits = new BitSet(1);
99          if (reverse) {
100             orderingBits.set(0);
101         }
102     }
103 
104     /**
105      * Constructs a ComparatorChain from the Comparators in the
106      * List.  All Comparators will default to the forward
107      * sort order.
108      *
109      * @param list   List of Comparators
110      * @see #ComparatorChain(List,BitSet)
111      */
112     public ComparatorChain(final List<Comparator<E>> list) {
113         this(list, new BitSet(list.size()));
114     }
115 
116     /**
117      * Constructs a ComparatorChain from the Comparators in the
118      * given List.  The sort order of each column will be
119      * drawn from the given BitSet.  When determining the sort
120      * order for Comparator at index <em>i</em> in the List,
121      * the ComparatorChain will call BitSet.get(<em>i</em>).
122      * If that method returns <em>false</em>, the forward
123      * sort order is used; a return value of <em>true</em>
124      * indicates reverse sort order.
125      *
126      * @param list   List of Comparators.  NOTE: This constructor does not perform a
127      *               defensive copy of the list
128      * @param bits   Sort order for each Comparator.  Extra bits are ignored,
129      *               unless extra Comparators are added by another method.
130      */
131     public ComparatorChain(final List<Comparator<E>> list, final BitSet bits) {
132         comparatorChain = list;
133         orderingBits = bits;
134     }
135 
136     /**
137      * Add a Comparator to the end of the chain using the
138      * forward sort order
139      *
140      * @param comparator Comparator with the forward sort order
141      */
142     public void addComparator(final Comparator<E> comparator) {
143         addComparator(comparator, false);
144     }
145 
146     /**
147      * Add a Comparator to the end of the chain using the
148      * given sort order
149      *
150      * @param comparator Comparator to add to the end of the chain
151      * @param reverse    false = forward sort order; true = reverse sort order
152      */
153     public void addComparator(final Comparator<E> comparator, final boolean reverse) {
154         checkLocked();
155 
156         comparatorChain.add(comparator);
157         if (reverse) {
158             orderingBits.set(comparatorChain.size() - 1);
159         }
160     }
161 
162     /**
163      * Throws an exception if the {@link ComparatorChain} is empty.
164      *
165      * @throws UnsupportedOperationException if the {@link ComparatorChain} is empty
166      */
167     private void checkChainIntegrity() {
168         if (comparatorChain.isEmpty()) {
169             throw new UnsupportedOperationException("ComparatorChains must contain at least one Comparator");
170         }
171     }
172 
173     /**
174      * Throws an exception if the {@link ComparatorChain} is locked.
175      *
176      * @throws UnsupportedOperationException if the {@link ComparatorChain} is locked
177      */
178     private void checkLocked() {
179         if (isLocked) {
180             throw new UnsupportedOperationException(
181                     "Comparator ordering cannot be changed after the first comparison is performed");
182         }
183     }
184 
185     /**
186      * Perform comparisons on the Objects as per
187      * Comparator.compare(o1, o2).
188      *
189      * @param o1  The first object to compare
190      * @param o2  The second object to compare
191      * @return -1, 0, or 1
192      * @throws UnsupportedOperationException if the ComparatorChain does not contain at least one Comparator
193      */
194     @Override
195     public int compare(final E o1, final E o2) throws UnsupportedOperationException {
196         if (!isLocked) {
197             checkChainIntegrity();
198             isLocked = true;
199         }
200 
201         // iterate over all comparators in the chain
202         final Iterator<Comparator<E>> comparators = comparatorChain.iterator();
203         for (int comparatorIndex = 0; comparators.hasNext(); ++comparatorIndex) {
204 
205             final Comparator<? super E> comparator = comparators.next();
206             int retval = comparator.compare(o1, o2);
207             if (retval != 0) {
208                 // invert the order if it is a reverse sort
209                 if (orderingBits.get(comparatorIndex)) {
210                     if (retval > 0) {
211                         retval = -1;
212                     } else {
213                         retval = 1;
214                     }
215                 }
216                 return retval;
217             }
218         }
219 
220         // if comparators are exhausted, return 0
221         return 0;
222     }
223 
224     /**
225      * Returns {@code true} iff <em>that</em> Object is
226      * a {@link Comparator} whose ordering is known to be
227      * equivalent to mine.
228      * <p>
229      * This implementation returns {@code true}
230      * iff {@code <em>object</em>.{@link Object#getClass() getClass()}}
231      * equals {@code this.getClass()}, and the underlying
232      * comparators and order bits are equal.
233      * Subclasses may want to override this behavior to remain consistent
234      * with the {@link Comparator#equals(Object)} contract.
235      *
236      * @param object  The object to compare with
237      * @return true if equal
238      * @since 3.0
239      */
240     @Override
241     public boolean equals(final Object object) {
242         if (this == object) {
243             return true;
244         }
245         if (object == null) {
246             return false;
247         }
248         if (object.getClass().equals(this.getClass())) {
249             final ComparatorChain<?> chain = (ComparatorChain<?>) object;
250             return Objects.equals(orderingBits, chain.orderingBits) &&
251                    Objects.equals(comparatorChain, chain.comparatorChain);
252         }
253         return false;
254     }
255 
256     /**
257      * Implement a hash code for this comparator that is consistent with
258      * {@link #equals(Object) equals}.
259      *
260      * @return A suitable hash code
261      * @since 3.0
262      */
263     @Override
264     public int hashCode() {
265         int hash = 0;
266         if (comparatorChain != null) {
267             hash ^= comparatorChain.hashCode();
268         }
269         if (orderingBits != null) {
270             hash ^= orderingBits.hashCode();
271         }
272         return hash;
273     }
274 
275     /**
276      * Determine if modifications can still be made to the
277      * ComparatorChain.  ComparatorChains cannot be modified
278      * once they have performed a comparison.
279      *
280      * @return true = ComparatorChain cannot be modified; false =
281      *         ComparatorChain can still be modified.
282      */
283     public boolean isLocked() {
284         return isLocked;
285     }
286 
287     /**
288      * Replace the Comparator at the given index, maintaining
289      * the existing sort order.
290      *
291      * @param index      index of the Comparator to replace
292      * @param comparator Comparator to place at the given index
293      * @throws IndexOutOfBoundsException
294      *                   if index &lt; 0 or index &gt;= size()
295      */
296     public void setComparator(final int index, final Comparator<E> comparator) throws IndexOutOfBoundsException {
297         setComparator(index, comparator, false);
298     }
299 
300     /**
301      * Replace the Comparator at the given index in the
302      * ComparatorChain, using the given sort order
303      *
304      * @param index      index of the Comparator to replace
305      * @param comparator Comparator to set
306      * @param reverse    false = forward sort order; true = reverse sort order
307      */
308     public void setComparator(final int index, final Comparator<E> comparator, final boolean reverse) {
309         checkLocked();
310 
311         comparatorChain.set(index, comparator);
312         if (reverse) {
313             orderingBits.set(index);
314         } else {
315             orderingBits.clear(index);
316         }
317     }
318 
319     /**
320      * Change the sort order at the given index in the
321      * ComparatorChain to a forward sort.
322      *
323      * @param index  Index of the ComparatorChain
324      */
325     public void setForwardSort(final int index) {
326         checkLocked();
327         orderingBits.clear(index);
328     }
329 
330     /**
331      * Change the sort order at the given index in the
332      * ComparatorChain to a reverse sort.
333      *
334      * @param index  Index of the ComparatorChain
335      */
336     public void setReverseSort(final int index) {
337         checkLocked();
338         orderingBits.set(index);
339     }
340 
341     /**
342      * Number of Comparators in the current ComparatorChain.
343      *
344      * @return Comparator count
345      */
346     public int size() {
347         return comparatorChain.size();
348     }
349 
350 }