From 41a1e8906da9182c59b0c1f9413581865f4bc133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bigorajski?= Date: Fri, 18 Oct 2024 14:54:18 +0200 Subject: [PATCH] [NU-1836] Handle extension methods in NoParamMethodPropertyAccessor --- .../internal/OptimizedEvaluationContext.scala | 3 +- .../spel/internal/propertyAccessors.scala | 41 +++++++++++++++---- .../engine/spel/SpelExpressionSpec.scala | 2 +- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/OptimizedEvaluationContext.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/OptimizedEvaluationContext.scala index 29285ee81fe..95b5e858194 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/OptimizedEvaluationContext.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/OptimizedEvaluationContext.scala @@ -95,7 +95,8 @@ object EvaluationContextPreparer { classDefinitionSet: ClassDefinitionSet ): EvaluationContextPreparer = { val conversionService = determineConversionService(expressionConfig) - val propertyAccessors = internal.propertyAccessors.configured() + val methodInvoker = new ExtensionsAwareMethodInvoker(new ExtensionMethodsInvoker(classLoader, classDefinitionSet)) + val propertyAccessors = internal.propertyAccessors.configured(methodInvoker) new EvaluationContextPreparer( classLoader, expressionConfig.globalImports, diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/propertyAccessors.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/propertyAccessors.scala index 58f92dc14c2..acd6a6bed7b 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/propertyAccessors.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/internal/propertyAccessors.scala @@ -4,6 +4,7 @@ import java.lang.reflect.{Method, Modifier} import java.util.Optional import org.apache.commons.lang3.ClassUtils import org.springframework.expression.spel.support.ReflectivePropertyAccessor +import org.springframework.expression.spel.support.ReflectivePropertyAccessor.OptimalPropertyAccessor import org.springframework.expression.{EvaluationContext, PropertyAccessor, TypedValue} import pl.touk.nussknacker.engine.api.dict.DictInstance import pl.touk.nussknacker.engine.api.exception.NonTransientException @@ -15,7 +16,7 @@ object propertyAccessors { // Order of accessors matters - property from first accessor that returns `true` from `canRead` will be chosen. // This general order can be overridden - each accessor can define target classes for which it will have precedence - // through the `getSpecificTargetClasses` method. - def configured(): Seq[PropertyAccessor] = { + def configured(methodInvoker: ExtensionsAwareMethodInvoker): Seq[PropertyAccessor] = { Seq( MapPropertyAccessor, // must be before NoParamMethodPropertyAccessor and ReflectivePropertyAccessor new ReflectivePropertyAccessor(), @@ -25,7 +26,7 @@ object propertyAccessors { PrimitiveOrWrappersPropertyAccessor, StaticPropertyAccessor, TypedDictInstancePropertyAccessor, // must be before NoParamMethodPropertyAccessor - NoParamMethodPropertyAccessor, + new NoParamMethodPropertyAccessor(methodInvoker), // it can add performance overhead so it will be better to keep it on the bottom MapLikePropertyAccessor, MapMissingPropertyToNullAccessor, // must be after NoParamMethodPropertyAccessor @@ -51,16 +52,22 @@ object propertyAccessors { This one is a bit tricky. We extend ReflectivePropertyAccessor, as it's the only sensible way to make it compilable, however it's not so easy to extend and in interpreted mode we skip original implementation */ - object NoParamMethodPropertyAccessor extends ReflectivePropertyAccessor with ReadOnly with Caching { + class NoParamMethodPropertyAccessor(methodInvoker: ExtensionsAwareMethodInvoker) + extends ReflectivePropertyAccessor + with ReadOnly + with Caching { + + private val methodsDiscovery = new ExtensionAwareMethodsDiscovery + private val emptyArray = Array[AnyRef]() override def findGetterForProperty(propertyName: String, clazz: Class[_], mustBeStatic: Boolean): Method = { findMethodFromClass(propertyName, clazz).orNull } override protected def reallyFindMethod(name: String, target: Class[_]): Option[Method] = { - target.getMethods.find(m => - !ClassUtils.isPrimitiveOrWrapper(target) && m.getParameterCount == 0 && m.getName == name - ) + methodsDiscovery + .discover(target) + .find(m => !ClassUtils.isPrimitiveOrWrapper(target) && m.getParameterCount == 0 && m.getName == name) } override protected def invokeMethod( @@ -69,10 +76,30 @@ object propertyAccessors { target: Any, context: EvaluationContext ): AnyRef = { - method.invoke(target) + methodInvoker.invoke(method, target, emptyArray) } override def getSpecificTargetClasses: Array[Class[_]] = null + + override def createOptimalAccessor(context: EvaluationContext, target: Any, name: String): PropertyAccessor = + super.createOptimalAccessor(context, target, name) match { + case o: OptimalPropertyAccessor => new NuOptimalAccessor(o) + case o => o + } + + private class NuOptimalAccessor(delegate: PropertyAccessor) extends PropertyAccessor { + override def getSpecificTargetClasses: Array[Class[_]] = + delegate.getSpecificTargetClasses + override def canWrite(context: EvaluationContext, target: Any, name: String): Boolean = + delegate.canWrite(context, target, name) + override def write(context: EvaluationContext, target: Any, name: String, newValue: Any): Unit = + delegate.write(context, target, name, newValue) + override def canRead(context: EvaluationContext, target: Any, name: String): Boolean = + NoParamMethodPropertyAccessor.this.canRead(context, target, name) + override def read(context: EvaluationContext, target: Any, name: String): TypedValue = + NoParamMethodPropertyAccessor.this.read(context, target, name) + } + } // Spring bytecode generation fails when we try to invoke methods on primitives, so we diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 2160bef6718..81345b6039c 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -1510,7 +1510,7 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD } test("should convert a Set to a List") { - val parsed = parse[Any](expr = "#setVal.toList()", context = ctx).validValue + val parsed = parse[Any](expr = "#setVal.toList", context = ctx).validValue parsed.returnType.withoutValue shouldBe Typed.genericTypeClass[JList[_]](List(Typed.typedClass[String])) parsed.expression.evaluateSync[Any](ctx) shouldBe List("a").asJava }