diff --git a/.gitignore b/.gitignore index ed80467..2eef251 100644 --- a/.gitignore +++ b/.gitignore @@ -1,42 +1,13 @@ -# Created by .ignore support plugin (hsz.mobi) -### Maven template +Thumbs.db +.DS_Store +.gradle +build/ target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -.mvn/wrapper/maven-wrapper.jar - -### Java template -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -.idea/* +out/ +.idea *.iml - -tmp/* \ No newline at end of file +*.ipr +*.iws +.project +.settings +.classpath diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a1b5c32 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: java + +jdk: openjdk8 + +script: + - ./gradlew clean build + +sudo: false + +install: true + +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2097541 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM adoptopenjdk/openjdk11-openj9:jdk-11.0.1.13-alpine-slim +COPY build/libs/quintessential-tasklist-zeebe-*-all.jar quintessential-tasklist-zeebe.jar +EXPOSE 8080 +CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar quintessential-tasklist-zeebe.jar \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..820eae4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 https://github.com/StephenOTT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e8a6fa0..7da5810 100644 --- a/README.md +++ b/README.md @@ -1,711 +1,169 @@ # quintessential-tasklist-zeebe The quintessential Zeebe tasklist for BPMN Human tasks with Drag and Drop Form builder, client and server side validations, and drop in Form Rendering -WIP +KOTLIN -Setup SLF4J logging: `-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory` -vertx run command: `run com.github.stephenott.MainVerticle -conf src/main/java/com/github/stephenott/conf/conf.json` -Current Zeebe Version: `0.21.0-alpha1` -Current Vertx Version: `3.8.0` -Java: `1.8` +## Workflow Linter +The workflow linter provides a linting/validation engine for BPMN workflows that are parsed by the Zeebe Model API. -# Cluster Architecture +The linter acts as a warning and error system allowing you to validate workflows during the modeling process, and you can +implement the linter at deployment time, so when deploying a workflow into the Zeebe Cluster, the deployment will be stopped if a model has warnings or errors defined in the linter rules. -![cluster-arch](./docs/design/cluster.png) +### Linter Rules -- Clients, Workers, and Executors can be added at startup and during runtime. -- Failed nodes in the Vertx Cluster (Clients, Workers, and Executors) will be re-instantiated through the vertx cluster manager's configuration. - - -# Form Building UI - -The Form Builder UI uses Formio.js as the Builder and Render. -The schema that was generated from the builder is persisted and used during the User Task Submission with Form flow. - -![builder1](./docs/design/form/FormBuilder1-build.png) - -![builder2](./docs/design/form/FormBuilder2-build.png) - -![builder3](./docs/design/form/FormBuilder3-build.png) - - -And then you can render and make a submission: - -![builder4](./docs/design/form/FormBuilder4-render.png) - - -Try out the builder on: https://formio.github.io/formio.js/app/builder - - -## User Task Submission with Form Data flow - -![dataflow](./docs/design/form/User-Task-Form-Completion-Flow.png) - - -# ZeebeClient/Worker/Executor Data Flow - -![data flow](./docs/design/dataflow.png) - - -# Configuration - -Extensive configuration capabilities are provided to control the exact setup of your application: - -The Yaml location can be configured through the applications config.json. Default is `./zeebe.yml`. - -Example: +1. Rules are additive. One rule cannot cancel out another rule. +1. Rules require a description, elementTypes, and a rule implementation. +1. Element Types: `ServiceTask`, `ReceiveTask`.... (more to come) +1. Rules can apply to multiple element types, and have various targeting rules. +1. the `Target` property defines "targeting rules" that applied to the rule. Targeting rules define when the rule should be applied for the specific Element Type. +1. See the User Task example below for common usage: "Only target ServiceTasks with a task type of `user-task`. This means the rule will only apply when a Service Task defines a type of `user-task` ```yaml -zeebe: - clients: - - name: MyCustomClient - brokerContactPoint: "localhost:25600" - requestTimeout: PT20S - workers: - - name: SimpleScriptWorker - jobTypes: - - type1 - timeout: PT10S - - name: UT-Worker - jobTypes: - - ut.generic - timeout: P1D - -executors: - - name: Script-Executor - address: "type1" - execute: ./scripts/script1.js - - name: CommonGenericExecutor - address: commonExecutor - execute: classpath:com.custom.executors.Executor1 - - name: IpBlocker - address: block-ip - execute: ./cyber/BlockIP.py - -userTaskExecutors: - - name: GenericUserTask - address: ut.generic - -managementServer: - enabled: true - apiRoot: server1 - corsRegex: ".*." - port: 8080 - instances: 1 - zeebeClient: - name: DeploymentClient - brokerContactPoint: "localhost:25600" - requestTimeout: PT10S - -formValidatorServer: - enabled: true - corsRegex: ".*." - port: 8082 - instances: 1 - formValidatorService: - host: localhost - port: 8083 - validateUri: /validate - requestTimeout: 5000 - -userTaskServer: - enabled: true - corsRegex: ".*." - port: 8080 - instances: 1 -``` - -# Zeebe Clients - -A Zeebe Client is a gRPC channel to a specific Zeebe Cluster. - -A client maintains a set of "Job Workers", which are long polling the Zeebe Cluster for Zeebe Jobs that have a `type` listed in the `jobTypes` array. - -Zeebe Clients have the following configuration: - -```yaml -zeebe: - clients: - - name: MyCustomClient - brokerContactPoint: "localhost:25600" - requestTimeout: PT20S - workers: - - name: SimpleScriptWorker - jobTypes: - - type1 - timeout: PT10S - - name: UT-Worker - jobTypes: - - ut.generic - timeout: P1D -``` - -Where `name` is the name of the client. The same name could be used by multiple clients in the same server or by other servers. The `name` is used as the `zeebeSource` in Executors and User Task Executors as the source system to send completed/failed Zeebe Jobs back to. - -Where `workers` is a array of Zeebe Worker definitions. A worker definition has a `name` and a list of `jobTypes`. - -`name` is the worker name that is provided to Zeebe as the worker that requested the job. - -`jobTypes` is the lsit of Zeebe job `types` that will be queried for using long polling. - -Jobs that are retrieved will be routed to the event bus using the address: `job.:jobType:`, where `:jobType:` is the specific Zeebe job's `type` property. -Make sure you have executors (Polyglot, User Task or custom) on the network connected to the vertx cluster or else the job will not be consumed by a worker. - - Take note of the usage of the `timeout` which is the deadline that the job will be locked for. - The usage has special applicability for Jobs that you want to use with User Task; where you will want to set the timeout as a much longer period than a typical executor. - - -# Executors - -Executors provide a polyglot execution solution for completing Zeebe Jobs. - -Executors have the following configuration: -Example of three different executors: - -```yaml -executors: - - name: Script-Executor - address: "type1" - execute: ./scripts/script1.js - instances: 2 - - name: CommonGenericExecutor - address: commonExecutor - execute: classpath:com.custom.executors.Executor1 - - name: IpBlocker - address: block-ip - execute: ./cyber/BlockIP.py -``` - -Executors can execute scripts and classes as defined in the executor's polyglot capabilities. +orchestrator: + workflow-linter: + rules: + global-rules: + enable: false + description: Global Restrictions for Service Task Types + elementTypes: + - ServiceTask + serviceTaskRule: + allowedTypes: + - some-type + - user-task + + user-task-rule: + description: Specific rule for User Task Configuration of Service Tasks + elementTypes: + - ServiceTask + target: + serviceTasks: + types: + - user-task + headerRule: + requiredKeys: + - title + - candidateGroups + - formKey + allowedNonDefinedKeys: false + allowedDuplicateKeys: false + optionalKeys: + - priority + - assignee + - candidateUsers + - dueDate + - description +``` + + +Responses can be of a WARNING or ERROR + +``` +Element ---> serviceTask ServiceTask_1luzsfd +Type: ERROR +Code: 0 +Element Type: serviceTask +Element Id: ServiceTask_1luzsfd +Message: Missing Required Headers: [title, candidateGroups, formKey] + +Type: ERROR +Code: 0 +Element Type: serviceTask +Element Id: ServiceTask_1luzsfd +Message: Found headers that are not part of Optional Headers list: [priority, assignee, candidateUsers, dueDate, description] +``` + +### Global Rules + +If the `target` property configuration is **not** provided, then the rule will be applied globally to all Element Types defined in the rule. + +Global rules can be a good way to implement some restrictions on your modeling teams to ensure that internal types and correlation keys are not used. + +Global rules can be valuable naming conventions as well: "task types cannot start with a underscore `_`." + +### Element Type Rules (WIP) + +Working model is to have rule factories for each major Element Type (Service Task, Receive Task, Catch message, etc). + +Element Type Rules provide specific rule implementations that focus on the Element Type's configuration possibilities: + +1. Service Task: + 1. Allowed Types + regex limit + 1. Allowed Retry Regex + 1. Allowed IO Mappings + 1. Allowed Headers (Required, Optional, duplicates, Key Value Pairs, Non-Defined Keys, etc) +1. Receive Task: + 1. Allowed Correlation Keys + 1. Correlation Key Restrictions (limit names allowed to be used + regex limit) + 1. Out-mapping restrictions. + + +### Formatting Rules (WIP) + +The Linter is not just about execution implementation rules, you can also define formatting rules for the BPMN. + +Formatting rules enable you to prevent common errors in formatting of BPMN. + +1. Label all Gateways +1. Pool Usage +1. Prevent label patterns with Regex +1. Gateways labels end in "?" +1. No Expressions +1. Double sequence flows +1. Sub-Process Names +1. Loop Characteristics naming +1. Start Timer names +1. Message Start Names +1. End Event Names +1. Intermediate Event Names +1. .. More? + + +### TODO + +1. Add support to define what will prevent a deployment (WARNING / ERRORS) +1. Add optional parameter on deployment endpoint to validate using linter +1. Provide JSON error support +1. Provide Language Server implementation of linter. +1. Add IO Mappings rules +1. Add Receive Task Rule +1. Add Message Rules +1. Add Formatting rules +1. Add Timer rules to prevent certain spectrum of timer durations / cycles +1. Add targeting based on BPMN Process Key +1. Add special negating rule for Task Types and Correlation Keys +1. Add Allowed Call Activity Process IDs: Rule to ensure only specific Process IDs can be called through the Modeler. -Where `address` is the Zeebe job `type` that would be configured in the task in the BPMN. -Where `execute` is the class/script that will be executor when jobs are sent to this executor +## Workflow Sanitizer -Where `name` is the unique name of the Executor used for logging purposes. +Workflow Sanitizer is the capability to remove aspects of a BPMN that are internal configuration that should not be shared when allowing users to download BPMN xml (such as when rendering a BPMN in the bpmn.js / bpmn.io modeler). -You can deploy a Executor with multiple `instances` to provide more more parallel throughput capacity. +Every element in a BPMN can be replaced with a sanitized version: a sanitized version is a new blank instance of the element that replaces the original element. +Only configurations that are explicitly desired in a sanitized BPMN are transferred over into the new instance. -Required properties: `name`, `address`, `execute` +The Sanitizer provides flexible usage options depending on your sanitizing needs: -Completion of Jobs sent to Executors is captured over the event bus with the JobResult object. -Completed (successfully or a failure such as a business error) are sent as a JobResult to event bus address: `sourceClient.job-aciton.completion`. +The core of Sanitizer provides a Workflow Linter that allows you to configure which types that inherit from `ModelElementInstance` will be targeted for cleaning. +The default Sanitizer Linter configuration targets all elements and applies a error code of `5000` ("it's Audi 5000"...). +The default Sanitizer that actions each of the found elements, will take a lean approach: -Where `sourceClient` is the ZeebeClient `name` that is used in the `zeebe.clients[].name` property. +1. element Id values are kept. This allows to continue targeting elements based on the IDs used in the BPMN's execution so you can do heatmaps, and BPMN status overlays (counts, what activities are currently failing, which ones have completed, loop counts, etc). +1. All names are kept (these are usually the labels/names on each element/task/gateway/sequence-flow/pool, etc) +1. All Annotations are kept. +1. All markings and definition types are kept: but only the fact that they exist; their actual configuration is removed. +1. Default Flow markings on sequence flows are kept. +1. `Process` elements are not modified. But their children would be modified as their are independent instances of `ModelElementInstance` -The `sourceClient` ensures that a completed job can be sent back to the same Zeebe Cluster, but not necessarily using the same instance of a ZeebeClient that consumed the job. +What is explicitly not kept? -JobResult's that have a `result=FAIL` will have their corresponding Zeebe Job actioned as a Failed Job. - - -# User Task Executors - -User Task(UT) Executors are a special type of executor that are dedicated to the logic handling of BPMN User Tasks. - -UT Executors have the following configuration: - -```yaml -userTaskExecutors: - - name: GenericUserTask - address: ut.generic - instances: 1 -``` - -Required properties: `name`, `address` - -Where `address` is the Zeebe job `type` that would be configured in the task in the BPMN. - -Internally executors have their addresses prefixed with a common job prefix to ensure proper message namespacing. - -Where `name` is the unique name of the UT Executor used for logging purposes. - -You can deploy a UT Executor with multiple `instances` to provide more more parallel throughput capacity. - -UT Executors primary function is to provide capture of UTs from Zeebe and convert the Zeebe jobs into a UserTaskEntity. -A UserTaskEntity is then saved in the storage of choice (such as a DB). - -Completion of User Tasks is captured over the event bus with the JobResult object. - -Completed (successfully or a failure such as a business error) are sent as a JobResult to event bus address: `sourceClient.job-aciton.completion`. - -Where `sourceClient` is the ZeebeClient `name` that is used in the `zeebe.clients[].name` property. - -The `sourceClient` ensures that a completed job can be sent back to the same Zeebe Cluster, but not necessarily using the same instance of a ZeebeClient that consumed the job. - -JobResult's that have a `result=FAIL` will have their corresponding Zeebe Job actioned as a Failed Job. - - -## User Tasks - -The default build of User Tasks seeks to provide a duplicate or similar User Task experience as Camunda's User Tasks implementation. - -See UserTaskEntity.class, UserTaskConfiguration.class for more details. - -A User Task can be configured in the Zeebe BPMN using custom headers. The supported headers are: - -|key|value|description| -|------|------|----------| -|title|`string`|The title of the task. Can be any string value that will be interpreted by the User Task storage system.| -|description|`string` |The description of the task. Can be any string value that will be interpreted by the User Task storage system.| -|priority|`int`|defaults to 0| -|assignee|`string`|The default assignee of the task. A single value.| -|candidateGroups|`string`|The list of groups that are candidates to claim this task. Comma separated list of strings. Example: `"cg1, cg2, cg3"`| -|candidateUsers|`string`|The list of users that are candidates to claim this task. Comma separated list of strings. Example: `"cu1, cu2, cu3"`| -|dueDate|`string`|The date on which the User Task is due. ISO8601 format| -|formKey|`string`|A value that represents the specific form that should be used by the user when completing this task.| - - -When generating a UserTaskEntity, some additional properties are stored for usage and indexing and convenience: - -In addition to the custom header values above, the following is stored in the UserTaskEntity: - -|key|value|description| -|------|------|----------| -|taskId|`string`|The unique ID of the task. Typically will be a business centric key defined during configuration. If not ID is provided then defaults to `user-task--:UUID:` where `:UUID:` is a random UUID.| -|zeebeSource|`string`|The source ZeebeClient `name` that the ZeebeJob was retrieved from. -|zeebeDeadline|`instant`|The Zeebe Job deadline property| -|zeebeJobKey|`long`|The unique job ID of the Zeebe Job.| -|bpmnProcessId|`string`|The BPMN Process Definition ID| -|zeebeVariables|`Map of String:Object`|The variables from the Zeebe Job| -|metadata|`Map of String:Object`|A generic data holder for additional User Task metadata| - -# Form Validation Server - -The Form Validation Server provides HTTP endpoints for validation a Form Submission based on a provided Form Schema. - -Configuration: - -```yaml -formValidatorServer: - enabled: true - corsRegex: ".*." - port: 8082 - instances: 1 - formValidatorService: - host: localhost - port: 8083 - validateUri: /validate - requestTimeout: 5000 -``` - -Where `formValidatorService` is the Form Validator service that performs the actual form validation. - -Example Validation Request: - -POST: `localhost:8083/validate` - -Body: - -```json -{ - "schema":{ - "display": "form", - "components": [ - { - "label": "Text Field", - "allowMultipleMasks": false, - "showWordCount": false, - "showCharCount": false, - "tableView": true, - "alwaysEnabled": false, - "type": "textfield", - "input": true, - "key": "textField2", - "defaultValue": "", - "validate": { - "customMessage": "", - "json": "", - "required": true - }, - "conditional": { - "show": "", - "when": "", - "json": "" - }, - "inputFormat": "plain", - "encrypted": false, - "properties": {}, - "customConditional": "", - "logic": [], - "attributes": {}, - "widget": { - "type": "" - }, - "reorder": false - }, - { - "type": "button", - "label": "Submit", - "key": "submit", - "disableOnInvalid": true, - "theme": "primary", - "input": true, - "tableView": true - } - ], - "settings": { - } - }, - "submission":{ - "data": { - "textField2": 123, - "dog": "cat" - }, - "metadata": {} -} -} -``` - -Response if validation passes: - -```json -{ - "processed_submission": { - "textField2": "sog" - } -} -``` - -Notice that the extra `dog` property is removed because it is not a valid field in the form schema. - -Response if validation fails: - -```json -{ - "isJoi": true, - "name": "ValidationError", - "details": [ - { - "message": "\"textField2\" must be a string", - "path": "textField2", - "type": "string.base", - "context": { - "value": 123, - "key": "textField2", - "label": "textField2" - } - } - ], - "_object": { - "textField2": 123, - "dog": "cat" - }, - "_validated": { - "textField2": 123 - } -} -``` - -The validation service is also available over the event bus at the `address` property defined in the Form Validation Server configuration. - - -# Management Server - -The management server provides HTTP endpoints for working with Zeebe clusters - -Configuration: - -```yaml -managementServer: - enabled: true - apiRoot: server1 - corsRegex: ".*." - port: 8080 - zeebeClient: - name: DeploymentClient - brokerContactPoint: "localhost:25600" - requestTimeout: PT10S - instances: 1 - fileUploadPath: ./tmp/uploads -``` - -required fields: `apiRoot`, `zeebeClient` - -`apiRoot` must be unique. - -## Deploy Workflow - -`POST localhost:8080/server1/deploy` - -Headers: -- `Content-Type: multipart/form-data` - -form-data: -- file name (must be a .bpmn or .yaml file) : file upload (the binary file you are uploading such as a .bpmn file) - -Where `server1` is the `apiRoot` value defined in the YAML configuration. - -You can deploy many management servers as needed. Each server can be deployed for different zeebe clusters. - -You can deploy the same server with multiple `instances` to provide more throughput. - -## Create Workflow Instance / Start Workflow - -`POST localhost:8080/server1/create-instance` - -Headers: -- `Content-Type: application/json` -- `Accept: application/json` - -Json Body: - -```json -{ - "workflowKey": 1234567890 -} -``` - -Where `workflowKey` is the unique workflow key that was generated for the BPMN process/pool during deployment. - -You may also use: - -```json -{ - "bpmnProcessId": "myProcess", - "bpmnProcessVersion": 2 -} -``` - -Where `bpmnProcessId` is the BPMN's process Id property (sometimes referred to as a process key). -The `bpmnProcessVersion` is optional. You can set the version number or set as `-1` which means "latest version" / newest. If you do not provide the property it will default to latest version. - -`varaibles` can also be provided as a json object: - -```json -{ - "workflowKey": 1234567890, - "variables": { - "myVar1": 123, - "myVar2": "some value", - "myVarABC": [1,2,5,10], - "myVarXYZ": { - "1": "A", - "2": "B" - } - } -} -``` - -The variables will be injected into the created workflow instance. - - -# User Task Server - -A User Task HTTP server that provides User Task persistence, querying, completion, etc. - -The server also provides a Form Schema Entity persistence, querying, and validation of submissions against the schema. -The Form Schema is what will be submitted to the Form Validator Service. - -## Server Configuration - -```yaml -userTaskServer: - enabled: true - corsRegex: ".*." - port: 8080 - instances: 1 -``` - -## Actions: - -1. Save Form Schema -1. Complete User Task -1. Get User Tasks -1. Submit Form to Complete a User Task -1. Delete User Task (TODO) -1. Claim User Task (TODO) -1. UnClaim User Task (TODO) -1. Assign User Task (TODO) -1. Create Custom User Task (not linked to Zeebe Job) - -## Save Form Schema - -POST `/form/schema` - -```json -{ - "owner": "Department-1", - "key": "MySimpleForm1", - "title": "My Simple Form 1", - "schema": { - "display": "form", - "components": [ - { - "label": "Text Field", - "allowMultipleMasks": false, - "showWordCount": false, - "showCharCount": false, - "tableView": true, - "alwaysEnabled": false, - "type": "textfield", - "input": true, - "key": "textField2", - "defaultValue": "", - "validate": { - "customMessage": "", - "json": "", - "required": true - }, - "conditional": { - "show": "", - "when": "", - "json": "" - }, - "inputFormat": "plain", - "encrypted": false, - "properties": {}, - "customConditional": "", - "logic": [], - "attributes": {}, - "widget": { - "type": "" - }, - "reorder": false - }, - { - "type": "button", - "label": "Submit", - "key": "submit", - "disableOnInvalid": true, - "theme": "primary", - "input": true, - "tableView": true - } - ], - "settings": { - } - } -} -``` - -The `key` property is the `formKey` value you setup in your zeebe task custom headers. - -Required fields: `owner`, `key`, `title`, `schema` - - -## Complete User Task - -Mainly used as a administrative endpoint to complete a User Task without any Form - -POST `/task/complete` - -```json -{ - "job": 2251799813685292, - "source": "MyCustomClient", - "completionVariables": {} -} -``` - -`Source` is the zeebe client name configured in your configuration yaml. - - -## Get Tasks - -GET `/task` - -JSON Body: - -Query is run as a `AND` query on each of the arguments - -```json -{ - "taskId": "", - "state": "", - "title": "", - "assignee": "", - "dueDate": "", - "zeebeJobKey": "", - "zeebeSource": "", - "bpmnProcessId": "" -} -``` - -You can pass `{}` as the body if you want to return all User Tasks. - - -## Submit Task with Form - -POST `/task/id/:taskId/submit` - -Example: `localhost:8088/task/id/user-task--080946c6-1355-4cd7-9fcf-86fc9c46d4c4/submit` - -Json Body: - -```json -{ - "data": { - "textField2": "sog", - "dog": "cat" - }, - "metadata": {} -} -``` - -This endpoint acts the same as the Validation Server's `/validate` endpoint. The difference is the User Task's endpoint will complete the User Task entity in the DB if the form is valid, and the Form fields will be saved in the Zeebe workflow as variables when the Job is completed. - -Upon successful form validation, and assuming the User Task is not already completed, then the User Task will be made complete and the completion variables will be saved. -Then a background worker is watching for completed user tasks and will attempt to report this back to the Zeebe Job. -The behaviour is this way so you can complete User Tasks without having to have a active connection to the Zeebe Cluster. - -## Delete User Task - -TODO... - - -## Claim User Task - -TODO... - -## UnClaim User Task - -TODO... - -## Assign User Task - -TODO... - -## Create Custom User Task (not backed by Zeebe Job) - -TODO... - ----- - -# Raw Notes - -1. Implements clustering and scaling through Vertx instances. -1. ZeebeClientVerticle can increase in number of instances: 1 instance == 1 ZeebeClient Channel connection. -1. ExecutorVerticle is independent of ZeebeClient. You can scale executors across the cluster to any number of instances and have full cluster feature set. -1. a JobResult is what holds the context of if a Zeebe Failure should occur in the context of the actual Work that a executor preformed. -1. Management HTTP takes a apiRoot namespace which is the prefix for the api calls to deploy and start process instances -1. TODO: Add a send message HTTP verticle -1. UserTaskConfiguration is the data that is received from a Zeebe Custom Headers which is used to generate the Entity -1. UserTaskVerticle is a example of a custom worker. I have made them individual so User Tasks can be managed as stand along systems -1. The Client Name property of the Client config is used as the EB address namespace for sending job completions back over the wire -1. TODO move executeBlocking code into their own worker verticles with their own thread pools -1. sourceClient is passed over the wire as a header which represents the client Name. The client name is the client (or any instance of that name/id) that is used to send back into zeebe. This supports multiple clients to different brokers (representing tenants for data separation) -1. Breakers needs to be added -1. Polling for Jobs is a executeBlocking action. When polling is complete (found jobs or did not find jobs), it will call the poll jobs again. It assumes long polling is enabled. -1. TODO review defaults and setup of entity build in the user task verticle as its very messy right now. -1. Management Server uses the route namespacing because it is assumed that security will be added by a proxy in the network. If app level security needs to be added, then the ManagementHttpVerticle can be easily copied and replaced with security logic. -1. TODO move EB addresses in a global static class for easy global management -1. TODO fix up the logging to be DEBUG and cleanup the language as the standard is all over the place at the moment. Also inlcude more context info for when reading the log as its unclear. -1. TODO ***** Add the defaults logic for the User Task assignments, where if the headers that are not provided in zeebe then the user tasks entity will default to those configured values. -1. TODO add the overrides logic: where if a override is provided then only the logic from the override is used and the provided header does not matter -1. TODO Refactor error handling on HTTP requests to provider better json errors - -```xml -... - - - - - - - - -... -``` +1. Expressions +1. Configurations on Events: message correlation keys, timer expressions, etc +1. Receive Task Message Correlation configurations. +1. Service Task Configurations: Headers, Type, Retries +1. IO Mappings +1. Loop Characteristic configurations (the "parallel" vs "sequential" marking is kept) +1. Message Elements (even if a message is not tied to a element ) \ No newline at end of file diff --git a/bpmn/bpmn1.bpmn b/bpmn/bpmn1.bpmn deleted file mode 100644 index d04a9bf..0000000 --- a/bpmn/bpmn1.bpmn +++ /dev/null @@ -1,77 +0,0 @@ - - - - - SequenceFlow_0mi3b9p - - - - - - - - - - - - SequenceFlow_0z6yvdy - SequenceFlow_0zyd6q2 - - - SequenceFlow_0zyd6q2 - - - - - - - SequenceFlow_0mi3b9p - SequenceFlow_1wzqads - - - - - - - - SequenceFlow_1wzqads - SequenceFlow_0z6yvdy - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..1db9b0d --- /dev/null +++ b/build.gradle @@ -0,0 +1,89 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "1.3.50" + id "org.jetbrains.kotlin.kapt" version "1.3.50" + id "org.jetbrains.kotlin.plugin.allopen" version "1.3.50" + id "com.github.johnrengelman.shadow" version "5.0.0" + id "application" + id "org.jetbrains.kotlin.plugin.jpa" version "1.3.60" +} + + + +version "0.1" +group "quintessential.tasklist.zeebe" + +repositories { + mavenCentral() + maven { url "https://jcenter.bintray.com" } +} + +configurations { + // for dependencies that are needed for development only + developmentOnly +} + +dependencies { + implementation platform("io.micronaut:micronaut-bom:$micronautVersion") + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}" + implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}" + implementation "io.micronaut:micronaut-runtime" + implementation "io.micronaut:micronaut-http-server-netty" + implementation "io.micronaut:micronaut-http-client" + implementation 'io.micronaut:micronaut-validation' + implementation("io.reactivex.rxjava2:rxkotlin:2.4.0") + kapt platform("io.micronaut:micronaut-bom:$micronautVersion") + kapt "io.micronaut:micronaut-inject-java" + kapt "io.micronaut:micronaut-validation" + kaptTest platform("io.micronaut:micronaut-bom:$micronautVersion") + kaptTest "io.micronaut:micronaut-inject-java" + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.10" + runtimeOnly "ch.qos.logback:logback-classic:1.2.3" + testImplementation platform("io.micronaut:micronaut-bom:$micronautVersion") + testImplementation "io.micronaut.test:micronaut-test-kotlintest" + testImplementation "io.mockk:mockk:1.9.3" + testImplementation "io.kotlintest:kotlintest-runner-junit5:3.3.2" + + kapt 'io.micronaut.data:micronaut-data-processor:1.0.0.M5' + implementation 'io.micronaut.data:micronaut-data-hibernate-jpa:1.0.0.M5' + runtime 'com.h2database:h2:1.4.200' + runtime 'io.micronaut.configuration:micronaut-jdbc-hikari' + implementation 'io.micronaut.configuration:micronaut-flyway' + + implementation "io.zeebe:zeebe-client-java:$zeebeVersion" +// testImplementation "io.zeebe:zeebe-test:$zeebeVersion" + +} + +test.classpath += configurations.developmentOnly + +mainClassName = "com.github.stephenott.qtz.Application" + +test { + useJUnitPlatform() +} + +allOpen { + annotation("io.micronaut.aop.Around") +} + +compileKotlin { + kotlinOptions { + jvmTarget = '1.8' + //Will retain parameter names for Java reflection + javaParameters = true + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = '1.8' + javaParameters = true + } +} + +shadowJar { + mergeServiceFiles() +} + +run.classpath += configurations.developmentOnly +run.jvmArgs('-noverify', '-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote') diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c5ff8fd..e69de29 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,12 +0,0 @@ -version: "2" - -services: - zeebe: - restart: always - container_name: zeebe_broker - image: camunda/zeebe:0.20.0 - environment: - - ZEEBE_LOG_LEVEL=info - ports: - - "26500:26500" - - "9600:9600" \ No newline at end of file diff --git a/docker/dockerfile-qtz b/docker/dockerfile-qtz new file mode 100644 index 0000000..3602c08 --- /dev/null +++ b/docker/dockerfile-qtz @@ -0,0 +1,3 @@ +FROM adoptopenjdk/openjdk11-openj9:jdk-11.0.1.13-alpine-slim +COPY build/libs/quintessential-tasklist-zeebe-*-all.jar QTZ.jar +EXPOSE 8080 \ No newline at end of file diff --git a/docs/design/cluster.png b/docs/design/cluster.png deleted file mode 100644 index 250c9ff..0000000 Binary files a/docs/design/cluster.png and /dev/null differ diff --git a/docs/design/dataflow.png b/docs/design/dataflow.png deleted file mode 100644 index e4fbd35..0000000 Binary files a/docs/design/dataflow.png and /dev/null differ diff --git a/docs/design/designs.graffle b/docs/design/designs.graffle deleted file mode 100644 index 7035fba..0000000 Binary files a/docs/design/designs.graffle and /dev/null differ diff --git a/docs/design/form/FormBuilder1-build.png b/docs/design/form/FormBuilder1-build.png deleted file mode 100644 index 5c6c3ce..0000000 Binary files a/docs/design/form/FormBuilder1-build.png and /dev/null differ diff --git a/docs/design/form/FormBuilder2-build.png b/docs/design/form/FormBuilder2-build.png deleted file mode 100644 index 31e35b2..0000000 Binary files a/docs/design/form/FormBuilder2-build.png and /dev/null differ diff --git a/docs/design/form/FormBuilder3-build.png b/docs/design/form/FormBuilder3-build.png deleted file mode 100644 index 874a828..0000000 Binary files a/docs/design/form/FormBuilder3-build.png and /dev/null differ diff --git a/docs/design/form/FormBuilder4-render.png b/docs/design/form/FormBuilder4-render.png deleted file mode 100644 index b32510b..0000000 Binary files a/docs/design/form/FormBuilder4-render.png and /dev/null differ diff --git a/docs/design/form/User-Task-Form-Completion-Flow.png b/docs/design/form/User-Task-Form-Completion-Flow.png deleted file mode 100644 index 0c08f90..0000000 Binary files a/docs/design/form/User-Task-Form-Completion-Flow.png and /dev/null differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f8e9e05 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +micronautVersion=1.2.6 +kotlinVersion=1.3.50 +zeebeVersion=0.21.1 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..91ca28c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..774deab --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Nov 22 16:34:48 EST 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/micronaut-cli.yml b/micronaut-cli.yml new file mode 100644 index 0000000..33c936e --- /dev/null +++ b/micronaut-cli.yml @@ -0,0 +1,5 @@ +profile: service +defaultPackage: qtz +--- +testFramework: kotlintest +sourceLanguage: kotlin \ No newline at end of file diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 27d5d55..0000000 --- a/pom.xml +++ /dev/null @@ -1,143 +0,0 @@ - - - 4.0.0 - - com.github.stephenott - Quintenssential-Tasklist-Zeebe - 0.5 - - - - UTF-8 - 1.8 - 1.8 - 3.8.1 - 0.21.0-alpha1 - - com.github.stephenott.MainVerticle - - - - - - io.vertx - vertx-stack-depchain - ${vertx.version} - pom - import - - - - com.fasterxml.jackson - jackson-bom - 2.9.9 - pom - import - - - - - - - - io.vertx - vertx-core - - - io.vertx - vertx-config - - - io.vertx - vertx-config-yaml - - - io.vertx - vertx-circuit-breaker - - - io.vertx - vertx-web - - - io.vertx - vertx-web-client - - - - org.mongodb - mongodb-driver-reactivestreams - 1.12.0 - - - - io.zeebe - zeebe-client-java - ${zeebe.version} - - - - com.fasterxml.jackson.module - jackson-module-parameter-names - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - - org.slf4j - slf4j-jdk14 - 1.7.28 - - - - - io.zeebe - zeebe-test - ${zeebe.version} - test - - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo - 2.2.0 - test - - - - io.vertx - vertx-codegen - processor - provided - - - io.vertx - vertx-service-proxy - - - - - - - - - - - maven-compiler-plugin - 3.8.1 - - 1.8 - 1.8 - - - - - \ No newline at end of file diff --git a/python-scripts/test1.py b/python-scripts/test1.py new file mode 100644 index 0000000..3220e17 --- /dev/null +++ b/python-scripts/test1.py @@ -0,0 +1,22 @@ +import json +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--inputs', dest='inputs') +args = parser.parse_args() + +with open(args.inputs) as json_file: + data = json.load(json_file) + +# Do some work + +myResult = { + "commandResult": "success", + "detectedIp": True, + "ipList": [ + "0.0.0.0", "0.2.3.4" + ], + "originalVarsFromZeebe": data +} + +print(json.dumps(myResult)) diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9d0112c --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name="quintessential-tasklist-zeebe" \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/MainVerticle.java b/src/main/java/com/github/stephenott/MainVerticle.java deleted file mode 100644 index 5f7d3e0..0000000 --- a/src/main/java/com/github/stephenott/MainVerticle.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.github.stephenott; - -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import com.github.stephenott.common.EventBusableMessageCodec; -import com.github.stephenott.conf.ApplicationConfiguration; -import com.github.stephenott.executors.JobResult; -import com.github.stephenott.executors.polyglot.ExecutorVerticle; -import com.github.stephenott.executors.usertask.UserTaskExecutorVerticle; -import com.github.stephenott.form.validator.FormValidationServerHttpVerticle; -import com.github.stephenott.form.validator.ValidationRequest; -import com.github.stephenott.form.validator.ValidationRequestResult; -import com.github.stephenott.managementserver.ManagementHttpVerticle; -import com.github.stephenott.usertask.*; -import com.github.stephenott.usertask.mongo.MongoManager; -import com.github.stephenott.zeebe.client.ZeebeClientVerticle; -import com.mongodb.MongoClientSettings; -import com.mongodb.reactivestreams.client.MongoClients; -import io.vertx.config.ConfigRetriever; -import io.vertx.config.ConfigRetrieverOptions; -import io.vertx.config.ConfigStoreOptions; -import io.vertx.core.*; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.json.Json; -import io.vertx.core.json.JsonObject; -import org.bson.codecs.configuration.CodecRegistry; -import org.bson.codecs.pojo.PojoCodecProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.bson.codecs.configuration.CodecRegistries.*; - -public class MainVerticle extends AbstractVerticle { - - private Logger log = LoggerFactory.getLogger(MainVerticle.class); - - private EventBus eb; - - private ConfigRetriever appConfigRetriever; - ApplicationConfiguration appConfig; - - @Override - public void start() throws Exception { - Json.mapper.registerModules(new ParameterNamesModule(), new Jdk8Module(), new JavaTimeModule()); - Json.prettyMapper.registerModules(new ParameterNamesModule(), new Jdk8Module(), new JavaTimeModule()); - Json.mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - Json.prettyMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - - - eb = vertx.eventBus(); - - eb.registerDefaultCodec(FailedDbActionException.class, new EventBusableMessageCodec<>(FailedDbActionException.class)); - eb.registerDefaultCodec(JobResult.class, new EventBusableMessageCodec<>(JobResult.class)); - eb.registerDefaultCodec(DbActionResult.class, new EventBusableMessageCodec<>(DbActionResult.class)); - eb.registerDefaultCodec(CompletionRequest.class, new EventBusableMessageCodec<>(CompletionRequest.class)); - eb.registerDefaultCodec(GetRequest.class, new EventBusableMessageCodec<>(GetRequest.class)); - eb.registerDefaultCodec(ValidationRequest.class, new EventBusableMessageCodec<>(ValidationRequest.class)); - eb.registerDefaultCodec(ValidationRequestResult.class, new EventBusableMessageCodec<>(ValidationRequestResult.class)); - - String configYmlPath = config().getString("configYmlPath"); - - retrieveAppConfig(configYmlPath, result -> { - if (result.succeeded()) { - appConfig = result.result(); - - //Setup Mongo: - CodecRegistry registry = fromRegistries( - MongoClients.getDefaultCodecRegistry(), - fromProviders(PojoCodecProvider.builder() - .automatic(true) - .build()) - ); - MongoClientSettings mSettings = MongoClientSettings.builder() - .codecRegistry(registry) - .build(); - MongoManager.setClient(MongoClients.create(mSettings)); - - //@TODO refactor this - vertx.deployVerticle(UserTaskActionsVerticle.class, new DeploymentOptions()); - //@TODO refactor this - - deployUserTaskHttpServer(appConfig.getUserTaskServer()); - - - appConfig.getExecutors().forEach(this::deployExecutorVerticle); - - appConfig.getUserTaskExecutors().forEach(this::deployUserTaskExecutorVerticle); - - appConfig.getZeebe().getClients().forEach(this::deployZeebeClient); - - if (appConfig.getManagementServer().isEnabled()) { - deployManagementClient(appConfig.getManagementServer()); - } - - if (appConfig.getFormValidatorServer().isEnabled()){ - deployFormValidationServer(appConfig.getFormValidatorServer()); - } - - } else { - throw new IllegalStateException("Unable to read yml configuration", result.cause()); - } - }); - } - - private void deployManagementClient(ApplicationConfiguration.ManagementHttpConfiguration config) { - DeploymentOptions options = new DeploymentOptions() - .setInstances(config.getInstances()) - .setConfig(JsonObject.mapFrom(config)); - - vertx.deployVerticle(ManagementHttpVerticle::new, options, deployResult -> { - if (deployResult.succeeded()) { - log.info("Management Client has successfully deployed"); - } else { - log.error("Management Client failed to deploy", deployResult.cause()); - } - }); - } - - private void deployUserTaskHttpServer(ApplicationConfiguration.UserTaskHttpServerConfiguration config) { - DeploymentOptions options = new DeploymentOptions() - .setInstances(config.getInstances()) - .setConfig(JsonObject.mapFrom(config)); - - vertx.deployVerticle(UserTaskHttpServerVerticle::new, options, deployResult -> { - if (deployResult.succeeded()) { - log.info("UserTask HTTP Server has successfully deployed"); - } else { - log.error("UserTask HTTP Server failed to deploy", deployResult.cause()); - } - }); - } - - private void deployFormValidationServer(ApplicationConfiguration.FormValidationServerConfiguration config) { - DeploymentOptions options = new DeploymentOptions() - .setInstances(config.getInstances()) - .setConfig(JsonObject.mapFrom(config)); - - vertx.deployVerticle(FormValidationServerHttpVerticle::new, options, deployResult -> { - if (deployResult.succeeded()) { - log.info("Form Validation Server has successfully deployed"); - } else { - log.error("Form Validation Server failed to deploy", deployResult.cause()); - } - }); - } - - - private void deployExecutorVerticle(ApplicationConfiguration.ExecutorConfiguration config) { - DeploymentOptions options = new DeploymentOptions() - .setInstances(config.getInstances()) - .setConfig(JsonObject.mapFrom(config)); - - vertx.deployVerticle(ExecutorVerticle::new, options, vert -> { - if (vert.succeeded()) { - log.info("Executor Verticle " + config.getName() + " has successfully deployed (" + config.getInstances() + " instances)"); - } else { - log.error("Executor Verticle " + config.getName() + " has failed to deploy!", vert.cause()); - } - }); - } - - private void deployUserTaskExecutorVerticle(ApplicationConfiguration.UserTaskExecutorConfiguration config) { - DeploymentOptions options = new DeploymentOptions(); - options.setConfig(JsonObject.mapFrom(config)); - - vertx.deployVerticle(UserTaskExecutorVerticle::new, options, vert -> { - if (vert.succeeded()) { - log.info("UserTask Executor Verticle " + config.getName() + " has successfully deployed"); - } else { - log.error("UserTask Executor Verticle " + config.getName() + " has failed to deploy!", vert.cause()); - } - }); - } - - private void deployZeebeClient(ApplicationConfiguration.ZeebeClientConfiguration config) { - DeploymentOptions options = new DeploymentOptions(); - options.setConfig(JsonObject.mapFrom(config)); - - vertx.deployVerticle(ZeebeClientVerticle::new, options, vert -> { - if (vert.succeeded()) { - log.info("Zeebe Client Verticle " + config.getName() + " has successfully deployed"); - } else { - log.error("Zeebe Client Verticle " + config.getName() + " has failed to deploy!", vert.cause()); - } - }); - } - - - private void retrieveAppConfig(String filePath, Handler> result) { - ConfigStoreOptions store = new ConfigStoreOptions() - .setType("file") - .setFormat("yaml") - .setConfig(new JsonObject() - .put("path", filePath) - ); - - appConfigRetriever = ConfigRetriever.create(vertx, new ConfigRetrieverOptions().addStore(store)); - - appConfigRetriever.getConfig(retrieverResult -> { - if (retrieverResult.succeeded()) { - result.handle(Future.succeededFuture(retrieverResult.result().mapTo(ApplicationConfiguration.class))); - - } else { - result.handle(Future.failedFuture(retrieverResult.cause())); - } - }); - } -} diff --git a/src/main/java/com/github/stephenott/common/Common.java b/src/main/java/com/github/stephenott/common/Common.java deleted file mode 100644 index 1cb3226..0000000 --- a/src/main/java/com/github/stephenott/common/Common.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.stephenott.common; - -public class Common { - - public static String JOB_ADDRESS_PREFIX = "job."; - -} diff --git a/src/main/java/com/github/stephenott/common/EventBusable.java b/src/main/java/com/github/stephenott/common/EventBusable.java deleted file mode 100644 index a37faf1..0000000 --- a/src/main/java/com/github/stephenott/common/EventBusable.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.stephenott.common; - -import io.vertx.core.json.JsonObject; - -public interface EventBusable { - - default JsonObject toJsonObject(){ - return JsonObject.mapFrom(this); - } - -} diff --git a/src/main/java/com/github/stephenott/common/EventBusableMessageCodec.java b/src/main/java/com/github/stephenott/common/EventBusableMessageCodec.java deleted file mode 100644 index 7d7ab0e..0000000 --- a/src/main/java/com/github/stephenott/common/EventBusableMessageCodec.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.stephenott.common; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.eventbus.MessageCodec; -import io.vertx.core.json.Json; - -public class EventBusableMessageCodec implements MessageCodec { - - private Class tClass; - - public EventBusableMessageCodec(Class tClass) { - this.tClass = tClass; - } - - @Override - public void encodeToWire(Buffer buffer, T t) { - Buffer encoded = t.toJsonObject().toBuffer(); - buffer.appendInt(encoded.length()); - buffer.appendBuffer(encoded); - } - - @Override - public T decodeFromWire(int pos, Buffer buffer) { - int length = buffer.getInt(pos); - pos += 4; - return Json.decodeValue(buffer.slice(pos, pos + length), tClass); - } - - @Override - public T transform(T t) { - return t.toJsonObject().copy().mapTo(tClass); - } - - @Override - public String name() { - return tClass.getCanonicalName(); - } - - @Override - public byte systemCodecID() { - return -1; - } -} diff --git a/src/main/java/com/github/stephenott/common/EventBusableReplyException.java b/src/main/java/com/github/stephenott/common/EventBusableReplyException.java deleted file mode 100644 index 755e519..0000000 --- a/src/main/java/com/github/stephenott/common/EventBusableReplyException.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.stephenott.common; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.stephenott.usertask.FailedDbActionException; -import io.vertx.core.eventbus.ReplyException; -import io.vertx.core.eventbus.ReplyFailure; - -@JsonAutoDetect(fieldVisibility = Visibility.NONE, - getterVisibility = Visibility.NONE, - setterVisibility = Visibility.NONE, - isGetterVisibility = Visibility.NONE) -public class EventBusableReplyException extends ReplyException implements EventBusable { - - @JsonProperty() - private Enum failureType; - - @JsonProperty() - private String internalErrorMessage; - - @JsonProperty() - private String userErrorMessage; -// -// public EventBusableReplyException(FailedDbActionException.FailureType failureType, Throwable internalError, String UserErrorMessage){ -// super(ReplyFailure.RECIPIENT_FAILURE, UserErrorMessage); -// this.internalErrorMessage -// } - - public EventBusableReplyException(Enum failureType, String internalErrorMessage, String userErrorMessage) { - super(ReplyFailure.RECIPIENT_FAILURE, userErrorMessage); - this.internalErrorMessage = internalErrorMessage; - this.userErrorMessage = userErrorMessage; - this.failureType = failureType; - } - - public String getInternalErrorMessage() { - return internalErrorMessage; - } - - public EventBusableReplyException setInternalErrorMessage(String internalErrorMessage) { - this.internalErrorMessage = internalErrorMessage; - return this; - } - - public String getUserErrorMessage() { - return userErrorMessage; - } - - public EventBusableReplyException setUserErrorMessage(String userErrorMessage) { - this.userErrorMessage = userErrorMessage; - return this; - } - - public Enum getFailureType() { - return failureType; - } - - public EventBusableReplyException setFailureType(Enum failureType) { - this.failureType = failureType; - return this; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/conf/ApplicationConfiguration.java b/src/main/java/com/github/stephenott/conf/ApplicationConfiguration.java deleted file mode 100644 index 3594b89..0000000 --- a/src/main/java/com/github/stephenott/conf/ApplicationConfiguration.java +++ /dev/null @@ -1,501 +0,0 @@ -package com.github.stephenott.conf; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.github.stephenott.executors.usertask.UserTaskConfiguration; - -import java.time.Duration; -import java.util.List; -import java.util.UUID; - -public class ApplicationConfiguration { - - private ZeebeConfiguration zeebe; - private List executors; - private List userTaskExecutors; - private ManagementHttpConfiguration managementServer; - private FormValidationServerConfiguration formValidatorServer; - private UserTaskHttpServerConfiguration userTaskServer; - - public ApplicationConfiguration() { - } - - public ZeebeConfiguration getZeebe() { - return zeebe; - } - - public void setZeebe(ZeebeConfiguration zeebe) { - this.zeebe = zeebe; - } - - public List getExecutors() { - return executors; - } - - public void setExecutors(List executors) { - this.executors = executors; - } - - public List getUserTaskExecutors() { - return userTaskExecutors; - } - - public void setUserTaskExecutors(List userTaskExecutors) { - this.userTaskExecutors = userTaskExecutors; - } - - public ManagementHttpConfiguration getManagementServer() { - return managementServer; - } - - public void setManagementServer(ManagementHttpConfiguration managementServer) { - this.managementServer = managementServer; - } - - public FormValidationServerConfiguration getFormValidatorServer() { - return formValidatorServer; - } - - public ApplicationConfiguration setFormValidatorServer(FormValidationServerConfiguration formValidatorServer) { - this.formValidatorServer = formValidatorServer; - return this; - } - - public UserTaskHttpServerConfiguration getUserTaskServer() { - return userTaskServer; - } - - public ApplicationConfiguration setUserTaskServer(UserTaskHttpServerConfiguration userTaskServer) { - this.userTaskServer = userTaskServer; - return this; - } - - public static class ZeebeConfiguration{ - private List clients; - - public ZeebeConfiguration() { - } - - public List getClients() { - return clients; - } - - public void setClients(List clients) { - this.clients = clients; - } - } - - - public static class ZeebeClientConfiguration{ - private String name; - private String brokerContactPoint = "localhost:25600"; - - @JsonFormat(shape = JsonFormat.Shape.STRING) - private Duration requestTimeout = Duration.ofSeconds(10); - - private List workers; - - public ZeebeClientConfiguration() { - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getBrokerContactPoint() { - return brokerContactPoint; - } - - public void setBrokerContactPoint(String brokerContactPoint) { - this.brokerContactPoint = brokerContactPoint; - } - - public Duration getRequestTimeout() { - return requestTimeout; - } - - public void setRequestTimeout(Duration requestTimeout) { - this.requestTimeout = requestTimeout; - } - - public List getWorkers() { - return workers; - } - - public void setWorkers(List workers) { - this.workers = workers; - } - } - - - public static class ZeebeWorkers { - private String name; - private List jobTypes; - - @JsonFormat(shape = JsonFormat.Shape.STRING) - private Duration timeout = Duration.ofSeconds(10); - - public ZeebeWorkers() { - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getJobTypes() { - return jobTypes; - } - - public void setJobTypes(List jobTypes) { - this.jobTypes = jobTypes; - } - - /** - * The Timeout of how long the Job is locked to the worker / subscription - * @return - */ - public Duration getTimeout() { - return timeout; - } - - public ZeebeWorkers setTimeout(Duration timeout) { - this.timeout = timeout; - return this; - } - } - - - public static class ExecutorConfiguration { - private String name; - private String address; - private String execute; - private int instances = 1; - - public ExecutorConfiguration() { - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public String getExecute() { - return execute; - } - - public void setExecute(String execute) { - this.execute = execute; - } - - public int getInstances() { - return instances; - } - - public void setInstances(int instances) { - this.instances = instances; - } - } - - public static class ManagementHttpConfiguration { - private boolean enabled = true; - private String apiRoot = UUID.randomUUID().toString(); - private ZeebeClientConfiguration zeebeClient; - private String fileUploadPath = "./tmp/uploads"; - private int instances = 1; - private int port = 8080; - private String corsRegex; - - public ManagementHttpConfiguration() { - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getApiRoot() { - return apiRoot; - } - - public void setApiRoot(String apiRoot) { - this.apiRoot = apiRoot; - } - - public ZeebeClientConfiguration getZeebeClient() { - return zeebeClient; - } - - public void setZeebeClient(ZeebeClientConfiguration zeebeClient) { - this.zeebeClient = zeebeClient; - } - - public String getFileUploadPath() { - return fileUploadPath; - } - - public void setFileUploadPath(String fileUploadPath) { - this.fileUploadPath = fileUploadPath; - } - - public int getInstances() { - return instances; - } - - public void setInstances(int instances) { - this.instances = instances; - } - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public String getCorsRegex() { - return corsRegex; - } - - public void setCorsRegex(String corsRegex) { - this.corsRegex = corsRegex; - } - } - - public static class UserTaskExecutorConfiguration { - private String name; - private String address; - private UserTaskConfiguration defaults; - private UserTaskConfiguration overrides; - private int instances; - - public UserTaskExecutorConfiguration() { - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public UserTaskConfiguration getDefaults() { - return defaults; - } - - public void setDefaults(UserTaskConfiguration defaults) { - this.defaults = defaults; - } - - public UserTaskConfiguration getOverrides() { - return overrides; - } - - public void setOverrides(UserTaskConfiguration overrides) { - this.overrides = overrides; - } - - public int getInstances() { - return instances; - } - - public void setInstances(int instances) { - this.instances = instances; - } - } - - /** - * The Form Validation Server (Verticle) - */ - public static class FormValidationServerConfiguration { - private boolean enabled = true; - private int instances = 1; - private int port = 8082; - private String corsRegex; - private String address = "form-validation"; - private FormValidatorServiceConfiguration formValidatorService; - - public FormValidationServerConfiguration() { - } - - public boolean isEnabled() { - return enabled; - } - - public FormValidationServerConfiguration setEnabled(boolean enabled) { - this.enabled = enabled; - return this; - } - - public int getInstances() { - return instances; - } - - public FormValidationServerConfiguration setInstances(int instances) { - this.instances = instances; - return this; - } - - public int getPort() { - return port; - } - - public FormValidationServerConfiguration setPort(int port) { - this.port = port; - return this; - } - - public String getCorsRegex() { - return corsRegex; - } - - public FormValidationServerConfiguration setCorsRegex(String corsRegex) { - this.corsRegex = corsRegex; - return this; - } - - public String getAddress() { - return address; - } - - public FormValidationServerConfiguration setAddress(String address) { - this.address = address; - return this; - } - - public FormValidatorServiceConfiguration getFormValidatorService() { - return formValidatorService; - } - - public FormValidationServerConfiguration setFormValidatorService(FormValidatorServiceConfiguration formValidatorService) { - this.formValidatorService = formValidatorService; - return this; - } - } - - /** - * The Form Validator Service - * (The external service that is communicated over HTTP that performs - * the actual validation against the supplied schema) - */ - public static class FormValidatorServiceConfiguration { - private String host = "localhost"; - private int port = 8083; - private String validateUri = "/validate"; - private long requestTimeout = 5000; - - public FormValidatorServiceConfiguration() { - } - - public String getHost() { - return host; - } - - public FormValidatorServiceConfiguration setHost(String host) { - this.host = host; - return this; - } - - public int getPort() { - return port; - } - - public FormValidatorServiceConfiguration setPort(int port) { - this.port = port; - return this; - } - - public String getValidateUri() { - return validateUri; - } - - public FormValidatorServiceConfiguration setValidateUri(String validateUri) { - this.validateUri = validateUri; - return this; - } - - //@TODO Refactor this to support the java8 Duration class - public long getRequestTimeout() { - return requestTimeout; - } - - public FormValidatorServiceConfiguration setRequestTimeout(long requestTimeout) { - this.requestTimeout = requestTimeout; - return this; - } - } - - public static class UserTaskHttpServerConfiguration { - private boolean enabled = true; - private int instances = 1; - private int port = 8080; - private String corsRegex; - - public boolean isEnabled() { - return enabled; - } - - public UserTaskHttpServerConfiguration setEnabled(boolean enabled) { - this.enabled = enabled; - return this; - } - - public int getInstances() { - return instances; - } - - public UserTaskHttpServerConfiguration setInstances(int instances) { - this.instances = instances; - return this; - } - - public int getPort() { - return port; - } - - public UserTaskHttpServerConfiguration setPort(int port) { - this.port = port; - return this; - } - - public String getCorsRegex() { - return corsRegex; - } - - public UserTaskHttpServerConfiguration setCorsRegex(String corsRegex) { - this.corsRegex = corsRegex; - return this; - } - } - -} \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/conf/config.json b/src/main/java/com/github/stephenott/conf/config.json deleted file mode 100644 index f1f6201..0000000 --- a/src/main/java/com/github/stephenott/conf/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "configYmlPath": "./zeebe.yml" -} \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/executors/JobResult.java b/src/main/java/com/github/stephenott/executors/JobResult.java deleted file mode 100644 index 6b9c714..0000000 --- a/src/main/java/com/github/stephenott/executors/JobResult.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.github.stephenott.executors; - -import com.github.stephenott.common.EventBusable; -import io.vertx.core.json.JsonObject; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -public class JobResult implements EventBusable { - - private Result result; - private long jobKey; - private Map variables = new HashMap<>(); - private int retries; - private String errorMessage = "An error occurred"; - - public enum Result { - COMPLETE, - FAIL - } - - private JobResult() { - } - - public JobResult(long jobKey, Result result, Map variables, int retries, String errorMessage) { - Objects.requireNonNull(result); - - this.result = result; - this.jobKey = jobKey; - this.variables = variables; - this.retries = retries; - this.errorMessage = errorMessage; - } - - public JobResult(long jobKey, Result result, int retries) { - this(jobKey, result, null, retries, null); - } - - /** - * Will set retries to 0. - * @param jobKey - * @param result - */ - public JobResult(long jobKey, Result result) { - setJobKey(jobKey); - setResult(result); - } - - public Result getResult() { - return result; - } - - public JobResult setResult(Result result) { - this.result = result; - return this; - } - - public long getJobKey() { - return jobKey; - } - - public JobResult setJobKey(long jobKey) { - this.jobKey = jobKey; - return this; - } - - public Map getVariables() { - return variables; - } - - public JobResult setVariables(Map variables) { - this.variables = variables; - return this; - } - - public int getRetries() { - return retries; - } - - public JobResult setRetries(int retries) { - this.retries = retries; - return this; - } - - public String getErrorMessage() { - return errorMessage; - } - - public JobResult setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - return this; - } - - // public JsonObject toJsonObject(){ -// return JsonObject.mapFrom(this); -// } -// -// public static JobResult fromJsonObject(JsonObject doneJob){ -// return doneJob.mapTo(JobResult.class); -// } - -} \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/executors/polyglot/ExecutorVerticle.java b/src/main/java/com/github/stephenott/executors/polyglot/ExecutorVerticle.java deleted file mode 100644 index f8a63ba..0000000 --- a/src/main/java/com/github/stephenott/executors/polyglot/ExecutorVerticle.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.stephenott.executors.polyglot; - -import com.github.stephenott.common.Common; -import com.github.stephenott.executors.JobResult; -import com.github.stephenott.conf.ApplicationConfiguration; -import io.vertx.core.AbstractVerticle; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.json.JsonObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ExecutorVerticle extends AbstractVerticle { - - private Logger log = LoggerFactory.getLogger(ExecutorVerticle.class); - - private EventBus eb; - private ApplicationConfiguration.ExecutorConfiguration executorConfiguration; - - @Override - public void start() throws Exception { - try { - executorConfiguration = config().mapTo(ApplicationConfiguration.ExecutorConfiguration.class); - log.info("Executor Config: " + JsonObject.mapFrom(executorConfiguration).toString()); - } catch (Exception e){ - throw new IllegalStateException("Could not load executor configuration"); - } - - eb = vertx.eventBus(); - - String address = Common.JOB_ADDRESS_PREFIX + executorConfiguration.getAddress(); - - eb.consumer(address, handler -> { - log.info("doing some work!!!"); - - String sourceClient = handler.headers().get("sourceClient"); - - JsonObject job = handler.body(); - - //@TODO Add polyexecutor - - JobResult jobResult = new JobResult( - job.getLong("key"), - JobResult.Result.COMPLETE, - (job.getInteger("retries") > 0) ? job.getInteger("retries") - 1 : 0); - - eb.send(sourceClient + ".job-action.completion", jobResult); - - }); - } -} diff --git a/src/main/java/com/github/stephenott/executors/usertask/UserTaskConfiguration.java b/src/main/java/com/github/stephenott/executors/usertask/UserTaskConfiguration.java deleted file mode 100644 index 3ed0b11..0000000 --- a/src/main/java/com/github/stephenott/executors/usertask/UserTaskConfiguration.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.stephenott.executors.usertask; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class UserTaskConfiguration { - - private String title; - private String description; - private int priority = 0; - private String assignee; - private String candidateGroups; - private String candidateUsers; - private String dueDate; - private String formKey; - - public UserTaskConfiguration() { - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public int getPriority() { - return priority; - } - - public void setPriority(int priority) { - this.priority = priority; - } - - public String getAssignee() { - return assignee; - } - - public void setAssignee(String assignee) { - this.assignee = assignee; - } - - public String getCandidateGroups() { - return candidateGroups; - } - - public void setCandidateGroups(String candidateGroups) { - this.candidateGroups = candidateGroups; - } - - public String getCandidateUsers() { - return candidateUsers; - } - - public void setCandidateUsers(String candidateUsers) { - this.candidateUsers = candidateUsers; - } - - public String getDueDate() { - return dueDate; - } - - public void setDueDate(String dueDate) { - this.dueDate = dueDate; - } - - public String getFormKey() { - return formKey; - } - - public void setFormKey(String formKey) { - this.formKey = formKey; - } -} - diff --git a/src/main/java/com/github/stephenott/executors/usertask/UserTaskExecutorVerticle.java b/src/main/java/com/github/stephenott/executors/usertask/UserTaskExecutorVerticle.java deleted file mode 100644 index 46e5e2a..0000000 --- a/src/main/java/com/github/stephenott/executors/usertask/UserTaskExecutorVerticle.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.github.stephenott.executors.usertask; - -import com.github.stephenott.common.Common; -import com.github.stephenott.conf.ApplicationConfiguration; -import com.github.stephenott.executors.JobResult; -import com.github.stephenott.usertask.entity.UserTaskEntity; -import com.github.stephenott.usertask.mongo.MongoManager; -import com.github.stephenott.usertask.mongo.Subscribers; -import com.github.stephenott.usertask.mongo.Subscribers.SimpleSubscriber; -import com.mongodb.reactivestreams.client.MongoCollection; -import com.mongodb.reactivestreams.client.Success; -import io.vertx.core.AbstractVerticle; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.json.Json; -import io.vertx.core.json.JsonObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Instant; -import java.util.Arrays; -import java.util.HashSet; -import java.util.UUID; - -public class UserTaskExecutorVerticle extends AbstractVerticle { - - private final Logger log = LoggerFactory.getLogger(UserTaskExecutorVerticle.class); - - private EventBus eb; - - private ApplicationConfiguration.UserTaskExecutorConfiguration utExecutorConfig; - - private MongoCollection tasksCollection = MongoManager.getDatabase().getCollection("tasks", UserTaskEntity.class); - - @Override - public void start() throws Exception { - try { - utExecutorConfig = config().mapTo(ApplicationConfiguration.UserTaskExecutorConfiguration.class); - } catch (Exception e) { - log.error("Unable to parse Ut Executor Config", e); - throw e; - } - - eb = vertx.eventBus(); - - String address = Common.JOB_ADDRESS_PREFIX + utExecutorConfig.getAddress(); - - eb.consumer(address, handler -> { - - log.info("User Task({}) has captured some Work.", address); - - String sourceClient = handler.headers().get("sourceClient"); - //@TODO add handler if sourceClient is missing then reject job. - - UserTaskConfiguration utConfig = handler.body() - .getJsonObject("customHeaders") - .mapTo(UserTaskConfiguration.class); - - UserTaskEntity utEntity = new UserTaskEntity() - .setZeebeSource(sourceClient) - .setTaskId("user-task--" + UUID.randomUUID().toString()) - .setTitle(utConfig.getTitle()) - .setDescription(utConfig.getDescription()) - .setPriority(utConfig.getPriority()) - .setAssignee(utConfig.getAssignee()) - .setCandidateGroups((utConfig.getCandidateGroups() != null) ? new HashSet(Arrays.asList(utConfig.getCandidateGroups().split(","))) : null) - .setCandidateUsers((utConfig.getCandidateUsers() != null) ? new HashSet(Arrays.asList(utConfig.getCandidateUsers().split(","))) : null) - .setDueDate((utConfig.getDueDate() != null) ? Instant.parse(utConfig.getDueDate()) : null) - .setFormKey(utConfig.getFormKey()) - .setZeebeDeadline(Instant.ofEpochMilli(handler.body().getLong("deadline"))) - .setZeebeJobKey(handler.body().getLong("key")) - .setBpmnProcessId(handler.body().getString("bpmnProcessId")) - .setBpmnProcessVersion(handler.body().getInteger("workflowDefinitionVersion")) - .setZeebeVariables(((JsonObject) Json.decodeValue(handler.body().getString("variables"))).getMap()) - .setTaskOriginalCapture(Instant.now()); - - log.info("User Task created: {}", JsonObject.mapFrom(utEntity).toString()); - - saveToDb(utEntity).setHandler(res -> { - if (res.succeeded()) { - log.info("UserTaskEntity has been saved to DB..."); - } else { - //@TODO update with better error - throw new IllegalStateException("Unable to save to DB", res.cause()); - } - }); - - }); - - log.info("User Task Executor Verticle consuming tasks at: {}", utExecutorConfig.getAddress()); - } - - public Future saveToDb(UserTaskEntity entity) { - Promise promise = Promise.promise(); - - tasksCollection.insertOne(entity) - .subscribe(new SimpleSubscriber().singleResult(result -> { - if (result.succeeded()) { - promise.complete(); - } else { - promise.fail(result.cause()); - } - })); - - return promise.future(); - } -} diff --git a/src/main/java/com/github/stephenott/form/validator/FormValidationServerHttpVerticle.java b/src/main/java/com/github/stephenott/form/validator/FormValidationServerHttpVerticle.java deleted file mode 100644 index a83e950..0000000 --- a/src/main/java/com/github/stephenott/form/validator/FormValidationServerHttpVerticle.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.github.stephenott.form.validator; - -import com.github.stephenott.conf.ApplicationConfiguration; -import com.github.stephenott.form.validator.exception.ValidationRequestResultException; -import com.github.stephenott.form.validator.exception.ValidationRequestResultException.ErrorType; -import io.vertx.core.*; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.client.WebClient; -import io.vertx.ext.web.client.WebClientOptions; -import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.ext.web.handler.CorsHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static com.github.stephenott.form.validator.ValidationRequestResult.*; -import static com.github.stephenott.form.validator.ValidationRequestResult.InvalidResult; -import static com.github.stephenott.form.validator.ValidationRequestResult.ValidResult; - -public class FormValidationServerHttpVerticle extends AbstractVerticle { - - private Logger log = LoggerFactory.getLogger(FormValidationServerHttpVerticle.class); - - private WebClient webClient; - - private EventBus eb; - - private ApplicationConfiguration.FormValidationServerConfiguration formValidationServerConfig; - - @Override - public void start() throws Exception { - formValidationServerConfig = config().mapTo(ApplicationConfiguration.FormValidationServerConfiguration.class); - - eb = vertx.eventBus(); - - if (formValidationServerConfig.isEnabled()) { - - WebClientOptions webClientOptions = new WebClientOptions(); - webClient = WebClient.create(vertx, webClientOptions); - - Router mainRouter = Router.router(vertx); - HttpServer server = vertx.createHttpServer(); - - mainRouter.route().failureHandler(failure -> { - - int statusCode = failure.statusCode(); - - HttpServerResponse response = failure.response(); - response.setStatusCode(statusCode) - .end(new JsonObject().put("error", failure.failure().getLocalizedMessage()).toBuffer()); - }); - - establishFormValidationRoute(mainRouter); - - server.requestHandler(mainRouter) - .listen(formValidationServerConfig.getPort()); - - } - - //@TODO Add a EB config toggle - establishFormValidationEbConsumer(); - - log.info("Form Validation Server deployed at: localhost:...., CORS is .... "); - } - - @Override - public void stop() throws Exception { - super.stop(); - } - - private void establishFormValidationRoute(Router router) { - Route route = router.route(HttpMethod.POST, "/validate") - .consumes("application/json") - .produces("application/json"); - - if (formValidationServerConfig.getCorsRegex() != null) { - route.handler(CorsHandler.create(formValidationServerConfig.getCorsRegex())); - } - - route.handler(BodyHandler.create()); - - route.handler(rc -> { //routing context - //@TODO add a generic helper to parse and handler errors or look at using a "throws" in the method def - // without this try the error is hidden from the logs - ValidationRequest request; - try { - request = rc.getBodyAsJson().mapTo(ValidationRequest.class); - - } catch (Exception e) { - throw new IllegalArgumentException("Unable to parse body", e); - } - - validateFormSubmission(request, handler -> { - if (handler.succeeded()) { - if (handler.result().getResult().equals(Result.VALID)) { - rc.response() - .setStatusCode(202) - .putHeader("content-type", "application/json; charset=utf-8") - .end(JsonObject.mapFrom(handler.result().getValidResultObject()).toBuffer()); - } else { - rc.response() - .setStatusCode(400) - .putHeader("content-type", "application/json; charset=utf-8") - .end(JsonObject.mapFrom(handler.result().getInvalidResultObject()).toBuffer()); - } - - } else { - log.error("Unable to execute validation request", handler.cause()); - rc.fail(500, handler.cause()); - } - }); - }); - } - - private void establishFormValidationEbConsumer() { - - String address = "forms.action.validate"; - - eb.consumer(address, ebHandler -> { - - validateFormSubmission(ebHandler.body(), valResult -> { - if (valResult.succeeded()) { - ebHandler.reply(valResult.result()); - } else { - if (valResult.cause().getClass().equals(ValidationRequestResultException.class)){ - ValidationRequestResultException exception = (ValidationRequestResultException)valResult.cause(); - ebHandler.reply(GenerateErrorResult(new ErrorResult(exception.getErrorType(),exception.getMessage(), exception.getCause().getMessage()))); - - } else { - //@TODO refactor to rethrow a critial error for monitoring purposes. - log.error("Unexpected result returned from validationFormSubmission, and thus unable to reply to " + - "EB message for validation request... Something went wrong...", valResult.cause()); - } - } - }); - }).exceptionHandler(error -> log.error("Could not read Validation Request message from EB", error)); - } - - public void validateFormSubmission(ValidationRequest validationRequest, Handler> handler) { - //@TODO Refactor this to reduce the wordiness... - ApplicationConfiguration.FormValidatorServiceConfiguration validatorConfig = formValidationServerConfig.getFormValidatorService(); - - String host = validatorConfig.getHost(); - int port = validatorConfig.getPort(); - long requestTimeout = validatorConfig.getRequestTimeout(); - String validateUri = validatorConfig.getValidateUri(); - - log.info("BODY: " + JsonObject.mapFrom(new ValidationServiceRequest(validationRequest)).toString()); - - //@TODO look at using the .expect predicate methods as part of .post() rather than using the if statusCode... - webClient.post(port, host, validateUri) - .timeout(requestTimeout) - .sendJson(new ValidationServiceRequest(validationRequest), res -> { - if (res.succeeded()) { - - int statusCode = res.result().statusCode(); - - if (statusCode == 202) { - log.info("FORMIO 202 RESULT: " + res.result().bodyAsString()); - handler.handle(Future.succeededFuture(GenerateValidResult(res.result().bodyAsJson(ValidResult.class)))); - - } else if (statusCode == 400) { - log.info("FORMIO 400 RESULT: " + res.result().bodyAsString()); - handler.handle(Future.succeededFuture(GenerateInvalidResult(res.result().bodyAsJson(InvalidResult.class)))); - - } else { - log.error("Unexpected response returned by form validator: code:" + res.result().statusCode() + ". Body: " + res.result().bodyAsString()); - - handler.handle(Future.failedFuture( - new ValidationRequestResultException(ErrorType.UNEXPECTED_STATUS_CODE, - "Unexpected response returned by form validator: code:" + res.result().statusCode() + ". Body: " + res.result().bodyAsString(), - "Something went wrong with validation server."))); - } - - } else { - log.error("Unable to complete HTTP request to validation server", res.cause()); - - handler.handle(Future.failedFuture( - new ValidationRequestResultException(ErrorType.HTTP_REQ_FAILURE, - "Internal Message: Unable to complete HTTP request to validation server, cause: " + res.cause().getLocalizedMessage(), - "Something went wrong while trying to contact validation server."))); - } - }); - } -} diff --git a/src/main/java/com/github/stephenott/form/validator/ValidationRequest.java b/src/main/java/com/github/stephenott/form/validator/ValidationRequest.java deleted file mode 100644 index 01caf45..0000000 --- a/src/main/java/com/github/stephenott/form/validator/ValidationRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.stephenott.form.validator; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.stephenott.common.EventBusable; - - -public class ValidationRequest implements EventBusable { - - @JsonProperty(required = true) - private ValidationSchemaObject schema; - - @JsonProperty(required = true) - private ValidationSubmissionObject submission; - - public ValidationRequest() { - } - - public ValidationSchemaObject getSchema() { - return schema; - } - - public ValidationRequest setSchema(ValidationSchemaObject schema) { - this.schema = schema; - return this; - } - - public ValidationSubmissionObject getSubmission() { - return submission; - } - - public ValidationRequest setSubmission(ValidationSubmissionObject submission) { - this.submission = submission; - return this; - } -} diff --git a/src/main/java/com/github/stephenott/form/validator/ValidationRequestResult.java b/src/main/java/com/github/stephenott/form/validator/ValidationRequestResult.java deleted file mode 100644 index 509b6d5..0000000 --- a/src/main/java/com/github/stephenott/form/validator/ValidationRequestResult.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.github.stephenott.form.validator; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.stephenott.common.EventBusable; -import com.github.stephenott.form.validator.exception.ValidationRequestResultException; -import io.vertx.core.json.JsonObject; - -import java.util.List; -import java.util.Map; - -public class ValidationRequestResult implements EventBusable { - - @JsonProperty(required = true) - private Result result; - - private ValidResult validResultObject = null; - private InvalidResult invalidResultObject = null; - private ErrorResult errorResult = null; - - public enum Result { - VALID, - INVALID, - ERROR - } - - public ValidationRequestResult() { - } - - public static ValidationRequestResult GenerateValidResult(ValidResult validResultObject){ - return new ValidationRequestResult() - .setResult(Result.VALID) - .setValidResultObject(validResultObject); - } - - public static ValidationRequestResult GenerateInvalidResult(InvalidResult invalidResultObject){ - return new ValidationRequestResult() - .setResult(Result.INVALID) - .setInvalidResultObject(invalidResultObject); - } - - public static ValidationRequestResult GenerateErrorResult(ErrorResult errorResult){ - return new ValidationRequestResult() - .setResult(Result.ERROR) - .setErrorResult(errorResult); - } - - public Result getResult() { - return result; - } - - public ValidationRequestResult setResult(Result result) { - this.result = result; - return this; - } - - public ValidResult getValidResultObject() { - return validResultObject; - } - - public ValidationRequestResult setValidResultObject(ValidResult validResultObject) { - this.validResultObject = validResultObject; - return this; - } - - public InvalidResult getInvalidResultObject() { - return invalidResultObject; - } - - public ValidationRequestResult setInvalidResultObject(InvalidResult invalidResultObject) { - this.invalidResultObject = invalidResultObject; - return this; - } - - public ErrorResult getErrorResult() { - return errorResult; - } - - public ValidationRequestResult setErrorResult(ErrorResult errorResult) { - this.errorResult = errorResult; - return this; - } - - public static class ValidResult { - - @JsonProperty("processed_submission") - private Map processedSubmission; - - public ValidResult() { - } - - public Map getProcessedSubmission() { - return processedSubmission; - } - - public ValidResult setProcessedSubmission(Map processedSubmission) { - this.processedSubmission = processedSubmission; - return this; - } - } - - - public static class InvalidResult{ - - private boolean isJoi; - - private String name; - - private List> details; - - @JsonProperty("_object") - private Map object; - - @JsonProperty("_validated") - private Map validated; - - public InvalidResult() { - } - - public boolean isJoi() { - return isJoi; - } - - public InvalidResult setJoi(boolean joi) { - isJoi = joi; - return this; - } - - public String getName() { - return name; - } - - public InvalidResult setName(String name) { - this.name = name; - return this; - } - - public List> getDetails() { - return details; - } - - public InvalidResult setDetails(List> details) { - this.details = details; - return this; - } - - public Map getObject() { - return object; - } - - public InvalidResult setObject(Map object) { - this.object = object; - return this; - } - - public Map getValidated() { - return validated; - } - - public InvalidResult setValidated(Map validated) { - this.validated = validated; - return this; - } - } - - public static class ErrorResult { - - ValidationRequestResultException.ErrorType errorType; - String internalErrorMessage; - String endUserMessage; - - private ErrorResult() { - } - - public ErrorResult(ValidationRequestResultException.ErrorType errorType, String internalErrorMessage, String endUserMessage) { - this.errorType = errorType; - this.internalErrorMessage = internalErrorMessage; - this.endUserMessage = endUserMessage; - } - - public ValidationRequestResultException.ErrorType getErrorType() { - return errorType; - } - - public ErrorResult setErrorType(ValidationRequestResultException.ErrorType errorType) { - this.errorType = errorType; - return this; - } - - public String getInternalErrorMessage() { - return internalErrorMessage; - } - - public ErrorResult setInternalErrorMessage(String internalErrorMessage) { - this.internalErrorMessage = internalErrorMessage; - return this; - } - - public String getEndUserMessage() { - return endUserMessage; - } - - public ErrorResult setEndUserMessage(String endUserMessage) { - this.endUserMessage = endUserMessage; - return this; - } - } -} - diff --git a/src/main/java/com/github/stephenott/form/validator/ValidationSchemaObject.java b/src/main/java/com/github/stephenott/form/validator/ValidationSchemaObject.java deleted file mode 100644 index 5b1172b..0000000 --- a/src/main/java/com/github/stephenott/form/validator/ValidationSchemaObject.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.stephenott.form.validator; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; -import java.util.Map; - -public class ValidationSchemaObject { - - @JsonProperty(required = true) - private String display; - - @JsonProperty(required = true) - private List> components; - - private Map settings; - - public ValidationSchemaObject() { - } - - public String getDisplay() { - return display; - } - - public ValidationSchemaObject setDisplay(String display) { - this.display = display; - return this; - } - - public List> getComponents() { - return components; - } - - public ValidationSchemaObject setComponents(List> components) { - this.components = components; - return this; - } - - public Map getSettings() { - return settings; - } - - public ValidationSchemaObject setSettings(Map settings) { - this.settings = settings; - return this; - } -} diff --git a/src/main/java/com/github/stephenott/form/validator/ValidationServiceRequest.java b/src/main/java/com/github/stephenott/form/validator/ValidationServiceRequest.java deleted file mode 100644 index f44fca8..0000000 --- a/src/main/java/com/github/stephenott/form/validator/ValidationServiceRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.stephenott.form.validator; - -public class ValidationServiceRequest { - - private ValidationSchemaObject schema; - private ValidationSubmissionObject submission; - - public ValidationServiceRequest() { - } - - public ValidationServiceRequest(ValidationRequest validationRequest) { - this.schema = validationRequest.getSchema(); - this.submission = validationRequest.getSubmission(); - } - - - public ValidationSchemaObject getSchema() { - return schema; - } - - public ValidationServiceRequest setSchema(ValidationSchemaObject schema) { - this.schema = schema; - return this; - } - - public ValidationSubmissionObject getSubmission() { - return submission; - } - - public ValidationServiceRequest setSubmission(ValidationSubmissionObject submission) { - this.submission = submission; - return this; - } -} diff --git a/src/main/java/com/github/stephenott/form/validator/ValidationSubmissionObject.java b/src/main/java/com/github/stephenott/form/validator/ValidationSubmissionObject.java deleted file mode 100644 index 4cebf75..0000000 --- a/src/main/java/com/github/stephenott/form/validator/ValidationSubmissionObject.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.stephenott.form.validator; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Map; - -public class ValidationSubmissionObject { - - @JsonProperty(required = true) - private Map data; - - private Map metadata; - - public ValidationSubmissionObject() { - } - - public Map getData() { - return data; - } - - public ValidationSubmissionObject setData(Map data) { - this.data = data; - return this; - } - - public Map getMetadata() { - return metadata; - } - - public ValidationSubmissionObject setMetadata(Map metadata) { - this.metadata = metadata; - return this; - } -} diff --git a/src/main/java/com/github/stephenott/form/validator/exception/InvalidFormSubmissionException.java b/src/main/java/com/github/stephenott/form/validator/exception/InvalidFormSubmissionException.java deleted file mode 100644 index 350f664..0000000 --- a/src/main/java/com/github/stephenott/form/validator/exception/InvalidFormSubmissionException.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.stephenott.form.validator.exception; - -import com.github.stephenott.form.validator.ValidationRequestResult; - -public class InvalidFormSubmissionException extends IllegalArgumentException { - private ValidationRequestResult.InvalidResult invalidResult; - - public InvalidFormSubmissionException(String s, ValidationRequestResult.InvalidResult invalidResult) { - super(s); - this.invalidResult = invalidResult; - } - - public ValidationRequestResult.InvalidResult getInvalidResult() { - return invalidResult; - } -} diff --git a/src/main/java/com/github/stephenott/form/validator/exception/ValidationRequestResultException.java b/src/main/java/com/github/stephenott/form/validator/exception/ValidationRequestResultException.java deleted file mode 100644 index 4e1a3e6..0000000 --- a/src/main/java/com/github/stephenott/form/validator/exception/ValidationRequestResultException.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.stephenott.form.validator.exception; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.vertx.core.json.JsonObject; - -public class ValidationRequestResultException extends RuntimeException { - - private ErrorType errorType; - - public enum ErrorType { - UNEXPECTED_STATUS_CODE, - HTTP_REQ_FAILURE - } - - @JsonCreator - public ValidationRequestResultException(@JsonProperty(value = "errorType", required = true) ErrorType errorType, - @JsonProperty(value = "internalErrorMessage", required = true) String internalErrorMessage, - @JsonProperty(value = "endUserMessage", required = true) String endUserMessage) { - super(endUserMessage, new IllegalStateException(internalErrorMessage)); - this.errorType = errorType; - } - - public ErrorType getErrorType() { - return errorType; - } - - public ValidationRequestResultException setErrorType(ErrorType errorType) { - this.errorType = errorType; - return this; - } - - -} diff --git a/src/main/java/com/github/stephenott/managementserver/ManagementHttpVerticle.java b/src/main/java/com/github/stephenott/managementserver/ManagementHttpVerticle.java deleted file mode 100644 index 9143225..0000000 --- a/src/main/java/com/github/stephenott/managementserver/ManagementHttpVerticle.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.github.stephenott.managementserver; - -import com.github.stephenott.conf.ApplicationConfiguration; -import com.github.stephenott.zeebe.client.CreateInstanceConfiguration; -import io.vertx.core.*; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.ext.web.FileUpload; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.ext.web.handler.CorsHandler; -import io.zeebe.client.ZeebeClient; -import io.zeebe.client.api.ZeebeFuture; -import io.zeebe.client.api.response.DeploymentEvent; -import io.zeebe.client.api.response.Workflow; -import io.zeebe.client.api.response.WorkflowInstanceEvent; -import io.zeebe.model.bpmn.Bpmn; -import io.zeebe.model.bpmn.BpmnModelInstance; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.Arrays; -import java.util.Set; - -public class ManagementHttpVerticle extends AbstractVerticle { - - private Logger log = LoggerFactory.getLogger(ManagementHttpVerticle.class); - - private EventBus eb; - - private ZeebeClient zClient; - - private String fileUploadPath; - - private ApplicationConfiguration.ManagementHttpConfiguration managementConfig; - - private String apiRoot; - - @Override - public void start() throws Exception { - managementConfig = config().mapTo(ApplicationConfiguration.ManagementHttpConfiguration.class); - - if (!managementConfig.isEnabled()){ - log.error("Stopping Management HTTP Verticle because the provided management config have enables=false"); - stop(); - } - - eb = vertx.eventBus(); - - fileUploadPath = managementConfig.getFileUploadPath(); - apiRoot = managementConfig.getApiRoot(); - - zClient = createZeebeClient(); - - Router mainRouter = Router.router(vertx); - Router apiRootRouter = Router.router(vertx); - - mainRouter.mountSubRouter("/" + apiRoot, apiRootRouter); - - HttpServer server = vertx.createHttpServer(); - - //Generic Failure Handler - //@TODO setup proper Exceptions with json responses - //@TODO setup faillback failure handlers (such as Resource not found) - //@TODO move to method - mainRouter.route().failureHandler(failure -> { - - int statusCode = failure.statusCode(); - - HttpServerResponse response = failure.response(); - response.setStatusCode(statusCode) - .end("DOG" + failure.failure().getLocalizedMessage()); - }); - //@TODO move to method - apiRootRouter.route().failureHandler(failure -> { - - int statusCode = failure.statusCode(); - - HttpServerResponse response = failure.response(); - response.setStatusCode(statusCode) - .end("DOG" + failure.failure().getLocalizedMessage()); - }); - - establishDeployRoute(apiRootRouter); - establishCreateWorkflowInstanceRoute(apiRootRouter); - - server.requestHandler(mainRouter) - .listen(managementConfig.getPort()); - - log.info("Server deployed at: localhost:{} under apiRoot: {}, CORS is {} ", managementConfig.getPort(), managementConfig.getApiRoot(), (managementConfig.getCorsRegex() != null) ? "enabled with: " + managementConfig.getCorsRegex() : "disabled"); - - } - - @Override - public void stop() throws Exception { - - } - - private void establishDeployRoute(Router router) { - Route route = router.route(HttpMethod.POST, "/deploy") - .consumes("multipart/form-data") - .produces("application/json"); - - if (managementConfig.getCorsRegex() != null) { - route.handler(CorsHandler.create(managementConfig.getCorsRegex())); - } - - route.handler(BodyHandler.create().setUploadsDirectory(fileUploadPath)); //@TODO Change this - - route.handler(rc -> { - Set uploads = rc.fileUploads(); - - if (uploads.size() != 1) { - rc.fail(403, new IllegalArgumentException("Must have only 1 file upload")); - - } else { - uploads.forEach(upload -> { - handleUploadForWorkflowDeployment(upload, rc); - }); - - } - }); - } - - private void establishCreateWorkflowInstanceRoute(Router router) { - Route route = router.route(HttpMethod.POST, "/create-instance") - .consumes("application/json") - .produces("application/json"); - - if (managementConfig.getCorsRegex() != null) { - route.handler(CorsHandler.create(managementConfig.getCorsRegex())); - } - - route.handler(BodyHandler.create()); - - route.handler(rc -> { - CreateInstanceConfiguration config = rc.getBodyAsJson().mapTo(CreateInstanceConfiguration.class); - - // Workflow key will take precedent over bpmn version: - - // Create Instance based on workerflow Key - - //@TODO move into its own handle method: - vertx.executeBlocking(code -> { - if (config.getWorkflowKey() != null) { - ZeebeFuture workflowInstanceEventFuture = - zClient.newCreateInstanceCommand() - .workflowKey(config.getWorkflowKey()) - .variables(config.getVariables()) - .send(); - - log.info("Starting workflow instance based on key: " + config.getWorkflowKey()); - try { - WorkflowInstanceEvent workflowInstanceEvent = workflowInstanceEventFuture.join(); - log.info("Workflow({}) instance started: {}", config.getWorkflowKey(), workflowInstanceEvent.getWorkflowInstanceKey()); - code.complete(workflowInstanceEvent); - - } catch (Exception e) { - log.error("Unable to start workflow instance", e); - code.fail(e); - } - - //Create instance based on bpmnProcessId - } else { - ZeebeFuture workflowInstanceEventFuture = - zClient.newCreateInstanceCommand() - .bpmnProcessId(config.getBpmnProcessId()) - .version((config.getBpmnProcessVersion() != null) ? config.getBpmnProcessVersion() : -1) - .variables(config.getVariables()) - .send(); - - String humanVersion = (config.getBpmnProcessVersion() != null) ? config.getBpmnProcessVersion().toString() : "latest"; - - log.info("Starting workflow instance based on bpmnProcessId: " + config.getBpmnProcessId() + "and version: " + humanVersion); - - try { - WorkflowInstanceEvent workflowInstanceEvent = workflowInstanceEventFuture.join(); - log.info("Workflow({}) instance started: {}", config.getWorkflowKey(), workflowInstanceEvent.getWorkflowInstanceKey()); - code.complete(workflowInstanceEvent); - - } catch (Exception e) { - log.error("Unable to start workflow instance", e); - code.fail(e); - } - } - - }, codeResult -> { - if (codeResult.succeeded()) { - rc.response() - .setStatusCode(200) - .end("Process Instance Started: " + codeResult.result().getWorkflowInstanceKey()); - - } else { - //Unable to start instance - rc.fail(403, codeResult.cause()); - } - }); - }); - } - - private void handleUploadForWorkflowDeployment(FileUpload upload, RoutingContext rc) { - readModelFromUpload(upload.uploadedFileName(), result -> { - if (result.succeeded()) { - createWorkflowDeployment(upload.name(), result.result()).setHandler(deployResult -> { - if (deployResult.succeeded()) { - rc.response() - .setStatusCode(200) - .end("Deployment Success"); - - } else { - //Unable to deploy - rc.fail(403, deployResult.cause()); - } - }); - - } else { - // Could not read file: - rc.fail(403, result.cause()); - } - }); - } - - private void readModelFromUpload(String uploadedFileName, Handler> asyncResultHandler) { - vertx.executeBlocking(code->{ - try { - BpmnModelInstance model = Bpmn.readModelFromFile(new File(uploadedFileName)); - code.complete(model); - - } catch (Exception e) { - code.fail(e); - } - }, codeResult->{ - if (codeResult.succeeded()){ - asyncResultHandler.handle(Future.succeededFuture(codeResult.result())); - } else{ - asyncResultHandler.handle(Future.failedFuture(codeResult.cause())); - } - }); - } - - - private Future createWorkflowDeployment(String workflowName, BpmnModelInstance modelInstance) { - Promise promise = Promise.promise(); - - if (modelInstance == null || workflowName == null) { - promise.fail("Must have at least 1 model to deploy"); - - } else { - vertx.executeBlocking(code -> { - ZeebeFuture deploymentEventFuture = zClient.newDeployCommand() - .addWorkflowModel(modelInstance, workflowName) - .send(); - - log.info("Deploying Workflow..."); - - try { - DeploymentEvent deploymentEvent = deploymentEventFuture.join(); - - //@TODO rebuild this log statement - log.info("Deployment Succeeded: Deployment Key:" + deploymentEvent.getKey() + " with workflows: " + Arrays.toString(deploymentEvent.getWorkflows().stream().map(Workflow::getWorkflowKey).toArray())); - - code.complete(deploymentEvent); - - } catch (Exception e) { - code.fail(e); - } - - }, codeResult -> { - if (codeResult.succeeded()) { - promise.complete(codeResult.result()); - - } else { - promise.fail(codeResult.cause()); - } - }); - } - - return promise.future(); - } - - - private ZeebeClient createZeebeClient() { - return ZeebeClient.newClientBuilder() - .brokerContactPoint(managementConfig.getZeebeClient().getBrokerContactPoint()) - .defaultRequestTimeout(managementConfig.getZeebeClient().getRequestTimeout()) - .usePlaintext() //@TODO remove and replace with cert /-/SECURITY/-/ - .build(); - } - - -} diff --git a/src/main/java/com/github/stephenott/package-info.java b/src/main/java/com/github/stephenott/package-info.java deleted file mode 100644 index f08a03d..0000000 --- a/src/main/java/com/github/stephenott/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@ModuleGen(groupPackage = "com.github.stephenott", name = "vertx-processor-service-proxy") -package com.github.stephenott; - -import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/usertask/CompletionRequest.java b/src/main/java/com/github/stephenott/usertask/CompletionRequest.java deleted file mode 100644 index 1c558bd..0000000 --- a/src/main/java/com/github/stephenott/usertask/CompletionRequest.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.stephenott.usertask; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.stephenott.common.EventBusable; - -import java.util.Map; - -public class CompletionRequest implements EventBusable { - - @JsonProperty(value = "job", required = true) - private long zeebeJobKey; - - @JsonProperty(value = "source", required = true) - private String zeebeSource; - - @JsonProperty("variables") - private Map completionVariables; - - private boolean bypassFormSubmission = false; - - private Object formSubmission; - - - public CompletionRequest() { - } - - public long getZeebeJobKey() { - return zeebeJobKey; - } - - public CompletionRequest setZeebeJobKey(long zeebeJobKey) { - this.zeebeJobKey = zeebeJobKey; - return this; - } - - public String getZeebeSource() { - return zeebeSource; - } - - public CompletionRequest setZeebeSource(String zeebeSource) { - this.zeebeSource = zeebeSource; - return this; - } - - public Map getCompletionVariables() { - return completionVariables; - } - - public CompletionRequest setCompletionVariables(Map completionVariables) { - this.completionVariables = completionVariables; - return this; - } - - public boolean isBypassFormSubmission() { - return bypassFormSubmission; - } - - public CompletionRequest setBypassFormSubmission(boolean bypassFormSubmission) { - this.bypassFormSubmission = bypassFormSubmission; - return this; - } - - public Object getFormSubmission() { - return formSubmission; - } - - public CompletionRequest setFormSubmission(Object formSubmission) { - this.formSubmission = formSubmission; - return this; - } -} diff --git a/src/main/java/com/github/stephenott/usertask/DbActionResult.java b/src/main/java/com/github/stephenott/usertask/DbActionResult.java deleted file mode 100644 index 1885988..0000000 --- a/src/main/java/com/github/stephenott/usertask/DbActionResult.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.stephenott.usertask; - -import com.github.stephenott.common.EventBusable; - -import java.util.Collections; -import java.util.List; - -public class DbActionResult implements EventBusable { - - private List resultObjects; - - private DbActionResult() { - } - - public DbActionResult(Object resultObjects){ - this.resultObjects = Collections.singletonList(resultObjects); - } - - public DbActionResult(List resultObjects) { - this.resultObjects = resultObjects; - } - - public List getResultObjects() { - return resultObjects; - } - - public DbActionResult setResultObjects(List resultObjects) { - this.resultObjects = resultObjects; - return this; - } - -} diff --git a/src/main/java/com/github/stephenott/usertask/FailedDbActionException.java b/src/main/java/com/github/stephenott/usertask/FailedDbActionException.java deleted file mode 100644 index 095c233..0000000 --- a/src/main/java/com/github/stephenott/usertask/FailedDbActionException.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.stephenott.usertask; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.stephenott.common.EventBusableReplyException; - -public class FailedDbActionException extends EventBusableReplyException { - - public enum FailureType { - CANT_CREATE, - CANT_READ, - CANT_FIND, - CANT_UPDATE, - CANT_DELETE, - CANT_CONTACT_DB, - FILTER_PARSE_ERROR, - CANT_COMPLETE_COMMAND - } - - - @JsonCreator - public FailedDbActionException(@JsonProperty(value = "failureType", required = true) FailureType failureType, - @JsonProperty(value = "internalErrorMessage", required = true) String internalErrorMessage, - @JsonProperty(value = "userErrorMessage", required = true) String userErrorMessage) { - super(failureType, internalErrorMessage, userErrorMessage); - } -} diff --git a/src/main/java/com/github/stephenott/usertask/FormSchemaService.java b/src/main/java/com/github/stephenott/usertask/FormSchemaService.java deleted file mode 100644 index beb6607..0000000 --- a/src/main/java/com/github/stephenott/usertask/FormSchemaService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.stephenott.usertask; - -import com.github.stephenott.usertask.entity.FormSchemaEntity; -import io.vertx.codegen.annotations.ProxyGen; -import io.vertx.core.Vertx; - -@ProxyGen -public interface FormSchemaService { - - public static FormSchemaService create(Vertx vertx){ - return new FormSchemaServiceImpl(); - } - - public static FormSchemaService createProxy(Vertx vertx, String address){ - return new FormSchemaServiceVertxEBProxy(vertx, address); - } - - void saveFormSchema(FormSchemaEntity formSchemaEntity); - -} diff --git a/src/main/java/com/github/stephenott/usertask/FormSchemaServiceImpl.java b/src/main/java/com/github/stephenott/usertask/FormSchemaServiceImpl.java deleted file mode 100644 index f55d595..0000000 --- a/src/main/java/com/github/stephenott/usertask/FormSchemaServiceImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.stephenott.usertask; - -public class FormSchemaServiceImpl implements FormSchemaService { -} diff --git a/src/main/java/com/github/stephenott/usertask/GetRequest.java b/src/main/java/com/github/stephenott/usertask/GetRequest.java deleted file mode 100644 index fc8a7a0..0000000 --- a/src/main/java/com/github/stephenott/usertask/GetRequest.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.github.stephenott.usertask; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.github.stephenott.common.EventBusable; -import com.github.stephenott.usertask.entity.UserTaskEntity; - -import java.time.Instant; -import java.util.Optional; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class GetRequest implements EventBusable { - - private Optional taskId = Optional.empty(); - - private Optional state = Optional.empty(); - - private Optional title = Optional.empty(); - - private Optional assignee = Optional.empty(); - - private Optional dueDate = Optional.empty(); - - private Optional zeebeJobKey = Optional.empty(); - - private Optional zeebeSource = Optional.empty(); - - private Optional bpmnProcessId = Optional.empty(); - - public GetRequest() { - } - - public Optional getTaskId() { - return taskId; - } - - public GetRequest setTaskId(Optional taskId) { - this.taskId = taskId; - return this; - } - - public Optional getState() { - return state; - } - - public GetRequest setState(Optional state) { - this.state = state; - return this; - } - - public Optional getTitle() { - return title; - } - - public GetRequest setTitle(Optional title) { - this.title = title; - return this; - } - - public Optional getAssignee() { - return assignee; - } - - public GetRequest setAssignee(Optional assignee) { - this.assignee = assignee; - return this; - } - - public Optional getDueDate() { - return dueDate; - } - - public GetRequest setDueDate(Optional dueDate) { - this.dueDate = dueDate; - return this; - } - - public Optional getZeebeJobKey() { - return zeebeJobKey; - } - - public GetRequest setZeebeJobKey(Optional zeebeJobKey) { - this.zeebeJobKey = zeebeJobKey; - return this; - } - - public Optional getZeebeSource() { - return zeebeSource; - } - - public GetRequest setZeebeSource(Optional zeebeSource) { - this.zeebeSource = zeebeSource; - return this; - } - - public Optional getBpmnProcessId() { - return bpmnProcessId; - } - - public GetRequest setBpmnProcessId(Optional bpmnProcessId) { - this.bpmnProcessId = bpmnProcessId; - return this; - } -} diff --git a/src/main/java/com/github/stephenott/usertask/GetTasksFormSchemaReqRes.java b/src/main/java/com/github/stephenott/usertask/GetTasksFormSchemaReqRes.java deleted file mode 100644 index 4b66874..0000000 --- a/src/main/java/com/github/stephenott/usertask/GetTasksFormSchemaReqRes.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.github.stephenott.usertask; - -import com.github.stephenott.common.EventBusable; - -import java.util.Map; - -public class GetTasksFormSchemaReqRes { - - public static class Request implements EventBusable { - - private String taskId; - - public Request() { - } - - public String getTaskId() { - return taskId; - } - - public Request setTaskId(String taskId) { - this.taskId = taskId; - return this; - } - } - - - public static class Response implements EventBusable { - - private String taskId; - private String formKey; - private Map schema; - private Map defaultValues; - - public Response() { - } - - public String getTaskId() { - return taskId; - } - - public Response setTaskId(String taskId) { - this.taskId = taskId; - return this; - } - - public String getFormKey() { - return formKey; - } - - public Response setFormKey(String formKey) { - this.formKey = formKey; - return this; - } - - public Map getSchema() { - return schema; - } - - public Response setSchema(Map schema) { - this.schema = schema; - return this; - } - - public Map getDefaultValues() { - return defaultValues; - } - - public Response setDefaultValues(Map defaultValues) { - this.defaultValues = defaultValues; - return this; - } - } -} diff --git a/src/main/java/com/github/stephenott/usertask/SubmitTaskComposeDto.java b/src/main/java/com/github/stephenott/usertask/SubmitTaskComposeDto.java deleted file mode 100644 index 5fcd73e..0000000 --- a/src/main/java/com/github/stephenott/usertask/SubmitTaskComposeDto.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.github.stephenott.usertask; - -import com.github.stephenott.executors.JobResult; -import com.github.stephenott.form.validator.ValidationRequestResult; -import com.github.stephenott.usertask.entity.FormSchemaEntity; -import com.github.stephenott.usertask.entity.UserTaskEntity; - -public class SubmitTaskComposeDto { - - UserTaskEntity userTaskEntity; - FormSchemaEntity formSchemaEntity; - ValidationRequestResult validationRequestResult; - DbActionResult dbActionResult; - JobResult jobResult; - boolean validForm; - - public SubmitTaskComposeDto() { - } - - public UserTaskEntity getUserTaskEntity() { - return userTaskEntity; - } - - public SubmitTaskComposeDto setUserTaskEntity(UserTaskEntity userTaskEntity) { - this.userTaskEntity = userTaskEntity; - return this; - } - - public FormSchemaEntity getFormSchemaEntity() { - return formSchemaEntity; - } - - public SubmitTaskComposeDto setFormSchemaEntity(FormSchemaEntity formSchemaEntity) { - this.formSchemaEntity = formSchemaEntity; - return this; - } - - public ValidationRequestResult getValidationRequestResult() { - return validationRequestResult; - } - - public SubmitTaskComposeDto setValidationRequestResult(ValidationRequestResult validationRequestResult) { - this.validationRequestResult = validationRequestResult; - return this; - } - - public DbActionResult getDbActionResult() { - return dbActionResult; - } - - public SubmitTaskComposeDto setDbActionResult(DbActionResult dbActionResult) { - this.dbActionResult = dbActionResult; - return this; - } - - public JobResult getJobResult() { - return jobResult; - } - - public SubmitTaskComposeDto setJobResult(JobResult jobResult) { - this.jobResult = jobResult; - return this; - } - - public boolean isValidForm() { - return validForm; - } - - public SubmitTaskComposeDto setValidForm(boolean validForm) { - this.validForm = validForm; - return this; - } -} diff --git a/src/main/java/com/github/stephenott/usertask/UserTaskActionsVerticle.java b/src/main/java/com/github/stephenott/usertask/UserTaskActionsVerticle.java deleted file mode 100644 index ddd4684..0000000 --- a/src/main/java/com/github/stephenott/usertask/UserTaskActionsVerticle.java +++ /dev/null @@ -1,239 +0,0 @@ -package com.github.stephenott.usertask; - -import com.github.stephenott.usertask.FailedDbActionException.FailureType; -import com.github.stephenott.usertask.entity.FormSchemaEntity; -import com.github.stephenott.usertask.entity.UserTaskEntity; -import com.github.stephenott.usertask.mongo.MongoManager; -import com.github.stephenott.usertask.mongo.Subscribers.SimpleSubscriber; -import com.mongodb.client.model.*; -import com.mongodb.client.result.UpdateResult; -import com.mongodb.reactivestreams.client.MongoCollection; -import io.vertx.core.AbstractVerticle; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.json.JsonObject; -import org.bson.Document; -import org.bson.conversions.Bson; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -public class UserTaskActionsVerticle extends AbstractVerticle { - - private final Logger log = LoggerFactory.getLogger(UserTaskActionsVerticle.class); - - private EventBus eb; - - private MongoCollection tasksCollection = MongoManager.getDatabase().getCollection("tasks", UserTaskEntity.class); - private MongoCollection formsCollection = MongoManager.getDatabase().getCollection("forms", FormSchemaEntity.class); - - @Override - public void start() throws Exception { - log.info("Starting UserTaskActionsVerticle"); - - eb = vertx.eventBus(); - - establishCompleteActionConsumer(); - establishGetActionConsumer(); - establishGetFormSchemaWithDefaultsForTaskIdConsumer(); - } - - @Override - public void stop() throws Exception { - super.stop(); - } - - private void establishCompleteActionConsumer() { - String address = "ut.action.complete"; - - eb.consumer(address, ebHandler -> { - - completeTask(ebHandler.body()).setHandler(mHandler -> { - - if (mHandler.succeeded()) { - ebHandler.reply(mHandler.result()); - log.info("Document was updated with Task Completion, new doc: " + mHandler.result().toString()); - - } else { - ebHandler.reply(new FailedDbActionException(FailureType.CANT_COMPLETE_COMMAND, "", "")); - log.error("Could not complete Mongo command to Update doc to COMPLETE", mHandler.cause()); - } - }); - - }).exceptionHandler(error -> log.error("Could not read eb message", error)); - } - - private void establishGetActionConsumer() { - String address = "ut.action.get"; - - eb.consumer(address, ebHandler -> { - - getTasks(ebHandler.body()).setHandler(mHandler -> { - - if (mHandler.succeeded()) { - log.info("Get Tasks command was completed"); - ebHandler.reply(mHandler.result()); - - } else { - log.error("Could not complete Mongo command to Get Tasks", mHandler.cause()); - ebHandler.reply(new FailedDbActionException( - FailureType.CANT_COMPLETE_COMMAND, - "Unable to complete the mongo command: " + mHandler.cause().getMessage(), - "Unable to process the request")); - } - }); - }).exceptionHandler(error -> log.error("Could not read eb message", error)); - } - - private void establishGetFormSchemaWithDefaultsForTaskIdConsumer() { - String address = "ut.action.get-form-schema-with-defaults"; - - eb.consumer(address, ebHandler -> { - - getFormSchemaWithDefaultsForTaskId(ebHandler.body()).setHandler(mHandler -> { - - if (mHandler.succeeded()) { - log.info("Get Form Schema With Defaults for Task ID completed"); - ebHandler.reply(new DbActionResult(mHandler.result())); - - } else { - log.error("Could not complete Get Form Schema with Defaults for Task ID", mHandler.cause()); - ebHandler.reply(new FailedDbActionException(FailureType.CANT_COMPLETE_COMMAND, "", "")); - - } - }); - - }).exceptionHandler(error -> log.error("Could not read eb message", error)); - } - - private Future completeTask(CompletionRequest completionRequest) { - Promise promise = Promise.promise(); - - Bson findQuery = Filters.and( - Filters.eq("zeebeSource", completionRequest.getZeebeSource()), - Filters.eq("zeebeJobKey", completionRequest.getZeebeJobKey()), - Filters.ne("state", UserTaskEntity.State.COMPLETED.toString()) // Should not be able to complete a task that is already completed - ); - - Document doc = new Document(); - doc.putAll(completionRequest.getCompletionVariables()); - - Bson updateDoc = Updates.combine( - Updates.set("state", UserTaskEntity.State.COMPLETED.toString()), - Updates.set("completeVariables", doc), - Updates.currentDate("completedAt") - ); - - FindOneAndUpdateOptions options = new FindOneAndUpdateOptions() - .returnDocument(ReturnDocument.AFTER); - - tasksCollection.findOneAndUpdate(findQuery, updateDoc, options) - .subscribe(new SimpleSubscriber().singleResult(ar -> { - if (ar.succeeded()) { - promise.complete(ar.result()); - } else { - promise.fail(ar.cause()); - } - })); - - return promise.future(); - } - - private Future> getTasks(GetRequest getRequest) { - Promise> promise = Promise.promise(); - - List findQueryItems = new ArrayList<>(); - log.info("GET REQUEST: " + getRequest.toJsonObject().toString()); - - getRequest.getTaskId().ifPresent(v -> findQueryItems.add(Filters.eq(v))); - getRequest.getState().ifPresent(v -> findQueryItems.add(Filters.eq("state", v.toString()))); - getRequest.getTitle().ifPresent(v -> findQueryItems.add(Filters.eq("title", v))); - getRequest.getAssignee().ifPresent(v -> findQueryItems.add(Filters.eq("assignee", v))); - getRequest.getDueDate().ifPresent(v -> findQueryItems.add(Filters.eq("dueDate", v))); - getRequest.getBpmnProcessId().ifPresent(v -> findQueryItems.add(Filters.eq("bpmnProcessId", v))); - getRequest.getZeebeJobKey().ifPresent(v -> findQueryItems.add(Filters.eq("zeebeJobKey", v))); - getRequest.getZeebeSource().ifPresent(v -> findQueryItems.add(Filters.eq("zeebeSource", v))); - - Bson queryFilter = (findQueryItems.isEmpty()) ? null : Filters.and(findQueryItems); - - tasksCollection.find().filter(queryFilter) - .subscribe(new SimpleSubscriber<>(ar -> { - if (ar.succeeded()) { - promise.complete(ar.result()); - - } else { - promise.fail(ar.cause()); - } - })); - return promise.future(); - } - - public Future getFormSchemaWithDefaultsForTaskId(GetTasksFormSchemaReqRes.Request request) { - Promise promise = Promise.promise(); - - promise.future().compose(task -> { - Promise stepProm = Promise.promise(); - - tasksCollection.find().filter(Filters.eq(request.getTaskId())) - .subscribe(new SimpleSubscriber().singleResult(result -> { - if (result.succeeded()) { - stepProm.complete(result.result().getFormKey()); - - } else { - stepProm.fail(result.cause()); - } - })); - return stepProm.future(); - - }).compose(formKey -> { - Promise stepProm = Promise.promise(); - - formsCollection.find().filter(Filters.eq(request.getTaskId())) - .subscribe(new SimpleSubscriber().singleResult(onDone -> { - if (onDone.succeeded()) { - stepProm.complete(onDone.result()); - -// } else { - stepProm.fail(onDone.cause()); - } - })); - return stepProm.future(); - - }).compose(formSchema -> { - promise.complete( - new GetTasksFormSchemaReqRes.Response() - .setDefaultValues(new HashMap<>()) - .setFormKey(formSchema.getKey()) - .setSchema(new JsonObject(formSchema.getSchema()).getMap()) - .setTaskId(request.getTaskId()) - ); - return Future.succeededFuture(); - }); - - return promise.future(); - } - - public Future saveFormSchema(FormSchemaEntity formSchemaEntity){ - Promise promise = Promise.promise(); - - ReplaceOptions options = new ReplaceOptions().upsert(true); - - Bson filter = Filters.eq("key", formSchemaEntity.getKey()); - - formsCollection.replaceOne(filter, formSchemaEntity, options) - .subscribe(new SimpleSubscriber().singleResult(handler -> { - if (handler.succeeded()) { - promise.complete(new DbActionResult(handler.result())); - } else { - promise.fail(new FailedDbActionException(FailureType.CANT_CREATE, "","")); - } - })); - - return promise.future(); - } - -} diff --git a/src/main/java/com/github/stephenott/usertask/UserTaskHttpServerVerticle.java b/src/main/java/com/github/stephenott/usertask/UserTaskHttpServerVerticle.java deleted file mode 100644 index a49c055..0000000 --- a/src/main/java/com/github/stephenott/usertask/UserTaskHttpServerVerticle.java +++ /dev/null @@ -1,513 +0,0 @@ -package com.github.stephenott.usertask; - -import com.github.stephenott.conf.ApplicationConfiguration; -import com.github.stephenott.executors.JobResult; -import com.github.stephenott.form.validator.ValidationRequest; -import com.github.stephenott.form.validator.ValidationRequestResult; -import com.github.stephenott.form.validator.ValidationRequestResult.Result; -import com.github.stephenott.form.validator.ValidationSchemaObject; -import com.github.stephenott.form.validator.ValidationSubmissionObject; -import com.github.stephenott.form.validator.exception.InvalidFormSubmissionException; -import com.github.stephenott.form.validator.exception.ValidationRequestResultException; -import com.github.stephenott.usertask.entity.FormSchemaEntity; -import com.github.stephenott.usertask.entity.UserTaskEntity; -import com.github.stephenott.usertask.mongo.MongoManager; -import com.github.stephenott.usertask.mongo.Subscribers.SimpleSubscriber; -import com.mongodb.client.model.Filters; -import com.mongodb.reactivestreams.client.MongoCollection; -import io.vertx.core.AbstractVerticle; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import org.bson.conversions.Bson; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static com.github.stephenott.usertask.UserTaskHttpServerVerticle.HttpUtils.addCommonHeaders; - -public class UserTaskHttpServerVerticle extends AbstractVerticle { - - private Logger log = LoggerFactory.getLogger(UserTaskHttpServerVerticle.class); - - private EventBus eb; - - private ApplicationConfiguration.UserTaskHttpServerConfiguration serverConfiguration; - - private MongoCollection formsCollection = MongoManager.getDatabase().getCollection("forms", FormSchemaEntity.class); - private MongoCollection tasksCollection = MongoManager.getDatabase().getCollection("tasks", UserTaskEntity.class); - - - @Override - public void start(Future startFuture) throws Exception { - try { - serverConfiguration = config().mapTo(ApplicationConfiguration.UserTaskHttpServerConfiguration.class); - } catch (Exception e) { - log.error("Unable to start User Task HTTP Server Verticle because config cannot be parsed", e); - stop(); - } - - int port = serverConfiguration.getPort(); - - log.info("Starting UserTaskHttpServerVerticle on port: {}", port); - - eb = vertx.eventBus(); - - Router mainRouter = Router.router(vertx); - - HttpServer server = vertx.createHttpServer(); - - mainRouter.route().failureHandler(rc -> { - addCommonHeaders(rc.response()); - log.info("PROCESSING ERROR: " + rc.failure().getClass().getCanonicalName()); - rc.response().setStatusCode(rc.statusCode()); - rc.response().end(new JsonObject().put("error", rc.failure().getMessage()).toBuffer()); - }); - - mainRouter.errorHandler(500, rc -> { - log.error("HTTP FAILURE!!!", rc.failure()); - rc.fail(500); - }); - - establishCompleteActionRoute(mainRouter); - establishGetTasksRoute(mainRouter); - establishSubmitTaskRoute(mainRouter); - establishSaveFormSchemaRoute(mainRouter); - - server.requestHandler(mainRouter).listen(port); - } - - @Override - public void stop() throws Exception { - super.stop(); - } - - private void establishSaveFormSchemaRoute(Router router) { - //@TODO move to common - String path = "/form/schema"; - - Route saveFormSchemaRoute = router.post(path) - .handler(BodyHandler.create()); //@TODO add cors - - saveFormSchemaRoute.handler(rc -> { - FormSchemaEntity formSchemaEntity = rc.getBodyAsJson().mapTo(FormSchemaEntity.class); - log.info("SCHEMA: " + formSchemaEntity.getSchema()); - - }); - } - - private void establishCompleteActionRoute(Router router) { - //@TODO move to common - String path = "/task/complete"; - - Route completeRoute = router.post(path) - .handler(BodyHandler.create()); //@TODO add cors - - completeRoute.handler(rc -> { - CompletionRequest completionRequest = rc.getBodyAsJson().mapTo(CompletionRequest.class); - - //@TODO move to common - String address = "ut.action.complete"; - - eb.request(address, completionRequest, reply -> { - if (reply.succeeded()) { - addCommonHeaders(rc.response()); - - if (reply.result().body().getResultObjects().size() == 1) { - rc.response().end(JsonObject.mapFrom(reply.result().body().getResultObjects().get(0)).toBuffer()); - } else { - log.error("No objects were returned in the resultObject of the Complete request"); - throw new IllegalStateException("Something went wrong"); - } - - } else { - throw new IllegalStateException(reply.cause()); - } - }); - - }); - } - - private void establishGetTasksRoute(Router router) { - //@TODO move to common - String address = "ut.action.get"; - //@TODO move to common - String path = "/task"; - - Route getRoute = router.get(path) - .handler(BodyHandler.create()); //@TODO add cors - - getRoute.handler(rc -> { - GetRequest getRequest = rc.getBodyAsJson().mapTo(GetRequest.class); - - eb.request(address, getRequest, reply -> { - if (reply.succeeded()) { - addCommonHeaders(rc.response()); - rc.response().end(new JsonArray(reply.result().body().getResultObjects()).toBuffer()); - - } else { - Class eClass = reply.cause().getClass(); - - if (eClass.equals(FailedDbActionException.class)) { - rc.fail(500, reply.cause()); - } else { - rc.fail(500, new IllegalStateException("Something went wrong", reply.cause())); - } - } - }); - }); - } - - private void establishSubmitTaskRoute(Router router) { - String path = "/task/id/:taskId/submit"; - - Route submitRoute = router.post(path) - .handler(BodyHandler.create()); //@TODO add cors - - submitRoute.handler(rc -> { - ValidationSubmissionObject submissionObject = rc.getBodyAsJson().mapTo(ValidationSubmissionObject.class); - - String taskId = Optional.of(rc.request().getParam("taskId")) - .orElseThrow(() -> new IllegalArgumentException("Invalid task id")); - - log.info("TASK ID: " + taskId); - - SubmitTaskComposeDto dto = new SubmitTaskComposeDto(); - getUserTaskByTaskId(taskId).compose(userTaskEntity -> { - dto.setUserTaskEntity(userTaskEntity); - return Future.succeededFuture(); - - }).compose(s2 -> { - return getFormSchemaByFormKey(dto.getUserTaskEntity().getFormKey()); - - }).compose(formSchema -> { - dto.setFormSchemaEntity(formSchema); - return Future.succeededFuture(); - - }).compose(s3 -> { - ValidationSchemaObject schema = new JsonObject(dto.getFormSchemaEntity().getSchema()) - .mapTo(ValidationSchemaObject.class); - - ValidationRequest validationRequest = new ValidationRequest() - .setSchema(schema) - .setSubmission(submissionObject); - - return validateFormSchema(validationRequest); - - }).compose(validationResult -> { - if (validationResult.getResult().equals(Result.VALID)) { - dto.setValidationRequestResult(validationResult); - dto.validForm = true; - return Future.succeededFuture(); - } else { - return Future.failedFuture(new IllegalStateException("Something went wrong, should not be here")); - } -// else { -// dto.setValidationRequestResult(validationResult); -// dto.validForm = false; -// return Future.failedFuture(new IllegalArgumentException("Invalid Form Submission")); -// } - }).compose(s4 -> { - Map completionVariables = new HashMap<>(); - String variableName = dto.getUserTaskEntity().getTaskId() + "_submission"; - completionVariables.put(variableName, dto.getValidationRequestResult().getValidResultObject().getProcessedSubmission()); - - //@TODO Refactor this call bck to zeebe to have a proper response handling - String zeebeSource = dto.getUserTaskEntity().getZeebeSource(); - JobResult jobResult = new JobResult( - dto.getUserTaskEntity().getZeebeJobKey(), - JobResult.Result.COMPLETE) - .setVariables(completionVariables); - - dto.setJobResult(jobResult); - - return completeZeebeJob(zeebeSource, jobResult); - }).compose(s5 -> { - CompletionRequest completionRequest = new CompletionRequest() - .setZeebeJobKey(dto.getUserTaskEntity().getZeebeJobKey()) - .setZeebeSource(dto.getUserTaskEntity().getZeebeSource()) - .setCompletionVariables(dto.getJobResult().getVariables()); - - return completeUserTask(completionRequest); - }).setHandler(result -> { - if (result.succeeded()) { - if (dto.validForm) { - // Valid Form Submsision and Everything went well - rc.response() - .setStatusCode(202) - .putHeader("content-type", "application/json; charset=utf-8") - .end(JsonObject.mapFrom(dto.getValidationRequestResult().getValidResultObject()).toBuffer()); - } else { - log.error("Task Submission DTO Error", new IllegalStateException("Task Submission with Form Validation succeeded by the DTO had a validForm=false... That should never occur.. something went wrong...")); - rc.fail(500, new IllegalStateException("Something went wrong. We are looking into it")); - } - } else { - if (result.cause().getClass().equals(InvalidFormSubmissionException.class)) { -// log.info("Form Submission was invalid: " + JsonObject.mapFrom(dto.getValidationRequestResult().getInvalidResultObject()).toString()); - InvalidFormSubmissionException exception = (InvalidFormSubmissionException) result.cause(); - rc.response() - .setStatusCode(400) - .putHeader("content-type", "application/json; charset=utf-8") - .end(JsonObject.mapFrom(exception.getInvalidResult()).toBuffer()); - - } else if (result.cause().getClass().equals(ValidationRequestResultException.class)) { - rc.fail(500, result.cause()); - - } else if (result.cause().getClass().equals(IllegalArgumentException.class)) { - rc.fail(400, result.cause()); - - } else { - log.error("Task Form Submission processing failed.", result.cause()); - rc.fail(500, new IllegalStateException("Something went wrong during task submission processing. We are looking into it")); - } - } - }); - - - //************ - //@TODO look at refactor with more fluent and compose - //@TODO DO MAJOR REFACTOR TO CLEAN THIS JUNK UP: WAY TOO DEEP of a PYRAMID -// getUserTaskByTaskId(taskId).setHandler(taskIdHandler -> { -// if (taskIdHandler.succeeded()) { -// -// log.info("Found Form Key: " + taskIdHandler.result().getFormKey()); -// -// getFormSchemaByFormKey(taskIdHandler.result().getFormKey()).setHandler(formKeyHandler -> { -// if (formKeyHandler.succeeded()) { -// -// String ebAddress = "forms.action.validate"; -// -// ValidationSchemaObject schema = new JsonObject(formKeyHandler.result().getSchema()).mapTo(ValidationSchemaObject.class); -// -// ValidationRequest validationRequest = new ValidationRequest() -// .setSchema(schema) -// .setSubmission(submissionObject); -// -// eb.request(ebAddress, validationRequest, ebHandler -> { -// if (ebHandler.succeeded()) { -// -// if (ebHandler.result().body().getResult().equals(Result.VALID)) { -// -// Map completionVariables = new HashMap<>(); -// String variableName = taskIdHandler.result().getTaskId() + "_submission"; -// completionVariables.put(variableName, ebHandler.result() -// .body().getValidResultObject().getProcessedSubmission()); -// -// CompletionRequest completionRequest = new CompletionRequest() -// .setZeebeJobKey(taskIdHandler.result().getZeebeJobKey()) -// .setZeebeSource(taskIdHandler.result().getZeebeSource()) -// .setCompletionVariables(completionVariables); -// -// //@TODO Refactor this call bck to zeebe to have a proper response handling -// String zeebeSource = taskIdHandler.result().getZeebeSource(); -// JobResult jobResult = new JobResult( -// taskIdHandler.result().getZeebeJobKey(), -// JobResult.Result.COMPLETE) -// .setVariables(completionVariables); -// -// //Complete the job in Zeebe: -// //@TODO Refactor this to use the future -// completeZeebeJob(zeebeSource, jobResult); -// -// -// eb.request("ut.action.complete", completionRequest, dbCompleteHandler -> { -// if (dbCompleteHandler.succeeded()) { -// -// -// } else { -// // EB Never returned a result for Completion -// log.error("Never received a response from DB Completion request over EB"); -// rc.response() -// .setStatusCode(400) -// .end(); -// } -// }); -// -// } else if (ebHandler.result().body().getResult().equals(Result.INVALID)){ -// rc.response() -// .setStatusCode(400) -// .putHeader("content-type", "application/json; charset=utf-8") -// .end(JsonObject.mapFrom(ebHandler.result().body().getInvalidResultObject()).toBuffer()); -// -// } else { -// ValidationRequestResultException exception = new JsonObject(ebHandler.result().body().getErrorResult()).mapTo(ValidationRequestResultException.class); -// rc.fail(500, exception); -// } -// } else { -// // Did not receive message back on EB from Validation Service -// throw new IllegalStateException("Did not receive a message back from the validation service"); -// } -// }); // End of Validation Request EB send -// -// } else { -// // Could not find Form Schema in DB -// throw new IllegalArgumentException("Unable to find Form Schema for provided Form key, or multiple keys were returned."); -// } -// }); // end of getFormSchemaByFormKey -// -// } else { -// // Could not find User Task in DB for the provided TaskId -// throw new IllegalArgumentException("Unable to find User Task for provided Task ID"); -// } -// }); - }); - } - - private Future completeUserTask(CompletionRequest completionRequest) { - Promise promise = Promise.promise(); - - String ebAddress = "ut.action.complete"; - - eb.request(ebAddress, completionRequest, dbCompleteHandler -> { - if (dbCompleteHandler.succeeded()) { -// promise.complete(dbCompleteHandler.result().body()); - promise.complete(); - } else { - promise.fail(new IllegalStateException("EB User Task Completion Handler failed.", dbCompleteHandler.cause())); - } - }); - return promise.future(); - } - - private Future validateFormSchema(ValidationRequest validationRequest) { - Promise promise = Promise.promise(); - - String ebAddress = "forms.action.validate"; //@TODO move to common - - eb.request(ebAddress, validationRequest, ebHandler -> { - if (ebHandler.succeeded()) { - Result result = ebHandler.result().body().getResult(); - - if (result.equals(Result.VALID)) { - promise.complete(ebHandler.result().body()); - - } else if (result.equals(Result.INVALID)) { - promise.fail(new InvalidFormSubmissionException("Form submission was invalid", ebHandler.result().body().getInvalidResultObject())); - - } else { //if it was a ERROR that was returned: - ValidationRequestResult.ErrorResult errorResult = ebHandler.result().body().getErrorResult(); - promise.fail(new ValidationRequestResultException( - errorResult.getErrorType(), - errorResult.getInternalErrorMessage(), - errorResult.getEndUserMessage())); - } - } else { - promise.fail(new IllegalStateException("Eb Response Failed for Validation Request", ebHandler.cause())); - } - }); - - return promise.future(); - } - - private Future completeZeebeJob(String zeebeSource, JobResult jobResult) { - Promise promise = Promise.promise(); - - String ebAddress = ".job-action.completion"; - - // @TODO Refactor to have a proper response - eb.request(zeebeSource + ebAddress, jobResult, result -> { - if (result.succeeded()) { - promise.complete(); - } else { - promise.fail(result.cause()); - } - }); - return promise.future(); - } - - private Future getUserTaskByTaskId(String taskId) { - //@TODO look at moving this into the UserTaskActionsVerticle - Promise promise = Promise.promise(); - - // @TODO future refactor to use a projection to only return the single Form Key field rather than the entire entity. - - if (taskId == null) { - promise.fail(new IllegalArgumentException("taskId cannot be null")); - } - - Bson findQuery = Filters.eq(taskId); - - tasksCollection.find().filter(findQuery) - .subscribe(new SimpleSubscriber().singleResult(result -> { - if (result.succeeded()) { - promise.complete(result.result()); - } else { - promise.fail(new IllegalArgumentException("Unable to find requested Task ID", result.cause())); - } - })); - return promise.future(); - } - - private Future getFormSchemaByFormKey(String formKey) { - //@TODO look at moving this its own verticle for a FormSchemaEntity Verticle - Promise promise = Promise.promise(); - - if (formKey == null) { - promise.fail(new IllegalArgumentException("formKey cannot be null")); - } - - Bson findQuery = Filters.eq("key", formKey); - - formsCollection.find().filter(findQuery) - .subscribe(new SimpleSubscriber().singleResult(result -> { - if (result.succeeded()) { - promise.complete(result.result()); - - } else { - promise.fail(new IllegalArgumentException("Unable to find Form Schema that was configured for the requested task.", result.cause())); - } - })); - return promise.future(); - } - - private void establishGetTasksRoute() { - //@TODO - } - - private void establishDeleteTaskRoute() { - //@TODO - } - - private void establishClaimTaskRoute() { - //@TODO - } - - private void establishUnClaimTaskRoute() { - //@TODO - } - - private void establishAssignTaskRoute() { - //@TODO - } - - private void establishCreateCustomTaskRoute() { - //@TODO - // Will use a custom BPMN that allows a custom single step task to be created. - // Create a config for this so the BPMN Process ID can be set in the YAML config - } - - - public static class HttpUtils { - - public static String applicationJson = "application/json"; - - public static HttpServerResponse addCommonHeaders(HttpServerResponse httpServerResponse) { - httpServerResponse.headers() - .add("content-type", applicationJson); - - return httpServerResponse; - } - - } - -} diff --git a/src/main/java/com/github/stephenott/usertask/entity/FormSchemaEntity.java b/src/main/java/com/github/stephenott/usertask/entity/FormSchemaEntity.java deleted file mode 100644 index 2184144..0000000 --- a/src/main/java/com/github/stephenott/usertask/entity/FormSchemaEntity.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.github.stephenott.usertask.entity; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRawValue; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.github.stephenott.usertask.json.deserializer.JsonToStringDeserializer; -import io.vertx.codegen.annotations.DataObject; -import io.vertx.core.json.JsonObject; - -import java.time.Instant; -import java.util.UUID; - -@DataObject -public class FormSchemaEntity { - - private String Id = UUID.randomUUID().toString(); - - private Instant createdAt = Instant.now(); - - private String owner; - - @JsonProperty(required = true) - private String key; - - @JsonProperty(required = true) - private String title; - - private String description; - - @JsonProperty(required = true) - @JsonDeserialize(using = JsonToStringDeserializer.class) - @JsonRawValue - private String schema; - - public FormSchemaEntity() { - } - - public FormSchemaEntity(JsonObject jsonObject){ - - } - - public String getId() { - return Id; - } - - public Instant getCreatedAt() { - return createdAt; - } - - public String getOwner() { - return owner; - } - - public FormSchemaEntity setOwner(String owner) { - this.owner = owner; - return this; - } - - public String getKey() { - return key; - } - - public FormSchemaEntity setKey(String key) { - this.key = key; - return this; - } - - public String getTitle() { - return title; - } - - public FormSchemaEntity setTitle(String title) { - this.title = title; - return this; - } - - public String getDescription() { - return description; - } - - public FormSchemaEntity setDescription(String description) { - this.description = description; - return this; - } - - public String getSchema() { - return schema; - } - - public FormSchemaEntity setSchema(String schema) { - this.schema = schema; - return this; - } - -} diff --git a/src/main/java/com/github/stephenott/usertask/entity/UserTaskEntity.java b/src/main/java/com/github/stephenott/usertask/entity/UserTaskEntity.java deleted file mode 100644 index 2bdb49f..0000000 --- a/src/main/java/com/github/stephenott/usertask/entity/UserTaskEntity.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.github.stephenott.usertask.entity; - -import org.bson.codecs.pojo.annotations.BsonDiscriminator; -import org.bson.codecs.pojo.annotations.BsonId; - -import java.time.Instant; -import java.util.Map; -import java.util.Set; - -@BsonDiscriminator -public class UserTaskEntity { - - @BsonId - private String taskId; - - private long olVersion = 1L; - - private Instant taskOriginalCapture = Instant.now(); - - private State state = State.NEW; - - private String title; - private String description; - private int priority = 0; - private String assignee; - private Set candidateGroups; - private Set candidateUsers; - private Instant dueDate; - private String formKey; - private Instant newAt = Instant.now(); - private Instant assignedAt; - private Instant delegatedAt; - private Instant completedAt; - private Map completeVariables; - private String zeebeSource; - private Instant zeebeDeadline; - private long zeebeJobKey; - private String bpmnProcessId; - private int bpmnProcessVersion; - private Map zeebeVariables; - - private Map metadata; - - public UserTaskEntity() { - } - - public enum State { - NEW, - ASSIGNED, - UNASSIGNED, - DELEGATED, - COMPLETED - } - - public String getTaskId() { - return taskId; - } - - public UserTaskEntity setTaskId(String taskId) { - this.taskId = taskId; - return this; - } - - /** - * Gets the optimistic locking version number - * @return - */ - public long getOlVersion() { - return olVersion; - } - - /** - * Set the optimistic locking version number - * @param olVersion - * @return - */ - public UserTaskEntity setOlVersion(long olVersion) { - this.olVersion = olVersion; - return this; - } - - public String getTitle() { - return title; - } - - public UserTaskEntity setTitle(String title) { - this.title = title; - return this; - } - - public String getDescription() { - return description; - } - - public UserTaskEntity setDescription(String description) { - this.description = description; - return this; - } - - public int getPriority() { - return priority; - } - - public UserTaskEntity setPriority(int priority) { - this.priority = priority; - return this; - } - - public String getAssignee() { - return assignee; - } - - public UserTaskEntity setAssignee(String assignee) { - this.assignee = assignee; - return this; - } - - public Set getCandidateGroups() { - return candidateGroups; - } - - public UserTaskEntity setCandidateGroups(Set candidateGroups) { - this.candidateGroups = candidateGroups; - return this; - } - - public Set getCandidateUsers() { - return candidateUsers; - } - - public UserTaskEntity setCandidateUsers(Set candidateUsers) { - this.candidateUsers = candidateUsers; - return this; - } - - public Instant getDueDate() { - return dueDate; - } - - public UserTaskEntity setDueDate(Instant dueDate) { - this.dueDate = dueDate; - return this; - } - - public String getFormKey() { - return formKey; - } - - public UserTaskEntity setFormKey(String formKey) { - this.formKey = formKey; - return this; - } - - public State getState() { - return state; - } - - public UserTaskEntity setState(State state) { - this.state = state; - return this; - } - - public Instant getNewAt() { - return newAt; - } - - public UserTaskEntity setNewAt(Instant newAt) { - this.newAt = newAt; - return this; - } - - public Instant getAssignedAt() { - return assignedAt; - } - - public UserTaskEntity setAssignedAt(Instant assignedAt) { - this.assignedAt = assignedAt; - return this; - } - - public Instant getDelegatedAt() { - return delegatedAt; - } - - public UserTaskEntity setDelegatedAt(Instant delegatedAt) { - this.delegatedAt = delegatedAt; - return this; - } - - public Instant getCompletedAt() { - return completedAt; - } - - public UserTaskEntity setCompletedAt(Instant completedAt) { - this.completedAt = completedAt; - return this; - } - - public Map getCompleteVariables() { - return completeVariables; - } - - public UserTaskEntity setCompleteVariables(Map completeVariables) { - this.completeVariables = completeVariables; - return this; - } - - public String getZeebeSource() { - return zeebeSource; - } - - public UserTaskEntity setZeebeSource(String zeebeSource) { - this.zeebeSource = zeebeSource; - return this; - } - - public Instant getZeebeDeadline() { - return zeebeDeadline; - } - - public UserTaskEntity setZeebeDeadline(Instant zeebeDeadline) { - this.zeebeDeadline = zeebeDeadline; - return this; - } - - public long getZeebeJobKey() { - return zeebeJobKey; - } - - public UserTaskEntity setZeebeJobKey(long zeebeJobKey) { - this.zeebeJobKey = zeebeJobKey; - return this; - } - - public String getBpmnProcessId() { - return bpmnProcessId; - } - - public UserTaskEntity setBpmnProcessId(String bpmnProcessId) { - this.bpmnProcessId = bpmnProcessId; - return this; - } - - public int getBpmnProcessVersion() { - return bpmnProcessVersion; - } - - public UserTaskEntity setBpmnProcessVersion(int bpmnProcessVersion) { - this.bpmnProcessVersion = bpmnProcessVersion; - return this; - } - - public Map getMetadata() { - return metadata; - } - - public UserTaskEntity setMetadata(Map metadata) { - this.metadata = metadata; - return this; - } - - public Map getZeebeVariables() { - return zeebeVariables; - } - - public UserTaskEntity setZeebeVariables(Map zeebeVariables) { - this.zeebeVariables = zeebeVariables; - return this; - } - - public Instant getTaskOriginalCapture() { - return taskOriginalCapture; - } - - public UserTaskEntity setTaskOriginalCapture(Instant taskOriginalCapture) { - this.taskOriginalCapture = taskOriginalCapture; - return this; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/usertask/json/deserializer/JsonToStringDeserializer.java b/src/main/java/com/github/stephenott/usertask/json/deserializer/JsonToStringDeserializer.java deleted file mode 100644 index 134de96..0000000 --- a/src/main/java/com/github/stephenott/usertask/json/deserializer/JsonToStringDeserializer.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.stephenott.usertask.json.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; - -import java.io.IOException; - -public class JsonToStringDeserializer extends JsonDeserializer { - @Override - public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { - return p.getCodec().readTree(p).toString(); - } -} diff --git a/src/main/java/com/github/stephenott/usertask/mongo/MongoManager.java b/src/main/java/com/github/stephenott/usertask/mongo/MongoManager.java deleted file mode 100644 index 116264d..0000000 --- a/src/main/java/com/github/stephenott/usertask/mongo/MongoManager.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.stephenott.usertask.mongo; - -import com.mongodb.reactivestreams.client.MongoClient; -import com.mongodb.reactivestreams.client.MongoDatabase; - -public class MongoManager { - - private static MongoClient client; - private static String databaseName = "default"; - - public static MongoClient getClient() { - return client; - } - - public static void setClient(MongoClient client) { - MongoManager.client = client; - } - - public static boolean isClientSet(){ - return client != null; - } - - public static String getDatabaseName() { - return databaseName; - } - - public static void setDatabaseName(String databaseName) { - MongoManager.databaseName = databaseName; - } - - public static MongoDatabase getDatabase(){ - return client.getDatabase(MongoManager.getDatabaseName()); - } -} diff --git a/src/main/java/com/github/stephenott/usertask/mongo/Subscribers.java b/src/main/java/com/github/stephenott/usertask/mongo/Subscribers.java deleted file mode 100644 index ec7a2c7..0000000 --- a/src/main/java/com/github/stephenott/usertask/mongo/Subscribers.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.github.stephenott.usertask.mongo; - -import com.mongodb.reactivestreams.client.Success; -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.Promise; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import java.util.ArrayList; -import java.util.List; - -public class Subscribers { - - public static class ObservableSubscriber implements Subscriber { - private Promise onNextPromise = Promise.promise(); - private Promise onCompletePromise = Promise.promise(); - private Promise onSubscribePromise = Promise.promise(); - private Handler> onNextHandler; - private Subscription subscription; - - private ObservableSubscriber() { - } - - - private ObservableSubscriber setOnSubscribeHandler(Handler> onSubscribeHandler){ - this.onSubscribePromise.future().setHandler(onSubscribeHandler); - return this; - } - - private ObservableSubscriber setOnNextHandler(Handler> onNextHandler){ - this.onNextHandler = onNextHandler; - this.onNextPromise.future().setHandler(onNextHandler); - return this; - } - - private ObservableSubscriber setOnCompleteHandler(Handler> onCompleteHandler){ - this.onCompletePromise.future().setHandler(onCompleteHandler); - return this; - } - - @Override - public void onSubscribe(final Subscription s) { - this.subscription = s; - this.onSubscribePromise.complete(s); - } - - @Override - public void onNext(final T t) { - onNextPromise.complete(t); - onNextPromise = Promise.promise(); - onNextPromise.future().setHandler(this.onNextHandler); - } - - @Override - public void onError(final Throwable t) { - this.onCompletePromise.fail(t); - } - - @Override - public void onComplete() { - this.onCompletePromise.complete(); - } - - public Subscription getSubscription() { - return subscription; - } - - public ObservableSubscriber setCancelSubscribtionTrigger(Promise cancelTrigger, Handler> resultHandler){ - setCancelSubscriptionTrigger(cancelTrigger).setHandler(resultHandler); - return this; - } - - public Future setCancelSubscriptionTrigger(Promise cancelTrigger){ - Promise cancelPromise = Promise.promise(); - - cancelPromise.future().setHandler(ar -> { - if (ar.succeeded()){ - try { - getSubscription().cancel(); - cancelPromise.complete(); - } catch (Exception e){ - cancelPromise.fail(new IllegalStateException("Unable to cancel promise", e)); - } - } - }); - return cancelPromise.future(); - } - - public Future cancelSubscription() { - Promise cancelSubscriptionPromise = Promise.promise(); - - try { - getSubscription().cancel(); - cancelSubscriptionPromise.complete(); - } catch (Exception e){ - cancelSubscriptionPromise.fail(new IllegalStateException("Unable to cancel subscription", e)); - } - return cancelSubscriptionPromise.future(); - } - } - - public static class SimpleSubscriber extends ObservableSubscriber { - private List received = new ArrayList<>(); - private Promise> onCompleteListPromise = Promise.promise(); - private long batchSize = 5; - private int receivedLimit = Integer.MAX_VALUE; - - public SimpleSubscriber(){ - super.setOnSubscribeHandler(this::onSubHandler) - .setOnNextHandler(this::onNextHandler) - .setOnCompleteHandler(this::onCompleteHandler); - } - - public SimpleSubscriber(Handler>> resultHandler) { - this(); - onCompleteListPromise.future().setHandler(resultHandler); - } - - public SimpleSubscriber singleResult(Handler> resultHandler){ - Promise singleResultPromise = Promise.promise(); - singleResultPromise.future().setHandler(resultHandler); - - setReceivedLimit(1); - - onCompleteListPromise.future().setHandler(asyncResult -> { - if (asyncResult.succeeded()){ - try { - singleResultPromise.complete(getReceived().get(0)); - } catch (Exception e){ - singleResultPromise.fail(new IllegalStateException("Unable to complete single result request", e)); - } - } else { - singleResultPromise.fail(new IllegalStateException("Async Result failed for On Complete Promise", asyncResult.cause())); - } - }); - return this; - } - - private void onSubHandler(AsyncResult asyncResult){ - if (asyncResult.succeeded()) { - getSubscription().request(getBatchSize()); - } else { - onCompleteListPromise.fail(new IllegalStateException("On Subscribe Handler failed.", asyncResult.cause())); - } - } - - private void onNextHandler(AsyncResult asyncResult){ - if (asyncResult.succeeded()){ - try { - getReceived().add(asyncResult.result()); - } catch (Exception e){ - getSubscription().cancel(); - onCompleteListPromise.fail(new IllegalStateException("On Next Handler failed because could not add more items to received list", e)); - } - } else { - onCompleteListPromise.fail(new IllegalStateException("On Next handler failed.", asyncResult.cause())); - } - } - - private void onCompleteHandler(AsyncResult asyncResult){ - if (asyncResult.succeeded()){ - int max = getReceivedLimit(); - - if (getReceived().size() <= max){ - onCompleteListPromise.complete(getReceived()); - } else { - onCompleteListPromise.fail(new IllegalStateException("Received " + getReceived().size() + " results, but received limit was: " + max)); - } - } else { - onCompleteListPromise.fail(new IllegalStateException("On Complete Handler failed.", asyncResult.cause())); - } - } - - public List getReceived() { - return received; - } - - public long getBatchSize() { - return batchSize; - } - - public SimpleSubscriber setBatchSize(long batchSize) { - this.batchSize = batchSize; - return this; - } - - public int getReceivedLimit() { - return receivedLimit; - } - - public SimpleSubscriber setReceivedLimit(int receivedLimit) { - this.receivedLimit = receivedLimit; - return this; - } - } -} diff --git a/src/main/java/com/github/stephenott/zeebe/client/CreateInstanceConfiguration.java b/src/main/java/com/github/stephenott/zeebe/client/CreateInstanceConfiguration.java deleted file mode 100644 index d56efde..0000000 --- a/src/main/java/com/github/stephenott/zeebe/client/CreateInstanceConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.stephenott.zeebe.client; - -import java.util.HashMap; -import java.util.Map; - -public class CreateInstanceConfiguration { - - private Long workflowKey; - private String bpmnProcessId; - private Integer bpmnProcessVersion; - private Map variables = new HashMap<>(); - - public CreateInstanceConfiguration() { - } - - public Long getWorkflowKey() { - return workflowKey; - } - - public void setWorkflowKey(Long workflowKey) { - this.workflowKey = workflowKey; - } - - public String getBpmnProcessId() { - return bpmnProcessId; - } - - public void setBpmnProcessId(String bpmnProcessId) { - this.bpmnProcessId = bpmnProcessId; - } - - public Integer getBpmnProcessVersion() { - return bpmnProcessVersion; - } - - public void setBpmnProcessVersion(Integer bpmnProcessVersion) { - if (bpmnProcessVersion < 1) { - throw new IllegalArgumentException("version cannot be less than 1"); - } else { - this.bpmnProcessVersion = bpmnProcessVersion; - } - } - - public Map getVariables() { - return variables; - } - - public void setVariables(Map variables) { - this.variables = variables; - } -} diff --git a/src/main/java/com/github/stephenott/zeebe/client/ZeebeClientConfigurationProperties.java b/src/main/java/com/github/stephenott/zeebe/client/ZeebeClientConfigurationProperties.java deleted file mode 100644 index baa9c25..0000000 --- a/src/main/java/com/github/stephenott/zeebe/client/ZeebeClientConfigurationProperties.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.github.stephenott.zeebe.client; - -import com.fasterxml.jackson.annotation.JsonCreator; -import io.vertx.core.json.JsonObject; - -import java.time.Duration; -import java.util.Objects; -import java.util.Optional; - -public class ZeebeClientConfigurationProperties { - - private JsonObject clientConfig; - - private Broker broker; - private Worker worker; - private Message message; - - @JsonCreator - public ZeebeClientConfigurationProperties(JsonObject clientConfig) { - Objects.requireNonNull(clientConfig); - - this.clientConfig = clientConfig; - - JsonObject defaults = Optional.ofNullable( - clientConfig.getJsonObject("default_config")) - .orElseThrow(IllegalStateException::new); - - this.broker = Optional.ofNullable( - defaults.getJsonObject("broker")) - .orElseThrow(IllegalStateException::new).mapTo(Broker.class); - - this.worker = Optional.ofNullable( - defaults.getJsonObject("worker").mapTo(Worker.class)) - .orElseThrow(IllegalStateException::new); - - this.message = Optional.ofNullable( - defaults.getJsonObject("message").mapTo(Message.class)) - .orElseThrow(IllegalStateException::new); - } - - public static class Broker { - private String contactPoint; - private Duration requestTimeout; - - public String getContactPoint() { - return contactPoint; - } - - public void setContactPoint(String contactPoint) { - this.contactPoint = contactPoint; - } - - public Duration getRequestTimeout() { - return requestTimeout; - } - - public void setRequestTimeout(Duration requestTimeout) { - this.requestTimeout = requestTimeout; - } - } - - public static class Worker { - private String name; - private Duration timeout; - private Integer maxJobsActive; - private Duration pollInterval; - private Integer threads; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Duration getTimeout() { - return timeout; - } - - public void setTimeout(Duration timeout) { - this.timeout = timeout; - } - - public Integer getMaxJobsActive() { - return maxJobsActive; - } - - public void setMaxJobsActive(Integer maxJobsActive) { - this.maxJobsActive = maxJobsActive; - } - - public Duration getPollInterval() { - return pollInterval; - } - - public void setPollInterval(Duration pollInterval) { - this.pollInterval = pollInterval; - } - - public Integer getThreads() { - return threads; - } - - public void setThreads(Integer threads) { - this.threads = threads; - } - } - - public static class Message { - private Duration timeToLive; - - public Duration getTimeToLive() { - return timeToLive; - } - - public void setTimeToLive(Duration timeToLive) { - this.timeToLive = timeToLive; - } - } - - public JsonObject getClientConfig() { - return clientConfig; - } - - public void setClientConfig(JsonObject clientConfig) { - this.clientConfig = clientConfig; - } - - public Broker getBroker() { - return broker; - } - - public void setBroker(Broker broker) { - this.broker = broker; - } - - public Worker getWorker() { - return worker; - } - - public void setWorker(Worker worker) { - this.worker = worker; - } - - public Message getMessage() { - return message; - } - - public void setMessage(Message message) { - this.message = message; - } -} diff --git a/src/main/java/com/github/stephenott/zeebe/client/ZeebeClientVerticle.java b/src/main/java/com/github/stephenott/zeebe/client/ZeebeClientVerticle.java deleted file mode 100644 index f769ed0..0000000 --- a/src/main/java/com/github/stephenott/zeebe/client/ZeebeClientVerticle.java +++ /dev/null @@ -1,290 +0,0 @@ -package com.github.stephenott.zeebe.client; - -import com.github.stephenott.common.Common; -import com.github.stephenott.executors.JobResult; -import com.github.stephenott.conf.ApplicationConfiguration; -import io.vertx.circuitbreaker.CircuitBreaker; -import io.vertx.circuitbreaker.CircuitBreakerOptions; -import io.vertx.core.*; -import io.vertx.core.eventbus.DeliveryOptions; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.json.Json; -import io.vertx.core.json.JsonObject; -import io.zeebe.client.ZeebeClient; -import io.zeebe.client.api.ZeebeFuture; -import io.zeebe.client.api.command.ClientException; -import io.zeebe.client.api.command.ClientStatusException; -import io.zeebe.client.api.command.FinalCommandStep; -import io.zeebe.client.api.response.ActivateJobsResponse; -import io.zeebe.client.api.response.ActivatedJob; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Duration; -import java.util.List; - - -public class ZeebeClientVerticle extends AbstractVerticle { - - private Logger log; - private EventBus eb; - - private ZeebeClient zClient; - - private ApplicationConfiguration.ZeebeClientConfiguration clientConfiguration; - - private CircuitBreaker pollingBreaker; - - @Override - public void start() throws Exception { - clientConfiguration = config().mapTo(ApplicationConfiguration.ZeebeClientConfiguration.class); - - log = LoggerFactory.getLogger("ClientVerticle." + clientConfiguration.getName()); - - pollingBreaker = CircuitBreaker.create("Breaker.ClientVerticle." + clientConfiguration.getName(), vertx, - new CircuitBreakerOptions() - .setMaxFailures(5) - .setTimeout(-1) // Timeout is currently managed by the zeebeClient usage and futures - .setFailuresRollingWindow(Duration.ofHours(1).toMillis()) - .setResetTimeout(Duration.ofMinutes(30).toMillis()) - ); - - eb = vertx.eventBus(); - - zClient = createZeebeClient(clientConfiguration); - - createJobCompletionConsumer(); - - eb.localConsumer("createJobWorker", act -> { - String jobType = act.body().getString("jobType"); - String workerName = act.body().getString("workerName"); - String timeout = act.body().getString("timeout"); - createJobWorker(jobType, workerName, timeout); - }); - - // Consumers are equal to "Zeebe Workers" - clientConfiguration.getWorkers().forEach(worker -> { - log.info("Calling for launch of Worker " + worker.getName()); - log.info("------------>TIMEOUT: " + worker.getTimeout().toString()); - worker.getJobTypes().forEach(jobType -> { - createJobWorkerWithEb(jobType, worker.getName(), worker.getTimeout()); - }); - }); - } - - - private ZeebeClient createZeebeClient(ApplicationConfiguration.ZeebeClientConfiguration configuration) { - log.info("Creating ZeebeClient for " + clientConfiguration.getName()); - - //@TODO add defaults from the App Config - return ZeebeClient.newClientBuilder() - .brokerContactPoint(configuration.getBrokerContactPoint()) - .defaultRequestTimeout(configuration.getRequestTimeout()) //@TODO Review a possible bug where if there is a PT10S, the request seems to go forever (could be because of alpha1) - .defaultMessageTimeToLive(Duration.ofHours(1)) - .usePlaintext() //@TODO remove and replace with cert /-/SECURITY/-/ - .build(); - } - - @Override - public void stop() throws Exception { - zClient.close(); - } - - private void createJobWorkerWithEb(String jobType, String workerName, Duration timeout) { - JsonObject body = new JsonObject() - .put("jobType", jobType) - .put("workerName", workerName) - .put("timeout", timeout.toString()); - - DeliveryOptions options = new DeliveryOptions().setLocalOnly(true); - eb.send("createJobWorker", body, options); - } - - private void createJobWorker(String jobType, String workerName, String timeout) { - -// pollingBreaker.execute(brkCmd -> { - - pollForJobs(jobType, workerName, timeout, pollResult -> { - - if (pollResult.succeeded()) { -// brkCmd.complete(); - - // If no results from poll: - if (pollResult.result().isEmpty()) { -// brkCmd.complete(); - log.info(workerName + " found NO Jobs for " + jobType + ", looping..."); - - createJobWorkerWithEb(jobType, workerName, Duration.parse(timeout)); - - //If found jobs in the results of the poll: - } else { - log.info(workerName + " found some Jobs for " + jobType + ", count: " + pollResult.result().size()); - - //For Each Job that was returned - pollResult.result().forEach(this::handleJob); - - log.info("Done handling jobs...."); - - createJobWorkerWithEb(jobType, workerName, Duration.parse(timeout)); //Basically a non-blocking loop - } - } else { - log.error("POLLING ERROR: ---->", pollResult.cause()); -// brkCmd.fail(pollResult.cause()); - } - }); // End of Poll - -// }); // End of Breaker - } - - private void createJobCompletionConsumer(){ - eb.consumer(clientConfiguration.getName() + ".job-action.completion").handler(msg->{ -// JobResult jobResult = msg.body().mapTo(JobResult.class); - - if (msg.body().getResult().equals(JobResult.Result.COMPLETE)){ - reportJobComplete(msg.body()).setHandler(result -> { - if (result.succeeded()){ - log.info("Zeebe job was successfully reported to cluster as completed"); - } else { - log.error("Unable to complete zeebe communication for reporting job success", result.cause()); - } - }); - //@TODO Add return over EB to confirm that job was completed - } else { - reportJobFail(msg.body()).setHandler(result -> { - if (result.succeeded()){ - log.info("Zeebe job was successfully reported to cluster as Job Fail"); - } else { - log.error("Unable to complete zeebe communication for reporting job failure", result.cause()); - } - }); - //@TODO Add return over EB to confirm that job was NOT completed - } - }); - } - - private void pollForJobs(String jobType, String workerName, String timeout, Handler>> handler) { - log.info("Starting Activate Jobs Command for: " + jobType + " " + workerName); - - //Convert to a dedicated Verticle / Thread Worker management - vertx.>executeBlocking(blockProm -> { - log.info(workerName + " is waiting for " + jobType + " jobs"); - - try { - FinalCommandStep finalCommandStep = zClient.newActivateJobsCommand() - .jobType(jobType) - .maxJobsToActivate(1) - .workerName(workerName) - .timeout(Duration.parse(timeout)); - - ZeebeFuture jobsResponse = finalCommandStep.send(); - - blockProm.complete(jobsResponse.join().getJobs()); - - } catch (Exception e) { - blockProm.fail(e); - } - }, false, result -> { - if (result.succeeded()) { - handler.handle(Future.succeededFuture(result.result())); - } else { - handler.handle(Future.failedFuture(result.cause())); - } - } - - ); - } - - private void handleJob(ActivatedJob job) { - log.info("Handling Job... {}", job.getKey()); - - DeliveryOptions options = new DeliveryOptions().setSendTimeout(1200) - .addHeader("sourceClient", clientConfiguration.getName()); - JsonObject object = (JsonObject) Json.decodeValue(job.toJson()); - - log.info("OBJECT:--> {}", object.toString()); - - String address = Common.JOB_ADDRESS_PREFIX + job.getType(); - log.info("Sending Job work to address: {}", address); - - eb.send(address, object, options); - } - - private Future reportJobComplete(JobResult jobResult) { - Promise promise = Promise.promise(); - - log.info("Reporting job is complete... {}", jobResult.getJobKey()); - - vertx.executeBlocking(blkProm -> { - //@TODO Add support for variables and custom timeout configs - // Variables are currently not supported do to complications with the builder - ZeebeFuture completeCommandFuture = zClient - .newCompleteCommand(jobResult.getJobKey()) - .send(); - - log.info("Sending Complete Command to Zeebe"); - - try { - completeCommandFuture.join(); - - log.info("Complete Command was successfully sent"); - - blkProm.complete(); - - } catch (ClientStatusException e) { - blkProm.fail(e); - } catch (ClientException e) { - blkProm.fail(e); - } - - }, false, res -> { - if (res.succeeded()) { - promise.complete(); - - } else { - promise.fail(res.cause()); - - } - }); - - return promise.future(); - } - - - private Future reportJobFail(JobResult jobResult) { - Promise promise = Promise.promise(); - - vertx.executeBlocking(blkProm -> { - ZeebeFuture failCommandFuture = zClient.newFailCommand(jobResult.getJobKey()) - .retries(jobResult.getRetries()) - .errorMessage(jobResult.getErrorMessage()) - .send(); - - log.info("Sending Fail-Command to Zeebe"); - - try { - failCommandFuture.join(); - - log.info("Fail-Command was successfully sent"); - - blkProm.complete(); - - } catch (Exception e) { - blkProm.fail(e); - } - - }, false, res -> { - if (res.succeeded()) { - //ExecuteBlocking was successfully completed - promise.complete(); - - } else { - // Error in the execute blocking - promise.fail(res.cause()); - - } - }); - - return promise.future(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/github/stephenott/zeebe/dto/ActivatedJobDto.java b/src/main/java/com/github/stephenott/zeebe/dto/ActivatedJobDto.java deleted file mode 100644 index 150f08b..0000000 --- a/src/main/java/com/github/stephenott/zeebe/dto/ActivatedJobDto.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.github.stephenott.zeebe.dto; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import io.vertx.core.json.Json; -import io.vertx.core.json.JsonObject; -import io.zeebe.client.api.response.ActivatedJob; - -import java.io.IOException; -import java.util.Map; - -public class ActivatedJobDto implements ActivatedJob { - - private long key; - private String type; - private Map customHeaders; - private long workflowInstanceKey; - private String bpmnProcessId; - private int workflowDefinitionVersion; - private long workflowKey; - private String elementId; - private long elementInstanceKey; - private String worker; - private int retries; - private long deadline; - private String variables; - - public ActivatedJobDto(){} - - public ActivatedJobDto(ActivatedJob activatedJob){ - this.key = activatedJob.getKey(); - this.type = activatedJob.getType(); - this.customHeaders = activatedJob.getCustomHeaders(); - this.workflowInstanceKey = activatedJob.getWorkflowInstanceKey(); - this.bpmnProcessId = activatedJob.getBpmnProcessId(); - this.workflowDefinitionVersion = activatedJob.getWorkflowDefinitionVersion(); - this.workflowKey = activatedJob.getWorkflowKey(); - this.elementId = activatedJob.getElementId(); - this.elementInstanceKey = activatedJob.getElementInstanceKey(); - this.worker = activatedJob.getWorker(); - this.retries = activatedJob.getRetries(); - this.deadline = activatedJob.getDeadline(); - this.variables = activatedJob.getVariables(); - } - - @Override - public long getKey() { - return this.key; - } - - @Override - public String getType() { - return this.type; - } - - @Override - public long getWorkflowInstanceKey() { - return this.workflowInstanceKey; - } - - @Override - public String getBpmnProcessId() { - return this.bpmnProcessId; - } - - @Override - public int getWorkflowDefinitionVersion() { - return this.workflowDefinitionVersion; - } - - @Override - public long getWorkflowKey() { - return this.workflowKey; - } - - @Override - public String getElementId() { - return this.elementId; - } - - @Override - public long getElementInstanceKey() { - return this.elementInstanceKey; - } - - @Override - public Map getCustomHeaders() { - return this.customHeaders; - } - - @Override - public String getWorker() { - return this.worker; - } - - @Override - public int getRetries() { - return this.retries; - } - - @Override - public long getDeadline() { - return this.deadline; - } - - @Override - public String getVariables() { - return this.variables; - } - - @Override - public Map getVariablesAsMap() { - try { - return Json.mapper.readValue(this.variables, new TypeReference>() {}); - } catch (IOException e) { - throw new IllegalStateException("Unable to convert variables to a Map", e); - } - } - - @Override - public T getVariablesAsType(Class variableType) { - try { - return Json.mapper.readValue(this.variables, variableType); - } catch (IOException e) { - throw new IllegalStateException("Unable to convert variables to type " + variableType.getName(), e); - } - } - - @Override - public String toJson() { - try { - return Json.mapper.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Unable to convert to Json String", e); - } - } - - public JsonObject toJsonObject() { - return JsonObject.mapFrom(this); - } - - - public void setKey(long key) { - this.key = key; - } - - public void setType(String type) { - this.type = type; - } - - public void setCustomHeaders(Map customHeaders) { - this.customHeaders = customHeaders; - } - - public void setWorkflowInstanceKey(long workflowInstanceKey) { - this.workflowInstanceKey = workflowInstanceKey; - } - - public void setBpmnProcessId(String bpmnProcessId) { - this.bpmnProcessId = bpmnProcessId; - } - - public void setWorkflowDefinitionVersion(int workflowDefinitionVersion) { - this.workflowDefinitionVersion = workflowDefinitionVersion; - } - - public void setWorkflowKey(long workflowKey) { - this.workflowKey = workflowKey; - } - - public void setElementId(String elementId) { - this.elementId = elementId; - } - - public void setElementInstanceKey(long elementInstanceKey) { - this.elementInstanceKey = elementInstanceKey; - } - - public void setWorker(String worker) { - this.worker = worker; - } - - public void setRetries(int retries) { - this.retries = retries; - } - - public void setDeadline(long deadline) { - this.deadline = deadline; - } - - public void setVariables(String variables) { - this.variables = variables; - } -} diff --git a/src/main/kotlin/com/github/stephenott/qtz/Application.kt b/src/main/kotlin/com/github/stephenott/qtz/Application.kt new file mode 100644 index 0000000..d6ca105 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/Application.kt @@ -0,0 +1,14 @@ +package com.github.stephenott.qtz + +import io.micronaut.runtime.Micronaut + +object Application { + + @JvmStatic + fun main(args: Array) { + Micronaut.build() + .packages("com.github.stephenott.qtz") + .mainClass(Application.javaClass) + .start() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/executors/CodeExecutor.kt b/src/main/kotlin/com/github/stephenott/qtz/executors/CodeExecutor.kt new file mode 100644 index 0000000..1083c46 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/executors/CodeExecutor.kt @@ -0,0 +1,12 @@ +package com.github.stephenott.qtz.executors + +import io.reactivex.Single +import java.io.File +import java.time.Duration + +interface CodeExecutor { + fun execute(script: File, + inputs: Map = mapOf(), + executionTimeout: Duration = Duration.ofSeconds(60) + ): Single> +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/executors/script/python/PythonExecutor.kt b/src/main/kotlin/com/github/stephenott/qtz/executors/script/python/PythonExecutor.kt new file mode 100644 index 0000000..9334f42 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/executors/script/python/PythonExecutor.kt @@ -0,0 +1,68 @@ +package com.github.stephenott.qtz.executors.script.python + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.github.stephenott.qtz.executors.CodeExecutor +import com.github.stephenott.qtz.workers.python.PythonExecutorWorkerConfiguration +import io.reactivex.Single +import java.io.File +import java.time.Duration +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PythonExecutor: CodeExecutor { + + @Inject + private lateinit var executorConfig: PythonExecutorWorkerConfiguration + + private val jsonMapper: ObjectMapper = jacksonObjectMapper() + + override fun execute(script: File, + inputs: Map, + executionTimeout: Duration): Single> { + return Single.fromCallable { + val inputFile: File = File.createTempFile(UUID.randomUUID().toString(), "-python-executor-input" ) + + jsonMapper.writeValue(inputFile, inputs) + + val executor: ExecutorService = Executors.newSingleThreadExecutor() + + val execution = executor.submit { + val pythonPath = executorConfig.pythonPath + val pythonFileToExecute = script.absolutePath + ProcessBuilder(listOf(pythonPath, pythonFileToExecute, "--inputs", inputFile.absolutePath)) + .start() + } + + val process = execution.get(executionTimeout.seconds, TimeUnit.SECONDS) + + process.waitFor(executionTimeout.seconds, TimeUnit.SECONDS) + + val exitCode:Int = process.exitValue() + + if (exitCode == 0){ + val output = process.inputStream.bufferedReader().use { it.readText() } + try{ + inputFile.delete() + jsonMapper.readValue>(output) + } catch(e: Exception) { + println("${executorConfig.workerName} Python Script Output: $output") + throw IllegalStateException("Script successfully executed but orchestrator failed to read result into JSON...", e) + } + + } else { + println("${executorConfig.workerName} Python Script Execution Exit Code: $exitCode") + + val errorOutput: String = process.errorStream.bufferedReader().use { it.readText() } + println("${executorConfig.workerName} Python Script Execution returned a failing error code: $errorOutput") + throw IllegalStateException("Failed Script Execution") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/controller/FormsController.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/controller/FormsController.kt new file mode 100644 index 0000000..18ab518 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/controller/FormsController.kt @@ -0,0 +1,124 @@ +package com.github.stephenott.qtz.forms.controller + +import com.github.stephenott.qtz.forms.domain.FormSchema +import com.github.stephenott.qtz.forms.domain.FormEntity +import com.github.stephenott.qtz.forms.domain.FormSchemaEntity +import com.github.stephenott.qtz.forms.repository.FormsRepository +import com.github.stephenott.qtz.forms.repository.FormSchemasRepository +import com.github.stephenott.qtz.forms.validator.exception.FormValidationException +import com.github.stephenott.qtz.forms.validator.client.FormValidatorServiceClient +import com.github.stephenott.qtz.forms.validator.client.ValidationResponseInvalid +import com.github.stephenott.qtz.tasks.service.UserTasksService +import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.* +import io.micronaut.validation.Validated +import io.reactivex.Single +import java.util.* +import javax.inject.Inject + + +@Controller("/forms") +class FormsController( + private val formValidatorServiceClient: FormValidatorServiceClient +) : FormsOperations { + + @Inject + lateinit var formsRepository: FormsRepository + + @Inject + lateinit var formSchemasRepository: FormSchemasRepository + + + @Post() + override fun saveForm(@Body form: Single): Single> { + return form.flatMapPublisher { + formsRepository.save(it.toFormEntity()) + }.singleOrError().map { + HttpResponse.ok(it) + } + } + + @Get("{?formId}{?formKey}") + override fun getForm(formId: UUID?, formKey: String?, pageable: Pageable?): Single>> { + formId?.let { uuid -> + return Single.fromPublisher(formsRepository.findById(uuid)).map { HttpResponse.ok(listOf(it)) } + } + formKey?.let { key -> + return formsRepository.findByFormKey(key) + .switchIfEmpty(Single.error(UserTasksService.FormKeyNotFoundException(key))) + .map { HttpResponse.ok(listOf(it)) } + } + return formsRepository.findAll(pageable ?: Pageable.from(0, 10)) //@TODO Refactor to a configurable default + .map { + HttpResponse.ok(it.content) + //@TODO Add headers function helper to set common list headers + //@TODO Add .next() support + .header("X-Total-Count", it.totalSize.toString()) + .header("X-Page-Count", it.numberOfElements.toString()) + } + } + + @Post("/{formId}/schemas") + override fun addSchema(formId: UUID, @Body schema: Single): Single> { + + return schema.flatMapPublisher { + val entity = it.toFormSchemaEntity(FormEntity(id = formId)) + formSchemasRepository.save(entity) + }.singleOrError().map { + HttpResponse.ok(it) + } + } + + @Get("/{formId}/schemas") + override fun getSchemasByFormId(formId: UUID, pageable: Pageable?): Single>> { + //@TODO Refactor to make pageable a configurable option + return formSchemasRepository.findByForm( + FormEntity(id = formId), + pageable + ?: Pageable.from(0, 10, Sort.of(Sort.Order.desc("version"))) + ).map { page -> + HttpResponse.ok(page.content) //@TODO Refactor to use projections on DB level + .header("X-Total-Count", page.totalSize.toString()) + .header("X-Page-Count", page.numberOfElements.toString()) + } + } + + @Error + fun formValidationError(request: HttpRequest<*>, exception: FormValidationException): HttpResponse { + return HttpResponse.badRequest(exception.responseBody) + } +} + +@Validated +interface FormsOperations { + + fun saveForm(form: Single): Single> + + fun getForm(formId: UUID?, formKey: String?, pageable: Pageable?): Single>> + + fun addSchema(formId: UUID, schema: Single): Single> + + fun getSchemasByFormId(formId: UUID, pageable: Pageable?): Single>> +} + + +data class FormSaveRequest( + val name: String, + val description: String? = null, + val formKey: String +) { + fun toFormEntity(): FormEntity { + return FormEntity(name = name, description = description, formKey = formKey) + } +} + +data class FormSchemaSaveRequest( + val schema: FormSchema +) { + fun toFormSchemaEntity(formEntity: FormEntity): FormSchemaEntity { + return FormSchemaEntity(schema = schema, form = formEntity) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/domain/Converters.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/Converters.kt new file mode 100644 index 0000000..d0b2628 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/Converters.kt @@ -0,0 +1,33 @@ +package com.github.stephenott.qtz.forms.domain + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +//@TODO Refactor to support a Generic converter that impl can extend from. +@Converter(autoApply = true) +class FormSchemaAttributeConverter : AttributeConverter { + + companion object { + //@TODO refactor to a common mapper for db operations + val mapper: ObjectMapper = jacksonObjectMapper() + } + + override fun convertToDatabaseColumn(attribute: FormSchema?): ByteArray? { + return if (attribute == null){ + null + } else{ + mapper.writeValueAsBytes(attribute) + } + } + + override fun convertToEntityAttribute(dbData: ByteArray?): FormSchema? { + return if (dbData == null || dbData.isEmpty()){ + null + } else { + mapper.readValue(dbData) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormEntity.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormEntity.kt new file mode 100644 index 0000000..ceffd55 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormEntity.kt @@ -0,0 +1,31 @@ +package com.github.stephenott.qtz.forms.domain + +import com.fasterxml.jackson.annotation.JsonIgnore +import io.micronaut.data.annotation.DateCreated +import io.micronaut.data.annotation.DateUpdated +import java.time.Instant +import java.util.* +import javax.persistence.* + +@Entity +data class FormEntity( + + @field:Id + var id: UUID? = UUID.randomUUID(), + + @field:DateCreated + var createdAt: Instant? = null, + + @field:DateUpdated + var updatedAt: Instant? = null, + + var name: String? = null, + + var description: String? = null, + + var formKey: String? = null, + + @field:OneToMany(mappedBy = "form", fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @get:JsonIgnore + var schemas: Set? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormSchema.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormSchema.kt new file mode 100644 index 0000000..9e3be05 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormSchema.kt @@ -0,0 +1,7 @@ +package com.github.stephenott.qtz.forms.domain + +data class FormSchema( + val display:String, + val components: List>, + val settings: Map? = null +){} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormSchemaEntity.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormSchemaEntity.kt new file mode 100644 index 0000000..8a90c32 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/domain/FormSchemaEntity.kt @@ -0,0 +1,31 @@ +package com.github.stephenott.qtz.forms.domain + +import com.fasterxml.jackson.annotation.JsonIgnore +import io.micronaut.data.annotation.DateCreated +import io.micronaut.data.annotation.DateUpdated +import java.time.Instant +import java.util.* +import javax.persistence.* + +@Entity +data class FormSchemaEntity( + + @field:Id + var id: UUID? = UUID.randomUUID(), + + @field:DateCreated + var createdAt: Instant? = null, + + @field:DateUpdated + var updatedAt: Instant? = null, + + var version: Long? = null, + + @field:ManyToOne(fetch = FetchType.EAGER, optional = false) + @field:JsonIgnore + var form: FormEntity? = null, + + @field:Column(columnDefinition = "JSON") + @field:Convert(converter = FormSchemaAttributeConverter::class) + var schema: FormSchema? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/repository/FormSchemasRepository.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/repository/FormSchemasRepository.kt new file mode 100644 index 0000000..02780bf --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/repository/FormSchemasRepository.kt @@ -0,0 +1,16 @@ +package com.github.stephenott.qtz.forms.repository + +import com.github.stephenott.qtz.forms.domain.FormEntity +import com.github.stephenott.qtz.forms.domain.FormSchemaEntity +import io.micronaut.data.annotation.Repository +import io.micronaut.data.model.Page +import io.micronaut.data.model.Pageable +import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository +import io.reactivex.Single +import java.util.* + +@Repository +interface FormSchemasRepository : ReactiveStreamsCrudRepository { + + fun findByForm(form: FormEntity, pageable: Pageable): Single> +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/repository/FormsRepository.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/repository/FormsRepository.kt new file mode 100644 index 0000000..af67a7d --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/repository/FormsRepository.kt @@ -0,0 +1,18 @@ +package com.github.stephenott.qtz.forms.repository + +import com.github.stephenott.qtz.forms.domain.FormEntity +import io.micronaut.data.annotation.Repository +import io.micronaut.data.model.Page +import io.micronaut.data.model.Pageable +import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository +import io.reactivex.Maybe +import io.reactivex.Single +import java.util.* + +@Repository +interface FormsRepository : ReactiveStreamsCrudRepository { + + fun findByFormKey(formKey: String): Maybe + + fun findAll(pageable: Pageable): Single> +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/service/FormsService.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/service/FormsService.kt new file mode 100644 index 0000000..c93174f --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/service/FormsService.kt @@ -0,0 +1,15 @@ +package com.github.stephenott.qtz.forms.service + +import javax.inject.Singleton + + +@Singleton +class FormsService(){ + + fun validateSubmission(){ + + } + + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/submission/Repository/SubmissionRepository.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/submission/Repository/SubmissionRepository.kt new file mode 100644 index 0000000..627f600 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/submission/Repository/SubmissionRepository.kt @@ -0,0 +1,10 @@ +package com.github.stephenott.qtz.forms.submission.repository + +import com.github.stephenott.qtz.forms.submission.domain.SubmissionEntity +import io.micronaut.data.annotation.Repository +import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository +import java.util.* + +@Repository +interface SubmissionRepository : ReactiveStreamsCrudRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/submission/domain/Converters.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/submission/domain/Converters.kt new file mode 100644 index 0000000..407b40e --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/submission/domain/Converters.kt @@ -0,0 +1,32 @@ +package com.github.stephenott.qtz.forms.submission.domain + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +@Converter(autoApply = true) +class SubmissionObjectAttributeConverter : AttributeConverter { + + companion object { + //@TODO refactor to a common mapper for db operations + val mapper: ObjectMapper = jacksonObjectMapper() + } + + override fun convertToDatabaseColumn(attribute: SubmissionObject?): ByteArray? { + return if (attribute == null){ + null + } else{ + mapper.writeValueAsBytes(attribute) + } + } + + override fun convertToEntityAttribute(dbData: ByteArray?): SubmissionObject? { + return if (dbData == null || dbData.isEmpty()){ + null + } else { + mapper.readValue(dbData) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/submission/domain/Submission.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/submission/domain/Submission.kt new file mode 100644 index 0000000..6e29f1f --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/submission/domain/Submission.kt @@ -0,0 +1,37 @@ +package com.github.stephenott.qtz.forms.submission.domain + +import io.micronaut.data.annotation.DateCreated +import io.micronaut.data.annotation.DateUpdated +import java.time.Instant +import java.util.* +import javax.persistence.Column +import javax.persistence.Convert +import javax.persistence.Entity +import javax.persistence.Id + +@Entity +data class SubmissionEntity( + + @field:Id + var id: UUID? = UUID.randomUUID(), + + @field:DateCreated + var createdAt: Instant? = null, + + @field:DateUpdated + var updatedAt: Instant? = null, + + var submitter: String? = null, + + @field:Column(columnDefinition = "JSON") + @field:Convert(converter = SubmissionObjectAttributeConverter::class) + var submission: SubmissionObject? = null, + + var formKeyAndVersion: String? = null, + + var destinationSystem: String? = null, + + var transferState: String? = null +) + +data class SubmissionObject(val submission: Map?) \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/validator/client/ValidatorClient.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/client/ValidatorClient.kt new file mode 100644 index 0000000..3dc90a4 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/client/ValidatorClient.kt @@ -0,0 +1,35 @@ +package com.github.stephenott.qtz.forms.validator.client + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES +import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES +import com.github.stephenott.qtz.forms.validator.domain.FormSubmission +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client +import io.micronaut.jackson.annotation.JacksonFeatures +import io.reactivex.Single + + +@Client("\${formValidatorService.host}") +@JacksonFeatures( + enabledDeserializationFeatures = [ + FAIL_ON_NULL_FOR_PRIMITIVES, + FAIL_ON_IGNORED_PROPERTIES + ] +) +interface FormValidatorServiceClient { + + @Post("/validate") + fun validate(@Body validationRequest: Single): Single> + +} + +data class ValidationResponseValid(val processed_submission: Map) + +data class ValidationResponseInvalid(@get:JsonProperty("isJoi") @param:JsonProperty("isJoi") val isJoi: Boolean, + val name: String, + val details: List>, + val _object: Map, + val _validated: Map) \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/validator/controller/FormsValidatorController.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/controller/FormsValidatorController.kt new file mode 100644 index 0000000..3a0840c --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/controller/FormsValidatorController.kt @@ -0,0 +1,52 @@ +package com.github.stephenott.qtz.forms.validator.controller + +import com.github.stephenott.qtz.forms.validator.domain.FormSubmission +import com.github.stephenott.qtz.forms.validator.exception.FormValidationException +import com.github.stephenott.qtz.forms.validator.client.FormValidatorServiceClient +import com.github.stephenott.qtz.forms.validator.client.ValidationResponseInvalid +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.validation.Validated +import io.reactivex.Single + + +@Controller("/forms") +class FormsController( + private val formValidatorServiceClient: FormValidatorServiceClient +) : FormsOperations { + + @Post(value = "/validate") + override fun validate(@Body submission: Single): Single>> { + return formValidatorServiceClient.validate(submission) + .onErrorResumeNext { + // @TODO Can eventually be replaced once micronaut-core fixes a issue where the response body is not passed to @Error handler when it catches the HttpClientResponseException + if (it is HttpClientResponseException) { + val body = it.response.getBody(ValidationResponseInvalid::class.java) + if (body.isPresent) { + Single.error(FormValidationException(body.get())) + } else { + Single.error(IllegalStateException("Invalid Response Received", it)) + } + } else { + Single.error(IllegalStateException("Unexpected Error received from Form Validation request.", it)) + } + }.map { + HttpResponse.ok(it.body()!!.processed_submission) + } + } + + @Error + fun formValidationError(request: HttpRequest<*>, exception: FormValidationException): HttpResponse { + return HttpResponse.badRequest(exception.responseBody) + } +} + +@Validated +interface FormsOperations { + fun validate(submission: Single): Single>> +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/validator/domain/FormSubmission.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/domain/FormSubmission.kt new file mode 100644 index 0000000..868f9e6 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/domain/FormSubmission.kt @@ -0,0 +1,12 @@ +package com.github.stephenott.qtz.forms.validator.domain + +import com.github.stephenott.qtz.forms.domain.FormSchema + +data class FormSubmission( + val schema: FormSchema, + val submission: FormSubmissionData) {} + +data class FormSubmissionData( + val data: Map, + val metadata: Map?) {} + diff --git a/src/main/kotlin/com/github/stephenott/qtz/forms/validator/exception/FormValidationExceptions.kt b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/exception/FormValidationExceptions.kt new file mode 100644 index 0000000..5df9e9a --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/forms/validator/exception/FormValidationExceptions.kt @@ -0,0 +1,5 @@ +package com.github.stephenott.qtz.forms.validator.exception + +import com.github.stephenott.qtz.forms.validator.client.ValidationResponseInvalid + +class FormValidationException(val responseBody: ValidationResponseInvalid) : RuntimeException("Form Validation Exception") {} diff --git a/src/main/kotlin/com/github/stephenott/qtz/linter/Cleaner.kt b/src/main/kotlin/com/github/stephenott/qtz/linter/Cleaner.kt new file mode 100644 index 0000000..b6c8791 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/linter/Cleaner.kt @@ -0,0 +1,154 @@ +package com.github.stephenott.qtz.linter + +import io.zeebe.model.bpmn.Bpmn +import io.zeebe.model.bpmn.BpmnModelInstance +import io.zeebe.model.bpmn.instance.* +import org.camunda.bpm.model.xml.impl.instance.DomElementImpl +import org.camunda.bpm.model.xml.instance.DomElement +import org.camunda.bpm.model.xml.instance.ModelElementInstance +import org.camunda.bpm.model.xml.validation.ModelElementValidator +import org.dom4j.dom.DOMElement +import java.io.File +import kotlin.reflect.full.isSubclassOf + +/** + * Cleaner will provide capability to clean sections of a BPMN that throw a specific error + * + * Cleaning means to replace a Element with a new instance of the element, but with zero configuration / a blank instance of the element + */ +class Cleaner(private val validators: List>, + private val CLEANER_CODE: Int = 5000) { + + fun cleanModel(bpmnFile: File): BpmnModelInstance { + return cleanModel(Bpmn.readModelFromFile(bpmnFile)) + } + + fun cleanModel(model: BpmnModelInstance): BpmnModelInstance { + model.validate(validators).results + .forEach { (element, results) -> + //@TODO add support for Gateway/sequence flow support to keep configs for default + println(element.elementType.instanceType) + println((element as BaseElement).id) + println("NEW ELEMENT") + results.filter { it.code == CLEANER_CODE }.forEach cleaners@{ eResult -> + val newInstance = eResult.element.modelInstance.newInstance(eResult.element.elementType, (eResult.element as BaseElement).id) + + if (eResult.element.elementType.instanceType == Process::class.java) { + return@cleaners + } + + if (eResult.element.elementType.instanceType.kotlin.isSubclassOf(Expression::class)) { + return@cleaners + } + + println("cat") + + if (eResult.element.elementType.instanceType == Collaboration::class.java) { + return@cleaners + } + + if (eResult.element.elementType.instanceType == MultiInstanceLoopCharacteristics::class.java) { + return@cleaners + } + + if (eResult.element.elementType.instanceType.kotlin.isSubclassOf(EventDefinition::class)) { + return@cleaners + } + + if (eResult.element.elementType.instanceType.kotlin == CategoryValue::class) { + (newInstance as CategoryValue).value = (eResult.element as CategoryValue).value + } + + if (eResult.element.elementType.instanceType.kotlin == LaneSet::class) { + (newInstance as LaneSet).lanes.addAll((eResult.element as LaneSet).lanes) + } + + if (eResult.element.elementType.instanceType.kotlin == Lane::class) { + (newInstance as Lane).flowNodeRefs.addAll((eResult.element as Lane).flowNodeRefs) + } + + if (eResult.element.elementType.instanceType.kotlin == Participant::class) { + //@TODO review how to do this without using the attributeValue processRef, and use proper typing. + // unclear on how to create a "process" and link it based on the processRef attribute. + if ((eResult.element as Participant).getAttributeValue("processRef") != null){ + (newInstance as Participant).setAttributeValue("processRef", (eResult.element as Participant).getAttributeValue("processRef")) + } + } + + if (eResult.element.elementType.instanceType.kotlin.isSubclassOf(BoundaryEvent::class)) { + (newInstance as BoundaryEvent).attachedTo = (eResult.element as BoundaryEvent).attachedTo + (newInstance as BoundaryEvent).setCancelActivity((eResult.element as BoundaryEvent).cancelActivity()) + + (eResult.element as BoundaryEvent).eventDefinitions.forEach { + val newEventInstance: EventDefinition = eResult.element.modelInstance.newInstance(it.elementType, it.id) + newInstance.addChildElement(newEventInstance) + } + } + + if (eResult.element.elementType.instanceType.kotlin.isSubclassOf(Activity::class)) { + + val loopChars = (eResult.element as Activity).getChildElementsByType(MultiInstanceLoopCharacteristics::class.java) + + if (loopChars.isNotEmpty()) { + check(loopChars.size == 1, lazyMessage = { "Bad BPMN...MultiInstanceLoopCharacteristics found more than 1 configuration..." }) + val newLoopChars = eResult.element.modelInstance.newInstance(MultiInstanceLoopCharacteristics::class.java, loopChars.single().id) + newLoopChars.isSequential = loopChars.single().isSequential + newInstance.addChildElement(newLoopChars) + } + } + + if (eResult.element.elementType.instanceType == SequenceFlow::class.java) { + (newInstance as SequenceFlow).source = (eResult.element as SequenceFlow).source + (newInstance as SequenceFlow).target = (eResult.element as SequenceFlow).target + } + + if (eResult.element.elementType.instanceType == MessageFlow::class.java) { + (newInstance as MessageFlow).source = (eResult.element as MessageFlow).source + (newInstance as MessageFlow).target = (eResult.element as MessageFlow).target + } + + if (eResult.element.elementType.instanceType == Association::class.java) { + (newInstance as Association).source = (eResult.element as Association).source + (newInstance as Association).target = (eResult.element as Association).target + (newInstance as Association).associationDirection = (eResult.element as Association).associationDirection + } + + if (eResult.element.elementType.instanceType == TextAnnotation::class.java) { + (newInstance as TextAnnotation).text = (eResult.element as TextAnnotation).text + (newInstance as TextAnnotation).textFormat = (eResult.element as TextAnnotation).textFormat + } + + if (eResult.element.elementType.instanceType.kotlin.isSubclassOf(Gateway::class)) { + val defaultAttributeValue = "default" + println("DEFAULT: ${eResult.element.getAttributeValue("default")}") + if (eResult.element.getAttributeValue(defaultAttributeValue) != null) { + newInstance.setAttributeValue(defaultAttributeValue, eResult.element.getAttributeValue(defaultAttributeValue)) + } + } + + if (eResult.element.getAttributeValue("name") != null) { + newInstance.setAttributeValue("name", eResult.element.getAttributeValue("name")) + } + + println("reached replacement") + when (element.elementType.instanceType.kotlin) { + SubProcess::class -> { + val elements = (element as SubProcess).flowElements + elements.forEach { newInstance.addChildElement(it) } + element.parentElement.replaceChildElement(element, newInstance) + } + Message::class -> { + // This ensures the Message is actually removed from the BPMN. + // If you remove all instances of usage of the Message (such as on Receive Tasks and Message Catch Events), the Message element is still present in the BPMN xml. + element.parentElement.removeChildElement(element) + } + else -> { + element.parentElement.replaceChildElement(element, newInstance) + } + } + } + } + return model + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/linter/ElementValidatorFactories.kt b/src/main/kotlin/com/github/stephenott/qtz/linter/ElementValidatorFactories.kt new file mode 100644 index 0000000..05e8241 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/linter/ElementValidatorFactories.kt @@ -0,0 +1,32 @@ +package com.github.stephenott.qtz.linter + +import org.camunda.bpm.model.xml.instance.ModelElementInstance +import org.camunda.bpm.model.xml.validation.ModelElementValidator +import org.camunda.bpm.model.xml.validation.ValidationResultCollector +import kotlin.reflect.KClass + +inline fun elementValidator(clazz: KClass, crossinline validationLogic: (element: T, validatorResultCollector: ValidationResultCollector) -> Unit): ModelElementValidator{ + return object:ModelElementValidator{ + override fun validate(element: T, validationResultCollector: ValidationResultCollector) { + validationLogic.invoke(element, validationResultCollector) + } + + override fun getElementType(): Class { + return clazz.java + } + } +} + +inline fun elementValidator(crossinline validationLogic: (element: T, validatorResultCollector: ValidationResultCollector) -> Unit): ModelElementValidator{ + return object:ModelElementValidator{ + override fun validate(element: T, validationResultCollector: ValidationResultCollector) { + validationLogic.invoke(element, validationResultCollector) + } + + override fun getElementType(): Class { + return T::class.java + } + } +} + + diff --git a/src/main/kotlin/com/github/stephenott/qtz/linter/Extensions.kt b/src/main/kotlin/com/github/stephenott/qtz/linter/Extensions.kt new file mode 100644 index 0000000..02390eb --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/linter/Extensions.kt @@ -0,0 +1,84 @@ +package com.github.stephenott.qtz.linter + +import io.zeebe.model.bpmn.instance.BaseElement +import io.zeebe.model.bpmn.instance.zeebe.* +import org.camunda.bpm.model.xml.instance.ModelElementInstance + +/** + * Returns null if no task definition info + */ +fun BaseElement.getZeebeTaskDefinition(): ZeebeTaskDefinition? { + return this.extensionElements.elementsQuery.filterByType(ZeebeTaskDefinition::class.java) + .list().singleOrNull() +} + +/** + * Returns null if no headers + */ +fun BaseElement.getZeebeTaskHeaders(): ZeebeTaskHeaders? { + return this.extensionElements.elementsQuery.filterByType(ZeebeTaskHeaders::class.java) + .list().singleOrNull() +} + +/** + * Returns nul if no subscription data + */ +fun BaseElement.getZeebeSubscription(): ZeebeSubscription? { + return this.extensionElements.elementsQuery.filterByType(ZeebeSubscription::class.java) + .list().singleOrNull() +} + +/** + * returns null if no IO mappings + */ +fun BaseElement.getZeebeIoMapping(): ZeebeIoMapping? { + return this.extensionElements.elementsQuery.filterByType(ZeebeIoMapping::class.java) + .list().singleOrNull() +} + +/** + * Returns null if no loop characteristics + */ +fun BaseElement.getZeebeLoopCharacteristics(): ZeebeLoopCharacteristics? { + return this.extensionElements.elementsQuery.filterByType(ZeebeLoopCharacteristics::class.java) + .list().singleOrNull() +} + +/** + * Basic helper to provide the element type name. Just saves a step. + */ +fun ModelElementInstance.getElementTypeName(): String{ + return this.elementType.typeName +} + +/** + * Checks if model element instance is a BaseElement. BaseElements are core of BPMN elements that the Model API of Zeebe uses from Camunda. + */ +fun ModelElementInstance.isBaseElement(): Boolean{ + return this is BaseElement +} + +/** + * Ensures that the headers have all required keys + */ +fun ZeebeTaskHeaders.hasRequiredKeys(keys: List): Boolean{ + return this.headers.map { it.key }.containsAll(keys) +} + +/** + * Ensures that the keys list contains all of the items that are in the headers. + * Should be used in together with .hasRequiredKeys() + */ +fun ZeebeTaskHeaders.hasOptionalAndRequiredKeys(keys: List): Boolean{ + return keys.containsAll(this.headers.map { it.key }) +} + +fun ZeebeTaskHeaders.noDuplicateKeys(restrictedKeys: List? = null): Boolean{ + return if (restrictedKeys == null){ + // No duplicate headers + !this.headers.groupingBy { it.key }.eachCount().any { it.value > 1 } + } else { + // no duplicate headers only if the header is in the restricted Keys list + !this.headers.filter { it.key in restrictedKeys }.groupingBy { it.key }.eachCount().any { it.value > 1 } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/linter/LinterCfg.kt b/src/main/kotlin/com/github/stephenott/qtz/linter/LinterCfg.kt new file mode 100644 index 0000000..bb6a35d --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/linter/LinterCfg.kt @@ -0,0 +1,138 @@ +package com.github.stephenott.qtz.linter + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.* +import io.micronaut.core.convert.ConversionContext +import io.micronaut.core.convert.TypeConverter +import org.camunda.bpm.model.xml.instance.ModelElementInstance +import org.camunda.bpm.model.xml.validation.ModelElementValidator +import java.util.* +import javax.inject.Singleton +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotEmpty +import kotlin.reflect.KClass + +//@TOOD Implement Regex mappings and rules + +@EachProperty("orchestrator.workflow-linter.rules") +@Context +class LinterRule { + var enabled: Boolean = true + + @NotBlank + var description: String? = null + + @NotBlank @NotEmpty + var elementTypes: List? = null + + var target: TargetRule? = null + + var headerRule: HeaderRule? = null + var serviceTaskRule: ServiceTaskRule? = null + var baseElementRule: BaseElementRule? = null + + + @ConfigurationProperties("target") + class TargetRule{ + var serviceTasks: TargetsServiceTaskRule? = null + var receiveTasks: TargetsReceiveTaskRule? = null + + @ConfigurationProperties("serviceTasks") + class TargetsServiceTaskRule { + var types: List = listOf() + } + + @ConfigurationProperties("receiveTasks") + class TargetsReceiveTaskRule{ + var correlationKeys: List = listOf() + } + } + + @ConfigurationProperties("headerRule") + class HeaderRule{ + var requiredKeys: List = listOf() + var optionalKeys: List = listOf() + var requiredKeysRegex: Regex? = null + var optionalKeysRegex: Regex? = null + var allowedNonDefinedKeys: Boolean = true + var allowedDuplicateKeys: Boolean = true + } + + @ConfigurationProperties("serviceTaskRule") + class ServiceTaskRule { + var allowedTypes: List? = null + var allowedTypesRegex: Regex? = null + var allowedRetriesRegex: Regex? = null + } + + @ConfigurationProperties("baseElementRule") + class BaseElementRule { + var elementNameRegex: Regex? = null + } +} + + +interface ElementTypeZeebe{ + val zeebeClass: KClass +} + +enum class ElementType: ElementTypeZeebe { + ServiceTask { + override val zeebeClass: KClass = io.zeebe.model.bpmn.instance.ServiceTask::class + } +} + +@Singleton +class ElementTypeTypeConverter: TypeConverter{ + override fun convert(`object`: String, targetType: Class?, context: ConversionContext?): Optional { + return Optional.of(ElementType.valueOf(`object`)) + } +} + +object LinterConfigurationParser{ + fun getLintRuleBeans(applicationContext: ApplicationContext): List{ + return applicationContext.getBeansOfType(LinterRule::class.java).toList() + } + + fun lintRulesToValidators(linterRules: List): List>{ + val myList: MutableList> = mutableListOf() + linterRules.forEach { lr -> + + //Apply the rules for Each Element type that was provided + // Current assumption is that rules should be aware of what elements they apply to + lr.elementTypes!!.forEach { elementType -> + val elementClass = elementType.zeebeClass + + lr.headerRule?.let { headerRule -> + LinterRules.processRequiredHeadersRule(elementClass, headerRule.requiredKeys, lr.target)?.let { v -> + myList.add(v) + } + + if (!headerRule.allowedNonDefinedKeys){ + LinterRules.processOptionalHeadersRule(elementClass, headerRule.optionalKeys, headerRule.requiredKeys, lr.target)?.let { v -> + myList.add(v) + } + } + + if (!headerRule.allowedDuplicateKeys){ + LinterRules.processDuplicateHeaderKeysRule(elementClass, lr.target)?.let { v -> + myList.add(v) + } + } + } + + lr.serviceTaskRule?.let { serviceTaskRule -> + //@TODO Review need to move this rule to global space for controlling global allowed types + serviceTaskRule.allowedTypes?.let { types -> + LinterRules.processServiceTaskAllowedTypesListRule(elementClass, types, lr.target)?.let { v -> + myList.add(v) + } + } + } + + } + + } + return myList + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/linter/LinterRules.kt b/src/main/kotlin/com/github/stephenott/qtz/linter/LinterRules.kt new file mode 100644 index 0000000..456a56d --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/linter/LinterRules.kt @@ -0,0 +1,74 @@ +package com.github.stephenott.qtz.linter + +import io.zeebe.model.bpmn.instance.ServiceTask +import org.camunda.bpm.model.xml.instance.ModelElementInstance +import org.camunda.bpm.model.xml.validation.ModelElementValidator +import kotlin.reflect.KClass + +object LinterRules { + fun processRequiredHeadersRule(element: KClass, requiredHeaders: List, targeting: LinterRule.TargetRule?): ModelElementValidator?{ + if (element != ServiceTask::class){ + return null + } else if (requiredHeaders.isNullOrEmpty()){ + return null + } + + return elementValidator(element){e, v -> + val sTask = e as ServiceTask + if (targeting == null || targeting.serviceTasks?.types!!.contains(sTask.getZeebeTaskDefinition()?.type)){ + if (sTask.getZeebeTaskHeaders()?.hasRequiredKeys(requiredHeaders) == false){ + v.addError(0, "Missing Required Headers: $requiredHeaders") + } + } + } + } + + fun processOptionalHeadersRule(element: KClass, optionalHeaders: List, requiredHeaders: List, targeting: LinterRule.TargetRule?): ModelElementValidator?{ + if (element != ServiceTask::class){ + return null + } else if (requiredHeaders.isNullOrEmpty()){ + return null + } + + val mergedList: List = optionalHeaders + requiredHeaders + + return elementValidator(element){e, v -> + val sTask = e as ServiceTask + if (targeting == null || targeting.serviceTasks?.types!!.contains(sTask.getZeebeTaskDefinition()?.type)){ + if (sTask.getZeebeTaskHeaders()?.hasOptionalAndRequiredKeys(mergedList) == false){ + v.addError(0, "Found headers that are not part of Optional Headers list: $optionalHeaders") + } + } + } + } + + fun processDuplicateHeaderKeysRule(element: KClass, targeting: LinterRule.TargetRule?): ModelElementValidator?{ + if (element != ServiceTask::class){ + return null + } + + return elementValidator(element){e, v -> + val sTask = e as ServiceTask + if (targeting == null || targeting.serviceTasks?.types!!.contains(sTask.getZeebeTaskDefinition()?.type)) { + if (sTask.getZeebeTaskHeaders()?.noDuplicateKeys() == false) { + v.addError(0, "Duplicates Keys were detected") + } + } + } + } + + fun processServiceTaskAllowedTypesListRule(element: KClass, allowedTypes: List, targeting: LinterRule.TargetRule?): ModelElementValidator?{ + if (element != ServiceTask::class){ + return null + } + + return elementValidator(element){e, v -> + val sTask = e as ServiceTask + sTask.getZeebeTaskDefinition()?.let { + if (it.type !in allowedTypes){ + v.addError(0, "Service Task Type ${it.type} is not in allowed types $allowedTypes") + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/linter/WorkflowLinter.kt b/src/main/kotlin/com/github/stephenott/qtz/linter/WorkflowLinter.kt new file mode 100644 index 0000000..362e62c --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/linter/WorkflowLinter.kt @@ -0,0 +1,28 @@ +package com.github.stephenott.qtz.linter + +import io.zeebe.model.bpmn.Bpmn +import io.zeebe.model.bpmn.BpmnModelInstance +import io.zeebe.model.bpmn.instance.BaseElement +import org.camunda.bpm.model.xml.validation.ModelElementValidator +import java.io.File + +class WorkflowLinter(file: File) { + + private val bpmmModelInstance: BpmnModelInstance = Bpmn.readModelFromFile(file) + + fun lintWithValidators(validators: List>){ + val result = bpmmModelInstance.validate(validators) + result.results.forEach { (model, results) -> + println("Element ---> ${model.elementType.typeName} ${(model.takeIf { it is BaseElement } as BaseElement).id}") + results.forEach { validationResult -> + println(""" + Type: ${validationResult.type} + Code: ${validationResult.code} + Element Type: ${model.elementType.typeName} + Element Id: ${(model.takeIf { it is BaseElement } as BaseElement).id} + Message: ${validationResult.message} + """.trimIndent()) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/tasks/controller/UserTasksController.kt b/src/main/kotlin/com/github/stephenott/qtz/tasks/controller/UserTasksController.kt new file mode 100644 index 0000000..c8f7d68 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/tasks/controller/UserTasksController.kt @@ -0,0 +1,132 @@ +package com.github.stephenott.qtz.tasks.controller + +import com.github.stephenott.qtz.forms.domain.FormSchema +import com.github.stephenott.qtz.forms.validator.domain.FormSubmissionData +import com.github.stephenott.qtz.forms.validator.exception.FormValidationException +import com.github.stephenott.qtz.forms.validator.client.ValidationResponseInvalid +import com.github.stephenott.qtz.tasks.domain.UserTaskEntity +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import com.github.stephenott.qtz.tasks.repository.UserTasksRepository +import com.github.stephenott.qtz.tasks.service.AssignTaskRequest +import com.github.stephenott.qtz.tasks.service.CreateCustomTaskRequest +import com.github.stephenott.qtz.tasks.service.UserTasksService +import io.micronaut.data.model.Pageable +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.* +import io.reactivex.Maybe +import io.reactivex.Single +import java.util.* +import javax.inject.Inject + +@Controller("/tasks") +open class UserTasksController() : UserTasksControllerOperations { + + @Inject + lateinit var userTaskRepository: UserTasksRepository + + @Inject + lateinit var userTasksService: UserTasksService + + @Get("/") + override fun getAllTasks(pageable: Pageable?): Single>> { + return userTaskRepository.findAll(pageable ?: Pageable.from(0, 50)).map { page -> + HttpResponse.ok(page.content) + .header("X-Total-Count", page.totalSize.toString()) + .header("X-Page-Count", page.numberOfElements.toString()) + } + } + + @Get("/{taskId}/") + override fun getTaskById(taskId: UUID): Maybe> { + return userTaskRepository.findById(taskId) + .map { + //@TODO add error handling for cannot find taskid + HttpResponse.ok(it) + } + } + + override fun claimTask(taskId: UUID): Single> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun unClaimTask(taskId: UUID): Single> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + @Post("/{taskId}/assign") + override fun assignTask(taskId: UUID, @Body assignee: Single): Single> { + return assignee.flatMap { + userTasksService.assignTask(taskId, it) + }.map { + HttpResponse.ok(it) + } + } + + + @Get("/{taskId}/form") + override fun getTaskForm(taskId: UUID): Single> { + //@TODO consider adding a wrapper object that has metadata such as formKey + return userTasksService.getMostRecentTaskForm(taskId) + .map { + HttpResponse.ok(it) + } + } + + @Post("/{taskId}/complete") + override fun completeTask(taskId: UUID, @Body variables: Single): Single> { + return variables.flatMap { vars -> + userTasksService.completeTask(taskId, vars) + }.doOnError { + println("Error occurred when trying to complete task ${taskId}: ${it.message}") + it.printStackTrace() + }.map { + HttpResponse.ok(it) + } + } + + @Post("/{taskId}/submit") + override fun submitTask(taskId: UUID, @Body submission: Single): Single> { + return submission.flatMap { + userTasksService.submitTask(taskId, it) + }.map { + HttpResponse.ok(it) + } + } + + @Post("/") + override fun createCustomTask(taskId: UUID, @Body task: Single): Single> { + return task.flatMap { taskRequest -> + userTasksService.createCustomTask(taskId, taskRequest).map { entity -> + HttpResponse.created(entity) + } + } + } + + @Error + fun formValidationError(request: HttpRequest<*>, exception: FormValidationException): HttpResponse { + return HttpResponse.badRequest(exception.responseBody) + } + +} + +interface UserTasksControllerOperations { + + fun getAllTasks(pageable: Pageable?): Single>> + + fun getTaskById(taskId: UUID): Maybe> + + fun claimTask(taskId: UUID): Single> + + fun unClaimTask(taskId: UUID): Single> + + fun assignTask(taskId: UUID, assignee: Single): Single> + + fun getTaskForm(taskId: UUID): Single> + + fun completeTask(taskId: UUID, variables: Single): Single> + + fun submitTask(taskId: UUID, submission: Single): Single> + + fun createCustomTask(taskId: UUID, task: Single): Single> +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/tasks/domain/Converters.kt b/src/main/kotlin/com/github/stephenott/qtz/tasks/domain/Converters.kt new file mode 100644 index 0000000..e2fe820 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/tasks/domain/Converters.kt @@ -0,0 +1,57 @@ +package com.github.stephenott.qtz.tasks.domain + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +@Converter(autoApply = true) +class ZeebeVariablesAttributeConverter : AttributeConverter { + + companion object { + //@TODO refactor to a common mapper for db operations + val mapper: ObjectMapper = jacksonObjectMapper() + } + + override fun convertToDatabaseColumn(attribute: ZeebeVariables?): ByteArray? { + return if (attribute == null){ + null + } else{ + mapper.writeValueAsBytes(attribute) + } + } + + override fun convertToEntityAttribute(dbData: ByteArray?): ZeebeVariables? { + return if (dbData == null || dbData.isEmpty()){ + null + } else { + mapper.readValue(dbData) + } + } +} + +@Converter(autoApply = true) +class UserTaskMetadataAttributeConverter : AttributeConverter { + + companion object { + //@TODO refactor to a common mapper for db operations + val mapper: ObjectMapper = jacksonObjectMapper() + } + + override fun convertToDatabaseColumn(attribute: UserTaskMetadata?): ByteArray? { + return if (attribute == null){ + null + } else{ + mapper.writeValueAsBytes(attribute) + } + } + + override fun convertToEntityAttribute(dbData: ByteArray?): UserTaskMetadata? { + return if (dbData == null || dbData.isEmpty()){ + null + } else { + mapper.readValue(dbData) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/tasks/domain/UserTask.kt b/src/main/kotlin/com/github/stephenott/qtz/tasks/domain/UserTask.kt new file mode 100644 index 0000000..91fff17 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/tasks/domain/UserTask.kt @@ -0,0 +1,87 @@ +package com.github.stephenott.qtz.tasks.domain + +import com.fasterxml.jackson.annotation.JsonAnyGetter +import com.fasterxml.jackson.annotation.JsonAnySetter +import io.micronaut.data.annotation.DateUpdated +import java.time.Instant +import java.util.* +import javax.persistence.* +import javax.validation.constraints.Max +import javax.validation.constraints.Min + +@Entity +data class UserTaskEntity( + @field:Id + @field:GeneratedValue + var taskId: UUID? = null, + + @field:Version + var olVersion: Long? = null, + + @field:DateUpdated + var updatedAt: Instant? = null, + + var taskOriginalCapture: Instant? = null, + + var state: UserTaskState? = null, + + var title: String? = null, + + var description: String? = null, + + @field:Min(-255) @field:Max(255) + var priority: Int? = null, + + var assignee: String? = null, + + @field:ElementCollection(fetch = FetchType.EAGER) + var candidateGroups: Set? = null, + + @field:ElementCollection(fetch = FetchType.EAGER) + var candidateUsers: Set? = null, + + var dueDate: Instant? = null, + + var formKey: String? = null, + + var completedAt: Instant? = null, + + @field:Column(columnDefinition = "JSON") + @field:Convert(converter = ZeebeVariablesAttributeConverter::class) + var completeVariables: ZeebeVariables? = null, + + var zeebeSource: String? = null, + + var zeebeJobKey: Long? = null, + + var zeebeBpmnProcessId: String? = null, + + var zeebeBpmnProcessKey: Long? = null, + + var zeebeElementInstanceKey: Long? = null, + + var zeebeElementId: String? = null, + + var zeebeBpmnProcessVersion: Int? = null, + + var zeebeJobRetriesRemaining: Int? = null, + + var zeebeJobDealine: Instant? = null, + + @field:Column(columnDefinition = "JSON") + @field:Convert(converter = ZeebeVariablesAttributeConverter::class) + var zeebeVariablesAtCapture: ZeebeVariables? = null, + + @field:Column(columnDefinition = "JSON") + @field:Convert(converter = UserTaskMetadataAttributeConverter::class) + var metadata: UserTaskMetadata? = null +) + +//@TODO Create History table that tracks changes to tasks such as claim, unclaim, assign, priority changes, etc. + +enum class UserTaskState { + NEW, ASSIGNED, UNASSIGNED, COMPLETED +} + +data class ZeebeVariables(@JsonAnySetter @get:JsonAnyGetter val variables: Map = mapOf()) +data class UserTaskMetadata(val metadata: Map? = null) \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/tasks/repository/UserTasksRepository.kt b/src/main/kotlin/com/github/stephenott/qtz/tasks/repository/UserTasksRepository.kt new file mode 100644 index 0000000..a5c908a --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/tasks/repository/UserTasksRepository.kt @@ -0,0 +1,21 @@ +package com.github.stephenott.qtz.tasks.repository + +import com.github.stephenott.qtz.tasks.domain.UserTaskEntity +import com.github.stephenott.qtz.tasks.domain.UserTaskState +import io.micronaut.data.annotation.Repository +import io.micronaut.data.model.Page +import io.micronaut.data.model.Pageable +import io.micronaut.data.repository.reactive.RxJavaCrudRepository +import io.reactivex.Single +import java.util.* + +@Repository +interface UserTasksRepository: RxJavaCrudRepository{ + fun update(entity: UserTaskEntity): Single + fun findAll(pageable: Pageable): Single> + fun findByStateInList(state: List?, pageable: Pageable): Single> + fun findByAssigneeAndStateInList(assignee: String?, state: List?, pageable: Pageable): Single> + fun findByCandidateGroupsInListAndStateInList(candidateGroups: List?, state: List?, pageable: Pageable): Single> + fun findByCandidateUsersInListAndStateInList(candidateUsers: List?, state: List?, pageable: Pageable): Single> + fun findByStateInListAndAssigneeInListOrCandidateGroupsInListOrCandidateUsersInList(state: List?, assignee: List?, candidateGroups: List?, candidateUsers: List?, pageable: Pageable): Single> +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/tasks/service/UserTasksService.kt b/src/main/kotlin/com/github/stephenott/qtz/tasks/service/UserTasksService.kt new file mode 100644 index 0000000..542d19a --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/tasks/service/UserTasksService.kt @@ -0,0 +1,269 @@ +package com.github.stephenott.qtz.tasks.service + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.github.stephenott.qtz.forms.domain.FormSchema +import com.github.stephenott.qtz.forms.domain.FormEntity +import com.github.stephenott.qtz.forms.repository.FormsRepository +import com.github.stephenott.qtz.forms.repository.FormSchemasRepository +import com.github.stephenott.qtz.forms.validator.client.FormValidatorServiceClient +import com.github.stephenott.qtz.forms.validator.client.ValidationResponseInvalid +import com.github.stephenott.qtz.forms.validator.domain.FormSubmission +import com.github.stephenott.qtz.forms.validator.domain.FormSubmissionData +import com.github.stephenott.qtz.forms.validator.exception.FormValidationException +import com.github.stephenott.qtz.tasks.domain.UserTaskEntity +import com.github.stephenott.qtz.tasks.domain.UserTaskMetadata +import com.github.stephenott.qtz.tasks.domain.UserTaskState +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import com.github.stephenott.qtz.tasks.repository.UserTasksRepository +import com.github.stephenott.qtz.zeebe.management.repository.ZeebeManagementRepository +import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.reactivex.Completable +import io.reactivex.Single +import java.time.Instant +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserTasksService() { + + @Inject + lateinit var userTasksRepository: UserTasksRepository + + @Inject + lateinit var formSchemaRepository: FormSchemasRepository + + @Inject + lateinit var formRepository: FormsRepository + + @Inject + lateinit var zeebeManagementRepository: ZeebeManagementRepository + + @Inject + lateinit var formValidatorService: FormValidatorServiceClient + + fun createCustomTask(taskId: UUID, task: CreateCustomTaskRequest): Single { + return Single.just(task).map { + UserTaskEntity(title = it.title, + description = it.description, + priority = it.priority, + assignee = it.assignee, + candidateGroups = it.candidateGroups, + candidateUsers = it.candidateUsers, + dueDate = it.dueDate, + formKey = it.formKey, + metadata = it.metadata) + + }.flatMap { + userTasksRepository.update(it).onErrorResumeNext { e -> + Single.error(e) //@TODO add proper error handling + } + } + } + + fun updateTask(entity: UserTaskEntity): Single { + requireNotNull(entity.taskId, lazyMessage = { "taskId cannot be null when updating a User Task." }) + return userTasksRepository.update(entity) + } + + fun deleteTask(taskId: UUID): Completable { + return userTasksRepository.deleteById(taskId) + } + + /** + * An Administrative endpoint that lets you submit the data directly into the UserTask DB and Zeebe without any Form Validation + */ + fun completeTask(taskId: UUID, variables: ZeebeVariables): Single { + return userTasksRepository.findById(taskId) + .map { task -> + require(task.state != UserTaskState.COMPLETED, lazyMessage = { "User Task is already completed." }) + task.completeVariables = variables + task.state = UserTaskState.COMPLETED + task.completedAt = Instant.now() + task + //@TODO review if this should be sent into another .map{} : + }.flatMapSingle { task -> + reportCompletionToZeebe(task.zeebeJobKey!!, variables).toSingleDefault(task) + }.flatMap { task -> + userTasksRepository.update(task) + .onErrorResumeNext { + Single.error(it) //@TODO add better error handling + } + } + } + + class UserTaskNotFoundException(val taskId: UUID) : RuntimeException("Provided Task ID $taskId could not be found") {} + class FormKeyNotFoundException(val formKey: String) : RuntimeException("Provided formKey $formKey could not be found") {} + class NoSchemasFoundForFormKeyException(val formKey: String) : RuntimeException("Provided formKey $formKey could not be found") {} + class UnableToCompleteZeebeJobException(val taskId: UUID, zeebeJobKey: Long) : RuntimeException("Unable to complete Zeebe job $zeebeJobKey for task $taskId") {} + class UnableToUpdateUserTaskWithCompletionException(val taskId: UUID) : RuntimeException("Unable to update user task $taskId to completed state.") + + /** + * Full flow of UserTask and Form Validation and Zebee Completion. + * The typical function used for submiting completed User Tasks. + */ + fun submitTask(taskId: UUID, submissionVariables: FormSubmissionData): Single { + return userTasksRepository.findById(taskId) + .switchIfEmpty(Single.error(UserTaskNotFoundException(taskId))) + .map { task -> + //Get User Task Entity and make some updates + require(task.state != UserTaskState.COMPLETED, lazyMessage = { "User Task is already completed." }) + + task.state = UserTaskState.COMPLETED + task.completedAt = Instant.now() + + SubmitTaskRequest(task).apply { + formSubmissionData = submissionVariables + } + + }.flatMap { submitTaskRequest -> + // Get the Form Schema for the task + getMostRecentTaskForm(submitTaskRequest.userTaskEntity.taskId!!) + .map { schema -> + submitTaskRequest.apply { + formSchema = schema + } + } + }.flatMap { submitTaskRequest -> + // Validate the submission against the Schema + formValidatorService.validate(Single.just(FormSubmission(submitTaskRequest.formSchema!!, submitTaskRequest.formSubmissionData!!))) + .map { validatorResponse -> + submitTaskRequest.apply { + val processedSubmission = validatorResponse.body()!!.processed_submission + userTaskEntity.completeVariables = ZeebeVariables(variables = mapOf( + Pair("${userTaskEntity.zeebeElementId!!}_task_submission", mapOf( + Pair("submission", processedSubmission), + Pair("formKey", userTaskEntity.formKey!!))))) + + validFormResponse = processedSubmission + } + }.onErrorResumeNext { + // @TODO Can eventually be replaced once micronaut-core fixes a issue where the response body is not passed to @Error handler when it catches the HttpClientResponseException + if (it is HttpClientResponseException) { + val body = it.response.getBody(ValidationResponseInvalid::class.java) + if (body.isPresent) { + Single.error(FormValidationException(body.get())) + } else { + Single.error(IllegalStateException("Invalid Response Received", it)) + } + } else { + Single.error(IllegalStateException("Unexpected Error received from Form Validation request.", it)) + } + } + }.flatMap { submitTaskRequest -> + //Report Completion to Zeebe + reportCompletionToZeebe(submitTaskRequest.userTaskEntity.zeebeJobKey!!, submitTaskRequest.userTaskEntity.completeVariables!!) + .onErrorResumeNext { + Completable.error(UnableToCompleteZeebeJobException(submitTaskRequest.userTaskEntity.taskId!!, submitTaskRequest.userTaskEntity.zeebeJobKey!!)) + }.toSingleDefault(submitTaskRequest) + .doOnSuccess { + println("User Task ${submitTaskRequest.userTaskEntity.taskId!!} has been reported to Zeebe as Job Complete.") + } + //@TODO future consideration: The CompletedAt timestamp for the UserTaskEntity is from the original submission and not the Zeebe job completion. + }.flatMap { submitTaskRequest -> + // Submit final result to DB + userTasksRepository.update(submitTaskRequest.userTaskEntity) + .onErrorResumeNext { + Single.error(UnableToUpdateUserTaskWithCompletionException(submitTaskRequest.userTaskEntity.taskId!!)) //@TODO add better error handling + } + } + } + + fun assignTask(taskId: UUID, assignee: AssignTaskRequest, requireUnassigned: Boolean = false): Single { + return userTasksRepository.findById(taskId).flatMapSingle { ut -> + + if (requireUnassigned && ut.assignee != null) { + throw IllegalStateException("Task is already assigned.") + } + + ut.assignee = assignee.assignee + + userTasksRepository.update(ut) + .onErrorResumeNext { e -> + Single.error(e) // @TODO add update error handling + } + }.onErrorResumeNext { e -> + Single.error(e) // @TODO add error handling + } + } + + fun removeAssigneeFromTask(taskId: UUID, requestingUserId: String): Single { + return userTasksRepository.findById(taskId).flatMapSingle { ut -> + + if (ut.assignee != requestingUserId) { + throw IllegalStateException("Task is not assigned to requesting user.") + } + + ut.assignee = null + + userTasksRepository.update(ut) + .onErrorResumeNext { e -> + Single.error(e) //@TODO + } + }.onErrorResumeNext { e -> + Single.error(e) //@TODO + } + } + + private fun reportCompletionToZeebe(jobKey: Long, completionVariables: ZeebeVariables): Completable { + return zeebeManagementRepository.completeJob(jobKey, completionVariables) + } + + private fun reportFailureToZeebe(jobKey: Long, errorMessage: String, remainingRetries: Int = 0): Completable { + return zeebeManagementRepository.reportJobFailure(jobKey, errorMessage, remainingRetries) + } + + fun getMostRecentTaskForm(taskId: UUID): Single { + return userTasksRepository.findById(taskId) + .switchIfEmpty(Single.error(UserTaskNotFoundException(taskId))) + .flatMap { + formRepository.findByFormKey(it.formKey!!) + .switchIfEmpty(Single.error(FormKeyNotFoundException(it.formKey!!))) + }.flatMap { formEntity -> + formSchemaRepository.findByForm(FormEntity(id = formEntity.id!!), + Pageable.from(0, 1, Sort.of(Sort.Order.desc("version")))) + .map { + if (it.isEmpty){ + throw NoSchemasFoundForFormKeyException(formEntity.formKey!!) + } else { + require(it.numberOfElements == 1, + lazyMessage = { "Could not find a form schema for the formKey of the provided Task ID." }) + it.content[0].schema!! + } + } + } + } +} + + +data class AssignTaskRequest( + val assignee: String +) + +data class SubmitTaskRequest( + @JsonIgnore var userTaskEntity: UserTaskEntity +) { + @JsonIgnore + var formSchema: FormSchema? = null + + @JsonIgnore + var formSubmissionData: FormSubmissionData? = null + + @JsonIgnore + var validFormResponse: Map? = null + +} + +data class CreateCustomTaskRequest( + val title: String, + val description: String? = null, + val priority: Int? = 0, + val assignee: String? = null, + val candidateGroups: Set? = null, + val candidateUsers: Set? = null, + val dueDate: Instant? = null, + val formKey: String, + val metadata: UserTaskMetadata? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/GenericWorker.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/GenericWorker.kt new file mode 100644 index 0000000..b432ff5 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/GenericWorker.kt @@ -0,0 +1,198 @@ +package com.github.stephenott.qtz.workers + +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import com.github.stephenott.qtz.zeebe.ZeebeClientConfiguration +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Single +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers +import io.zeebe.client.ZeebeClient +import io.zeebe.client.api.command.ClientStatusException +import io.zeebe.client.api.response.ActivatedJob +import java.time.Duration +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + + +class GenericWorker( + private val jobProcessor: JobProcessor, + private val jobFailedProcessor: JobFailedProcessor, + private val zClientConfig: ZeebeClientConfiguration, + private val workerConfig: WorkerConfiguration +) { + + private var workerActive: Boolean = false + + private lateinit var zClient: ZeebeClient + + private var currentActiveJobs: AtomicInteger = AtomicInteger(0) + + fun getWorkerName(): String { + return workerConfig.workerName + } + + fun workerIsActive(): Boolean { + return workerActive + } + + fun start(): Completable { + return Completable.fromAction { + if (!workerActive) { + this.zClient = createDefaultZeebeClient() + + this.workerActive = true + + //@TODO add retryWhen logic to support specific expected errors such as inability to connect to Zeebe broker + + startTaskCapture() + .repeatUntil { !this.workerActive } + .subscribeOn(Schedulers.io()).subscribeBy( + onError = { + if (it is ClientStatusException && it.message == "Channel shutdownNow invoked" && !workerActive) { + workerActive = false + + println("${getWorkerName()} worker was stopped due to requested stop command.") + + } else { + workerActive = false + + zClient.close() + + throw IllegalStateException("${getWorkerName()} Worker has unexpectedly stopped due to: ${it.message}", it) + } + }, + //@TODO add error catching that caches when Channel ShutdownNow invoked error occurs (from when management controller stops the worker. + onComplete = { + println("${getWorkerName()} Looping complete...") + }, + onNext = { + if (it.isEmpty()) { + Thread.sleep(1000) // Wait for 1 second so looping does not occur at overly-aggressive rate + } else { + processJobs(it).subscribeBy( + onComplete = { println("${getWorkerName()} batch for processing jobs complete") }, + onError = { println("Major error with ${getWorkerName()} processing jobs: ${it.message}") }) + } + } + ) + } else { + println("${getWorkerName()} worker is already active, no need to start the worker.") + } + } + } + + fun stop(): Completable { + return Completable.fromAction { + if (!workerActive) { + println("${getWorkerName()} Deactivation of worker was requested, but worker is already stopped.") + } else { + println("${getWorkerName()} Stopping worker...") + this.workerActive = false + this.zClient.close() + } + } + } + + private fun processJobs(jobs: List): Completable { + return Completable.fromAction { + Flowable.fromIterable(jobs).forEach { job -> + jobProcessor.processJob(job) + .subscribeOn(Schedulers.io()).subscribeBy( + onSuccess = { jobResult -> + this.currentActiveJobs.decrementAndGet() + + println("${getWorkerName()} Worker has completed processing Job ${job.key}.") + + if (jobResult.reportResult) { + reportJobSuccess(job, jobResult.resultVariables) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onError = { error -> + //@TODO add retry support + println("${getWorkerName()} Failed to report success of job ${job.key} back to Zeebe: ${error.message}") + error.printStackTrace() + }, + onComplete = { + println("${getWorkerName()} Job ${job.key} was successfully reported as a success to Zeebe.") + }) + } + }, + onError = { + this.currentActiveJobs.decrementAndGet() + + val errorMessage = it.message + ?: "${getWorkerName()} Job ${job.key} failed to complete successfully." + + jobFailedProcessor.processFailedJob(this.zClient, job, errorMessage) + } + ) + } + } + } + + private fun reportJobSuccess(job: ActivatedJob, resultVariables: ZeebeVariables): Completable { + return Completable.fromAction { + zClient.newCompleteCommand(job.key) + .variables(resultVariables.variables) + .send().join(zClientConfig.commandTimeout.seconds.plus(1), TimeUnit.SECONDS) + } + } + + private fun startTaskCapture(): Single> { + return Single.fromCallable { + val currentActiveJobsSnapshot: Int = this.currentActiveJobs.get() + val maxJobsToActivate: Int = this.workerConfig.maxBatchSize - currentActiveJobsSnapshot + + if (maxJobsToActivate != 0) { + check(currentActiveJobsSnapshot in 0..this.workerConfig.maxBatchSize, + lazyMessage = { "${getWorkerName()} Max Jobs bound/limit was out breached!" }) + + val jobs = pollForZeebeJobs( + this.workerConfig.taskType, + this.workerConfig.workerName, + this.workerConfig.taskMaxZeebeLock, + maxJobsToActivate) + + .toList() + .doOnSuccess { + println("${getWorkerName()} Polling returned: ${it.size} jobs.") + } + .subscribeOn(Schedulers.io()).blockingGet() + + this.currentActiveJobs.addAndGet(jobs.size) + + jobs + + } else { + listOf() + } + } + } + + private fun pollForZeebeJobs(jobType: String, workerName: String, taskLockTimeout: Duration, maxJobsToActivate: Int): Flowable { + return Flowable.fromCallable { + this.zClient.newActivateJobsCommand().jobType(jobType) + .maxJobsToActivate(maxJobsToActivate) + .timeout(taskLockTimeout) + .workerName(workerName) + .requestTimeout(zClientConfig.longPollTimeout) + .send() + .join(zClientConfig.longPollTimeout.seconds.plus(5), TimeUnit.SECONDS) + + }.doOnSubscribe { + println("${getWorkerName()} Starting Long Polling for $maxJobsToActivate jobs (${zClientConfig.longPollTimeout.seconds} second cycle)...") + }.flatMap { + Flowable.fromIterable(it.jobs) + } + } + + private fun createDefaultZeebeClient(): ZeebeClient { + return ZeebeClient.newClientBuilder() + .brokerContactPoint(zClientConfig.brokerContactPoint) //@TODO add rest of default configurations + .defaultRequestTimeout(zClientConfig.commandTimeout) + .defaultMessageTimeToLive(zClientConfig.messageTimeToLive) + .usePlaintext() //@TODO remove and replace with cert /-/SECURITY/-/ + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/JobFailedProcessor.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/JobFailedProcessor.kt new file mode 100644 index 0000000..30280b8 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/JobFailedProcessor.kt @@ -0,0 +1,9 @@ +package com.github.stephenott.qtz.workers + +import io.reactivex.Completable +import io.zeebe.client.ZeebeClient +import io.zeebe.client.api.response.ActivatedJob + +interface JobFailedProcessor { + fun processFailedJob(zClient: ZeebeClient, job: ActivatedJob, errorMessage: String): Completable +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/JobProcessor.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/JobProcessor.kt new file mode 100644 index 0000000..be52fae --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/JobProcessor.kt @@ -0,0 +1,20 @@ +package com.github.stephenott.qtz.workers + +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import io.reactivex.Single +import io.zeebe.client.api.response.ActivatedJob + +interface JobProcessor { + fun processJob(job: ActivatedJob): Single +} + +data class JobResult( + val resultVariables: ZeebeVariables = ZeebeVariables(), + + /** + * Indicates if the Worker should report the successful processing of the job right away. + * Typically set to True. + * Use Case for setting to False is for User Tasks where a Job Completion only occurs sometime in the future. + */ + val reportResult: Boolean = true +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/WorkerConfiguration.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/WorkerConfiguration.kt new file mode 100644 index 0000000..37c4efb --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/WorkerConfiguration.kt @@ -0,0 +1,11 @@ +package com.github.stephenott.qtz.workers + +import java.time.Duration + +interface WorkerConfiguration { + var enabled: Boolean + var taskType: String + var workerName: String + var taskMaxZeebeLock: Duration + var maxBatchSize: Int +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorFailedJobProcessor.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorFailedJobProcessor.kt new file mode 100644 index 0000000..aa8ad03 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorFailedJobProcessor.kt @@ -0,0 +1,38 @@ +package com.github.stephenott.qtz.workers.python + +import com.github.stephenott.qtz.workers.JobFailedProcessor +import com.github.stephenott.qtz.zeebe.ZeebeManagementClientConfiguration +import io.reactivex.Completable +import io.reactivex.schedulers.Schedulers +import io.zeebe.client.ZeebeClient +import io.zeebe.client.api.response.ActivatedJob +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PythonExecutorFailedJobProcessor: JobFailedProcessor { + + @Inject + private lateinit var zClientConfig: ZeebeManagementClientConfiguration + + override fun processFailedJob(zClient: ZeebeClient, job: ActivatedJob, errorMessage: String): Completable { + return Completable.fromCallable { + zClient.newFailCommand(job.key) + .retries(job.retries.minus(1)) + .errorMessage(errorMessage) + .send() + .join(zClientConfig.commandTimeout.seconds.plus(5), TimeUnit.SECONDS) + + }.subscribeOn(Schedulers.io()) + .doOnSubscribe { + println("Attempting to report failure of Python Executor job: ${job.key} with error message: ${errorMessage}.") + }.doOnComplete { + println("Successfully reported Failure of Python Executor Job: ${job.key} with error message: ${errorMessage}.") + }.doOnError { + println("Unable to report Python Executor job ${job.key} failure: Error was: ${it.message}") + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorJobProcessor.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorJobProcessor.kt new file mode 100644 index 0000000..7c69b87 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorJobProcessor.kt @@ -0,0 +1,46 @@ +package com.github.stephenott.qtz.workers.python + +import com.github.stephenott.qtz.executors.script.python.PythonExecutor +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import com.github.stephenott.qtz.workers.JobProcessor +import com.github.stephenott.qtz.workers.JobResult +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import io.zeebe.client.api.response.ActivatedJob +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PythonExecutorJobProcessor : JobProcessor { + + @Inject + private lateinit var workerConfig: PythonExecutorWorkerConfiguration + + @Inject + lateinit var executor: PythonExecutor + + override fun processJob(job: ActivatedJob): Single { + return Single.fromCallable { + println("${workerConfig.workerName} Processing Python Job ${job.key}...") + + val fileName = job.customHeaders["script"] ?: throw IllegalArgumentException("missing script parameter in job header") + + val scriptFile = File(workerConfig.scriptFolder, fileName) + + val inputs = job.variablesAsMap + + val executionResult = executor.execute(scriptFile, inputs) + .doOnSuccess { + println("${workerConfig.workerName} Python Execution Result: $it") + + }.doOnError { + println("${workerConfig.workerName} Python Script Execution resulted in a error (script may have successfully executed, but parsing result failed: see stacktrace.)") + it.printStackTrace() + + }.subscribeOn(Schedulers.io()).blockingGet() + + JobResult(resultVariables = ZeebeVariables(executionResult)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorWorkerConfiguration.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorWorkerConfiguration.kt new file mode 100644 index 0000000..7fe4afd --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorWorkerConfiguration.kt @@ -0,0 +1,25 @@ +package com.github.stephenott.qtz.workers.python + +import com.github.stephenott.qtz.workers.WorkerConfiguration +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Context +import java.time.Duration +import java.util.* + +@ConfigurationProperties("executors.python") +@Context +class PythonExecutorWorkerConfiguration: WorkerConfiguration { + var pythonPath: String = "python3" + + var scriptFolder: String = "/python-scripts" + + override var enabled: Boolean = true + + override var taskType: String = "script-python" + + override var workerName: String = "Python-Executor-Worker:${UUID.randomUUID()}" + + override var taskMaxZeebeLock: Duration = Duration.ofSeconds(60) + + override var maxBatchSize: Int = 1 +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorWorkerStartupListener.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorWorkerStartupListener.kt new file mode 100644 index 0000000..0d0fd68 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorWorkerStartupListener.kt @@ -0,0 +1,33 @@ +package com.github.stephenott.qtz.workers.python + +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.runtime.server.event.ServerStartupEvent +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PythonExecutorWorkerStartupListener : ApplicationEventListener { + + @Inject + lateinit var pythonExecutorWorker: PythonExecutorZeebeWorker + + @Inject + lateinit var workerConfig: PythonExecutorWorkerConfiguration + + override fun onApplicationEvent(event: ServerStartupEvent?) { + if (workerConfig.enabled) { + + println("${workerConfig.workerName} Creating and Starting Python Executor Worker...") + + pythonExecutorWorker.create().start() + .doOnComplete { + println("${workerConfig.workerName} Python Executor Worker has started") + } + .subscribeOn(Schedulers.io()).subscribe() + + } else { + println("${workerConfig.workerName} Python Executor Worker is disabled in configuration.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorZeebeWorker.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorZeebeWorker.kt new file mode 100644 index 0000000..3dad9b4 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/python/PythonExecutorZeebeWorker.kt @@ -0,0 +1,36 @@ +package com.github.stephenott.qtz.workers.python + +import com.github.stephenott.qtz.workers.GenericWorker +import com.github.stephenott.qtz.zeebe.ZeebeManagementClientConfiguration +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PythonExecutorZeebeWorker() { + + @Inject + lateinit var zClientConfig: ZeebeManagementClientConfiguration + + @Inject + lateinit var workerConfig: PythonExecutorWorkerConfiguration + + @Inject + private lateinit var jobProcessor: PythonExecutorJobProcessor + + @Inject + private lateinit var jobFailedProcessor: PythonExecutorFailedJobProcessor + + private lateinit var worker: GenericWorker + + fun create(): GenericWorker { + worker = GenericWorker(jobProcessor, + jobFailedProcessor, + zClientConfig, + workerConfig) + return worker + } + + fun getWorker(): GenericWorker { + return worker + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskWorkerStartupListener.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskWorkerStartupListener.kt new file mode 100644 index 0000000..7a555d8 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskWorkerStartupListener.kt @@ -0,0 +1,32 @@ +package com.github.stephenott.qtz.workers.usertask + +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.runtime.server.event.ServerStartupEvent +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserTaskWorkerStartupListener : ApplicationEventListener { + + @Inject + lateinit var userTaskWorker: UserTaskZeebeWorker + + @Inject + lateinit var workerConfig: ZeebeUserTaskWorkerConfiguration + + override fun onApplicationEvent(event: ServerStartupEvent?) { + if (workerConfig.enabled) { + println("Creating and Starting User Task Worker...") + + userTaskWorker.create().start() + .doOnComplete { + println("User Task Worker has started") + } + .subscribeOn(Schedulers.io()).subscribe() + + } else { + println("User Task Worker is disabled in configuration.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeFailedJobProcessor.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeFailedJobProcessor.kt new file mode 100644 index 0000000..623c675 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeFailedJobProcessor.kt @@ -0,0 +1,39 @@ +package com.github.stephenott.qtz.workers.usertask + +import com.github.stephenott.qtz.workers.JobFailedProcessor +import com.github.stephenott.qtz.zeebe.ZeebeManagementClientConfiguration +import io.reactivex.Completable +import io.reactivex.schedulers.Schedulers +import io.zeebe.client.ZeebeClient +import io.zeebe.client.api.response.ActivatedJob +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + + +@Singleton +class UserTaskZeebeFailedJobProcessor: JobFailedProcessor { + + @Inject + private lateinit var zClientConfig: ZeebeManagementClientConfiguration + + override fun processFailedJob(zClient: ZeebeClient, job: ActivatedJob, errorMessage: String): Completable { + return Completable.fromCallable { + zClient.newFailCommand(job.key) + .retries(job.retries.minus(1)) + .errorMessage(errorMessage) + .send() + .join(zClientConfig.commandTimeout.seconds.plus(2), TimeUnit.SECONDS) + + }.subscribeOn(Schedulers.io()) + .doOnSubscribe { + println("Attempting to report failure to Zeebe for job: ${job.key} with error message: ${errorMessage}.") + }.doOnComplete { + println("Successfully reported Failure of Job: ${job.key} with error message: ${errorMessage}.") + }.doOnError { + println("Unable to report failure of ${job.key}: Error was: ${it.message}") + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeJobProcessor.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeJobProcessor.kt new file mode 100644 index 0000000..5c5a5d4 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeJobProcessor.kt @@ -0,0 +1,72 @@ +package com.github.stephenott.qtz.workers.usertask + +import com.github.stephenott.qtz.tasks.domain.UserTaskEntity +import com.github.stephenott.qtz.tasks.domain.UserTaskState +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import com.github.stephenott.qtz.tasks.repository.UserTasksRepository +import com.github.stephenott.qtz.workers.JobProcessor +import com.github.stephenott.qtz.workers.JobResult +import com.github.stephenott.qtz.zeebe.ZeebeManagementClientConfiguration +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import io.zeebe.client.api.response.ActivatedJob +import java.time.Instant +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserTaskZeebeJobProcessor: JobProcessor { + + @Inject + private lateinit var userTaskRepository: UserTasksRepository + + @Inject + private lateinit var workerConfig: ZeebeUserTaskWorkerConfiguration + + @Inject + private lateinit var zClientConfig: ZeebeManagementClientConfiguration + + override fun processJob(job: ActivatedJob): Single { + return Single.fromCallable { + println("${workerConfig.workerName} Processing User Task Job...") + val entity = zeebeJobToUserTaskEntity(job, this.zClientConfig) + val taskEntity = userTaskRepository.save(entity) + .subscribeOn(Schedulers.io()) + .doOnSuccess { ut -> + println("User Task was captured from Zeebe and saved: ${ut.taskId}") + }.blockingGet() + + JobResult(resultVariables = ZeebeVariables(mapOf(Pair("createdUserTask", taskEntity))), + reportResult = false) + } + + } + + private fun zeebeJobToUserTaskEntity(job: ActivatedJob, config: ZeebeManagementClientConfiguration): UserTaskEntity { + return UserTaskEntity( + state = UserTaskState.NEW, + taskOriginalCapture = Instant.now(), + title = job.customHeaders["title"] + ?: throw IllegalArgumentException("Missing task title configuration"), + description = job.customHeaders["description"], + priority = job.customHeaders["priority"]?.toInt() ?: 0, + assignee = job.customHeaders["assignee"], + candidateGroups = job.customHeaders["candidateGroups"]?.split(",")?.toSet(), + candidateUsers = job.customHeaders["candidateGroups"]?.split(",")?.toSet(), +// dueDate = Instant.parse(job.customHeaders["dueDate"]) ?: null, //@TODO!!! + dueDate = null, + formKey = job.customHeaders["formKey"] + ?: throw IllegalArgumentException("formKey is missing."), + zeebeJobKey = job.key, + zeebeVariablesAtCapture = ZeebeVariables(job.variablesAsMap), + zeebeSource = config.clusterName, + zeebeBpmnProcessId = job.bpmnProcessId, + zeebeBpmnProcessVersion = job.workflowDefinitionVersion, + zeebeBpmnProcessKey = job.workflowKey, + zeebeElementInstanceKey = job.elementInstanceKey, + zeebeElementId = job.elementId, + zeebeJobDealine = Instant.ofEpochMilli(job.deadline), + zeebeJobRetriesRemaining = job.retries) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeWorker.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeWorker.kt new file mode 100644 index 0000000..5da36f8 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/UserTaskZeebeWorker.kt @@ -0,0 +1,36 @@ +package com.github.stephenott.qtz.workers.usertask + +import com.github.stephenott.qtz.workers.GenericWorker +import com.github.stephenott.qtz.zeebe.ZeebeManagementClientConfiguration +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserTaskZeebeWorker() { + + @Inject + lateinit var zClientConfig: ZeebeManagementClientConfiguration + + @Inject + lateinit var userTaskWorkerConfig: ZeebeUserTaskWorkerConfiguration + + @Inject + private lateinit var jobProcessor: UserTaskZeebeJobProcessor + + @Inject + private lateinit var jobFailedProcessor: UserTaskZeebeFailedJobProcessor + + private lateinit var worker: GenericWorker + + fun create(): GenericWorker { + worker = GenericWorker(jobProcessor, + jobFailedProcessor, + zClientConfig, + userTaskWorkerConfig) + return worker + } + + fun getWorker(): GenericWorker { + return worker + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/ZeebeUserTaskWorkerConfiguration.kt b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/ZeebeUserTaskWorkerConfiguration.kt new file mode 100644 index 0000000..66c3ec6 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/workers/usertask/ZeebeUserTaskWorkerConfiguration.kt @@ -0,0 +1,37 @@ +package com.github.stephenott.qtz.workers.usertask + +import com.github.stephenott.qtz.workers.WorkerConfiguration +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Context +import java.time.Duration +import java.util.* + +@ConfigurationProperties("userTask") +@Context +class ZeebeUserTaskWorkerConfiguration: WorkerConfiguration { + + /** + * The default state of the worker. + */ + override var enabled: Boolean = true + + /** + * The Zeebe Task Topic that will be queried for. Represents the "User Task" type. + */ + override var taskType: String = "user-task" + + /** + * The worker name that Zeebe Jobs will be locked with. + */ + override var workerName: String = "User-Task-Worker:${UUID.randomUUID()}" + + /** + * The Zeebe job exclusiveLockTimeout value. Represents the User Task lock. + */ + override var taskMaxZeebeLock: Duration = Duration.ofDays(30) + + /** + * The maximum number of Jobs that can be locked on each poll request. + */ + override var maxBatchSize: Int = 1 +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/zeebe/ZeebeClientConfiguration.kt b/src/main/kotlin/com/github/stephenott/qtz/zeebe/ZeebeClientConfiguration.kt new file mode 100644 index 0000000..474f39a --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/zeebe/ZeebeClientConfiguration.kt @@ -0,0 +1,11 @@ +package com.github.stephenott.qtz.zeebe + +import java.time.Duration + +interface ZeebeClientConfiguration{ + var brokerContactPoint: String + var clusterName: String + var longPollTimeout: Duration + var commandTimeout: Duration + var messageTimeToLive: Duration +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/zeebe/ZeebeManagementClientConfiguration.kt b/src/main/kotlin/com/github/stephenott/qtz/zeebe/ZeebeManagementClientConfiguration.kt new file mode 100644 index 0000000..4c04c91 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/zeebe/ZeebeManagementClientConfiguration.kt @@ -0,0 +1,36 @@ +package com.github.stephenott.qtz.zeebe + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Context +import java.time.Duration +import java.util.* + +@ConfigurationProperties("orchestrator.management.client") +@Context +class ZeebeManagementClientConfiguration: ZeebeClientConfiguration { + + /** + * The zeebe broker contact point URL + */ + override var brokerContactPoint: String = "localhost:26500" + + /** + * The name of the Zeebe Cluster. Used by the User Task DB to track what Zeebe Cluster the user task belongs to. + */ + override var clusterName: String = "zeebe:${UUID.randomUUID()}" + + /** + * The max duration of the Zeebe long poll for user tasks. + */ + override var longPollTimeout: Duration = Duration.ofMinutes(10) + + /** + * The max duration of the Zeebe gRPC commands except for the Long poll, which is covered with the longPollTimeout. + */ + override var commandTimeout: Duration = Duration.ofSeconds(30) + + /** + * The message time to live for Zeebe. + */ + override var messageTimeToLive: Duration = Duration.ofHours(1) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/zeebe/management/controler/ZeebeManagementController.kt b/src/main/kotlin/com/github/stephenott/qtz/zeebe/management/controler/ZeebeManagementController.kt new file mode 100644 index 0000000..422c410 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/zeebe/management/controler/ZeebeManagementController.kt @@ -0,0 +1,142 @@ +package com.github.stephenott.qtz.zeebe.management.controler + +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import com.github.stephenott.qtz.workers.python.PythonExecutorZeebeWorker +import com.github.stephenott.qtz.workers.usertask.UserTaskZeebeWorker +import com.github.stephenott.qtz.zeebe.ZeebeManagementClientConfiguration +import com.github.stephenott.qtz.zeebe.management.repository.ZeebeManagementRepository +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClientConfiguration +import io.micronaut.http.multipart.StreamingFileUpload +import io.reactivex.Single +import io.zeebe.client.api.response.WorkflowInstanceEvent +import java.io.File +import javax.inject.Inject + +@Controller("/zeebe/management") +open class ZeebeManagementController( + val config: ZeebeManagementClientConfiguration) : ZeebeManagementOperations { + + @Inject + lateinit var zeebeManagementRepository: ZeebeManagementRepository + + @Inject + lateinit var userTaskZeebeWorker: UserTaskZeebeWorker + + @Inject + lateinit var pythonExecutorZeebeWorker: PythonExecutorZeebeWorker + + @Post(value = "/workflow/deployment", consumes = [MediaType.MULTIPART_FORM_DATA]) + override fun deployWorkflow(workflow: StreamingFileUpload): Single> { + val tempFile = File.createTempFile(workflow.filename, "_temp_bpmn") //@TODO make this configurable + + val uploadPublisher = Single.fromPublisher(workflow.transferTo(tempFile)) + + return uploadPublisher.flatMap { success -> + if (success) { + zeebeManagementRepository.deployWorkflow(tempFile, workflow.filename) + .onErrorResumeNext { + Single.error(ZeebeFailedDeploymentException(WorkflowDeploymentFailedResponse("Unable to deploy workflow: ${it.message}"))) + }.map { de -> + HttpResponse.ok(WorkflowDeploymentResponse("Success: " + de.workflows.map { it.workflowKey })) + } + } else { + throw ZeebeFileUploadException(WorkflowDeploymentFailedResponse("Could not upload file")) + } + } + } + + @Post("/workflow/instance") + override fun createWorkflowInstance(@Body instanceCreationRequest: Single): Single> { + return instanceCreationRequest.flatMap { + zeebeManagementRepository.createWorkflowInstance( + it.workflowKey, + it.startVariables) + .map { response -> + HttpResponse.created(response) + } + } + + //@TODO add better error handling for when workflow creation fails. + } + + @Post("/worker/usertask/start") + override fun startUserTaskWorker(): Single> { + return if (userTaskZeebeWorker.getWorker().workerIsActive()){ + Single.just(HttpResponse.ok()) + } else { + userTaskZeebeWorker.getWorker().start().toSingleDefault(HttpResponse.ok()) + } + } + + @Post("/worker/usertask/stop") + override fun stopUserTaskWorker(): Single> { + return if (userTaskZeebeWorker.getWorker().workerIsActive()){ + userTaskZeebeWorker.getWorker().stop().toSingleDefault(HttpResponse.ok()) + } else { + Single.just(HttpResponse.ok()) + } + } + + @Post("/worker/executors/python/start") + override fun startPythonExecutorWorker(): Single> { + return if (pythonExecutorZeebeWorker.getWorker().workerIsActive()){ + Single.just(HttpResponse.ok()) + } else { + pythonExecutorZeebeWorker.getWorker().start().toSingleDefault(HttpResponse.ok()) + } + } + + @Post("/worker/executors/python/stop") + override fun stopPythonExecutorWorker(): Single> { + return if (pythonExecutorZeebeWorker.getWorker().workerIsActive()){ + pythonExecutorZeebeWorker.getWorker().stop().toSingleDefault(HttpResponse.ok()) + } else { + Single.just(HttpResponse.ok()) + } + } + + @Error + fun fileUploadError(request: HttpRequest<*>, exception: ZeebeFileUploadException): HttpResponse { + return HttpResponse.badRequest(exception.responseBody) + } + + @Error + fun zeebeWorkflowDeploymentError(request: HttpRequest<*>, exception: ZeebeFailedDeploymentException): HttpResponse { + return HttpResponse.badRequest(exception.responseBody) + } +} + +interface ZeebeManagementOperations { + + fun deployWorkflow(workflow: StreamingFileUpload): Single> + + fun createWorkflowInstance(instanceCreationRequest: Single): Single> + + fun startUserTaskWorker(): Single> + + fun stopUserTaskWorker(): Single> + + fun startPythonExecutorWorker(): Single> + + fun stopPythonExecutorWorker(): Single> + +} + +data class WorkflowDeploymentRequest(val name: String) // @TODO review usage need + +data class WorkflowDeploymentResponse(val result: String) + +data class WorkflowDeploymentFailedResponse(val reason: String) + +data class WorkflowInstanceCreateRequest(val workflowKey: Long, val startVariables: ZeebeVariables) + +class ZeebeFileUploadException(val responseBody: WorkflowDeploymentFailedResponse) : RuntimeException("File failed to upload.") {} + +class ZeebeFailedDeploymentException(val responseBody: WorkflowDeploymentFailedResponse) : RuntimeException("Zeebe Workflow Deployment Failure.") {} \ No newline at end of file diff --git a/src/main/kotlin/com/github/stephenott/qtz/zeebe/management/repository/ZeebeManagementRepository.kt b/src/main/kotlin/com/github/stephenott/qtz/zeebe/management/repository/ZeebeManagementRepository.kt new file mode 100644 index 0000000..da6bfd1 --- /dev/null +++ b/src/main/kotlin/com/github/stephenott/qtz/zeebe/management/repository/ZeebeManagementRepository.kt @@ -0,0 +1,110 @@ +package com.github.stephenott.qtz.zeebe.management.repository + +import com.github.stephenott.qtz.tasks.domain.ZeebeVariables +import com.github.stephenott.qtz.zeebe.ZeebeManagementClientConfiguration +import io.micronaut.context.annotation.Secondary +import io.reactivex.Completable +import io.reactivex.Single +import io.zeebe.client.ZeebeClient +import io.zeebe.client.api.response.DeploymentEvent +import io.zeebe.client.api.response.WorkflowInstanceEvent +import io.zeebe.model.bpmn.Bpmn +import io.zeebe.model.bpmn.BpmnModelInstance +import java.io.File +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +interface ZeebeManagementRepository { + val config: ZeebeManagementClientConfiguration + val zClient: ZeebeClient + + fun deployWorkflow(file: File, fileName: String): Single + fun createWorkflowInstance(workflowKey: Long, startVariables: ZeebeVariables): Single + fun completeJob(jobKey: Long, completionVariables: ZeebeVariables): Completable + fun reportJobFailure(jobKey: Long, errorMessage: String, remainingRetries: Int = 0): Completable +} + +//@TODO refactor to support multiple repositories? Or should just deploy multiple containers, each with their own config? +@Singleton +@Secondary //@TODO refactor to support the @Replaces annotation of new micronaut release +class ZeebeManagementRepositoryImpl( + override val config: ZeebeManagementClientConfiguration +) : ZeebeManagementRepository { + + override val zClient: ZeebeClient = createDefaultZeebeClient(config) + + override fun createWorkflowInstance(workflowKey: Long, startVariables: ZeebeVariables): Single { + return Single.fromCallable { + zClient.newCreateInstanceCommand() + .workflowKey(workflowKey) + .variables(startVariables.variables ?: mapOf()) + .send().join(config.commandTimeout.seconds + 1, TimeUnit.SECONDS) + + }.doOnSubscribe { + println("${config.clusterName} Starting workflow instance based on key: " + workflowKey) + }.doOnSuccess { + println("${config.clusterName} Workflow(${workflowKey}) instance started: ${it.workflowInstanceKey}") + }.doOnError { + println("${config.clusterName} Unable to start workflow instance: ${it.message}") + } + } + + override fun deployWorkflow(file: File, fileName: String): Single { + return fileToBpmnModelInstance(file).flatMap { + createWorkflowDeployment(it, fileName) + } + } + + private fun fileToBpmnModelInstance(file: File): Single { + return Single.fromCallable { + Bpmn.readModelFromFile(file); + } //@TODO add error handling to return better errors to the client based on bpmn. + //@TODO add bpmn model linter support + } + + private fun createWorkflowDeployment(modelInstance: BpmnModelInstance, filename: String): Single { + return Single.fromCallable { + zClient.newDeployCommand() + .addWorkflowModel(modelInstance, filename) + .send().join(config.commandTimeout.seconds + 1, TimeUnit.SECONDS)// move timeout to configuration + + }.onErrorResumeNext { + it.printStackTrace() + Single.error(it) //@TODO add better error handling + } + } + + override fun reportJobFailure(jobKey: Long, errorMessage: String, remainingRetries: Int): Completable { + return Completable.fromCallable { + zClient.newFailCommand(jobKey) + .retries(remainingRetries) + .errorMessage(errorMessage) + .send().join(config.commandTimeout.seconds + 1, TimeUnit.SECONDS) + }.onErrorResumeNext { + it.printStackTrace() + Completable.error(it) + } + } + + override fun completeJob(jobKey: Long, completionVariables: ZeebeVariables): Completable { + return Completable.fromCallable { + zClient.newCompleteCommand(jobKey) + .variables(completionVariables.variables) + .send().join(config.commandTimeout.seconds + 1, TimeUnit.SECONDS) + }.onErrorResumeNext { + it.printStackTrace() + Completable.error(it) + } + } + + companion object { + fun createDefaultZeebeClient(config: ZeebeManagementClientConfiguration): ZeebeClient { + return ZeebeClient.newClientBuilder() + .brokerContactPoint(config.brokerContactPoint) //@TODO add rest of default configurations + .defaultRequestTimeout(config.commandTimeout) + .defaultMessageTimeToLive(config.messageTimeToLive) + .usePlaintext() //@TODO remove and replace with cert /-/SECURITY/-/ + .build() + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..b144250 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,81 @@ +micronaut: + application: + name: quintessential-tasklist-zeebe + http: + client: + read-timeout: 5s + +datasources: + default: + url: jdbc:h2:./build/DB/dbdevDb1 # jdbc:h2:~/devDb1;MODE=PostgreSQL;AUTO_SERVER=TRUE + driverClassName: org.h2.Driver + username: sa + password: sa + dialect: H2 # POSTGRES + +jpa: + default: + packages-to-scan: + - 'com.github.stephenott.qtz.forms' + - 'com.github.stephenott.qtz.tasks' + properties: + hibernate: + hbm2ddl: + auto: update + show_sql: true + +flyway: + datasources: + default: + locations: classpath:databasemigrations + +formValidatorService: + host: http://localhost:8081 + +orchestrator: + management: + client: + brokerContactPoint: localhost:26500 + longPollTimeout: 30s + workflow-linter: + rules: + global-rules: + enable: false + description: Global Restrictions for Service Task Types + elementTypes: + - ServiceTask + serviceTaskRule: + allowedTypes: + - some-type + - user-task + + user-task-rule: + description: Specific rule for User Task Configuration of Service Tasks + elementTypes: + - ServiceTask + target: + serviceTasks: + types: + - user-task + headerRule: + requiredKeys: + - title + - candidateGroups + - formKey + allowedNonDefinedKeys: false + allowedDuplicateKeys: false + optionalKeys: + - priority + - assignee + - candidateUsers + - dueDate + - description + +userTask: + enabled: false + maxBatchSize: 10 + +executors: + python: + enabled: false + scriptFolder: "./python-scripts" \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..ae9b716 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/src/test/java/com/github/stephenott/MyTest.java b/src/test/java/com/github/stephenott/MyTest.java deleted file mode 100644 index 589779e..0000000 --- a/src/test/java/com/github/stephenott/MyTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.stephenott; - -import de.flapdoodle.embed.mongo.MongodExecutable; -import de.flapdoodle.embed.mongo.MongodProcess; -import de.flapdoodle.embed.mongo.MongodStarter; -import de.flapdoodle.embed.mongo.config.IMongodConfig; -import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; -import de.flapdoodle.embed.mongo.config.Net; -import de.flapdoodle.embed.mongo.distribution.Version; -import de.flapdoodle.embed.process.runtime.Network; -import io.zeebe.test.ZeebeTestRule; -import org.junit.Rule; -import org.junit.Test; - -import java.util.concurrent.CountDownLatch; - -public class MyTest { - - @Rule public final ZeebeTestRule testRule = new ZeebeTestRule(); - private MongodExecutable mongodExecutable; - - @Test - public void test() throws Exception { - - MongodStarter runtime = MongodStarter.getDefaultInstance(); - IMongodConfig mongodConfig = new MongodConfigBuilder().version(Version.Main.PRODUCTION) - .net(new Net("localhost", 27017, Network.localhostIsIPv6())) - .build(); - - MongodStarter starter = MongodStarter.getDefaultInstance(); - mongodExecutable = starter.prepare(mongodConfig); - mongodExecutable.start(); - - try { - Thread.sleep(2000000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - -// client -// .newDeployCommand() -// .addResourceFromClasspath("process.bpmn") -// .send() -// .join(); -// -// final WorkflowInstanceEvent workflowInstance = -// client -// .newCreateInstanceCommand() -// .bpmnProcessId("process") -// .latestVersion() -// .send() -// .join(); - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/stephenott/qtz/zeebe/LinterTest1.kt b/src/test/kotlin/com/github/stephenott/qtz/zeebe/LinterTest1.kt new file mode 100644 index 0000000..6a66205 --- /dev/null +++ b/src/test/kotlin/com/github/stephenott/qtz/zeebe/LinterTest1.kt @@ -0,0 +1,42 @@ +package com.github.stephenott.qtz.zeebe + +import com.github.stephenott.qtz.linter.* +import io.kotlintest.specs.StringSpec +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.annotation.MicronautTest +import io.zeebe.model.bpmn.Bpmn +import io.zeebe.model.bpmn.instance.BaseElement +import io.zeebe.model.bpmn.instance.Gateway +import org.camunda.bpm.model.xml.instance.ModelElementInstance +import org.camunda.bpm.model.xml.validation.ModelElementValidator +import org.camunda.bpm.model.xml.validation.ValidationResultCollector +import java.io.File + +@MicronautTest +class LinterTest1( + private var server: EmbeddedServer +): StringSpec({ + "Test Linter"{ + + val file = File("src/test/resources/test1.bpmn") + +// val rules: List = +// LinterConfigurationParser.getLintRuleBeans(server.applicationContext) +// +// val validators: List> = +// LinterConfigurationParser.lintRulesToValidators(rules) +// +// +// WorkflowLinter(file).lintWithValidators(validators) + + val cleanerValidators = listOf( + elementValidator { element, validatorResultCollector -> + validatorResultCollector.addError(5000, "cleaner code") + } + ) + + val cleanModel = Cleaner(cleanerValidators).cleanModel(file) + println(Bpmn.convertToString(cleanModel)) + + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/stephenott/qtz/zeebe/ZeebeBroker1Test.kt b/src/test/kotlin/com/github/stephenott/qtz/zeebe/ZeebeBroker1Test.kt new file mode 100644 index 0000000..778e6f8 --- /dev/null +++ b/src/test/kotlin/com/github/stephenott/qtz/zeebe/ZeebeBroker1Test.kt @@ -0,0 +1,48 @@ +package com.github.stephenott.qtz.zeebe + +import io.kotlintest.Spec +import io.kotlintest.specs.StringSpec +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.test.annotation.MicronautTest +import java.io.File + +@MicronautTest +class ZeebeBroker1Test : StringSpec({ + +// "zeebe client should be running" { +// val client: ZeebeClient = ZeebeClient.newClientBuilder() +// .brokerContactPoint(broker.getExternalAddress(ZeebePort.GATEWAY)) +// .build() +// println("CLIENT is RUNNING -->>$client") +// +// client shouldNotBe null +// } + + "deploy a workflow"{ + + val file = File("test1.bpmn") + + val requestBody = MultipartBody.builder() + .addPart("data", + file.name, + MediaType.TEXT_PLAIN_TYPE, + file).build() + + val dog = HttpRequest.POST("localhost:8080/zeebe/management/deployment", requestBody) + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + + + } + +}) { + companion object { +// var broker = ZeebeBrokerContainer() //@Todo is a .start needed? + } + + override fun afterSpec(spec: Spec) { +// broker.stop() + //@TODO move to common spec abstract class + } +} diff --git a/src/test/kotlin/io/kotlintest/provided/ProjectConfig.kt b/src/test/kotlin/io/kotlintest/provided/ProjectConfig.kt new file mode 100644 index 0000000..0ac5994 --- /dev/null +++ b/src/test/kotlin/io/kotlintest/provided/ProjectConfig.kt @@ -0,0 +1,9 @@ +package io.kotlintest.provided + +import io.kotlintest.AbstractProjectConfig +import io.micronaut.test.extensions.kotlintest.MicornautKotlinTestExtension + +object ProjectConfig : AbstractProjectConfig() { + override fun listeners() = listOf(MicornautKotlinTestExtension) + override fun extensions() = listOf(MicornautKotlinTestExtension) +} diff --git a/src/test/resources/python1.bpmn b/src/test/resources/python1.bpmn new file mode 100644 index 0000000..985dafa --- /dev/null +++ b/src/test/resources/python1.bpmn @@ -0,0 +1,44 @@ + + + + + SequenceFlow_0yxj38p + + + + + + + + + SequenceFlow_0yxj38p + SequenceFlow_12jre1g + + + + SequenceFlow_12jre1g + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/test1.bpmn b/src/test/resources/test1.bpmn new file mode 100644 index 0000000..1e1d596 --- /dev/null +++ b/src/test/resources/test1.bpmn @@ -0,0 +1,380 @@ + + + + + + + + + + + + + SubProcess_0th6hv3 + + + StartEvent_1 + ServiceTask_1luzsfd + EndEvent_1qai5bq + ExclusiveGateway_0zw2nt1 + EndEvent_1m14ym9 + + + Task_1pjpqz0 + Task_10ugd34 + Task_00znhpl + IntermediateThrowEvent_1c9t58w + IntermediateThrowEvent_1jlnvjm + IntermediateThrowEvent_0rqamn8 + Task_05r7ocx + IntermediateThrowEvent_0pftlc9 + IntermediateThrowEvent_0ue7kjs + + + + SequenceFlow_1hnbx9h + + + SequenceFlow_1hnbx9h + SequenceFlow_1uts8z1 + + + SequenceFlow_0bjdega + + + SequenceFlow_1uts8z1 + SequenceFlow_135m3sa + SequenceFlow_0bjdega + SequenceFlow_1oknyaf + + + SequenceFlow_1oknyaf + + + SequenceFlow_1edsqdq + + + + SequenceFlow_1edsqdq + SequenceFlow_1fetaup + + + + SequenceFlow_1fetaup + + + + + SequenceFlow_15yu6zp + + + + SequenceFlow_15yu6zp + + + + + + + + + + + + + + + + + + + + + + + SequenceFlow_135m3sa + + + + + + + www + + + happy + + + + + + + + StartEvent_0io0u2p + Task_0f18plf + Task_1qf8jgl + Task_0dokmc7 + EndEvent_1g6ltxd + + + + + SequenceFlow_09rhfmj + + + SequenceFlow_09rhfmj + SequenceFlow_0d193ce + + + SequenceFlow_0d193ce + SequenceFlow_1iomtvq + + + + SequenceFlow_1iomtvq + SequenceFlow_1f0wtyq + + + + SequenceFlow_1f0wtyq + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/zeebe.yml b/zeebe.yml deleted file mode 100644 index d6f1340..0000000 --- a/zeebe.yml +++ /dev/null @@ -1,57 +0,0 @@ -zeebe: - clients: - - name: MyCustomClient - brokerContactPoint: "localhost:25600" - requestTimeout: PT20S - workers: - - name: SimpleScriptWorker - jobTypes: - - type1 - timeout: PT10S - - name: UT-Worker - jobTypes: - - ut.generic - timeout: P1D - -executors: - - name: Script-Executor - address: "type1" - execute: ./scripts/script1.js - - name: CommonGenericExecutor - address: commonExecutor - execute: classpath:com.custom.executors.Executor1 - - name: IpBlocker - address: block-ip - execute: ./cyber/BlockIP.py - -userTaskExecutors: - - name: GenericUserTask - address: ut.generic - -managementServer: - enabled: true - apiRoot: server1 - corsRegex: ".*." - port: 8080 - instances: 1 - zeebeClient: - name: DeploymentClient - brokerContactPoint: "localhost:25600" - requestTimeout: PT10S - -formValidatorServer: - enabled: true - corsRegex: ".*." - port: 8082 - instances: 1 - formValidatorService: - host: localhost - port: 8083 - validateUri: /validate - requestTimeout: 5000 - -userTaskServer: - enabled: true - corsRegex: ".*." - port: 8088 - instances: 1 \ No newline at end of file