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.Comparator; 21 import java.util.HashMap; 22 import java.util.List; 23 import java.util.Map; 24 import java.util.Objects; 25 26 /** 27 * A Comparator which imposes a specific order on a specific set of Objects. 28 * Objects are presented to the FixedOrderComparator in a specified order and 29 * subsequent calls to {@link #compare(Object, Object) compare} yield that order. 30 * For example: 31 * <pre> 32 * String[] planets = {"Mercury", "Venus", "Earth", "Mars"}; 33 * FixedOrderComparator distanceFromSun = new FixedOrderComparator(planets); 34 * Arrays.sort(planets); // Sort to alphabetical order 35 * Arrays.sort(planets, distanceFromSun); // Back to original order 36 * </pre> 37 * <p> 38 * Once {@code compare} has been called, the FixedOrderComparator is locked 39 * and attempts to modify it yield an UnsupportedOperationException. 40 * </p> 41 * <p> 42 * Instances of FixedOrderComparator are not synchronized. The class is not 43 * thread-safe at construction time, but it is thread-safe to perform 44 * multiple comparisons after all the setup operations are complete. 45 * </p> 46 * <p> 47 * This class is Serializable from Commons Collections 4.0. 48 * </p> 49 * 50 * @param <T> the type of objects compared by this comparator 51 * @since 3.0 52 */ 53 public class FixedOrderComparator<T> implements Comparator<T>, Serializable { 54 55 /** 56 * Enumerates the unknown object behaviors. 57 * 58 * @since 4.0 59 */ 60 public enum UnknownObjectBehavior { 61 62 /** 63 * Before unknown object behaviors. 64 */ 65 BEFORE, 66 67 /** 68 * After unknown object behaviors. 69 */ 70 AFTER, 71 72 /** 73 * Exception unknown object behaviors. 74 */ 75 EXCEPTION 76 } 77 78 /** Serialization version from Collections 4.0. */ 79 private static final long serialVersionUID = 82794675842863201L; 80 81 /** Internal map of object to position */ 82 private final Map<T, Integer> map = new HashMap<>(); 83 84 /** Counter used in determining the position in the map */ 85 private int counter; 86 87 /** Is the comparator locked against further change */ 88 private boolean isLocked; 89 90 /** The behavior in the case of an unknown object */ 91 private UnknownObjectBehavior unknownObjectBehavior = UnknownObjectBehavior.EXCEPTION; 92 93 // Constructors 94 /** 95 * Constructs an empty FixedOrderComparator. 96 */ 97 public FixedOrderComparator() { 98 } 99 100 /** 101 * Constructs a FixedOrderComparator which uses the order of the given list 102 * to compare the objects. 103 * <p> 104 * The list is copied, so later changes will not affect the comparator. 105 * 106 * @param items the items that the comparator can compare in order 107 * @throws NullPointerException if the list is null 108 */ 109 public FixedOrderComparator(final List<T> items) { 110 for (final T t : Objects.requireNonNull(items, "items")) { 111 add(t); 112 } 113 } 114 115 /** 116 * Constructs a FixedOrderComparator which uses the order of the given array 117 * to compare the objects. 118 * <p> 119 * The array is copied, so later changes will not affect the comparator. 120 * 121 * @param items the items that the comparator can compare in order 122 * @throws NullPointerException if the array is null 123 */ 124 public FixedOrderComparator(final T... items) { 125 for (final T item : Objects.requireNonNull(items, "items")) { 126 add(item); 127 } 128 } 129 130 // Methods for adding items 131 /** 132 * Adds an item, which compares as after all items known to the Comparator. 133 * If the item is already known to the Comparator, its old position is 134 * replaced with the new position. 135 * 136 * @param obj the item to be added to the Comparator. 137 * @return true if obj has been added for the first time, false if 138 * it was already known to the Comparator. 139 * @throws UnsupportedOperationException if a comparison has already been made 140 */ 141 public boolean add(final T obj) { 142 checkLocked(); 143 final Integer position = map.put(obj, Integer.valueOf(counter++)); 144 return position == null; 145 } 146 147 /** 148 * Adds a new item, which compares as equal to the given existing item. 149 * 150 * @param existingObj an item already in the Comparator's set of 151 * known objects 152 * @param newObj an item to be added to the Comparator's set of 153 * known objects 154 * @return true if newObj has been added for the first time, false if 155 * it was already known to the Comparator. 156 * @throws IllegalArgumentException if existingObject is not in the 157 * Comparator's set of known objects. 158 * @throws UnsupportedOperationException if a comparison has already been made 159 */ 160 public boolean addAsEqual(final T existingObj, final T newObj) { 161 checkLocked(); 162 final Integer position = map.get(existingObj); 163 if (position == null) { 164 throw new IllegalArgumentException(existingObj + " not known to " + this); 165 } 166 final Integer result = map.put(newObj, position); 167 return result == null; 168 } 169 170 /** 171 * Checks to see whether the comparator is now locked against further changes. 172 * 173 * @throws UnsupportedOperationException if the comparator is locked 174 */ 175 protected void checkLocked() { 176 if (isLocked()) { 177 throw new UnsupportedOperationException("Cannot modify a FixedOrderComparator after a comparison"); 178 } 179 } 180 181 // Comparator methods 182 /** 183 * Compares two objects according to the order of this Comparator. 184 * <p> 185 * It is important to note that this class will throw an IllegalArgumentException 186 * in the case of an unrecognized object. This is not specified in the 187 * Comparator interface, but is the most appropriate exception. 188 * 189 * @param obj1 the first object to compare 190 * @param obj2 the second object to compare 191 * @return negative if obj1 is less, positive if greater, zero if equal 192 * @throws IllegalArgumentException if obj1 or obj2 are not known 193 * to this Comparator and an alternative behavior has not been set 194 * via {@link #setUnknownObjectBehavior(UnknownObjectBehavior)}. 195 */ 196 @Override 197 public int compare(final T obj1, final T obj2) { 198 isLocked = true; 199 final Integer position1 = map.get(obj1); 200 final Integer position2 = map.get(obj2); 201 if (position1 == null || position2 == null) { 202 switch (unknownObjectBehavior) { 203 case BEFORE: 204 return position1 == null ? position2 == null ? 0 : -1 : 1; 205 case AFTER: 206 return position1 == null ? position2 == null ? 0 : 1 : -1; 207 case EXCEPTION: 208 final Object unknownObj = position1 == null ? obj1 : obj2; 209 throw new IllegalArgumentException("Attempting to compare unknown object " 210 + unknownObj); 211 default: //could be null 212 throw new UnsupportedOperationException("Unknown unknownObjectBehavior: " 213 + unknownObjectBehavior); 214 } 215 } 216 return position1.compareTo(position2); 217 } 218 219 @Override 220 public boolean equals(final Object obj) { 221 if (this == obj) { 222 return true; 223 } 224 if (obj == null) { 225 return false; 226 } 227 if (getClass() != obj.getClass()) { 228 return false; 229 } 230 final FixedOrderComparator<?> other = (FixedOrderComparator<?>) obj; 231 return counter == other.counter && isLocked == other.isLocked && Objects.equals(map, other.map) && unknownObjectBehavior == other.unknownObjectBehavior; 232 } 233 234 /** 235 * Gets the behavior for comparing unknown objects. 236 * 237 * @return {@link UnknownObjectBehavior} 238 */ 239 public UnknownObjectBehavior getUnknownObjectBehavior() { 240 return unknownObjectBehavior; 241 } 242 243 @Override 244 public int hashCode() { 245 return Objects.hash(counter, isLocked, map, unknownObjectBehavior); 246 } 247 248 // Bean methods / state querying methods 249 /** 250 * Returns true if modifications cannot be made to the FixedOrderComparator. 251 * FixedOrderComparators cannot be modified once they have performed a comparison. 252 * 253 * @return true if attempts to change the FixedOrderComparator yield an 254 * UnsupportedOperationException, false if it can be changed. 255 */ 256 public boolean isLocked() { 257 return isLocked; 258 } 259 260 /** 261 * Sets the behavior for comparing unknown objects. 262 * 263 * @param unknownObjectBehavior the flag for unknown behavior - 264 * UNKNOWN_AFTER, UNKNOWN_BEFORE or UNKNOWN_THROW_EXCEPTION 265 * @throws UnsupportedOperationException if a comparison has been performed 266 * @throws NullPointerException if unknownObjectBehavior is null 267 */ 268 public void setUnknownObjectBehavior(final UnknownObjectBehavior unknownObjectBehavior) { 269 checkLocked(); 270 this.unknownObjectBehavior = Objects.requireNonNull(unknownObjectBehavior, "unknownObjectBehavior"); 271 } 272 273 }