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.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 /**
94 * Constructs an empty FixedOrderComparator.
95 */
96 public FixedOrderComparator() {
97 }
98
99 /**
100 * Constructs a FixedOrderComparator which uses the order of the given list
101 * to compare the objects.
102 * <p>
103 * The list is copied, so later changes will not affect the comparator.
104 *
105 * @param items The items that the comparator can compare in order
106 * @throws NullPointerException if the list is null
107 */
108 public FixedOrderComparator(final List<T> items) {
109 for (final T t : Objects.requireNonNull(items, "items")) {
110 add(t);
111 }
112 }
113
114 /**
115 * Constructs a FixedOrderComparator which uses the order of the given array
116 * to compare the objects.
117 * <p>
118 * The array is copied, so later changes will not affect the comparator.
119 *
120 * @param items The items that the comparator can compare in order
121 * @throws NullPointerException if the array is null
122 */
123 public FixedOrderComparator(final T... items) {
124 for (final T item : Objects.requireNonNull(items, "items")) {
125 add(item);
126 }
127 }
128
129 /**
130 * Adds an item, which compares as after all items known to the Comparator.
131 * If the item is already known to the Comparator, its old position is
132 * replaced with the new position.
133 *
134 * @param obj The item to be added to the Comparator.
135 * @return true if obj has been added for the first time, false if
136 * it was already known to the Comparator.
137 * @throws UnsupportedOperationException if a comparison has already been made
138 */
139 public boolean add(final T obj) {
140 checkLocked();
141 final Integer position = map.put(obj, Integer.valueOf(counter++));
142 return position == null;
143 }
144
145 /**
146 * Adds a new item, which compares as equal to the given existing item.
147 *
148 * @param existingObj An item already in the Comparator's set of
149 * known objects
150 * @param newObj An item to be added to the Comparator's set of
151 * known objects
152 * @return true if newObj has been added for the first time, false if
153 * it was already known to the Comparator.
154 * @throws IllegalArgumentException if existingObject is not in the
155 * Comparator's set of known objects.
156 * @throws UnsupportedOperationException if a comparison has already been made
157 */
158 public boolean addAsEqual(final T existingObj, final T newObj) {
159 checkLocked();
160 final Integer position = map.get(existingObj);
161 if (position == null) {
162 throw new IllegalArgumentException(existingObj + " not known to " + this);
163 }
164 final Integer result = map.put(newObj, position);
165 return result == null;
166 }
167
168 /**
169 * Checks to see whether the comparator is now locked against further changes.
170 *
171 * @throws UnsupportedOperationException if the comparator is locked
172 */
173 protected void checkLocked() {
174 if (isLocked()) {
175 throw new UnsupportedOperationException("Cannot modify a FixedOrderComparator after a comparison");
176 }
177 }
178
179 /**
180 * Compares two objects according to the order of this Comparator.
181 * <p>
182 * It is important to note that this class will throw an IllegalArgumentException
183 * in the case of an unrecognized object. This is not specified in the
184 * Comparator interface, but is the most appropriate exception.
185 *
186 * @param obj1 The first object to compare
187 * @param obj2 The second object to compare
188 * @return negative if obj1 is less, positive if greater, zero if equal
189 * @throws IllegalArgumentException if obj1 or obj2 are not known
190 * to this Comparator and an alternative behavior has not been set
191 * via {@link #setUnknownObjectBehavior(UnknownObjectBehavior)}.
192 */
193 @Override
194 public int compare(final T obj1, final T obj2) {
195 isLocked = true;
196 final Integer position1 = map.get(obj1);
197 final Integer position2 = map.get(obj2);
198 if (position1 == null || position2 == null) {
199 switch (unknownObjectBehavior) {
200 case BEFORE:
201 return position1 == null ? position2 == null ? 0 : -1 : 1;
202 case AFTER:
203 return position1 == null ? position2 == null ? 0 : 1 : -1;
204 case EXCEPTION:
205 final Object unknownObj = position1 == null ? obj1 : obj2;
206 throw new IllegalArgumentException("Attempting to compare unknown object "
207 + unknownObj);
208 default: //could be null
209 throw new UnsupportedOperationException("Unknown unknownObjectBehavior: "
210 + unknownObjectBehavior);
211 }
212 }
213 return position1.compareTo(position2);
214 }
215
216 @Override
217 public boolean equals(final Object obj) {
218 if (this == obj) {
219 return true;
220 }
221 if (obj == null) {
222 return false;
223 }
224 if (getClass() != obj.getClass()) {
225 return false;
226 }
227 final FixedOrderComparator<?> other = (FixedOrderComparator<?>) obj;
228 return counter == other.counter && isLocked == other.isLocked && Objects.equals(map, other.map) && unknownObjectBehavior == other.unknownObjectBehavior;
229 }
230
231 /**
232 * Gets the behavior for comparing unknown objects.
233 *
234 * @return {@link UnknownObjectBehavior}
235 */
236 public UnknownObjectBehavior getUnknownObjectBehavior() {
237 return unknownObjectBehavior;
238 }
239
240 @Override
241 public int hashCode() {
242 return Objects.hash(counter, isLocked, map, unknownObjectBehavior);
243 }
244
245 /**
246 * Returns true if modifications cannot be made to the FixedOrderComparator.
247 * FixedOrderComparators cannot be modified once they have performed a comparison.
248 *
249 * @return true if attempts to change the FixedOrderComparator yield an
250 * UnsupportedOperationException, false if it can be changed.
251 */
252 public boolean isLocked() {
253 return isLocked;
254 }
255
256 /**
257 * Sets the behavior for comparing unknown objects.
258 *
259 * @param unknownObjectBehavior The flag for unknown behavior -
260 * UNKNOWN_AFTER, UNKNOWN_BEFORE or UNKNOWN_THROW_EXCEPTION
261 * @throws UnsupportedOperationException if a comparison has been performed
262 * @throws NullPointerException if unknownObjectBehavior is null
263 */
264 public void setUnknownObjectBehavior(final UnknownObjectBehavior unknownObjectBehavior) {
265 checkLocked();
266 this.unknownObjectBehavior = Objects.requireNonNull(unknownObjectBehavior, "unknownObjectBehavior");
267 }
268
269 }