EmailRecipientsHelper.java
package com.bonitasoft.processbuilder.extension;
import com.bonitasoft.processbuilder.enums.ActionParameterType;
import com.fasterxml.jackson.databind.JsonNode;
import org.bonitasoft.engine.api.IdentityAPI;
import org.bonitasoft.engine.identity.ContactData;
import org.bonitasoft.engine.identity.User;
import org.bonitasoft.engine.identity.UserNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* Utility class for retrieving email addresses from Bonita Identity API.
* <p>
* This class provides thread-safe and stateless methods for:
* </p>
* <ul>
* <li>Retrieving email addresses by user ID or manager ID</li>
* <li>Extracting recipient information from JSON action parameters</li>
* <li>Processing user IDs from step execution results (DAO-independent)</li>
* <li>Processing membership-based user lookups (DAO-independent)</li>
* <li>Validating and filtering email addresses</li>
* </ul>
*
* <p><b>Design Philosophy:</b> All methods receive their dependencies as parameters,
* making them easily testable and independent of specific DAO implementations.
* Methods that process step or membership data accept generic types with extractor
* functions, allowing scripts to pass BDM objects without compile-time dependencies.</p>
*
* <p><b>Usage Example (Groovy Script):</b></p>
* <pre>{@code
* // Extract userId from step results
* def steps = pBStepProcessInstanceDAO.findLastByRefStepAndRootProcessInstanceId(
* rootProcessInstanceId, stepIdParam, 0, 1)
* Long userId = EmailRecipientsHelper.extractUserIdFromFirstStep(steps, { it.userId })
*
* // Get email for user
* Optional<String> email = EmailRecipientsHelper.getEmailByUserId(identityAPI, userId)
* }</pre>
*
* @author Bonitasoft
* @since 1.0
* @see ActionParameterType
* @see IdentityUtils
*/
public final class EmailRecipientsHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(EmailRecipientsHelper.class);
private EmailRecipientsHelper() {
// Utility class - prevent instantiation
}
// ═══════════════════════════════════════════════════════════════════
// EMAIL RETRIEVAL METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Retrieves email for a single user ID.
*
* @param identityAPI the Bonita Identity API
* @param userId the user ID (must be positive)
* @return Optional containing the email if found and valid
*/
public static Optional<String> getEmailByUserId(IdentityAPI identityAPI, Long userId) {
Objects.requireNonNull(identityAPI, "IdentityAPI cannot be null");
if (!isValidUserId(userId)) {
LOGGER.debug("Invalid userId provided: {}", userId);
return Optional.empty();
}
try {
ContactData contactData = identityAPI.getUserContactData(userId, false);
return extractEmail(contactData);
} catch (UserNotFoundException e) {
LOGGER.warn("User not found for userId: {}", userId);
} catch (Exception e) {
LOGGER.error("Error retrieving contact data for userId: {}", userId, e);
}
return Optional.empty();
}
/**
* Retrieves manager's email for a given user ID.
*
* @param identityAPI the Bonita Identity API
* @param userId the user ID whose manager email is requested
* @return Optional containing the manager's email if found
*/
public static Optional<String> getManagerEmailByUserId(IdentityAPI identityAPI, Long userId) {
Objects.requireNonNull(identityAPI, "IdentityAPI cannot be null");
if (!isValidUserId(userId)) {
LOGGER.debug("Invalid userId for manager lookup: {}", userId);
return Optional.empty();
}
try {
User user = identityAPI.getUser(userId);
long managerId = user.getManagerUserId();
if (managerId <= 0) {
LOGGER.debug("User {} has no manager assigned", userId);
return Optional.empty();
}
return getEmailByUserId(identityAPI, managerId);
} catch (UserNotFoundException e) {
LOGGER.warn("User not found for manager lookup, userId: {}", userId);
} catch (Exception e) {
LOGGER.error("Error retrieving manager for userId: {}", userId, e);
}
return Optional.empty();
}
/**
* Retrieves emails for multiple user IDs in batch.
*
* @param identityAPI the Bonita Identity API
* @param userIds collection of user IDs
* @return Set of unique, valid email addresses (preserves insertion order)
*/
public static Set<String> getEmailsByUserIds(IdentityAPI identityAPI, Collection<Long> userIds) {
Objects.requireNonNull(identityAPI, "IdentityAPI cannot be null");
if (userIds == null || userIds.isEmpty()) {
LOGGER.debug("Empty or null userIds collection provided");
return Collections.emptySet();
}
return userIds.stream()
.filter(EmailRecipientsHelper::isValidUserId)
.distinct()
.map(userId -> getEmailByUserId(identityAPI, userId))
.flatMap(Optional::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
// ═══════════════════════════════════════════════════════════════════
// JSON PARAMETER EXTRACTION METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Extracts user IDs from JSON parameters (RECIPIENTS_USER_IDS).
*
* @param parameters the JSON parameters node
* @return List of valid user IDs
*/
public static List<Long> extractUserIdsFromParameters(JsonNode parameters) {
String userIdsParam = ActionParameterType.RECIPIENTS_USER_IDS.getKey();
JsonNode userIdsNode = JsonNodeUtils.getValueByPath(parameters, userIdsParam);
if (userIdsNode == null || !userIdsNode.isArray()) {
LOGGER.debug("No valid userIds array found in parameters");
return Collections.emptyList();
}
List<Long> userIds = StreamSupport.stream(userIdsNode.spliterator(), false)
.map(JsonNode::asLong)
.filter(EmailRecipientsHelper::isValidUserId)
.collect(Collectors.toList());
LOGGER.info("Extracted {} valid userIds from parameters", userIds.size());
return userIds;
}
/**
* Extracts membership reference IDs from JSON parameters.
*
* @param parameters the JSON parameters node
* @return Array of membership reference strings
*/
public static String[] extractMembershipRefs(JsonNode parameters) {
String membershipParam = ActionParameterType.RECIPIENTS_MEMBERSHIP_IDS.getKey();
JsonNode membershipNode = JsonNodeUtils.getValueByPath(parameters, membershipParam);
if (membershipNode == null || !membershipNode.isArray()) {
LOGGER.debug("No valid membership array found in parameters");
return new String[0];
}
String[] refs = StreamSupport.stream(membershipNode.spliterator(), false)
.map(JsonNode::asText)
.filter(text -> text != null && !text.isBlank())
.toArray(String[]::new);
LOGGER.debug("Extracted {} membership references", refs.length);
return refs;
}
/**
* Extracts specific email addresses from JSON parameters.
*
* @param parameters the JSON parameters node
* @return Set of valid email addresses
*/
public static Set<String> extractSpecificEmails(JsonNode parameters) {
String specificParam = ActionParameterType.RECIPIENTS_SPECIFIC_EMAILS.getKey();
JsonNode specificNode = JsonNodeUtils.getValueByPath(parameters, specificParam);
if (specificNode == null || !specificNode.isArray()) {
LOGGER.debug("No valid specific emails array found");
return Collections.emptySet();
}
Set<String> emails = StreamSupport.stream(specificNode.spliterator(), false)
.map(JsonNode::asText)
.filter(EmailRecipientsHelper::isValidEmail)
.collect(Collectors.toCollection(LinkedHashSet::new));
LOGGER.info("Extracted {} specific emails", emails.size());
return emails;
}
/**
* Gets the step ID parameter key.
*
* @return the step ID parameter key
*/
public static String getStepIdParameterKey() {
return ActionParameterType.RECIPIENTS_STEP_ID.getKey();
}
// ═══════════════════════════════════════════════════════════════════
// PROCESSING METHODS (for use with extracted user IDs)
// ═══════════════════════════════════════════════════════════════════
/**
* Processes user IDs from parameters and returns their emails.
*
* @param identityAPI the Bonita Identity API
* @param parameters the JSON parameters node
* @return Set of email addresses
*/
public static Set<String> processUsersRecipients(IdentityAPI identityAPI, JsonNode parameters) {
List<Long> userIds = extractUserIdsFromParameters(parameters);
Set<String> emails = getEmailsByUserIds(identityAPI, userIds);
LOGGER.info("USERS - Processed {} emails from {} userIds", emails.size(), userIds.size());
return emails;
}
/**
* Processes a single user ID and adds email (direct or manager) to the set.
*
* @param emails the set to add the email to
* @param identityAPI the Bonita Identity API
* @param userId the user ID
* @param fetchManager if true, fetch manager's email; otherwise fetch user's email
*/
public static void addEmailForUser(
Set<String> emails,
IdentityAPI identityAPI,
Long userId,
boolean fetchManager) {
if (!isValidUserId(userId)) {
LOGGER.debug("Invalid userId, skipping email lookup");
return;
}
Optional<String> emailOpt = fetchManager
? getManagerEmailByUserId(identityAPI, userId)
: getEmailByUserId(identityAPI, userId);
emailOpt.ifPresent(email -> {
emails.add(email);
LOGGER.debug("Added {} email: {}", fetchManager ? "manager" : "user", email);
});
}
// ═══════════════════════════════════════════════════════════════════
// UTILITY METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Filters and validates a collection of email strings.
*
* @param emails collection of email strings (may contain nulls/blanks)
* @return Set of valid, non-blank email addresses
*/
public static Set<String> filterValidEmails(Collection<String> emails) {
if (emails == null || emails.isEmpty()) {
return Collections.emptySet();
}
return emails.stream()
.filter(EmailRecipientsHelper::isValidEmail)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Joins email addresses into a comma-separated string.
*
* @param emails collection of emails
* @return comma-separated string of unique emails
*/
public static String joinEmails(Collection<String> emails) {
if (emails == null || emails.isEmpty()) {
return "";
}
return emails.stream()
.filter(EmailRecipientsHelper::isValidEmail)
.distinct()
.collect(Collectors.joining(", "));
}
/**
* Validates if a userId is valid (non-null and positive).
*
* @param userId the user ID to validate
* @return true if valid, false otherwise
*/
public static boolean isValidUserId(Long userId) {
return userId != null && userId > 0;
}
/**
* Validates if an email string is valid (non-null and non-blank).
*
* @param email the email to validate
* @return true if valid, false otherwise
*/
public static boolean isValidEmail(String email) {
return email != null && !email.isBlank();
}
// ═══════════════════════════════════════════════════════════════════
// DAO-INDEPENDENT EXTRACTION METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Extracts the user ID from the first element of a step results list.
* <p>
* This method is designed to work with DAO query results without requiring
* a compile-time dependency on BDM types. It accepts a generic list and a
* function to extract the user ID from each element.
* </p>
*
* <p><b>Usage Example (Groovy Script):</b></p>
* <pre>{@code
* // Query the DAO for step instances
* def steps = pBStepProcessInstanceDAO.findLastByRefStepAndRootProcessInstanceId(
* rootProcessInstanceId,
* EmailRecipientsHelper.getStepIdParameterKey(),
* 0, 1
* )
*
* // Extract userId using a closure as the extractor function
* Long userId = EmailRecipientsHelper.extractUserIdFromFirstStep(steps, { step -> step.userId })
*
* // Alternative syntax with property access
* Long userId = EmailRecipientsHelper.extractUserIdFromFirstStep(steps, { it.userId })
* }</pre>
*
* @param <T> the type of elements in the list (e.g., PBStepProcessInstance)
* @param steps the list of step process instances from a DAO query (may be null or empty)
* @param userIdExtractor a function that extracts the user ID from a step object
* @return the user ID from the first step, or {@code null} if the list is null/empty
* or the extractor returns null
*/
public static <T> Long extractUserIdFromFirstStep(List<T> steps, Function<T, Long> userIdExtractor) {
if (steps == null || steps.isEmpty()) {
LOGGER.debug("No step instances provided for userId extraction");
return null;
}
Objects.requireNonNull(userIdExtractor, "userIdExtractor function cannot be null");
T firstStep = steps.get(0);
Long userId = userIdExtractor.apply(firstStep);
LOGGER.debug("Extracted userId from first step: {}", userId);
return userId;
}
/**
* Extracts user IDs from a collection of membership-based user list objects.
* <p>
* This method processes membership query results and extracts unique user IDs
* using the provided extractor function. It's designed to work with BDM objects
* like PBUserList without requiring compile-time dependencies.
* </p>
*
* <p><b>Usage Example (Groovy Script):</b></p>
* <pre>{@code
* // Get membership references from action parameters
* String[] refMemberships = EmailRecipientsHelper.extractMembershipRefs(pbActionContent.parameters)
*
* // Query user lists from DAO
* def userLists = pBUserListDAO.findByProcessIdAndRefMemberships(
* processId, refMemberships, 0, Integer.MAX_VALUE
* )
*
* // Extract user IDs using a closure
* Set<Long> userIds = EmailRecipientsHelper.extractUserIdsFromMembershipResults(
* userLists,
* { userList -> userList.userId }
* )
*
* // Get emails for all extracted user IDs
* Set<String> emails = EmailRecipientsHelper.getEmailsByUserIds(identityAPI, userIds)
* }</pre>
*
* @param <T> the type of elements in the collection (e.g., PBUserList)
* @param userLists the collection of user list objects from a DAO query (may be null or empty)
* @param userIdExtractor a function that extracts the user ID from each user list object
* @return a set of unique, valid user IDs extracted from the user lists;
* returns an empty set if the input is null/empty
*/
public static <T> Set<Long> extractUserIdsFromMembershipResults(
Collection<T> userLists,
Function<T, Long> userIdExtractor) {
if (userLists == null || userLists.isEmpty()) {
LOGGER.debug("No user lists provided for membership userId extraction");
return Collections.emptySet();
}
Objects.requireNonNull(userIdExtractor, "userIdExtractor function cannot be null");
Set<Long> userIds = userLists.stream()
.map(userIdExtractor)
.filter(EmailRecipientsHelper::isValidUserId)
.collect(Collectors.toCollection(LinkedHashSet::new));
LOGGER.debug("Extracted {} unique userIds from {} membership entries", userIds.size(), userLists.size());
return userIds;
}
/**
* Processes step-based recipients and retrieves their email addresses.
* <p>
* This is a convenience method that combines step user ID extraction with email retrieval.
* It supports both direct user emails and manager emails based on the {@code fetchManager} flag.
* </p>
*
* <p><b>Usage Example (Groovy Script):</b></p>
* <pre>{@code
* // For STEP_USERS recipients type
* def steps = pBStepProcessInstanceDAO.findLastByRefStepAndRootProcessInstanceId(
* rootProcessInstanceId, stepIdParam, 0, 1
* )
* Set<String> emails = EmailRecipientsHelper.processStepBasedRecipients(
* identityAPI, steps, { it.userId }, false // false = user email
* )
*
* // For STEP_MANAGERS recipients type
* Set<String> managerEmails = EmailRecipientsHelper.processStepBasedRecipients(
* identityAPI, steps, { it.userId }, true // true = manager email
* )
* }</pre>
*
* @param <T> the type of elements in the steps list
* @param identityAPI the Bonita Identity API for email lookup
* @param steps the list of step process instances from a DAO query
* @param userIdExtractor a function that extracts the user ID from a step object
* @param fetchManager if {@code true}, retrieves the manager's email; otherwise retrieves the user's email
* @return a set containing the email address (empty if not found or invalid)
*/
public static <T> Set<String> processStepBasedRecipients(
IdentityAPI identityAPI,
List<T> steps,
Function<T, Long> userIdExtractor,
boolean fetchManager) {
Objects.requireNonNull(identityAPI, "IdentityAPI cannot be null");
Long userId = extractUserIdFromFirstStep(steps, userIdExtractor);
if (!isValidUserId(userId)) {
LOGGER.debug("No valid userId extracted from steps for {} lookup",
fetchManager ? "manager" : "user");
return Collections.emptySet();
}
Set<String> emails = new LinkedHashSet<>();
addEmailForUser(emails, identityAPI, userId, fetchManager);
LOGGER.info("{} - Processed email for userId: {}, found: {}",
fetchManager ? "STEP_MANAGERS" : "STEP_USERS",
userId,
!emails.isEmpty());
return emails;
}
/**
* Processes membership-based recipients and retrieves their email addresses.
* <p>
* This is a convenience method that combines membership user ID extraction with
* batch email retrieval. It processes all user IDs from the membership results
* and returns their corresponding email addresses.
* </p>
*
* <p><b>Usage Example (Groovy Script):</b></p>
* <pre>{@code
* // Get membership references from parameters
* String[] refMemberships = EmailRecipientsHelper.extractMembershipRefs(pbActionContent.parameters)
*
* // Query user lists
* def userLists = pBUserListDAO.findByProcessIdAndRefMemberships(
* processId, refMemberships, 0, Integer.MAX_VALUE
* )
*
* // Process and get all emails in one call
* Set<String> emails = EmailRecipientsHelper.processMembershipBasedRecipients(
* identityAPI, userLists, { it.userId }
* )
* }</pre>
*
* @param <T> the type of elements in the user lists collection
* @param identityAPI the Bonita Identity API for email lookup
* @param userLists the collection of user list objects from a DAO query
* @param userIdExtractor a function that extracts the user ID from each user list object
* @return a set of email addresses for all valid users in the membership results
*/
public static <T> Set<String> processMembershipBasedRecipients(
IdentityAPI identityAPI,
Collection<T> userLists,
Function<T, Long> userIdExtractor) {
Objects.requireNonNull(identityAPI, "IdentityAPI cannot be null");
Set<Long> userIds = extractUserIdsFromMembershipResults(userLists, userIdExtractor);
if (userIds.isEmpty()) {
LOGGER.info("MEMBERSHIP - No valid userIds extracted from membership results");
return Collections.emptySet();
}
Set<String> emails = getEmailsByUserIds(identityAPI, userIds);
LOGGER.info("MEMBERSHIP - Processed {} emails from {} userIds", emails.size(), userIds.size());
return emails;
}
// ═══════════════════════════════════════════════════════════════════
// PRIVATE HELPER METHODS
// ═══════════════════════════════════════════════════════════════════
private static Optional<String> extractEmail(ContactData contactData) {
if (contactData == null) {
return Optional.empty();
}
String email = contactData.getEmail();
return isValidEmail(email) ? Optional.of(email) : Optional.empty();
}
}