001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017–2019 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.configuration.spi; 018 019import java.io.Serializable; 020 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.List; 025import java.util.Objects; 026 027/** 028 * A {@link Comparator} that ranks {@linkplain 029 * #getComparisonObject(Object) other objects} for comparison 030 * purposes. 031 * 032 * <p>A <em>rank</em> for the purposes of this class is a positive 033 * integer. One integer representing a rank <em>outranks</em> another 034 * integer representing a rank if the first integer is greater than 035 * the second. Hence, <em>e.g.</em>, {@code 1} outranks {@code 0}. A 036 * negative integer cannot represent a rank and in the context of 037 * ranking means, effectively, that any rank it might otherwise 038 * represent is unknown.</p> 039 * 040 * <p>A {@link RankedComparator} can accept an optional {@link List} 041 * at {@linkplain #RankedComparator(List) construction time} that can 042 * be used for ranking purposes in case the type of objects the {@link 043 * RankedComparator} compares are not instances of {@link Ranked}. In 044 * such a case, the {@linkplain List#indexOf(Object) index of} an 045 * object in a {@linkplain Collections#reverse(List) reversed} copy of 046 * the supplied {@link List} is used as its rank.</p> 047 * 048 * @param <T> the type of object to {@linkplain #compare(Object, 049 * Object) compare} 050 * 051 * @author <a href="https://about.me/lairdnelson" 052 * target="_parent">Laird Nelson</a> 053 * 054 * @see #RankedComparator(List) 055 * 056 * @see #compare(Object, Object) 057 */ 058public class RankedComparator<T> implements Comparator<T>, Serializable { 059 060 061 /* 062 * Static fields. 063 */ 064 065 066 /** 067 * The version of this class for {@linkplain Serializable 068 * serialization purposes}. 069 */ 070 private static final long serialVersionUID = 1L; 071 072 073 /* 074 * Instance fields. 075 */ 076 077 078 /** 079 * An {@linkplain Collections#unmodifiableList(List) unmodifiable} 080 * {@link List} of {@link Object}s to be used for ranking purposes; 081 * the {@link Object} at position {@code 0} is deemed to have the 082 * highest rank. 083 * 084 * <p>This field is never {@code null}.</p> 085 * 086 * @see #RankedComparator(List) 087 */ 088 private final List<?> items; 089 090 091 /* 092 * Constructors. 093 */ 094 095 096 /** 097 * Creates a new {@link RankedComparator}. 098 * 099 * @see #RankedComparator(List) 100 */ 101 public RankedComparator() { 102 this(null); 103 } 104 105 /** 106 * Creates a new {@link RankedComparator}. 107 * 108 * @param items a {@link List} of {@link Object}s that will be used 109 * for ranking purposes; may be {@code null}; the item at position 110 * {@code 0} is deemed to outrank all others; copied by value 111 * 112 * @see #getComparisonObject(Object) 113 */ 114 public RankedComparator(final List<?> items) { 115 super(); 116 if (items == null || items.isEmpty()) { 117 this.items = Collections.emptyList(); 118 } else { 119 final List<?> copy = new ArrayList<>(items); 120 Collections.reverse(copy); 121 this.items = Collections.unmodifiableList(copy); 122 } 123 } 124 125 126 /* 127 * Instance methods. 128 */ 129 130 131 /** 132 * Returns {@code true} if this {@link RankedComparator} explicitly 133 * ranks the supplied object. 134 * 135 * @param object the object to test; may be {@code null} 136 * 137 * @return {@code true} if this {@link RankedComparator} explicitly 138 * ranks the supplied object; {@code false} otherwise 139 * 140 * @see #getComparisonObject(Object) 141 */ 142 public final boolean ranks(final T object) { 143 final Object comparisonObject = this.getComparisonObject(object); 144 return comparisonObject != null && (comparisonObject instanceof Ranked || this.items.contains(comparisonObject)); 145 } 146 147 private final int getRank(final Object object) { 148 final int rank; 149 if (object == null) { 150 rank = -1; 151 } else if (object instanceof Ranked) { 152 rank = ((Ranked)object).getRank(); 153 } else { 154 rank = this.items.indexOf(object); 155 } 156 return rank; 157 } 158 159 /** 160 * Compares two objects, returning {@code -1} if {@code one} 161 * outranks {@code two}, {@code 1} if {@code two} outranks {@code 162 * one}, and {@code 0} if the two objects are either ranked equally 163 * or their ranks could not be determined. 164 * 165 * <p>This method makes use of the {@link List} {@linkplain 166 * #RankedComparator(List) supplied at construction time} for 167 * ranking information.</p> 168 * 169 * <p>Non-{@code null} objects outrank {@code null} objects.</p> 170 * 171 * @param one the first object; may be {@code null} 172 * 173 * @param two the second object; may be {@code null} 174 * 175 * @return {@code -1} if {@code one} outranks {@code two}, {@code 1} 176 * if {@code two} outranks {@code one}, and {@code 0} if the two 177 * objects are either ranked equally or their ranks could not be 178 * determined 179 * 180 * @see #getComparisonObject(Object) 181 * 182 * @see #RankedComparator(List) 183 */ 184 @Override 185 public final int compare(final T one, final T two) { 186 final int returnValue; 187 if (one == two) { 188 returnValue = 0; 189 } else if (one == null) { 190 assert two != null; 191 returnValue = 1; // nulls sort "right"; non-nulls win 192 } else if (two == null) { 193 assert one != null; 194 returnValue = -1; // nulls sort "right"; non-nulls win 195 } else { 196 final int oneRank = this.getRank(this.getComparisonObject(one)); 197 final int twoRank = this.getRank(this.getComparisonObject(two)); 198 if (oneRank < 0) { 199 if (twoRank < 0) { 200 returnValue = 0; // a negative rank means "no idea" 201 } else { 202 returnValue = 1; // two "won"; it's ranked; one isn't 203 } 204 } else if (oneRank > twoRank) { 205 returnValue = -1; // one "won"; its rank is higher than two's 206 } else if (oneRank == twoRank) { 207 returnValue = 0; 208 } else { 209 returnValue = 1; // two "won"; its rank is higher than one's 210 } 211 } 212 return returnValue; 213 } 214 215 /** 216 * Given an object to be {@linkplain #compare(Object, Object) 217 * compared}, returns the {@link Object} that should be used to 218 * determine ranking (as determined by the {@link List} {@linkplain 219 * #RankedComparator(List) supplied at construction time}). 220 * 221 * <p>This method may return {@code null}.</p> 222 * 223 * <p>Overrides of this method may return {@code null}.</p> 224 * 225 * <p>The default implementation of this method simply returns the 226 * supplied {@code realObject} parameter value.</p> 227 * 228 * @param realObject the object to be {@linkplain #compare(Object, 229 * Object) compared}; may be {@code null} 230 * 231 * @return the {@link Object} that should be used to determine 232 * ranking (as determined by the {@link List} {@linkplain 233 * #RankedComparator(List) supplied at construction time}), possibly 234 * {@code null} 235 * 236 * @see #RankedComparator(List) 237 * 238 * @see #compare(Object, Object) 239 */ 240 protected Object getComparisonObject(final T realObject) { 241 return realObject; 242 } 243 244}