Kris, thank you for the direction.
After much thought and time, I have found a few things :
- Group DeveloperName is NOT unique - this was a bit of a surprise for me.
- You must query for all Groups and their members anyway to construct this for Queues, so I extended the functionality to include all active users for any group in the system.
- Group has an attribute called
DoesIncludeBosses
which if true will include any users in the role hierarchy above the group members.
- The response that is returned holds a lot of data, so we will lazy load it to save queries and run time calculations when used multiple times.
Here is the solution which I came up with. It genereates a Map<String,Set<Id>>
where the key is group.DeveloperName + '-' + group.Type
and the value is a set of all active users for that group. It uses the Role Hierarchy which was described in the link posted by Kris.
Here are some of the helper methods for roles
public static Map<Id, List<UserRole>> hierarchy {
get {
hierarchy = new Map<Id, List<UserRole>>();
for (UserRole role : userRoleMap.values()) {
if (!hierarchy.containsKey(role.ParentRoleId)) {
hierarchy.put(role.ParentRoleId, new List<UserRole>());
}
hierarchy.get(role.ParentRoleId).add(role);
}
return hierarchy;
}
private set;
}
public static List<UserRole> getChildren(Id userRoleId) {
return hierarchy.containsKey(userRoleId) ? hierarchy.get(userRoleId) : new List<UserRole>();
}
public static Set<Id> getSubHierarchy(Id userRoleId) {
Set<Id> roleIds = new Set<Id> { userRoleId };
for (UserRole childRole : getChildren(userRoleId)) {
roleIds.addAll(getSubHierarchy(childRole.Id));
}
return roleIds;
}
public static Set<Id> getParentRoles(Id userRoleId) {
Set<Id> roleIds = new Set<Id>();
UserRole currentRole = userRoleMap.get(userRoleId);
while (currentRole.ParentRoleId != null) {
roleIds.add(currentRole.ParentRoleId);
currentRole = userRoleMap.get(currentRole.ParentRoleId);
}
return roleIds;
}
private static Map<Id,Set<Id>> getRoleIdToUsers(Map<Id,User> userMap) {
Map<Id, Set<Id>> roleIdToUsers = new Map<Id,Set<Id>>();
for (User u : userMap.values()) {
if (u.UserRoleId != null ){
if (!roleIdToUsers.containsKey(u.UserRoleId)) {
roleIdToUsers.put(u.UserRoleId, new Set<Id>());
}
roleIdToUsers.get(u.UserRoleId).add(u.Id);
}
}
return roleIdToUsers;
}
And here is the main method which does the calculation for you. The basic strategy here is to calculate the entire group based on type. If the Type is Queue or Regular then the groupMembers can be other groups, so we must calculate their membership recursively.
@TestVisible
public static Map<String,Set<Id>> groupDeveloperNameAndTypeToActiveUserIds {
get {
if (groupDeveloperNameAndTypeToActiveUserIds == null) {
groupDeveloperNameAndTypeToActiveUserIds = new Map<String,Set<Id>>();
//query for all relevant data
Map<Id,Group> groupMap = new Map<Id,Group>([SELECT Id, DeveloperName, DoesIncludeBosses, Type, RelatedId, (SELECT UserOrGroupId FROM GroupMembers) FROM Group]);
Map<Id,User> userMap = new Map<Id,User>([SELECT Id, IsActive, IsPortalEnabled, UserRoleId FROM User WHERE IsActive = true]);
Map<Id,Set<Id>> roleIdToUsers = getRoleIdToUsers(userMap);
//calculate group users for each individual group
for (Group g : groupMap.values()) {
groupDeveloperNameAndTypeToActiveUserIds.put(g.DeveloperName + '-' + g.Type, getGroupUsers(g, groupMap, userMap, roleIdToUsers));
}
//add users above in the hierarchy
for (Group g : groupMap.values()) {
if (g.DoesIncludeBosses == true) {
Set<Id> rolesForUsersInThisGroup = new Set<Id>();
for (id userId : groupDeveloperNameAndTypeToActiveUserIds.get(g.DeveloperName + '-' + g.Type)) {
User u = userMap.get(userId);
if (u.UserRoleId != null) {
rolesForUsersInThisGroup.add(u.UserRoleId);
}
}
Set<Id> allParentRoles = new Set<Id>();
for (Id roleId : rolesForUsersInThisGroup) {
allParentRoles.addAll(getParentRoles(roleId));
}
for (Id parentRoleId : allParentRoles) {
groupDeveloperNameAndTypeToActiveUserIds.get(g.DeveloperName + '-' + g.Type).addAll(roleIdToUsers.get(parentRoleId));
}
}
}
}
return groupDeveloperNameAndTypeToActiveUserIds;
}
private set;
}
static Set<Id> getGroupUsers(Group g, Map<Id,Group> groupMap, Map<Id,User> userMap, Map<Id,Set<Id>> roleIdToUsers) {
Set<Id> usersForThisGroup = new Set<Id>();
switch on g.Type {
when 'Organization' {
for (User u : userMap.values()) {
if (!u.IsPortalEnabled) {
usersForThisGroup.add(u.id);
}
}
}
when 'Regular' {
for (GroupMember gm : g.GroupMembers) {
String sobjectType = gm.UserOrGroupId.getSObjectType().getDescribe().getName();
if (sobjectType == 'User') {
if (userMap.containsKey(gm.UserOrGroupId)) {
usersForThisGroup.add(gm.UserOrGroupId);
}
} else {
//recurse down the group tree
usersForThisGroup.addAll(getGroupUsers(groupmap.get(gm.UserOrGroupId), groupMap, userMap, roleIdToUsers));
}
}
}
when 'Queue' {
for (GroupMember gm : g.GroupMembers) {
String sobjectType = gm.UserOrGroupId.getSObjectType().getDescribe().getName();
if (sobjectType == 'User') {
if (userMap.containsKey(gm.UserOrGroupId)) {
usersForThisGroup.add(gm.UserOrGroupId);
}
} else {
//recurse down the group tree
usersForThisGroup.addAll(getGroupUsers(groupmap.get(gm.UserOrGroupId), groupMap, userMap, roleIdToUsers));
}
}
}
when 'AllCustomerPortal' {
for (User u : userMap.values()) {
if (u.IsPortalEnabled) {
usersForThisGroup.add(u.id);
}
}
}
when 'Role' {
if (roleIdToUsers.containsKey(g.RelatedId)) {
usersForThisGroup.addAll(roleIdToUsers.get(g.RelatedId));
}
}
when 'RoleAndSubordinates' {
Set<Id> subordinateRoles = getSubHierarchy(g.RelatedId);
for (Id roleId : subordinateRoles) {
if (roleIdToUsers.keySet().contains(roleId)) {
usersForThisGroup.addAll(roleIdToUsers.get(g.RelatedId));
}
}
}
when 'RoleAndSubordinatesInternal' {
Set<Id> subordinateRoles = getSubHierarchy(g.RelatedId);
for (Id roleId : subordinateRoles) {
if (roleIdToUsers.keySet().contains(roleId)) {
for (Id userId : (roleIdToUsers.get(g.RelatedId))) {
if (!userMap.get(userId).IsPortalEnabled) {
usersForThisGroup.add(userId);
}
}
}
}
}
}
return usersForThisGroup;
}