├── .gitignore ├── README.md ├── bpmn └── bpmn1.bpmn ├── docker └── docker-compose.yml ├── docs └── design │ ├── cluster.png │ ├── dataflow.png │ ├── designs.graffle │ └── form │ ├── FormBuilder1-build.png │ ├── FormBuilder2-build.png │ ├── FormBuilder3-build.png │ ├── FormBuilder4-render.png │ └── User-Task-Form-Completion-Flow.png ├── pom.xml ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── stephenott │ │ ├── MainVerticle.java │ │ ├── common │ │ ├── Common.java │ │ ├── EventBusable.java │ │ ├── EventBusableMessageCodec.java │ │ └── EventBusableReplyException.java │ │ ├── conf │ │ ├── ApplicationConfiguration.java │ │ └── config.json │ │ ├── executors │ │ ├── JobResult.java │ │ ├── polyglot │ │ │ └── ExecutorVerticle.java │ │ └── usertask │ │ │ ├── UserTaskConfiguration.java │ │ │ └── UserTaskExecutorVerticle.java │ │ ├── form │ │ └── validator │ │ │ ├── FormValidationServerHttpVerticle.java │ │ │ ├── ValidationRequest.java │ │ │ ├── ValidationRequestResult.java │ │ │ ├── ValidationSchemaObject.java │ │ │ ├── ValidationServiceRequest.java │ │ │ ├── ValidationSubmissionObject.java │ │ │ └── exception │ │ │ ├── InvalidFormSubmissionException.java │ │ │ └── ValidationRequestResultException.java │ │ ├── managementserver │ │ └── ManagementHttpVerticle.java │ │ ├── package-info.java │ │ ├── usertask │ │ ├── CompletionRequest.java │ │ ├── DbActionResult.java │ │ ├── FailedDbActionException.java │ │ ├── FormSchemaService.java │ │ ├── FormSchemaServiceImpl.java │ │ ├── GetRequest.java │ │ ├── GetTasksFormSchemaReqRes.java │ │ ├── SubmitTaskComposeDto.java │ │ ├── UserTaskActionsVerticle.java │ │ ├── UserTaskHttpServerVerticle.java │ │ ├── entity │ │ │ ├── FormSchemaEntity.java │ │ │ └── UserTaskEntity.java │ │ ├── json │ │ │ └── deserializer │ │ │ │ └── JsonToStringDeserializer.java │ │ └── mongo │ │ │ ├── MongoManager.java │ │ │ └── Subscribers.java │ │ └── zeebe │ │ ├── client │ │ ├── CreateInstanceConfiguration.java │ │ ├── ZeebeClientConfigurationProperties.java │ │ └── ZeebeClientVerticle.java │ │ └── dto │ │ └── ActivatedJobDto.java └── test │ └── java │ └── com │ └── github │ └── stephenott │ └── MyTest.java └── zeebe.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Maven template 3 | target/ 4 | pom.xml.tag 5 | pom.xml.releaseBackup 6 | pom.xml.versionsBackup 7 | pom.xml.next 8 | release.properties 9 | dependency-reduced-pom.xml 10 | buildNumber.properties 11 | .mvn/timing.properties 12 | .mvn/wrapper/maven-wrapper.jar 13 | 14 | ### Java template 15 | # Compiled class file 16 | *.class 17 | 18 | # Log file 19 | *.log 20 | 21 | # BlueJ files 22 | *.ctxt 23 | 24 | # Mobile Tools for Java (J2ME) 25 | .mtj.tmp/ 26 | 27 | # Package Files # 28 | *.jar 29 | *.war 30 | *.nar 31 | *.ear 32 | *.zip 33 | *.tar.gz 34 | *.rar 35 | 36 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 37 | hs_err_pid* 38 | 39 | .idea/* 40 | *.iml 41 | 42 | tmp/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quintessential-tasklist-zeebe 2 | The quintessential Zeebe tasklist for BPMN Human tasks with Drag and Drop Form builder, client and server side validations, and drop in Form Rendering 3 | 4 | WIP 5 | 6 | Setup SLF4J logging: `-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory` 7 | 8 | vertx run command: `run com.github.stephenott.MainVerticle -conf src/main/java/com/github/stephenott/conf/conf.json` 9 | 10 | Current Zeebe Version: `0.21.0-alpha1` 11 | Current Vertx Version: `3.8.0` 12 | Java: `1.8` 13 | 14 | 15 | # Cluster Architecture 16 | 17 | ![cluster-arch](./docs/design/cluster.png) 18 | 19 | - Clients, Workers, and Executors can be added at startup and during runtime. 20 | - Failed nodes in the Vertx Cluster (Clients, Workers, and Executors) will be re-instantiated through the vertx cluster manager's configuration. 21 | 22 | 23 | # Form Building UI 24 | 25 | The Form Builder UI uses Formio.js as the Builder and Render. 26 | The schema that was generated from the builder is persisted and used during the User Task Submission with Form flow. 27 | 28 | ![builder1](./docs/design/form/FormBuilder1-build.png) 29 | 30 | ![builder2](./docs/design/form/FormBuilder2-build.png) 31 | 32 | ![builder3](./docs/design/form/FormBuilder3-build.png) 33 | 34 | 35 | And then you can render and make a submission: 36 | 37 | ![builder4](./docs/design/form/FormBuilder4-render.png) 38 | 39 | 40 | Try out the builder on: https://formio.github.io/formio.js/app/builder 41 | 42 | 43 | ## User Task Submission with Form Data flow 44 | 45 | ![dataflow](./docs/design/form/User-Task-Form-Completion-Flow.png) 46 | 47 | 48 | # ZeebeClient/Worker/Executor Data Flow 49 | 50 | ![data flow](./docs/design/dataflow.png) 51 | 52 | 53 | # Configuration 54 | 55 | Extensive configuration capabilities are provided to control the exact setup of your application: 56 | 57 | The Yaml location can be configured through the applications config.json. Default is `./zeebe.yml`. 58 | 59 | Example: 60 | 61 | ```yaml 62 | zeebe: 63 | clients: 64 | - name: MyCustomClient 65 | brokerContactPoint: "localhost:25600" 66 | requestTimeout: PT20S 67 | workers: 68 | - name: SimpleScriptWorker 69 | jobTypes: 70 | - type1 71 | timeout: PT10S 72 | - name: UT-Worker 73 | jobTypes: 74 | - ut.generic 75 | timeout: P1D 76 | 77 | executors: 78 | - name: Script-Executor 79 | address: "type1" 80 | execute: ./scripts/script1.js 81 | - name: CommonGenericExecutor 82 | address: commonExecutor 83 | execute: classpath:com.custom.executors.Executor1 84 | - name: IpBlocker 85 | address: block-ip 86 | execute: ./cyber/BlockIP.py 87 | 88 | userTaskExecutors: 89 | - name: GenericUserTask 90 | address: ut.generic 91 | 92 | managementServer: 93 | enabled: true 94 | apiRoot: server1 95 | corsRegex: ".*." 96 | port: 8080 97 | instances: 1 98 | zeebeClient: 99 | name: DeploymentClient 100 | brokerContactPoint: "localhost:25600" 101 | requestTimeout: PT10S 102 | 103 | formValidatorServer: 104 | enabled: true 105 | corsRegex: ".*." 106 | port: 8082 107 | instances: 1 108 | formValidatorService: 109 | host: localhost 110 | port: 8083 111 | validateUri: /validate 112 | requestTimeout: 5000 113 | 114 | userTaskServer: 115 | enabled: true 116 | corsRegex: ".*." 117 | port: 8080 118 | instances: 1 119 | ``` 120 | 121 | # Zeebe Clients 122 | 123 | A Zeebe Client is a gRPC channel to a specific Zeebe Cluster. 124 | 125 | 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. 126 | 127 | Zeebe Clients have the following configuration: 128 | 129 | ```yaml 130 | zeebe: 131 | clients: 132 | - name: MyCustomClient 133 | brokerContactPoint: "localhost:25600" 134 | requestTimeout: PT20S 135 | workers: 136 | - name: SimpleScriptWorker 137 | jobTypes: 138 | - type1 139 | timeout: PT10S 140 | - name: UT-Worker 141 | jobTypes: 142 | - ut.generic 143 | timeout: P1D 144 | ``` 145 | 146 | 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. 147 | 148 | Where `workers` is a array of Zeebe Worker definitions. A worker definition has a `name` and a list of `jobTypes`. 149 | 150 | `name` is the worker name that is provided to Zeebe as the worker that requested the job. 151 | 152 | `jobTypes` is the lsit of Zeebe job `types` that will be queried for using long polling. 153 | 154 | 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. 155 | 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. 156 | 157 | Take note of the usage of the `timeout` which is the deadline that the job will be locked for. 158 | 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. 159 | 160 | 161 | # Executors 162 | 163 | Executors provide a polyglot execution solution for completing Zeebe Jobs. 164 | 165 | Executors have the following configuration: 166 | Example of three different executors: 167 | 168 | ```yaml 169 | executors: 170 | - name: Script-Executor 171 | address: "type1" 172 | execute: ./scripts/script1.js 173 | instances: 2 174 | - name: CommonGenericExecutor 175 | address: commonExecutor 176 | execute: classpath:com.custom.executors.Executor1 177 | - name: IpBlocker 178 | address: block-ip 179 | execute: ./cyber/BlockIP.py 180 | ``` 181 | 182 | Executors can execute scripts and classes as defined in the executor's polyglot capabilities. 183 | 184 | Where `address` is the Zeebe job `type` that would be configured in the task in the BPMN. 185 | 186 | Where `execute` is the class/script that will be executor when jobs are sent to this executor 187 | 188 | Where `name` is the unique name of the Executor used for logging purposes. 189 | 190 | You can deploy a Executor with multiple `instances` to provide more more parallel throughput capacity. 191 | 192 | Required properties: `name`, `address`, `execute` 193 | 194 | Completion of Jobs sent to Executors is captured over the event bus with the JobResult object. 195 | Completed (successfully or a failure such as a business error) are sent as a JobResult to event bus address: `sourceClient.job-aciton.completion`. 196 | 197 | Where `sourceClient` is the ZeebeClient `name` that is used in the `zeebe.clients[].name` property. 198 | 199 | 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. 200 | 201 | JobResult's that have a `result=FAIL` will have their corresponding Zeebe Job actioned as a Failed Job. 202 | 203 | 204 | # User Task Executors 205 | 206 | User Task(UT) Executors are a special type of executor that are dedicated to the logic handling of BPMN User Tasks. 207 | 208 | UT Executors have the following configuration: 209 | 210 | ```yaml 211 | userTaskExecutors: 212 | - name: GenericUserTask 213 | address: ut.generic 214 | instances: 1 215 | ``` 216 | 217 | Required properties: `name`, `address` 218 | 219 | Where `address` is the Zeebe job `type` that would be configured in the task in the BPMN. 220 | 221 | Internally executors have their addresses prefixed with a common job prefix to ensure proper message namespacing. 222 | 223 | Where `name` is the unique name of the UT Executor used for logging purposes. 224 | 225 | You can deploy a UT Executor with multiple `instances` to provide more more parallel throughput capacity. 226 | 227 | UT Executors primary function is to provide capture of UTs from Zeebe and convert the Zeebe jobs into a UserTaskEntity. 228 | A UserTaskEntity is then saved in the storage of choice (such as a DB). 229 | 230 | Completion of User Tasks is captured over the event bus with the JobResult object. 231 | 232 | Completed (successfully or a failure such as a business error) are sent as a JobResult to event bus address: `sourceClient.job-aciton.completion`. 233 | 234 | Where `sourceClient` is the ZeebeClient `name` that is used in the `zeebe.clients[].name` property. 235 | 236 | 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. 237 | 238 | JobResult's that have a `result=FAIL` will have their corresponding Zeebe Job actioned as a Failed Job. 239 | 240 | 241 | ## User Tasks 242 | 243 | The default build of User Tasks seeks to provide a duplicate or similar User Task experience as Camunda's User Tasks implementation. 244 | 245 | See UserTaskEntity.class, UserTaskConfiguration.class for more details. 246 | 247 | A User Task can be configured in the Zeebe BPMN using custom headers. The supported headers are: 248 | 249 | |key|value|description| 250 | |------|------|----------| 251 | |title|`string`|The title of the task. Can be any string value that will be interpreted by the User Task storage system.| 252 | |description|`string` |The description of the task. Can be any string value that will be interpreted by the User Task storage system.| 253 | |priority|`int`|defaults to 0| 254 | |assignee|`string`|The default assignee of the task. A single value.| 255 | |candidateGroups|`string`|The list of groups that are candidates to claim this task. Comma separated list of strings. Example: `"cg1, cg2, cg3"`| 256 | |candidateUsers|`string`|The list of users that are candidates to claim this task. Comma separated list of strings. Example: `"cu1, cu2, cu3"`| 257 | |dueDate|`string`|The date on which the User Task is due. ISO8601 format| 258 | |formKey|`string`|A value that represents the specific form that should be used by the user when completing this task.| 259 | 260 | 261 | When generating a UserTaskEntity, some additional properties are stored for usage and indexing and convenience: 262 | 263 | In addition to the custom header values above, the following is stored in the UserTaskEntity: 264 | 265 | |key|value|description| 266 | |------|------|----------| 267 | |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.| 268 | |zeebeSource|`string`|The source ZeebeClient `name` that the ZeebeJob was retrieved from. 269 | |zeebeDeadline|`instant`|The Zeebe Job deadline property| 270 | |zeebeJobKey|`long`|The unique job ID of the Zeebe Job.| 271 | |bpmnProcessId|`string`|The BPMN Process Definition ID| 272 | |zeebeVariables|`Map of String:Object`|The variables from the Zeebe Job| 273 | |metadata|`Map of String:Object`|A generic data holder for additional User Task metadata| 274 | 275 | # Form Validation Server 276 | 277 | The Form Validation Server provides HTTP endpoints for validation a Form Submission based on a provided Form Schema. 278 | 279 | Configuration: 280 | 281 | ```yaml 282 | formValidatorServer: 283 | enabled: true 284 | corsRegex: ".*." 285 | port: 8082 286 | instances: 1 287 | formValidatorService: 288 | host: localhost 289 | port: 8083 290 | validateUri: /validate 291 | requestTimeout: 5000 292 | ``` 293 | 294 | Where `formValidatorService` is the Form Validator service that performs the actual form validation. 295 | 296 | Example Validation Request: 297 | 298 | POST: `localhost:8083/validate` 299 | 300 | Body: 301 | 302 | ```json 303 | { 304 | "schema":{ 305 | "display": "form", 306 | "components": [ 307 | { 308 | "label": "Text Field", 309 | "allowMultipleMasks": false, 310 | "showWordCount": false, 311 | "showCharCount": false, 312 | "tableView": true, 313 | "alwaysEnabled": false, 314 | "type": "textfield", 315 | "input": true, 316 | "key": "textField2", 317 | "defaultValue": "", 318 | "validate": { 319 | "customMessage": "", 320 | "json": "", 321 | "required": true 322 | }, 323 | "conditional": { 324 | "show": "", 325 | "when": "", 326 | "json": "" 327 | }, 328 | "inputFormat": "plain", 329 | "encrypted": false, 330 | "properties": {}, 331 | "customConditional": "", 332 | "logic": [], 333 | "attributes": {}, 334 | "widget": { 335 | "type": "" 336 | }, 337 | "reorder": false 338 | }, 339 | { 340 | "type": "button", 341 | "label": "Submit", 342 | "key": "submit", 343 | "disableOnInvalid": true, 344 | "theme": "primary", 345 | "input": true, 346 | "tableView": true 347 | } 348 | ], 349 | "settings": { 350 | } 351 | }, 352 | "submission":{ 353 | "data": { 354 | "textField2": 123, 355 | "dog": "cat" 356 | }, 357 | "metadata": {} 358 | } 359 | } 360 | ``` 361 | 362 | Response if validation passes: 363 | 364 | ```json 365 | { 366 | "processed_submission": { 367 | "textField2": "sog" 368 | } 369 | } 370 | ``` 371 | 372 | Notice that the extra `dog` property is removed because it is not a valid field in the form schema. 373 | 374 | Response if validation fails: 375 | 376 | ```json 377 | { 378 | "isJoi": true, 379 | "name": "ValidationError", 380 | "details": [ 381 | { 382 | "message": "\"textField2\" must be a string", 383 | "path": "textField2", 384 | "type": "string.base", 385 | "context": { 386 | "value": 123, 387 | "key": "textField2", 388 | "label": "textField2" 389 | } 390 | } 391 | ], 392 | "_object": { 393 | "textField2": 123, 394 | "dog": "cat" 395 | }, 396 | "_validated": { 397 | "textField2": 123 398 | } 399 | } 400 | ``` 401 | 402 | The validation service is also available over the event bus at the `address` property defined in the Form Validation Server configuration. 403 | 404 | 405 | # Management Server 406 | 407 | The management server provides HTTP endpoints for working with Zeebe clusters 408 | 409 | Configuration: 410 | 411 | ```yaml 412 | managementServer: 413 | enabled: true 414 | apiRoot: server1 415 | corsRegex: ".*." 416 | port: 8080 417 | zeebeClient: 418 | name: DeploymentClient 419 | brokerContactPoint: "localhost:25600" 420 | requestTimeout: PT10S 421 | instances: 1 422 | fileUploadPath: ./tmp/uploads 423 | ``` 424 | 425 | required fields: `apiRoot`, `zeebeClient` 426 | 427 | `apiRoot` must be unique. 428 | 429 | ## Deploy Workflow 430 | 431 | `POST localhost:8080/server1/deploy` 432 | 433 | Headers: 434 | - `Content-Type: multipart/form-data` 435 | 436 | form-data: 437 | - file name (must be a .bpmn or .yaml file) : file upload (the binary file you are uploading such as a .bpmn file) 438 | 439 | Where `server1` is the `apiRoot` value defined in the YAML configuration. 440 | 441 | You can deploy many management servers as needed. Each server can be deployed for different zeebe clusters. 442 | 443 | You can deploy the same server with multiple `instances` to provide more throughput. 444 | 445 | ## Create Workflow Instance / Start Workflow 446 | 447 | `POST localhost:8080/server1/create-instance` 448 | 449 | Headers: 450 | - `Content-Type: application/json` 451 | - `Accept: application/json` 452 | 453 | Json Body: 454 | 455 | ```json 456 | { 457 | "workflowKey": 1234567890 458 | } 459 | ``` 460 | 461 | Where `workflowKey` is the unique workflow key that was generated for the BPMN process/pool during deployment. 462 | 463 | You may also use: 464 | 465 | ```json 466 | { 467 | "bpmnProcessId": "myProcess", 468 | "bpmnProcessVersion": 2 469 | } 470 | ``` 471 | 472 | Where `bpmnProcessId` is the BPMN's process Id property (sometimes referred to as a process key). 473 | 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. 474 | 475 | `varaibles` can also be provided as a json object: 476 | 477 | ```json 478 | { 479 | "workflowKey": 1234567890, 480 | "variables": { 481 | "myVar1": 123, 482 | "myVar2": "some value", 483 | "myVarABC": [1,2,5,10], 484 | "myVarXYZ": { 485 | "1": "A", 486 | "2": "B" 487 | } 488 | } 489 | } 490 | ``` 491 | 492 | The variables will be injected into the created workflow instance. 493 | 494 | 495 | # User Task Server 496 | 497 | A User Task HTTP server that provides User Task persistence, querying, completion, etc. 498 | 499 | The server also provides a Form Schema Entity persistence, querying, and validation of submissions against the schema. 500 | The Form Schema is what will be submitted to the Form Validator Service. 501 | 502 | ## Server Configuration 503 | 504 | ```yaml 505 | userTaskServer: 506 | enabled: true 507 | corsRegex: ".*." 508 | port: 8080 509 | instances: 1 510 | ``` 511 | 512 | ## Actions: 513 | 514 | 1. Save Form Schema 515 | 1. Complete User Task 516 | 1. Get User Tasks 517 | 1. Submit Form to Complete a User Task 518 | 1. Delete User Task (TODO) 519 | 1. Claim User Task (TODO) 520 | 1. UnClaim User Task (TODO) 521 | 1. Assign User Task (TODO) 522 | 1. Create Custom User Task (not linked to Zeebe Job) 523 | 524 | ## Save Form Schema 525 | 526 | POST `/form/schema` 527 | 528 | ```json 529 | { 530 | "owner": "Department-1", 531 | "key": "MySimpleForm1", 532 | "title": "My Simple Form 1", 533 | "schema": { 534 | "display": "form", 535 | "components": [ 536 | { 537 | "label": "Text Field", 538 | "allowMultipleMasks": false, 539 | "showWordCount": false, 540 | "showCharCount": false, 541 | "tableView": true, 542 | "alwaysEnabled": false, 543 | "type": "textfield", 544 | "input": true, 545 | "key": "textField2", 546 | "defaultValue": "", 547 | "validate": { 548 | "customMessage": "", 549 | "json": "", 550 | "required": true 551 | }, 552 | "conditional": { 553 | "show": "", 554 | "when": "", 555 | "json": "" 556 | }, 557 | "inputFormat": "plain", 558 | "encrypted": false, 559 | "properties": {}, 560 | "customConditional": "", 561 | "logic": [], 562 | "attributes": {}, 563 | "widget": { 564 | "type": "" 565 | }, 566 | "reorder": false 567 | }, 568 | { 569 | "type": "button", 570 | "label": "Submit", 571 | "key": "submit", 572 | "disableOnInvalid": true, 573 | "theme": "primary", 574 | "input": true, 575 | "tableView": true 576 | } 577 | ], 578 | "settings": { 579 | } 580 | } 581 | } 582 | ``` 583 | 584 | The `key` property is the `formKey` value you setup in your zeebe task custom headers. 585 | 586 | Required fields: `owner`, `key`, `title`, `schema` 587 | 588 | 589 | ## Complete User Task 590 | 591 | Mainly used as a administrative endpoint to complete a User Task without any Form 592 | 593 | POST `/task/complete` 594 | 595 | ```json 596 | { 597 | "job": 2251799813685292, 598 | "source": "MyCustomClient", 599 | "completionVariables": {} 600 | } 601 | ``` 602 | 603 | `Source` is the zeebe client name configured in your configuration yaml. 604 | 605 | 606 | ## Get Tasks 607 | 608 | GET `/task` 609 | 610 | JSON Body: 611 | 612 | Query is run as a `AND` query on each of the arguments 613 | 614 | ```json 615 | { 616 | "taskId": "", 617 | "state": "", 618 | "title": "", 619 | "assignee": "", 620 | "dueDate": "", 621 | "zeebeJobKey": "", 622 | "zeebeSource": "", 623 | "bpmnProcessId": "" 624 | } 625 | ``` 626 | 627 | You can pass `{}` as the body if you want to return all User Tasks. 628 | 629 | 630 | ## Submit Task with Form 631 | 632 | POST `/task/id/:taskId/submit` 633 | 634 | Example: `localhost:8088/task/id/user-task--080946c6-1355-4cd7-9fcf-86fc9c46d4c4/submit` 635 | 636 | Json Body: 637 | 638 | ```json 639 | { 640 | "data": { 641 | "textField2": "sog", 642 | "dog": "cat" 643 | }, 644 | "metadata": {} 645 | } 646 | ``` 647 | 648 | 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. 649 | 650 | 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. 651 | Then a background worker is watching for completed user tasks and will attempt to report this back to the Zeebe Job. 652 | The behaviour is this way so you can complete User Tasks without having to have a active connection to the Zeebe Cluster. 653 | 654 | ## Delete User Task 655 | 656 | TODO... 657 | 658 | 659 | ## Claim User Task 660 | 661 | TODO... 662 | 663 | ## UnClaim User Task 664 | 665 | TODO... 666 | 667 | ## Assign User Task 668 | 669 | TODO... 670 | 671 | ## Create Custom User Task (not backed by Zeebe Job) 672 | 673 | TODO... 674 | 675 | ---- 676 | 677 | # Raw Notes 678 | 679 | 1. Implements clustering and scaling through Vertx instances. 680 | 1. ZeebeClientVerticle can increase in number of instances: 1 instance == 1 ZeebeClient Channel connection. 681 | 1. ExecutorVerticle is independent of ZeebeClient. You can scale executors across the cluster to any number of instances and have full cluster feature set. 682 | 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. 683 | 1. Management HTTP takes a apiRoot namespace which is the prefix for the api calls to deploy and start process instances 684 | 1. TODO: Add a send message HTTP verticle 685 | 1. UserTaskConfiguration is the data that is received from a Zeebe Custom Headers which is used to generate the Entity 686 | 1. UserTaskVerticle is a example of a custom worker. I have made them individual so User Tasks can be managed as stand along systems 687 | 1. The Client Name property of the Client config is used as the EB address namespace for sending job completions back over the wire 688 | 1. TODO move executeBlocking code into their own worker verticles with their own thread pools 689 | 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) 690 | 1. Breakers needs to be added 691 | 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. 692 | 1. TODO review defaults and setup of entity build in the user task verticle as its very messy right now. 693 | 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. 694 | 1. TODO move EB addresses in a global static class for easy global management 695 | 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. 696 | 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. 697 | 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 698 | 1. TODO Refactor error handling on HTTP requests to provider better json errors 699 | 700 | ```xml 701 | ... 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | ... 711 | ``` 712 | -------------------------------------------------------------------------------- /bpmn/bpmn1.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0mi3b9p 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | SequenceFlow_0z6yvdy 18 | SequenceFlow_0zyd6q2 19 | 20 | 21 | SequenceFlow_0zyd6q2 22 | 23 | 24 | 25 | 26 | 27 | 28 | SequenceFlow_0mi3b9p 29 | SequenceFlow_1wzqads 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | SequenceFlow_1wzqads 38 | SequenceFlow_0z6yvdy 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | zeebe: 5 | restart: always 6 | container_name: zeebe_broker 7 | image: camunda/zeebe:0.20.0 8 | environment: 9 | - ZEEBE_LOG_LEVEL=info 10 | ports: 11 | - "26500:26500" 12 | - "9600:9600" -------------------------------------------------------------------------------- /docs/design/cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/cluster.png -------------------------------------------------------------------------------- /docs/design/dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/dataflow.png -------------------------------------------------------------------------------- /docs/design/designs.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/designs.graffle -------------------------------------------------------------------------------- /docs/design/form/FormBuilder1-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/form/FormBuilder1-build.png -------------------------------------------------------------------------------- /docs/design/form/FormBuilder2-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/form/FormBuilder2-build.png -------------------------------------------------------------------------------- /docs/design/form/FormBuilder3-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/form/FormBuilder3-build.png -------------------------------------------------------------------------------- /docs/design/form/FormBuilder4-render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/form/FormBuilder4-render.png -------------------------------------------------------------------------------- /docs/design/form/User-Task-Form-Completion-Flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/Quintessential-Tasklist-Zeebe/f61520e062a38b79fca5fa9bc8c82aad9b84316e/docs/design/form/User-Task-Form-Completion-Flow.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.stephenott 8 | Quintenssential-Tasklist-Zeebe 9 | 0.5 10 | 11 | 12 | 13 | UTF-8 14 | 1.8 15 | 1.8 16 | 3.8.1 17 | 0.21.0-alpha1 18 | 19 | com.github.stephenott.MainVerticle 20 | 21 | 22 | 23 | 24 | 25 | io.vertx 26 | vertx-stack-depchain 27 | ${vertx.version} 28 | pom 29 | import 30 | 31 | 32 | 33 | com.fasterxml.jackson 34 | jackson-bom 35 | 2.9.9 36 | pom 37 | import 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | io.vertx 46 | vertx-core 47 | 48 | 49 | io.vertx 50 | vertx-config 51 | 52 | 53 | io.vertx 54 | vertx-config-yaml 55 | 56 | 57 | io.vertx 58 | vertx-circuit-breaker 59 | 60 | 61 | io.vertx 62 | vertx-web 63 | 64 | 65 | io.vertx 66 | vertx-web-client 67 | 68 | 69 | 70 | org.mongodb 71 | mongodb-driver-reactivestreams 72 | 1.12.0 73 | 74 | 75 | 76 | io.zeebe 77 | zeebe-client-java 78 | ${zeebe.version} 79 | 80 | 81 | 82 | com.fasterxml.jackson.module 83 | jackson-module-parameter-names 84 | 85 | 86 | com.fasterxml.jackson.datatype 87 | jackson-datatype-jdk8 88 | 89 | 90 | com.fasterxml.jackson.datatype 91 | jackson-datatype-jsr310 92 | 93 | 94 | 95 | org.slf4j 96 | slf4j-jdk14 97 | 1.7.28 98 | 99 | 100 | 101 | 102 | io.zeebe 103 | zeebe-test 104 | ${zeebe.version} 105 | test 106 | 107 | 108 | 109 | de.flapdoodle.embed 110 | de.flapdoodle.embed.mongo 111 | 2.2.0 112 | test 113 | 114 | 115 | 116 | io.vertx 117 | vertx-codegen 118 | processor 119 | provided 120 | 121 | 122 | io.vertx 123 | vertx-service-proxy 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | maven-compiler-plugin 135 | 3.8.1 136 | 137 | 1.8 138 | 1.8 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/MainVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott; 2 | 3 | import com.fasterxml.jackson.databind.SerializationFeature; 4 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 6 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 7 | import com.github.stephenott.common.EventBusableMessageCodec; 8 | import com.github.stephenott.conf.ApplicationConfiguration; 9 | import com.github.stephenott.executors.JobResult; 10 | import com.github.stephenott.executors.polyglot.ExecutorVerticle; 11 | import com.github.stephenott.executors.usertask.UserTaskExecutorVerticle; 12 | import com.github.stephenott.form.validator.FormValidationServerHttpVerticle; 13 | import com.github.stephenott.form.validator.ValidationRequest; 14 | import com.github.stephenott.form.validator.ValidationRequestResult; 15 | import com.github.stephenott.managementserver.ManagementHttpVerticle; 16 | import com.github.stephenott.usertask.*; 17 | import com.github.stephenott.usertask.mongo.MongoManager; 18 | import com.github.stephenott.zeebe.client.ZeebeClientVerticle; 19 | import com.mongodb.MongoClientSettings; 20 | import com.mongodb.reactivestreams.client.MongoClients; 21 | import io.vertx.config.ConfigRetriever; 22 | import io.vertx.config.ConfigRetrieverOptions; 23 | import io.vertx.config.ConfigStoreOptions; 24 | import io.vertx.core.*; 25 | import io.vertx.core.eventbus.EventBus; 26 | import io.vertx.core.json.Json; 27 | import io.vertx.core.json.JsonObject; 28 | import org.bson.codecs.configuration.CodecRegistry; 29 | import org.bson.codecs.pojo.PojoCodecProvider; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import static org.bson.codecs.configuration.CodecRegistries.*; 34 | 35 | public class MainVerticle extends AbstractVerticle { 36 | 37 | private Logger log = LoggerFactory.getLogger(MainVerticle.class); 38 | 39 | private EventBus eb; 40 | 41 | private ConfigRetriever appConfigRetriever; 42 | ApplicationConfiguration appConfig; 43 | 44 | @Override 45 | public void start() throws Exception { 46 | Json.mapper.registerModules(new ParameterNamesModule(), new Jdk8Module(), new JavaTimeModule()); 47 | Json.prettyMapper.registerModules(new ParameterNamesModule(), new Jdk8Module(), new JavaTimeModule()); 48 | Json.mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 49 | Json.prettyMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 50 | 51 | 52 | eb = vertx.eventBus(); 53 | 54 | eb.registerDefaultCodec(FailedDbActionException.class, new EventBusableMessageCodec<>(FailedDbActionException.class)); 55 | eb.registerDefaultCodec(JobResult.class, new EventBusableMessageCodec<>(JobResult.class)); 56 | eb.registerDefaultCodec(DbActionResult.class, new EventBusableMessageCodec<>(DbActionResult.class)); 57 | eb.registerDefaultCodec(CompletionRequest.class, new EventBusableMessageCodec<>(CompletionRequest.class)); 58 | eb.registerDefaultCodec(GetRequest.class, new EventBusableMessageCodec<>(GetRequest.class)); 59 | eb.registerDefaultCodec(ValidationRequest.class, new EventBusableMessageCodec<>(ValidationRequest.class)); 60 | eb.registerDefaultCodec(ValidationRequestResult.class, new EventBusableMessageCodec<>(ValidationRequestResult.class)); 61 | 62 | String configYmlPath = config().getString("configYmlPath"); 63 | 64 | retrieveAppConfig(configYmlPath, result -> { 65 | if (result.succeeded()) { 66 | appConfig = result.result(); 67 | 68 | //Setup Mongo: 69 | CodecRegistry registry = fromRegistries( 70 | MongoClients.getDefaultCodecRegistry(), 71 | fromProviders(PojoCodecProvider.builder() 72 | .automatic(true) 73 | .build()) 74 | ); 75 | MongoClientSettings mSettings = MongoClientSettings.builder() 76 | .codecRegistry(registry) 77 | .build(); 78 | MongoManager.setClient(MongoClients.create(mSettings)); 79 | 80 | //@TODO refactor this 81 | vertx.deployVerticle(UserTaskActionsVerticle.class, new DeploymentOptions()); 82 | //@TODO refactor this 83 | 84 | deployUserTaskHttpServer(appConfig.getUserTaskServer()); 85 | 86 | 87 | appConfig.getExecutors().forEach(this::deployExecutorVerticle); 88 | 89 | appConfig.getUserTaskExecutors().forEach(this::deployUserTaskExecutorVerticle); 90 | 91 | appConfig.getZeebe().getClients().forEach(this::deployZeebeClient); 92 | 93 | if (appConfig.getManagementServer().isEnabled()) { 94 | deployManagementClient(appConfig.getManagementServer()); 95 | } 96 | 97 | if (appConfig.getFormValidatorServer().isEnabled()){ 98 | deployFormValidationServer(appConfig.getFormValidatorServer()); 99 | } 100 | 101 | } else { 102 | throw new IllegalStateException("Unable to read yml configuration", result.cause()); 103 | } 104 | }); 105 | } 106 | 107 | private void deployManagementClient(ApplicationConfiguration.ManagementHttpConfiguration config) { 108 | DeploymentOptions options = new DeploymentOptions() 109 | .setInstances(config.getInstances()) 110 | .setConfig(JsonObject.mapFrom(config)); 111 | 112 | vertx.deployVerticle(ManagementHttpVerticle::new, options, deployResult -> { 113 | if (deployResult.succeeded()) { 114 | log.info("Management Client has successfully deployed"); 115 | } else { 116 | log.error("Management Client failed to deploy", deployResult.cause()); 117 | } 118 | }); 119 | } 120 | 121 | private void deployUserTaskHttpServer(ApplicationConfiguration.UserTaskHttpServerConfiguration config) { 122 | DeploymentOptions options = new DeploymentOptions() 123 | .setInstances(config.getInstances()) 124 | .setConfig(JsonObject.mapFrom(config)); 125 | 126 | vertx.deployVerticle(UserTaskHttpServerVerticle::new, options, deployResult -> { 127 | if (deployResult.succeeded()) { 128 | log.info("UserTask HTTP Server has successfully deployed"); 129 | } else { 130 | log.error("UserTask HTTP Server failed to deploy", deployResult.cause()); 131 | } 132 | }); 133 | } 134 | 135 | private void deployFormValidationServer(ApplicationConfiguration.FormValidationServerConfiguration config) { 136 | DeploymentOptions options = new DeploymentOptions() 137 | .setInstances(config.getInstances()) 138 | .setConfig(JsonObject.mapFrom(config)); 139 | 140 | vertx.deployVerticle(FormValidationServerHttpVerticle::new, options, deployResult -> { 141 | if (deployResult.succeeded()) { 142 | log.info("Form Validation Server has successfully deployed"); 143 | } else { 144 | log.error("Form Validation Server failed to deploy", deployResult.cause()); 145 | } 146 | }); 147 | } 148 | 149 | 150 | private void deployExecutorVerticle(ApplicationConfiguration.ExecutorConfiguration config) { 151 | DeploymentOptions options = new DeploymentOptions() 152 | .setInstances(config.getInstances()) 153 | .setConfig(JsonObject.mapFrom(config)); 154 | 155 | vertx.deployVerticle(ExecutorVerticle::new, options, vert -> { 156 | if (vert.succeeded()) { 157 | log.info("Executor Verticle " + config.getName() + " has successfully deployed (" + config.getInstances() + " instances)"); 158 | } else { 159 | log.error("Executor Verticle " + config.getName() + " has failed to deploy!", vert.cause()); 160 | } 161 | }); 162 | } 163 | 164 | private void deployUserTaskExecutorVerticle(ApplicationConfiguration.UserTaskExecutorConfiguration config) { 165 | DeploymentOptions options = new DeploymentOptions(); 166 | options.setConfig(JsonObject.mapFrom(config)); 167 | 168 | vertx.deployVerticle(UserTaskExecutorVerticle::new, options, vert -> { 169 | if (vert.succeeded()) { 170 | log.info("UserTask Executor Verticle " + config.getName() + " has successfully deployed"); 171 | } else { 172 | log.error("UserTask Executor Verticle " + config.getName() + " has failed to deploy!", vert.cause()); 173 | } 174 | }); 175 | } 176 | 177 | private void deployZeebeClient(ApplicationConfiguration.ZeebeClientConfiguration config) { 178 | DeploymentOptions options = new DeploymentOptions(); 179 | options.setConfig(JsonObject.mapFrom(config)); 180 | 181 | vertx.deployVerticle(ZeebeClientVerticle::new, options, vert -> { 182 | if (vert.succeeded()) { 183 | log.info("Zeebe Client Verticle " + config.getName() + " has successfully deployed"); 184 | } else { 185 | log.error("Zeebe Client Verticle " + config.getName() + " has failed to deploy!", vert.cause()); 186 | } 187 | }); 188 | } 189 | 190 | 191 | private void retrieveAppConfig(String filePath, Handler> result) { 192 | ConfigStoreOptions store = new ConfigStoreOptions() 193 | .setType("file") 194 | .setFormat("yaml") 195 | .setConfig(new JsonObject() 196 | .put("path", filePath) 197 | ); 198 | 199 | appConfigRetriever = ConfigRetriever.create(vertx, new ConfigRetrieverOptions().addStore(store)); 200 | 201 | appConfigRetriever.getConfig(retrieverResult -> { 202 | if (retrieverResult.succeeded()) { 203 | result.handle(Future.succeededFuture(retrieverResult.result().mapTo(ApplicationConfiguration.class))); 204 | 205 | } else { 206 | result.handle(Future.failedFuture(retrieverResult.cause())); 207 | } 208 | }); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/common/Common.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.common; 2 | 3 | public class Common { 4 | 5 | public static String JOB_ADDRESS_PREFIX = "job."; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/common/EventBusable.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.common; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | 5 | public interface EventBusable { 6 | 7 | default JsonObject toJsonObject(){ 8 | return JsonObject.mapFrom(this); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/common/EventBusableMessageCodec.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.common; 2 | 3 | import io.vertx.core.buffer.Buffer; 4 | import io.vertx.core.eventbus.MessageCodec; 5 | import io.vertx.core.json.Json; 6 | 7 | public class EventBusableMessageCodec implements MessageCodec { 8 | 9 | private Class tClass; 10 | 11 | public EventBusableMessageCodec(Class tClass) { 12 | this.tClass = tClass; 13 | } 14 | 15 | @Override 16 | public void encodeToWire(Buffer buffer, T t) { 17 | Buffer encoded = t.toJsonObject().toBuffer(); 18 | buffer.appendInt(encoded.length()); 19 | buffer.appendBuffer(encoded); 20 | } 21 | 22 | @Override 23 | public T decodeFromWire(int pos, Buffer buffer) { 24 | int length = buffer.getInt(pos); 25 | pos += 4; 26 | return Json.decodeValue(buffer.slice(pos, pos + length), tClass); 27 | } 28 | 29 | @Override 30 | public T transform(T t) { 31 | return t.toJsonObject().copy().mapTo(tClass); 32 | } 33 | 34 | @Override 35 | public String name() { 36 | return tClass.getCanonicalName(); 37 | } 38 | 39 | @Override 40 | public byte systemCodecID() { 41 | return -1; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/common/EventBusableReplyException.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.common; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.github.stephenott.usertask.FailedDbActionException; 7 | import io.vertx.core.eventbus.ReplyException; 8 | import io.vertx.core.eventbus.ReplyFailure; 9 | 10 | @JsonAutoDetect(fieldVisibility = Visibility.NONE, 11 | getterVisibility = Visibility.NONE, 12 | setterVisibility = Visibility.NONE, 13 | isGetterVisibility = Visibility.NONE) 14 | public class EventBusableReplyException extends ReplyException implements EventBusable { 15 | 16 | @JsonProperty() 17 | private Enum failureType; 18 | 19 | @JsonProperty() 20 | private String internalErrorMessage; 21 | 22 | @JsonProperty() 23 | private String userErrorMessage; 24 | // 25 | // public EventBusableReplyException(FailedDbActionException.FailureType failureType, Throwable internalError, String UserErrorMessage){ 26 | // super(ReplyFailure.RECIPIENT_FAILURE, UserErrorMessage); 27 | // this.internalErrorMessage 28 | // } 29 | 30 | public EventBusableReplyException(Enum failureType, String internalErrorMessage, String userErrorMessage) { 31 | super(ReplyFailure.RECIPIENT_FAILURE, userErrorMessage); 32 | this.internalErrorMessage = internalErrorMessage; 33 | this.userErrorMessage = userErrorMessage; 34 | this.failureType = failureType; 35 | } 36 | 37 | public String getInternalErrorMessage() { 38 | return internalErrorMessage; 39 | } 40 | 41 | public EventBusableReplyException setInternalErrorMessage(String internalErrorMessage) { 42 | this.internalErrorMessage = internalErrorMessage; 43 | return this; 44 | } 45 | 46 | public String getUserErrorMessage() { 47 | return userErrorMessage; 48 | } 49 | 50 | public EventBusableReplyException setUserErrorMessage(String userErrorMessage) { 51 | this.userErrorMessage = userErrorMessage; 52 | return this; 53 | } 54 | 55 | public Enum getFailureType() { 56 | return failureType; 57 | } 58 | 59 | public EventBusableReplyException setFailureType(Enum failureType) { 60 | this.failureType = failureType; 61 | return this; 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/conf/ApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.conf; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.github.stephenott.executors.usertask.UserTaskConfiguration; 5 | 6 | import java.time.Duration; 7 | import java.util.List; 8 | import java.util.UUID; 9 | 10 | public class ApplicationConfiguration { 11 | 12 | private ZeebeConfiguration zeebe; 13 | private List executors; 14 | private List userTaskExecutors; 15 | private ManagementHttpConfiguration managementServer; 16 | private FormValidationServerConfiguration formValidatorServer; 17 | private UserTaskHttpServerConfiguration userTaskServer; 18 | 19 | public ApplicationConfiguration() { 20 | } 21 | 22 | public ZeebeConfiguration getZeebe() { 23 | return zeebe; 24 | } 25 | 26 | public void setZeebe(ZeebeConfiguration zeebe) { 27 | this.zeebe = zeebe; 28 | } 29 | 30 | public List getExecutors() { 31 | return executors; 32 | } 33 | 34 | public void setExecutors(List executors) { 35 | this.executors = executors; 36 | } 37 | 38 | public List getUserTaskExecutors() { 39 | return userTaskExecutors; 40 | } 41 | 42 | public void setUserTaskExecutors(List userTaskExecutors) { 43 | this.userTaskExecutors = userTaskExecutors; 44 | } 45 | 46 | public ManagementHttpConfiguration getManagementServer() { 47 | return managementServer; 48 | } 49 | 50 | public void setManagementServer(ManagementHttpConfiguration managementServer) { 51 | this.managementServer = managementServer; 52 | } 53 | 54 | public FormValidationServerConfiguration getFormValidatorServer() { 55 | return formValidatorServer; 56 | } 57 | 58 | public ApplicationConfiguration setFormValidatorServer(FormValidationServerConfiguration formValidatorServer) { 59 | this.formValidatorServer = formValidatorServer; 60 | return this; 61 | } 62 | 63 | public UserTaskHttpServerConfiguration getUserTaskServer() { 64 | return userTaskServer; 65 | } 66 | 67 | public ApplicationConfiguration setUserTaskServer(UserTaskHttpServerConfiguration userTaskServer) { 68 | this.userTaskServer = userTaskServer; 69 | return this; 70 | } 71 | 72 | public static class ZeebeConfiguration{ 73 | private List clients; 74 | 75 | public ZeebeConfiguration() { 76 | } 77 | 78 | public List getClients() { 79 | return clients; 80 | } 81 | 82 | public void setClients(List clients) { 83 | this.clients = clients; 84 | } 85 | } 86 | 87 | 88 | public static class ZeebeClientConfiguration{ 89 | private String name; 90 | private String brokerContactPoint = "localhost:25600"; 91 | 92 | @JsonFormat(shape = JsonFormat.Shape.STRING) 93 | private Duration requestTimeout = Duration.ofSeconds(10); 94 | 95 | private List workers; 96 | 97 | public ZeebeClientConfiguration() { 98 | } 99 | 100 | public String getName() { 101 | return name; 102 | } 103 | 104 | public void setName(String name) { 105 | this.name = name; 106 | } 107 | 108 | public String getBrokerContactPoint() { 109 | return brokerContactPoint; 110 | } 111 | 112 | public void setBrokerContactPoint(String brokerContactPoint) { 113 | this.brokerContactPoint = brokerContactPoint; 114 | } 115 | 116 | public Duration getRequestTimeout() { 117 | return requestTimeout; 118 | } 119 | 120 | public void setRequestTimeout(Duration requestTimeout) { 121 | this.requestTimeout = requestTimeout; 122 | } 123 | 124 | public List getWorkers() { 125 | return workers; 126 | } 127 | 128 | public void setWorkers(List workers) { 129 | this.workers = workers; 130 | } 131 | } 132 | 133 | 134 | public static class ZeebeWorkers { 135 | private String name; 136 | private List jobTypes; 137 | 138 | @JsonFormat(shape = JsonFormat.Shape.STRING) 139 | private Duration timeout = Duration.ofSeconds(10); 140 | 141 | public ZeebeWorkers() { 142 | } 143 | 144 | public String getName() { 145 | return name; 146 | } 147 | 148 | public void setName(String name) { 149 | this.name = name; 150 | } 151 | 152 | public List getJobTypes() { 153 | return jobTypes; 154 | } 155 | 156 | public void setJobTypes(List jobTypes) { 157 | this.jobTypes = jobTypes; 158 | } 159 | 160 | /** 161 | * The Timeout of how long the Job is locked to the worker / subscription 162 | * @return 163 | */ 164 | public Duration getTimeout() { 165 | return timeout; 166 | } 167 | 168 | public ZeebeWorkers setTimeout(Duration timeout) { 169 | this.timeout = timeout; 170 | return this; 171 | } 172 | } 173 | 174 | 175 | public static class ExecutorConfiguration { 176 | private String name; 177 | private String address; 178 | private String execute; 179 | private int instances = 1; 180 | 181 | public ExecutorConfiguration() { 182 | } 183 | 184 | public String getName() { 185 | return name; 186 | } 187 | 188 | public void setName(String name) { 189 | this.name = name; 190 | } 191 | 192 | public String getAddress() { 193 | return address; 194 | } 195 | 196 | public void setAddress(String address) { 197 | this.address = address; 198 | } 199 | 200 | public String getExecute() { 201 | return execute; 202 | } 203 | 204 | public void setExecute(String execute) { 205 | this.execute = execute; 206 | } 207 | 208 | public int getInstances() { 209 | return instances; 210 | } 211 | 212 | public void setInstances(int instances) { 213 | this.instances = instances; 214 | } 215 | } 216 | 217 | public static class ManagementHttpConfiguration { 218 | private boolean enabled = true; 219 | private String apiRoot = UUID.randomUUID().toString(); 220 | private ZeebeClientConfiguration zeebeClient; 221 | private String fileUploadPath = "./tmp/uploads"; 222 | private int instances = 1; 223 | private int port = 8080; 224 | private String corsRegex; 225 | 226 | public ManagementHttpConfiguration() { 227 | } 228 | 229 | public boolean isEnabled() { 230 | return enabled; 231 | } 232 | 233 | public void setEnabled(boolean enabled) { 234 | this.enabled = enabled; 235 | } 236 | 237 | public String getApiRoot() { 238 | return apiRoot; 239 | } 240 | 241 | public void setApiRoot(String apiRoot) { 242 | this.apiRoot = apiRoot; 243 | } 244 | 245 | public ZeebeClientConfiguration getZeebeClient() { 246 | return zeebeClient; 247 | } 248 | 249 | public void setZeebeClient(ZeebeClientConfiguration zeebeClient) { 250 | this.zeebeClient = zeebeClient; 251 | } 252 | 253 | public String getFileUploadPath() { 254 | return fileUploadPath; 255 | } 256 | 257 | public void setFileUploadPath(String fileUploadPath) { 258 | this.fileUploadPath = fileUploadPath; 259 | } 260 | 261 | public int getInstances() { 262 | return instances; 263 | } 264 | 265 | public void setInstances(int instances) { 266 | this.instances = instances; 267 | } 268 | 269 | public int getPort() { 270 | return port; 271 | } 272 | 273 | public void setPort(int port) { 274 | this.port = port; 275 | } 276 | 277 | public String getCorsRegex() { 278 | return corsRegex; 279 | } 280 | 281 | public void setCorsRegex(String corsRegex) { 282 | this.corsRegex = corsRegex; 283 | } 284 | } 285 | 286 | public static class UserTaskExecutorConfiguration { 287 | private String name; 288 | private String address; 289 | private UserTaskConfiguration defaults; 290 | private UserTaskConfiguration overrides; 291 | private int instances; 292 | 293 | public UserTaskExecutorConfiguration() { 294 | } 295 | 296 | public String getName() { 297 | return name; 298 | } 299 | 300 | public void setName(String name) { 301 | this.name = name; 302 | } 303 | 304 | public String getAddress() { 305 | return address; 306 | } 307 | 308 | public void setAddress(String address) { 309 | this.address = address; 310 | } 311 | 312 | public UserTaskConfiguration getDefaults() { 313 | return defaults; 314 | } 315 | 316 | public void setDefaults(UserTaskConfiguration defaults) { 317 | this.defaults = defaults; 318 | } 319 | 320 | public UserTaskConfiguration getOverrides() { 321 | return overrides; 322 | } 323 | 324 | public void setOverrides(UserTaskConfiguration overrides) { 325 | this.overrides = overrides; 326 | } 327 | 328 | public int getInstances() { 329 | return instances; 330 | } 331 | 332 | public void setInstances(int instances) { 333 | this.instances = instances; 334 | } 335 | } 336 | 337 | /** 338 | * The Form Validation Server (Verticle) 339 | */ 340 | public static class FormValidationServerConfiguration { 341 | private boolean enabled = true; 342 | private int instances = 1; 343 | private int port = 8082; 344 | private String corsRegex; 345 | private String address = "form-validation"; 346 | private FormValidatorServiceConfiguration formValidatorService; 347 | 348 | public FormValidationServerConfiguration() { 349 | } 350 | 351 | public boolean isEnabled() { 352 | return enabled; 353 | } 354 | 355 | public FormValidationServerConfiguration setEnabled(boolean enabled) { 356 | this.enabled = enabled; 357 | return this; 358 | } 359 | 360 | public int getInstances() { 361 | return instances; 362 | } 363 | 364 | public FormValidationServerConfiguration setInstances(int instances) { 365 | this.instances = instances; 366 | return this; 367 | } 368 | 369 | public int getPort() { 370 | return port; 371 | } 372 | 373 | public FormValidationServerConfiguration setPort(int port) { 374 | this.port = port; 375 | return this; 376 | } 377 | 378 | public String getCorsRegex() { 379 | return corsRegex; 380 | } 381 | 382 | public FormValidationServerConfiguration setCorsRegex(String corsRegex) { 383 | this.corsRegex = corsRegex; 384 | return this; 385 | } 386 | 387 | public String getAddress() { 388 | return address; 389 | } 390 | 391 | public FormValidationServerConfiguration setAddress(String address) { 392 | this.address = address; 393 | return this; 394 | } 395 | 396 | public FormValidatorServiceConfiguration getFormValidatorService() { 397 | return formValidatorService; 398 | } 399 | 400 | public FormValidationServerConfiguration setFormValidatorService(FormValidatorServiceConfiguration formValidatorService) { 401 | this.formValidatorService = formValidatorService; 402 | return this; 403 | } 404 | } 405 | 406 | /** 407 | * The Form Validator Service 408 | * (The external service that is communicated over HTTP that performs 409 | * the actual validation against the supplied schema) 410 | */ 411 | public static class FormValidatorServiceConfiguration { 412 | private String host = "localhost"; 413 | private int port = 8083; 414 | private String validateUri = "/validate"; 415 | private long requestTimeout = 5000; 416 | 417 | public FormValidatorServiceConfiguration() { 418 | } 419 | 420 | public String getHost() { 421 | return host; 422 | } 423 | 424 | public FormValidatorServiceConfiguration setHost(String host) { 425 | this.host = host; 426 | return this; 427 | } 428 | 429 | public int getPort() { 430 | return port; 431 | } 432 | 433 | public FormValidatorServiceConfiguration setPort(int port) { 434 | this.port = port; 435 | return this; 436 | } 437 | 438 | public String getValidateUri() { 439 | return validateUri; 440 | } 441 | 442 | public FormValidatorServiceConfiguration setValidateUri(String validateUri) { 443 | this.validateUri = validateUri; 444 | return this; 445 | } 446 | 447 | //@TODO Refactor this to support the java8 Duration class 448 | public long getRequestTimeout() { 449 | return requestTimeout; 450 | } 451 | 452 | public FormValidatorServiceConfiguration setRequestTimeout(long requestTimeout) { 453 | this.requestTimeout = requestTimeout; 454 | return this; 455 | } 456 | } 457 | 458 | public static class UserTaskHttpServerConfiguration { 459 | private boolean enabled = true; 460 | private int instances = 1; 461 | private int port = 8080; 462 | private String corsRegex; 463 | 464 | public boolean isEnabled() { 465 | return enabled; 466 | } 467 | 468 | public UserTaskHttpServerConfiguration setEnabled(boolean enabled) { 469 | this.enabled = enabled; 470 | return this; 471 | } 472 | 473 | public int getInstances() { 474 | return instances; 475 | } 476 | 477 | public UserTaskHttpServerConfiguration setInstances(int instances) { 478 | this.instances = instances; 479 | return this; 480 | } 481 | 482 | public int getPort() { 483 | return port; 484 | } 485 | 486 | public UserTaskHttpServerConfiguration setPort(int port) { 487 | this.port = port; 488 | return this; 489 | } 490 | 491 | public String getCorsRegex() { 492 | return corsRegex; 493 | } 494 | 495 | public UserTaskHttpServerConfiguration setCorsRegex(String corsRegex) { 496 | this.corsRegex = corsRegex; 497 | return this; 498 | } 499 | } 500 | 501 | } -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/conf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "configYmlPath": "./zeebe.yml" 3 | } -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/executors/JobResult.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.executors; 2 | 3 | import com.github.stephenott.common.EventBusable; 4 | import io.vertx.core.json.JsonObject; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | 10 | public class JobResult implements EventBusable { 11 | 12 | private Result result; 13 | private long jobKey; 14 | private Map variables = new HashMap<>(); 15 | private int retries; 16 | private String errorMessage = "An error occurred"; 17 | 18 | public enum Result { 19 | COMPLETE, 20 | FAIL 21 | } 22 | 23 | private JobResult() { 24 | } 25 | 26 | public JobResult(long jobKey, Result result, Map variables, int retries, String errorMessage) { 27 | Objects.requireNonNull(result); 28 | 29 | this.result = result; 30 | this.jobKey = jobKey; 31 | this.variables = variables; 32 | this.retries = retries; 33 | this.errorMessage = errorMessage; 34 | } 35 | 36 | public JobResult(long jobKey, Result result, int retries) { 37 | this(jobKey, result, null, retries, null); 38 | } 39 | 40 | /** 41 | * Will set retries to 0. 42 | * @param jobKey 43 | * @param result 44 | */ 45 | public JobResult(long jobKey, Result result) { 46 | setJobKey(jobKey); 47 | setResult(result); 48 | } 49 | 50 | public Result getResult() { 51 | return result; 52 | } 53 | 54 | public JobResult setResult(Result result) { 55 | this.result = result; 56 | return this; 57 | } 58 | 59 | public long getJobKey() { 60 | return jobKey; 61 | } 62 | 63 | public JobResult setJobKey(long jobKey) { 64 | this.jobKey = jobKey; 65 | return this; 66 | } 67 | 68 | public Map getVariables() { 69 | return variables; 70 | } 71 | 72 | public JobResult setVariables(Map variables) { 73 | this.variables = variables; 74 | return this; 75 | } 76 | 77 | public int getRetries() { 78 | return retries; 79 | } 80 | 81 | public JobResult setRetries(int retries) { 82 | this.retries = retries; 83 | return this; 84 | } 85 | 86 | public String getErrorMessage() { 87 | return errorMessage; 88 | } 89 | 90 | public JobResult setErrorMessage(String errorMessage) { 91 | this.errorMessage = errorMessage; 92 | return this; 93 | } 94 | 95 | // public JsonObject toJsonObject(){ 96 | // return JsonObject.mapFrom(this); 97 | // } 98 | // 99 | // public static JobResult fromJsonObject(JsonObject doneJob){ 100 | // return doneJob.mapTo(JobResult.class); 101 | // } 102 | 103 | } -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/executors/polyglot/ExecutorVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.executors.polyglot; 2 | 3 | import com.github.stephenott.common.Common; 4 | import com.github.stephenott.executors.JobResult; 5 | import com.github.stephenott.conf.ApplicationConfiguration; 6 | import io.vertx.core.AbstractVerticle; 7 | import io.vertx.core.eventbus.EventBus; 8 | import io.vertx.core.json.JsonObject; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class ExecutorVerticle extends AbstractVerticle { 13 | 14 | private Logger log = LoggerFactory.getLogger(ExecutorVerticle.class); 15 | 16 | private EventBus eb; 17 | private ApplicationConfiguration.ExecutorConfiguration executorConfiguration; 18 | 19 | @Override 20 | public void start() throws Exception { 21 | try { 22 | executorConfiguration = config().mapTo(ApplicationConfiguration.ExecutorConfiguration.class); 23 | log.info("Executor Config: " + JsonObject.mapFrom(executorConfiguration).toString()); 24 | } catch (Exception e){ 25 | throw new IllegalStateException("Could not load executor configuration"); 26 | } 27 | 28 | eb = vertx.eventBus(); 29 | 30 | String address = Common.JOB_ADDRESS_PREFIX + executorConfiguration.getAddress(); 31 | 32 | eb.consumer(address, handler -> { 33 | log.info("doing some work!!!"); 34 | 35 | String sourceClient = handler.headers().get("sourceClient"); 36 | 37 | JsonObject job = handler.body(); 38 | 39 | //@TODO Add polyexecutor 40 | 41 | JobResult jobResult = new JobResult( 42 | job.getLong("key"), 43 | JobResult.Result.COMPLETE, 44 | (job.getInteger("retries") > 0) ? job.getInteger("retries") - 1 : 0); 45 | 46 | eb.send(sourceClient + ".job-action.completion", jobResult); 47 | 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/executors/usertask/UserTaskConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.executors.usertask; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class UserTaskConfiguration { 7 | 8 | private String title; 9 | private String description; 10 | private int priority = 0; 11 | private String assignee; 12 | private String candidateGroups; 13 | private String candidateUsers; 14 | private String dueDate; 15 | private String formKey; 16 | 17 | public UserTaskConfiguration() { 18 | } 19 | 20 | public String getTitle() { 21 | return title; 22 | } 23 | 24 | public void setTitle(String title) { 25 | this.title = title; 26 | } 27 | 28 | public String getDescription() { 29 | return description; 30 | } 31 | 32 | public void setDescription(String description) { 33 | this.description = description; 34 | } 35 | 36 | public int getPriority() { 37 | return priority; 38 | } 39 | 40 | public void setPriority(int priority) { 41 | this.priority = priority; 42 | } 43 | 44 | public String getAssignee() { 45 | return assignee; 46 | } 47 | 48 | public void setAssignee(String assignee) { 49 | this.assignee = assignee; 50 | } 51 | 52 | public String getCandidateGroups() { 53 | return candidateGroups; 54 | } 55 | 56 | public void setCandidateGroups(String candidateGroups) { 57 | this.candidateGroups = candidateGroups; 58 | } 59 | 60 | public String getCandidateUsers() { 61 | return candidateUsers; 62 | } 63 | 64 | public void setCandidateUsers(String candidateUsers) { 65 | this.candidateUsers = candidateUsers; 66 | } 67 | 68 | public String getDueDate() { 69 | return dueDate; 70 | } 71 | 72 | public void setDueDate(String dueDate) { 73 | this.dueDate = dueDate; 74 | } 75 | 76 | public String getFormKey() { 77 | return formKey; 78 | } 79 | 80 | public void setFormKey(String formKey) { 81 | this.formKey = formKey; 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/executors/usertask/UserTaskExecutorVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.executors.usertask; 2 | 3 | import com.github.stephenott.common.Common; 4 | import com.github.stephenott.conf.ApplicationConfiguration; 5 | import com.github.stephenott.executors.JobResult; 6 | import com.github.stephenott.usertask.entity.UserTaskEntity; 7 | import com.github.stephenott.usertask.mongo.MongoManager; 8 | import com.github.stephenott.usertask.mongo.Subscribers; 9 | import com.github.stephenott.usertask.mongo.Subscribers.SimpleSubscriber; 10 | import com.mongodb.reactivestreams.client.MongoCollection; 11 | import com.mongodb.reactivestreams.client.Success; 12 | import io.vertx.core.AbstractVerticle; 13 | import io.vertx.core.Future; 14 | import io.vertx.core.Promise; 15 | import io.vertx.core.eventbus.EventBus; 16 | import io.vertx.core.json.Json; 17 | import io.vertx.core.json.JsonObject; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.time.Instant; 22 | import java.util.Arrays; 23 | import java.util.HashSet; 24 | import java.util.UUID; 25 | 26 | public class UserTaskExecutorVerticle extends AbstractVerticle { 27 | 28 | private final Logger log = LoggerFactory.getLogger(UserTaskExecutorVerticle.class); 29 | 30 | private EventBus eb; 31 | 32 | private ApplicationConfiguration.UserTaskExecutorConfiguration utExecutorConfig; 33 | 34 | private MongoCollection tasksCollection = MongoManager.getDatabase().getCollection("tasks", UserTaskEntity.class); 35 | 36 | @Override 37 | public void start() throws Exception { 38 | try { 39 | utExecutorConfig = config().mapTo(ApplicationConfiguration.UserTaskExecutorConfiguration.class); 40 | } catch (Exception e) { 41 | log.error("Unable to parse Ut Executor Config", e); 42 | throw e; 43 | } 44 | 45 | eb = vertx.eventBus(); 46 | 47 | String address = Common.JOB_ADDRESS_PREFIX + utExecutorConfig.getAddress(); 48 | 49 | eb.consumer(address, handler -> { 50 | 51 | log.info("User Task({}) has captured some Work.", address); 52 | 53 | String sourceClient = handler.headers().get("sourceClient"); 54 | //@TODO add handler if sourceClient is missing then reject job. 55 | 56 | UserTaskConfiguration utConfig = handler.body() 57 | .getJsonObject("customHeaders") 58 | .mapTo(UserTaskConfiguration.class); 59 | 60 | UserTaskEntity utEntity = new UserTaskEntity() 61 | .setZeebeSource(sourceClient) 62 | .setTaskId("user-task--" + UUID.randomUUID().toString()) 63 | .setTitle(utConfig.getTitle()) 64 | .setDescription(utConfig.getDescription()) 65 | .setPriority(utConfig.getPriority()) 66 | .setAssignee(utConfig.getAssignee()) 67 | .setCandidateGroups((utConfig.getCandidateGroups() != null) ? new HashSet(Arrays.asList(utConfig.getCandidateGroups().split(","))) : null) 68 | .setCandidateUsers((utConfig.getCandidateUsers() != null) ? new HashSet(Arrays.asList(utConfig.getCandidateUsers().split(","))) : null) 69 | .setDueDate((utConfig.getDueDate() != null) ? Instant.parse(utConfig.getDueDate()) : null) 70 | .setFormKey(utConfig.getFormKey()) 71 | .setZeebeDeadline(Instant.ofEpochMilli(handler.body().getLong("deadline"))) 72 | .setZeebeJobKey(handler.body().getLong("key")) 73 | .setBpmnProcessId(handler.body().getString("bpmnProcessId")) 74 | .setBpmnProcessVersion(handler.body().getInteger("workflowDefinitionVersion")) 75 | .setZeebeVariables(((JsonObject) Json.decodeValue(handler.body().getString("variables"))).getMap()) 76 | .setTaskOriginalCapture(Instant.now()); 77 | 78 | log.info("User Task created: {}", JsonObject.mapFrom(utEntity).toString()); 79 | 80 | saveToDb(utEntity).setHandler(res -> { 81 | if (res.succeeded()) { 82 | log.info("UserTaskEntity has been saved to DB..."); 83 | } else { 84 | //@TODO update with better error 85 | throw new IllegalStateException("Unable to save to DB", res.cause()); 86 | } 87 | }); 88 | 89 | }); 90 | 91 | log.info("User Task Executor Verticle consuming tasks at: {}", utExecutorConfig.getAddress()); 92 | } 93 | 94 | public Future saveToDb(UserTaskEntity entity) { 95 | Promise promise = Promise.promise(); 96 | 97 | tasksCollection.insertOne(entity) 98 | .subscribe(new SimpleSubscriber().singleResult(result -> { 99 | if (result.succeeded()) { 100 | promise.complete(); 101 | } else { 102 | promise.fail(result.cause()); 103 | } 104 | })); 105 | 106 | return promise.future(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/FormValidationServerHttpVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator; 2 | 3 | import com.github.stephenott.conf.ApplicationConfiguration; 4 | import com.github.stephenott.form.validator.exception.ValidationRequestResultException; 5 | import com.github.stephenott.form.validator.exception.ValidationRequestResultException.ErrorType; 6 | import io.vertx.core.*; 7 | import io.vertx.core.eventbus.EventBus; 8 | import io.vertx.core.http.HttpMethod; 9 | import io.vertx.core.http.HttpServer; 10 | import io.vertx.core.http.HttpServerResponse; 11 | import io.vertx.core.json.JsonObject; 12 | import io.vertx.ext.web.Route; 13 | import io.vertx.ext.web.Router; 14 | import io.vertx.ext.web.client.WebClient; 15 | import io.vertx.ext.web.client.WebClientOptions; 16 | import io.vertx.ext.web.handler.BodyHandler; 17 | import io.vertx.ext.web.handler.CorsHandler; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import static com.github.stephenott.form.validator.ValidationRequestResult.*; 22 | import static com.github.stephenott.form.validator.ValidationRequestResult.InvalidResult; 23 | import static com.github.stephenott.form.validator.ValidationRequestResult.ValidResult; 24 | 25 | public class FormValidationServerHttpVerticle extends AbstractVerticle { 26 | 27 | private Logger log = LoggerFactory.getLogger(FormValidationServerHttpVerticle.class); 28 | 29 | private WebClient webClient; 30 | 31 | private EventBus eb; 32 | 33 | private ApplicationConfiguration.FormValidationServerConfiguration formValidationServerConfig; 34 | 35 | @Override 36 | public void start() throws Exception { 37 | formValidationServerConfig = config().mapTo(ApplicationConfiguration.FormValidationServerConfiguration.class); 38 | 39 | eb = vertx.eventBus(); 40 | 41 | if (formValidationServerConfig.isEnabled()) { 42 | 43 | WebClientOptions webClientOptions = new WebClientOptions(); 44 | webClient = WebClient.create(vertx, webClientOptions); 45 | 46 | Router mainRouter = Router.router(vertx); 47 | HttpServer server = vertx.createHttpServer(); 48 | 49 | mainRouter.route().failureHandler(failure -> { 50 | 51 | int statusCode = failure.statusCode(); 52 | 53 | HttpServerResponse response = failure.response(); 54 | response.setStatusCode(statusCode) 55 | .end(new JsonObject().put("error", failure.failure().getLocalizedMessage()).toBuffer()); 56 | }); 57 | 58 | establishFormValidationRoute(mainRouter); 59 | 60 | server.requestHandler(mainRouter) 61 | .listen(formValidationServerConfig.getPort()); 62 | 63 | } 64 | 65 | //@TODO Add a EB config toggle 66 | establishFormValidationEbConsumer(); 67 | 68 | log.info("Form Validation Server deployed at: localhost:...., CORS is .... "); 69 | } 70 | 71 | @Override 72 | public void stop() throws Exception { 73 | super.stop(); 74 | } 75 | 76 | private void establishFormValidationRoute(Router router) { 77 | Route route = router.route(HttpMethod.POST, "/validate") 78 | .consumes("application/json") 79 | .produces("application/json"); 80 | 81 | if (formValidationServerConfig.getCorsRegex() != null) { 82 | route.handler(CorsHandler.create(formValidationServerConfig.getCorsRegex())); 83 | } 84 | 85 | route.handler(BodyHandler.create()); 86 | 87 | route.handler(rc -> { //routing context 88 | //@TODO add a generic helper to parse and handler errors or look at using a "throws" in the method def 89 | // without this try the error is hidden from the logs 90 | ValidationRequest request; 91 | try { 92 | request = rc.getBodyAsJson().mapTo(ValidationRequest.class); 93 | 94 | } catch (Exception e) { 95 | throw new IllegalArgumentException("Unable to parse body", e); 96 | } 97 | 98 | validateFormSubmission(request, handler -> { 99 | if (handler.succeeded()) { 100 | if (handler.result().getResult().equals(Result.VALID)) { 101 | rc.response() 102 | .setStatusCode(202) 103 | .putHeader("content-type", "application/json; charset=utf-8") 104 | .end(JsonObject.mapFrom(handler.result().getValidResultObject()).toBuffer()); 105 | } else { 106 | rc.response() 107 | .setStatusCode(400) 108 | .putHeader("content-type", "application/json; charset=utf-8") 109 | .end(JsonObject.mapFrom(handler.result().getInvalidResultObject()).toBuffer()); 110 | } 111 | 112 | } else { 113 | log.error("Unable to execute validation request", handler.cause()); 114 | rc.fail(500, handler.cause()); 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | private void establishFormValidationEbConsumer() { 121 | 122 | String address = "forms.action.validate"; 123 | 124 | eb.consumer(address, ebHandler -> { 125 | 126 | validateFormSubmission(ebHandler.body(), valResult -> { 127 | if (valResult.succeeded()) { 128 | ebHandler.reply(valResult.result()); 129 | } else { 130 | if (valResult.cause().getClass().equals(ValidationRequestResultException.class)){ 131 | ValidationRequestResultException exception = (ValidationRequestResultException)valResult.cause(); 132 | ebHandler.reply(GenerateErrorResult(new ErrorResult(exception.getErrorType(),exception.getMessage(), exception.getCause().getMessage()))); 133 | 134 | } else { 135 | //@TODO refactor to rethrow a critial error for monitoring purposes. 136 | log.error("Unexpected result returned from validationFormSubmission, and thus unable to reply to " + 137 | "EB message for validation request... Something went wrong...", valResult.cause()); 138 | } 139 | } 140 | }); 141 | }).exceptionHandler(error -> log.error("Could not read Validation Request message from EB", error)); 142 | } 143 | 144 | public void validateFormSubmission(ValidationRequest validationRequest, Handler> handler) { 145 | //@TODO Refactor this to reduce the wordiness... 146 | ApplicationConfiguration.FormValidatorServiceConfiguration validatorConfig = formValidationServerConfig.getFormValidatorService(); 147 | 148 | String host = validatorConfig.getHost(); 149 | int port = validatorConfig.getPort(); 150 | long requestTimeout = validatorConfig.getRequestTimeout(); 151 | String validateUri = validatorConfig.getValidateUri(); 152 | 153 | log.info("BODY: " + JsonObject.mapFrom(new ValidationServiceRequest(validationRequest)).toString()); 154 | 155 | //@TODO look at using the .expect predicate methods as part of .post() rather than using the if statusCode... 156 | webClient.post(port, host, validateUri) 157 | .timeout(requestTimeout) 158 | .sendJson(new ValidationServiceRequest(validationRequest), res -> { 159 | if (res.succeeded()) { 160 | 161 | int statusCode = res.result().statusCode(); 162 | 163 | if (statusCode == 202) { 164 | log.info("FORMIO 202 RESULT: " + res.result().bodyAsString()); 165 | handler.handle(Future.succeededFuture(GenerateValidResult(res.result().bodyAsJson(ValidResult.class)))); 166 | 167 | } else if (statusCode == 400) { 168 | log.info("FORMIO 400 RESULT: " + res.result().bodyAsString()); 169 | handler.handle(Future.succeededFuture(GenerateInvalidResult(res.result().bodyAsJson(InvalidResult.class)))); 170 | 171 | } else { 172 | log.error("Unexpected response returned by form validator: code:" + res.result().statusCode() + ". Body: " + res.result().bodyAsString()); 173 | 174 | handler.handle(Future.failedFuture( 175 | new ValidationRequestResultException(ErrorType.UNEXPECTED_STATUS_CODE, 176 | "Unexpected response returned by form validator: code:" + res.result().statusCode() + ". Body: " + res.result().bodyAsString(), 177 | "Something went wrong with validation server."))); 178 | } 179 | 180 | } else { 181 | log.error("Unable to complete HTTP request to validation server", res.cause()); 182 | 183 | handler.handle(Future.failedFuture( 184 | new ValidationRequestResultException(ErrorType.HTTP_REQ_FAILURE, 185 | "Internal Message: Unable to complete HTTP request to validation server, cause: " + res.cause().getLocalizedMessage(), 186 | "Something went wrong while trying to contact validation server."))); 187 | } 188 | }); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/ValidationRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.github.stephenott.common.EventBusable; 5 | 6 | 7 | public class ValidationRequest implements EventBusable { 8 | 9 | @JsonProperty(required = true) 10 | private ValidationSchemaObject schema; 11 | 12 | @JsonProperty(required = true) 13 | private ValidationSubmissionObject submission; 14 | 15 | public ValidationRequest() { 16 | } 17 | 18 | public ValidationSchemaObject getSchema() { 19 | return schema; 20 | } 21 | 22 | public ValidationRequest setSchema(ValidationSchemaObject schema) { 23 | this.schema = schema; 24 | return this; 25 | } 26 | 27 | public ValidationSubmissionObject getSubmission() { 28 | return submission; 29 | } 30 | 31 | public ValidationRequest setSubmission(ValidationSubmissionObject submission) { 32 | this.submission = submission; 33 | return this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/ValidationRequestResult.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.github.stephenott.common.EventBusable; 5 | import com.github.stephenott.form.validator.exception.ValidationRequestResultException; 6 | import io.vertx.core.json.JsonObject; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class ValidationRequestResult implements EventBusable { 12 | 13 | @JsonProperty(required = true) 14 | private Result result; 15 | 16 | private ValidResult validResultObject = null; 17 | private InvalidResult invalidResultObject = null; 18 | private ErrorResult errorResult = null; 19 | 20 | public enum Result { 21 | VALID, 22 | INVALID, 23 | ERROR 24 | } 25 | 26 | public ValidationRequestResult() { 27 | } 28 | 29 | public static ValidationRequestResult GenerateValidResult(ValidResult validResultObject){ 30 | return new ValidationRequestResult() 31 | .setResult(Result.VALID) 32 | .setValidResultObject(validResultObject); 33 | } 34 | 35 | public static ValidationRequestResult GenerateInvalidResult(InvalidResult invalidResultObject){ 36 | return new ValidationRequestResult() 37 | .setResult(Result.INVALID) 38 | .setInvalidResultObject(invalidResultObject); 39 | } 40 | 41 | public static ValidationRequestResult GenerateErrorResult(ErrorResult errorResult){ 42 | return new ValidationRequestResult() 43 | .setResult(Result.ERROR) 44 | .setErrorResult(errorResult); 45 | } 46 | 47 | public Result getResult() { 48 | return result; 49 | } 50 | 51 | public ValidationRequestResult setResult(Result result) { 52 | this.result = result; 53 | return this; 54 | } 55 | 56 | public ValidResult getValidResultObject() { 57 | return validResultObject; 58 | } 59 | 60 | public ValidationRequestResult setValidResultObject(ValidResult validResultObject) { 61 | this.validResultObject = validResultObject; 62 | return this; 63 | } 64 | 65 | public InvalidResult getInvalidResultObject() { 66 | return invalidResultObject; 67 | } 68 | 69 | public ValidationRequestResult setInvalidResultObject(InvalidResult invalidResultObject) { 70 | this.invalidResultObject = invalidResultObject; 71 | return this; 72 | } 73 | 74 | public ErrorResult getErrorResult() { 75 | return errorResult; 76 | } 77 | 78 | public ValidationRequestResult setErrorResult(ErrorResult errorResult) { 79 | this.errorResult = errorResult; 80 | return this; 81 | } 82 | 83 | public static class ValidResult { 84 | 85 | @JsonProperty("processed_submission") 86 | private Map processedSubmission; 87 | 88 | public ValidResult() { 89 | } 90 | 91 | public Map getProcessedSubmission() { 92 | return processedSubmission; 93 | } 94 | 95 | public ValidResult setProcessedSubmission(Map processedSubmission) { 96 | this.processedSubmission = processedSubmission; 97 | return this; 98 | } 99 | } 100 | 101 | 102 | public static class InvalidResult{ 103 | 104 | private boolean isJoi; 105 | 106 | private String name; 107 | 108 | private List> details; 109 | 110 | @JsonProperty("_object") 111 | private Map object; 112 | 113 | @JsonProperty("_validated") 114 | private Map validated; 115 | 116 | public InvalidResult() { 117 | } 118 | 119 | public boolean isJoi() { 120 | return isJoi; 121 | } 122 | 123 | public InvalidResult setJoi(boolean joi) { 124 | isJoi = joi; 125 | return this; 126 | } 127 | 128 | public String getName() { 129 | return name; 130 | } 131 | 132 | public InvalidResult setName(String name) { 133 | this.name = name; 134 | return this; 135 | } 136 | 137 | public List> getDetails() { 138 | return details; 139 | } 140 | 141 | public InvalidResult setDetails(List> details) { 142 | this.details = details; 143 | return this; 144 | } 145 | 146 | public Map getObject() { 147 | return object; 148 | } 149 | 150 | public InvalidResult setObject(Map object) { 151 | this.object = object; 152 | return this; 153 | } 154 | 155 | public Map getValidated() { 156 | return validated; 157 | } 158 | 159 | public InvalidResult setValidated(Map validated) { 160 | this.validated = validated; 161 | return this; 162 | } 163 | } 164 | 165 | public static class ErrorResult { 166 | 167 | ValidationRequestResultException.ErrorType errorType; 168 | String internalErrorMessage; 169 | String endUserMessage; 170 | 171 | private ErrorResult() { 172 | } 173 | 174 | public ErrorResult(ValidationRequestResultException.ErrorType errorType, String internalErrorMessage, String endUserMessage) { 175 | this.errorType = errorType; 176 | this.internalErrorMessage = internalErrorMessage; 177 | this.endUserMessage = endUserMessage; 178 | } 179 | 180 | public ValidationRequestResultException.ErrorType getErrorType() { 181 | return errorType; 182 | } 183 | 184 | public ErrorResult setErrorType(ValidationRequestResultException.ErrorType errorType) { 185 | this.errorType = errorType; 186 | return this; 187 | } 188 | 189 | public String getInternalErrorMessage() { 190 | return internalErrorMessage; 191 | } 192 | 193 | public ErrorResult setInternalErrorMessage(String internalErrorMessage) { 194 | this.internalErrorMessage = internalErrorMessage; 195 | return this; 196 | } 197 | 198 | public String getEndUserMessage() { 199 | return endUserMessage; 200 | } 201 | 202 | public ErrorResult setEndUserMessage(String endUserMessage) { 203 | this.endUserMessage = endUserMessage; 204 | return this; 205 | } 206 | } 207 | } 208 | 209 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/ValidationSchemaObject.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class ValidationSchemaObject { 9 | 10 | @JsonProperty(required = true) 11 | private String display; 12 | 13 | @JsonProperty(required = true) 14 | private List> components; 15 | 16 | private Map settings; 17 | 18 | public ValidationSchemaObject() { 19 | } 20 | 21 | public String getDisplay() { 22 | return display; 23 | } 24 | 25 | public ValidationSchemaObject setDisplay(String display) { 26 | this.display = display; 27 | return this; 28 | } 29 | 30 | public List> getComponents() { 31 | return components; 32 | } 33 | 34 | public ValidationSchemaObject setComponents(List> components) { 35 | this.components = components; 36 | return this; 37 | } 38 | 39 | public Map getSettings() { 40 | return settings; 41 | } 42 | 43 | public ValidationSchemaObject setSettings(Map settings) { 44 | this.settings = settings; 45 | return this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/ValidationServiceRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator; 2 | 3 | public class ValidationServiceRequest { 4 | 5 | private ValidationSchemaObject schema; 6 | private ValidationSubmissionObject submission; 7 | 8 | public ValidationServiceRequest() { 9 | } 10 | 11 | public ValidationServiceRequest(ValidationRequest validationRequest) { 12 | this.schema = validationRequest.getSchema(); 13 | this.submission = validationRequest.getSubmission(); 14 | } 15 | 16 | 17 | public ValidationSchemaObject getSchema() { 18 | return schema; 19 | } 20 | 21 | public ValidationServiceRequest setSchema(ValidationSchemaObject schema) { 22 | this.schema = schema; 23 | return this; 24 | } 25 | 26 | public ValidationSubmissionObject getSubmission() { 27 | return submission; 28 | } 29 | 30 | public ValidationServiceRequest setSubmission(ValidationSubmissionObject submission) { 31 | this.submission = submission; 32 | return this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/ValidationSubmissionObject.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Map; 6 | 7 | public class ValidationSubmissionObject { 8 | 9 | @JsonProperty(required = true) 10 | private Map data; 11 | 12 | private Map metadata; 13 | 14 | public ValidationSubmissionObject() { 15 | } 16 | 17 | public Map getData() { 18 | return data; 19 | } 20 | 21 | public ValidationSubmissionObject setData(Map data) { 22 | this.data = data; 23 | return this; 24 | } 25 | 26 | public Map getMetadata() { 27 | return metadata; 28 | } 29 | 30 | public ValidationSubmissionObject setMetadata(Map metadata) { 31 | this.metadata = metadata; 32 | return this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/exception/InvalidFormSubmissionException.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator.exception; 2 | 3 | import com.github.stephenott.form.validator.ValidationRequestResult; 4 | 5 | public class InvalidFormSubmissionException extends IllegalArgumentException { 6 | private ValidationRequestResult.InvalidResult invalidResult; 7 | 8 | public InvalidFormSubmissionException(String s, ValidationRequestResult.InvalidResult invalidResult) { 9 | super(s); 10 | this.invalidResult = invalidResult; 11 | } 12 | 13 | public ValidationRequestResult.InvalidResult getInvalidResult() { 14 | return invalidResult; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/form/validator/exception/ValidationRequestResultException.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.form.validator.exception; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import io.vertx.core.json.JsonObject; 6 | 7 | public class ValidationRequestResultException extends RuntimeException { 8 | 9 | private ErrorType errorType; 10 | 11 | public enum ErrorType { 12 | UNEXPECTED_STATUS_CODE, 13 | HTTP_REQ_FAILURE 14 | } 15 | 16 | @JsonCreator 17 | public ValidationRequestResultException(@JsonProperty(value = "errorType", required = true) ErrorType errorType, 18 | @JsonProperty(value = "internalErrorMessage", required = true) String internalErrorMessage, 19 | @JsonProperty(value = "endUserMessage", required = true) String endUserMessage) { 20 | super(endUserMessage, new IllegalStateException(internalErrorMessage)); 21 | this.errorType = errorType; 22 | } 23 | 24 | public ErrorType getErrorType() { 25 | return errorType; 26 | } 27 | 28 | public ValidationRequestResultException setErrorType(ErrorType errorType) { 29 | this.errorType = errorType; 30 | return this; 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/managementserver/ManagementHttpVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.managementserver; 2 | 3 | import com.github.stephenott.conf.ApplicationConfiguration; 4 | import com.github.stephenott.zeebe.client.CreateInstanceConfiguration; 5 | import io.vertx.core.*; 6 | import io.vertx.core.eventbus.EventBus; 7 | import io.vertx.core.http.HttpMethod; 8 | import io.vertx.core.http.HttpServer; 9 | import io.vertx.core.http.HttpServerResponse; 10 | import io.vertx.ext.web.FileUpload; 11 | import io.vertx.ext.web.Route; 12 | import io.vertx.ext.web.Router; 13 | import io.vertx.ext.web.RoutingContext; 14 | import io.vertx.ext.web.handler.BodyHandler; 15 | import io.vertx.ext.web.handler.CorsHandler; 16 | import io.zeebe.client.ZeebeClient; 17 | import io.zeebe.client.api.ZeebeFuture; 18 | import io.zeebe.client.api.response.DeploymentEvent; 19 | import io.zeebe.client.api.response.Workflow; 20 | import io.zeebe.client.api.response.WorkflowInstanceEvent; 21 | import io.zeebe.model.bpmn.Bpmn; 22 | import io.zeebe.model.bpmn.BpmnModelInstance; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.io.File; 27 | import java.util.Arrays; 28 | import java.util.Set; 29 | 30 | public class ManagementHttpVerticle extends AbstractVerticle { 31 | 32 | private Logger log = LoggerFactory.getLogger(ManagementHttpVerticle.class); 33 | 34 | private EventBus eb; 35 | 36 | private ZeebeClient zClient; 37 | 38 | private String fileUploadPath; 39 | 40 | private ApplicationConfiguration.ManagementHttpConfiguration managementConfig; 41 | 42 | private String apiRoot; 43 | 44 | @Override 45 | public void start() throws Exception { 46 | managementConfig = config().mapTo(ApplicationConfiguration.ManagementHttpConfiguration.class); 47 | 48 | if (!managementConfig.isEnabled()){ 49 | log.error("Stopping Management HTTP Verticle because the provided management config have enables=false"); 50 | stop(); 51 | } 52 | 53 | eb = vertx.eventBus(); 54 | 55 | fileUploadPath = managementConfig.getFileUploadPath(); 56 | apiRoot = managementConfig.getApiRoot(); 57 | 58 | zClient = createZeebeClient(); 59 | 60 | Router mainRouter = Router.router(vertx); 61 | Router apiRootRouter = Router.router(vertx); 62 | 63 | mainRouter.mountSubRouter("/" + apiRoot, apiRootRouter); 64 | 65 | HttpServer server = vertx.createHttpServer(); 66 | 67 | //Generic Failure Handler 68 | //@TODO setup proper Exceptions with json responses 69 | //@TODO setup faillback failure handlers (such as Resource not found) 70 | //@TODO move to method 71 | mainRouter.route().failureHandler(failure -> { 72 | 73 | int statusCode = failure.statusCode(); 74 | 75 | HttpServerResponse response = failure.response(); 76 | response.setStatusCode(statusCode) 77 | .end("DOG" + failure.failure().getLocalizedMessage()); 78 | }); 79 | //@TODO move to method 80 | apiRootRouter.route().failureHandler(failure -> { 81 | 82 | int statusCode = failure.statusCode(); 83 | 84 | HttpServerResponse response = failure.response(); 85 | response.setStatusCode(statusCode) 86 | .end("DOG" + failure.failure().getLocalizedMessage()); 87 | }); 88 | 89 | establishDeployRoute(apiRootRouter); 90 | establishCreateWorkflowInstanceRoute(apiRootRouter); 91 | 92 | server.requestHandler(mainRouter) 93 | .listen(managementConfig.getPort()); 94 | 95 | log.info("Server deployed at: localhost:{} under apiRoot: {}, CORS is {} ", managementConfig.getPort(), managementConfig.getApiRoot(), (managementConfig.getCorsRegex() != null) ? "enabled with: " + managementConfig.getCorsRegex() : "disabled"); 96 | 97 | } 98 | 99 | @Override 100 | public void stop() throws Exception { 101 | 102 | } 103 | 104 | private void establishDeployRoute(Router router) { 105 | Route route = router.route(HttpMethod.POST, "/deploy") 106 | .consumes("multipart/form-data") 107 | .produces("application/json"); 108 | 109 | if (managementConfig.getCorsRegex() != null) { 110 | route.handler(CorsHandler.create(managementConfig.getCorsRegex())); 111 | } 112 | 113 | route.handler(BodyHandler.create().setUploadsDirectory(fileUploadPath)); //@TODO Change this 114 | 115 | route.handler(rc -> { 116 | Set uploads = rc.fileUploads(); 117 | 118 | if (uploads.size() != 1) { 119 | rc.fail(403, new IllegalArgumentException("Must have only 1 file upload")); 120 | 121 | } else { 122 | uploads.forEach(upload -> { 123 | handleUploadForWorkflowDeployment(upload, rc); 124 | }); 125 | 126 | } 127 | }); 128 | } 129 | 130 | private void establishCreateWorkflowInstanceRoute(Router router) { 131 | Route route = router.route(HttpMethod.POST, "/create-instance") 132 | .consumes("application/json") 133 | .produces("application/json"); 134 | 135 | if (managementConfig.getCorsRegex() != null) { 136 | route.handler(CorsHandler.create(managementConfig.getCorsRegex())); 137 | } 138 | 139 | route.handler(BodyHandler.create()); 140 | 141 | route.handler(rc -> { 142 | CreateInstanceConfiguration config = rc.getBodyAsJson().mapTo(CreateInstanceConfiguration.class); 143 | 144 | // Workflow key will take precedent over bpmn version: 145 | 146 | // Create Instance based on workerflow Key 147 | 148 | //@TODO move into its own handle method: 149 | vertx.executeBlocking(code -> { 150 | if (config.getWorkflowKey() != null) { 151 | ZeebeFuture workflowInstanceEventFuture = 152 | zClient.newCreateInstanceCommand() 153 | .workflowKey(config.getWorkflowKey()) 154 | .variables(config.getVariables()) 155 | .send(); 156 | 157 | log.info("Starting workflow instance based on key: " + config.getWorkflowKey()); 158 | try { 159 | WorkflowInstanceEvent workflowInstanceEvent = workflowInstanceEventFuture.join(); 160 | log.info("Workflow({}) instance started: {}", config.getWorkflowKey(), workflowInstanceEvent.getWorkflowInstanceKey()); 161 | code.complete(workflowInstanceEvent); 162 | 163 | } catch (Exception e) { 164 | log.error("Unable to start workflow instance", e); 165 | code.fail(e); 166 | } 167 | 168 | //Create instance based on bpmnProcessId 169 | } else { 170 | ZeebeFuture workflowInstanceEventFuture = 171 | zClient.newCreateInstanceCommand() 172 | .bpmnProcessId(config.getBpmnProcessId()) 173 | .version((config.getBpmnProcessVersion() != null) ? config.getBpmnProcessVersion() : -1) 174 | .variables(config.getVariables()) 175 | .send(); 176 | 177 | String humanVersion = (config.getBpmnProcessVersion() != null) ? config.getBpmnProcessVersion().toString() : "latest"; 178 | 179 | log.info("Starting workflow instance based on bpmnProcessId: " + config.getBpmnProcessId() + "and version: " + humanVersion); 180 | 181 | try { 182 | WorkflowInstanceEvent workflowInstanceEvent = workflowInstanceEventFuture.join(); 183 | log.info("Workflow({}) instance started: {}", config.getWorkflowKey(), workflowInstanceEvent.getWorkflowInstanceKey()); 184 | code.complete(workflowInstanceEvent); 185 | 186 | } catch (Exception e) { 187 | log.error("Unable to start workflow instance", e); 188 | code.fail(e); 189 | } 190 | } 191 | 192 | }, codeResult -> { 193 | if (codeResult.succeeded()) { 194 | rc.response() 195 | .setStatusCode(200) 196 | .end("Process Instance Started: " + codeResult.result().getWorkflowInstanceKey()); 197 | 198 | } else { 199 | //Unable to start instance 200 | rc.fail(403, codeResult.cause()); 201 | } 202 | }); 203 | }); 204 | } 205 | 206 | private void handleUploadForWorkflowDeployment(FileUpload upload, RoutingContext rc) { 207 | readModelFromUpload(upload.uploadedFileName(), result -> { 208 | if (result.succeeded()) { 209 | createWorkflowDeployment(upload.name(), result.result()).setHandler(deployResult -> { 210 | if (deployResult.succeeded()) { 211 | rc.response() 212 | .setStatusCode(200) 213 | .end("Deployment Success"); 214 | 215 | } else { 216 | //Unable to deploy 217 | rc.fail(403, deployResult.cause()); 218 | } 219 | }); 220 | 221 | } else { 222 | // Could not read file: 223 | rc.fail(403, result.cause()); 224 | } 225 | }); 226 | } 227 | 228 | private void readModelFromUpload(String uploadedFileName, Handler> asyncResultHandler) { 229 | vertx.executeBlocking(code->{ 230 | try { 231 | BpmnModelInstance model = Bpmn.readModelFromFile(new File(uploadedFileName)); 232 | code.complete(model); 233 | 234 | } catch (Exception e) { 235 | code.fail(e); 236 | } 237 | }, codeResult->{ 238 | if (codeResult.succeeded()){ 239 | asyncResultHandler.handle(Future.succeededFuture(codeResult.result())); 240 | } else{ 241 | asyncResultHandler.handle(Future.failedFuture(codeResult.cause())); 242 | } 243 | }); 244 | } 245 | 246 | 247 | private Future createWorkflowDeployment(String workflowName, BpmnModelInstance modelInstance) { 248 | Promise promise = Promise.promise(); 249 | 250 | if (modelInstance == null || workflowName == null) { 251 | promise.fail("Must have at least 1 model to deploy"); 252 | 253 | } else { 254 | vertx.executeBlocking(code -> { 255 | ZeebeFuture deploymentEventFuture = zClient.newDeployCommand() 256 | .addWorkflowModel(modelInstance, workflowName) 257 | .send(); 258 | 259 | log.info("Deploying Workflow..."); 260 | 261 | try { 262 | DeploymentEvent deploymentEvent = deploymentEventFuture.join(); 263 | 264 | //@TODO rebuild this log statement 265 | log.info("Deployment Succeeded: Deployment Key:" + deploymentEvent.getKey() + " with workflows: " + Arrays.toString(deploymentEvent.getWorkflows().stream().map(Workflow::getWorkflowKey).toArray())); 266 | 267 | code.complete(deploymentEvent); 268 | 269 | } catch (Exception e) { 270 | code.fail(e); 271 | } 272 | 273 | }, codeResult -> { 274 | if (codeResult.succeeded()) { 275 | promise.complete(codeResult.result()); 276 | 277 | } else { 278 | promise.fail(codeResult.cause()); 279 | } 280 | }); 281 | } 282 | 283 | return promise.future(); 284 | } 285 | 286 | 287 | private ZeebeClient createZeebeClient() { 288 | return ZeebeClient.newClientBuilder() 289 | .brokerContactPoint(managementConfig.getZeebeClient().getBrokerContactPoint()) 290 | .defaultRequestTimeout(managementConfig.getZeebeClient().getRequestTimeout()) 291 | .usePlaintext() //@TODO remove and replace with cert /-/SECURITY/-/ 292 | .build(); 293 | } 294 | 295 | 296 | } 297 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/package-info.java: -------------------------------------------------------------------------------- 1 | @ModuleGen(groupPackage = "com.github.stephenott", name = "vertx-processor-service-proxy") 2 | package com.github.stephenott; 3 | 4 | import io.vertx.codegen.annotations.ModuleGen; -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/CompletionRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.github.stephenott.common.EventBusable; 5 | 6 | import java.util.Map; 7 | 8 | public class CompletionRequest implements EventBusable { 9 | 10 | @JsonProperty(value = "job", required = true) 11 | private long zeebeJobKey; 12 | 13 | @JsonProperty(value = "source", required = true) 14 | private String zeebeSource; 15 | 16 | @JsonProperty("variables") 17 | private Map completionVariables; 18 | 19 | private boolean bypassFormSubmission = false; 20 | 21 | private Object formSubmission; 22 | 23 | 24 | public CompletionRequest() { 25 | } 26 | 27 | public long getZeebeJobKey() { 28 | return zeebeJobKey; 29 | } 30 | 31 | public CompletionRequest setZeebeJobKey(long zeebeJobKey) { 32 | this.zeebeJobKey = zeebeJobKey; 33 | return this; 34 | } 35 | 36 | public String getZeebeSource() { 37 | return zeebeSource; 38 | } 39 | 40 | public CompletionRequest setZeebeSource(String zeebeSource) { 41 | this.zeebeSource = zeebeSource; 42 | return this; 43 | } 44 | 45 | public Map getCompletionVariables() { 46 | return completionVariables; 47 | } 48 | 49 | public CompletionRequest setCompletionVariables(Map completionVariables) { 50 | this.completionVariables = completionVariables; 51 | return this; 52 | } 53 | 54 | public boolean isBypassFormSubmission() { 55 | return bypassFormSubmission; 56 | } 57 | 58 | public CompletionRequest setBypassFormSubmission(boolean bypassFormSubmission) { 59 | this.bypassFormSubmission = bypassFormSubmission; 60 | return this; 61 | } 62 | 63 | public Object getFormSubmission() { 64 | return formSubmission; 65 | } 66 | 67 | public CompletionRequest setFormSubmission(Object formSubmission) { 68 | this.formSubmission = formSubmission; 69 | return this; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/DbActionResult.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.github.stephenott.common.EventBusable; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | public class DbActionResult implements EventBusable { 9 | 10 | private List resultObjects; 11 | 12 | private DbActionResult() { 13 | } 14 | 15 | public DbActionResult(Object resultObjects){ 16 | this.resultObjects = Collections.singletonList(resultObjects); 17 | } 18 | 19 | public DbActionResult(List resultObjects) { 20 | this.resultObjects = resultObjects; 21 | } 22 | 23 | public List getResultObjects() { 24 | return resultObjects; 25 | } 26 | 27 | public DbActionResult setResultObjects(List resultObjects) { 28 | this.resultObjects = resultObjects; 29 | return this; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/FailedDbActionException.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.github.stephenott.common.EventBusableReplyException; 6 | 7 | public class FailedDbActionException extends EventBusableReplyException { 8 | 9 | public enum FailureType { 10 | CANT_CREATE, 11 | CANT_READ, 12 | CANT_FIND, 13 | CANT_UPDATE, 14 | CANT_DELETE, 15 | CANT_CONTACT_DB, 16 | FILTER_PARSE_ERROR, 17 | CANT_COMPLETE_COMMAND 18 | } 19 | 20 | 21 | @JsonCreator 22 | public FailedDbActionException(@JsonProperty(value = "failureType", required = true) FailureType failureType, 23 | @JsonProperty(value = "internalErrorMessage", required = true) String internalErrorMessage, 24 | @JsonProperty(value = "userErrorMessage", required = true) String userErrorMessage) { 25 | super(failureType, internalErrorMessage, userErrorMessage); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/FormSchemaService.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.github.stephenott.usertask.entity.FormSchemaEntity; 4 | import io.vertx.codegen.annotations.ProxyGen; 5 | import io.vertx.core.Vertx; 6 | 7 | @ProxyGen 8 | public interface FormSchemaService { 9 | 10 | public static FormSchemaService create(Vertx vertx){ 11 | return new FormSchemaServiceImpl(); 12 | } 13 | 14 | public static FormSchemaService createProxy(Vertx vertx, String address){ 15 | return new FormSchemaServiceVertxEBProxy(vertx, address); 16 | } 17 | 18 | void saveFormSchema(FormSchemaEntity formSchemaEntity); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/FormSchemaServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | public class FormSchemaServiceImpl implements FormSchemaService { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/GetRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.github.stephenott.common.EventBusable; 5 | import com.github.stephenott.usertask.entity.UserTaskEntity; 6 | 7 | import java.time.Instant; 8 | import java.util.Optional; 9 | 10 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 11 | public class GetRequest implements EventBusable { 12 | 13 | private Optional taskId = Optional.empty(); 14 | 15 | private Optional state = Optional.empty(); 16 | 17 | private Optional title = Optional.empty(); 18 | 19 | private Optional assignee = Optional.empty(); 20 | 21 | private Optional dueDate = Optional.empty(); 22 | 23 | private Optional zeebeJobKey = Optional.empty(); 24 | 25 | private Optional zeebeSource = Optional.empty(); 26 | 27 | private Optional bpmnProcessId = Optional.empty(); 28 | 29 | public GetRequest() { 30 | } 31 | 32 | public Optional getTaskId() { 33 | return taskId; 34 | } 35 | 36 | public GetRequest setTaskId(Optional taskId) { 37 | this.taskId = taskId; 38 | return this; 39 | } 40 | 41 | public Optional getState() { 42 | return state; 43 | } 44 | 45 | public GetRequest setState(Optional state) { 46 | this.state = state; 47 | return this; 48 | } 49 | 50 | public Optional getTitle() { 51 | return title; 52 | } 53 | 54 | public GetRequest setTitle(Optional title) { 55 | this.title = title; 56 | return this; 57 | } 58 | 59 | public Optional getAssignee() { 60 | return assignee; 61 | } 62 | 63 | public GetRequest setAssignee(Optional assignee) { 64 | this.assignee = assignee; 65 | return this; 66 | } 67 | 68 | public Optional getDueDate() { 69 | return dueDate; 70 | } 71 | 72 | public GetRequest setDueDate(Optional dueDate) { 73 | this.dueDate = dueDate; 74 | return this; 75 | } 76 | 77 | public Optional getZeebeJobKey() { 78 | return zeebeJobKey; 79 | } 80 | 81 | public GetRequest setZeebeJobKey(Optional zeebeJobKey) { 82 | this.zeebeJobKey = zeebeJobKey; 83 | return this; 84 | } 85 | 86 | public Optional getZeebeSource() { 87 | return zeebeSource; 88 | } 89 | 90 | public GetRequest setZeebeSource(Optional zeebeSource) { 91 | this.zeebeSource = zeebeSource; 92 | return this; 93 | } 94 | 95 | public Optional getBpmnProcessId() { 96 | return bpmnProcessId; 97 | } 98 | 99 | public GetRequest setBpmnProcessId(Optional bpmnProcessId) { 100 | this.bpmnProcessId = bpmnProcessId; 101 | return this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/GetTasksFormSchemaReqRes.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.github.stephenott.common.EventBusable; 4 | 5 | import java.util.Map; 6 | 7 | public class GetTasksFormSchemaReqRes { 8 | 9 | public static class Request implements EventBusable { 10 | 11 | private String taskId; 12 | 13 | public Request() { 14 | } 15 | 16 | public String getTaskId() { 17 | return taskId; 18 | } 19 | 20 | public Request setTaskId(String taskId) { 21 | this.taskId = taskId; 22 | return this; 23 | } 24 | } 25 | 26 | 27 | public static class Response implements EventBusable { 28 | 29 | private String taskId; 30 | private String formKey; 31 | private Map schema; 32 | private Map defaultValues; 33 | 34 | public Response() { 35 | } 36 | 37 | public String getTaskId() { 38 | return taskId; 39 | } 40 | 41 | public Response setTaskId(String taskId) { 42 | this.taskId = taskId; 43 | return this; 44 | } 45 | 46 | public String getFormKey() { 47 | return formKey; 48 | } 49 | 50 | public Response setFormKey(String formKey) { 51 | this.formKey = formKey; 52 | return this; 53 | } 54 | 55 | public Map getSchema() { 56 | return schema; 57 | } 58 | 59 | public Response setSchema(Map schema) { 60 | this.schema = schema; 61 | return this; 62 | } 63 | 64 | public Map getDefaultValues() { 65 | return defaultValues; 66 | } 67 | 68 | public Response setDefaultValues(Map defaultValues) { 69 | this.defaultValues = defaultValues; 70 | return this; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/SubmitTaskComposeDto.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.github.stephenott.executors.JobResult; 4 | import com.github.stephenott.form.validator.ValidationRequestResult; 5 | import com.github.stephenott.usertask.entity.FormSchemaEntity; 6 | import com.github.stephenott.usertask.entity.UserTaskEntity; 7 | 8 | public class SubmitTaskComposeDto { 9 | 10 | UserTaskEntity userTaskEntity; 11 | FormSchemaEntity formSchemaEntity; 12 | ValidationRequestResult validationRequestResult; 13 | DbActionResult dbActionResult; 14 | JobResult jobResult; 15 | boolean validForm; 16 | 17 | public SubmitTaskComposeDto() { 18 | } 19 | 20 | public UserTaskEntity getUserTaskEntity() { 21 | return userTaskEntity; 22 | } 23 | 24 | public SubmitTaskComposeDto setUserTaskEntity(UserTaskEntity userTaskEntity) { 25 | this.userTaskEntity = userTaskEntity; 26 | return this; 27 | } 28 | 29 | public FormSchemaEntity getFormSchemaEntity() { 30 | return formSchemaEntity; 31 | } 32 | 33 | public SubmitTaskComposeDto setFormSchemaEntity(FormSchemaEntity formSchemaEntity) { 34 | this.formSchemaEntity = formSchemaEntity; 35 | return this; 36 | } 37 | 38 | public ValidationRequestResult getValidationRequestResult() { 39 | return validationRequestResult; 40 | } 41 | 42 | public SubmitTaskComposeDto setValidationRequestResult(ValidationRequestResult validationRequestResult) { 43 | this.validationRequestResult = validationRequestResult; 44 | return this; 45 | } 46 | 47 | public DbActionResult getDbActionResult() { 48 | return dbActionResult; 49 | } 50 | 51 | public SubmitTaskComposeDto setDbActionResult(DbActionResult dbActionResult) { 52 | this.dbActionResult = dbActionResult; 53 | return this; 54 | } 55 | 56 | public JobResult getJobResult() { 57 | return jobResult; 58 | } 59 | 60 | public SubmitTaskComposeDto setJobResult(JobResult jobResult) { 61 | this.jobResult = jobResult; 62 | return this; 63 | } 64 | 65 | public boolean isValidForm() { 66 | return validForm; 67 | } 68 | 69 | public SubmitTaskComposeDto setValidForm(boolean validForm) { 70 | this.validForm = validForm; 71 | return this; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/UserTaskActionsVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.github.stephenott.usertask.FailedDbActionException.FailureType; 4 | import com.github.stephenott.usertask.entity.FormSchemaEntity; 5 | import com.github.stephenott.usertask.entity.UserTaskEntity; 6 | import com.github.stephenott.usertask.mongo.MongoManager; 7 | import com.github.stephenott.usertask.mongo.Subscribers.SimpleSubscriber; 8 | import com.mongodb.client.model.*; 9 | import com.mongodb.client.result.UpdateResult; 10 | import com.mongodb.reactivestreams.client.MongoCollection; 11 | import io.vertx.core.AbstractVerticle; 12 | import io.vertx.core.Future; 13 | import io.vertx.core.Promise; 14 | import io.vertx.core.eventbus.EventBus; 15 | import io.vertx.core.json.JsonObject; 16 | import org.bson.Document; 17 | import org.bson.conversions.Bson; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.util.ArrayList; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | 25 | public class UserTaskActionsVerticle extends AbstractVerticle { 26 | 27 | private final Logger log = LoggerFactory.getLogger(UserTaskActionsVerticle.class); 28 | 29 | private EventBus eb; 30 | 31 | private MongoCollection tasksCollection = MongoManager.getDatabase().getCollection("tasks", UserTaskEntity.class); 32 | private MongoCollection formsCollection = MongoManager.getDatabase().getCollection("forms", FormSchemaEntity.class); 33 | 34 | @Override 35 | public void start() throws Exception { 36 | log.info("Starting UserTaskActionsVerticle"); 37 | 38 | eb = vertx.eventBus(); 39 | 40 | establishCompleteActionConsumer(); 41 | establishGetActionConsumer(); 42 | establishGetFormSchemaWithDefaultsForTaskIdConsumer(); 43 | } 44 | 45 | @Override 46 | public void stop() throws Exception { 47 | super.stop(); 48 | } 49 | 50 | private void establishCompleteActionConsumer() { 51 | String address = "ut.action.complete"; 52 | 53 | eb.consumer(address, ebHandler -> { 54 | 55 | completeTask(ebHandler.body()).setHandler(mHandler -> { 56 | 57 | if (mHandler.succeeded()) { 58 | ebHandler.reply(mHandler.result()); 59 | log.info("Document was updated with Task Completion, new doc: " + mHandler.result().toString()); 60 | 61 | } else { 62 | ebHandler.reply(new FailedDbActionException(FailureType.CANT_COMPLETE_COMMAND, "", "")); 63 | log.error("Could not complete Mongo command to Update doc to COMPLETE", mHandler.cause()); 64 | } 65 | }); 66 | 67 | }).exceptionHandler(error -> log.error("Could not read eb message", error)); 68 | } 69 | 70 | private void establishGetActionConsumer() { 71 | String address = "ut.action.get"; 72 | 73 | eb.consumer(address, ebHandler -> { 74 | 75 | getTasks(ebHandler.body()).setHandler(mHandler -> { 76 | 77 | if (mHandler.succeeded()) { 78 | log.info("Get Tasks command was completed"); 79 | ebHandler.reply(mHandler.result()); 80 | 81 | } else { 82 | log.error("Could not complete Mongo command to Get Tasks", mHandler.cause()); 83 | ebHandler.reply(new FailedDbActionException( 84 | FailureType.CANT_COMPLETE_COMMAND, 85 | "Unable to complete the mongo command: " + mHandler.cause().getMessage(), 86 | "Unable to process the request")); 87 | } 88 | }); 89 | }).exceptionHandler(error -> log.error("Could not read eb message", error)); 90 | } 91 | 92 | private void establishGetFormSchemaWithDefaultsForTaskIdConsumer() { 93 | String address = "ut.action.get-form-schema-with-defaults"; 94 | 95 | eb.consumer(address, ebHandler -> { 96 | 97 | getFormSchemaWithDefaultsForTaskId(ebHandler.body()).setHandler(mHandler -> { 98 | 99 | if (mHandler.succeeded()) { 100 | log.info("Get Form Schema With Defaults for Task ID completed"); 101 | ebHandler.reply(new DbActionResult(mHandler.result())); 102 | 103 | } else { 104 | log.error("Could not complete Get Form Schema with Defaults for Task ID", mHandler.cause()); 105 | ebHandler.reply(new FailedDbActionException(FailureType.CANT_COMPLETE_COMMAND, "", "")); 106 | 107 | } 108 | }); 109 | 110 | }).exceptionHandler(error -> log.error("Could not read eb message", error)); 111 | } 112 | 113 | private Future completeTask(CompletionRequest completionRequest) { 114 | Promise promise = Promise.promise(); 115 | 116 | Bson findQuery = Filters.and( 117 | Filters.eq("zeebeSource", completionRequest.getZeebeSource()), 118 | Filters.eq("zeebeJobKey", completionRequest.getZeebeJobKey()), 119 | Filters.ne("state", UserTaskEntity.State.COMPLETED.toString()) // Should not be able to complete a task that is already completed 120 | ); 121 | 122 | Document doc = new Document(); 123 | doc.putAll(completionRequest.getCompletionVariables()); 124 | 125 | Bson updateDoc = Updates.combine( 126 | Updates.set("state", UserTaskEntity.State.COMPLETED.toString()), 127 | Updates.set("completeVariables", doc), 128 | Updates.currentDate("completedAt") 129 | ); 130 | 131 | FindOneAndUpdateOptions options = new FindOneAndUpdateOptions() 132 | .returnDocument(ReturnDocument.AFTER); 133 | 134 | tasksCollection.findOneAndUpdate(findQuery, updateDoc, options) 135 | .subscribe(new SimpleSubscriber().singleResult(ar -> { 136 | if (ar.succeeded()) { 137 | promise.complete(ar.result()); 138 | } else { 139 | promise.fail(ar.cause()); 140 | } 141 | })); 142 | 143 | return promise.future(); 144 | } 145 | 146 | private Future> getTasks(GetRequest getRequest) { 147 | Promise> promise = Promise.promise(); 148 | 149 | List findQueryItems = new ArrayList<>(); 150 | log.info("GET REQUEST: " + getRequest.toJsonObject().toString()); 151 | 152 | getRequest.getTaskId().ifPresent(v -> findQueryItems.add(Filters.eq(v))); 153 | getRequest.getState().ifPresent(v -> findQueryItems.add(Filters.eq("state", v.toString()))); 154 | getRequest.getTitle().ifPresent(v -> findQueryItems.add(Filters.eq("title", v))); 155 | getRequest.getAssignee().ifPresent(v -> findQueryItems.add(Filters.eq("assignee", v))); 156 | getRequest.getDueDate().ifPresent(v -> findQueryItems.add(Filters.eq("dueDate", v))); 157 | getRequest.getBpmnProcessId().ifPresent(v -> findQueryItems.add(Filters.eq("bpmnProcessId", v))); 158 | getRequest.getZeebeJobKey().ifPresent(v -> findQueryItems.add(Filters.eq("zeebeJobKey", v))); 159 | getRequest.getZeebeSource().ifPresent(v -> findQueryItems.add(Filters.eq("zeebeSource", v))); 160 | 161 | Bson queryFilter = (findQueryItems.isEmpty()) ? null : Filters.and(findQueryItems); 162 | 163 | tasksCollection.find().filter(queryFilter) 164 | .subscribe(new SimpleSubscriber<>(ar -> { 165 | if (ar.succeeded()) { 166 | promise.complete(ar.result()); 167 | 168 | } else { 169 | promise.fail(ar.cause()); 170 | } 171 | })); 172 | return promise.future(); 173 | } 174 | 175 | public Future getFormSchemaWithDefaultsForTaskId(GetTasksFormSchemaReqRes.Request request) { 176 | Promise promise = Promise.promise(); 177 | 178 | promise.future().compose(task -> { 179 | Promise stepProm = Promise.promise(); 180 | 181 | tasksCollection.find().filter(Filters.eq(request.getTaskId())) 182 | .subscribe(new SimpleSubscriber().singleResult(result -> { 183 | if (result.succeeded()) { 184 | stepProm.complete(result.result().getFormKey()); 185 | 186 | } else { 187 | stepProm.fail(result.cause()); 188 | } 189 | })); 190 | return stepProm.future(); 191 | 192 | }).compose(formKey -> { 193 | Promise stepProm = Promise.promise(); 194 | 195 | formsCollection.find().filter(Filters.eq(request.getTaskId())) 196 | .subscribe(new SimpleSubscriber().singleResult(onDone -> { 197 | if (onDone.succeeded()) { 198 | stepProm.complete(onDone.result()); 199 | 200 | // } else { 201 | stepProm.fail(onDone.cause()); 202 | } 203 | })); 204 | return stepProm.future(); 205 | 206 | }).compose(formSchema -> { 207 | promise.complete( 208 | new GetTasksFormSchemaReqRes.Response() 209 | .setDefaultValues(new HashMap<>()) 210 | .setFormKey(formSchema.getKey()) 211 | .setSchema(new JsonObject(formSchema.getSchema()).getMap()) 212 | .setTaskId(request.getTaskId()) 213 | ); 214 | return Future.succeededFuture(); 215 | }); 216 | 217 | return promise.future(); 218 | } 219 | 220 | public Future saveFormSchema(FormSchemaEntity formSchemaEntity){ 221 | Promise promise = Promise.promise(); 222 | 223 | ReplaceOptions options = new ReplaceOptions().upsert(true); 224 | 225 | Bson filter = Filters.eq("key", formSchemaEntity.getKey()); 226 | 227 | formsCollection.replaceOne(filter, formSchemaEntity, options) 228 | .subscribe(new SimpleSubscriber().singleResult(handler -> { 229 | if (handler.succeeded()) { 230 | promise.complete(new DbActionResult(handler.result())); 231 | } else { 232 | promise.fail(new FailedDbActionException(FailureType.CANT_CREATE, "","")); 233 | } 234 | })); 235 | 236 | return promise.future(); 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/UserTaskHttpServerVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask; 2 | 3 | import com.github.stephenott.conf.ApplicationConfiguration; 4 | import com.github.stephenott.executors.JobResult; 5 | import com.github.stephenott.form.validator.ValidationRequest; 6 | import com.github.stephenott.form.validator.ValidationRequestResult; 7 | import com.github.stephenott.form.validator.ValidationRequestResult.Result; 8 | import com.github.stephenott.form.validator.ValidationSchemaObject; 9 | import com.github.stephenott.form.validator.ValidationSubmissionObject; 10 | import com.github.stephenott.form.validator.exception.InvalidFormSubmissionException; 11 | import com.github.stephenott.form.validator.exception.ValidationRequestResultException; 12 | import com.github.stephenott.usertask.entity.FormSchemaEntity; 13 | import com.github.stephenott.usertask.entity.UserTaskEntity; 14 | import com.github.stephenott.usertask.mongo.MongoManager; 15 | import com.github.stephenott.usertask.mongo.Subscribers.SimpleSubscriber; 16 | import com.mongodb.client.model.Filters; 17 | import com.mongodb.reactivestreams.client.MongoCollection; 18 | import io.vertx.core.AbstractVerticle; 19 | import io.vertx.core.Future; 20 | import io.vertx.core.Promise; 21 | import io.vertx.core.eventbus.EventBus; 22 | import io.vertx.core.http.HttpServer; 23 | import io.vertx.core.http.HttpServerResponse; 24 | import io.vertx.core.json.JsonArray; 25 | import io.vertx.core.json.JsonObject; 26 | import io.vertx.ext.web.Route; 27 | import io.vertx.ext.web.Router; 28 | import io.vertx.ext.web.handler.BodyHandler; 29 | import org.bson.conversions.Bson; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import java.util.HashMap; 34 | import java.util.Map; 35 | import java.util.Optional; 36 | 37 | import static com.github.stephenott.usertask.UserTaskHttpServerVerticle.HttpUtils.addCommonHeaders; 38 | 39 | public class UserTaskHttpServerVerticle extends AbstractVerticle { 40 | 41 | private Logger log = LoggerFactory.getLogger(UserTaskHttpServerVerticle.class); 42 | 43 | private EventBus eb; 44 | 45 | private ApplicationConfiguration.UserTaskHttpServerConfiguration serverConfiguration; 46 | 47 | private MongoCollection formsCollection = MongoManager.getDatabase().getCollection("forms", FormSchemaEntity.class); 48 | private MongoCollection tasksCollection = MongoManager.getDatabase().getCollection("tasks", UserTaskEntity.class); 49 | 50 | 51 | @Override 52 | public void start(Future startFuture) throws Exception { 53 | try { 54 | serverConfiguration = config().mapTo(ApplicationConfiguration.UserTaskHttpServerConfiguration.class); 55 | } catch (Exception e) { 56 | log.error("Unable to start User Task HTTP Server Verticle because config cannot be parsed", e); 57 | stop(); 58 | } 59 | 60 | int port = serverConfiguration.getPort(); 61 | 62 | log.info("Starting UserTaskHttpServerVerticle on port: {}", port); 63 | 64 | eb = vertx.eventBus(); 65 | 66 | Router mainRouter = Router.router(vertx); 67 | 68 | HttpServer server = vertx.createHttpServer(); 69 | 70 | mainRouter.route().failureHandler(rc -> { 71 | addCommonHeaders(rc.response()); 72 | log.info("PROCESSING ERROR: " + rc.failure().getClass().getCanonicalName()); 73 | rc.response().setStatusCode(rc.statusCode()); 74 | rc.response().end(new JsonObject().put("error", rc.failure().getMessage()).toBuffer()); 75 | }); 76 | 77 | mainRouter.errorHandler(500, rc -> { 78 | log.error("HTTP FAILURE!!!", rc.failure()); 79 | rc.fail(500); 80 | }); 81 | 82 | establishCompleteActionRoute(mainRouter); 83 | establishGetTasksRoute(mainRouter); 84 | establishSubmitTaskRoute(mainRouter); 85 | establishSaveFormSchemaRoute(mainRouter); 86 | 87 | server.requestHandler(mainRouter).listen(port); 88 | } 89 | 90 | @Override 91 | public void stop() throws Exception { 92 | super.stop(); 93 | } 94 | 95 | private void establishSaveFormSchemaRoute(Router router) { 96 | //@TODO move to common 97 | String path = "/form/schema"; 98 | 99 | Route saveFormSchemaRoute = router.post(path) 100 | .handler(BodyHandler.create()); //@TODO add cors 101 | 102 | saveFormSchemaRoute.handler(rc -> { 103 | FormSchemaEntity formSchemaEntity = rc.getBodyAsJson().mapTo(FormSchemaEntity.class); 104 | log.info("SCHEMA: " + formSchemaEntity.getSchema()); 105 | 106 | }); 107 | } 108 | 109 | private void establishCompleteActionRoute(Router router) { 110 | //@TODO move to common 111 | String path = "/task/complete"; 112 | 113 | Route completeRoute = router.post(path) 114 | .handler(BodyHandler.create()); //@TODO add cors 115 | 116 | completeRoute.handler(rc -> { 117 | CompletionRequest completionRequest = rc.getBodyAsJson().mapTo(CompletionRequest.class); 118 | 119 | //@TODO move to common 120 | String address = "ut.action.complete"; 121 | 122 | eb.request(address, completionRequest, reply -> { 123 | if (reply.succeeded()) { 124 | addCommonHeaders(rc.response()); 125 | 126 | if (reply.result().body().getResultObjects().size() == 1) { 127 | rc.response().end(JsonObject.mapFrom(reply.result().body().getResultObjects().get(0)).toBuffer()); 128 | } else { 129 | log.error("No objects were returned in the resultObject of the Complete request"); 130 | throw new IllegalStateException("Something went wrong"); 131 | } 132 | 133 | } else { 134 | throw new IllegalStateException(reply.cause()); 135 | } 136 | }); 137 | 138 | }); 139 | } 140 | 141 | private void establishGetTasksRoute(Router router) { 142 | //@TODO move to common 143 | String address = "ut.action.get"; 144 | //@TODO move to common 145 | String path = "/task"; 146 | 147 | Route getRoute = router.get(path) 148 | .handler(BodyHandler.create()); //@TODO add cors 149 | 150 | getRoute.handler(rc -> { 151 | GetRequest getRequest = rc.getBodyAsJson().mapTo(GetRequest.class); 152 | 153 | eb.request(address, getRequest, reply -> { 154 | if (reply.succeeded()) { 155 | addCommonHeaders(rc.response()); 156 | rc.response().end(new JsonArray(reply.result().body().getResultObjects()).toBuffer()); 157 | 158 | } else { 159 | Class eClass = reply.cause().getClass(); 160 | 161 | if (eClass.equals(FailedDbActionException.class)) { 162 | rc.fail(500, reply.cause()); 163 | } else { 164 | rc.fail(500, new IllegalStateException("Something went wrong", reply.cause())); 165 | } 166 | } 167 | }); 168 | }); 169 | } 170 | 171 | private void establishSubmitTaskRoute(Router router) { 172 | String path = "/task/id/:taskId/submit"; 173 | 174 | Route submitRoute = router.post(path) 175 | .handler(BodyHandler.create()); //@TODO add cors 176 | 177 | submitRoute.handler(rc -> { 178 | ValidationSubmissionObject submissionObject = rc.getBodyAsJson().mapTo(ValidationSubmissionObject.class); 179 | 180 | String taskId = Optional.of(rc.request().getParam("taskId")) 181 | .orElseThrow(() -> new IllegalArgumentException("Invalid task id")); 182 | 183 | log.info("TASK ID: " + taskId); 184 | 185 | SubmitTaskComposeDto dto = new SubmitTaskComposeDto(); 186 | getUserTaskByTaskId(taskId).compose(userTaskEntity -> { 187 | dto.setUserTaskEntity(userTaskEntity); 188 | return Future.succeededFuture(); 189 | 190 | }).compose(s2 -> { 191 | return getFormSchemaByFormKey(dto.getUserTaskEntity().getFormKey()); 192 | 193 | }).compose(formSchema -> { 194 | dto.setFormSchemaEntity(formSchema); 195 | return Future.succeededFuture(); 196 | 197 | }).compose(s3 -> { 198 | ValidationSchemaObject schema = new JsonObject(dto.getFormSchemaEntity().getSchema()) 199 | .mapTo(ValidationSchemaObject.class); 200 | 201 | ValidationRequest validationRequest = new ValidationRequest() 202 | .setSchema(schema) 203 | .setSubmission(submissionObject); 204 | 205 | return validateFormSchema(validationRequest); 206 | 207 | }).compose(validationResult -> { 208 | if (validationResult.getResult().equals(Result.VALID)) { 209 | dto.setValidationRequestResult(validationResult); 210 | dto.validForm = true; 211 | return Future.succeededFuture(); 212 | } else { 213 | return Future.failedFuture(new IllegalStateException("Something went wrong, should not be here")); 214 | } 215 | // else { 216 | // dto.setValidationRequestResult(validationResult); 217 | // dto.validForm = false; 218 | // return Future.failedFuture(new IllegalArgumentException("Invalid Form Submission")); 219 | // } 220 | }).compose(s4 -> { 221 | Map completionVariables = new HashMap<>(); 222 | String variableName = dto.getUserTaskEntity().getTaskId() + "_submission"; 223 | completionVariables.put(variableName, dto.getValidationRequestResult().getValidResultObject().getProcessedSubmission()); 224 | 225 | //@TODO Refactor this call bck to zeebe to have a proper response handling 226 | String zeebeSource = dto.getUserTaskEntity().getZeebeSource(); 227 | JobResult jobResult = new JobResult( 228 | dto.getUserTaskEntity().getZeebeJobKey(), 229 | JobResult.Result.COMPLETE) 230 | .setVariables(completionVariables); 231 | 232 | dto.setJobResult(jobResult); 233 | 234 | return completeZeebeJob(zeebeSource, jobResult); 235 | }).compose(s5 -> { 236 | CompletionRequest completionRequest = new CompletionRequest() 237 | .setZeebeJobKey(dto.getUserTaskEntity().getZeebeJobKey()) 238 | .setZeebeSource(dto.getUserTaskEntity().getZeebeSource()) 239 | .setCompletionVariables(dto.getJobResult().getVariables()); 240 | 241 | return completeUserTask(completionRequest); 242 | }).setHandler(result -> { 243 | if (result.succeeded()) { 244 | if (dto.validForm) { 245 | // Valid Form Submsision and Everything went well 246 | rc.response() 247 | .setStatusCode(202) 248 | .putHeader("content-type", "application/json; charset=utf-8") 249 | .end(JsonObject.mapFrom(dto.getValidationRequestResult().getValidResultObject()).toBuffer()); 250 | } else { 251 | 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...")); 252 | rc.fail(500, new IllegalStateException("Something went wrong. We are looking into it")); 253 | } 254 | } else { 255 | if (result.cause().getClass().equals(InvalidFormSubmissionException.class)) { 256 | // log.info("Form Submission was invalid: " + JsonObject.mapFrom(dto.getValidationRequestResult().getInvalidResultObject()).toString()); 257 | InvalidFormSubmissionException exception = (InvalidFormSubmissionException) result.cause(); 258 | rc.response() 259 | .setStatusCode(400) 260 | .putHeader("content-type", "application/json; charset=utf-8") 261 | .end(JsonObject.mapFrom(exception.getInvalidResult()).toBuffer()); 262 | 263 | } else if (result.cause().getClass().equals(ValidationRequestResultException.class)) { 264 | rc.fail(500, result.cause()); 265 | 266 | } else if (result.cause().getClass().equals(IllegalArgumentException.class)) { 267 | rc.fail(400, result.cause()); 268 | 269 | } else { 270 | log.error("Task Form Submission processing failed.", result.cause()); 271 | rc.fail(500, new IllegalStateException("Something went wrong during task submission processing. We are looking into it")); 272 | } 273 | } 274 | }); 275 | 276 | 277 | //************ 278 | //@TODO look at refactor with more fluent and compose 279 | //@TODO DO MAJOR REFACTOR TO CLEAN THIS JUNK UP: WAY TOO DEEP of a PYRAMID 280 | // getUserTaskByTaskId(taskId).setHandler(taskIdHandler -> { 281 | // if (taskIdHandler.succeeded()) { 282 | // 283 | // log.info("Found Form Key: " + taskIdHandler.result().getFormKey()); 284 | // 285 | // getFormSchemaByFormKey(taskIdHandler.result().getFormKey()).setHandler(formKeyHandler -> { 286 | // if (formKeyHandler.succeeded()) { 287 | // 288 | // String ebAddress = "forms.action.validate"; 289 | // 290 | // ValidationSchemaObject schema = new JsonObject(formKeyHandler.result().getSchema()).mapTo(ValidationSchemaObject.class); 291 | // 292 | // ValidationRequest validationRequest = new ValidationRequest() 293 | // .setSchema(schema) 294 | // .setSubmission(submissionObject); 295 | // 296 | // eb.request(ebAddress, validationRequest, ebHandler -> { 297 | // if (ebHandler.succeeded()) { 298 | // 299 | // if (ebHandler.result().body().getResult().equals(Result.VALID)) { 300 | // 301 | // Map completionVariables = new HashMap<>(); 302 | // String variableName = taskIdHandler.result().getTaskId() + "_submission"; 303 | // completionVariables.put(variableName, ebHandler.result() 304 | // .body().getValidResultObject().getProcessedSubmission()); 305 | // 306 | // CompletionRequest completionRequest = new CompletionRequest() 307 | // .setZeebeJobKey(taskIdHandler.result().getZeebeJobKey()) 308 | // .setZeebeSource(taskIdHandler.result().getZeebeSource()) 309 | // .setCompletionVariables(completionVariables); 310 | // 311 | // //@TODO Refactor this call bck to zeebe to have a proper response handling 312 | // String zeebeSource = taskIdHandler.result().getZeebeSource(); 313 | // JobResult jobResult = new JobResult( 314 | // taskIdHandler.result().getZeebeJobKey(), 315 | // JobResult.Result.COMPLETE) 316 | // .setVariables(completionVariables); 317 | // 318 | // //Complete the job in Zeebe: 319 | // //@TODO Refactor this to use the future 320 | // completeZeebeJob(zeebeSource, jobResult); 321 | // 322 | // 323 | // eb.request("ut.action.complete", completionRequest, dbCompleteHandler -> { 324 | // if (dbCompleteHandler.succeeded()) { 325 | // 326 | // 327 | // } else { 328 | // // EB Never returned a result for Completion 329 | // log.error("Never received a response from DB Completion request over EB"); 330 | // rc.response() 331 | // .setStatusCode(400) 332 | // .end(); 333 | // } 334 | // }); 335 | // 336 | // } else if (ebHandler.result().body().getResult().equals(Result.INVALID)){ 337 | // rc.response() 338 | // .setStatusCode(400) 339 | // .putHeader("content-type", "application/json; charset=utf-8") 340 | // .end(JsonObject.mapFrom(ebHandler.result().body().getInvalidResultObject()).toBuffer()); 341 | // 342 | // } else { 343 | // ValidationRequestResultException exception = new JsonObject(ebHandler.result().body().getErrorResult()).mapTo(ValidationRequestResultException.class); 344 | // rc.fail(500, exception); 345 | // } 346 | // } else { 347 | // // Did not receive message back on EB from Validation Service 348 | // throw new IllegalStateException("Did not receive a message back from the validation service"); 349 | // } 350 | // }); // End of Validation Request EB send 351 | // 352 | // } else { 353 | // // Could not find Form Schema in DB 354 | // throw new IllegalArgumentException("Unable to find Form Schema for provided Form key, or multiple keys were returned."); 355 | // } 356 | // }); // end of getFormSchemaByFormKey 357 | // 358 | // } else { 359 | // // Could not find User Task in DB for the provided TaskId 360 | // throw new IllegalArgumentException("Unable to find User Task for provided Task ID"); 361 | // } 362 | // }); 363 | }); 364 | } 365 | 366 | private Future completeUserTask(CompletionRequest completionRequest) { 367 | Promise promise = Promise.promise(); 368 | 369 | String ebAddress = "ut.action.complete"; 370 | 371 | eb.request(ebAddress, completionRequest, dbCompleteHandler -> { 372 | if (dbCompleteHandler.succeeded()) { 373 | // promise.complete(dbCompleteHandler.result().body()); 374 | promise.complete(); 375 | } else { 376 | promise.fail(new IllegalStateException("EB User Task Completion Handler failed.", dbCompleteHandler.cause())); 377 | } 378 | }); 379 | return promise.future(); 380 | } 381 | 382 | private Future validateFormSchema(ValidationRequest validationRequest) { 383 | Promise promise = Promise.promise(); 384 | 385 | String ebAddress = "forms.action.validate"; //@TODO move to common 386 | 387 | eb.request(ebAddress, validationRequest, ebHandler -> { 388 | if (ebHandler.succeeded()) { 389 | Result result = ebHandler.result().body().getResult(); 390 | 391 | if (result.equals(Result.VALID)) { 392 | promise.complete(ebHandler.result().body()); 393 | 394 | } else if (result.equals(Result.INVALID)) { 395 | promise.fail(new InvalidFormSubmissionException("Form submission was invalid", ebHandler.result().body().getInvalidResultObject())); 396 | 397 | } else { //if it was a ERROR that was returned: 398 | ValidationRequestResult.ErrorResult errorResult = ebHandler.result().body().getErrorResult(); 399 | promise.fail(new ValidationRequestResultException( 400 | errorResult.getErrorType(), 401 | errorResult.getInternalErrorMessage(), 402 | errorResult.getEndUserMessage())); 403 | } 404 | } else { 405 | promise.fail(new IllegalStateException("Eb Response Failed for Validation Request", ebHandler.cause())); 406 | } 407 | }); 408 | 409 | return promise.future(); 410 | } 411 | 412 | private Future completeZeebeJob(String zeebeSource, JobResult jobResult) { 413 | Promise promise = Promise.promise(); 414 | 415 | String ebAddress = ".job-action.completion"; 416 | 417 | // @TODO Refactor to have a proper response 418 | eb.request(zeebeSource + ebAddress, jobResult, result -> { 419 | if (result.succeeded()) { 420 | promise.complete(); 421 | } else { 422 | promise.fail(result.cause()); 423 | } 424 | }); 425 | return promise.future(); 426 | } 427 | 428 | private Future getUserTaskByTaskId(String taskId) { 429 | //@TODO look at moving this into the UserTaskActionsVerticle 430 | Promise promise = Promise.promise(); 431 | 432 | // @TODO future refactor to use a projection to only return the single Form Key field rather than the entire entity. 433 | 434 | if (taskId == null) { 435 | promise.fail(new IllegalArgumentException("taskId cannot be null")); 436 | } 437 | 438 | Bson findQuery = Filters.eq(taskId); 439 | 440 | tasksCollection.find().filter(findQuery) 441 | .subscribe(new SimpleSubscriber().singleResult(result -> { 442 | if (result.succeeded()) { 443 | promise.complete(result.result()); 444 | } else { 445 | promise.fail(new IllegalArgumentException("Unable to find requested Task ID", result.cause())); 446 | } 447 | })); 448 | return promise.future(); 449 | } 450 | 451 | private Future getFormSchemaByFormKey(String formKey) { 452 | //@TODO look at moving this its own verticle for a FormSchemaEntity Verticle 453 | Promise promise = Promise.promise(); 454 | 455 | if (formKey == null) { 456 | promise.fail(new IllegalArgumentException("formKey cannot be null")); 457 | } 458 | 459 | Bson findQuery = Filters.eq("key", formKey); 460 | 461 | formsCollection.find().filter(findQuery) 462 | .subscribe(new SimpleSubscriber().singleResult(result -> { 463 | if (result.succeeded()) { 464 | promise.complete(result.result()); 465 | 466 | } else { 467 | promise.fail(new IllegalArgumentException("Unable to find Form Schema that was configured for the requested task.", result.cause())); 468 | } 469 | })); 470 | return promise.future(); 471 | } 472 | 473 | private void establishGetTasksRoute() { 474 | //@TODO 475 | } 476 | 477 | private void establishDeleteTaskRoute() { 478 | //@TODO 479 | } 480 | 481 | private void establishClaimTaskRoute() { 482 | //@TODO 483 | } 484 | 485 | private void establishUnClaimTaskRoute() { 486 | //@TODO 487 | } 488 | 489 | private void establishAssignTaskRoute() { 490 | //@TODO 491 | } 492 | 493 | private void establishCreateCustomTaskRoute() { 494 | //@TODO 495 | // Will use a custom BPMN that allows a custom single step task to be created. 496 | // Create a config for this so the BPMN Process ID can be set in the YAML config 497 | } 498 | 499 | 500 | public static class HttpUtils { 501 | 502 | public static String applicationJson = "application/json"; 503 | 504 | public static HttpServerResponse addCommonHeaders(HttpServerResponse httpServerResponse) { 505 | httpServerResponse.headers() 506 | .add("content-type", applicationJson); 507 | 508 | return httpServerResponse; 509 | } 510 | 511 | } 512 | 513 | } 514 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/entity/FormSchemaEntity.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.annotation.JsonRawValue; 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import com.github.stephenott.usertask.json.deserializer.JsonToStringDeserializer; 7 | import io.vertx.codegen.annotations.DataObject; 8 | import io.vertx.core.json.JsonObject; 9 | 10 | import java.time.Instant; 11 | import java.util.UUID; 12 | 13 | @DataObject 14 | public class FormSchemaEntity { 15 | 16 | private String Id = UUID.randomUUID().toString(); 17 | 18 | private Instant createdAt = Instant.now(); 19 | 20 | private String owner; 21 | 22 | @JsonProperty(required = true) 23 | private String key; 24 | 25 | @JsonProperty(required = true) 26 | private String title; 27 | 28 | private String description; 29 | 30 | @JsonProperty(required = true) 31 | @JsonDeserialize(using = JsonToStringDeserializer.class) 32 | @JsonRawValue 33 | private String schema; 34 | 35 | public FormSchemaEntity() { 36 | } 37 | 38 | public FormSchemaEntity(JsonObject jsonObject){ 39 | 40 | } 41 | 42 | public String getId() { 43 | return Id; 44 | } 45 | 46 | public Instant getCreatedAt() { 47 | return createdAt; 48 | } 49 | 50 | public String getOwner() { 51 | return owner; 52 | } 53 | 54 | public FormSchemaEntity setOwner(String owner) { 55 | this.owner = owner; 56 | return this; 57 | } 58 | 59 | public String getKey() { 60 | return key; 61 | } 62 | 63 | public FormSchemaEntity setKey(String key) { 64 | this.key = key; 65 | return this; 66 | } 67 | 68 | public String getTitle() { 69 | return title; 70 | } 71 | 72 | public FormSchemaEntity setTitle(String title) { 73 | this.title = title; 74 | return this; 75 | } 76 | 77 | public String getDescription() { 78 | return description; 79 | } 80 | 81 | public FormSchemaEntity setDescription(String description) { 82 | this.description = description; 83 | return this; 84 | } 85 | 86 | public String getSchema() { 87 | return schema; 88 | } 89 | 90 | public FormSchemaEntity setSchema(String schema) { 91 | this.schema = schema; 92 | return this; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/entity/UserTaskEntity.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask.entity; 2 | 3 | import org.bson.codecs.pojo.annotations.BsonDiscriminator; 4 | import org.bson.codecs.pojo.annotations.BsonId; 5 | 6 | import java.time.Instant; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | @BsonDiscriminator 11 | public class UserTaskEntity { 12 | 13 | @BsonId 14 | private String taskId; 15 | 16 | private long olVersion = 1L; 17 | 18 | private Instant taskOriginalCapture = Instant.now(); 19 | 20 | private State state = State.NEW; 21 | 22 | private String title; 23 | private String description; 24 | private int priority = 0; 25 | private String assignee; 26 | private Set candidateGroups; 27 | private Set candidateUsers; 28 | private Instant dueDate; 29 | private String formKey; 30 | private Instant newAt = Instant.now(); 31 | private Instant assignedAt; 32 | private Instant delegatedAt; 33 | private Instant completedAt; 34 | private Map completeVariables; 35 | private String zeebeSource; 36 | private Instant zeebeDeadline; 37 | private long zeebeJobKey; 38 | private String bpmnProcessId; 39 | private int bpmnProcessVersion; 40 | private Map zeebeVariables; 41 | 42 | private Map metadata; 43 | 44 | public UserTaskEntity() { 45 | } 46 | 47 | public enum State { 48 | NEW, 49 | ASSIGNED, 50 | UNASSIGNED, 51 | DELEGATED, 52 | COMPLETED 53 | } 54 | 55 | public String getTaskId() { 56 | return taskId; 57 | } 58 | 59 | public UserTaskEntity setTaskId(String taskId) { 60 | this.taskId = taskId; 61 | return this; 62 | } 63 | 64 | /** 65 | * Gets the optimistic locking version number 66 | * @return 67 | */ 68 | public long getOlVersion() { 69 | return olVersion; 70 | } 71 | 72 | /** 73 | * Set the optimistic locking version number 74 | * @param olVersion 75 | * @return 76 | */ 77 | public UserTaskEntity setOlVersion(long olVersion) { 78 | this.olVersion = olVersion; 79 | return this; 80 | } 81 | 82 | public String getTitle() { 83 | return title; 84 | } 85 | 86 | public UserTaskEntity setTitle(String title) { 87 | this.title = title; 88 | return this; 89 | } 90 | 91 | public String getDescription() { 92 | return description; 93 | } 94 | 95 | public UserTaskEntity setDescription(String description) { 96 | this.description = description; 97 | return this; 98 | } 99 | 100 | public int getPriority() { 101 | return priority; 102 | } 103 | 104 | public UserTaskEntity setPriority(int priority) { 105 | this.priority = priority; 106 | return this; 107 | } 108 | 109 | public String getAssignee() { 110 | return assignee; 111 | } 112 | 113 | public UserTaskEntity setAssignee(String assignee) { 114 | this.assignee = assignee; 115 | return this; 116 | } 117 | 118 | public Set getCandidateGroups() { 119 | return candidateGroups; 120 | } 121 | 122 | public UserTaskEntity setCandidateGroups(Set candidateGroups) { 123 | this.candidateGroups = candidateGroups; 124 | return this; 125 | } 126 | 127 | public Set getCandidateUsers() { 128 | return candidateUsers; 129 | } 130 | 131 | public UserTaskEntity setCandidateUsers(Set candidateUsers) { 132 | this.candidateUsers = candidateUsers; 133 | return this; 134 | } 135 | 136 | public Instant getDueDate() { 137 | return dueDate; 138 | } 139 | 140 | public UserTaskEntity setDueDate(Instant dueDate) { 141 | this.dueDate = dueDate; 142 | return this; 143 | } 144 | 145 | public String getFormKey() { 146 | return formKey; 147 | } 148 | 149 | public UserTaskEntity setFormKey(String formKey) { 150 | this.formKey = formKey; 151 | return this; 152 | } 153 | 154 | public State getState() { 155 | return state; 156 | } 157 | 158 | public UserTaskEntity setState(State state) { 159 | this.state = state; 160 | return this; 161 | } 162 | 163 | public Instant getNewAt() { 164 | return newAt; 165 | } 166 | 167 | public UserTaskEntity setNewAt(Instant newAt) { 168 | this.newAt = newAt; 169 | return this; 170 | } 171 | 172 | public Instant getAssignedAt() { 173 | return assignedAt; 174 | } 175 | 176 | public UserTaskEntity setAssignedAt(Instant assignedAt) { 177 | this.assignedAt = assignedAt; 178 | return this; 179 | } 180 | 181 | public Instant getDelegatedAt() { 182 | return delegatedAt; 183 | } 184 | 185 | public UserTaskEntity setDelegatedAt(Instant delegatedAt) { 186 | this.delegatedAt = delegatedAt; 187 | return this; 188 | } 189 | 190 | public Instant getCompletedAt() { 191 | return completedAt; 192 | } 193 | 194 | public UserTaskEntity setCompletedAt(Instant completedAt) { 195 | this.completedAt = completedAt; 196 | return this; 197 | } 198 | 199 | public Map getCompleteVariables() { 200 | return completeVariables; 201 | } 202 | 203 | public UserTaskEntity setCompleteVariables(Map completeVariables) { 204 | this.completeVariables = completeVariables; 205 | return this; 206 | } 207 | 208 | public String getZeebeSource() { 209 | return zeebeSource; 210 | } 211 | 212 | public UserTaskEntity setZeebeSource(String zeebeSource) { 213 | this.zeebeSource = zeebeSource; 214 | return this; 215 | } 216 | 217 | public Instant getZeebeDeadline() { 218 | return zeebeDeadline; 219 | } 220 | 221 | public UserTaskEntity setZeebeDeadline(Instant zeebeDeadline) { 222 | this.zeebeDeadline = zeebeDeadline; 223 | return this; 224 | } 225 | 226 | public long getZeebeJobKey() { 227 | return zeebeJobKey; 228 | } 229 | 230 | public UserTaskEntity setZeebeJobKey(long zeebeJobKey) { 231 | this.zeebeJobKey = zeebeJobKey; 232 | return this; 233 | } 234 | 235 | public String getBpmnProcessId() { 236 | return bpmnProcessId; 237 | } 238 | 239 | public UserTaskEntity setBpmnProcessId(String bpmnProcessId) { 240 | this.bpmnProcessId = bpmnProcessId; 241 | return this; 242 | } 243 | 244 | public int getBpmnProcessVersion() { 245 | return bpmnProcessVersion; 246 | } 247 | 248 | public UserTaskEntity setBpmnProcessVersion(int bpmnProcessVersion) { 249 | this.bpmnProcessVersion = bpmnProcessVersion; 250 | return this; 251 | } 252 | 253 | public Map getMetadata() { 254 | return metadata; 255 | } 256 | 257 | public UserTaskEntity setMetadata(Map metadata) { 258 | this.metadata = metadata; 259 | return this; 260 | } 261 | 262 | public Map getZeebeVariables() { 263 | return zeebeVariables; 264 | } 265 | 266 | public UserTaskEntity setZeebeVariables(Map zeebeVariables) { 267 | this.zeebeVariables = zeebeVariables; 268 | return this; 269 | } 270 | 271 | public Instant getTaskOriginalCapture() { 272 | return taskOriginalCapture; 273 | } 274 | 275 | public UserTaskEntity setTaskOriginalCapture(Instant taskOriginalCapture) { 276 | this.taskOriginalCapture = taskOriginalCapture; 277 | return this; 278 | } 279 | } -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/json/deserializer/JsonToStringDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask.json.deserializer; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.DeserializationContext; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | 8 | import java.io.IOException; 9 | 10 | public class JsonToStringDeserializer extends JsonDeserializer { 11 | @Override 12 | public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { 13 | return p.getCodec().readTree(p).toString(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/mongo/MongoManager.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask.mongo; 2 | 3 | import com.mongodb.reactivestreams.client.MongoClient; 4 | import com.mongodb.reactivestreams.client.MongoDatabase; 5 | 6 | public class MongoManager { 7 | 8 | private static MongoClient client; 9 | private static String databaseName = "default"; 10 | 11 | public static MongoClient getClient() { 12 | return client; 13 | } 14 | 15 | public static void setClient(MongoClient client) { 16 | MongoManager.client = client; 17 | } 18 | 19 | public static boolean isClientSet(){ 20 | return client != null; 21 | } 22 | 23 | public static String getDatabaseName() { 24 | return databaseName; 25 | } 26 | 27 | public static void setDatabaseName(String databaseName) { 28 | MongoManager.databaseName = databaseName; 29 | } 30 | 31 | public static MongoDatabase getDatabase(){ 32 | return client.getDatabase(MongoManager.getDatabaseName()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/usertask/mongo/Subscribers.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.usertask.mongo; 2 | 3 | import com.mongodb.reactivestreams.client.Success; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.Promise; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class Subscribers { 15 | 16 | public static class ObservableSubscriber implements Subscriber { 17 | private Promise onNextPromise = Promise.promise(); 18 | private Promise onCompletePromise = Promise.promise(); 19 | private Promise onSubscribePromise = Promise.promise(); 20 | private Handler> onNextHandler; 21 | private Subscription subscription; 22 | 23 | private ObservableSubscriber() { 24 | } 25 | 26 | 27 | private ObservableSubscriber setOnSubscribeHandler(Handler> onSubscribeHandler){ 28 | this.onSubscribePromise.future().setHandler(onSubscribeHandler); 29 | return this; 30 | } 31 | 32 | private ObservableSubscriber setOnNextHandler(Handler> onNextHandler){ 33 | this.onNextHandler = onNextHandler; 34 | this.onNextPromise.future().setHandler(onNextHandler); 35 | return this; 36 | } 37 | 38 | private ObservableSubscriber setOnCompleteHandler(Handler> onCompleteHandler){ 39 | this.onCompletePromise.future().setHandler(onCompleteHandler); 40 | return this; 41 | } 42 | 43 | @Override 44 | public void onSubscribe(final Subscription s) { 45 | this.subscription = s; 46 | this.onSubscribePromise.complete(s); 47 | } 48 | 49 | @Override 50 | public void onNext(final T t) { 51 | onNextPromise.complete(t); 52 | onNextPromise = Promise.promise(); 53 | onNextPromise.future().setHandler(this.onNextHandler); 54 | } 55 | 56 | @Override 57 | public void onError(final Throwable t) { 58 | this.onCompletePromise.fail(t); 59 | } 60 | 61 | @Override 62 | public void onComplete() { 63 | this.onCompletePromise.complete(); 64 | } 65 | 66 | public Subscription getSubscription() { 67 | return subscription; 68 | } 69 | 70 | public ObservableSubscriber setCancelSubscribtionTrigger(Promise cancelTrigger, Handler> resultHandler){ 71 | setCancelSubscriptionTrigger(cancelTrigger).setHandler(resultHandler); 72 | return this; 73 | } 74 | 75 | public Future setCancelSubscriptionTrigger(Promise cancelTrigger){ 76 | Promise cancelPromise = Promise.promise(); 77 | 78 | cancelPromise.future().setHandler(ar -> { 79 | if (ar.succeeded()){ 80 | try { 81 | getSubscription().cancel(); 82 | cancelPromise.complete(); 83 | } catch (Exception e){ 84 | cancelPromise.fail(new IllegalStateException("Unable to cancel promise", e)); 85 | } 86 | } 87 | }); 88 | return cancelPromise.future(); 89 | } 90 | 91 | public Future cancelSubscription() { 92 | Promise cancelSubscriptionPromise = Promise.promise(); 93 | 94 | try { 95 | getSubscription().cancel(); 96 | cancelSubscriptionPromise.complete(); 97 | } catch (Exception e){ 98 | cancelSubscriptionPromise.fail(new IllegalStateException("Unable to cancel subscription", e)); 99 | } 100 | return cancelSubscriptionPromise.future(); 101 | } 102 | } 103 | 104 | public static class SimpleSubscriber extends ObservableSubscriber { 105 | private List received = new ArrayList<>(); 106 | private Promise> onCompleteListPromise = Promise.promise(); 107 | private long batchSize = 5; 108 | private int receivedLimit = Integer.MAX_VALUE; 109 | 110 | public SimpleSubscriber(){ 111 | super.setOnSubscribeHandler(this::onSubHandler) 112 | .setOnNextHandler(this::onNextHandler) 113 | .setOnCompleteHandler(this::onCompleteHandler); 114 | } 115 | 116 | public SimpleSubscriber(Handler>> resultHandler) { 117 | this(); 118 | onCompleteListPromise.future().setHandler(resultHandler); 119 | } 120 | 121 | public SimpleSubscriber singleResult(Handler> resultHandler){ 122 | Promise singleResultPromise = Promise.promise(); 123 | singleResultPromise.future().setHandler(resultHandler); 124 | 125 | setReceivedLimit(1); 126 | 127 | onCompleteListPromise.future().setHandler(asyncResult -> { 128 | if (asyncResult.succeeded()){ 129 | try { 130 | singleResultPromise.complete(getReceived().get(0)); 131 | } catch (Exception e){ 132 | singleResultPromise.fail(new IllegalStateException("Unable to complete single result request", e)); 133 | } 134 | } else { 135 | singleResultPromise.fail(new IllegalStateException("Async Result failed for On Complete Promise", asyncResult.cause())); 136 | } 137 | }); 138 | return this; 139 | } 140 | 141 | private void onSubHandler(AsyncResult asyncResult){ 142 | if (asyncResult.succeeded()) { 143 | getSubscription().request(getBatchSize()); 144 | } else { 145 | onCompleteListPromise.fail(new IllegalStateException("On Subscribe Handler failed.", asyncResult.cause())); 146 | } 147 | } 148 | 149 | private void onNextHandler(AsyncResult asyncResult){ 150 | if (asyncResult.succeeded()){ 151 | try { 152 | getReceived().add(asyncResult.result()); 153 | } catch (Exception e){ 154 | getSubscription().cancel(); 155 | onCompleteListPromise.fail(new IllegalStateException("On Next Handler failed because could not add more items to received list", e)); 156 | } 157 | } else { 158 | onCompleteListPromise.fail(new IllegalStateException("On Next handler failed.", asyncResult.cause())); 159 | } 160 | } 161 | 162 | private void onCompleteHandler(AsyncResult asyncResult){ 163 | if (asyncResult.succeeded()){ 164 | int max = getReceivedLimit(); 165 | 166 | if (getReceived().size() <= max){ 167 | onCompleteListPromise.complete(getReceived()); 168 | } else { 169 | onCompleteListPromise.fail(new IllegalStateException("Received " + getReceived().size() + " results, but received limit was: " + max)); 170 | } 171 | } else { 172 | onCompleteListPromise.fail(new IllegalStateException("On Complete Handler failed.", asyncResult.cause())); 173 | } 174 | } 175 | 176 | public List getReceived() { 177 | return received; 178 | } 179 | 180 | public long getBatchSize() { 181 | return batchSize; 182 | } 183 | 184 | public SimpleSubscriber setBatchSize(long batchSize) { 185 | this.batchSize = batchSize; 186 | return this; 187 | } 188 | 189 | public int getReceivedLimit() { 190 | return receivedLimit; 191 | } 192 | 193 | public SimpleSubscriber setReceivedLimit(int receivedLimit) { 194 | this.receivedLimit = receivedLimit; 195 | return this; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/zeebe/client/CreateInstanceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.zeebe.client; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class CreateInstanceConfiguration { 7 | 8 | private Long workflowKey; 9 | private String bpmnProcessId; 10 | private Integer bpmnProcessVersion; 11 | private Map variables = new HashMap<>(); 12 | 13 | public CreateInstanceConfiguration() { 14 | } 15 | 16 | public Long getWorkflowKey() { 17 | return workflowKey; 18 | } 19 | 20 | public void setWorkflowKey(Long workflowKey) { 21 | this.workflowKey = workflowKey; 22 | } 23 | 24 | public String getBpmnProcessId() { 25 | return bpmnProcessId; 26 | } 27 | 28 | public void setBpmnProcessId(String bpmnProcessId) { 29 | this.bpmnProcessId = bpmnProcessId; 30 | } 31 | 32 | public Integer getBpmnProcessVersion() { 33 | return bpmnProcessVersion; 34 | } 35 | 36 | public void setBpmnProcessVersion(Integer bpmnProcessVersion) { 37 | if (bpmnProcessVersion < 1) { 38 | throw new IllegalArgumentException("version cannot be less than 1"); 39 | } else { 40 | this.bpmnProcessVersion = bpmnProcessVersion; 41 | } 42 | } 43 | 44 | public Map getVariables() { 45 | return variables; 46 | } 47 | 48 | public void setVariables(Map variables) { 49 | this.variables = variables; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/zeebe/client/ZeebeClientConfigurationProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.zeebe.client; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import io.vertx.core.json.JsonObject; 5 | 6 | import java.time.Duration; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | 10 | public class ZeebeClientConfigurationProperties { 11 | 12 | private JsonObject clientConfig; 13 | 14 | private Broker broker; 15 | private Worker worker; 16 | private Message message; 17 | 18 | @JsonCreator 19 | public ZeebeClientConfigurationProperties(JsonObject clientConfig) { 20 | Objects.requireNonNull(clientConfig); 21 | 22 | this.clientConfig = clientConfig; 23 | 24 | JsonObject defaults = Optional.ofNullable( 25 | clientConfig.getJsonObject("default_config")) 26 | .orElseThrow(IllegalStateException::new); 27 | 28 | this.broker = Optional.ofNullable( 29 | defaults.getJsonObject("broker")) 30 | .orElseThrow(IllegalStateException::new).mapTo(Broker.class); 31 | 32 | this.worker = Optional.ofNullable( 33 | defaults.getJsonObject("worker").mapTo(Worker.class)) 34 | .orElseThrow(IllegalStateException::new); 35 | 36 | this.message = Optional.ofNullable( 37 | defaults.getJsonObject("message").mapTo(Message.class)) 38 | .orElseThrow(IllegalStateException::new); 39 | } 40 | 41 | public static class Broker { 42 | private String contactPoint; 43 | private Duration requestTimeout; 44 | 45 | public String getContactPoint() { 46 | return contactPoint; 47 | } 48 | 49 | public void setContactPoint(String contactPoint) { 50 | this.contactPoint = contactPoint; 51 | } 52 | 53 | public Duration getRequestTimeout() { 54 | return requestTimeout; 55 | } 56 | 57 | public void setRequestTimeout(Duration requestTimeout) { 58 | this.requestTimeout = requestTimeout; 59 | } 60 | } 61 | 62 | public static class Worker { 63 | private String name; 64 | private Duration timeout; 65 | private Integer maxJobsActive; 66 | private Duration pollInterval; 67 | private Integer threads; 68 | 69 | public String getName() { 70 | return name; 71 | } 72 | 73 | public void setName(String name) { 74 | this.name = name; 75 | } 76 | 77 | public Duration getTimeout() { 78 | return timeout; 79 | } 80 | 81 | public void setTimeout(Duration timeout) { 82 | this.timeout = timeout; 83 | } 84 | 85 | public Integer getMaxJobsActive() { 86 | return maxJobsActive; 87 | } 88 | 89 | public void setMaxJobsActive(Integer maxJobsActive) { 90 | this.maxJobsActive = maxJobsActive; 91 | } 92 | 93 | public Duration getPollInterval() { 94 | return pollInterval; 95 | } 96 | 97 | public void setPollInterval(Duration pollInterval) { 98 | this.pollInterval = pollInterval; 99 | } 100 | 101 | public Integer getThreads() { 102 | return threads; 103 | } 104 | 105 | public void setThreads(Integer threads) { 106 | this.threads = threads; 107 | } 108 | } 109 | 110 | public static class Message { 111 | private Duration timeToLive; 112 | 113 | public Duration getTimeToLive() { 114 | return timeToLive; 115 | } 116 | 117 | public void setTimeToLive(Duration timeToLive) { 118 | this.timeToLive = timeToLive; 119 | } 120 | } 121 | 122 | public JsonObject getClientConfig() { 123 | return clientConfig; 124 | } 125 | 126 | public void setClientConfig(JsonObject clientConfig) { 127 | this.clientConfig = clientConfig; 128 | } 129 | 130 | public Broker getBroker() { 131 | return broker; 132 | } 133 | 134 | public void setBroker(Broker broker) { 135 | this.broker = broker; 136 | } 137 | 138 | public Worker getWorker() { 139 | return worker; 140 | } 141 | 142 | public void setWorker(Worker worker) { 143 | this.worker = worker; 144 | } 145 | 146 | public Message getMessage() { 147 | return message; 148 | } 149 | 150 | public void setMessage(Message message) { 151 | this.message = message; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/zeebe/client/ZeebeClientVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.zeebe.client; 2 | 3 | import com.github.stephenott.common.Common; 4 | import com.github.stephenott.executors.JobResult; 5 | import com.github.stephenott.conf.ApplicationConfiguration; 6 | import io.vertx.circuitbreaker.CircuitBreaker; 7 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 8 | import io.vertx.core.*; 9 | import io.vertx.core.eventbus.DeliveryOptions; 10 | import io.vertx.core.eventbus.EventBus; 11 | import io.vertx.core.json.Json; 12 | import io.vertx.core.json.JsonObject; 13 | import io.zeebe.client.ZeebeClient; 14 | import io.zeebe.client.api.ZeebeFuture; 15 | import io.zeebe.client.api.command.ClientException; 16 | import io.zeebe.client.api.command.ClientStatusException; 17 | import io.zeebe.client.api.command.FinalCommandStep; 18 | import io.zeebe.client.api.response.ActivateJobsResponse; 19 | import io.zeebe.client.api.response.ActivatedJob; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.time.Duration; 24 | import java.util.List; 25 | 26 | 27 | public class ZeebeClientVerticle extends AbstractVerticle { 28 | 29 | private Logger log; 30 | private EventBus eb; 31 | 32 | private ZeebeClient zClient; 33 | 34 | private ApplicationConfiguration.ZeebeClientConfiguration clientConfiguration; 35 | 36 | private CircuitBreaker pollingBreaker; 37 | 38 | @Override 39 | public void start() throws Exception { 40 | clientConfiguration = config().mapTo(ApplicationConfiguration.ZeebeClientConfiguration.class); 41 | 42 | log = LoggerFactory.getLogger("ClientVerticle." + clientConfiguration.getName()); 43 | 44 | pollingBreaker = CircuitBreaker.create("Breaker.ClientVerticle." + clientConfiguration.getName(), vertx, 45 | new CircuitBreakerOptions() 46 | .setMaxFailures(5) 47 | .setTimeout(-1) // Timeout is currently managed by the zeebeClient usage and futures 48 | .setFailuresRollingWindow(Duration.ofHours(1).toMillis()) 49 | .setResetTimeout(Duration.ofMinutes(30).toMillis()) 50 | ); 51 | 52 | eb = vertx.eventBus(); 53 | 54 | zClient = createZeebeClient(clientConfiguration); 55 | 56 | createJobCompletionConsumer(); 57 | 58 | eb.localConsumer("createJobWorker", act -> { 59 | String jobType = act.body().getString("jobType"); 60 | String workerName = act.body().getString("workerName"); 61 | String timeout = act.body().getString("timeout"); 62 | createJobWorker(jobType, workerName, timeout); 63 | }); 64 | 65 | // Consumers are equal to "Zeebe Workers" 66 | clientConfiguration.getWorkers().forEach(worker -> { 67 | log.info("Calling for launch of Worker " + worker.getName()); 68 | log.info("------------>TIMEOUT: " + worker.getTimeout().toString()); 69 | worker.getJobTypes().forEach(jobType -> { 70 | createJobWorkerWithEb(jobType, worker.getName(), worker.getTimeout()); 71 | }); 72 | }); 73 | } 74 | 75 | 76 | private ZeebeClient createZeebeClient(ApplicationConfiguration.ZeebeClientConfiguration configuration) { 77 | log.info("Creating ZeebeClient for " + clientConfiguration.getName()); 78 | 79 | //@TODO add defaults from the App Config 80 | return ZeebeClient.newClientBuilder() 81 | .brokerContactPoint(configuration.getBrokerContactPoint()) 82 | .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) 83 | .defaultMessageTimeToLive(Duration.ofHours(1)) 84 | .usePlaintext() //@TODO remove and replace with cert /-/SECURITY/-/ 85 | .build(); 86 | } 87 | 88 | @Override 89 | public void stop() throws Exception { 90 | zClient.close(); 91 | } 92 | 93 | private void createJobWorkerWithEb(String jobType, String workerName, Duration timeout) { 94 | JsonObject body = new JsonObject() 95 | .put("jobType", jobType) 96 | .put("workerName", workerName) 97 | .put("timeout", timeout.toString()); 98 | 99 | DeliveryOptions options = new DeliveryOptions().setLocalOnly(true); 100 | eb.send("createJobWorker", body, options); 101 | } 102 | 103 | private void createJobWorker(String jobType, String workerName, String timeout) { 104 | 105 | // pollingBreaker.execute(brkCmd -> { 106 | 107 | pollForJobs(jobType, workerName, timeout, pollResult -> { 108 | 109 | if (pollResult.succeeded()) { 110 | // brkCmd.complete(); 111 | 112 | // If no results from poll: 113 | if (pollResult.result().isEmpty()) { 114 | // brkCmd.complete(); 115 | log.info(workerName + " found NO Jobs for " + jobType + ", looping..."); 116 | 117 | createJobWorkerWithEb(jobType, workerName, Duration.parse(timeout)); 118 | 119 | //If found jobs in the results of the poll: 120 | } else { 121 | log.info(workerName + " found some Jobs for " + jobType + ", count: " + pollResult.result().size()); 122 | 123 | //For Each Job that was returned 124 | pollResult.result().forEach(this::handleJob); 125 | 126 | log.info("Done handling jobs...."); 127 | 128 | createJobWorkerWithEb(jobType, workerName, Duration.parse(timeout)); //Basically a non-blocking loop 129 | } 130 | } else { 131 | log.error("POLLING ERROR: ---->", pollResult.cause()); 132 | // brkCmd.fail(pollResult.cause()); 133 | } 134 | }); // End of Poll 135 | 136 | // }); // End of Breaker 137 | } 138 | 139 | private void createJobCompletionConsumer(){ 140 | eb.consumer(clientConfiguration.getName() + ".job-action.completion").handler(msg->{ 141 | // JobResult jobResult = msg.body().mapTo(JobResult.class); 142 | 143 | if (msg.body().getResult().equals(JobResult.Result.COMPLETE)){ 144 | reportJobComplete(msg.body()).setHandler(result -> { 145 | if (result.succeeded()){ 146 | log.info("Zeebe job was successfully reported to cluster as completed"); 147 | } else { 148 | log.error("Unable to complete zeebe communication for reporting job success", result.cause()); 149 | } 150 | }); 151 | //@TODO Add return over EB to confirm that job was completed 152 | } else { 153 | reportJobFail(msg.body()).setHandler(result -> { 154 | if (result.succeeded()){ 155 | log.info("Zeebe job was successfully reported to cluster as Job Fail"); 156 | } else { 157 | log.error("Unable to complete zeebe communication for reporting job failure", result.cause()); 158 | } 159 | }); 160 | //@TODO Add return over EB to confirm that job was NOT completed 161 | } 162 | }); 163 | } 164 | 165 | private void pollForJobs(String jobType, String workerName, String timeout, Handler>> handler) { 166 | log.info("Starting Activate Jobs Command for: " + jobType + " " + workerName); 167 | 168 | //Convert to a dedicated Verticle / Thread Worker management 169 | vertx.>executeBlocking(blockProm -> { 170 | log.info(workerName + " is waiting for " + jobType + " jobs"); 171 | 172 | try { 173 | FinalCommandStep finalCommandStep = zClient.newActivateJobsCommand() 174 | .jobType(jobType) 175 | .maxJobsToActivate(1) 176 | .workerName(workerName) 177 | .timeout(Duration.parse(timeout)); 178 | 179 | ZeebeFuture jobsResponse = finalCommandStep.send(); 180 | 181 | blockProm.complete(jobsResponse.join().getJobs()); 182 | 183 | } catch (Exception e) { 184 | blockProm.fail(e); 185 | } 186 | }, false, result -> { 187 | if (result.succeeded()) { 188 | handler.handle(Future.succeededFuture(result.result())); 189 | } else { 190 | handler.handle(Future.failedFuture(result.cause())); 191 | } 192 | } 193 | 194 | ); 195 | } 196 | 197 | private void handleJob(ActivatedJob job) { 198 | log.info("Handling Job... {}", job.getKey()); 199 | 200 | DeliveryOptions options = new DeliveryOptions().setSendTimeout(1200) 201 | .addHeader("sourceClient", clientConfiguration.getName()); 202 | JsonObject object = (JsonObject) Json.decodeValue(job.toJson()); 203 | 204 | log.info("OBJECT:--> {}", object.toString()); 205 | 206 | String address = Common.JOB_ADDRESS_PREFIX + job.getType(); 207 | log.info("Sending Job work to address: {}", address); 208 | 209 | eb.send(address, object, options); 210 | } 211 | 212 | private Future reportJobComplete(JobResult jobResult) { 213 | Promise promise = Promise.promise(); 214 | 215 | log.info("Reporting job is complete... {}", jobResult.getJobKey()); 216 | 217 | vertx.executeBlocking(blkProm -> { 218 | //@TODO Add support for variables and custom timeout configs 219 | // Variables are currently not supported do to complications with the builder 220 | ZeebeFuture completeCommandFuture = zClient 221 | .newCompleteCommand(jobResult.getJobKey()) 222 | .send(); 223 | 224 | log.info("Sending Complete Command to Zeebe"); 225 | 226 | try { 227 | completeCommandFuture.join(); 228 | 229 | log.info("Complete Command was successfully sent"); 230 | 231 | blkProm.complete(); 232 | 233 | } catch (ClientStatusException e) { 234 | blkProm.fail(e); 235 | } catch (ClientException e) { 236 | blkProm.fail(e); 237 | } 238 | 239 | }, false, res -> { 240 | if (res.succeeded()) { 241 | promise.complete(); 242 | 243 | } else { 244 | promise.fail(res.cause()); 245 | 246 | } 247 | }); 248 | 249 | return promise.future(); 250 | } 251 | 252 | 253 | private Future reportJobFail(JobResult jobResult) { 254 | Promise promise = Promise.promise(); 255 | 256 | vertx.executeBlocking(blkProm -> { 257 | ZeebeFuture failCommandFuture = zClient.newFailCommand(jobResult.getJobKey()) 258 | .retries(jobResult.getRetries()) 259 | .errorMessage(jobResult.getErrorMessage()) 260 | .send(); 261 | 262 | log.info("Sending Fail-Command to Zeebe"); 263 | 264 | try { 265 | failCommandFuture.join(); 266 | 267 | log.info("Fail-Command was successfully sent"); 268 | 269 | blkProm.complete(); 270 | 271 | } catch (Exception e) { 272 | blkProm.fail(e); 273 | } 274 | 275 | }, false, res -> { 276 | if (res.succeeded()) { 277 | //ExecuteBlocking was successfully completed 278 | promise.complete(); 279 | 280 | } else { 281 | // Error in the execute blocking 282 | promise.fail(res.cause()); 283 | 284 | } 285 | }); 286 | 287 | return promise.future(); 288 | } 289 | 290 | } -------------------------------------------------------------------------------- /src/main/java/com/github/stephenott/zeebe/dto/ActivatedJobDto.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott.zeebe.dto; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import io.vertx.core.json.Json; 6 | import io.vertx.core.json.JsonObject; 7 | import io.zeebe.client.api.response.ActivatedJob; 8 | 9 | import java.io.IOException; 10 | import java.util.Map; 11 | 12 | public class ActivatedJobDto implements ActivatedJob { 13 | 14 | private long key; 15 | private String type; 16 | private Map customHeaders; 17 | private long workflowInstanceKey; 18 | private String bpmnProcessId; 19 | private int workflowDefinitionVersion; 20 | private long workflowKey; 21 | private String elementId; 22 | private long elementInstanceKey; 23 | private String worker; 24 | private int retries; 25 | private long deadline; 26 | private String variables; 27 | 28 | public ActivatedJobDto(){} 29 | 30 | public ActivatedJobDto(ActivatedJob activatedJob){ 31 | this.key = activatedJob.getKey(); 32 | this.type = activatedJob.getType(); 33 | this.customHeaders = activatedJob.getCustomHeaders(); 34 | this.workflowInstanceKey = activatedJob.getWorkflowInstanceKey(); 35 | this.bpmnProcessId = activatedJob.getBpmnProcessId(); 36 | this.workflowDefinitionVersion = activatedJob.getWorkflowDefinitionVersion(); 37 | this.workflowKey = activatedJob.getWorkflowKey(); 38 | this.elementId = activatedJob.getElementId(); 39 | this.elementInstanceKey = activatedJob.getElementInstanceKey(); 40 | this.worker = activatedJob.getWorker(); 41 | this.retries = activatedJob.getRetries(); 42 | this.deadline = activatedJob.getDeadline(); 43 | this.variables = activatedJob.getVariables(); 44 | } 45 | 46 | @Override 47 | public long getKey() { 48 | return this.key; 49 | } 50 | 51 | @Override 52 | public String getType() { 53 | return this.type; 54 | } 55 | 56 | @Override 57 | public long getWorkflowInstanceKey() { 58 | return this.workflowInstanceKey; 59 | } 60 | 61 | @Override 62 | public String getBpmnProcessId() { 63 | return this.bpmnProcessId; 64 | } 65 | 66 | @Override 67 | public int getWorkflowDefinitionVersion() { 68 | return this.workflowDefinitionVersion; 69 | } 70 | 71 | @Override 72 | public long getWorkflowKey() { 73 | return this.workflowKey; 74 | } 75 | 76 | @Override 77 | public String getElementId() { 78 | return this.elementId; 79 | } 80 | 81 | @Override 82 | public long getElementInstanceKey() { 83 | return this.elementInstanceKey; 84 | } 85 | 86 | @Override 87 | public Map getCustomHeaders() { 88 | return this.customHeaders; 89 | } 90 | 91 | @Override 92 | public String getWorker() { 93 | return this.worker; 94 | } 95 | 96 | @Override 97 | public int getRetries() { 98 | return this.retries; 99 | } 100 | 101 | @Override 102 | public long getDeadline() { 103 | return this.deadline; 104 | } 105 | 106 | @Override 107 | public String getVariables() { 108 | return this.variables; 109 | } 110 | 111 | @Override 112 | public Map getVariablesAsMap() { 113 | try { 114 | return Json.mapper.readValue(this.variables, new TypeReference>() {}); 115 | } catch (IOException e) { 116 | throw new IllegalStateException("Unable to convert variables to a Map", e); 117 | } 118 | } 119 | 120 | @Override 121 | public T getVariablesAsType(Class variableType) { 122 | try { 123 | return Json.mapper.readValue(this.variables, variableType); 124 | } catch (IOException e) { 125 | throw new IllegalStateException("Unable to convert variables to type " + variableType.getName(), e); 126 | } 127 | } 128 | 129 | @Override 130 | public String toJson() { 131 | try { 132 | return Json.mapper.writeValueAsString(this); 133 | } catch (JsonProcessingException e) { 134 | throw new IllegalStateException("Unable to convert to Json String", e); 135 | } 136 | } 137 | 138 | public JsonObject toJsonObject() { 139 | return JsonObject.mapFrom(this); 140 | } 141 | 142 | 143 | public void setKey(long key) { 144 | this.key = key; 145 | } 146 | 147 | public void setType(String type) { 148 | this.type = type; 149 | } 150 | 151 | public void setCustomHeaders(Map customHeaders) { 152 | this.customHeaders = customHeaders; 153 | } 154 | 155 | public void setWorkflowInstanceKey(long workflowInstanceKey) { 156 | this.workflowInstanceKey = workflowInstanceKey; 157 | } 158 | 159 | public void setBpmnProcessId(String bpmnProcessId) { 160 | this.bpmnProcessId = bpmnProcessId; 161 | } 162 | 163 | public void setWorkflowDefinitionVersion(int workflowDefinitionVersion) { 164 | this.workflowDefinitionVersion = workflowDefinitionVersion; 165 | } 166 | 167 | public void setWorkflowKey(long workflowKey) { 168 | this.workflowKey = workflowKey; 169 | } 170 | 171 | public void setElementId(String elementId) { 172 | this.elementId = elementId; 173 | } 174 | 175 | public void setElementInstanceKey(long elementInstanceKey) { 176 | this.elementInstanceKey = elementInstanceKey; 177 | } 178 | 179 | public void setWorker(String worker) { 180 | this.worker = worker; 181 | } 182 | 183 | public void setRetries(int retries) { 184 | this.retries = retries; 185 | } 186 | 187 | public void setDeadline(long deadline) { 188 | this.deadline = deadline; 189 | } 190 | 191 | public void setVariables(String variables) { 192 | this.variables = variables; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/test/java/com/github/stephenott/MyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.stephenott; 2 | 3 | import de.flapdoodle.embed.mongo.MongodExecutable; 4 | import de.flapdoodle.embed.mongo.MongodProcess; 5 | import de.flapdoodle.embed.mongo.MongodStarter; 6 | import de.flapdoodle.embed.mongo.config.IMongodConfig; 7 | import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; 8 | import de.flapdoodle.embed.mongo.config.Net; 9 | import de.flapdoodle.embed.mongo.distribution.Version; 10 | import de.flapdoodle.embed.process.runtime.Network; 11 | import io.zeebe.test.ZeebeTestRule; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | 15 | import java.util.concurrent.CountDownLatch; 16 | 17 | public class MyTest { 18 | 19 | @Rule public final ZeebeTestRule testRule = new ZeebeTestRule(); 20 | private MongodExecutable mongodExecutable; 21 | 22 | @Test 23 | public void test() throws Exception { 24 | 25 | MongodStarter runtime = MongodStarter.getDefaultInstance(); 26 | IMongodConfig mongodConfig = new MongodConfigBuilder().version(Version.Main.PRODUCTION) 27 | .net(new Net("localhost", 27017, Network.localhostIsIPv6())) 28 | .build(); 29 | 30 | MongodStarter starter = MongodStarter.getDefaultInstance(); 31 | mongodExecutable = starter.prepare(mongodConfig); 32 | mongodExecutable.start(); 33 | 34 | try { 35 | Thread.sleep(2000000); 36 | } catch (InterruptedException e) { 37 | e.printStackTrace(); 38 | } 39 | 40 | // client 41 | // .newDeployCommand() 42 | // .addResourceFromClasspath("process.bpmn") 43 | // .send() 44 | // .join(); 45 | // 46 | // final WorkflowInstanceEvent workflowInstance = 47 | // client 48 | // .newCreateInstanceCommand() 49 | // .bpmnProcessId("process") 50 | // .latestVersion() 51 | // .send() 52 | // .join(); 53 | } 54 | } -------------------------------------------------------------------------------- /zeebe.yml: -------------------------------------------------------------------------------- 1 | zeebe: 2 | clients: 3 | - name: MyCustomClient 4 | brokerContactPoint: "localhost:25600" 5 | requestTimeout: PT20S 6 | workers: 7 | - name: SimpleScriptWorker 8 | jobTypes: 9 | - type1 10 | timeout: PT10S 11 | - name: UT-Worker 12 | jobTypes: 13 | - ut.generic 14 | timeout: P1D 15 | 16 | executors: 17 | - name: Script-Executor 18 | address: "type1" 19 | execute: ./scripts/script1.js 20 | - name: CommonGenericExecutor 21 | address: commonExecutor 22 | execute: classpath:com.custom.executors.Executor1 23 | - name: IpBlocker 24 | address: block-ip 25 | execute: ./cyber/BlockIP.py 26 | 27 | userTaskExecutors: 28 | - name: GenericUserTask 29 | address: ut.generic 30 | 31 | managementServer: 32 | enabled: true 33 | apiRoot: server1 34 | corsRegex: ".*." 35 | port: 8080 36 | instances: 1 37 | zeebeClient: 38 | name: DeploymentClient 39 | brokerContactPoint: "localhost:25600" 40 | requestTimeout: PT10S 41 | 42 | formValidatorServer: 43 | enabled: true 44 | corsRegex: ".*." 45 | port: 8082 46 | instances: 1 47 | formValidatorService: 48 | host: localhost 49 | port: 8083 50 | validateUri: /validate 51 | requestTimeout: 5000 52 | 53 | userTaskServer: 54 | enabled: true 55 | corsRegex: ".*." 56 | port: 8088 57 | instances: 1 --------------------------------------------------------------------------------