From 44a8ded3c27865f47de60af76e8b10a72a773a85 Mon Sep 17 00:00:00 2001 From: razvantufisi Date: Sat, 4 Jan 2025 15:33:51 +0200 Subject: [PATCH] #287 Try approach using CriteriaApi --- .../model/jpa/OrganizationAdapter.java | 75 +++++++++++++++++-- .../jpa/entity/OrganizationMemberEntity.java | 8 -- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java b/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java index 7bc1c9a..b23095c 100644 --- a/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java +++ b/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java @@ -1,6 +1,10 @@ package io.phasetwo.service.model.jpa; import static io.phasetwo.service.Orgs.*; +import static org.keycloak.models.UserModel.EMAIL; +import static org.keycloak.models.UserModel.FIRST_NAME; +import static org.keycloak.models.UserModel.LAST_NAME; +import static org.keycloak.models.UserModel.USERNAME; import static org.keycloak.models.jpa.PaginationUtils.paginateQuery; import static org.keycloak.utils.StreamsUtil.closing; @@ -20,18 +24,28 @@ import io.phasetwo.service.util.IdentityProviders; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; + +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.jpa.JpaModel; +import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.utils.KeycloakModelUtils; public class OrganizationAdapter implements OrganizationModel, JpaModel { @@ -41,6 +55,8 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel getOrganizationMembersStream() { @Override public Stream searchForOrganizationMembersStream(String search, Integer firstResult, Integer maxResults) { - TypedQuery query; - if(search != null && !search.isEmpty()) { - query = em.createNamedQuery("searchOrganizationMembers", OrganizationMemberEntity.class); - query.setParameter("organization", org); - query.setParameter("search", search); - } else { - query = em.createNamedQuery("getOrganizationMembers", OrganizationMemberEntity.class); - query.setParameter("organization", org); + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(OrganizationMemberEntity.class); + + Root root = criteriaQuery.from(OrganizationMemberEntity.class); + + List predicates = new ArrayList<>(); + // defining the organization search clause + predicates.add(criteriaBuilder.equal(root.get("organization"), org)); + if (search != null && !search.isEmpty()) { + var userIds = userIdsSubquery(criteriaQuery, search); + predicates.add(root.get("userId").in(userIds)); } + criteriaQuery.where(predicates.toArray(Predicate[]::new)).orderBy(criteriaBuilder.asc(root.get("createdAt"))); + + TypedQuery query = em.createQuery(criteriaQuery); + return closing(paginateQuery(query, firstResult, maxResults).getResultStream()) .filter(Objects::nonNull) .map(organizationMemberEntity -> new OrganizationMemberAdapter(session, realm, em, organizationMemberEntity)); } + private Subquery userIdsSubquery(CriteriaQuery query, String search) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + Subquery subquery = query.subquery(String.class); + Root subRoot = subquery.from(UserEntity.class); + + subquery.select(subRoot.get("id")); + List subqueryPredicates = new ArrayList<>(); + + subqueryPredicates.add(cb.equal(subRoot.get("realmId"), realm.getId())); + + List searchTermsPredicates = new ArrayList<>(); + //define search terms + for (String stringToSearch : search.trim().split(",")) { + searchTermsPredicates.add(cb.or(getSearchOptionPredicateArray(stringToSearch, cb, subRoot))); + } + Predicate searchPredicate = cb.or(searchTermsPredicates.toArray(Predicate[]::new)); + subqueryPredicates.add(searchPredicate); + + subquery.where(subqueryPredicates.toArray(Predicate[]::new)); + + return subquery; + } + @Override public Long getMembersCount() { TypedQuery query = em.createNamedQuery("getOrganizationMembersCount", Long.class); @@ -368,4 +414,17 @@ public Stream getIdentityProvidersStream() { return orgs.contains(getId()); }); } + + private Predicate[] getSearchOptionPredicateArray(String value, CriteriaBuilder builder, From from) { + value = value.trim().toLowerCase(); + List orPredicates = new ArrayList<>(); + if (!value.isEmpty()) { + value = "%" + value + "%"; //contains in SQL query manner + orPredicates.add(builder.like(from.get(USERNAME), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(from.get(EMAIL), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(builder.lower(from.get(FIRST_NAME)), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value, ESCAPE_BACKSLASH)); + } + return orPredicates.toArray(Predicate[]::new); + } } diff --git a/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java index e054218..827cdfc 100644 --- a/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java +++ b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java @@ -33,14 +33,6 @@ name = "getOrganizationMembers", query = "SELECT m FROM OrganizationMemberEntity m WHERE m.organization = :organization ORDER BY m.createdAt"), - - @NamedQuery( - name = "searchOrganizationMembers", - query = "SELECT m FROM OrganizationMemberEntity m inner join UserEntity ue ON ue.id = m.userId" + - " WHERE m.organization = :organization" + - " AND (lower(ue.username) like lower(concat('%',:search,'%')) OR lower(ue.firstName) like lower(concat('%',:search,'%'))" + - " OR lower(ue.lastName) like lower(concat('%',:search,'%')) OR lower(ue.email) like lower(concat('%',:search,'%')))" + - " ORDER BY m.createdAt"), @NamedQuery( name = "getOrganizationMemberByUserId", query =