001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.collections4.comparators;
018
019import java.io.Serializable;
020import java.util.Comparator;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025/**
026 * A Comparator which imposes a specific order on a specific set of Objects.
027 * Objects are presented to the FixedOrderComparator in a specified order and
028 * subsequent calls to {@link #compare(Object, Object) compare} yield that order.
029 * For example:
030 * <pre>
031 * String[] planets = {"Mercury", "Venus", "Earth", "Mars"};
032 * FixedOrderComparator distanceFromSun = new FixedOrderComparator(planets);
033 * Arrays.sort(planets);                     // Sort to alphabetical order
034 * Arrays.sort(planets, distanceFromSun);    // Back to original order
035 * </pre>
036 * <p>
037 * Once <code>compare</code> has been called, the FixedOrderComparator is locked
038 * and attempts to modify it yield an UnsupportedOperationException.
039 * <p>
040 * Instances of FixedOrderComparator are not synchronized.  The class is not
041 * thread-safe at construction time, but it is thread-safe to perform
042 * multiple comparisons  after all the setup operations are complete.
043 * <p>
044 * This class is Serializable from Commons Collections 4.0.
045 *
046 * @param <T> the type of objects compared by this comparator
047 * @since 3.0
048 */
049public class FixedOrderComparator<T> implements Comparator<T>, Serializable {
050
051    /** Serialization version from Collections 4.0. */
052    private static final long serialVersionUID = 82794675842863201L;
053
054    /**
055     * Unknown object behavior enum.
056     * @since 4.0
057     */
058    public enum UnknownObjectBehavior {
059        BEFORE, AFTER, EXCEPTION;
060    }
061
062    /** Internal map of object to position */
063    private final Map<T, Integer> map = new HashMap<>();
064
065    /** Counter used in determining the position in the map */
066    private int counter = 0;
067
068    /** Is the comparator locked against further change */
069    private boolean isLocked = false;
070
071    /** The behaviour in the case of an unknown object */
072    private UnknownObjectBehavior unknownObjectBehavior = UnknownObjectBehavior.EXCEPTION;
073
074    // Constructors
075    //-----------------------------------------------------------------------
076    /**
077     * Constructs an empty FixedOrderComparator.
078     */
079    public FixedOrderComparator() {
080        super();
081    }
082
083    /**
084     * Constructs a FixedOrderComparator which uses the order of the given array
085     * to compare the objects.
086     * <p>
087     * The array is copied, so later changes will not affect the comparator.
088     *
089     * @param items  the items that the comparator can compare in order
090     * @throws NullPointerException if the array is null
091     */
092    public FixedOrderComparator(final T... items) {
093        super();
094        if (items == null) {
095            throw new NullPointerException("The list of items must not be null");
096        }
097        for (final T item : items) {
098            add(item);
099        }
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 NullPointerException if the list is null
110     */
111    public FixedOrderComparator(final List<T> items) {
112        super();
113        if (items == null) {
114            throw new NullPointerException("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 NullPointerException if unknownObjectBehavior is null
161     */
162    public void setUnknownObjectBehavior(final UnknownObjectBehavior unknownObjectBehavior) {
163        checkLocked();
164        if (unknownObjectBehavior == null) {
165            throw new NullPointerException("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 unrecognized 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    @Override
228    public int compare(final T obj1, final T obj2) {
229        isLocked = true;
230        final Integer position1 = map.get(obj1);
231        final Integer position2 = map.get(obj2);
232        if (position1 == null || position2 == null) {
233            switch (unknownObjectBehavior) {
234            case BEFORE:
235                return position1 == null ? position2 == null ? 0 : -1 : 1;
236            case AFTER:
237                return position1 == null ? position2 == null ? 0 : 1 : -1;
238            case EXCEPTION:
239                final Object unknownObj = position1 == null ? obj1 : obj2;
240                throw new IllegalArgumentException("Attempting to compare unknown object "
241                        + unknownObj);
242            default: //could be null
243                throw new UnsupportedOperationException("Unknown unknownObjectBehavior: "
244                        + unknownObjectBehavior);
245            }
246        }
247        return position1.compareTo(position2);
248    }
249
250    //-----------------------------------------------------------------------
251    /**
252     * Implement a hash code for this comparator that is consistent with
253     * {@link #equals(Object) equals}.
254     *
255     * @return a hash code for this comparator.
256     */
257    @Override
258    public int hashCode() {
259        int total = 17;
260        total = total*37 + (map == null ? 0 : map.hashCode());
261        total = total*37 + (unknownObjectBehavior == null ? 0 : unknownObjectBehavior.hashCode());
262        total = total*37 + counter;
263        total = total*37 + (isLocked ? 0 : 1);
264        return total;
265    }
266
267    /**
268     * Returns <code>true</code> iff <i>that</i> Object is
269     * is a {@link Comparator} whose ordering is known to be
270     * equivalent to mine.
271     * <p>
272     * This implementation returns <code>true</code>
273     * iff <code><i>that</i></code> is a {@link FixedOrderComparator}
274     * whose attributes are equal to mine.
275     *
276     * @param object  the object to compare to
277     * @return true if equal
278     */
279    @Override
280    public boolean equals(final Object object) {
281        if (this == object) {
282            return true;
283        }
284        if (null == object) {
285            return false;
286        }
287        if (object.getClass().equals(this.getClass())) {
288            final FixedOrderComparator<?> comp = (FixedOrderComparator<?>) object;
289            return (null == map ? null == comp.map : map.equals(comp.map)) &&
290                   (null == unknownObjectBehavior ? null == comp.unknownObjectBehavior :
291                        unknownObjectBehavior == comp.unknownObjectBehavior &&
292                        counter == comp.counter &&
293                        isLocked == comp.isLocked &&
294                        unknownObjectBehavior == comp.unknownObjectBehavior);
295        }
296        return false;
297    }
298
299}