Skip to content

Commit

Permalink
[KOGITO-9354] Python service (#3079)
Browse files Browse the repository at this point in the history
* [KOGITO-9354] Python service

* [KOGITO-9354] Adding unit and integrationt test

* [KOGITO-9354] Adding search path configuration

* Update api/kogito-api/src/main/java/org/kie/kogito/internal/utils/ConversionUtils.java

Co-authored-by: Marián Macik <macik.marian@gmail.com>

* Update api/kogito-api/src/main/java/org/kie/kogito/internal/utils/ConversionUtils.java

Co-authored-by: Marián Macik <macik.marian@gmail.com>

* [KOGITO-9354] Renaming property

* [KOGITO-9354] Mariams comments

---------

Co-authored-by: Marián Macik <macik.marian@gmail.com>
  • Loading branch information
fjtirado and MarianMacik committed Jun 26, 2023
1 parent 8ee9f46 commit a219c4b
Show file tree
Hide file tree
Showing 26 changed files with 452 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -34,6 +38,34 @@ public static <T> T convert(Object value, Class<T> clazz) {
return convert(value, clazz, Object::toString);
}

/**
* Converts a string into a list of objects using `,` as a separator
*
* @param <T>
* @param value object to be converted into list
* @param clazz the item target class
* @return a collection
*/
public static <T> Collection<T> convertToCollection(Object value, Class<T> clazz) {
return convertToCollection(value, clazz, ",");
}

/**
* Converts a string into a list of objects
*
* @param <T>
* @param value object to be converted into list
* @param clazz the item target class
* @param separator the separator of values
* @return a collection
*/
public static <T> Collection<T> convertToCollection(Object value, Class<T> clazz, String separator) {
if (value == null) {
return Collections.emptyList();
}
return Arrays.stream(value.toString().split(separator)).map(v -> ConversionUtils.convert(v, clazz)).collect(Collectors.toList());
}

/**
* Converts an object to an instance of the provided class
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
package org.kie.kogito.internal.utils;

import java.util.Arrays;
import java.util.Objects;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.kie.kogito.internal.utils.ConversionUtils.concatPaths;
import static org.kie.kogito.internal.utils.ConversionUtils.convert;
import static org.kie.kogito.internal.utils.ConversionUtils.convertToCollection;
import static org.kie.kogito.internal.utils.ConversionUtils.toCamelCase;

class ConversionUtilsTest {
Expand Down Expand Up @@ -127,6 +129,10 @@ public void testConcatPaths() {
assertThat(concatPaths("http:localhost:8080/pepe", "pepa/pepi")).isEqualTo(expected);
assertThat(concatPaths("http:localhost:8080/pepe/", "pepa/pepi")).isEqualTo(expected);
assertThat(concatPaths("http:localhost:8080/pepe", "/pepa/pepi")).isEqualTo(expected);
}

@Test
public void testConvertToCollection() {
assertThat(convertToCollection("1,2,3", Integer.class)).isEqualTo(Arrays.asList(1, 2, 3));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.serverlessworkflow.api.functions.FunctionRef;

import static org.kie.kogito.serverless.workflow.SWFConstants.PYTHON;
import static org.kie.kogito.serverless.workflow.SWFConstants.PYTHON_SCRIPT;
import static org.kie.kogito.serverless.workflow.SWFConstants.SCRIPT;
import static org.kie.kogito.serverless.workflow.parser.FunctionTypeHandlerFactory.trimCustomOperation;
import static org.kie.kogito.serverless.workflow.parser.handlers.ActionNodeUtils.actionNode;
Expand All @@ -49,7 +50,7 @@ public String type() {
if (PYTHON.equalsIgnoreCase(lang)) {
return addFunctionArgs(workflow,
buildWorkItem(embeddedSubProcess, context, varInfo.getInputVar(), varInfo.getOutputVar()).name(functionDef.getName()),
functionRef).workName(PYTHON);
functionRef).workName(PYTHON_SCRIPT);
} else {
return actionNode(embeddedSubProcess, context, functionDef).action(JavaDialect.ID,
functionRef.getArguments().get(SCRIPT).asText());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,22 @@
import io.serverlessworkflow.api.functions.FunctionDefinition;
import io.serverlessworkflow.api.functions.FunctionRef;

import static org.kie.kogito.serverless.workflow.SWFConstants.JAVA;
import static org.kie.kogito.serverless.workflow.SWFConstants.PYTHON;
import static org.kie.kogito.serverless.workflow.SWFConstants.PYTHON_SVC;
import static org.kie.kogito.serverless.workflow.SWFConstants.SERVICE_IMPL_KEY;
import static org.kie.kogito.serverless.workflow.SWFConstants.SERVICE_TASK_TYPE;
import static org.kie.kogito.serverless.workflow.SWFConstants.WORKITEM_INTERFACE;
import static org.kie.kogito.serverless.workflow.SWFConstants.WORKITEM_INTERFACE_IMPL;
import static org.kie.kogito.serverless.workflow.SWFConstants.WORKITEM_OPERATION;
import static org.kie.kogito.serverless.workflow.SWFConstants.WORKITEM_OPERATION_IMPL;
import static org.kie.kogito.serverless.workflow.parser.FunctionTypeHandlerFactory.trimCustomOperation;
import static org.kie.kogito.serverless.workflow.utils.ServerlessWorkflowUtils.resolveFunctionMetadata;

public class ServiceTypeHandler extends WorkItemTypeHandler {

public static final String SERVICE_TYPE = "service";
public static final String SERVICE_TASK_TYPE = "Service Task";
public static final String WORKITEM_INTERFACE = "Interface";
public static final String WORKITEM_OPERATION = "Operation";
public static final String INTFC_SEPARATOR = "::";
public static final String SERVICE_IMPL_KEY = "implementation";
public static final String WORKITEM_INTERFACE_IMPL = "interfaceImplementationRef";
public static final String WORKITEM_OPERATION_IMPL = "operationImplementationRef";
private static final String WORKITEM_PARAM_TYPE = "ParameterType";

private static final String LANG_SEPARATOR = ":";
Expand Down Expand Up @@ -79,15 +82,23 @@ public class ServiceTypeHandler extends WorkItemTypeHandler {
}
if (lang == null) {
lang = resolveFunctionMetadata(
functionDef, SERVICE_IMPL_KEY, context.getContext(), String.class, "Java");
functionDef, SERVICE_IMPL_KEY, context.getContext(), String.class, JAVA);
}
switch (lang) {
case PYTHON:
node.workName(PYTHON_SVC);
break;
case JAVA:
default:
node.workName(SERVICE_TASK_TYPE);
break;
}
return node.workParameter(WORKITEM_INTERFACE, intfc)
.workParameter(WORKITEM_OPERATION, method)
.workParameter(WORKITEM_INTERFACE_IMPL, intfc)
.workParameter(WORKITEM_OPERATION_IMPL, method)
.workParameter(SERVICE_IMPL_KEY, lang)
.metaData(TaskDescriptor.KEY_WORKITEM_TYPE, SERVICE_TASK_TYPE)
.workName(SERVICE_TASK_TYPE);
.metaData(TaskDescriptor.KEY_WORKITEM_TYPE, SERVICE_TASK_TYPE);
}

@Override
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,29 @@
*/
package org.kie.kogito.serverless.workflow.executor;

import org.kie.kogito.serverless.workflow.python.PythonWorkItemHandler;
import org.kie.kogito.serverless.workflow.python.PythonScriptWorkItemHandler;
import org.kie.kogito.serverless.workflow.python.PythonServiceWorkItemHandler;

public class StaticPythonScriptRegister implements StaticApplicationRegister {

private PythonWorkItemHandler wih;
private PythonScriptWorkItemHandler scriptWIH;
private PythonServiceWorkItemHandler svcWIH;

@Override
public void register(StaticWorkflowApplication application) {
wih = new PythonWorkItemHandler();
application.registerHandler(wih);
scriptWIH = new PythonScriptWorkItemHandler();
application.registerHandler(scriptWIH);
svcWIH = new PythonServiceWorkItemHandler();
application.registerHandler(svcWIH);
}

@Override
public void close() {
if (wih != null) {
wih.close();
if (scriptWIH != null) {
scriptWIH.close();
}
if (svcWIH != null) {
svcWIH.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@

import org.junit.jupiter.api.Test;
import org.kie.kogito.serverless.workflow.fluent.ActionBuilder.ScriptType;
import org.kie.kogito.serverless.workflow.python.PythonWorkItemHandlerUtils;

import com.fasterxml.jackson.databind.node.TextNode;

import io.serverlessworkflow.api.Workflow;

import static org.assertj.core.api.Assertions.assertThat;
import static org.kie.kogito.serverless.workflow.fluent.ActionBuilder.call;
import static org.kie.kogito.serverless.workflow.fluent.ActionBuilder.script;
import static org.kie.kogito.serverless.workflow.fluent.StateBuilder.*;
import static org.kie.kogito.serverless.workflow.fluent.FunctionBuilder.python;
import static org.kie.kogito.serverless.workflow.fluent.StateBuilder.operation;
import static org.kie.kogito.serverless.workflow.fluent.WorkflowBuilder.jsonObject;
import static org.kie.kogito.serverless.workflow.fluent.WorkflowBuilder.workflow;

Expand All @@ -48,4 +53,22 @@ void testPythonWithArgs() {
assertThat(application.execute(workflow, Map.of("x", 2)).getWorkflowdata().get("result").asInt()).isEqualTo(4);
}
}

@Test
void testPythonService() {
try (StaticWorkflowApplication application = StaticWorkflowApplication.create()) {
Workflow workflow = workflow("Factorial").start(operation().action(call(python("factorial", "math", "factorial"), new TextNode(".x")).outputFilter(".result")))
.end().build();
assertThat(application.execute(workflow, Map.of("x", 5)).getWorkflowdata().get("result").asInt()).isEqualTo(120);
}
}

@Test
void testNotStandardPythonService() {
try (StaticWorkflowApplication application = StaticWorkflowApplication.create(Map.of(PythonWorkItemHandlerUtils.SEARCH_PATH_PROPERTY, "./src/test/resources/"))) {
Workflow workflow = workflow("Factorial").start(operation().action(call(python("factorial", "custom", "factorial"), new TextNode(".x")).outputFilter(".result")))
.end().build();
assertThat(application.execute(workflow, Map.of("x", 5)).getWorkflowdata().get("result").asInt()).isEqualTo(120);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def factorial(x):
result = 1
if x > 1:
for i in range(2,x+1):
result = result*i
return result
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,36 @@
*/
package org.kie.kogito.serverless.workflow.executor;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jbpm.compiler.canonical.ReflectionUtils;
import org.kie.kogito.serverless.workflow.ServiceWorkItemHandler;

import static org.kie.kogito.serverless.workflow.SWFConstants.SERVICE_TASK_TYPE;

public class StaticServiceWorkItemHandler extends ServiceWorkItemHandler {

@Override
protected Object invoke(String className, String methodName, Object... parameters) throws ReflectiveOperationException {
ClassLoader cls = Thread.currentThread().getContextClassLoader();
Class<?> clazz = cls.loadClass(className);
return ReflectionUtils.getMethod(cls, clazz, methodName, Stream.of(parameters).map(Object::getClass).map(Class::getName).collect(Collectors.toList())).invoke(getInstance(clazz), parameters);
protected Object invoke(String className, String methodName, Object parameters) {
try {
ClassLoader cls = Thread.currentThread().getContextClassLoader();
Class<?> clazz = cls.loadClass(className);
Object[] args = parameters instanceof Map ? ((Map<String, Object>) parameters).values().toArray() : new Object[] { parameters };
return ReflectionUtils.getMethod(cls, clazz, methodName, Stream.of(args).map(Object::getClass).map(Class::getName).collect(Collectors.toList())).invoke(getInstance(clazz),
args);
} catch (ReflectiveOperationException ex) {
throw new IllegalStateException(ex);
}
}

protected Object getInstance(Class<?> clazz) throws ReflectiveOperationException {
return clazz.getConstructor().newInstance();
}

@Override
public String getName() {
return SERVICE_TASK_TYPE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Objects;
import java.util.function.Function;

import org.kie.kogito.serverless.workflow.SWFConstants;
import org.kie.kogito.serverless.workflow.actions.WorkflowLogLevel;
import org.kie.kogito.serverless.workflow.functions.FunctionDefinitionEx;
import org.kie.kogito.serverless.workflow.parser.types.ServiceTypeHandler;
Expand Down Expand Up @@ -59,13 +60,21 @@ public static FunctionBuilder expr(String name, String expression) {
return new FunctionBuilder(new FunctionDefinition(name).withType(Type.EXPRESSION).withOperation(expression));
}

public static <T, V> FunctionBuilder java(String name, Function<T, V> function) {
return new FunctionBuilder(new FunctionDefinitionEx<T, V>(name).withFunction(function).withType(Type.CUSTOM).withOperation("java"));
public static <T, V> FunctionBuilder java(String funcName, Function<T, V> function) {
return new FunctionBuilder(new FunctionDefinitionEx<T, V>(funcName).withFunction(function).withType(Type.CUSTOM).withOperation("java"));
}

public static FunctionBuilder java(String name, String className, String methodName) {
public static FunctionBuilder java(String funcName, String className, String methodName) {
return service(funcName, SWFConstants.JAVA, className, methodName);
}

public static FunctionBuilder python(String funcName, String moduleName, String methodName) {
return service(funcName, SWFConstants.PYTHON, moduleName, methodName);
}

private static FunctionBuilder service(String name, String langName, String moduleName, String methodName) {
return new FunctionBuilder(new FunctionDefinition(name).withType(Type.CUSTOM)
.withOperation(ServiceTypeHandler.SERVICE_TYPE + CUSTOM_TYPE_SEPARATOR + className + ServiceTypeHandler.INTFC_SEPARATOR + methodName));
.withOperation(ServiceTypeHandler.SERVICE_TYPE + CUSTOM_TYPE_SEPARATOR + langName + CUSTOM_TYPE_SEPARATOR + moduleName + ServiceTypeHandler.INTFC_SEPARATOR + methodName));
}

public static FunctionBuilder log(String name, WorkflowLogLevel level) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class PythonContextResolver implements KogitoProcessContextResolverExtens

@Override
public Map<String, Function<KogitoProcessContext, Object>> getKogitoProcessContextResolver() {
return Map.of(SWFConstants.PYTHON, k -> new FunctionJsonNode(PythonWorkItemHandler::getValue));
return Map.of(SWFConstants.PYTHON, k -> new FunctionJsonNode(PythonWorkItemHandlerUtils::getValue));
}

}
Loading

0 comments on commit a219c4b

Please sign in to comment.