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; } }