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 2925c0b
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.phasetwo.service.model;

import java.util.List;
import java.util.Map;

public interface OrganizationMemberModel extends WithAttributes {

Expand Down
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,17 @@ public Stream<IdentityProviderModel> getIdentityProvidersStream() {
return orgs.contains(getId());
});
}

private Predicate[] getSearchOptionPredicateArray(String value, CriteriaBuilder builder, From<?, UserEntity> from) {
value = value.trim().toLowerCase();
List<Predicate> 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);
}
}
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.phasetwo.service.representation;

import org.keycloak.representations.idm.UserRepresentation;

import java.util.List;
import java.util.Map;

public class UserOrganizationMember extends UserRepresentation {

Map<String, List<String>> organizationAttributes;
String organizationId;
List<String> organizationRoles;

public Map<String, List<String>> getOrganizationAttributes() {
return organizationAttributes;
}

public void setOrganizationAttributes(Map<String, List<String>> organizationAttributes) {
this.organizationAttributes = organizationAttributes;
}

public String getOrganizationId() {
return organizationId;
}

public void setOrganizationId(String organizationId) {
this.organizationId = organizationId;
}

public List<String> getOrganizationRoles() {
return organizationRoles;
}

public void setOrganizationRoles(List<String> organizationRoles) {
this.organizationRoles = organizationRoles;
}
}
150 changes: 78 additions & 72 deletions src/main/java/io/phasetwo/service/resource/Converters.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,87 +9,93 @@
import io.phasetwo.service.representation.Invitation;
import io.phasetwo.service.representation.Organization;
import io.phasetwo.service.representation.OrganizationRole;

import java.util.List;
import java.util.Map;

import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.representations.account.UserRepresentation;

/** Utilities for converting Entities to/from Representations. */
import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser;

/**
* Utilities for converting Entities to/from Representations.
*/
public class Converters {

public static OrganizationRole convertOrganizationRole(OrganizationRoleModel m) {
OrganizationRole r =
new OrganizationRole().id(m.getId()).name(m.getName()).description(m.getDescription());
return r;
}
public static OrganizationRole convertOrganizationRole(OrganizationRoleModel m) {
OrganizationRole r =
new OrganizationRole().id(m.getId()).name(m.getName()).description(m.getDescription());
return r;
}

public static Organization convertOrganizationModelToOrganization(OrganizationModel e) {
Organization o =
new Organization()
.id(e.getId())
.name(e.getName())
.displayName(e.getDisplayName())
.domains(e.getDomains())
.url(e.getUrl())
.realm(e.getRealm().getName());
o.setAttributes(e.getAttributes());
return o;
}
public static Organization convertOrganizationModelToOrganization(OrganizationModel e) {
Organization o =
new Organization()
.id(e.getId())
.name(e.getName())
.displayName(e.getDisplayName())
.domains(e.getDomains())
.url(e.getUrl())
.realm(e.getRealm().getName());
o.setAttributes(e.getAttributes());
return o;
}

public static UserRepresentation convertUserEntityToUserRepresentation(UserEntity e) {
UserRepresentation r = new UserRepresentation();
r.setEmail(e.getEmail());
r.setFirstName(e.getFirstName());
r.setLastName(e.getLastName());
r.setUsername(e.getUsername());
r.setEmailVerified(e.isEmailVerified());
r.setId(e.getId());
Map<String, List<String>> attr = Maps.newHashMap();
e.getAttributes()
.forEach(
a -> {
List<String> l = attr.get(a.getName());
if (l == null) l = Lists.newArrayList();
if (!l.contains(a.getValue())) l.add(a.getValue());
attr.put(a.getName(), l);
});
r.setAttributes(attr);
return r;
}
public static UserRepresentation convertUserEntityToUserRepresentation(UserEntity e) {
UserRepresentation r = new UserRepresentation();
r.setEmail(e.getEmail());
r.setFirstName(e.getFirstName());
r.setLastName(e.getLastName());
r.setUsername(e.getUsername());
r.setEmailVerified(e.isEmailVerified());
r.setId(e.getId());
Map<String, List<String>> attr = Maps.newHashMap();
e.getAttributes()
.forEach(
a -> {
List<String> l = attr.get(a.getName());
if (l == null) l = Lists.newArrayList();
if (!l.contains(a.getValue())) l.add(a.getValue());
attr.put(a.getName(), l);
});
r.setAttributes(attr);
return r;
}

public static Invitation convertInvitationEntityToInvitation(InvitationEntity e) {
Invitation i =
new Invitation()
.id(e.getId())
.email(e.getEmail())
.createdAt(e.getCreatedAt())
.inviterId(e.getInviterId())
.organizationId(e.getOrganization().getId())
.roles(Lists.newArrayList(e.getRoles()));
Map<String, List<String>> attr = Maps.newHashMap();
e.getAttributes()
.forEach(
a -> {
List<String> l = attr.get(a.getName());
if (l == null) l = Lists.newArrayList();
if (!l.contains(a.getValue())) l.add(a.getValue());
attr.put(a.getName(), l);
});
i.setAttributes(attr);
return i;
}
public static Invitation convertInvitationEntityToInvitation(InvitationEntity e) {
Invitation i =
new Invitation()
.id(e.getId())
.email(e.getEmail())
.createdAt(e.getCreatedAt())
.inviterId(e.getInviterId())
.organizationId(e.getOrganization().getId())
.roles(Lists.newArrayList(e.getRoles()));
Map<String, List<String>> attr = Maps.newHashMap();
e.getAttributes()
.forEach(
a -> {
List<String> l = attr.get(a.getName());
if (l == null) l = Lists.newArrayList();
if (!l.contains(a.getValue())) l.add(a.getValue());
attr.put(a.getName(), l);
});
i.setAttributes(attr);
return i;
}

public static Invitation convertInvitationModelToInvitation(InvitationModel e) {
Invitation i =
new Invitation()
.id(e.getId())
.email(e.getEmail())
.createdAt(e.getCreatedAt())
.inviterId(e.getInviter().getId())
.invitationUrl(e.getUrl())
.organizationId(e.getOrganization().getId())
.roles(Lists.newArrayList(e.getRoles()));
i.setAttributes(Maps.newHashMap(e.getAttributes()));
return i;
}
public static Invitation convertInvitationModelToInvitation(InvitationModel e) {
Invitation i =
new Invitation()
.id(e.getId())
.email(e.getEmail())
.createdAt(e.getCreatedAt())
.inviterId(e.getInviter().getId())
.invitationUrl(e.getUrl())
.organizationId(e.getOrganization().getId())
.roles(Lists.newArrayList(e.getRoles()));
i.setAttributes(Maps.newHashMap(e.getAttributes()));
return i;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
import static io.phasetwo.service.resource.OrganizationResourceType.*;
import static org.keycloak.events.EventType.UPDATE_PROFILE;
import static org.keycloak.models.utils.ModelToRepresentation.*;

import com.google.common.base.Strings;
import io.phasetwo.service.model.OrganizationModel;
Expand Down Expand Up @@ -42,10 +41,7 @@ public Stream<UserRepresentation> getMembers(
maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS;
return organization
.searchForOrganizationMembersStream(searchQuery, firstResult, maxResults)
.map(m ->{
var user = session.users().getUserById(realm, m.getUserId());
return toRepresentation(session, realm, user);
});
.map(m -> UserOrganizationMemberConverter.toRepresentation(session, realm, m));
}

@GET
Expand Down
Loading

0 comments on commit 2925c0b

Please sign in to comment.