TraceSpecification.java

package fr.avenirsesr.portfolio.trace.infrastructure.adapter.specification;

import fr.avenirsesr.portfolio.ams.domain.model.AMS;
import fr.avenirsesr.portfolio.ams.infrastructure.adapter.mapper.AMSMapper;
import fr.avenirsesr.portfolio.association.domain.model.EAssociationType;
import fr.avenirsesr.portfolio.association.infrastructure.adapter.model.AssociationEntity;
import fr.avenirsesr.portfolio.common.data.domain.model.User;
import fr.avenirsesr.portfolio.common.language.domain.model.enums.ELanguage;
import fr.avenirsesr.portfolio.file.infrastructure.adapter.model.TraceAttachmentEntity;
import fr.avenirsesr.portfolio.student.progress.declared.skill.domain.model.DeclaredSkillProgress;
import fr.avenirsesr.portfolio.student.progress.declared.skill.infrastructure.adapter.mapper.DeclaredSkillProgressMapper;
import fr.avenirsesr.portfolio.student.progress.imported.domain.model.SkillLevelProgress;
import fr.avenirsesr.portfolio.student.progress.imported.infrastructure.adapter.mapper.SkillLevelProgressMapper;
import fr.avenirsesr.portfolio.trace.domain.model.Trace;
import fr.avenirsesr.portfolio.trace.infrastructure.adapter.model.TraceEntity;
import fr.avenirsesr.portfolio.user.infrastructure.adapter.mapper.UserMapper;
import jakarta.persistence.criteria.*;
import java.util.UUID;
import org.springframework.data.jpa.domain.Specification;

public class TraceSpecification {
  public static Specification<TraceEntity> ofUser(User user) {
    return (root, query, criteriaBuilder) ->
        criteriaBuilder.equal(root.get("user"), UserMapper.INSTANCE.fromDomain(user));
  }

  public static Specification<TraceEntity> unassociated() {
    return (root, query, criteriaBuilder) ->
        criteriaBuilder.not(associated().toPredicate(root, query, criteriaBuilder));
  }

  public static Specification<TraceEntity> associated() {
    return (root, query, cb) -> {
      if (query == null) return null;

      Subquery<UUID> subquery = query.subquery(UUID.class);
      Root<AssociationEntity> associationRoot = subquery.from(AssociationEntity.class);

      var traceId = root.get("id");

      var id1Match = cb.equal(associationRoot.get("id1"), traceId);
      var id2Match = cb.equal(associationRoot.get("id2"), traceId);

      var typeIn =
          associationRoot.get("associationType").in(EAssociationType.getAllBy(Trace.class));

      subquery.select(associationRoot.get("id")).where(cb.and(cb.or(id1Match, id2Match), typeIn));

      query.distinct(true);

      return cb.exists(subquery);
    };
  }

  public static Specification<TraceEntity> notDeleted() {
    return (root, query, cb) -> cb.isNull(root.get("deletedAt"));
  }

  public static Specification<TraceEntity> ofAms(AMS ams) {
    return (root, query, criteriaBuilder) ->
        criteriaBuilder.isMember(AMSMapper.INSTANCE.fromDomain(ams), root.get("amses"));
  }

  public static Specification<TraceEntity> ofSkillLevelProgress(
      SkillLevelProgress skillLevelProgress) {
    return (root, query, criteriaBuilder) ->
        criteriaBuilder.isMember(
            SkillLevelProgressMapper.INSTANCE.fromDomain(skillLevelProgress),
            root.get("skillLevels"));
  }

  public static Specification<TraceEntity> ofDeclaredSkillProgress(
      DeclaredSkillProgress declaredSkillProgress) {
    return (root, query, criteriaBuilder) ->
        criteriaBuilder.isMember(
            DeclaredSkillProgressMapper.INSTANCE.fromDomain(declaredSkillProgress),
            root.get("declaredSkillsProgresses"));
  }

  public static Specification<TraceEntity> search(String keyword, ELanguage language) {
    return (root, query, criteriaBuilder) -> {
      if (keyword == null || keyword.trim().isEmpty() || query == null) {
        return criteriaBuilder.conjunction();
      }

      query.distinct(true);

      String pattern = "%" + keyword.toLowerCase() + "%";

      // Trace
      var titlePredicate = criteriaBuilder.like(criteriaBuilder.lower(root.get("title")), pattern);
      var aiUsePredicate =
          criteriaBuilder.like(criteriaBuilder.lower(root.get("aiUseJustification")), pattern);
      var personalNotePredicate =
          criteriaBuilder.like(criteriaBuilder.lower(root.get("personalNote")), pattern);

      // Attachment
      Subquery<TraceAttachmentEntity> attachSub = query.subquery(TraceAttachmentEntity.class);
      Root<TraceAttachmentEntity> attachRoot = attachSub.from(TraceAttachmentEntity.class);
      attachSub
          .select(attachRoot)
          .where(
              criteriaBuilder.equal(attachRoot.get("trace").get("id"), root.get("id")),
              criteriaBuilder.isTrue(attachRoot.get("isActiveVersion")),
              criteriaBuilder.like(criteriaBuilder.lower(attachRoot.get("name")), pattern));
      Predicate attachmentPredicate = criteriaBuilder.exists(attachSub);

      // Declared skills
      var declaredSkillJoin =
          root.join("declaredSkillsProgresses", JoinType.LEFT).join("declaredSkill", JoinType.LEFT);

      var declaredSkillPredicate =
          criteriaBuilder.like(criteriaBuilder.lower(declaredSkillJoin.get("libelle")), pattern);

      // Skill level
      var slpJoin =
          root.join("skillLevels", JoinType.LEFT)
              .join("skillLevel", JoinType.LEFT)
              .join("translations", JoinType.LEFT);
      var slpLangPredicate = criteriaBuilder.equal(slpJoin.get("language"), language);
      var slpNamePredicate =
          criteriaBuilder.like(criteriaBuilder.lower(slpJoin.get("name")), pattern);
      var slpDescPredicate =
          criteriaBuilder.like(criteriaBuilder.lower(slpJoin.get("description")), pattern);
      var skillLevelPredicate =
          criteriaBuilder.and(
              slpLangPredicate, criteriaBuilder.or(slpNamePredicate, slpDescPredicate));

      // AMS
      var amsJoin = root.join("amses", JoinType.LEFT).join("translations", JoinType.LEFT);
      var amsLangPredicate = criteriaBuilder.equal(amsJoin.get("language"), language);
      var amsTitlePredicate =
          criteriaBuilder.like(criteriaBuilder.lower(amsJoin.get("title")), pattern);
      var amsPredicate = criteriaBuilder.and(amsLangPredicate, amsTitlePredicate);

      return criteriaBuilder.or(
          titlePredicate,
          aiUsePredicate,
          personalNotePredicate,
          attachmentPredicate,
          declaredSkillPredicate,
          skillLevelPredicate,
          amsPredicate);
    };
  }
}