diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ExpressionSuggesterSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ExpressionSuggesterSpec.scala index 1a22aebb0f8..6398d9fdb03 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ExpressionSuggesterSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ExpressionSuggesterSpec.scala @@ -779,7 +779,7 @@ class ExpressionSuggesterSpec ) } - test("should suggest parameters for casts methods") { + test("should suggest parameters for conversion methods") { spelSuggestionsFor("#unknown.to('')", column = 13) should contain theSameElementsAs List( suggestion("BigDecimal", Typed[java.math.BigDecimal]), suggestion("Boolean", Typed[java.lang.Boolean]), diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 55764a56223..b7bba4457d9 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -607,8 +607,12 @@ private[spel] class Typer( val w = Writer.value[List[ExpressionParseError], TypingResult](Unknown) if (methodExecutionForUnknownAllowed) w - else - w.tell(List(IllegalPropertyAccessError(Unknown))) + else { + // we allow some methods to be used on unknown + unknownPropertyTypeBasedOnMethod(e) + .map(valid) + .getOrElse(w.tell(List(IllegalPropertyAccessError(Unknown)))) + } case TypedNull => invalid(IllegalPropertyAccessError(TypedNull), fallbackType = TypedNull) case s: SingleTypingResult => @@ -679,6 +683,9 @@ private[spel] class Typer( classDefinitionSet.get(clazz.klass).flatMap(_.getPropertyOrFieldType(invocationTarget, e.getName)) } + private def unknownPropertyTypeBasedOnMethod(e: PropertyOrFieldReference): Option[TypingResult] = + classDefinitionSet.unknown.flatMap(_.getPropertyOrFieldType(Unknown, e.getName)) + private def extractIterativeType(parent: TypingResult): TypingR[TypingResult] = parent match { case tc: SingleTypingResult if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Collection[_]]) || 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..6da863ae22d 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,9 +4,11 @@ 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 +import pl.touk.nussknacker.engine.extension.{ExtensionAwareMethodsDiscovery, ExtensionsAwareMethodInvoker} import scala.collection.concurrent.TrieMap @@ -52,15 +54,17 @@ object propertyAccessors { however it's not so easy to extend and in interpreted mode we skip original implementation */ object NoParamMethodPropertyAccessor extends ReflectivePropertyAccessor with ReadOnly with Caching { + private val methodInvoker = new ExtensionsAwareMethodInvoker() + 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 - ) + ExtensionAwareMethodsDiscovery + .discover(target) + .find(m => !ClassUtils.isPrimitiveOrWrapper(target) && m.getParameterCount == 0 && m.getName == name) } override protected def invokeMethod( @@ -68,17 +72,38 @@ object propertyAccessors { method: Method, target: Any, context: EvaluationContext - ): AnyRef = { - method.invoke(target) - } + ): Any = + 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 // *do not* extend ReflectivePropertyAccessor and we force interpreted mode // TODO: figure out how to make bytecode generation work also in this case object PrimitiveOrWrappersPropertyAccessor extends PropertyAccessor with ReadOnly with Caching { + private val methodInvoker = new ExtensionsAwareMethodInvoker() + private val emptyArray = Array[AnyRef]() override def getSpecificTargetClasses: Array[Class[_]] = null @@ -87,12 +112,12 @@ object propertyAccessors { method: Method, target: Any, context: EvaluationContext - ): Any = method.invoke(target) + ): Any = methodInvoker.invoke(method)(target, emptyArray) override protected def reallyFindMethod(name: String, target: Class[_]): Option[Method] = { - target.getMethods.find(m => - ClassUtils.isPrimitiveOrWrapper(target) && m.getParameterCount == 0 && m.getName == name - ) + ExtensionAwareMethodsDiscovery + .discover(target) + .find(m => ClassUtils.isPrimitiveOrWrapper(target) && m.getParameterCount == 0 && m.getName == name) } } 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 c0c6a27dd9f..9d20a38ce4b 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 @@ -1448,17 +1448,17 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD Table( ("expression", "expectedType", "expectedResult"), ( - "#stringMap.![{key: #this.key + '_k', value: #this.value + '_v'}].toMap()", + "#stringMap.![{key: #this.key + '_k', value: #this.value + '_v'}].toMap", mapStringStringType, Map("foo_k" -> "bar_v", "baz_k" -> "qux_v").asJava ), ( - "#mapWithDifferentValueTypes.![{key: #this.key, value: #this.value}].toMap()", + "#mapWithDifferentValueTypes.![{key: #this.key, value: #this.value}].toMap", mapStringUnknownType, mapWithDifferentValueTypes ), ( - "#nullableMap.![{key: #this.key, value: #this.value}].toMap()", + "#nullableMap.![{key: #this.key, value: #this.value}].toMap", mapStringStringType, nullableMap ) @@ -1663,25 +1663,25 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD ("#unknownMap.value.is('Map')", true), ("#unknownListOfTuples.value.is('List')", true), ("#unknownListOfTuples.value.is('Map')", true), - ("#unknownBoolean.value.isBoolean()", true), - ("#unknownBooleanString.value.isBoolean()", true), - ("#unknownString.value.isBoolean()", false), - ("#unknownLong.value.isLong()", true), - ("#unknownLongString.value.isLong()", true), - ("#unknownString.value.isLong()", false), - ("#unknownDouble.value.isDouble()", true), - ("#unknownDoubleString.value.isDouble()", true), - ("#unknownString.value.isDouble()", false), - ("#unknownBigDecimal.value.isBigDecimal()", true), - ("#unknownBigDecimalString.value.isBigDecimal()", true), - ("#unknownString.value.isBigDecimal()", false), - ("#unknownList.value.isList()", true), - ("#unknownList.value.isMap()", false), - ("#unknownMap.value.isList()", false), - ("#unknownMap.value.isMap()", true), - ("#unknownListOfTuples.value.isList()", true), - ("#unknownListOfTuples.value.isMap()", true), - ("#arrayOfUnknown.isList()", true), + ("#unknownBoolean.value.isBoolean", true), + ("#unknownBooleanString.value.isBoolean", true), + ("#unknownString.value.isBoolean", false), + ("#unknownLong.value.isLong", true), + ("#unknownLongString.value.isLong", true), + ("#unknownString.value.isLong", false), + ("#unknownDouble.value.isDouble", true), + ("#unknownDoubleString.value.isDouble", true), + ("#unknownString.value.isDouble", false), + ("#unknownBigDecimal.value.isBigDecimal", true), + ("#unknownBigDecimalString.value.isBigDecimal", true), + ("#unknownString.value.isBigDecimal", false), + ("#unknownList.value.isList", true), + ("#unknownList.value.isMap", false), + ("#unknownMap.value.isList", false), + ("#unknownMap.value.isMap", true), + ("#unknownListOfTuples.value.isList", true), + ("#unknownListOfTuples.value.isMap", true), + ("#arrayOfUnknown.isList", true), ) ) { (expression, result) => evaluate[Any](expression, customCtx) shouldBe result @@ -1718,6 +1718,10 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD } } + test("should allow use no param method property accessor on unknown") { + evaluate[String]("#unknownString.value.toString") shouldBe "unknown" + } + } case class SampleObject(list: java.util.List[SampleValue])