JpaSpecificationFinder.java

/*
 * *********************************************************************************************************************
 *
 * SolidBlue 3: Data safety
 * http://tidalwave.it/projects/solidblue3
 *
 * Copyright (C) 2023 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
 *
 * *********************************************************************************************************************
 *
 * 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.
 *
 * *********************************************************************************************************************
 *
 * git clone https://bitbucket.org/tidalwave/solidblue3j-src
 * git clone https://github.com/tidalwave-it/solidblue3j-src
 *
 * *********************************************************************************************************************
 */
package it.tidalwave.util.spring.jpa;

import jakarta.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.io.Serial;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import it.tidalwave.util.Finder;
import it.tidalwave.util.spi.HierarchicFinderSupport;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import static it.tidalwave.util.CollectionUtils.concat;
import static lombok.AccessLevel.PRIVATE;

/***********************************************************************************************************************
 *
 * A {@link Finder} that works with a repository extending {@link JpaSpecificationExecutor}.
 *
 * @param   <M> the static type of the model object
 * @param   <E> the static type of the JPA entity
 * @param   <F> the static type of the {@code Finder}
 * @param   <R> the static type of the repository
 * @stereotype  Finder
 * @author      Fabrizio Giudici
 *
 **********************************************************************************************************************/
@AllArgsConstructor(access = PRIVATE) @Slf4j
public class JpaSpecificationFinder<M, E, F extends Finder<M>, R extends JpaSpecificationExecutor<E>>
        extends HierarchicFinderSupport<M, F>
  {
    @Serial private static final long serialVersionUID = 0L;

    @Nonnull
    protected final R repository;

    @Nonnull
    protected final Function<E, M> entityToModel;

    @Nonnull
    protected final List<JpaSorter> sorters;

    /*******************************************************************************************************************
     *
     ******************************************************************************************************************/
    private record JpaSortCriterion (@Nonnull Enum<?> sortingKey) implements SortCriterion
      {
      }

    /*******************************************************************************************************************
     *
     ******************************************************************************************************************/
    public record JpaSorter (@Nonnull JpaSortCriterion criterion, @Nonnull SortDirection direction)
      {
        @Nonnull
        public Sort.Order toOrder()
          {
            return new Sort.Order(direction == SortDirection.ASCENDING ? Sort.Direction.ASC : Sort.Direction.DESC,
                                  getName(criterion.sortingKey));
          }

        @Override @Nonnull
        public String toString()
          {
            return "JpaSorter(%s, %s)".formatted(getName(criterion.sortingKey), direction.name());
          }

        @Nonnull @SneakyThrows
        private static String getName (@Nonnull final Enum<?> sortingKey)
          {
            return (String)sortingKey.getClass().getMethod("getName").invoke(sortingKey);
          }
      }

    /*******************************************************************************************************************
     *
     * Creates a new instance given a repository and a model-to-entity transformer.
     *
     * @param     repository    the repository
     * @param     entityToModel the transformer
     *
     ******************************************************************************************************************/
    public JpaSpecificationFinder (@Nonnull final R repository, @Nonnull final Function<E, M> entityToModel)
      {
        this(repository, entityToModel, List.of());
      }

    /*******************************************************************************************************************
     *
     * The required constructor for subclasses of {@link HierarchicFinderSupport}.
     *
     ******************************************************************************************************************/
    @SuppressWarnings("unchecked")
    public JpaSpecificationFinder (@Nonnull final JpaSpecificationFinder<M, E, F, R> other, @Nonnull final Object override)
      {
        super(other, override);
        final var source = getSource(JpaSpecificationFinder.class, other, override);
        this.repository = (R)source.repository; // See https://stackoverflow.com/questions/76129388
        this.entityToModel = source.entityToModel;
        this.sorters = source.sorters;
      }

    /*******************************************************************************************************************
     * {@inheritDoc}
     ******************************************************************************************************************/
    @Override @Nonnull
    public F sort (@Nonnull final SortCriterion criterion, @Nonnull final SortDirection direction)
      {
        if (criterion instanceof final JpaSortCriterion jpaSortCriterion)
          {
            final var sorters = concat(this.sorters, new JpaSorter(jpaSortCriterion, direction));
            return clonedWith(new JpaSpecificationFinder<>(repository, entityToModel, sorters));
          }

        return super.sort(criterion, direction);
      }

    /*******************************************************************************************************************
     *
     * Creates a {@link SortCriterion} by key.
     *
     * @param     sortingKey  the key
     * @return                the {@code SortCriterion}
     *
     ******************************************************************************************************************/
    @Nonnull
    public static SortCriterion by (@Nonnull final Enum<?> sortingKey)
      {
        return new JpaSortCriterion(sortingKey);
      }

    /*******************************************************************************************************************
     * {@inheritDoc}
     ******************************************************************************************************************/
    @Override @Nonnull
    protected final List<M> computeNeededResults()
      {
        final var baseTime = System.currentTimeMillis();
        final var specification = getSpecification();
        final var pageRequest = PageRequest.of(firstResult, maxResults,
                                               Sort.by(sorters.stream().map(JpaSorter::toOrder).toList()));
        log.info("computeNeededResults() - {}", pageRequest);
        final var result = repository.findAll(specification, pageRequest).stream().map(entityToModel).toList();
        log.info(">>>> returning {} items in {} msec", result.size(), System.currentTimeMillis() - baseTime);
        log.trace(">>>> returning {}", result);
        return result;
      }

    /*******************************************************************************************************************
     *
     ******************************************************************************************************************/
    @Nonnull
    protected Specification<E> getSpecification()
      {
        return (root, query, criteriaBuilder) ->
          {
            final var predicates = new ArrayList<Predicate>();
            composeSpecification(root, criteriaBuilder, predicates);
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
          };
      }

    /*******************************************************************************************************************
     *
     ******************************************************************************************************************/
    protected void composeSpecification (@Nonnull final Root<E> root,
                                         @Nonnull final CriteriaBuilder criteriaBuilder,
                                         @Nonnull final List<? super Predicate> predicates)
      {
      }
  }