TraceSpecification.java

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

import fr.avenirsesr.portfolio.additionalskill.infrastructure.adapter.model.AdditionalSkillProgressEntity;
import fr.avenirsesr.portfolio.ams.infrastructure.adapter.model.AMSEntity;
import fr.avenirsesr.portfolio.common.language.domain.model.enums.ELanguage;
import fr.avenirsesr.portfolio.file.infrastructure.adapter.model.TraceAttachmentEntity;
import fr.avenirsesr.portfolio.program.infrastructure.adapter.model.SkillLevelEntity;
import fr.avenirsesr.portfolio.student.progress.infrastructure.adapter.model.SkillLevelProgressEntity;
import fr.avenirsesr.portfolio.trace.infrastructure.adapter.model.TraceEntity;
import fr.avenirsesr.portfolio.user.infrastructure.adapter.model.UserEntity;
import jakarta.persistence.criteria.*;
import org.springframework.data.jpa.domain.Specification;

public class TraceSpecification {
  public static Specification<TraceEntity> ofUser(UserEntity user) {
    return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("user"), 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, criteriaBuilder) -> {
      if (query == null) return null;

      Subquery<SkillLevelEntity> skillLevelSubquery = query.subquery(SkillLevelEntity.class);
      Root<TraceEntity> skillLevelSubRoot = skillLevelSubquery.from(TraceEntity.class);
      Join<TraceEntity, SkillLevelEntity> skillLevelJoin = skillLevelSubRoot.join("skillLevels");
      skillLevelSubquery
          .select(skillLevelJoin)
          .where(criteriaBuilder.equal(skillLevelSubRoot.get("id"), root.get("id")));

      Subquery<AMSEntity> amsSubquery = query.subquery(AMSEntity.class);
      Root<TraceEntity> amsSubRoot = amsSubquery.from(TraceEntity.class);
      Join<TraceEntity, AMSEntity> amsJoin = amsSubRoot.join("amses");
      amsSubquery
          .select(amsJoin)
          .where(criteriaBuilder.equal(amsSubRoot.get("id"), root.get("id")));

      Subquery<AdditionalSkillProgressEntity> addSkillSubquery =
          query.subquery(AdditionalSkillProgressEntity.class);
      Root<TraceEntity> addSkillSubRoot = addSkillSubquery.from(TraceEntity.class);
      Join<TraceEntity, AdditionalSkillProgressEntity> addSkillJoin =
          addSkillSubRoot.join("additionalSkillsProgresses");
      addSkillSubquery
          .select(addSkillJoin)
          .where(criteriaBuilder.equal(addSkillSubRoot.get("id"), root.get("id")));

      Predicate hasSkillLevels = criteriaBuilder.exists(skillLevelSubquery);
      Predicate hasAmses = criteriaBuilder.exists(amsSubquery);
      Predicate noAdditionalSkills = criteriaBuilder.exists(addSkillSubquery);

      query.distinct(true);

      return criteriaBuilder.or(hasSkillLevels, hasAmses, noAdditionalSkills);
    };
  }

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

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

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

  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);

      // Additional skills
      var additionalSkillJoin =
          root.join("additionalSkillsProgresses", JoinType.LEFT)
              .join("additionalSkill", JoinType.LEFT);

      var additionalSkillPredicate =
          criteriaBuilder.like(criteriaBuilder.lower(additionalSkillJoin.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,
          additionalSkillPredicate,
          skillLevelPredicate,
          amsPredicate);
    };
  }
}