Initial commit
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
package com.customer.work.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.flowable.common.engine.api.variable.VariableContainer;
|
||||
import org.flowable.common.engine.impl.el.function.AbstractFlowableVariableExpressionFunction;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Flowable EL function delegate that reads a variable by dot-separated path.
|
||||
* Works in both BPMN processes and CMMN cases without any explicit scope parameter.
|
||||
*
|
||||
* Flowable's AbstractFlowableVariableExpressionFunction automatically injects the
|
||||
* current VariableContainer (execution / planItemInstance / caseInstance) as the
|
||||
* first argument before the expression is evaluated, so no ThreadLocal or listener
|
||||
* is required.
|
||||
*
|
||||
* Registered automatically by Flowable's Spring Boot integration when it finds a
|
||||
* FlowableFunctionDelegate bean in the application context.
|
||||
*
|
||||
* Usage in a Flowable backend expression (prefixes var / vars / variables all work):
|
||||
* ${var:getPath('myVar')}
|
||||
* ${var:getPath('order.customer.name')}
|
||||
* ${var:getPath('order.lines.0.price')} ← list index by position
|
||||
*
|
||||
* Path resolution per segment:
|
||||
* - java.util.List / array → integer index (e.g. "2")
|
||||
* - java.util.Map → key lookup
|
||||
* - Jackson JsonNode → field lookup, scalar nodes unwrapped to Java types
|
||||
* - POJO → public getter (getX / isX) or field, superclass included
|
||||
*
|
||||
* Returns null when any segment along the path does not exist.
|
||||
*/
|
||||
@Component
|
||||
public class VariableGetPathFunction extends AbstractFlowableVariableExpressionFunction {
|
||||
|
||||
public VariableGetPathFunction() {
|
||||
super(List.of("getPath"), "getPath");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by Flowable's EL engine. The {@code container} is injected automatically;
|
||||
* {@code path} is the dot-separated path string supplied in the expression.
|
||||
*/
|
||||
public static Object getPath(VariableContainer container, String path) {
|
||||
if (container == null || path == null || path.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] segments = path.split("\\.", -1);
|
||||
Object current = container.getVariable(segments[0]);
|
||||
|
||||
for (int i = 1; i < segments.length; i++) {
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
current = step(current, segments[i]);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Path navigation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Object step(Object obj, String segment) {
|
||||
|
||||
// List / array index
|
||||
if (obj instanceof List<?> list) {
|
||||
try {
|
||||
int idx = Integer.parseInt(segment);
|
||||
return (idx >= 0 && idx < list.size()) ? list.get(idx) : null;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
if (obj instanceof Object[] arr) {
|
||||
try {
|
||||
int idx = Integer.parseInt(segment);
|
||||
return (idx >= 0 && idx < arr.length) ? arr[idx] : null;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// Map key lookup
|
||||
if (obj instanceof Map<?, ?> map) {
|
||||
return map.get(segment);
|
||||
}
|
||||
|
||||
// Jackson JsonNode — unwrap scalar nodes to plain Java values
|
||||
if (obj instanceof JsonNode jn) {
|
||||
JsonNode node = jn.get(segment);
|
||||
if (node == null || node.isNull()) return null;
|
||||
if (node.isTextual()) return node.asText();
|
||||
if (node.isBoolean()) return node.asBoolean();
|
||||
if (node.isLong()) return node.asLong();
|
||||
if (node.isInt()) return node.asInt();
|
||||
if (node.isDouble()) return node.asDouble();
|
||||
return node;
|
||||
}
|
||||
|
||||
// POJO: try getter (getX / isX), then field with superclass walk
|
||||
try {
|
||||
Method m = obj.getClass().getMethod(
|
||||
"get" + Character.toUpperCase(segment.charAt(0)) + segment.substring(1));
|
||||
return m.invoke(obj);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
try {
|
||||
Method m = obj.getClass().getMethod(
|
||||
"is" + Character.toUpperCase(segment.charAt(0)) + segment.substring(1));
|
||||
return m.invoke(obj);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
try {
|
||||
java.lang.reflect.Field f = findField(obj.getClass(), segment);
|
||||
if (f != null) {
|
||||
f.setAccessible(true);
|
||||
return f.get(obj);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static java.lang.reflect.Field findField(Class<?> clazz, String name) {
|
||||
while (clazz != null && clazz != Object.class) {
|
||||
try {
|
||||
return clazz.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException e) {
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user