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}