Skip to content

Commit

Permalink
[NU-1701] Casts suggester (#6974)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasz-bigorajski authored Oct 4, 2024
1 parent d2e1d2c commit 353e063
Show file tree
Hide file tree
Showing 19 changed files with 345 additions and 149 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package pl.touk.nussknacker.ui.suggester

import io.circe.generic.JsonCodec
import pl.touk.nussknacker.engine.ModelData
import pl.touk.nussknacker.engine.api.dict.UiDictServices
import pl.touk.nussknacker.engine.api.typed.typing.TypingResult
import pl.touk.nussknacker.engine.definition.component.ComponentDefinitionWithImplementation
import pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionSet
import pl.touk.nussknacker.engine.definition.globalvariables.ExpressionConfigDefinition
import pl.touk.nussknacker.engine.graph.expression.Expression
import pl.touk.nussknacker.engine.graph.expression.Expression.Language
import pl.touk.nussknacker.engine.spel.{ExpressionSuggestion, SpelExpressionSuggester}
import pl.touk.nussknacker.engine.variables.GlobalVariablesPreparer
import pl.touk.nussknacker.engine.ModelData
import pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionSet
import pl.touk.nussknacker.engine.util.CaretPosition2d
import pl.touk.nussknacker.engine.variables.GlobalVariablesPreparer

import scala.concurrent.{ExecutionContext, Future}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,21 @@ class ExpressionSuggesterSpec
)
}

test("should suggest parameters for casts methods") {
spelSuggestionsFor("#unknown.canCastTo('')", column = 20).toSet shouldBe Set(
suggestion("String", Typed[String]),
suggestion("Duration", Typed[Duration]),
suggestion("LocalDateTime", Typed[LocalDateTime]),
suggestion("Map", Typed[java.util.Map[_, _]]),
suggestion("A", Typed[A]),
suggestion("AA", Typed[AA]),
suggestion("B", Typed[B]),
suggestion("C", Typed[C]),
suggestion("Util", Typed[Util]),
suggestion("WithList", Typed[WithList]),
)
}

}

object ExpressionSuggesterTestData {
Expand Down
1 change: 1 addition & 0 deletions docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
* [#6935](https://github.com/TouK/nussknacker/pull/6935) Spel: Scenario labels added to meta variable - `#meta.scenarioLabels`
* [#6952](https://github.com/TouK/nussknacker/pull/6952) Improvement: TypeInformation support for scala.Option
* [#6840](https://github.com/TouK/nussknacker/pull/6840) Introduce canCastTo, castTo and castToOrNull extension methods in SpeL.
* [#6974](https://github.com/TouK/nussknacker/pull/6974) Add SpeL suggestions for cast methods parameter.

## 1.17

Expand Down
2 changes: 1 addition & 1 deletion docs/scenarios_authoring/Spel.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ When a type cannot be determined by parser, the type is presented as `Unknown`.
runtime, we can cast a given type, and then we can operate on the cast type.

E.g. having a variable `obj` of a type: `List[Unknown]` and we know the elements are strings then we can cast elements
to String: `#obj.![#this.castToOrNull('java.lang.String')]`.
to String: `#obj.![#this.castToOrNull('String')]`.

Available methods:
- `canCastTo` - checks if a type can be cast to a given class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import org.springframework.expression.spel.support.ReflectionHelper;
import org.springframework.expression.spel.support.ReflectiveMethodExecutor;
import org.springframework.util.ReflectionUtils;
import pl.touk.nussknacker.engine.spel.internal.RuntimeConversionHandler;
import pl.touk.nussknacker.engine.spel.internal.ConversionAndExtensionsAwareMethodInvoker;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
Expand All @@ -19,8 +19,6 @@
// As an additional feature we allow to invoke list methods on arrays and
// in the point of the method invocation we convert an array to a list
public class NuReflectiveMethodExecutor extends ReflectiveMethodExecutor {
private static final RuntimeConversionHandler.ConversionAwareMethodInvoker methodInvoker =
new RuntimeConversionHandler.ConversionAwareMethodInvoker();

private final Method method;

Expand All @@ -32,9 +30,10 @@ public class NuReflectiveMethodExecutor extends ReflectiveMethodExecutor {

private boolean argumentConversionOccurred = false;

private final ClassLoader classLoader;
private final ConversionAndExtensionsAwareMethodInvoker methodInvoker;

public NuReflectiveMethodExecutor(ReflectiveMethodExecutor original, ClassLoader classLoader) {
public NuReflectiveMethodExecutor(ReflectiveMethodExecutor original,
ConversionAndExtensionsAwareMethodInvoker methodInvoker) {
super(original.getMethod());
this.method = original.getMethod();
if (method.isVarArgs()) {
Expand All @@ -44,7 +43,7 @@ public NuReflectiveMethodExecutor(ReflectiveMethodExecutor original, ClassLoader
else {
this.varargsPosition = null;
}
this.classLoader = classLoader;
this.methodInvoker = methodInvoker;
}

/**
Expand Down Expand Up @@ -100,8 +99,8 @@ public TypedValue execute(EvaluationContext context, Object target, Object... ar
arguments = ReflectionHelper.setupArgumentsForVarargsInvocation(this.method.getParameterTypes(), arguments);
}
ReflectionUtils.makeAccessible(this.method);
//Nussknacker: we use custom method invoker which is aware of array conversion
Object value = methodInvoker.invoke(this.method, target, arguments, this.classLoader);
//Nussknacker: we use custom method invoker which is aware of array conversion and extension methods
Object value = methodInvoker.invoke(this.method, target, arguments);
return new TypedValue(value, new TypeDescriptor(new MethodParameter(this.method, -1)).narrow(value));
}
catch (Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package pl.touk.nussknacker.engine.spel.internal;

import pl.touk.nussknacker.engine.extension.Cast;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class ConversionAndExtensionAwareMethodsDiscovery {
private static final Method[] CAST_METHODS = Cast.class.getMethods();
private static final Method[] LIST_AND_CAST_METHODS = concatArrays(List.class.getMethods(), CAST_METHODS);

public Method[] discover(Class<?> type) {
// todo: lbg I'm planning to remove below branch for the sake of implementing array auto conversion to list as
// an extension methods added to array.
// Additionally appropriate extension methods should be added to classes automatically.
if (type.isArray()) {
return LIST_AND_CAST_METHODS;
}
return concatArrays(type.getMethods(), CAST_METHODS);
}

private static Method[] concatArrays(Method[] a, Method[] b) {
return Stream
.concat(Arrays.stream(a), Arrays.stream(b))
.toArray(Method[]::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pl.touk.nussknacker.engine.spel.internal;

import pl.touk.nussknacker.engine.extension.ExtensionMethodsInvoker;
import scala.PartialFunction;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;

public class ConversionAndExtensionsAwareMethodInvoker {
private final ExtensionMethodsInvoker extensionMethodsInvoker;

public ConversionAndExtensionsAwareMethodInvoker(ExtensionMethodsInvoker extensionMethodsInvoker) {
this.extensionMethodsInvoker = extensionMethodsInvoker;
}

public Object invoke(Method method,
Object target,
Object[] arguments) throws IllegalAccessException, InvocationTargetException {
Class<?> methodDeclaringClass = method.getDeclaringClass();
// todo: lbg I'm planning to remove below branch for the sake of implementing array auto conversion to list as
// an extension methods added to array
if (target != null && target.getClass().isArray() && methodDeclaringClass.isAssignableFrom(List.class)) {
return method.invoke(RuntimeConversionHandler.convert(target), arguments);
}
PartialFunction<Method, Object> extMethod = extensionMethodsInvoker.invoke(target, arguments);
if (extMethod.isDefinedAt(method)) {
return extMethod.apply(method);
} else {
return method.invoke(target, arguments);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,15 @@
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.lang.Nullable;
import pl.touk.nussknacker.engine.extension.Cast;
import pl.touk.nussknacker.engine.extension.ExtensionMethods;
import scala.PartialFunction;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

public class RuntimeConversionHandler {
public static final class ConversionAwareMethodsDiscovery {
private static final Method[] CAST_METHODS = Cast.class.getMethods();
private static final Method[] LIST_AND_CAST_METHODS = concatArrays(List.class.getMethods(), CAST_METHODS);

public Method[] discover(Class<?> type) {
if (type.isArray()) {
return LIST_AND_CAST_METHODS;
}
return concatArrays(type.getMethods(), CAST_METHODS);
}

private static Method[] concatArrays(Method[] a, Method[] b) {
return Stream
.concat(Arrays.stream(a), Arrays.stream(b))
.toArray(Method[]::new);
}
}

public static final class ConversionAwareMethodInvoker {

public Object invoke(Method method,
Object target,
Object[] arguments,
ClassLoader classLoader) throws IllegalAccessException, InvocationTargetException {
Class<?> methodDeclaringClass = method.getDeclaringClass();
if (target != null && target.getClass().isArray() && methodDeclaringClass.isAssignableFrom(List.class)) {
return method.invoke(RuntimeConversionHandler.convert(target), arguments);
}
PartialFunction<Class<?>, Object> extMethod =
ExtensionMethods.invoke(method, target, arguments, classLoader);
if (extMethod.isDefinedAt(methodDeclaringClass)) {
return extMethod.apply(methodDeclaringClass);
} else {
return method.invoke(target, arguments);
}
}
}

public static final class ArrayToListConverter implements ConditionalGenericConverter {

private final ConversionService conversionService;
Expand Down Expand Up @@ -100,7 +56,7 @@ public Object convert(

}

private static List<Object> convert(Object target) {
static List<Object> convert(Object target) {
if (target.getClass().getComponentType().isPrimitive()) {
int length = Array.getLength(target);
List<Object> result = new ArrayList<>(length);
Expand Down
Loading

0 comments on commit 353e063

Please sign in to comment.