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 * https://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; 024import java.util.Objects; 025 026/** 027 * A Comparator which imposes a specific order on a specific set of Objects. 028 * Objects are presented to the FixedOrderComparator in a specified order and 029 * subsequent calls to {@link #compare(Object, Object) compare} yield that order. 030 * For example: 031 * <pre> 032 * String[] planets = {"Mercury", "Venus", "Earth", "Mars"}; 033 * FixedOrderComparator distanceFromSun = new FixedOrderComparator(planets); 034 * Arrays.sort(planets); // Sort to alphabetical order 035 * Arrays.sort(planets, distanceFromSun); // Back to original order 036 * </pre> 037 * <p> 038 * Once {@code compare} has been called, the FixedOrderComparator is locked 039 * and attempts to modify it yield an UnsupportedOperationException. 040 * </p> 041 * <p> 042 * Instances of FixedOrderComparator are not synchronized. The class is not 043 * thread-safe at construction time, but it is thread-safe to perform 044 * multiple comparisons after all the setup operations are complete. 045 * </p> 046 * <p> 047 * This class is Serializable from Commons Collections 4.0. 048 * </p> 049 * 050 * @param <T> The type of objects compared by this comparator 051 * @since 3.0 052 */ 053public class FixedOrderComparator<T> implements Comparator<T>, Serializable { 054 055 /** 056 * Enumerates the unknown object behaviors. 057 * 058 * @since 4.0 059 */ 060 public enum UnknownObjectBehavior { 061 062 /** 063 * Before unknown object behaviors. 064 */ 065 BEFORE, 066 067 /** 068 * After unknown object behaviors. 069 */ 070 AFTER, 071 072 /** 073 * Exception unknown object behaviors. 074 */ 075 EXCEPTION 076 } 077 078 /** Serialization version from Collections 4.0. */ 079 private static final long serialVersionUID = 82794675842863201L; 080 081 /** Internal map of object to position */ 082 private final Map<T, Integer> map = new HashMap<>(); 083 084 /** Counter used in determining the position in the map */ 085 private int counter; 086 087 /** Is the comparator locked against further change */ 088 private boolean isLocked; 089 090 /** The behavior in the case of an unknown object */ 091 private UnknownObjectBehavior unknownObjectBehavior = UnknownObjectBehavior.EXCEPTION; 092 093 /** 094 * Constructs an empty FixedOrderComparator. 095 */ 096 public FixedOrderComparator() { 097 } 098 099 /** 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}