InMemoryJpaRepository.java

package com.reallifedeveloper.tools.test.database.inmemory;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.DeleteSpecification;
import org.springframework.data.jpa.domain.PredicateSpecification;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.domain.UpdateSpecification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;

import jakarta.persistence.EmbeddedId;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;

/**
 * An implementation of the Spring Data JPA {@code JpaRepository} interface that holds entities in a map. Useful for testing.
 *
 * @param <T>  the type of the entities handled by this repository
 * @param <ID> the type of the entities' primary keys
 *
 * @author RealLifeDeveloper
 */
@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.TooManyMethods" })
public class InMemoryJpaRepository<T, ID extends Comparable<? super ID>> extends AbstractInMemoryCrudRepository<T, ID>
        implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

    private final PredicateSpecificationEvaluator<T> predicateSpecificationEvaluator = new PredicateSpecificationEvaluator<>();

    /**
     * Creates a new {@code InMemoryJpaRepository} with no primary key generator. If an entity with a {@code null} primary key is saved, an
     * exception is thrown.
     */
    public InMemoryJpaRepository() {
        super();
    }

    /**
     * Creates a new {@code InMemoryJpaRepository} with the provided primary key generator. If an entity with a {@code null} primary key is
     * saved, the generator is used to create a new primary key that is stored in the entity before saving.
     *
     * @param primaryKeyGenerator the primary key generator to use
     */
    public InMemoryJpaRepository(PrimaryKeyGenerator<ID> primaryKeyGenerator) {
        super(primaryKeyGenerator);
    }

    /**
     * Gives the {@link PredicateSpecificationEvaluator} used by this repository. This is useful for registering functions used by
     * {@code PredicateSpecifications}, and could occasionally be useful to directly check if a predicate specification matches an entity.
     *
     * @return the {@code PredicateSpecificationEvaluator} used by this repository.
     */
    public PredicateSpecificationEvaluator<T> predicateSpecificationEvaluator() {
        return predicateSpecificationEvaluator;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteAllInBatch() {
        deleteAll();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteAllInBatch(Iterable<T> entities) {
        deleteAll(entities);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteAllByIdInBatch(Iterable<ID> ids) {
        deleteAllById(ids);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void flush() {
        // Do nothing.
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <S extends T> S saveAndFlush(S entity) {
        S savedEntity = save(entity);
        flush();
        return savedEntity;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {
        List<S> savedEntities = saveAll(entities);
        flush();
        return savedEntities;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("deprecation")
    public T getById(ID id) {
        Optional<T> optionalElement = findById(id);
        return optionalElement.orElseThrow(() -> new EntityNotFoundException("Entity with ID " + id + " not found"));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("deprecation")
    public T getOne(ID id) {
        return getById(id);
    }

    @Override
    protected boolean isIdField(Field field) {
        return field.getAnnotation(Id.class) != null || field.getAnnotation(EmbeddedId.class) != null;
    }

    @Override
    protected boolean isIdMethod(Method method) {
        return method.getAnnotation(Id.class) != null || method.getAnnotation(EmbeddedId.class) != null;
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Optional<Class<ID>> getCompositeIdClass(Object entity) {
        IdClass idClass = entity.getClass().getAnnotation(IdClass.class);
        if (idClass == null) {
            return Optional.empty();
        } else {
            return Optional.ofNullable((Class<ID>) idClass.value());
        }
    }

    @Override
    public <S extends T, R> R findBy(Example<S> example, Function<FetchableFluentQuery<S>, R> queryFunction) {
        throw new UnsupportedOperationException("findBy(Example, Function<FetchableFluentQuery, R>)");
    }

    @Override
    public T getReferenceById(ID id) {
        throw new UnsupportedOperationException("getReferenceById(ID)");
    }

    //
    // JpaSpecificationExecutor methods
    //

    @Override
    public long count(PredicateSpecification<T> spec) {
        return findAll(spec).size();
    }

    @Override
    public long delete(PredicateSpecification<T> spec) {
        List<T> toDelete = findAll(spec);
        deleteAll(toDelete);
        return toDelete.size();
    }

    @Override
    public boolean exists(PredicateSpecification<T> spec) {
        return !findAll(spec).isEmpty();
    }

    @Override
    public List<T> findAll(PredicateSpecification<T> spec) {
        return predicateSpecificationEvaluator.filter(spec, findAll());
    }

    @Override
    public <S extends T, R> R findBy(PredicateSpecification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction) {
        throw new UnsupportedOperationException("findBy(PredicateSpecification, Function<SpecificationFluentQuery, R>)");
    }

    @Override
    public Optional<T> findOne(PredicateSpecification<T> spec) {
        List<T> found = findAll(spec);
        if (found.isEmpty()) {
            return Optional.empty();
        } else if (found.size() == 1) {
            return Optional.of(found.getFirst());
        } else {
            throw new IncorrectResultSizeDataAccessException(1, found.size());
        }
    }

    @Override
    public long delete(DeleteSpecification<T> spec) {
        throw new UnsupportedOperationException("delete(DeleteSpecification)");
    }

    @Override
    public long count(Specification<T> spec) {
        throw new UnsupportedOperationException("count(Specification)");
    }

    @Override
    public boolean exists(Specification<T> spec) {
        throw new UnsupportedOperationException("exists(Specification)");
    }

    @Override
    public List<T> findAll(Specification<T> spec) {
        throw new UnsupportedOperationException("findAll(Specification)");
    }

    @Override
    public Page<T> findAll(Specification<T> spec, Pageable pageable) {
        throw new UnsupportedOperationException("findAll(Specification, Pageable)");
    }

    @Override
    public List<T> findAll(Specification<T> spec, Sort sort) {
        throw new UnsupportedOperationException("findAll(Specification, Sort)");
    }

    @Override
    public Page<T> findAll(Specification<T> spec, Specification<T> countSpec, Pageable pageable) {
        throw new UnsupportedOperationException("findAll(Specification, Specification, Pageable)");
    }

    @Override
    public <S extends T, R> R findBy(Specification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction) {
        throw new UnsupportedOperationException("findBy(Specification, Function<SpecificationFluentQuery, R>)");
    }

    @Override
    public Optional<T> findOne(Specification<T> spec) {
        throw new UnsupportedOperationException("findOne(Specification)");
    }

    @Override
    public long update(UpdateSpecification<T> spec) {
        throw new UnsupportedOperationException("update(UpdateSpecification)");
    }

}