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