diff --git a/prebuilt-tasks/pom.xml b/prebuilt-tasks/pom.xml index bdb8e4652..c940d1cf9 100644 --- a/prebuilt-tasks/pom.xml +++ b/prebuilt-tasks/pom.xml @@ -59,6 +59,10 @@ jira-rest-java-client-core ${jira-rest-client.version} + + org.springframework.boot + spring-boot-starter-jdbc + org.springframework.boot spring-boot-starter-mail diff --git a/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcService.java b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcService.java new file mode 100644 index 000000000..cd77e830c --- /dev/null +++ b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcService.java @@ -0,0 +1,14 @@ +package com.redhat.parodos.tasks.jdbc; + +import java.util.List; +import java.util.Map; + +interface JdbcService { + + List> query(String url, String statement); + + void update(String url, String statement); + + void execute(String url, String statement); + +} diff --git a/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcServiceImpl.java b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcServiceImpl.java new file mode 100644 index 000000000..c12aa478a --- /dev/null +++ b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcServiceImpl.java @@ -0,0 +1,34 @@ +package com.redhat.parodos.tasks.jdbc; + +import java.util.List; +import java.util.Map; + +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +class JdbcServiceImpl implements JdbcService { + + @Override + public List> query(String url, String statement) { + return createJdbcTemplate(url).query(statement, new ColumnMapRowMapper()); + } + + @Override + public void update(String url, String statement) { + createJdbcTemplate(url).update(statement); + } + + @Override + public void execute(String url, String statement) { + createJdbcTemplate(url).execute(statement); + } + + private JdbcTemplate createJdbcTemplate(String url) { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setUrl(url); + + return new JdbcTemplate(dataSource); + } + +} diff --git a/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcWorkFlowTask.java b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcWorkFlowTask.java new file mode 100644 index 000000000..330ee081b --- /dev/null +++ b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/JdbcWorkFlowTask.java @@ -0,0 +1,98 @@ +package com.redhat.parodos.tasks.jdbc; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.redhat.parodos.workflow.exception.MissingParameterException; +import com.redhat.parodos.workflow.parameter.WorkParameter; +import com.redhat.parodos.workflow.parameter.WorkParameterType; +import com.redhat.parodos.workflow.task.BaseWorkFlowTask; +import com.redhat.parodos.workflow.task.enums.WorkFlowTaskOutput; +import com.redhat.parodos.workflows.work.DefaultWorkReport; +import com.redhat.parodos.workflows.work.WorkContext; +import com.redhat.parodos.workflows.work.WorkReport; +import com.redhat.parodos.workflows.work.WorkStatus; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class JdbcWorkFlowTask extends BaseWorkFlowTask { + + enum OperationType { + + QUERY, UPDATE, EXECUTE + + } + + private JdbcService service; + + public JdbcWorkFlowTask() { + this.service = new JdbcServiceImpl(); + } + + JdbcWorkFlowTask(String beanName, JdbcService service) { + this.setBeanName(beanName); + this.service = service; + } + + @Override + public @NonNull List getWorkFlowTaskParameters() { + LinkedList params = new LinkedList<>(); + params.add(WorkParameter.builder().key("url").type(WorkParameterType.TEXT).optional(false).description( + "JDBC URL. E.g. jdbc:postgresql://localhost:5432/service?user=service&password=service123&ssl=true&sslmode=verify-full") + .build()); + params.add(WorkParameter.builder().key("operation").type(WorkParameterType.TEXT).optional(false).description( + "Type of operation this statement is performing. One of " + Arrays.toString(OperationType.values())) + .build()); + params.add(WorkParameter.builder().key("statement").type(WorkParameterType.TEXT).optional(false) + .description("The database statement to execute. E.g. 'select * from table'").build()); + params.add(WorkParameter.builder().key("result-ctx-key").type(WorkParameterType.TEXT).optional(true) + .description("In query operation the result is stored in WorkContext with the provided key").build()); + return params; + } + + @Override + public @NonNull List getWorkFlowTaskOutputs() { + return List.of(WorkFlowTaskOutput.OTHER); + } + + @Override + public WorkReport execute(WorkContext workContext) { + String url = ""; + try { + url = getRequiredParameterValue("url"); + String operation = getRequiredParameterValue("operation"); + String statement = getRequiredParameterValue("statement"); + String resultCtxKey = getOptionalParameterValue("result-ctx-key", ""); + + OperationType operationType = OperationType.valueOf(operation.toUpperCase()); + + switch (operationType) { + + case QUERY -> { + List> result = this.service.query(url, statement); + if (!resultCtxKey.isEmpty()) { + workContext.put(resultCtxKey, new ObjectMapper().writeValueAsString(result)); + } + } + case UPDATE -> { + this.service.update(url, statement); + } + case EXECUTE -> { + this.service.execute(url, statement); + } + } + } + catch (MissingParameterException | JsonProcessingException e) { + log.error("Jdbc task failed for URL " + url, e); + return new DefaultWorkReport(WorkStatus.FAILED, workContext, e); + } + + return new DefaultWorkReport(WorkStatus.COMPLETED, workContext); + } + +} diff --git a/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/README.md b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/README.md new file mode 100644 index 000000000..a335688fa --- /dev/null +++ b/prebuilt-tasks/src/main/java/com/redhat/parodos/tasks/jdbc/README.md @@ -0,0 +1,22 @@ +# JDBC WorkFlow task + +## Motivation +Provide a task that can be used for CRUD operations via JDBC + +## Input +- URL - a JDBC URL that has all required connection properties. E.g. jdbc:postgresql://localhost:5432/service?user=service&password=service123&ssl=true&sslmode=verify-full. +- Statement - the textual message/request/query that is passed to the JDBC driver. E.g. "select * from items". +- Operation type - operations are divided into 3 types. Execute is for statements such as 'drop table', 'create user'. Update is for statements such as insert, update, delete records (e.g. rows in table). Query is for retrieving records/items/info. E.g. SQL select. +- Context key for result - the context key where result of query operation is stored. + +## Result +A failure to perform the operation will result in failed task execution. Only operation of type query returns a result. The result is stored with the context key that is passed as input. The result is a JSON representation of the result set. + +## Configuration +The task uses the JDBC driver specified in the URL. E.g. mysql. In order to load the driver you need to add it as a maven dependency. E.g. +``` + + mysql + mysql-connector-java + runtime + \ No newline at end of file diff --git a/prebuilt-tasks/src/test/java/com/redhat/parodos/tasks/jdbc/JdbcWorkFlowTaskTest.java b/prebuilt-tasks/src/test/java/com/redhat/parodos/tasks/jdbc/JdbcWorkFlowTaskTest.java new file mode 100644 index 000000000..f9f555035 --- /dev/null +++ b/prebuilt-tasks/src/test/java/com/redhat/parodos/tasks/jdbc/JdbcWorkFlowTaskTest.java @@ -0,0 +1,83 @@ +package com.redhat.parodos.tasks.jdbc; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.redhat.parodos.workflow.context.WorkContextDelegate; +import com.redhat.parodos.workflow.exception.MissingParameterException; +import com.redhat.parodos.workflows.work.WorkContext; +import com.redhat.parodos.workflows.work.WorkReport; +import com.redhat.parodos.workflows.work.WorkStatus; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class JdbcWorkFlowTaskTest { + + private final JdbcService service = mock(JdbcService.class); + + private final JdbcWorkFlowTask task = new JdbcWorkFlowTask("Test", service); + + @Test + public void missingArgs() { + WorkContext ctx = createWorkContext(null); + task.preExecute(ctx); + WorkReport result = task.execute(ctx); + assertEquals(WorkStatus.FAILED, result.getStatus()); + assertEquals(MissingParameterException.class, result.getError().getClass()); + } + + @Test + public void query() { + WorkContext ctx = createWorkContext(JdbcWorkFlowTask.OperationType.QUERY.name()); + List> resultSet = List.of(Map.of("name", "value")); + String resultJson = "[{\"name\":\"value\"}]"; + doReturn(resultSet).when(service).query(any(), any()); + task.preExecute(ctx); + WorkReport result = task.execute(ctx); + assertEquals(WorkStatus.COMPLETED, result.getStatus()); + assertEquals(resultJson, ctx.get("thekey")); + } + + @Test + public void update() { + WorkContext ctx = createWorkContext(JdbcWorkFlowTask.OperationType.UPDATE.name()); + task.preExecute(ctx); + WorkReport result = task.execute(ctx); + assertEquals(WorkStatus.COMPLETED, result.getStatus()); + verify(service, times(1)).update("theurl", "thestatement"); + } + + @Test + public void execute() { + WorkContext ctx = createWorkContext(JdbcWorkFlowTask.OperationType.EXECUTE.name()); + task.preExecute(ctx); + WorkReport result = task.execute(ctx); + assertEquals(WorkStatus.COMPLETED, result.getStatus()); + verify(service, times(1)).execute("theurl", "thestatement"); + } + + private WorkContext createWorkContext(String operation) { + WorkContext ctx = new WorkContext(); + HashMap map = new HashMap<>(); + map.put("url", "theurl"); + + if (operation != null) { + map.put("operation", operation); + } + + map.put("statement", "thestatement"); + map.put("result-ctx-key", "thekey"); + + WorkContextDelegate.write(ctx, WorkContextDelegate.ProcessType.WORKFLOW_TASK_EXECUTION, task.getName(), + WorkContextDelegate.Resource.ARGUMENTS, map); + return ctx; + } + +} diff --git a/workflow-examples/src/main/java/com/redhat/parodos/examples/JdbcWorkFlowConfiguration.java b/workflow-examples/src/main/java/com/redhat/parodos/examples/JdbcWorkFlowConfiguration.java new file mode 100644 index 000000000..800bd7931 --- /dev/null +++ b/workflow-examples/src/main/java/com/redhat/parodos/examples/JdbcWorkFlowConfiguration.java @@ -0,0 +1,27 @@ +package com.redhat.parodos.examples; + +import com.redhat.parodos.tasks.jdbc.JdbcWorkFlowTask; +import com.redhat.parodos.workflow.annotation.Infrastructure; +import com.redhat.parodos.workflows.workflow.SequentialFlow; +import com.redhat.parodos.workflows.workflow.WorkFlow; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("jdbc") +public class JdbcWorkFlowConfiguration { + + @Bean + JdbcWorkFlowTask jdbcTask() { + return new JdbcWorkFlowTask(); + } + + @Bean + @Infrastructure + WorkFlow jdbcWorkFlow(JdbcWorkFlowTask jdbcTask) { + return SequentialFlow.Builder.aNewSequentialFlow().named("jdbcWorkFlow").execute(jdbcTask).build(); + } + +} diff --git a/workflow-examples/src/test/java/com/redhat/parodos/examples/jdbc/JdbcWorkFlow.java b/workflow-examples/src/test/java/com/redhat/parodos/examples/jdbc/JdbcWorkFlow.java new file mode 100644 index 000000000..170c20297 --- /dev/null +++ b/workflow-examples/src/test/java/com/redhat/parodos/examples/jdbc/JdbcWorkFlow.java @@ -0,0 +1,118 @@ +package com.redhat.parodos.examples.jdbc; + +import java.util.Arrays; +import java.util.List; + +import com.redhat.parodos.sdk.api.WorkflowApi; +import com.redhat.parodos.sdk.api.WorkflowDefinitionApi; +import com.redhat.parodos.sdk.invoker.ApiClient; +import com.redhat.parodos.sdk.invoker.Configuration; +import com.redhat.parodos.sdk.model.ArgumentRequestDTO; +import com.redhat.parodos.sdk.model.ProjectResponseDTO; +import com.redhat.parodos.sdk.model.WorkFlowDefinitionResponseDTO; +import com.redhat.parodos.sdk.model.WorkFlowExecutionResponseDTO; +import com.redhat.parodos.sdk.model.WorkFlowRequestDTO; +import com.redhat.parodos.sdk.model.WorkRequestDTO; +import com.redhat.parodos.workflow.utils.CredUtils; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; + +import static com.redhat.parodos.sdkutils.SdkUtils.getProjectAsync; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/* + * JDBC example with MySQL + * After installing a MySQL server it listens on port 3306 for non-secure/non-encrypted connections + * We show a simple SQL SELECT from the server's predefined tables. + * You need to create a user 'test' with password 'test'. Use the following commands in mysql prompt: + * create user 'test'@'localhost' identified by 'test' + * grant all privileges on *.* to 'test'@'localhost' + * You will also need to add the MySQL JDBC driver as a Maven dependency + */ +public class JdbcWorkFlow { + + private final String projectName = "project-1"; + + private final String projectDescription = "Jdbc example project"; + + private final String workflowName = "jdbcWorkFlow"; + + private final String taskName = "jdbcTask"; + + @Test + public void runFlow() { + ApiClient defaultClient = Configuration.getDefaultApiClient(); + + defaultClient.addDefaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + CredUtils.getBase64Creds("test", "test")); + + try { + ProjectResponseDTO testProject = getProjectAsync(defaultClient, projectName, projectDescription); + + // GET workflow DEFINITIONS + WorkflowDefinitionApi workflowDefinitionApi = new WorkflowDefinitionApi(defaultClient); + List simpleSequentialWorkFlowDefinitions = workflowDefinitionApi + .getWorkFlowDefinitions(workflowName); + assertEquals(1, simpleSequentialWorkFlowDefinitions.size()); + + // GET WORKFLOW DEFINITION BY Id + WorkFlowDefinitionResponseDTO simpleSequentialWorkFlowDefinition = workflowDefinitionApi + .getWorkFlowDefinitionById(simpleSequentialWorkFlowDefinitions.get(0).getId()); + + // EXECUTE WORKFLOW + WorkflowApi workflowApi = new WorkflowApi(); + + // 1 - Define WorkRequests + WorkRequestDTO workCreateDB = new WorkRequestDTO().workName(taskName) + .arguments(Arrays.asList( + new ArgumentRequestDTO().key("url") + .value("jdbc:mysql://localhost:3306/?user=test&password=test"), + new ArgumentRequestDTO().key("operation").value("execute"), + new ArgumentRequestDTO().key("statement").value("create database service"))); + WorkRequestDTO workCreateTable = new WorkRequestDTO().workName(taskName) + .arguments(Arrays.asList( + new ArgumentRequestDTO().key("url") + .value("jdbc:mysql://localhost:3306/service?user=test&password=test"), + new ArgumentRequestDTO().key("operation").value("execute"), + new ArgumentRequestDTO().key("statement").value("create table items (id VARCHAR(255))"))); + WorkRequestDTO workInsert = new WorkRequestDTO().workName(taskName) + .arguments(Arrays.asList( + new ArgumentRequestDTO().key("url") + .value("jdbc:mysql://localhost:3306/service?user=test&password=test"), + new ArgumentRequestDTO().key("operation").value("update"), + new ArgumentRequestDTO().key("statement").value("insert into items values('item-test')"))); + WorkRequestDTO workSelect = new WorkRequestDTO().workName(taskName) + .arguments(Arrays.asList( + new ArgumentRequestDTO().key("url") + .value("jdbc:mysql://localhost:3306/service?user=test&password=test"), + new ArgumentRequestDTO().key("operation").value("query"), + new ArgumentRequestDTO().key("statement").value("select * from items"), + new ArgumentRequestDTO().key("result-ctx-key").value("theresult"))); + + WorkFlowRequestDTO workFlowRequestDb = new WorkFlowRequestDTO().projectId(testProject.getId()) + .workFlowName(workflowName).works(Arrays.asList(workCreateDB)); + WorkFlowRequestDTO workFlowRequestTable = new WorkFlowRequestDTO().projectId(testProject.getId()) + .workFlowName(workflowName).works(Arrays.asList(workCreateTable)); + WorkFlowRequestDTO workFlowRequestInsert = new WorkFlowRequestDTO().projectId(testProject.getId()) + .workFlowName(workflowName).works(Arrays.asList(workInsert)); + WorkFlowRequestDTO workFlowRequestSelect = new WorkFlowRequestDTO().projectId(testProject.getId()) + .workFlowName(workflowName).works(Arrays.asList(workSelect)); + + WorkFlowRequestDTO workFlowRequests[] = new WorkFlowRequestDTO[] { workFlowRequestDb, workFlowRequestTable, + workFlowRequestInsert, workFlowRequestSelect }; + + // 3 - Execute WorkFlowRequests + for (WorkFlowRequestDTO workFlowRequest : workFlowRequests) { + WorkFlowExecutionResponseDTO execute = workflowApi.execute(workFlowRequest); + + assertNotNull(execute.getWorkFlowExecutionId()); + } + } + catch (Exception e) { + fail("Execution of jdbc workflow failed with error: " + e.getMessage()); + } + } + +}