├── .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 | 
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 | 
29 |
30 | 
31 |
32 | 
33 |
34 |
35 | And then you can render and make a submission:
36 |
37 | 
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 | 
46 |
47 |
48 | # ZeebeClient/Worker/Executor Data Flow
49 |
50 | 
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