View Javadoc
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 }