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..ffc4fe9a096 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 @@ -14,9 +14,11 @@ import java.beans.FeatureDescriptor; import java.math.BigDecimal; +import java.util.Arrays; import java.util.Date; import java.util.Iterator; +import org.flowable.common.engine.impl.javax.el.BeanELResolver; import org.flowable.common.engine.impl.javax.el.CompositeELResolver; import org.flowable.common.engine.impl.javax.el.ELContext; import org.flowable.common.engine.impl.javax.el.ELException; @@ -37,19 +39,21 @@ public class JsonNodeELResolver extends ELResolver { private final boolean readOnly; + private final BeanELResolver beanELResolver; /** - * 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; + this.beanELResolver = new BeanELResolver(readOnly); } /** @@ -257,6 +261,36 @@ 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 (context == null) { + throw new NullPointerException("context is null"); + } + + Object result = null; + if (isResolvable(base)) { + // Use BeanELResolver to invoke the method. + // If a JsonNode method has a number as a parameter (e.g. an index) it is always an int (with the exception of methods like asLong, asDouble etc.). + // In JUEL expressions, literal numbers are treated as Long. Therefore, we are converting them to int here. + Object[] convertedParams; + if (params == null || method.toString().startsWith("as")) { + convertedParams = params; + } else { + convertedParams = Arrays.asList(params).stream().map(param -> { + if (param instanceof Number) { + return ((Number) param).intValue(); + } else { + return param; + } + }).toArray(); + } + result = beanELResolver.invoke(context, base, method, paramTypes, convertedParams); + context.setPropertyResolved(true); + } + + return result; + } /** * 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..07d8fef6767 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 @@ -15,6 +15,7 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.HashMap; import java.util.Map; @@ -32,6 +33,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 +277,60 @@ 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); + + assertEquals(getExpressionValue("${array.get(0).isTextual()}", processInstance), true); + assertEquals(getExpressionValue("${array.get(0).textValue()}", processInstance), "firstValue"); + assertEquals(getExpressionValue("${array.get(0).isNumber()}", processInstance), false); + + assertEquals(getExpressionValue("${array.get(2).isNumber()}", processInstance), true); + assertEquals(getExpressionValue("${array.get(2).asInt()}", processInstance), 42); + assertEquals(getExpressionValue("${array.get(2).asLong()}", processInstance), 42L); + + assertEquals(getExpressionValue("${array.get(1).textValue()}", processInstance), "secondValue"); + assertEquals(getExpressionValue("${array.get(1).asLong(123)}", processInstance), 123L); + } + + @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); + + assertEquals(getExpressionValue("${object.get(\"firstAttribute\").isTextual()}", processInstance), true); + assertEquals(getExpressionValue("${object.get(\"firstAttribute\").textValue()}", processInstance), "foo"); + assertEquals(getExpressionValue("${object.get(\"firstAttribute\").isNumber()}", processInstance), false); + + assertEquals(getExpressionValue("${object.get(\"thirdAttribute\").isNumber()}", processInstance), true); + assertEquals(getExpressionValue("${object.get(\"thirdAttribute\").asInt()}", processInstance), 42); + assertEquals(getExpressionValue("${object.get(\"thirdAttribute\").asLong()}", processInstance), 42L); + + assertEquals(getExpressionValue("${object.get(\"secondAttribute\").textValue()}", processInstance), "bar"); + assertEquals(getExpressionValue("${object.get(\"secondAttribute\").asLong(123)}", processInstance), 123L); + } + + 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" })