ValueComparator.java

/*
 * Copyright 2019-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bremersee.comparator;

import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import lombok.ToString;
import org.bremersee.comparator.model.SortOrderItem;

/**
 * The value comparator extracts field value of the specified field name or path and uses the
 * specified description (ascending or descending, case-sensitive or insensitive and
 * 'null-handling') for sorting.
 *
 * @author Christian Bremer
 */
@ToString
public class ValueComparator implements Comparator<Object> {

  private final ValueExtractor valueExtractor;

  private final SortOrderItem sortOrder;

  /**
   * Instantiates a new value comparator.
   *
   * @param sortOrder the sort order
   */
  public ValueComparator(SortOrderItem sortOrder) {
    this(sortOrder, null);
  }

  /**
   * Instantiates a new value comparator.
   *
   * @param sortOrder the sort order
   * @param valueExtractor a custom value extractor (if it is {@code null}, a default will be
   *     used)
   */
  public ValueComparator(
      SortOrderItem sortOrder,
      ValueExtractor valueExtractor) {
    this.sortOrder = Objects.requireNonNullElseGet(sortOrder, () -> SortOrderItem.by(null));
    this.valueExtractor = valueExtractor != null ? valueExtractor : new DefaultValueExtractor();
  }

  @Override
  public int compare(Object o1, Object o2) {
    final Object v1 = valueExtractor.findValue(o1, sortOrder.getField());
    final Object v2 = valueExtractor.findValue(o2, sortOrder.getField());

    return compareNullSafe(v1, v2)
        .or(() -> compareNonNull(v1, v2))
        .orElseThrow(() -> new ComparatorException(
            "Comparison of field '" + sortOrder.getField() + "' is not possible."));
  }

  private Optional<Integer> compareNullSafe(Object v1, Object v2) {
    if (v1 == null && v2 == null) {
      return Optional.of(0);
    }
    if (v1 == null) {
      return Optional.of(firstIsNull());
    }
    if (v2 == null) {
      return Optional.of(-1 * firstIsNull());
    }
    return Optional.empty();
  }

  private int firstIsNull() {
    int r;
    if (sortOrder.getDirection().isAscending()) {
      r = sortOrder.getNullHandling().isNullFirst() ? -1 : 1;
    } else {
      r = sortOrder.getNullHandling().isNullFirst() ? 1 : -1;
    }
    return r;
  }

  @SuppressWarnings("rawtypes")
  private Optional<Integer> compareNonNull(Object v1, Object v2) {
    if (sortOrder.getDirection().isAscending() && v1 instanceof Comparable c1) {
      if (sortOrder.getCaseHandling().isInsensitive()
          && v1 instanceof String s1 && v2 instanceof String s2) {
        return Optional.of(s1.compareToIgnoreCase(s2));
      } else {
        //noinspection unchecked
        return Optional.of(c1.compareTo(v2));
      }

    } else if (!sortOrder.getDirection().isAscending() && v2 instanceof Comparable c2) {

      if (sortOrder.getCaseHandling().isInsensitive()
          && v1 instanceof String s1 && v2 instanceof String s2) {
        return Optional.of(s2.compareToIgnoreCase(s1));
      } else {
        //noinspection unchecked
        return Optional.of(c2.compareTo(v1));
      }
    }
    return Optional.empty();
  }

}