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 < 0 or index >= 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 }