Content of file 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()
no comment
{ 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) { } }