aspose template, trackVars bugfix
This commit is contained in:
1 parent
f5f495567c
commit
aaef7116f8
6 files changed
+111
-171
No files matched your search
@@ -1,42 +0,0 @@
|
|||||||
package com.customer.work.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import org.flowable.eventregistry.api.EventRegistry;
|
|
||||||
import org.flowable.eventregistry.api.model.EventPayloadTypes;
|
|
||||||
import org.flowable.eventregistry.api.runtime.EventPayloadInstance;
|
|
||||||
import org.flowable.eventregistry.impl.runtime.EventInstanceImpl;
|
|
||||||
import org.flowable.eventregistry.impl.runtime.EventPayloadInstanceImpl;
|
|
||||||
import org.flowable.eventregistry.model.EventPayload;
|
|
||||||
import org.flowable.task.service.delegate.DelegateTask;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class TaskListenerService {
|
|
||||||
private final EventRegistry eventRegistry;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public TaskListenerService(EventRegistry eventRegistry, ObjectMapper objectMapper) {
|
|
||||||
this.eventRegistry = eventRegistry;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void throwTaskListenerEvent(String rootCaseId, String eventKey, DelegateTask task, String message) {
|
|
||||||
ObjectNode jsonTask = objectMapper.createObjectNode();
|
|
||||||
jsonTask.put("modelId", task.getTaskDefinitionKey());
|
|
||||||
jsonTask.put("assignee", task.getAssignee());
|
|
||||||
jsonTask.put("name", task.getName());
|
|
||||||
|
|
||||||
Collection<EventPayloadInstance> instances = new ArrayList<>();
|
|
||||||
instances.add(new EventPayloadInstanceImpl(EventPayload.correlation("caseId", EventPayloadTypes.STRING), rootCaseId));
|
|
||||||
instances.add(new EventPayloadInstanceImpl(new EventPayload("taskListenerTask", EventPayloadTypes.JSON), jsonTask));
|
|
||||||
instances.add(new EventPayloadInstanceImpl(new EventPayload("taskListenerMessage", EventPayloadTypes.STRING), message));
|
|
||||||
|
|
||||||
EventInstanceImpl event = new EventInstanceImpl(eventKey, instances, task.getTenantId());
|
|
||||||
eventRegistry.sendSystemEventOutbound(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.customer.work.service;
|
|
||||||
|
|
||||||
import org.flowable.engine.RuntimeService;
|
|
||||||
import org.flowable.task.service.delegate.DelegateTask;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component("taskProcessStarter")
|
|
||||||
public class TaskProcessStarter {
|
|
||||||
@Autowired
|
|
||||||
private RuntimeService runtimeService;
|
|
||||||
|
|
||||||
public void start(String processKey, DelegateTask task) {
|
|
||||||
|
|
||||||
Map<String, Object> vars = new HashMap<>(task.getVariables()); // carry over existing vars
|
|
||||||
vars.put("originTaskId", task.getId());
|
|
||||||
vars.put("originTaskName", task.getName());
|
|
||||||
|
|
||||||
runtimeService.startProcessInstanceByKey(processKey, vars);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,12 +9,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
|||||||
import org.flowable.cmmn.api.CmmnRuntimeService;
|
import org.flowable.cmmn.api.CmmnRuntimeService;
|
||||||
import org.flowable.cmmn.api.runtime.CaseInstance;
|
import org.flowable.cmmn.api.runtime.CaseInstance;
|
||||||
import org.flowable.cmmn.api.runtime.PlanItemInstance;
|
import org.flowable.cmmn.api.runtime.PlanItemInstance;
|
||||||
import org.flowable.cmmn.engine.CmmnEngineConfiguration;
|
|
||||||
import org.flowable.cmmn.engine.impl.persistence.entity.CaseInstanceEntity;
|
import org.flowable.cmmn.engine.impl.persistence.entity.CaseInstanceEntity;
|
||||||
import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
|
|
||||||
import org.flowable.common.engine.api.delegate.event.FlowableEvent;
|
|
||||||
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
|
|
||||||
import org.flowable.engine.ProcessEngineConfiguration;
|
|
||||||
import org.flowable.engine.RuntimeService;
|
import org.flowable.engine.RuntimeService;
|
||||||
import org.flowable.engine.delegate.DelegateExecution;
|
import org.flowable.engine.delegate.DelegateExecution;
|
||||||
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
|
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
|
||||||
@@ -23,7 +18,6 @@ import org.flowable.engine.runtime.ProcessInstance;
|
|||||||
import org.flowable.variable.api.delegate.VariableScope;
|
import org.flowable.variable.api.delegate.VariableScope;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -33,29 +27,19 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General-purpose Flowable variable utility bean.
|
* General-purpose Flowable variable utility bean for tracking variable changes.
|
||||||
* Usable in BPMN process and CMMN case backend expressions without any scope parameter:
|
* Usable in BPMN process and CMMN case backend expressions:
|
||||||
* ${varUtils.get('order.customer.name')}
|
* ${varUtils.trackVars('root.snapshot', 'order.customer.name,order.total,status')}
|
||||||
* ${varUtils.track('order.customer.name,order.total,status')}
|
* ${varUtils.trackVars(execution, 'root.snapshot', 'status')} — BPMN, explicit scope
|
||||||
* Root variables are supported.
|
* ${varUtils.trackVars(planItemInstance, 'root.snapshot', 'status')} — CMMN, explicit scope
|
||||||
* How the scope is resolved without a parameter
|
* Variable paths use dot notation; a leading "root." prefix addresses the root
|
||||||
* -----------------------------------------------
|
* instance of a nested case/process hierarchy.
|
||||||
* The bean registers itself as a Flowable event listener on both engines.
|
|
||||||
* Before a service task expression is evaluated, Flowable fires events on the
|
|
||||||
* same thread that allow us to capture the current variable scope:
|
|
||||||
* BPMN — PROCESS_STARTED fires a FlowableEngineEntityEvent.
|
|
||||||
* The execution is retrieved via event.getEntity().
|
|
||||||
|
|
||||||
* CMMN — CASE_STARTED fires a FlowableEngineEntityEvent.; Instead, when a plan
|
|
||||||
* The case instance is retrieved via event.getEntity().
|
|
||||||
*/
|
*/
|
||||||
@Component("varUtils")
|
@Component("varUtils")
|
||||||
public class VarUtils implements FlowableEventListener, SmartInitializingSingleton {
|
public class VarUtils {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(VarUtils.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(VarUtils.class);
|
||||||
|
|
||||||
private static final ThreadLocal<VariableScope> SCOPE = new ThreadLocal<>();
|
|
||||||
|
|
||||||
private static final ObjectMapper MAPPER;
|
private static final ObjectMapper MAPPER;
|
||||||
static {
|
static {
|
||||||
MAPPER = new ObjectMapper();
|
MAPPER = new ObjectMapper();
|
||||||
@@ -63,72 +47,12 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ProcessEngineConfiguration processEngineConfiguration;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private CmmnEngineConfiguration cmmnEngineConfiguration;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RuntimeService runtimeService;
|
private RuntimeService runtimeService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CmmnRuntimeService cmmnRuntimeService;
|
private CmmnRuntimeService cmmnRuntimeService;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Listener registration — runs after all beans are ready
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
@Override
|
|
||||||
public void afterSingletonsInstantiated() {
|
|
||||||
if (processEngineConfiguration != null) {
|
|
||||||
processEngineConfiguration.getEventDispatcher().addEventListener(this);
|
|
||||||
LOGGER.debug("varUtils registered on BPMN event dispatcher");
|
|
||||||
}
|
|
||||||
if (cmmnEngineConfiguration != null) {
|
|
||||||
cmmnEngineConfiguration.getEventDispatcher().addEventListener(this);
|
|
||||||
LOGGER.debug("varUtils registered on CMMN event dispatcher");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// FlowableEventListener — bind / unbind the scope ThreadLocal
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
@Override
|
|
||||||
public void onEvent(FlowableEvent event) {
|
|
||||||
|
|
||||||
if (event instanceof FlowableEngineEntityEvent entityEvent) {
|
|
||||||
String typeName = event.getType().name();
|
|
||||||
Object entity = entityEvent.getEntity();
|
|
||||||
|
|
||||||
// BPMN execution
|
|
||||||
if (entity instanceof ExecutionEntity execution) {
|
|
||||||
switch (typeName) {
|
|
||||||
case "PROCESS_STARTED"
|
|
||||||
-> SCOPE.set(execution);
|
|
||||||
case "PROCESS_CANCELLED",
|
|
||||||
"PROCESS_COMPLETED",
|
|
||||||
"PROCESS_COMPLETED_WITH_ERROR_END_EVENT",
|
|
||||||
"PROCESS_COMPLETED_WITH_ESCALATION_END_EVENT",
|
|
||||||
"PROCESS_COMPLETED_WITH_TERMINATE_END_EVENT"
|
|
||||||
-> SCOPE.remove();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CMMN case instance itself
|
|
||||||
if (entity instanceof CaseInstanceEntity caseInstance) {
|
|
||||||
if ("CASE_STARTED".equals(typeName))
|
|
||||||
SCOPE.set(caseInstance);
|
|
||||||
else if ("CASE_ENDED".equals(typeName))
|
|
||||||
SCOPE.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean isFailOnException() { return false; }
|
|
||||||
@Override public boolean isFireOnTransactionLifecycleEvent() { return false; }
|
|
||||||
@Override public String getOnTransaction() { return null; }
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Public API — called from Flowable expressions
|
// Public API — called from Flowable expressions
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -136,39 +60,41 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
/**
|
/**
|
||||||
* Checks which of the given comma-separated variable paths changed since the
|
* Checks which of the given comma-separated variable paths changed since the
|
||||||
* last call and returns a JSON array describing each change.
|
* last call and returns a JSON array describing each change.
|
||||||
* ${varUtils.trackVars('root.snapshot', 'root.myString, status')}
|
* Example call:
|
||||||
* {@code snapshotPath} is a dot-notation path (supports the {@code root.} prefix)
|
* ${varUtils.trackVars(self, 'root.snapshot', 'root.status, status')}
|
||||||
* pointing to where the previous-values snapshot is stored and updated.
|
|
||||||
* Each subsequent call compares against the previous snapshot and updates it.
|
|
||||||
* Example return value:
|
* Example return value:
|
||||||
* [{"path":"root.myString","oldValue":"a","newValue":"b"}]
|
* [{"path":"root.status","oldValue":"a","newValue":"b"}, {"path":"status","oldValue":"c","newValue":"d"}]
|
||||||
*/
|
*/
|
||||||
public ArrayNode trackVars(String snapshotPath, String variablePathsCsv) {
|
public ArrayNode trackVars(VariableScope currentScope, String snapshotPath, String variablePathsCsv) {
|
||||||
|
|
||||||
// Check parameters
|
// Reject blank parameters early — report "no changes" instead of failing,
|
||||||
|
// so a misconfigured expression cannot break the surrounding process/case.
|
||||||
if (snapshotPath == null || snapshotPath.isBlank() || variablePathsCsv == null || variablePathsCsv.isBlank()) {
|
if (snapshotPath == null || snapshotPath.isBlank() || variablePathsCsv == null || variablePathsCsv.isBlank()) {
|
||||||
LOGGER.debug("{}.trackVars: empty parameters", getClass().getName());
|
LOGGER.debug("{}.trackVars: empty parameters", getClass().getName());
|
||||||
return MAPPER.createArrayNode();
|
return MAPPER.createArrayNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current scope
|
// Without a resolvable scope there is nothing to read from or write to.
|
||||||
VariableScope currentScope = getCurrentScope();
|
|
||||||
if (currentScope == null) {
|
if (currentScope == null) {
|
||||||
LOGGER.debug("{}.trackVars: currentScope not found", getClass().getName());
|
LOGGER.debug("{}.trackVars: currentScope not found", getClass().getName());
|
||||||
return MAPPER.createArrayNode();
|
return MAPPER.createArrayNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create paths list
|
// Split the CSV into individual variable paths, dropping blanks and whitespace.
|
||||||
List<String> paths = Arrays.stream(variablePathsCsv.split(","))
|
List<String> paths = Arrays.stream(variablePathsCsv.split(","))
|
||||||
.map(String::trim).filter(s -> !s.isEmpty()).toList();
|
.map(String::trim).filter(s -> !s.isEmpty()).toList();
|
||||||
|
|
||||||
|
// Load the previous values (persisted as a JSON string variable at
|
||||||
|
// snapshotPath) to compare the current values against.
|
||||||
Map<String, JsonNode> oldSnapshot = loadSnapshot(currentScope, snapshotPath);
|
Map<String, JsonNode> oldSnapshot = loadSnapshot(currentScope, snapshotPath);
|
||||||
Map<String, JsonNode> newSnapshot = new HashMap<>();
|
Map<String, JsonNode> newSnapshot = new HashMap<>();
|
||||||
ArrayNode changes = MAPPER.createArrayNode();
|
ArrayNode changes = MAPPER.createArrayNode();
|
||||||
|
|
||||||
// Loop paths list
|
// For every tracked path: read the current value, normalize it so it
|
||||||
|
// compares consistently with the reloaded snapshot, and record a change
|
||||||
|
// entry whenever old != new.
|
||||||
for (String path : paths) {
|
for (String path : paths) {
|
||||||
JsonNode newValue = toJson(readVariableFromPath(currentScope, path));
|
JsonNode newValue = normalize(toJson(readVariableFromPath(currentScope, path)));
|
||||||
newSnapshot.put(path, newValue);
|
newSnapshot.put(path, newValue);
|
||||||
|
|
||||||
JsonNode oldValue = oldSnapshot.getOrDefault(path, MAPPER.nullNode());
|
JsonNode oldValue = oldSnapshot.getOrDefault(path, MAPPER.nullNode());
|
||||||
@@ -181,6 +107,7 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the current values as the reference snapshot for the next call.
|
||||||
saveSnapshot(currentScope, snapshotPath, newSnapshot);
|
saveSnapshot(currentScope, snapshotPath, newSnapshot);
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
@@ -190,10 +117,18 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
// Path resolution
|
// Path resolution
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a value via a dot-notation path, e.g. "order.customer.name".
|
||||||
|
* The first segment names a Flowable variable; the remaining segments navigate
|
||||||
|
* into that value (maps, lists/arrays by index, JsonNodes, POJOs — see
|
||||||
|
* {@link #getNestedVariable}). Returns null when any segment cannot be resolved.
|
||||||
|
*/
|
||||||
private Object readVariableFromPath(VariableScope scope, String path) {
|
private Object readVariableFromPath(VariableScope scope, String path) {
|
||||||
String[] segments = path.split("\\.", -1);
|
String[] segments = path.split("\\.", -1);
|
||||||
if (segments.length < 1) return null;
|
if (segments.length < 1) return null;
|
||||||
|
|
||||||
|
// A leading "root." switches to the root instance of the surrounding
|
||||||
|
// case/process hierarchy before resolving the variable.
|
||||||
int startIndex = 0;
|
int startIndex = 0;
|
||||||
if ("root".equals(segments[0])) {
|
if ("root".equals(segments[0])) {
|
||||||
if (segments.length < 2) return null;
|
if (segments.length < 2) return null;
|
||||||
@@ -202,6 +137,7 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
}
|
}
|
||||||
if (scope == null) return null;
|
if (scope == null) return null;
|
||||||
|
|
||||||
|
// Read the top-level variable, then walk the remaining segments into it.
|
||||||
Object currentValue = scope.getVariable(segments[startIndex]);
|
Object currentValue = scope.getVariable(segments[startIndex]);
|
||||||
for (int i = startIndex + 1; i < segments.length; i++) {
|
for (int i = startIndex + 1; i < segments.length; i++) {
|
||||||
if (currentValue == null) return null;
|
if (currentValue == null) return null;
|
||||||
@@ -210,10 +146,18 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
return currentValue;
|
return currentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a value via a dot-notation path (same syntax as
|
||||||
|
* {@link #readVariableFromPath}). A single-segment path sets a plain Flowable
|
||||||
|
* variable; a nested path mutates the container inside the top-level variable
|
||||||
|
* and writes that variable back so the engine persists the change.
|
||||||
|
*/
|
||||||
private void writeVariableToPath(VariableScope scope, String path, Object value) {
|
private void writeVariableToPath(VariableScope scope, String path, Object value) {
|
||||||
String[] segments = path.split("\\.", -1);
|
String[] segments = path.split("\\.", -1);
|
||||||
if (segments.length < 1) return;
|
if (segments.length < 1) return;
|
||||||
|
|
||||||
|
// A leading "root." redirects the write to the root instance of the
|
||||||
|
// surrounding case/process hierarchy.
|
||||||
VariableScope targetScope;
|
VariableScope targetScope;
|
||||||
String[] varSegments;
|
String[] varSegments;
|
||||||
if ("root".equals(segments[0])) {
|
if ("root".equals(segments[0])) {
|
||||||
@@ -249,6 +193,11 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
targetScope.setVariable(topVar, topValue);
|
targetScope.setVariable(topVar, topValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets one key on a mutable container (Map or ObjectNode). Anything else —
|
||||||
|
* including POJOs, which are read-only for this bean — is rejected with a
|
||||||
|
* warning so a bad path never breaks the calling expression.
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private boolean setNestedValue(Object parent, String key, Object value, String path) {
|
private boolean setNestedValue(Object parent, String key, Object value, String path) {
|
||||||
if (parent instanceof Map map) {
|
if (parent instanceof Map map) {
|
||||||
@@ -268,20 +217,27 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
// Scope resolution
|
// Scope resolution
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private VariableScope getCurrentScope() {
|
/**
|
||||||
VariableScope scope = SCOPE.get();
|
* Climbs from the given scope to the root VariableScope of the surrounding
|
||||||
if (scope == null) {
|
* case/process hierarchy, following call-activity parents (BPMN) and
|
||||||
LOGGER.error("{}.getCurrentScope called without an active Flowable scope", getClass().getName());
|
* parent/callback links (CMMN) across engine boundaries.
|
||||||
}
|
*/
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VariableScope getRootScope(VariableScope scope) {
|
private VariableScope getRootScope(VariableScope scope) {
|
||||||
if (scope instanceof DelegateExecution ex) return findBpmnRootScope(ex.getProcessInstanceId());
|
if (scope instanceof DelegateExecution ex) return findBpmnRootScope(ex.getProcessInstanceId());
|
||||||
|
if (scope instanceof PlanItemInstance pii) return findCmmnRootScope(pii.getCaseInstanceId());
|
||||||
if (scope instanceof CaseInstance ci) return findCmmnRootScope(ci.getId());
|
if (scope instanceof CaseInstance ci) return findCmmnRootScope(ci.getId());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the root scope starting from a BPMN process instance:
|
||||||
|
* 1. started by a call activity → recurse into the calling process instance,
|
||||||
|
* 2. started from a CMMN plan item (callback) → continue climbing in the case,
|
||||||
|
* 3. otherwise this process instance is itself the root.
|
||||||
|
* NOTE: the returned object is a detached query result used as VariableScope;
|
||||||
|
* its variable access only works because expression evaluation runs inside an
|
||||||
|
* active Flowable command context.
|
||||||
|
*/
|
||||||
private VariableScope findBpmnRootScope(String processInstanceId) {
|
private VariableScope findBpmnRootScope(String processInstanceId) {
|
||||||
if (processInstanceId == null || runtimeService == null) return null;
|
if (processInstanceId == null || runtimeService == null) return null;
|
||||||
try {
|
try {
|
||||||
@@ -309,6 +265,13 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the root scope starting from a CMMN case instance:
|
||||||
|
* 1. sub-case → recurse into the parent case,
|
||||||
|
* 2. started from a BPMN call/task (callback) → continue climbing in the process,
|
||||||
|
* 3. otherwise this case instance is itself the root.
|
||||||
|
* Same detached-query-result caveat as {@link #findBpmnRootScope}.
|
||||||
|
*/
|
||||||
private VariableScope findCmmnRootScope(String caseInstanceId) {
|
private VariableScope findCmmnRootScope(String caseInstanceId) {
|
||||||
if (caseInstanceId == null || cmmnRuntimeService == null) return null;
|
if (caseInstanceId == null || cmmnRuntimeService == null) return null;
|
||||||
try {
|
try {
|
||||||
@@ -331,7 +294,16 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves one path segment against a value: list/array index, map key,
|
||||||
|
* JsonNode field (scalars are unwrapped to plain Java values), and finally
|
||||||
|
* POJO access via getX()/isX() getters or a declared field.
|
||||||
|
*/
|
||||||
private static Object getNestedVariable(Object obj, String segment) {
|
private static Object getNestedVariable(Object obj, String segment) {
|
||||||
|
// Empty segments (e.g. from "a..b" or a trailing dot) cannot address anything.
|
||||||
|
if (segment == null || segment.isEmpty()) return null;
|
||||||
|
|
||||||
|
// Lists and arrays are addressed by numeric index, e.g. "items.0.name".
|
||||||
if (obj instanceof List<?> list) {
|
if (obj instanceof List<?> list) {
|
||||||
try {
|
try {
|
||||||
int i = Integer.parseInt(segment);
|
int i = Integer.parseInt(segment);
|
||||||
@@ -345,6 +317,9 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
}
|
||||||
if (obj instanceof Map<?, ?> map) return map.get(segment);
|
if (obj instanceof Map<?, ?> map) return map.get(segment);
|
||||||
|
|
||||||
|
// JsonNode: unwrap scalar fields to plain Java values so they compare and
|
||||||
|
// serialize the same way as values read from Map/POJO variables.
|
||||||
if (obj instanceof JsonNode jn) {
|
if (obj instanceof JsonNode jn) {
|
||||||
JsonNode node = jn.get(segment);
|
JsonNode node = jn.get(segment);
|
||||||
if (node == null || node.isNull()) return null;
|
if (node == null || node.isNull()) return null;
|
||||||
@@ -355,6 +330,9 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
if (node.isDouble()) return node.asDouble();
|
if (node.isDouble()) return node.asDouble();
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback for POJOs: try bean getters first, then a declared field
|
||||||
|
// (climbing the class hierarchy). Read-only — writes reject POJO parents.
|
||||||
String cap = Character.toUpperCase(segment.charAt(0)) + segment.substring(1);
|
String cap = Character.toUpperCase(segment.charAt(0)) + segment.substring(1);
|
||||||
try {
|
try {
|
||||||
return obj.getClass().getMethod("get" + cap).invoke(obj);
|
return obj.getClass().getMethod("get" + cap).invoke(obj);
|
||||||
@@ -370,6 +348,7 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Looks up a declared field by name, walking up the class hierarchy. */
|
||||||
private static java.lang.reflect.Field findField(Class<?> c, String name) {
|
private static java.lang.reflect.Field findField(Class<?> c, String name) {
|
||||||
while (c != null && c != Object.class) {
|
while (c != null && c != Object.class) {
|
||||||
try { return c.getDeclaredField(name); }
|
try { return c.getDeclaredField(name); }
|
||||||
@@ -382,6 +361,12 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
// Snapshot persistence
|
// Snapshot persistence
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the previous-values snapshot: reads the variable at {@code path}
|
||||||
|
* (persisted as a JSON string by {@link #saveSnapshot}) and parses it back
|
||||||
|
* into a path → value-node map. Returns an empty map when there is no
|
||||||
|
* snapshot yet (first call) or it is unreadable.
|
||||||
|
*/
|
||||||
private Map<String, JsonNode> loadSnapshot(VariableScope scope, String path) {
|
private Map<String, JsonNode> loadSnapshot(VariableScope scope, String path) {
|
||||||
Object raw = readVariableFromPath(scope, path);
|
Object raw = readVariableFromPath(scope, path);
|
||||||
if (raw == null) return new HashMap<>();
|
if (raw == null) return new HashMap<>();
|
||||||
@@ -398,6 +383,7 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Persists the snapshot map as a JSON string variable at {@code path}. */
|
||||||
private void saveSnapshot(VariableScope scope, String path, Map<String, JsonNode> snapshot) {
|
private void saveSnapshot(VariableScope scope, String path, Map<String, JsonNode> snapshot) {
|
||||||
try {
|
try {
|
||||||
writeVariableToPath(scope, path, MAPPER.writeValueAsString(snapshot));
|
writeVariableToPath(scope, path, MAPPER.writeValueAsString(snapshot));
|
||||||
@@ -410,9 +396,25 @@ public class VarUtils implements FlowableEventListener, SmartInitializingSinglet
|
|||||||
// Helpers
|
// Helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Wraps any Java value into a JsonNode (null-safe). */
|
||||||
private static JsonNode toJson(Object value) {
|
private static JsonNode toJson(Object value) {
|
||||||
if (value == null) return MAPPER.nullNode();
|
if (value == null) return MAPPER.nullNode();
|
||||||
if (value instanceof JsonNode jn) return jn;
|
if (value instanceof JsonNode jn) return jn;
|
||||||
return MAPPER.valueToTree(value);
|
return MAPPER.valueToTree(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes and re-parses a node so its value types match what
|
||||||
|
* {@link #loadSnapshot} produces when reading the persisted snapshot back
|
||||||
|
* (e.g. a Long 5 read from a variable and an Integer 5 parsed from the
|
||||||
|
* snapshot JSON both become the same numeric node). Without this, unchanged
|
||||||
|
* numeric values would be reported as changes on every call.
|
||||||
|
*/
|
||||||
|
private static JsonNode normalize(JsonNode node) {
|
||||||
|
try {
|
||||||
|
return MAPPER.readTree(MAPPER.writeValueAsString(node));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -34,8 +34,12 @@ flowable.platform.enable-latest-form-definition-lookup=true
|
|||||||
|
|
||||||
# Server URL for REST calls
|
# Server URL for REST calls
|
||||||
baseUrl=http://localhost:8105
|
baseUrl=http://localhost:8105
|
||||||
|
flowable.security.oauth2.post-logout-redirect-url=http://localhost:8105
|
||||||
|
flowable.security.basic-auth.username=admin
|
||||||
|
flowable.security.basic-auth.password=test
|
||||||
|
|
||||||
# Email: Fake SMTP
|
# Email: Fake SMTP
|
||||||
flowable.mail.server.host=localhost
|
flowable.mail.server.host=localhost
|
||||||
flowable.mail.server.port=2525
|
flowable.mail.server.port=2525
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -26,7 +26,7 @@
|
|||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<com.flowable.platform.version>2025.2.05</com.flowable.platform.version>
|
<com.flowable.platform.version>2025.2.07</com.flowable.platform.version>
|
||||||
<liquibase.skip>false</liquibase.skip>
|
<liquibase.skip>false</liquibase.skip>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user