TaskAssignationUtils.java
package com.bonitasoft.processbuilder.extension;
import com.bonitasoft.processbuilder.records.StepFieldRef;
import com.bonitasoft.processbuilder.records.UsersConfigRecord;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bonitasoft.engine.api.IdentityAPI;
import org.slf4j.Logger;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
/**
* Utility class for task assignation operations.
* <p>
* Provides optimized methods to parse user configuration JSON and collect
* candidate user IDs for task assignation filters in Bonita processes.
* </p>
* <p>
* This class is designed to work without direct BDM dependencies. All BDM
* access is performed through functional interfaces (Suppliers and Functions)
* that are provided by the calling Groovy script.
* </p>
* <p>
* Example usage in Groovy script:
* </p>
* <pre>{@code
* UsersConfigRecord config = TaskAssignationUtils.parseUsersConfig(pbAction.content, logger)
* Set<Long> userIds = TaskAssignationUtils.collectAllUserIds(
* config,
* { stepRef -> getMostRecentStepInstance(stepRef, processInstanceId, dao, logger) },
* { stepInstance -> IdentityUtils.getUserIdFromObject(stepInstance, "getUserId") },
* { membershipRefs -> pBUserListDAO.findByProcessIdAndRefMemberships(processId, membershipRefs, 0, Integer.MAX_VALUE) },
* identityAPI,
* logger
* )
* }</pre>
*
* @author Bonitasoft
* @since 1.0
*/
public final class TaskAssignationUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private TaskAssignationUtils() {
throw new UnsupportedOperationException("This is a " + this.getClass().getSimpleName()
+ " class and cannot be instantiated.");
}
// ========================================================================
// JSON Parsing Methods
// ========================================================================
/**
* Parses JSON content and extracts the user configuration for task assignation.
* <p>
* Expected JSON structure:
* </p>
* <pre>{@code
* {
* "users": {
* "stepUser": "step_xxx",
* "stepManager": "step_yyy",
* "memberShips": ["membership_1", "membership_2"],
* "membersShipsInput": "step_zzz:field_www"
* }
* }
* }</pre>
*
* @param jsonContent The JSON content string to parse (nullable)
* @param logger Logger for reporting (nullable)
* @return UsersConfigRecord with parsed values, or empty config if parsing fails
*/
public static UsersConfigRecord parseUsersConfig(String jsonContent, Logger logger) {
if (jsonContent == null || jsonContent.isBlank()) {
logWarn(logger, "JSON content is null or blank, returning empty users config");
return UsersConfigRecord.empty();
}
try {
JsonNode rootNode = OBJECT_MAPPER.readTree(jsonContent);
if (rootNode == null) {
logWarn(logger, "Failed to parse JSON content, returning empty users config");
return UsersConfigRecord.empty();
}
JsonNode usersNode = rootNode.get(UsersConfigRecord.USERS_KEY);
return UsersConfigRecord.fromUsersNode(usersNode, logger);
} catch (JsonProcessingException e) {
logWarn(logger, "JSON parsing error: {}", e.getMessage());
return UsersConfigRecord.empty();
}
}
/**
* Parses JSON content and returns the "users" node directly.
* <p>
* This method is useful when you need more control over the JSON parsing
* or want to access other nodes in the JSON.
* </p>
*
* @param jsonContent The JSON content string to parse (nullable)
* @param logger Logger for reporting (nullable)
* @return Optional containing the users JsonNode, or empty if not found
*/
public static Optional<JsonNode> parseUsersNode(String jsonContent, Logger logger) {
if (jsonContent == null || jsonContent.isBlank()) {
return Optional.empty();
}
try {
JsonNode rootNode = OBJECT_MAPPER.readTree(jsonContent);
if (rootNode == null) {
return Optional.empty();
}
JsonNode usersNode = rootNode.get(UsersConfigRecord.USERS_KEY);
if (usersNode == null || usersNode.isNull()) {
logWarn(logger, "Key '{}' not found in JSON", UsersConfigRecord.USERS_KEY);
return Optional.empty();
}
return Optional.of(usersNode);
} catch (JsonProcessingException e) {
logWarn(logger, "JSON parsing error: {}", e.getMessage());
return Optional.empty();
}
}
// ========================================================================
// User ID Collection Methods
// ========================================================================
/**
* Collects all candidate user IDs based on the user configuration.
* <p>
* This method processes all user sources defined in the configuration:
* </p>
* <ol>
* <li>stepUser - User who executed a specific step</li>
* <li>stepManager - Manager of the user who executed a specific step</li>
* <li>memberShips - Users from static membership references</li>
* <li>membersShipsInput - Users from dynamically retrieved membership</li>
* </ol>
* <p>
* The method uses functional interfaces to access BDM data, ensuring no
* direct BDM dependencies in this library.
* </p>
*
* @param <T> Type of the step instance object
* @param <M> Type of the membership list object
* @param config The parsed user configuration
* @param stepInstanceFinder Function that finds a step instance by reference
* @param userIdExtractor Function that extracts user ID from a step instance
* @param membershipFinder Function that finds membership objects by reference array
* @param identityAPI Bonita Identity API for user lookups
* @param logger Logger for reporting (nullable)
* @return Set of unique candidate user IDs (never null, may be empty)
*/
public static <T, M> Set<Long> collectAllUserIds(
UsersConfigRecord config,
Function<String, T> stepInstanceFinder,
Function<T, Long> userIdExtractor,
Function<String[], List<M>> membershipFinder,
IdentityAPI identityAPI,
Logger logger) {
if (config == null || !config.hasAnySource()) {
logDebug(logger, "No user sources defined in configuration");
return Collections.emptySet();
}
Set<Long> userIds = new HashSet<>();
// Process stepUser
processStepUser(config, stepInstanceFinder, userIdExtractor, logger)
.ifPresent(userId -> {
userIds.add(userId);
logInfo(logger, "Added stepUser ID: {}", userId);
});
// Process stepManager
processStepManager(config, stepInstanceFinder, userIdExtractor, identityAPI, logger)
.ifPresent(managerId -> {
userIds.add(managerId);
logInfo(logger, "Added stepManager ID: {}", managerId);
});
// Process static memberShips
if (config.hasMemberShips()) {
Set<Long> membershipUsers = processMemberships(
config.memberShips(), membershipFinder, identityAPI, logger);
userIds.addAll(membershipUsers);
logInfo(logger, "Added {} users from static memberShips", membershipUsers.size());
}
// Process dynamic membersShipsInput
processDynamicMembership(config, stepInstanceFinder, userIdExtractor, membershipFinder,
identityAPI, logger, userIds);
logInfo(logger, "Total unique candidate user IDs collected: {}", userIds.size());
return userIds;
}
/**
* Processes the stepUser configuration and returns the user ID.
*
* @param <T> Type of the step instance object
* @param config The parsed user configuration
* @param stepInstanceFinder Function that finds a step instance by reference
* @param userIdExtractor Function that extracts user ID from a step instance
* @param logger Logger for reporting (nullable)
* @return Optional containing the step user ID, or empty if not found
*/
public static <T> Optional<Long> processStepUser(
UsersConfigRecord config,
Function<String, T> stepInstanceFinder,
Function<T, Long> userIdExtractor,
Logger logger) {
if (config == null || !config.hasStepUser()) {
return Optional.empty();
}
String stepRef = config.stepUser();
logDebug(logger, "Processing stepUser reference: {}", stepRef);
return findStepInstanceAndExtractUserId(stepRef, stepInstanceFinder, userIdExtractor, logger);
}
/**
* Processes the stepManager configuration and returns the manager's user ID.
*
* @param <T> Type of the step instance object
* @param config The parsed user configuration
* @param stepInstanceFinder Function that finds a step instance by reference
* @param userIdExtractor Function that extracts user ID from a step instance
* @param identityAPI Bonita Identity API for manager lookup
* @param logger Logger for reporting (nullable)
* @return Optional containing the manager user ID, or empty if not found
*/
public static <T> Optional<Long> processStepManager(
UsersConfigRecord config,
Function<String, T> stepInstanceFinder,
Function<T, Long> userIdExtractor,
IdentityAPI identityAPI,
Logger logger) {
if (config == null || !config.hasStepManager()) {
return Optional.empty();
}
String stepRef = config.stepManager();
logDebug(logger, "Processing stepManager reference: {}", stepRef);
Optional<Long> stepUserIdOpt = findStepInstanceAndExtractUserId(
stepRef, stepInstanceFinder, userIdExtractor, logger);
if (stepUserIdOpt.isEmpty()) {
logWarn(logger, "No user found for stepManager reference: {}", stepRef);
return Optional.empty();
}
Long stepUserId = stepUserIdOpt.get();
Long managerId = IdentityUtils.getUserManager(stepUserId, identityAPI);
if (managerId == null) {
logWarn(logger, "No manager found for user ID: {}", stepUserId);
return Optional.empty();
}
return Optional.of(managerId);
}
/**
* Processes a list of membership references and returns matching user IDs.
*
* @param <M> Type of the membership list object
* @param membershipRefs List of membership reference strings
* @param membershipFinder Function that finds membership objects by reference array
* @param identityAPI Bonita Identity API for user lookups
* @param logger Logger for reporting (nullable)
* @return Set of user IDs from the memberships (never null)
*/
public static <M> Set<Long> processMemberships(
List<String> membershipRefs,
Function<String[], List<M>> membershipFinder,
IdentityAPI identityAPI,
Logger logger) {
if (membershipRefs == null || membershipRefs.isEmpty()) {
return Collections.emptySet();
}
try {
String[] refArray = membershipRefs.toArray(new String[0]);
logDebug(logger, "Processing {} membership references", refArray.length);
List<M> membershipList = membershipFinder.apply(refArray);
if (membershipList == null || membershipList.isEmpty()) {
logDebug(logger, "No membership objects found for references");
return Collections.emptySet();
}
logDebug(logger, "Found {} membership objects", membershipList.size());
// Use IdentityUtils to get users by memberships (uses reflection)
return IdentityUtils.getUsersByMemberships((List<?>) membershipList, identityAPI);
} catch (Exception e) {
logWarn(logger, "Error processing memberships: {}", e.getMessage());
return Collections.emptySet();
}
}
/**
* Processes a single membership reference and returns matching user IDs.
* <p>
* This is a convenience method for processing a single membership reference
* instead of a list.
* </p>
*
* @param <M> Type of the membership list object
* @param membershipRef Single membership reference string
* @param membershipFinder Function that finds membership objects by reference array
* @param identityAPI Bonita Identity API for user lookups
* @param logger Logger for reporting (nullable)
* @return Set of user IDs from the membership (never null)
*/
public static <M> Set<Long> processSingleMembership(
String membershipRef,
Function<String[], List<M>> membershipFinder,
IdentityAPI identityAPI,
Logger logger) {
if (membershipRef == null || membershipRef.isBlank()) {
return Collections.emptySet();
}
return processMemberships(List.of(membershipRef.trim()), membershipFinder, identityAPI, logger);
}
// ========================================================================
// Dynamic Membership Methods
// ========================================================================
/**
* Extracts a membership ID from a step's JSON input field.
* <p>
* This method parses the step's jsonInput and extracts the value of the
* specified field. The step and field are specified in the stepFieldRef
* parameter in format "step_xxx:field_yyy".
* </p>
*
* @param <T> Type of the step instance object
* @param stepFieldRefString The step:field reference in format "step_xxx:field_yyy"
* @param stepInstanceFinder Function that finds a step instance by reference
* @param jsonInputExtractor Function that extracts the jsonInput string from a step instance
* @param logger Logger for reporting (nullable)
* @return Optional containing the membership ID, or empty if not found
*/
public static <T> Optional<String> extractMembershipFromStepInput(
String stepFieldRefString,
Function<String, T> stepInstanceFinder,
Function<T, String> jsonInputExtractor,
Logger logger) {
if (stepFieldRefString == null || stepFieldRefString.isBlank()) {
return Optional.empty();
}
// Parse the step:field reference
StepFieldRef stepFieldRef = StepFieldRef.parse(stepFieldRefString);
if (stepFieldRef == null) {
logWarn(logger, "Invalid stepFieldRef format: {}", stepFieldRefString);
return Optional.empty();
}
logDebug(logger, "Extracting membership from step '{}' field '{}'",
stepFieldRef.stepRef(), stepFieldRef.fieldRef());
// Find the step instance
T stepInstance = stepInstanceFinder.apply(stepFieldRef.stepRef());
if (stepInstance == null) {
logWarn(logger, "No step instance found for reference: {}", stepFieldRef.stepRef());
return Optional.empty();
}
// Extract the jsonInput from the step instance
String jsonInput = jsonInputExtractor.apply(stepInstance);
if (jsonInput == null || jsonInput.isBlank()) {
logWarn(logger, "jsonInput is empty for step '{}'", stepFieldRef.stepRef());
return Optional.empty();
}
// Parse jsonInput and extract the field value
return extractFieldFromJson(jsonInput, stepFieldRef.fieldRef(), logger);
}
/**
* Extracts a field value from a JSON string.
*
* @param jsonString JSON string to parse
* @param fieldName Field name to extract
* @param logger Logger for reporting (nullable)
* @return Optional containing the field value, or empty if not found
*/
public static Optional<String> extractFieldFromJson(String jsonString, String fieldName, Logger logger) {
if (jsonString == null || jsonString.isBlank()) {
return Optional.empty();
}
try {
JsonNode rootNode = OBJECT_MAPPER.readTree(jsonString);
if (rootNode == null) {
return Optional.empty();
}
JsonNode fieldNode = rootNode.get(fieldName);
if (fieldNode == null || fieldNode.isNull()) {
logWarn(logger, "Field '{}' not found in JSON", fieldName);
return Optional.empty();
}
String value = fieldNode.asText();
if (value == null || value.isBlank()) {
logDebug(logger, "Field '{}' is blank", fieldName);
return Optional.empty();
}
return Optional.of(value.trim());
} catch (JsonProcessingException e) {
logWarn(logger, "Error parsing JSON to extract field '{}': {}", fieldName, e.getMessage());
return Optional.empty();
}
}
// ========================================================================
// Convenience Methods for Complete Flow
// ========================================================================
/**
* Complete flow: parses JSON and collects all user IDs.
* <p>
* This is a convenience method that combines JSON parsing and user ID collection
* in a single call.
* </p>
*
* @param <T> Type of the step instance object
* @param <M> Type of the membership list object
* @param jsonContent The JSON content string containing user configuration
* @param stepInstanceFinder Function that finds a step instance by reference
* @param userIdExtractor Function that extracts user ID from a step instance
* @param membershipFinder Function that finds membership objects by reference array
* @param identityAPI Bonita Identity API for user lookups
* @param logger Logger for reporting (nullable)
* @return Set of unique candidate user IDs (never null)
*/
public static <T, M> Set<Long> parseAndCollectUserIds(
String jsonContent,
Function<String, T> stepInstanceFinder,
Function<T, Long> userIdExtractor,
Function<String[], List<M>> membershipFinder,
IdentityAPI identityAPI,
Logger logger) {
UsersConfigRecord config = parseUsersConfig(jsonContent, logger);
return collectAllUserIds(config, stepInstanceFinder, userIdExtractor, membershipFinder,
identityAPI, logger);
}
/**
* Complete flow with dynamic membership extraction.
* <p>
* This version also handles the membersShipsInput field which requires
* extracting a membership reference from a step's input JSON.
* </p>
*
* @param <T> Type of the step instance object
* @param <M> Type of the membership list object
* @param jsonContent The JSON content string containing user configuration
* @param stepInstanceFinder Function that finds a step instance by reference
* @param userIdExtractor Function that extracts user ID from a step instance
* @param jsonInputExtractor Function that extracts jsonInput from a step instance
* @param membershipFinder Function that finds membership objects by reference array
* @param identityAPI Bonita Identity API for user lookups
* @param logger Logger for reporting (nullable)
* @return Set of unique candidate user IDs (never null)
*/
public static <T, M> Set<Long> parseAndCollectUserIdsWithDynamicMembership(
String jsonContent,
Function<String, T> stepInstanceFinder,
Function<T, Long> userIdExtractor,
Function<T, String> jsonInputExtractor,
Function<String[], List<M>> membershipFinder,
IdentityAPI identityAPI,
Logger logger) {
UsersConfigRecord config = parseUsersConfig(jsonContent, logger);
if (config == null || !config.hasAnySource()) {
return Collections.emptySet();
}
Set<Long> userIds = new HashSet<>();
// Process stepUser
processStepUser(config, stepInstanceFinder, userIdExtractor, logger)
.ifPresent(userIds::add);
// Process stepManager
processStepManager(config, stepInstanceFinder, userIdExtractor, identityAPI, logger)
.ifPresent(userIds::add);
// Process static memberShips
if (config.hasMemberShips()) {
userIds.addAll(processMemberships(config.memberShips(), membershipFinder, identityAPI, logger));
}
// Process dynamic membersShipsInput
if (config.hasMembersShipsInput()) {
extractMembershipFromStepInput(config.membersShipsInput(), stepInstanceFinder,
jsonInputExtractor, logger)
.ifPresent(membershipId -> {
logInfo(logger, "Extracted dynamic membership ID: {}", membershipId);
Set<Long> dynamicUsers = processSingleMembership(
membershipId, membershipFinder, identityAPI, logger);
userIds.addAll(dynamicUsers);
logInfo(logger, "Added {} users from dynamic membership", dynamicUsers.size());
});
}
logInfo(logger, "Total unique candidate user IDs: {}", userIds.size());
return userIds;
}
// ========================================================================
// Private Helper Methods
// ========================================================================
private static <T> Optional<Long> findStepInstanceAndExtractUserId(
String stepRef,
Function<String, T> stepInstanceFinder,
Function<T, Long> userIdExtractor,
Logger logger) {
if (stepRef == null || stepRef.isBlank()) {
return Optional.empty();
}
T stepInstance = stepInstanceFinder.apply(stepRef);
if (stepInstance == null) {
logWarn(logger, "No step instance found for reference: {}", stepRef);
return Optional.empty();
}
Long userId = userIdExtractor.apply(stepInstance);
if (userId == null || userId <= 0) {
logWarn(logger, "No valid user ID found in step instance for reference: {}", stepRef);
return Optional.empty();
}
return Optional.of(userId);
}
private static <T, M> void processDynamicMembership(
UsersConfigRecord config,
Function<String, T> stepInstanceFinder,
Function<T, Long> userIdExtractor,
Function<String[], List<M>> membershipFinder,
IdentityAPI identityAPI,
Logger logger,
Set<Long> userIds) {
if (!config.hasMembersShipsInput()) {
return;
}
Optional<StepFieldRef> stepFieldRefOpt = config.parseMembersShipsInput();
if (stepFieldRefOpt.isEmpty()) {
logWarn(logger, "Could not parse membersShipsInput: {}", config.membersShipsInput());
return;
}
StepFieldRef stepFieldRef = stepFieldRefOpt.get();
logDebug(logger, "Processing dynamic membership from step '{}' field '{}'",
stepFieldRef.stepRef(), stepFieldRef.fieldRef());
// Find the step instance
T stepInstance = stepInstanceFinder.apply(stepFieldRef.stepRef());
if (stepInstance == null) {
logWarn(logger, "No step instance found for dynamic membership reference: {}",
stepFieldRef.stepRef());
return;
}
// For dynamic membership, we need to extract jsonInput from the step instance
// This requires the step instance to have a getJsonInput() method
String jsonInput = extractJsonInputFromStepInstance(stepInstance, logger);
if (jsonInput == null || jsonInput.isBlank()) {
logWarn(logger, "No jsonInput found in step instance for dynamic membership");
return;
}
// Extract the membership ID from jsonInput
Optional<String> membershipIdOpt = extractFieldFromJson(jsonInput, stepFieldRef.fieldRef(), logger);
if (membershipIdOpt.isEmpty()) {
logWarn(logger, "Could not extract membership ID from field '{}'", stepFieldRef.fieldRef());
return;
}
String membershipId = membershipIdOpt.get();
logInfo(logger, "Extracted dynamic membership ID: {}", membershipId);
// Process the dynamic membership
Set<Long> dynamicUsers = processSingleMembership(membershipId, membershipFinder, identityAPI, logger);
userIds.addAll(dynamicUsers);
logInfo(logger, "Added {} users from dynamic membersShipsInput", dynamicUsers.size());
}
private static <T> String extractJsonInputFromStepInstance(T stepInstance, Logger logger) {
if (stepInstance == null) {
return null;
}
try {
java.lang.reflect.Method method = stepInstance.getClass().getMethod("getJsonInput");
Object result = method.invoke(stepInstance);
return result != null ? result.toString() : null;
} catch (NoSuchMethodException e) {
logDebug(logger, "Step instance does not have getJsonInput() method");
return null;
} catch (Exception e) {
logWarn(logger, "Error extracting jsonInput from step instance: {}", e.getMessage());
return null;
}
}
// ========================================================================
// Logging Helpers
// ========================================================================
private static void logDebug(Logger logger, String message, Object... args) {
if (logger != null && logger.isDebugEnabled()) {
logger.debug(message, args);
}
}
private static void logInfo(Logger logger, String message, Object... args) {
if (logger != null && logger.isInfoEnabled()) {
logger.info(message, args);
}
}
private static void logWarn(Logger logger, String message, Object... args) {
if (logger != null) {
logger.warn(message, args);
}
}
}