Skip to content

Commit

Permalink
#287 Try approach using CriteriaApi
Browse files Browse the repository at this point in the history
  • Loading branch information
rtufisi committed Jan 5, 2025
1 parent a07d4a3 commit 3d6aca4
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<ExtOrganizationEntity> {
Expand All @@ -41,6 +55,8 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel<ExtOrgan
protected final EntityManager em;
protected final RealmModel realm;

private static final char ESCAPE_BACKSLASH = '\\';

public OrganizationAdapter(
KeycloakSession session, RealmModel realm, EntityManager em, ExtOrganizationEntity org) {
this.session = session;
Expand Down Expand Up @@ -203,21 +219,51 @@ public Stream<OrganizationMemberModel> getOrganizationMembersStream() {

@Override
public Stream<OrganizationMemberModel> searchForOrganizationMembersStream(String search, Integer firstResult, Integer maxResults) {
TypedQuery<OrganizationMemberEntity> 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<OrganizationMemberEntity> criteriaQuery = criteriaBuilder.createQuery(OrganizationMemberEntity.class);

Root<OrganizationMemberEntity> root = criteriaQuery.from(OrganizationMemberEntity.class);

List<Predicate> 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<OrganizationMemberEntity> query = em.createQuery(criteriaQuery);

return closing(paginateQuery(query, firstResult, maxResults).getResultStream())
.filter(Objects::nonNull)
.map(organizationMemberEntity -> new OrganizationMemberAdapter(session, realm, em, organizationMemberEntity));
}

private Subquery<String> userIdsSubquery(CriteriaQuery<?> query, String search) {
CriteriaBuilder cb = em.getCriteriaBuilder();
Subquery<String> subquery = query.subquery(String.class);
Root<UserEntity> subRoot = subquery.from(UserEntity.class);

subquery.select(subRoot.get("id"));
List<Predicate> subqueryPredicates = new ArrayList<>();

subqueryPredicates.add(cb.equal(subRoot.get("realmId"), realm.getId()));

List<Predicate> 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<Long> query = em.createNamedQuery("getOrganizationMembersCount", Long.class);
Expand Down Expand Up @@ -368,4 +414,33 @@ public Stream<IdentityProviderModel> getIdentityProvidersStream() {
return orgs.contains(getId());
});
}


// copied org.keycloak.models.jpa.getSearchOptionPredicateArray; I think is ok. Right @xgp?
private Predicate[] getSearchOptionPredicateArray(String value, CriteriaBuilder builder, From<?, UserEntity> from) {
value = value.toLowerCase();

List<Predicate> orPredicates = new ArrayList<>();

if (value.length() >= 2 && value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') {
// exact search
value = value.substring(1, value.length() - 1);

orPredicates.add(builder.equal(from.get(USERNAME), value));
orPredicates.add(builder.equal(from.get(EMAIL), value));
orPredicates.add(builder.equal(builder.lower(from.get(FIRST_NAME)), value));
orPredicates.add(builder.equal(builder.lower(from.get(LAST_NAME)), value));
} else {
value = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
value = value.replace("*", "%");
if (value.isEmpty() || value.charAt(value.length() - 1) != '%') value += "%";

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit 3d6aca4

Please sign in to comment.