SortMapper.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.spring.mapper;

import static java.util.Objects.isNull;
import static org.springframework.util.ObjectUtils.isEmpty;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bremersee.comparator.model.SortOrder;
import org.bremersee.comparator.model.SortOrderItem;
import org.bremersee.comparator.model.SortOrderItem.CaseHandling;
import org.bremersee.comparator.model.SortOrderTextSeparators;
import org.bremersee.comparator.spring.converter.SortOrderConverter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.NullHandling;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

/**
 * This mapper provides methods to transform a {@link SortOrderItem} into a {@code Sort} object from
 * the Spring framework (spring-data-common) and vice versa.
 *
 * @author Christian Bremer
 */
public interface SortMapper {

  /**
   * Returns default sort mapper.
   *
   * @return the sort mapper
   */
  static SortMapper defaultSortMapper() {
    return defaultSortMapper(SortOrderTextSeparators.defaults());
  }

  /**
   * Returns default sort mapper.
   *
   * @param sortOrderTextSeparators the sort order text separators
   * @return the sort mapper
   */
  static SortMapper defaultSortMapper(SortOrderTextSeparators sortOrderTextSeparators) {
    return defaultSortMapper(new SortOrderConverter(sortOrderTextSeparators));
  }

  /**
   * Returns default sort mapper.
   *
   * @param sortOrderConverter the sort order converter
   * @return the sort mapper
   */
  static SortMapper defaultSortMapper(SortOrderConverter sortOrderConverter) {
    return new DefaultSortMapper(sortOrderConverter);
  }

  /**
   * Gets sort order from text.
   *
   * @param sortOrderText the sort order text
   * @return the sort order
   */
  SortOrder getSortOrder(@Nullable String sortOrderText);

  /**
   * Gets sort order text.
   *
   * @param sortOrder the sort order
   * @return the sort order text
   */
  String getSortOrderText(@Nullable SortOrder sortOrder);

  /**
   * Gets sort order text of items.
   *
   * @param sortOrder the sort order
   * @return the sort order item text
   */
  List<String> getSortOrderItemText(@Nullable SortOrder sortOrder);

  /**
   * Transforms sort order into a {@code Sort} object.
   *
   * @param sortOrder the sort order
   * @return the sort
   */
  @NonNull
  default Sort toSort(@Nullable SortOrder sortOrder) {
    List<Sort.Order> orderList = Stream.ofNullable(sortOrder)
        .map(SortOrder::getItems)
        .flatMap(Collection::stream)
        .filter(Objects::nonNull)
        .map(this::toSortOrder)
        .toList();
    return orderList.isEmpty() ? Sort.unsorted() : Sort.by(orderList);
  }

  /**
   * Transforms the sort order into a {@code Sort.Order} object.
   *
   * @param sortOrderItem the sort order
   * @return the sort object
   */
  @Nullable
  default Sort.Order toSortOrder(@Nullable SortOrderItem sortOrderItem) {
    if (sortOrderItem == null || sortOrderItem.getField() == null) {
      return null;
    }
    Direction direction = sortOrderItem.getDirection().isAscending()
        ? Direction.ASC
        : Direction.DESC;
    NullHandling nullHandling = switch (sortOrderItem.getNullHandling()) {
      case NULLS_FIRST -> NullHandling.NULLS_FIRST;
      case NULLS_LAST -> NullHandling.NULLS_LAST;
      case NATIVE -> NullHandling.NATIVE;
    };
    Sort.Order order = new Sort.Order(direction, sortOrderItem.getField(), nullHandling);
    return sortOrderItem.getCaseHandling().isInsensitive()
        ? order.ignoreCase()
        : order;
  }


  /**
   * Transforms a {@code Sort} object into a sort order.
   *
   * @param sort the {@code Sort} object
   * @return the sort order
   */
  @NonNull
  default SortOrder fromSort(@Nullable Sort sort) {
    List<SortOrderItem> items = Stream.ofNullable(sort)
        .flatMap(Sort::stream)
        .map(this::fromSortOrder)
        .filter(Objects::nonNull)
        .toList();
    return new SortOrder(items);
  }

  /**
   * Transforms a {@code Sort.Order} object into a sort order.
   *
   * @param sortOrder the {@code Sort.Order} object
   * @return the sort order
   */
  @Nullable
  default SortOrderItem fromSortOrder(@Nullable Sort.Order sortOrder) {
    if (isNull(sortOrder)) {
      return null;
    }
    SortOrderItem.Direction direction = sortOrder.getDirection().isAscending()
        ? SortOrderItem.Direction.ASC
        : SortOrderItem.Direction.DESC;
    CaseHandling caseHandling = sortOrder.isIgnoreCase()
        ? CaseHandling.INSENSITIVE
        : CaseHandling.SENSITIVE;
    SortOrderItem.NullHandling nullHandling = switch (sortOrder.getNullHandling()) {
      case NULLS_FIRST -> SortOrderItem.NullHandling.NULLS_FIRST;
      case NULLS_LAST -> SortOrderItem.NullHandling.NULLS_LAST;
      case NATIVE -> SortOrderItem.NullHandling.NATIVE;
    };
    return new SortOrderItem(
        sortOrder.getProperty(),
        direction,
        caseHandling,
        nullHandling);
  }

  /**
   * Apply defaults to page request.
   *
   * @param source the source
   * @param direction the direction
   * @param ignoreCase the ignore case
   * @param nullHandling the null handling
   * @param properties the properties
   * @return the pageable
   */
  @Nullable
  default Pageable applyDefaults(
      @Nullable Pageable source,
      @Nullable Direction direction,
      @Nullable Boolean ignoreCase,
      @Nullable NullHandling nullHandling,
      @Nullable String... properties) {

    return isNull(source) ? null : PageRequest.of(
        source.getPageNumber(),
        source.getPageSize(),
        applyDefaults(source.getSort(), direction, ignoreCase, nullHandling, properties));
  }

  /**
   * Apply defaults to sort.
   *
   * @param source the source
   * @param direction the direction
   * @param ignoreCase the ignore case
   * @param nullHandling the null handling
   * @param properties the properties
   * @return the sort
   */
  @NonNull
  default Sort applyDefaults(
      @Nullable Sort source,
      @Nullable Direction direction,
      @Nullable Boolean ignoreCase,
      @Nullable NullHandling nullHandling,
      @Nullable String... properties) {

    if (isNull(source)) {
      return Sort.unsorted();
    }
    if (isNull(direction) && isNull(ignoreCase) && isNull(nullHandling)) {
      return source;
    }
    Set<String> names;
    if (isEmpty(properties)) {
      names = source.stream().map(Sort.Order::getProperty).collect(Collectors.toSet());
    } else {
      names = Arrays.stream(properties).collect(Collectors.toSet());
    }
    return Sort.by(source.stream()
        .map(sortOrder -> {
          if (names.contains(sortOrder.getProperty())) {
            Sort.Order order = Sort.Order.by(sortOrder.getProperty())
                .with(newDirection(sortOrder.getDirection(), direction))
                .with(newNullHandling(sortOrder.getNullHandling(), nullHandling));
            return withNewCaseHandling(order, sortOrder.isIgnoreCase(), ignoreCase);
          }
          return sortOrder;
        })
        .toList());
  }

  private Direction newDirection(Direction oldDirection, Direction newDirection) {
    return Optional.ofNullable(newDirection)
        .orElse(oldDirection);
  }

  private NullHandling newNullHandling(NullHandling oldNullHandling, NullHandling newNullHandling) {
    return Optional.ofNullable(newNullHandling)
        .orElse(oldNullHandling);
  }

  private Sort.Order withNewCaseHandling(
      Sort.Order order,
      boolean oldIgnoresCase,
      Boolean newIgnoresCase) {
    //noinspection ConstantConditions
    return Optional.ofNullable(newIgnoresCase)
        .map(ignoreCase -> ignoreCase ? order.ignoreCase() : order)
        .orElseGet(() -> oldIgnoresCase ? order.ignoreCase() : order);
  }

  /**
   * The default sort mapper.
   */
  @SuppressWarnings("ClassCanBeRecord")
  class DefaultSortMapper implements SortMapper {

    private final SortOrderConverter converter;

    /**
     * Instantiates a new default sort mapper.
     *
     * @param converter the converter
     */
    DefaultSortMapper(SortOrderConverter converter) {
      this.converter = Objects.requireNonNullElseGet(converter, SortOrderConverter::new);
    }

    @Override
    public SortOrder getSortOrder(@Nullable String sortOrderText) {
      if (isNull(sortOrderText)) {
        return SortOrder.unsorted();
      }
      return converter.convert(sortOrderText);
    }

    @Override
    public String getSortOrderText(@Nullable SortOrder sortOrder) {
      if (isNull(sortOrder)) {
        return null;
      }
      if (sortOrder.isEmpty()) {
        return "";
      }
      return sortOrder.getSortOrderText(converter.getSeparators());
    }

    @Override
    public List<String> getSortOrderItemText(SortOrder sortOrder) {
      return Stream.ofNullable(sortOrder)
          .map(SortOrder::getItems)
          .filter(Objects::nonNull)
          .flatMap(Collection::stream)
          .map(item -> item.getSortOrderText(converter.getSeparators()))
          .filter(Objects::nonNull)
          .toList();
    }

  }

}