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    *      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 }