Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NU-1701] Casts suggester #6974

Merged
merged 5 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')]`.
mk-software-pl marked this conversation as resolved.
Show resolved Hide resolved

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) {
mk-software-pl marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
mk-software-pl marked this conversation as resolved.
Show resolved Hide resolved
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)) {
mk-software-pl marked this conversation as resolved.
Show resolved Hide resolved
return method.invoke(RuntimeConversionHandler.convert(target), arguments);
mk-software-pl marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading