From a7b7bcf45bec4ef02b69071502c069139e4b4712 Mon Sep 17 00:00:00 2001 From: rssap <163425752+rssap@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:42:40 +0100 Subject: [PATCH] Allow JsonNodeELResolver to invoke `get` and `path` with a long index (#3996) --- .../engine/impl/el/JsonNodeELResolver.java | 36 ++++++++++- .../engine/test/el/ExpressionManagerTest.java | 61 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/el/JsonNodeELResolver.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/el/JsonNodeELResolver.java index 5259c7cd5e9..d1504971652 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/el/JsonNodeELResolver.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/el/JsonNodeELResolver.java @@ -39,14 +39,14 @@ public class JsonNodeELResolver extends ELResolver { private final boolean readOnly; /** - * Creates a new read/write BeanELResolver. + * Creates a new read/write JsonNodeELResolver. */ public JsonNodeELResolver() { this(false); } /** - * Creates a new BeanELResolver whose read-only status is determined by the given parameter. + * Creates a new JsonNodeELResolver whose read-only status is determined by the given parameter. */ public JsonNodeELResolver(boolean readOnly) { this.readOnly = readOnly; @@ -257,6 +257,38 @@ public boolean isReadOnly(ELContext context, Object base, Object property) { } return readOnly; } + + @Override + public Object invoke(ELContext context, Object base, Object method, Class[] paramTypes, Object[] params) { + if (!isResolvable(base)) { + return null; + } + if (method == null) { + return null; + } + if (params.length != 1) { + return null; + } + Object param = params[0]; + if (!(param instanceof Long)) { + return null; + } + + int index = ((Long) param).intValue(); + String methodName = method.toString(); + JsonNode node = (JsonNode) base; + if (methodName.equals("path")) { + JsonNode valueNode = node.path(index); + context.setPropertyResolved(true); + return valueNode; + } else if (methodName.equals("get")) { + JsonNode valueNode = node.get(index); + context.setPropertyResolved(true); + return valueNode; + } + + return null; + } /** * If the base object is a map, attempts to set the value associated with the given key, as specified by the property argument. If the base is a Map, the propertyResolved property of the ELContext diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/el/ExpressionManagerTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/el/ExpressionManagerTest.java index 5fb09157697..72016568ad4 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/el/ExpressionManagerTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/el/ExpressionManagerTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; /** @@ -275,6 +276,66 @@ public void testInvokeIntegerMethodWithNullParameter() { assertThat(value).isNull(); } + @Test + @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") + public void testInvokeOnArrayNode() { + Map vars = new HashMap<>(); + ArrayNode arrayNode = processEngineConfiguration.getObjectMapper().createArrayNode(); + arrayNode.add("firstValue"); + arrayNode.add("secondValue"); + arrayNode.add(42); + + vars.put("array", arrayNode); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneTaskProcess", vars); + + assertThat(getExpressionValue("${array.get(0).isTextual()}", processInstance)).isEqualTo(true); + assertThat(getExpressionValue("${array.get(0).textValue()}", processInstance)).isEqualTo("firstValue"); + assertThat(getExpressionValue("${array.get(0).isNumber()}", processInstance)).isEqualTo(false); + + assertThat(getExpressionValue("${array.get(2).isNumber()}", processInstance)).isEqualTo(true); + assertThat(getExpressionValue("${array.get(2).asInt()}", processInstance)).isEqualTo(42); + assertThat(getExpressionValue("${array.get(2).asLong()}", processInstance)).isEqualTo(42L); + + assertThat(getExpressionValue("${array.get(1).textValue()}", processInstance)).isEqualTo("secondValue"); + assertThat(getExpressionValue("${array.get(1).asLong(123)}", processInstance)).isEqualTo(123L); + + assertThat(getExpressionValue("${array.get(3)}", processInstance)).isNull(); + assertThat(getExpressionValue("${array.path(3).isMissingNode()}", processInstance)).isEqualTo(true); + } + + @Test + @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") + public void testInvokeOnObjectNode() { + Map vars = new HashMap<>(); + ObjectNode objectNode = processEngineConfiguration.getObjectMapper().createObjectNode(); + objectNode.put("firstAttribute", "foo"); + objectNode.put("secondAttribute", "bar"); + objectNode.put("thirdAttribute", 42); + + vars.put("object", objectNode); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneTaskProcess", vars); + + assertThat(getExpressionValue("${object.get(\"firstAttribute\").isTextual()}", processInstance)).isEqualTo(true); + assertThat(getExpressionValue("${object.get(\"firstAttribute\").textValue()}", processInstance)).isEqualTo("foo"); + assertThat(getExpressionValue("${object.get(\"firstAttribute\").isNumber()}", processInstance)).isEqualTo(false); + + assertThat(getExpressionValue("${object.get(\"thirdAttribute\").isNumber()}", processInstance)).isEqualTo(true); + assertThat(getExpressionValue("${object.get(\"thirdAttribute\").asInt()}", processInstance)).isEqualTo(42); + assertThat(getExpressionValue("${object.get(\"thirdAttribute\").asLong()}", processInstance)).isEqualTo(42L); + + assertThat(getExpressionValue("${object.get(\"secondAttribute\").textValue()}", processInstance)).isEqualTo("bar"); + assertThat(getExpressionValue("${object.get(\"secondAttribute\").asLong(123)}", processInstance)).isEqualTo(123L); + + assertThat(getExpressionValue("${object.get(\"dummyAttribute\")}", processInstance)).isNull(); + assertThat(getExpressionValue("${object.path(\"dummyAttribute\").isMissingNode()}", processInstance)).isEqualTo(true); + } + + private Object getExpressionValue(String expressionStr, ProcessInstance processInstance) { + Expression expression = this.processEngineConfiguration.getExpressionManager().createExpression(expressionStr); + return managementService.executeCommand(commandContext -> + expression.getValue((ExecutionEntity) runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()).includeProcessVariables().singleResult())); + } + @ParameterizedTest @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") @ValueSource(strings = { "", "flowable" })