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 * <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 /** 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 <em>i</em> in the List,
119 * the ComparatorChain will call BitSet.get(<em>i</em>).
120 * If that method returns <em>false</em>, the forward
121 * sort order is used; a return value of <em>true</em>
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 <em>that</em> 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 <em>object</em>.{@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 < 0 or index >= 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 }