From 112e234ade17002de170f6bd042fb8e85768f256 Mon Sep 17 00:00:00 2001 From: Robert Schwanhold Date: Fri, 6 Dec 2024 13:45:01 +0100 Subject: [PATCH 1/4] Allow BeanELResolver to invoke ArrayNode::get with a Long --- .../common/engine/impl/javax/el/Util.java | 2 +- .../impl/javax/el/BeanELResolverTest.java | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java index a7346d1d500..0d84cb1e554 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java @@ -222,7 +222,7 @@ private static Wrapper findWrapper(Class clazz, List> wrapp if (!candidates.isEmpty()) { String errorMsg = "Unable to find unambiguous method: " + clazz + "." + name + "(" + paramString(paramTypes) + ")"; - return findMostSpecificWrapper(candidates, paramTypes, candidatesType == CandidatesType.ASSIGNABLE, errorMsg); + return findMostSpecificWrapper(candidates, paramTypes, candidatesType == CandidatesType.ASSIGNABLE || candidatesType == CandidatesType.COERCIBLE, errorMsg); } throw new MethodNotFoundException("Method not found: " + clazz + "." + name + "(" + paramString(paramTypes) + ")"); diff --git a/modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java b/modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java new file mode 100644 index 00000000000..7ee6f8faeb8 --- /dev/null +++ b/modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java @@ -0,0 +1,47 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.common.engine.impl.javax.el; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.flowable.common.engine.impl.el.FlowableElContext; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.TextNode; + +public class BeanELResolverTest { + + @Test + public void testInvokingGetOnArrayNode() { + // This test checks if the BeanELResolver is able to invoke "get" on an ArrayNode, where a Long is passed as the index. + FlowableElContext context = new FlowableElContext(null, null); + + ObjectMapper om = new ObjectMapper(); + ArrayNode base = om.createArrayNode(); + base.add("firstValue"); + base.add("secondValue"); + base.add("thirdValue"); + + String method = "get"; + Class[] paramTypes = null; + // Note: the parameter is a Long, not an Integer. When a JUEL-expression like "${array.get(0) == \"firstValue\"}" is evaluated, the 0 is treated as a Long. + Object[] params = {Long.valueOf(0L)}; + + Object result = new BeanELResolver().invoke(context, base, method, paramTypes, params); + + assertEquals(((TextNode)result).asText(), "firstValue"); + } + +} From 7b3e3060ed6458dc579d0f97ff4f1ae1d8e68aa6 Mon Sep 17 00:00:00 2001 From: Robert Schwanhold Date: Mon, 9 Dec 2024 13:24:26 +0100 Subject: [PATCH 2/4] revert change to Util class --- .../java/org/flowable/common/engine/impl/javax/el/Util.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java index 0d84cb1e554..a7346d1d500 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/Util.java @@ -222,7 +222,7 @@ private static Wrapper findWrapper(Class clazz, List> wrapp if (!candidates.isEmpty()) { String errorMsg = "Unable to find unambiguous method: " + clazz + "." + name + "(" + paramString(paramTypes) + ")"; - return findMostSpecificWrapper(candidates, paramTypes, candidatesType == CandidatesType.ASSIGNABLE || candidatesType == CandidatesType.COERCIBLE, errorMsg); + return findMostSpecificWrapper(candidates, paramTypes, candidatesType == CandidatesType.ASSIGNABLE, errorMsg); } throw new MethodNotFoundException("Method not found: " + clazz + "." + name + "(" + paramString(paramTypes) + ")"); From 933d23c8a5298b4d99638c8917bd2977cf8c4bd3 Mon Sep 17 00:00:00 2001 From: Robert Schwanhold Date: Mon, 9 Dec 2024 13:25:08 +0100 Subject: [PATCH 3/4] Delete BeanELResolverTest --- .../impl/javax/el/BeanELResolverTest.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java diff --git a/modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java b/modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java deleted file mode 100644 index 7ee6f8faeb8..00000000000 --- a/modules/flowable-engine-common/src/test/java/org/flowable/common/engine/impl/javax/el/BeanELResolverTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.flowable.common.engine.impl.javax.el; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.flowable.common.engine.impl.el.FlowableElContext; -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.TextNode; - -public class BeanELResolverTest { - - @Test - public void testInvokingGetOnArrayNode() { - // This test checks if the BeanELResolver is able to invoke "get" on an ArrayNode, where a Long is passed as the index. - FlowableElContext context = new FlowableElContext(null, null); - - ObjectMapper om = new ObjectMapper(); - ArrayNode base = om.createArrayNode(); - base.add("firstValue"); - base.add("secondValue"); - base.add("thirdValue"); - - String method = "get"; - Class[] paramTypes = null; - // Note: the parameter is a Long, not an Integer. When a JUEL-expression like "${array.get(0) == \"firstValue\"}" is evaluated, the 0 is treated as a Long. - Object[] params = {Long.valueOf(0L)}; - - Object result = new BeanELResolver().invoke(context, base, method, paramTypes, params); - - assertEquals(((TextNode)result).asText(), "firstValue"); - } - -} From aa7fdcf31ab969a8389a409b151e7d5c9c5662bf Mon Sep 17 00:00:00 2001 From: Robert Schwanhold Date: Mon, 9 Dec 2024 13:25:45 +0100 Subject: [PATCH 4/4] Enhance JsonNodeELResolver to invoke methods --- .../engine/impl/el/JsonNodeELResolver.java | 38 ++++++++++++- .../engine/test/el/ExpressionManagerTest.java | 56 +++++++++++++++++++ 2 files changed, 92 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..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" })