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 }