InvolvedUsersParser.java
package com.bonitasoft.processbuilder.extension;
import com.bonitasoft.processbuilder.records.InvolvedUsersData;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Optional;
import java.util.stream.StreamSupport;
/**
* Utility class responsible for parsing JSON configuration related to
* involved users, actors, and assignment logic within a Bonita process extension.
* This class provides a static method to extract, validate, and structure data
* from a JSON string into an immutable {@code InvolvedUsersData} record in a single pass.
*/
public class InvolvedUsersParser {
/**
* Helper method to extract a required text field from a JsonNode.
* @param node The parent JsonNode to search within.
* @param fieldName The name of the required field.
* @return The String value of the field.
* @throws IllegalArgumentException if the field is missing, null, or not a valid text value.
*/
public static String extractRequiredTextField(JsonNode node, String fieldName) {
return Optional.ofNullable(node.get(fieldName))
.filter(n -> !n.isNull()) // Ensure it's not JSON 'null'
.filter(JsonNode::isTextual) // Ensure it is a String
.map(JsonNode::asText)
// Validate content: must not be empty or whitespace (new check added here)
.filter(s -> !s.trim().isEmpty())
.orElseThrow(() -> new IllegalArgumentException(
"Required field '" + fieldName + "' is missing, null, empty, or not a valid text value in the JSON configuration."
));
}
/**
* Helper method to extract a field that MUST BE PRESENT, but can have a null or empty value.
*
* @param node The parent JsonNode to search within.
* @param fieldName The name of the required, but nullable/empty, field.
* @return The String value of the field, or null if the value is JSON null, or an empty string if the content is empty.
* @throws IllegalArgumentException if the field is completely missing from the JSON object.
*/
public static String extractNullableTextField(JsonNode node, String fieldName) {
JsonNode fieldNode = Optional.ofNullable(node.get(fieldName))
.orElseThrow(() -> new IllegalArgumentException(
"Required field '" + fieldName + "' is MISSING from the JSON configuration, even though its content can be null or empty."
));
if (fieldNode.isNull()) {
return null;
}
if (fieldNode.isTextual()) {
return fieldNode.asText();
}
throw new IllegalArgumentException(
"Required field '" + fieldName + "' is present but is not a valid text value (found type: " + fieldNode.getNodeType() + ").");
}
/**
* Parses the 'involvedUsers' JSON string, expecting the string to contain actor configuration
* (stepManager, stepUser, memberShips).
*
* This method combines validation and data extraction into a single, optimized process.
*
* @param jsonString The JSON string to parse. This string is expected to be the object
* containing required fields like 'stepManager', 'stepUser', and 'memberShips'.
* @return An {@code InvolvedUsersData} object containing the parsed actor references.
* @throws IllegalArgumentException if the JSON string is null, empty, or cannot be parsed,
* OR if any required field is missing or invalid (including empty strings for text fields).
*/
public static InvolvedUsersData parseInvolvedUsersJson(final String jsonString) {
if (jsonString == null || jsonString.isEmpty()) {
throw new IllegalArgumentException("Input JSON string cannot be null or empty.");
}
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode involvedUsersNode = objectMapper.readTree(jsonString);
// 1. Extract REQUIRED stepManagerRef (Checks for null, non-textual, and empty/whitespace)
String stepManagerRef = extractNullableTextField(involvedUsersNode, "stepManager");
// 2. Extract REQUIRED stepUserRef (Checks for null, non-textual, and empty/whitespace)
String stepUserRef = extractNullableTextField(involvedUsersNode, "stepUser");
// 3. Extract REQUIRED Memberships List (Checks array structure and content validity)
List<String> memberships = Optional.ofNullable(involvedUsersNode.get("memberShips"))
.filter(JsonNode::isArray)
.map(jsonArray ->
StreamSupport.stream(jsonArray.spliterator(), false)
// Validate each element: must be textual (string)
.filter(JsonNode::isTextual)
.map(JsonNode::asText)
// Validate each element content: must not be empty/whitespace
.filter(s -> !s.trim().isEmpty())
.toList() // Java 16+ equivalent of collect(Collectors.toList())
)
// If 'memberShips' is missing or not an array, throw an error
.orElseThrow(() -> new IllegalArgumentException(
"Required field 'memberShips' is missing or not a valid array in the JSON configuration."
));
// Return the Record (Assignment and validation completed in one pass)
return new InvolvedUsersData(stepManagerRef, stepUserRef, memberships);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse JSON string. Invalid JSON format.", e);
} catch (IllegalArgumentException e) {
// Re-throw specific errors (like missing/invalid fields) caught from helper methods
throw e;
} catch (Exception e) {
// Catch unexpected runtime errors and wrap them
throw new IllegalArgumentException("An unexpected error occurred during JSON processing: " + e.getMessage(), e);
}
}
}