ProcessUtils.java

package com.bonitasoft.processbuilder.extension;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.bonitasoft.processbuilder.enums.ActionType;
import com.bonitasoft.processbuilder.mapper.UserMapper;
import com.bonitasoft.processbuilder.records.UserRecord;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

import org.bonitasoft.engine.api.APIAccessor;
import org.bonitasoft.engine.api.IdentityAPI;
import org.bonitasoft.engine.api.ProcessAPI;
import org.bonitasoft.engine.api.ProfileAPI;
import org.bonitasoft.engine.bpm.flownode.ActivityInstanceNotFoundException;
import org.bonitasoft.engine.bpm.flownode.HumanTaskInstance;
import org.bonitasoft.engine.bpm.process.ProcessInstance;
import org.bonitasoft.engine.exception.RetrieveException;
import org.bonitasoft.engine.identity.ContactData;
import org.bonitasoft.engine.identity.MemberType;
import org.bonitasoft.engine.identity.User;
import org.bonitasoft.engine.identity.UserCriterion;
import org.bonitasoft.engine.identity.UserNotFoundException;
import org.bonitasoft.engine.identity.UserSearchDescriptor;
import org.bonitasoft.engine.profile.Profile;
import org.bonitasoft.engine.profile.ProfileMember;
import org.bonitasoft.engine.profile.ProfileMemberSearchDescriptor;
import org.bonitasoft.engine.profile.ProfileSearchDescriptor;
import org.bonitasoft.engine.search.SearchOptionsBuilder;
import org.bonitasoft.engine.search.SearchResult;
import org.bonitasoft.engine.session.InvalidSessionException;


/**
 * Utility class providing common process and BDM (Business Data Model) operations.
 * This includes retrieving the process initiator and utility methods for BDM validation
 * and deletion handling, specifically tailored for Bonita API interaction.
 * <p>
 * This class is non-instantiable and all methods are static.
 * </p>
 * @author Bonitasoft
 * @since 1.0
 */
public final class ProcessUtils {

    /**
     * A logger for this class, used to record log messages and provide debugging information.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(ProcessUtils.class);

    /**
     * Private constructor to prevent instantiation of this utility class.
     * All methods in this class are static and should be called directly on the class itself.
     * @throws UnsupportedOperationException always, to enforce the utility pattern.
     */
    private ProcessUtils() {
        throw new UnsupportedOperationException("This is a "+this.getClass().getSimpleName()+" class and cannot be instantiated.");
    }

    /**
     * Retrieves the user who started a specific process instance.
     * This method accesses the Bonita process and identity APIs to find the initiator's details.
     * If the initiator is not found, or an unexpected error occurs, a default 'unknown_user' is returned.
     *
     * @param apiAccessor An instance of {@link APIAccessor} to get the Bonita APIs.
     * @param processInstanceId The unique identifier of the process instance.
     * @return A {@link UserRecord} record containing the initiator's ID, username, and full name, etc.
     */
    public static UserRecord getProcessInitiator(APIAccessor apiAccessor, long processInstanceId) {
        try {
            LOGGER.info("Attempting to retrieve the user who started the process instance ID: {}", processInstanceId);
            ProcessAPI processAPI = apiAccessor.getProcessAPI();
            IdentityAPI identityAPI = apiAccessor.getIdentityAPI();

            ProcessInstance processInstance = processAPI.getProcessInstance(processInstanceId);
            long startedByUserId = processInstance.getStartedBy();
            User processInitiator = identityAPI.getUser(startedByUserId);

            String firstName = processInitiator.getFirstName();
            String lastName = processInitiator.getLastName();
            String creationFullName = firstName + " " + lastName;
            String creationUserName = processInitiator.getUserName();

            String email = null;
            try {
                ContactData startedByUserContactData = identityAPI.getUserContactData(startedByUserId, false);
                if (startedByUserContactData != null) {
                    email = startedByUserContactData.getEmail();
                }
            } catch (Exception e) {
                LOGGER.warn("Could not retrieve contact data for user ID {}: {}", startedByUserId, e.getMessage());
                email = null;
            }
            LOGGER.debug("Successfully retrieved initiator user: {}", creationFullName);
            return new UserRecord(startedByUserId, creationUserName, creationFullName, firstName, lastName, email);

        } catch (UserNotFoundException e) {
            LOGGER.warn("The user who started process instance ID {} was not found. Using 'unknown_user'.", processInstanceId, e);
            return new UserRecord(null, "unknown_user", "unknown_user", "unknown_user", "unknown_user", "unknown_user");

        } catch (Exception e) {
            LOGGER.error("An unexpected error occurred while retrieving the process initiator for process instance ID {}: {}"
                , processInstanceId, e.getMessage(), e);
            return new UserRecord(null, "unknown_user", "unknown_user", "unknown_user", "unknown_user", "unknown_user");
        }
    }

    /**
     * Retrieves the user who executed a specific human task instance.
     * This method accesses the Bonita Process and Identity APIs to find the executor's details.
     * If the executor is not found, or an unexpected error occurs, a default 'unknown_user' is returned.
     *
     * @param apiAccessor An instance of {@link APIAccessor} to get the Bonita APIs.
     * @param activityInstanceId The unique identifier of the human task instance (activityId).
     * @return A {@link UserRecord} record containing the executor's ID, username, and full name.
     */
    public static UserRecord getTaskExecutor(APIAccessor apiAccessor, long activityInstanceId) {
        try {
            LOGGER.info("Attempting to retrieve the user who executed the task instance ID: {}", activityInstanceId);

            HumanTaskInstance humanTaskInstance = apiAccessor.getProcessAPI().getHumanTaskInstance(activityInstanceId);

            long executedByUserId = humanTaskInstance.getExecutedBy();

            if (executedByUserId <= 0) {
                LOGGER.debug("Task instance ID {} was not executed by a human user (executedBy ID: {}).",
                            activityInstanceId, executedByUserId);
                return new UserRecord(
                        executedByUserId, "system_or_unassigned", "System or Unassigned",
                        "system_or_unassigned", "system_or_unassigned", "system_or_unassigned");
            }

            IdentityAPI identityAPI = apiAccessor.getIdentityAPI();
            User taskExecutor = identityAPI.getUser(executedByUserId);

            String firstName = taskExecutor.getFirstName();
            String lastName = taskExecutor.getLastName();
            String executorFullName = firstName + " " + lastName;
            String executorUserName = taskExecutor.getUserName();

            String email = null;
            try {
                ContactData startedByUserContactData = identityAPI.getUserContactData(executedByUserId, false);
                if (startedByUserContactData != null) {
                    email = startedByUserContactData.getEmail();
                }
            } catch (Exception e) {
                LOGGER.warn("Could not retrieve contact data for user ID {}: {}", executedByUserId, e.getMessage());
                email = null;
            }

            LOGGER.debug("Successfully retrieved executor user: {}", executorFullName);
            return new UserRecord(executedByUserId, executorUserName, executorFullName, firstName, lastName, email);

        } catch (UserNotFoundException e) {
            LOGGER.warn("The user who executed task instance ID {} was not found. Using 'unknown_user'.", activityInstanceId, e);
            return new UserRecord(null, "unknown_user", "Unknown User", "unknown_user", "unknown_user", "unknown_user");

        } catch (InvalidSessionException | ActivityInstanceNotFoundException | RetrieveException e) {
            LOGGER.error("An API error occurred while retrieving the task executor for activity ID {}: {}",
                        activityInstanceId, e.getMessage(), e);
            return new UserRecord(null, "api_error", "API Error", "api_error", "api_error", "api_error");

        } catch (Exception e) {
            LOGGER.error("An unexpected error occurred while retrieving the task executor for activity ID {}: {}",
                        activityInstanceId, e.getMessage(), e);
            return new UserRecord(null, "unexpected_error", "Unexpected Error", "unexpected_error", "unexpected_error", "unexpected_error");
        }
    }

    /**
     * Searches for a BDM object by its persistence ID and validates its existence.
     * This method should be used internally after the persistence ID has been successfully converted to a Long.
     * 
     * @param <T> The generic type of the BDM object (e.g., PBProcess, PBCategory).
     * @param persistenceId The ID (Long) used to search for the object. Must not be null.
     * @param searchFunction The function (e.g., DAO method) to perform the search: (Long ID -> T Object).
     * @param objectType The name of the BDM object class (e.g., "PBProcess" or "PBCategory"). Used for error logging.
     * @return The found BDM object of type T.
     * @throws RuntimeException if no object is found for the given ID (i.e., the search function returns {@code null}).
     */
    private static <T> T searchAndValidate(
            Long persistenceId, 
            Function<Long, T> searchFunction, 
            String objectType) throws RuntimeException {

        // Apply the search function
        T bdmObject = searchFunction.apply(persistenceId);

        if (bdmObject == null) {
            String errorMessage = String.format(
                "No existing %s found for persistenceId: '%s'. Cannot update.", 
                objectType, 
                persistenceId
            );
            LOGGER.error(errorMessage);
            // Uses an external utility to log the error and throw the exception
            throw ExceptionUtils.logAndThrow(() -> new RuntimeException(errorMessage), errorMessage);
        }
        
        return bdmObject;
    }

    /**
     * Validates that the BDM object exists before performing a DELETE action.
     * If the object is not found (i.e., is {@code null}), a {@code RuntimeException} is thrown.
     * 
     * @param <T> The generic type of the BDM object (e.g., PBProcess, PBCategory).
     * @param bdmObject The BDM object retrieved from the search (may be null).
     * @param persistenceId The ID used for logging.
     * @param objectType The name of the BDM object class.
     * @return The existing BDM object (T).
     * @throws RuntimeException if the object is null (not found for deletion).
     */
    private static <T> T validateForDelete(T bdmObject, Long persistenceId, String objectType) throws RuntimeException {
        
        if (bdmObject == null) {
            String errorMessage = String.format(
                "No existing %s found to delete with persistenceId: '%s'.", 
                objectType, 
                persistenceId
            );
            
            // Uses an external utility to log the error and throw the exception
            throw ExceptionUtils.logAndThrow(() -> new RuntimeException(errorMessage), errorMessage);
        }
        
        LOGGER.info("Finished processing DELETE action for ID: {}", persistenceId);
        return bdmObject;
    }

    /**
     * Searches for a BDM object by its persistence ID, handling the ID conversion from String to Long.
     * Validates that the object exists if the persistence ID is present.
     *
     * @param <T> The generic type of the BDM object (e.g., PBProcess, PBCategory).
     * @param persistenceIdString The persistence ID as a String (can be null or empty).
     * @param searchFunction The function (e.g., DAO method) to perform the search: (Long ID -> T Object).
     * @param objectType The name of the BDM object class (e.g., "PBProcess" or "PBCategory").
     * @return The found BDM object of type T, or {@code null} if the persistence ID is null or empty.
     * @throws RuntimeException if the String cannot be converted to Long, or if the object is not found (and ID was present).
     */
    public static <T> T searchAndValidateId(
            String persistenceIdString, 
            Function<Long, T> searchFunction, 
            String objectType) throws RuntimeException  {

        Long persistenceId = null;

        if (persistenceIdString != null && !persistenceIdString.isEmpty()) {
            try {
                // First: Convert the ID string to Long
                persistenceId = Long.valueOf(persistenceIdString);
            } catch (NumberFormatException e) {
                String errorMessage = String.format(
                    "Invalid format for %s persistence ID: '%s'. Must be a valid number.", 
                    objectType, 
                    persistenceIdString
                );
                LOGGER.error(errorMessage, e);
                throw ExceptionUtils.logAndThrow(() -> new RuntimeException(errorMessage), errorMessage);
            }
            
            // Second: Search and validate the object's existence
            T bdmObject = searchAndValidate(persistenceId, searchFunction, objectType);
            
            return bdmObject;
        }

        // Return null if the input ID string was null or empty
        return null;
    }

    /**
     * Combines the check for a DELETE action with the validation that the BDM object exists.
     * If the {@code actionTypeInput} is "DELETE", it delegates to {@code validateForDelete}.
     * For any other action type (or null), it returns {@code null}, assuming the caller will handle
     * creation or update logic on the {@code bdmObject}.
     *
     * @param <T> The generic type of the BDM object.
     * @param bdmObject The BDM object retrieved from the search (may be null).
     * @param actionTypeInput The action type (e.g., "DELETE", "INSERT", "UPDATE").
     * @param persistenceId The ID used for logging.
     * @param objectType The name of the BDM object class.
     * @return The existing BDM object (T) if the action is DELETE and the object exists; otherwise, returns {@code null}.
     * @throws RuntimeException if the action is DELETE but the object is null (not found).
     */
    public static <T> T validateActionAndDelete(
            T bdmObject, 
            String actionTypeInput, 
            Long persistenceId, 
            String objectType) {
        
        if (actionTypeInput != null && actionTypeInput.equalsIgnoreCase(ActionType.DELETE.name())) {
            // Validate existence before deletion
            return validateForDelete(bdmObject, persistenceId, objectType);
        }
        // Return null if it's not a DELETE action (e.g., CREATE or UPDATE)
        return null;
    }

   /**
     * Searches for a BDM object using a provided search function (closure/lambda).
     * This method is a generic wrapper that accepts a search function and applies it to retrieve the object.
     * It does not perform any validation; use {@code searchAndValidateId} for validated searches.
     * <p>
     * This method is useful when you want to defer the actual search logic to the caller,
     * allowing the same retrieval method to work with different BDM types through their respective DAOs.
     * </p>
     *
     * @param <T> The generic type of the BDM object to be retrieved.
     * @param persistenceId The ID (Long) used to search for the object.
     * @param searchFunction The function that performs the search. Must accept a Long ID and return the BDM object (or null if not found).
     * @param objectType The name of the BDM object class (e.g., "PBProcess"). Used for logging purposes only.
     * @return The BDM object found by the search function, or {@code null} if the search function returns null.
     */
    public static <T> T searchById(
            Long persistenceId, 
            Function<Long, T> searchFunction, 
            String objectType) {
        
        if (persistenceId == null || persistenceId <= 0) {
            LOGGER.warn("Skipping search for {} with invalid persistenceId: {}", objectType, persistenceId);
            return null;
        }

        try {
            LOGGER.debug("Searching for {} with persistenceId: {}", objectType, persistenceId);
            T result = searchFunction.apply(persistenceId);
            
            if (result != null) {
                LOGGER.debug("Successfully retrieved {} with persistenceId: {}", objectType, persistenceId);
            } else {
                LOGGER.debug("No {} found for persistenceId: {}", objectType, persistenceId);
            }
            
            return result;
        } catch (Exception e) {
            LOGGER.error("Error occurred while searching for {} with persistenceId: {}. Message: {}", 
                        objectType, persistenceId, e.getMessage(), e);
            return null;
        }
    }

    /**
     * Searches for a BDM object by its persistence ID string, handling conversion and validation.
     * This is a convenience method that combines ID parsing and object search in a single call.
     * <p>
     * This method is particularly useful for Bonita scripts where the persistence ID comes as a String
     * and you want to retrieve any BDM object type without writing multiple lines of boilerplate code.
     * </p>
     *
     * @param <T> The generic type of the BDM object to be retrieved.
     * @param persistenceIdInput The persistence ID as a String (can be null or empty).
     * @param searchFunction The function that performs the search. Must accept a Long ID and return the BDM object (or null if not found).
     * @param objectType The name of the BDM object class (e.g., "PBProcess" or "PBCategory"). Used for logging purposes.
     * @return The BDM object if found, or {@code null} if the persistence ID is null/empty, invalid format, or object not found.
     */
    public static <T> T searchBDM(
            String persistenceIdInput,
            Function<Long, T> searchFunction,
            String objectType) {

        if (persistenceIdInput == null || persistenceIdInput.trim().isEmpty()) {
            LOGGER.warn("Skipping search: persistenceId is null or empty for object type {}", objectType);
            return null;
        }

        try {
            Long persistenceId = Long.valueOf(persistenceIdInput.trim());
            return searchById(persistenceId, searchFunction, objectType);
        } catch (NumberFormatException e) {
            LOGGER.error("Invalid persistenceId format '{}' for {}. Must be a valid number.",
                        persistenceIdInput, objectType, e);
            return null;
        } catch (Exception e) {
            LOGGER.error("Error searching for {} with persistenceId: {}", objectType, persistenceIdInput, e);
            return null;
        }
    }

    /**
     * Searches for a BDM object by a string key or reference, handling validation and error handling.
     * This is a convenience method for queries that use string-based identifiers (like stepActionRef)
     * instead of numeric persistence IDs.
     * <p>
     * This method is particularly useful for Bonita scripts where you need to find BDM objects
     * by their business reference strings rather than their persistence IDs.
     * </p>
     * <p>
     * Example usage:
     * </p>
     * <pre>{@code
     * PBAction pbAction = ProcessUtils.searchByStringKey(
     *     stepActionRefInput,
     *     stepActionRef -> pBActionDAO.findByStepActionRef(stepActionRef),
     *     "PBAction"
     * );
     * }</pre>
     *
     * @param <T> The generic type of the BDM object to be retrieved.
     * @param searchKeyInput The string key/reference used to search for the object (can be null or empty).
     * @param searchFunction The function that performs the search. Must accept a String key and return the BDM object (or null if not found).
     * @param objectType The name of the BDM object class (e.g., "PBAction"). Used for logging purposes.
     * @return The BDM object if found, or {@code null} if the search key is null/empty or object not found.
     */
    public static <T> T searchByStringKey(
            String searchKeyInput,
            Function<String, T> searchFunction,
            String objectType) {

        if (searchKeyInput == null || searchKeyInput.trim().isEmpty()) {
            LOGGER.warn("Skipping search: searchKey is null or empty for object type {}", objectType);
            return null;
        }

        String trimmedKey = searchKeyInput.trim();

        try {
            LOGGER.debug("Searching for {} with key: {}", objectType, trimmedKey);
            T result = searchFunction.apply(trimmedKey);

            if (result != null) {
                LOGGER.debug("Successfully retrieved {} with key: {}", objectType, trimmedKey);
            } else {
                LOGGER.debug("No {} found for key: {}", objectType, trimmedKey);
            }

            return result;
        } catch (Exception e) {
            LOGGER.error("Error searching for {} with key: {}. Message: {}",
                        objectType, trimmedKey, e.getMessage(), e);
            return null;
        }
    }

    /**
     * Searches for a list of BDM objects by a persistence ID, handling validation and error handling.
     * This is a convenience method for queries that return collections based on a parent entity ID.
     * <p>
     * This method is particularly useful for Bonita scripts where you need to retrieve
     * related BDM objects (e.g., all PBActionContent for a given PBAction).
     * </p>
     *
     * @param <T> The generic type of the BDM objects in the returned list.
     * @param persistenceIdInput The persistence ID of the parent entity (can be null or not positive).
     * @param searchFunction The function that performs the search. Must accept a Long ID and return a List of BDM objects.
     * @param objectType The name of the BDM object class (e.g., "PBActionContent"). Used for logging purposes.
     * @return A list of BDM objects if found, or an empty list if the persistence ID is invalid or no results found.
     */
    public static <T> List<T> searchBDMList(
            Long persistenceIdInput,
            Function<Long, List<T>> searchFunction,
            String objectType) {

        if (persistenceIdInput == null || persistenceIdInput <= 0L) {
            LOGGER.warn("Skipping search: persistenceId is null or not positive for object type {}. Received: {}",
                        objectType, persistenceIdInput);
            return Collections.emptyList();
        }

        LOGGER.info("Fetching {} list for persistenceId: {}", objectType, persistenceIdInput);

        try {
            List<T> resultList = searchFunction.apply(persistenceIdInput);

            if (resultList == null || resultList.isEmpty()) {
                LOGGER.info("No {} found for persistenceId: {}", objectType, persistenceIdInput);
                return Collections.emptyList();
            }

            LOGGER.info("Successfully retrieved {} {}(s) for persistenceId: {}",
                        resultList.size(), objectType, persistenceIdInput);

            return resultList;

        } catch (Exception e) {
            LOGGER.error("Error fetching {} list for persistenceId: {}. Cause: {}",
                        objectType, persistenceIdInput, e.getMessage(), e);
            return Collections.emptyList();
        }
    }

    /**
     * Finds the most recent step process instance using a search function.
     * <p>
     * This method accepts a search function that returns a list of step instances
     * ordered by recency (most recent first). It returns the first element from the list,
     * which represents the most recent instance.
     * </p>
     * <p>
     * The search function should be provided by the caller as a lambda or method reference
     * that encapsulates the specific DAO call and its parameters, maintaining independence
     * from BDM classes.
     * </p>
     * Example usage:
     * <pre>{@code
     * PBStepProcessInstance mostRecent = ProcessUtils.findMostRecentStepInstance(
     *     () -> pBStepProcessInstanceDAO.findMostRecentByProcessInstanceAndRefStepAndStatus(
     *         processInstancePersistenceId,
     *         refStep,
     *         StepProcessInstanceStateType.ENDED.getKey(),
     *         0,
     *         1
     *     ),
     *     "PBStepProcessInstance"
     * );
     * }</pre>
     *
     * @param <T> The generic type of the step process instance object
     * @param searchFunction A supplier that performs the search and returns a list of instances
     * @param objectType The name of the object type for logging purposes (e.g., "PBStepProcessInstance")
     * @return The most recent step instance (first element in the list), or {@code null} if no instances found or on error
     */
    public static <T> T findMostRecentStepInstance(
            Supplier<List<T>> searchFunction,
            String objectType) {

        try {
            LOGGER.debug("Searching for most recent {} instance", objectType);

            List<T> stepInstances = searchFunction.get();

            if (stepInstances == null || stepInstances.isEmpty()) {
                LOGGER.warn("No {} instances found", objectType);
                return null;
            }

            // Query orders by persistenceId DESC, so take the first one (most recent)
            T mostRecent = stepInstances.get(0);
            LOGGER.debug("Successfully retrieved most recent {} instance", objectType);

            return mostRecent;

        } catch (Exception e) {
            LOGGER.error("Error occurred while searching for most recent {} instance: {}",
                        objectType, e.getMessage(), e);
            return null;
        }
    }

    /**
     * Retrieves the list of user IDs associated with a specific profile.
     * <p>
     * This method searches for all users assigned to the specified profile, including users
     * assigned directly, through roles, through groups, or through memberships (role + group).
     * </p>
     *
     * @param apiAccessor An instance of {@link APIAccessor} to access Bonita APIs.
     * @param profileName The name of the profile to search for user assignments.
     * @return A list of user IDs associated with the profile, or an empty list if an error occurs.
     */
    public List<Long> getUserIdsInProfile(APIAccessor apiAccessor, String profileName) {
        try {
            List<Long> userIds = new ArrayList<>();

            ProfileAPI profileAPI = apiAccessor.getProfileAPI();
            IdentityAPI identityAPI = apiAccessor.getIdentityAPI();

            SearchOptionsBuilder searchBuilder = new SearchOptionsBuilder(0, Integer.MAX_VALUE);
            searchBuilder.filter(ProfileSearchDescriptor.NAME, "PBAdministrator");

            SearchResult<Profile> searchResultProfile = profileAPI.searchProfiles(searchBuilder.done());

            List<Profile> profileList = searchResultProfile.getResult();

            Profile administratorProfile = profileList.get(0);

            searchBuilder = new SearchOptionsBuilder(0, Integer.MAX_VALUE);
            searchBuilder.filter(ProfileMemberSearchDescriptor.PROFILE_ID, administratorProfile.getId());
            SearchResult<ProfileMember> searchResultProfileMember =
                    profileAPI.searchProfileMembers(MemberType.USER.name(), searchBuilder.done());

            for (ProfileMember profileMember : searchResultProfileMember.getResult()) {
                if (profileMember.getUserId() > 0) {
                    userIds.add(profileMember.getUserId());
                }
            }

            searchBuilder = new SearchOptionsBuilder(0, Integer.MAX_VALUE);
            searchBuilder.filter(ProfileMemberSearchDescriptor.PROFILE_ID, administratorProfile.getId());
            searchResultProfileMember =
                    profileAPI.searchProfileMembers(MemberType.ROLE.name(), searchBuilder.done());

            for (ProfileMember profileMember : searchResultProfileMember.getResult()) {
                if (profileMember.getRoleId() > 0) {
                    List<User> roleUsers = identityAPI.getActiveUsersInRole(
                            profileMember.getRoleId(), 0, Integer.MAX_VALUE, UserCriterion.USER_NAME_ASC);
                    userIds.addAll(UserMapper.toLongIds(roleUsers));
                }
            }

            searchBuilder = new SearchOptionsBuilder(0, Integer.MAX_VALUE);
            searchBuilder.filter(ProfileMemberSearchDescriptor.PROFILE_ID, administratorProfile.getId());
            searchResultProfileMember =
                    profileAPI.searchProfileMembers(MemberType.GROUP.name(), searchBuilder.done());

            for (ProfileMember profileMember : searchResultProfileMember.getResult()) {
                if (profileMember.getGroupId() > 0) {
                    List<User> groupUsers = identityAPI.getActiveUsersInGroup(
                            profileMember.getGroupId(), 0, Integer.MAX_VALUE, UserCriterion.USER_NAME_ASC);
                    userIds.addAll(UserMapper.toLongIds(groupUsers));
                }
            }

            searchBuilder = new SearchOptionsBuilder(0, Integer.MAX_VALUE);
            searchBuilder.filter(ProfileMemberSearchDescriptor.PROFILE_ID, administratorProfile.getId());
            searchResultProfileMember =
                    profileAPI.searchProfileMembers(MemberType.MEMBERSHIP.name(), searchBuilder.done());

            for (ProfileMember profileMember : searchResultProfileMember.getResult()) {
                if (profileMember.getGroupId() > 0 && profileMember.getRoleId() > 0) {
                    searchBuilder = new SearchOptionsBuilder(0, Integer.MAX_VALUE);
                    searchBuilder.filter(UserSearchDescriptor.GROUP_ID,profileMember.getGroupId());
                    searchBuilder.filter(UserSearchDescriptor.ROLE_ID,profileMember.getRoleId());
                    
                    SearchResult<User> searchResultUser = identityAPI.searchUsers(searchBuilder.done());
                    List<User> userList = searchResultUser.getResult();
                            
                    userIds.addAll(UserMapper.toLongIds(userList));
                }
            }

            return userIds;
        } catch (Exception e) {
            LOGGER.error("Error retrieving user IDs for profile '{}': {}", profileName, e.getMessage(), e);
            return List.of();
        }
    }
}