TemplateDataResolver.java
package com.bonitasoft.processbuilder.extension;
import com.bonitasoft.processbuilder.enums.DataResolverType;
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.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Utility class for resolving template variables in notification messages.
* <p>
* This class provides methods to resolve standard template variables like recipient information
* (firstname, lastname, email) using the Bonita Identity API. It is designed to work with
* {@link PBStringUtils#resolveTemplateVariables(String, BiFunction)} method.
* </p>
*
* <p>The resolver supports two variable formats:</p>
* <ul>
* <li>{@code {{dataName}}} - Simple variable without prefix (refStep will be null)</li>
* <li>{@code {{refStep:dataName}}} - Variable with step reference prefix</li>
* </ul>
*
* <p><b>Usage Example (Groovy Script):</b></p>
* <pre>{@code
* // Create the base resolver for standard variables
* BiFunction<String, String, String> resolver = TemplateDataResolver.createResolver(
* identityAPI,
* recipientUserId,
* hostUrl,
* humanTaskId,
* // Custom resolver for BDM-specific data
* { refStep, dataName ->
* // Your custom BDM lookup logic here
* return myCustomValue
* }
* );
*
* // Resolve template variables
* String result = PBStringUtils.resolveTemplateVariables(template, resolver);
* }</pre>
*
* @author Bonitasoft
* @since 1.0
* @see PBStringUtils#resolveTemplateVariables(String, BiFunction)
* @see DataResolverType
*/
public final class TemplateDataResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(TemplateDataResolver.class);
private TemplateDataResolver() {
throw new UnsupportedOperationException("This is a " + this.getClass().getSimpleName() + " class and cannot be instantiated.");
}
// ═══════════════════════════════════════════════════════════════════
// USER INFORMATION METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Gets the first name of a user.
*
* @param identityAPI the Bonita Identity API
* @param userId the user ID
* @return Optional containing the first name if found
*/
public static Optional<String> getUserFirstName(IdentityAPI identityAPI, Long userId) {
return getUser(identityAPI, userId).map(User::getFirstName);
}
/**
* Gets the last name of a user.
*
* @param identityAPI the Bonita Identity API
* @param userId the user ID
* @return Optional containing the last name if found
*/
public static Optional<String> getUserLastName(IdentityAPI identityAPI, Long userId) {
return getUser(identityAPI, userId).map(User::getLastName);
}
/**
* Gets the email of a user.
*
* @param identityAPI the Bonita Identity API
* @param userId the user ID
* @return Optional containing the email if found
*/
public static Optional<String> getUserEmail(IdentityAPI identityAPI, Long userId) {
if (identityAPI == null || !isValidUserId(userId)) {
return Optional.empty();
}
try {
ContactData contactData = identityAPI.getUserContactData(userId, false);
if (contactData != null && contactData.getEmail() != null && !contactData.getEmail().isBlank()) {
return Optional.of(contactData.getEmail());
}
} catch (UserNotFoundException e) {
LOGGER.warn("User not found for email lookup: userId={}", userId);
} catch (Exception e) {
LOGGER.error("Error retrieving user email for userId={}: {}", userId, e.getMessage(), e);
}
return Optional.empty();
}
/**
* Gets the full name (first + last) of a user.
*
* @param identityAPI the Bonita Identity API
* @param userId the user ID
* @return Optional containing the full name if found
*/
public static Optional<String> getUserFullName(IdentityAPI identityAPI, Long userId) {
return getUser(identityAPI, userId).map(user -> {
String firstName = user.getFirstName();
String lastName = user.getLastName();
if (firstName != null && lastName != null) {
return firstName + " " + lastName;
} else if (firstName != null) {
return firstName;
} else if (lastName != null) {
return lastName;
}
return user.getUserName();
});
}
// ═══════════════════════════════════════════════════════════════════
// LINK GENERATION METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Generates a task link HTML anchor tag.
*
* @param hostUrl the base host URL (e.g., "https://bonita.example.com")
* @param taskId the human task ID
* @return the HTML link string
*/
public static String generateTaskLink(String hostUrl, Long taskId) {
if (hostUrl == null || hostUrl.isBlank()) {
LOGGER.warn("Host URL is null or blank for task link generation");
return "#" + taskId;
}
if (taskId == null || taskId <= 0) {
LOGGER.warn("Invalid taskId for link generation: {}", taskId);
return "#invalid-task";
}
String cleanHost = hostUrl.endsWith("/") ? hostUrl.substring(0, hostUrl.length() - 1) : hostUrl;
String url = cleanHost + "/app/process-builder?taskId=" + taskId;
return "<a href=\"" + url + "\">#" + taskId + "</a>";
}
/**
* Generates a plain task URL (without HTML anchor).
*
* @param hostUrl the base host URL
* @param taskId the human task ID
* @return the URL string
*/
public static String generateTaskUrl(String hostUrl, Long taskId) {
if (hostUrl == null || hostUrl.isBlank() || taskId == null || taskId <= 0) {
return null;
}
String cleanHost = hostUrl.endsWith("/") ? hostUrl.substring(0, hostUrl.length() - 1) : hostUrl;
return cleanHost + "/app/process-builder?taskId=" + taskId;
}
// ═══════════════════════════════════════════════════════════════════
// RESOLVER FACTORY METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Creates a complete BiFunction resolver for template variables.
* <p>
* This method creates a resolver that handles standard variables
* ({@link DataResolverType}) and delegates unknown variables to a custom fallback resolver.
* </p>
*
* @param identityAPI the Bonita Identity API for user lookups
* @param recipientUserId the user ID of the recipient (for recipient_* variables)
* @param hostUrl the base host URL (for task_link variable)
* @param humanTaskId the human task ID (for task_link variable)
* @param customResolver optional custom resolver for BDM-specific or step-based variables.
* Called when standard variables don't match. May be null.
* @return BiFunction resolver for use with {@link PBStringUtils#resolveTemplateVariables}
*/
public static BiFunction<String, String, String> createResolver(
IdentityAPI identityAPI,
Long recipientUserId,
String hostUrl,
Long humanTaskId,
BiFunction<String, String, String> customResolver) {
Objects.requireNonNull(identityAPI, "IdentityAPI cannot be null");
return (refStep, dataName) -> {
LOGGER.debug("Resolving variable: refStep={}, dataName={}", refStep, dataName);
// Handle standard recipient variables (no refStep required)
if (dataName != null) {
String standardResult = resolveStandardVariable(
identityAPI, recipientUserId, hostUrl, humanTaskId, dataName);
if (standardResult != null) {
LOGGER.debug("Resolved standard variable {}={}", dataName, standardResult);
return standardResult;
}
}
// Delegate to custom resolver for BDM-specific or step-based lookups
if (customResolver != null) {
String customResult = customResolver.apply(refStep, dataName);
if (customResult != null) {
LOGGER.debug("Resolved via custom resolver: {}:{}={}", refStep, dataName, customResult);
return customResult;
}
}
LOGGER.warn("Variable not resolved: refStep={}, dataName={}", refStep, dataName);
return null;
};
}
/**
* Creates a simple resolver for recipient-only variables.
* <p>
* Use this when you only need to resolve recipient_firstname, recipient_lastname,
* and recipient_email variables without any custom BDM lookups.
* </p>
*
* @param identityAPI the Bonita Identity API
* @param recipientUserId the user ID of the recipient
* @return BiFunction resolver for recipient variables only
*/
public static BiFunction<String, String, String> createRecipientResolver(
IdentityAPI identityAPI,
Long recipientUserId) {
return createResolver(identityAPI, recipientUserId, null, null, null);
}
/**
* Creates a resolver with task link support.
*
* @param identityAPI the Bonita Identity API
* @param recipientUserId the user ID of the recipient
* @param hostUrl the base host URL
* @param humanTaskId the human task ID
* @return BiFunction resolver for recipient and task link variables
*/
public static BiFunction<String, String, String> createResolverWithTaskLink(
IdentityAPI identityAPI,
Long recipientUserId,
String hostUrl,
Long humanTaskId) {
return createResolver(identityAPI, recipientUserId, hostUrl, humanTaskId, null);
}
// ═══════════════════════════════════════════════════════════════════
// STEP-BASED DATA HELPER
// ═══════════════════════════════════════════════════════════════════
/**
* Creates a step-based data extractor function.
* <p>
* This helper creates a function that can be used as part of the custom resolver
* to handle step_user_name and step_status variables from BDM step data.
* </p>
*
* <p><b>Usage Example (Groovy Script):</b></p>
* <pre>{@code
* // Define step data lookup
* Function<String, Object> stepLookup = { refStep ->
* def steps = pBStepProcessInstanceDAO.findLastByRefStepAndRootProcessInstanceId(
* rootProcessInstanceId, refStep, 0, 1)
* return steps?.isEmpty() ? null : steps.get(0)
* }
*
* // Create step data resolver
* BiFunction<String, String, String> stepResolver = TemplateDataResolver.createStepDataResolver(
* stepLookup,
* { step -> step.getUsername() },
* { step -> step.getStepStatus() }
* );
* }</pre>
*
* @param <T> the type of step object returned by the lookup
* @param stepLookup function that takes refStep and returns the step object (or null)
* @param usernameExtractor function to extract username from step object
* @param statusExtractor function to extract status from step object
* @return BiFunction that resolves step_user_name and step_status variables
*/
public static <T> BiFunction<String, String, String> createStepDataResolver(
Function<String, T> stepLookup,
Function<T, String> usernameExtractor,
Function<T, String> statusExtractor) {
Objects.requireNonNull(stepLookup, "stepLookup cannot be null");
Objects.requireNonNull(usernameExtractor, "usernameExtractor cannot be null");
Objects.requireNonNull(statusExtractor, "statusExtractor cannot be null");
return (refStep, dataName) -> {
if (refStep == null) {
LOGGER.debug("No refStep provided for step data lookup");
return null;
}
if (!DataResolverType.STEP_USER_NAME.getKey().equals(dataName) &&
!DataResolverType.STEP_STATUS.getKey().equals(dataName)) {
return null;
}
T stepData = stepLookup.apply(refStep);
if (stepData == null) {
LOGGER.warn("No step data found for refStep={}", refStep);
return null;
}
if (DataResolverType.STEP_USER_NAME.getKey().equals(dataName)) {
return usernameExtractor.apply(stepData);
} else {
return statusExtractor.apply(stepData);
}
};
}
// ═══════════════════════════════════════════════════════════════════
// PRIVATE HELPER METHODS
// ═══════════════════════════════════════════════════════════════════
private static Optional<User> getUser(IdentityAPI identityAPI, Long userId) {
if (identityAPI == null || !isValidUserId(userId)) {
return Optional.empty();
}
try {
return Optional.of(identityAPI.getUser(userId));
} catch (UserNotFoundException e) {
LOGGER.warn("User not found: userId={}", userId);
} catch (Exception e) {
LOGGER.error("Error retrieving user userId={}: {}", userId, e.getMessage(), e);
}
return Optional.empty();
}
private static boolean isValidUserId(Long userId) {
return userId != null && userId > 0;
}
private static String resolveStandardVariable(
IdentityAPI identityAPI,
Long recipientUserId,
String hostUrl,
Long humanTaskId,
String dataName) {
// Check if it's a known DataResolverType
DataResolverType type = DataResolverType.fromKey(dataName);
if (type == null) {
return null;
}
return switch (type) {
case RECIPIENT_FIRSTNAME -> getUserFirstName(identityAPI, recipientUserId).orElse(null);
case RECIPIENT_LASTNAME -> getUserLastName(identityAPI, recipientUserId).orElse(null);
case RECIPIENT_EMAIL -> getUserEmail(identityAPI, recipientUserId).orElse(null);
case TASK_LINK -> generateTaskLink(hostUrl, humanTaskId);
// STEP_USER_NAME and STEP_STATUS require BDM lookup, handled by custom resolver
case STEP_USER_NAME, STEP_STATUS -> null;
};
}
}