PBStringUtils.java
package com.bonitasoft.processbuilder.extension;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class providing common String manipulation methods, focusing on
* normalization and case formatting.
* <p>
* This class is designed to be non-instantiable and should only be accessed
* via static methods.
* </p>
*
* @author Bonitasoft
* @since 1.0
*/
public final class PBStringUtils {
/**
* A logger for this class, used to record log messages and provide debugging information.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(PBStringUtils.class);
/**
* Regex pattern to capture variables in formats:
* - {{refStep:dataName}} - with prefix (refStep in group 1, dataName in group 2)
* - {{dataName}} - without prefix (group 1 is null, dataName in group 2)
*/
private static final Pattern VARIABLE_PATTERN;
private static final String DEFAULT_REPLACEMENT = "VAR_NOT_RESOLVED";
// Static block for pattern compilation
static {
Pattern p;
try {
// Pattern explanation:
// \\{\\{ - matches opening {{
// (?:([^:}]+):)? - optional non-capturing group for prefix:
// ([^:}]+) - captures refStep (any char except : and })
// : - matches the colon separator
// ([^}]+) - captures dataName (any char except })
// \\}\\} - matches closing }}
p = Pattern.compile("\\{\\{(?:([^:}]+):)?([^}]+)\\}\\}");
} catch (PatternSyntaxException e) {
LOGGER.error("Fatal Error: Could not compile template variable regex.", e);
p = null;
}
VARIABLE_PATTERN = p;
}
/**
* Private constructor to prevent instantiation of this utility class.
*
* @throws UnsupportedOperationException always, to enforce the utility pattern.
*/
private PBStringUtils() {
throw new UnsupportedOperationException("This is a " + this.getClass().getSimpleName() + " class and cannot be instantiated.");
}
// ----------------------------------------------------------------------
// Case Normalization Methods
// ----------------------------------------------------------------------
/**
* Normalizes the input String to Title Case format: the first letter is
* capitalized, and all subsequent letters are lowercased.
* <p>
* This implementation is optimized to minimize intermediate String object creation.
* </p>
* <ul>
* <li>{@code "CATEGORY"} becomes {@code "Category"}</li>
* <li>{@code "category"} becomes {@code "Category"}</li>
* <li>{@code "CaTegory"} becomes {@code "Category"}</li>
* <li>{@code null} remains {@code null}</li>
* <li>{@code ""} remains {@code ""}</li>
* <li>{@code "a"} becomes {@code "A"}</li>
* </ul>
* @param str The string to normalize.
* @return The string in Title Case, or the original string if null or empty.
*/
public static String normalizeTitleCase(String str) {
// Handle null or empty cases first (fastest check)
if (str == null || str.isEmpty()) {
return str;
}
// Handle single character case separately for clarity, though it works below
if (str.length() == 1) {
return str.toUpperCase();
}
// 1. Get the first character, convert to uppercase.
String firstChar = String.valueOf(str.charAt(0)).toUpperCase();
// 2. Get the rest of the string and convert it to lowercase.
String rest = str.substring(1).toLowerCase();
// 3. Return the combined string.
return firstChar + rest;
}
/**
* Converts a string from human-readable format (e.g., spaces) to
* {@code snake_case} format.
*
* The conversion process involves:
* <ul>
* <li>Converting the entire string to lowercase.</li>
* <li>Replacing all space characters (' ') with underscores ('_').</li>
* </ul>
*
* <ul>
* <li>{@code "Bonita and delete"} becomes {@code "bonita_and_delete"}</li>
* <li>{@code "A Long Name"} becomes {@code "a_long_name"}</li>
* <li>{@code null} remains {@code null}</li>
* </ul>
*
* @param input The string to convert.
* @return The string in snake_case format, or the original string if null.
*/
public static String toLowerSnakeCase(String input) {
if (input == null) {
return null;
}
// 1. Convert to lowercase.
String lowerCase = input.toLowerCase();
// 2. Replace all spaces with underscores.
String snakeCase = lowerCase.replace(' ', '_');
return snakeCase;
}
/**
* Converts a string from human-readable format (e.g., spaces) to
* {@code snake_case} format.
*
* The conversion process involves:
* <ul>
* <li>Converting the entire string to uppercase.</li>
* <li>Replacing all space characters (' ') with underscores ('_').</li>
* </ul>
*
* <ul>
* <li>{@code "Bonita and delete"} becomes {@code "BONITA_AND_DELETE"}</li>
* <li>{@code "A Long Name"} becomes {@code "A_LONG_NAME"}</li>
* <li>{@code null} remains {@code null}</li>
* </ul>
*
* @param input The string to convert.
* @return The string in snake_case format, or the original string if null.
*/
public static String toUpperSnakeCase(String input) {
if (input == null) {
return null;
}
// 1. Convert to uppercase.
String upperCase = input.toUpperCase();
// 2. Replace all spaces with underscores.
String snakeCase = upperCase.replace(' ', '_');
return snakeCase;
}
/**
* Resolves and replaces all variables in the format {@code {{refStep:dataName}}} within a template string.
* The replacement value is retrieved via a functional interface provided by the caller.
*
* @param template The string containing the variables to be resolved.
* @param dataValueResolver A function that takes (refStep, dataName) and returns the corresponding data value as a String.
* This function encapsulates the DAO lookup logic (e.g., PBDataProcessInstanceDAO.findByStepRefAndDataName).
* @return The template with all variables resolved, or the original template if it's null/empty.
*/
public static String resolveTemplateVariables(String template, BiFunction<String, String, String> dataValueResolver) {
if (template == null || template.isEmpty() || VARIABLE_PATTERN == null) {
return template;
}
if (dataValueResolver == null) {
LOGGER.error("Data value resolver function is null. Cannot resolve template variables.");
return template;
}
final Matcher matcher = VARIABLE_PATTERN.matcher(template);
final StringBuffer result = new StringBuffer();
while (matcher.find()) {
final String refStep = matcher.group(1); // Reference Step ID
final String dataName = matcher.group(2); // Data Name/Field
String replacementValue = DEFAULT_REPLACEMENT;
try {
// Call the functional interface to get the data (decoupled from the DAO implementation)
LOGGER.debug("DataValueResolver applay {}:{} ", refStep, dataName);
String retrievedValue = dataValueResolver.apply(refStep, dataName);
LOGGER.debug("Resolved variable {}:{} to value: {}", refStep, dataName, retrievedValue != null ? retrievedValue : "null");
if (retrievedValue != null) {
replacementValue = retrievedValue;
LOGGER.debug("Resolved variable {{}:{}} to value: {}", refStep, dataName, replacementValue);
} else {
LOGGER.warn("Variable not resolved: {{}:{}} - Resolver returned null.", refStep, dataName);
}
} catch (Exception e) {
LOGGER.error("Error executing resolver for variable {{}:{}}.", refStep, dataName, e);
}
// Append replacement and advance matcher position
matcher.appendReplacement(result, Matcher.quoteReplacement(replacementValue));
}
matcher.appendTail(result);
return result.toString();
}
}