diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 163dd227a..475b7c612 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -35,16 +35,16 @@ jobs:
run: chmod +x ./mvnw
- name: Build with Maven
- run: ./mvnw clean verify -U -B -T4
+ run: ./mvnw clean verify -U -B -T4 -ntp
- name: Prepare I-Tests
run: mkdir -p target/jgiven-reports/json
- name: Run I-Tests with Maven
- run: ./mvnw integration-test failsafe:verify -Pitest -B -T4
+ run: ./mvnw integration-test failsafe:verify -Pitest -B -T4 -ntp
- name: Deploy a new version to central
- run: ./mvnw deploy -B -DskipTests -Prelease -Dgpg.keyname="${{secrets.GPG_KEYNAME}}" -Dgpg.passphrase="${{secrets.GPG_PASSPHRASE}}"
+ run: ./mvnw deploy -B -ntp -DskipTests -Prelease -Dgpg.keyname="${{secrets.GPG_KEYNAME}}" -Dgpg.passphrase="${{secrets.GPG_PASSPHRASE}}"
env:
OSS_CENTRAL_USERNAME: "${{ secrets.SONATYPE_USERNAME }}"
OSS_CENTRAL_PASSWORD: "${{ secrets.SONATYPE_PASSWORD }}"
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index a3f9f1872..a407f99ee 100755
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip
diff --git a/README.md b/README.md
index affe8e6a0..0139a71ca 100755
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
[![stable](https://img.shields.io/badge/lifecycle-STABLE-green.svg)](https://github.com/holisticon#open-source-lifecycle)
-[![Build Status](https://github.com/holunda-io/camunda-bpm-taskpool/workflows/Development%20braches/badge.svg)](https://github.com/holunda-io/camunda-bpm-taskpool/actions)
+[![Master and Snapshot release](https://github.com/holunda-io/camunda-bpm-taskpool/actions/workflows/release.yml/badge.svg)](https://github.com/holunda-io/camunda-bpm-taskpool/actions/workflows/release.yml)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.holunda.polyflow/polyflow-taskpool-dependencies/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.holunda.polyflow/polyflow-taskpool-dependencies/)
[![Code Coverage](https://codecov.io/gh/holunda-io/camunda-bpm-taskpool/branch/master/graph/badge.svg)](https://codecov.io/gh/holunda-io/camunda-bpm-taskpool)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/6e15ca2f4ab64cfe85a736f324136086)](https://www.codacy.com/gh/holunda-io/camunda-bpm-taskpool/dashboard?utm_source=github.com&utm_medium=referral&utm_content=holunda-io/camunda-bpm-taskpool&utm_campaign=Badge_Grade)
diff --git a/bom/datapool-dependencies/pom.xml b/bom/datapool-dependencies/pom.xml
index e94755223..a1c32bd6f 100644
--- a/bom/datapool-dependencies/pom.xml
+++ b/bom/datapool-dependencies/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../parent/pom.xml
diff --git a/bom/parent/pom.xml b/bom/parent/pom.xml
index 7cd71dd3d..2de680fd5 100644
--- a/bom/parent/pom.xml
+++ b/bom/parent/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-root
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../../pom.xml
@@ -18,21 +18,21 @@
https://github.com/holunda-io/camunda-bpm-taskpool/
- 2.7.10
+ 2.7.15
7.18.0
- 4.6.3
+ 4.6.6
4.7.0
1.1.2
4.2.0
- 4.1.0
- 1.2.5
+ 5.0.0
+ 1.3.0
+ 1.3.0.0
**/*ITest.*
**/itest/**/*.*
- 1.2.5.0
@@ -296,13 +296,17 @@
clean package
+
+ src/main/kotlin
+ src/test/kotlin
+
org.jacoco
jacoco-maven-plugin
- 0.8.9
+ 0.8.10
pre-unit-test
@@ -422,45 +426,11 @@
-
-
- org.springframework.boot
- spring-boot-maven-plugin
- ${springboot.version}
-
-
-
- repackage
-
-
-
-
-
-
- commons-logging
- commons-logging
-
-
- log4j
- log4j
-
-
- org.apache.logging.log4j
- log4j-core
-
-
- org.apache.logging.log4j
- log4j-api
-
-
-
-
-
org.apache.maven.plugins
maven-dependency-plugin
- 3.5.0
+ 3.6.0
@@ -476,8 +446,8 @@
${kotlin.version}
${java.version}
- 1.5
- 1.5
+ 1.8
+ 1.8
-Xjsr305=strict
@@ -500,9 +470,6 @@
kapt
-
- ${project.basedir}/src/main/kotlin
-
org.springframework.boot
@@ -517,12 +484,6 @@
compile
-
-
- ${project.basedir}/target/generated-sources/swagger/src/main/java
- ${project.basedir}/src/main/kotlin
-
-
test-compile
@@ -552,7 +513,7 @@
maven-surefire-plugin
- 3.0.0
+ 3.1.2
false
random
@@ -563,7 +524,7 @@
- -Djava.awt.headless=true ${surefireArgLine} -XX:+StartAttachListener
+ -Djava.awt.headless=true @{surefireArgLine} -XX:+StartAttachListener
${project.build.directory}/jgiven-reports
@@ -573,7 +534,7 @@
org.apache.maven.plugins
maven-failsafe-plugin
- 3.0.0
+ 3.1.2
@@ -589,7 +550,7 @@
- -Djava.awt.headless=true ${failsafeArgLine} -XX:+StartAttachListener
+ -Djava.awt.headless=true @{failsafeArgLine} -XX:+StartAttachListener
@@ -597,7 +558,7 @@
org.jetbrains.dokka
dokka-maven-plugin
- 1.8.10
+ 1.9.0
attach-javadocs
@@ -613,7 +574,7 @@
org.codehaus.mojo
build-helper-maven-plugin
- 3.3.0
+ 3.4.0
generate-sources
@@ -633,7 +594,7 @@
org.apache.maven.plugins
maven-source-plugin
- 3.2.1
+ 3.3.0
attach-sources
diff --git a/bom/taskpool-dependencies/pom.xml b/bom/taskpool-dependencies/pom.xml
index b67baef5f..2c16aabec 100644
--- a/bom/taskpool-dependencies/pom.xml
+++ b/bom/taskpool-dependencies/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../parent/pom.xml
diff --git a/core/bus-jackson/pom.xml b/core/bus-jackson/pom.xml
index 299382971..4da605ba7 100755
--- a/core/bus-jackson/pom.xml
+++ b/core/bus-jackson/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../../bom/parent/pom.xml
diff --git a/core/datapool/datapool-api/pom.xml b/core/datapool/datapool-api/pom.xml
index 46142922d..46a6e1c55 100755
--- a/core/datapool/datapool-api/pom.xml
+++ b/core/datapool/datapool-api/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-datapool-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-datapool-api
diff --git a/core/datapool/datapool-core/pom.xml b/core/datapool/datapool-core/pom.xml
index bcec1b9a0..ebef07c91 100644
--- a/core/datapool/datapool-core/pom.xml
+++ b/core/datapool/datapool-core/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-datapool-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-datapool-core
diff --git a/core/datapool/datapool-event/pom.xml b/core/datapool/datapool-event/pom.xml
index 9314e97b2..cde70462f 100755
--- a/core/datapool/datapool-event/pom.xml
+++ b/core/datapool/datapool-event/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-datapool-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-datapool-event
diff --git a/core/datapool/pom.xml b/core/datapool/pom.xml
index caf46fdc1..a5f0dca0e 100755
--- a/core/datapool/pom.xml
+++ b/core/datapool/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../../bom/parent/pom.xml
diff --git a/core/taskpool/pom.xml b/core/taskpool/pom.xml
index 616b23b1a..43e02f17c 100755
--- a/core/taskpool/pom.xml
+++ b/core/taskpool/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../../bom/parent/pom.xml
diff --git a/core/taskpool/taskpool-api/pom.xml b/core/taskpool/taskpool-api/pom.xml
index 63b42f860..3a675fd3c 100755
--- a/core/taskpool/taskpool-api/pom.xml
+++ b/core/taskpool/taskpool-api/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-taskpool-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-taskpool-api
diff --git a/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/process/variable/ProcessVariableValue.kt b/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/process/variable/ProcessVariableValue.kt
index b26ac814f..cd43b2f2b 100644
--- a/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/process/variable/ProcessVariableValue.kt
+++ b/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/process/variable/ProcessVariableValue.kt
@@ -8,7 +8,7 @@ import org.camunda.bpm.engine.variable.value.TypedValue
*/
interface ProcessVariableValue {
val type: ProcessVariableValueType
- val value: Any
+ val value: Any?
}
/**
@@ -23,7 +23,7 @@ data class TypedValueProcessVariableValue(override val value: TypedValue)
* Implementation of the process variable value, where the value itself is represented by a 'primitive' class.
*/
data class PrimitiveProcessVariableValue(
- override val value: Any
+ override val value: Any?
) : ProcessVariableValue {
override val type: ProcessVariableValueType = ProcessVariableValueType.PRIMITIVE
}
@@ -46,7 +46,7 @@ enum class ProcessVariableValueType {
*/
TYPE_VALUE,
/**
- * Some of the types are native to serialize. These are:
+ * Some types are native to serialize. These are:
* - Numbers (Integer, Float, Double)
* - Boolean
* - String
diff --git a/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommandFilter.kt b/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommandFilter.kt
new file mode 100644
index 000000000..021952fd5
--- /dev/null
+++ b/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommandFilter.kt
@@ -0,0 +1,15 @@
+package io.holunda.camunda.taskpool.api.task
+
+import java.util.function.Predicate
+
+/**
+ * Filter for task commands.
+ */
+interface EngineTaskCommandFilter : Predicate {
+ /**
+ * Tests if the task command should be sent.
+ * @param engineTaskCommand commend to test.
+ * @return true, if command should be emitted. Defaults to false.
+ */
+ override fun test(engineTaskCommand: EngineTaskCommand): Boolean = false
+}
diff --git a/core/taskpool/taskpool-core/pom.xml b/core/taskpool/taskpool-core/pom.xml
index 7036704af..5114fb412 100755
--- a/core/taskpool/taskpool-core/pom.xml
+++ b/core/taskpool/taskpool-core/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-taskpool-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-taskpool-core
diff --git a/core/taskpool/taskpool-core/src/main/kotlin/io/holunda/polyflow/taskpool/core/task/TaskAggregate.kt b/core/taskpool/taskpool-core/src/main/kotlin/io/holunda/polyflow/taskpool/core/task/TaskAggregate.kt
index 560b20677..b7554eaa2 100755
--- a/core/taskpool/taskpool-core/src/main/kotlin/io/holunda/polyflow/taskpool/core/task/TaskAggregate.kt
+++ b/core/taskpool/taskpool-core/src/main/kotlin/io/holunda/polyflow/taskpool/core/task/TaskAggregate.kt
@@ -24,8 +24,8 @@ class TaskAggregate() {
private lateinit var id: String
internal lateinit var task: Task
- private var deleted = false
- private var completed = false
+ internal var deleted = false
+ internal var completed = false
/**
* This a non-static handler for create command.
@@ -277,5 +277,4 @@ class TaskAggregate() {
fun on(event: TaskDeletedEngineEvent) {
this.deleted = true
}
-
}
diff --git a/core/taskpool/taskpool-core/src/main/kotlin/io/holunda/polyflow/taskpool/core/task/TaskAggregateEngineTaskCommandFilter.kt b/core/taskpool/taskpool-core/src/main/kotlin/io/holunda/polyflow/taskpool/core/task/TaskAggregateEngineTaskCommandFilter.kt
new file mode 100644
index 000000000..4843aac40
--- /dev/null
+++ b/core/taskpool/taskpool-core/src/main/kotlin/io/holunda/polyflow/taskpool/core/task/TaskAggregateEngineTaskCommandFilter.kt
@@ -0,0 +1,43 @@
+package io.holunda.polyflow.taskpool.core.task
+
+import io.holunda.camunda.taskpool.api.task.CreateTaskCommand
+import io.holunda.camunda.taskpool.api.task.EngineTaskCommand
+import io.holunda.camunda.taskpool.api.task.EngineTaskCommandFilter
+import io.holunda.polyflow.taskpool.core.loadOptional
+import org.axonframework.eventsourcing.EventSourcingRepository
+import org.axonframework.messaging.GenericMessage
+import org.axonframework.messaging.unitofwork.DefaultUnitOfWork
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
+import org.springframework.stereotype.Component
+
+/**
+ * Filter checking the tasks in the event store.
+ */
+@Component
+@ConditionalOnProperty(
+ prefix = "polyflow.integration.collector.camunda.task.importer",
+ name = ["task-filter-type"],
+ havingValue = "eventstore",
+ matchIfMissing = false
+)
+class TaskAggregateEngineTaskCommandFilter(
+ private val eventSourcingRepository: EventSourcingRepository
+) : EngineTaskCommandFilter {
+
+ override fun test(engineTaskCommand: EngineTaskCommand): Boolean {
+ return when (engineTaskCommand) {
+
+ is CreateTaskCommand -> {
+
+ DefaultUnitOfWork.startAndGet(GenericMessage.asMessage(engineTaskCommand)).executeWithResult {
+ eventSourcingRepository.loadOptional(engineTaskCommand.id)
+ .map { false } // if the task exists, the CreateCommand should not be emitted
+ .orElse(true) // if the task doesn't exist, emit the CreateTaskCommand
+ }.payload
+
+ }
+
+ else -> false // reject all others
+ }
+ }
+}
diff --git a/core/taskpool/taskpool-event/pom.xml b/core/taskpool/taskpool-event/pom.xml
index 698bdf8a6..2bb6e367e 100644
--- a/core/taskpool/taskpool-event/pom.xml
+++ b/core/taskpool/taskpool-event/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-taskpool-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-taskpool-event
diff --git a/docs/developer-guide/project-setup.md b/docs/developer-guide/project-setup.md
index 5f45badfa..42e36d70c 100644
--- a/docs/developer-guide/project-setup.md
+++ b/docs/developer-guide/project-setup.md
@@ -60,7 +60,7 @@ You can generate this by executing the following build command:
### Build Documentation
We are using MkDocs for generation of a static site documentation and rely on Markdown as much as possible.
-MkDocs is a written in Python 3 and needs to be installed on your machine. For the installation please run the following
+MkDocs is written in Python 3 and needs to be installed on your machine. For the installation please run the following
command from your command line:
```bash
diff --git a/docs/reference-guide/components/camunda-taskpool-collector.md b/docs/reference-guide/components/camunda-taskpool-collector.md
index f06408894..1923afb12 100644
--- a/docs/reference-guide/components/camunda-taskpool-collector.md
+++ b/docs/reference-guide/components/camunda-taskpool-collector.md
@@ -8,14 +8,14 @@ Taskpool Collector is a component deployed as a part of the process application
(aside with Camunda BPM Engine) that is responsible for collecting information from
the Camunda BPM Engine. It detects the _intent_ of the operations executed inside the engine
and creates the corresponding commands for the Taskpool. The commands are enriched with data and transmitted to
-other taskpool components (via Command Bus).
+other Taskpool components (via Command Bus).
In the following description, we use the terms _event_ and _command_. Event denotes an entity
received from Camunda BPM Engine (from delegate event listener or from history event listener)
which is passed over to the Taskpool Collector using internal **Spring eventing** mechanism. The Taskpool
Collector converts the series of such events into a Taskpool Command - an entity carrying an intent
of change inside the Taskpool core. Please note that _event_ has another meaning in CQRS/ES systems
-and other components of the Taskpool, but in the context of Taskpool collector an event always originates from
+and other components of the Taskpool, but in the context of Taskpool Collector an event always originates from
Spring eventing.
### Features
@@ -25,6 +25,7 @@ Spring eventing.
- Collection of process variable change events
- Collection of task events and history events
- Creation of task engine commands
+- Collection of tasks assignment information
- Enrichment of task engine commands with process variables
- Attachment of correlation information to task engine commands
- Transmission of commands to Axon command bus
@@ -55,7 +56,7 @@ In order to enable collector component, include the Maven dependency to your pro
```
-Then activate the taskpool collector by providing the annotation on any Spring Configuration:
+Then activate the Taskpool Collector by providing the annotation on any Spring Configuration:
```java
@Configuration
@@ -68,7 +69,7 @@ class MyProcessApplicationConfiguration {
### Event collection
-Taskpool Collector registers Spring Event Listener to the following events, fired by Camunda Eventing Engine Plugin:
+By default, Taskpool Collector registers Spring Event Listener to the following events, fired by Camunda Eventing Engine Plugin:
* `DelegateTask` events:
** create
@@ -86,6 +87,22 @@ The events are transformed into corresponding commands and passed over to the pr
is fired using custom listeners only and polyflow components don't rely on that but rather on own implementation of built-in (unskippable) listeners.
For this purpose, it is important to disable Camunda Platform custom listeners by setting `camunda.bpm.eventing.task` property to `false`.
+During collection of task information, you can control which listeners are registered. By default, all listeners are considered but you can change
+this behaviour by setting values of two properties:
+
+```yaml
+
+polyflow:
+ integration:
+ collector:
+ camunda:
+ task:
+ excludedTaskEventNames: assignment, delete
+ excludedHistoryEventNames: add-identity-link, delete-identity-link
+```
+This particular setting is helpful, if you want to disable assignment in the engine entirely and want to provide you own custom task assignment algorithm or
+use assignment based on process variables (see below for more details).
+
### Task commands enrichment
Alongside with attributes received from the Camunda BPM engine, the engine task commands
@@ -126,7 +143,7 @@ A `VariableFilter` can be of the following type:
Here is an example, how the process variable filter can configure the enrichment:
```java
- @Configuration
+@Configuration
public class MyTaskCollectorConfiguration {
@Bean
@@ -168,6 +185,12 @@ public class MyTaskCollectorConfiguration {
If you want to implement a custom enrichment, please provide your own implementation of the interface `VariablesEnricher`
(register a Spring Component of the type) and set the property `polyflow.integration.collector.camunda.task.enricher.type` to `custom`.
+!!! warning
+ Avoid using a classic Camunda `TaskListener` which modifies process variables on task creation, since changes of those
+ listeners can't be used during task enrichment. A proper way to modify instance or task variables is to implement an ordered Spring
+ `EventListener` listening on `DelegateTask`, put it before the enricher by providing `@Order(TaskEventCollectorService.ORDER - 80)` and scope the event listener to
+ the task of your interest using condition: `@EventListener(condition = "#delegateTask.taskDefinitionKey.equals('my-task-key') && #delegateTask.eventName.equals('create')")`
+
### Data Correlation
Apart from task payload attached by the enricher, the so-called _Correlation_ with data entries can
@@ -236,16 +259,67 @@ and the process reaches the task `task_approve_request`, the task will get the f
> Please note that the logger root hierarchy is `io.holunda.polyflow.taskpool.collector`
-| Message Code | Severity | Logger* | Description | Meaning |
-|------------------|----------|:----------------------|:----------------------------------------------------------------------------------------------------------------------------|:--------|
-| `COLLECTOR-001` | `INFO` | | Task commands will be collected. | |
-| `COLLECTOR-002` | `INFO` | | Task commands not be collected. | |
-| `COLLECTOR-005` | `TRACE` | `.process.definition` | Sending process definition command: $command | |
-| `COLLECTOR-006` | `TRACE` | `.process.instance` | Sending process instance command: $command | |
-| `COLLECTOR-007` | `TRACE` | `.process.variable` | Sending process variable command: $command | |
-| `COLLECTOR-008` | `TRACE` | `.task` | Sending engine task command: $command. | |
-| `ENRICHER-001` | `INFO` | | Task commands will be enriched with process variables. | |
-| `ENRICHER-002` | `INFO` | | Task commands will not be enriched. | |
-| `ENRICHER-003` | `INFO` | | Task commands will be enriched by a custom enricher. | |
-| `ENRICHER-004` | `DEBUG` | `.task.enricher` | Could not enrich variables from running execution ${command.sourceReference.executionId}, since it doesn't exist (anymore). | |
+| Message Code | Severity | Logger* | Description | Meaning |
+|------------------|----------|:----------------------|:----------------------------------------------------------------------------------------------------------------------------|:---------|
+| `COLLECTOR-001` | `INFO` | | Task commands will be collected. | |
+| `COLLECTOR-002` | `INFO` | | Task commands not be collected. | |
+| `COLLECTOR-005` | `TRACE` | `.process.definition` | Sending process definition command: $command | |
+| `COLLECTOR-006` | `TRACE` | `.process.instance` | Sending process instance command: $command | |
+| `COLLECTOR-007` | `TRACE` | `.process.variable` | Sending process variable command: $command | |
+| `COLLECTOR-008` | `TRACE` | `.task` | Sending engine task command: $command. | |
+| `ENRICHER-001` | `INFO` | | Task commands will be enriched with process variables. | |
+| `ENRICHER-002` | `INFO` | | Task commands will not be enriched. | |
+| `ENRICHER-003` | `INFO` | | Task commands will be enriched by a custom enricher. | |
+| `ENRICHER-004` | `DEBUG` | `.task.enricher` | Could not enrich variables from running execution ${command.sourceReference.executionId}, since it doesn't exist (anymore). | |
+
+### Task Assignment
+
+User task assignment is a core functionality for every process application fostering task-oriented work. By default, Taskpool Collector uses
+information from Camunda User Task and maps that one-to-one to properties of the user task commands. The task attribute
+`assignee`, `candidateUsers` and `candidateGroups` are mapped to the corresponding attributes automatically.
+
+To control the task assignment mode you can configure Taskpool Collector using application properties. The property
+`polyflow.integration.collector.camunda.task.assigner.type` has the following values:
+
+* `no`: No additional assignment takes place, the Camunda task attributes are used (default)
+* `process-variables`: Use process variables for assignment information, see below
+* `custom`: User provides own implementation implementing a bean implementing `TaskAssigner` interface.
+
+If the value is set to `process-variables`, you can set up a constant mapping defining the process variables carrying the assignment
+information. The corresponding properties are:
+
+```yaml
+polyflow:
+ integration:
+ collector:
+ camunda:
+ task:
+ assigner:
+ type: process-variables
+ assignee: my-assignee-var
+ candidateUsers: my-candidate-users-var
+ candidateGroup: my-candidate-group-var
+```
+
+### Task Importer
+
+Alongside with the event-based Task Collector based on Camunda Eventing, there exists a dedicated service which can query Camunda database for existing
+user tasks and publish the results. In order to avoid duplications in tasks, the collected tasks are filtered by a special filter. Currently, you may choose
+between the supplied `eventstore` filter or supply your own `custom` filter by providing your own implementation of a `EngineTaskCommandFilter` interface as
+a Spring Bean. If you want to use this task importer facility, you need to activate it first in your application configuration.
+
+The following property block is used for configuration:
+
+```yaml
+polyflow:
+ integration:
+ collector:
+ camunda:
+ task:
+ importer:
+ enabled: true
+ task-filter-type: eventstore
+```
+By doing so, the `TaskServiceCollectorService` Bean is made available and can be used to trigger the import. The `eventstore` filter is useful in scenarios,
+in which the [Taskpool Core](./core-taskpool) is deployed on together with Taskpool Collector as part of the Process Application.
diff --git a/docs/reference-guide/components/view-api-client.md b/docs/reference-guide/components/view-api-client.md
index 27d0569aa..af5b9c400 100644
--- a/docs/reference-guide/components/view-api-client.md
+++ b/docs/reference-guide/components/view-api-client.md
@@ -5,7 +5,7 @@ in order to query the configured views. By doing so, it defines an easy-to-use A
## Usage
-Pleas put the following component to you class path:
+Please put the following component to you class path:
```xml
@@ -22,4 +22,12 @@ The components available are:
* `io.holunda.polyflow.view.ProcessVariableQueryClient`
* `io.holunda.polyflow.view.TaskQueryClient`
+To initialize the client, you need to pass the `queryGateway` to it:
+
+```kotlin
+@Bean
+fun myTaskClient(queryGateway: QueryGateway) = TaskQueryClient(queryGateway)
+
+```
+
If you are using Kotlin, you might like the extension functions of the `QueryGateway` provided by `io.holunda.polyflow.view.QueryGatewayExt` object.
diff --git a/docs/reference-guide/components/view-api.md b/docs/reference-guide/components/view-api.md
index b5eb4f0ec..af9e7d127 100644
--- a/docs/reference-guide/components/view-api.md
+++ b/docs/reference-guide/components/view-api.md
@@ -21,18 +21,19 @@ and generic query paging and sorting.
The Task API allows to query for tasks handled by the task-pool.
-| Query Type | Description | Payload types | In-Memory | JPA | Mongo DB |
-|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|-----------|------------|----------|
-| AllTasksQuery | Retrieves a list of tasks applying additional filters | List | yes | yes | no |
-| TasksForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters | List | yes | yes | yes |
-| TasksForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters | List | yes | yes | no |
-| TaskForIdQuery | Retrieves a task by id (without any other filters) | Task or null | yes | yes | yes |
-| TasksForApplicationQuery | Retrieves all tasks by given application name (without any further filters) | List | yes | yes | yes |
-| AllTasksWithDataEntriesQuery | Retrieves a list of tasks applying additional filters and correlates result with data entries, if available | List<(Task, List) | yes | incubation | no |
-| TasksWithDataEntriesForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters and correlates result with data entries, if available | List<(Task, List) | yes | incubation | no |
-| TasksWithDataEntriesForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters and correlates result with data entries, if available | List<(Task, List) | yes | incubation | yes |
-| TaskWithDataEntriesForIdQuery | Retrieves a task by id and correlates result with data entries, if available | (Task, List) or null | yes | yes | yes |
-| TaskCountByApplicationQuery | Counts tasks grouped by application names, useful for monitoring | List<(ApplicationName, Count)> | yes | no | yes |
+| Query Type | Description | Payload types | In-Memory | JPA | Mongo DB |
+|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|-----------|------------|----------|
+| AllTasksQuery | Retrieves a list of tasks applying additional filters | List | yes | yes | no |
+| TasksForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters | List | yes | yes | yes |
+| TasksForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters | List | yes | yes | no |
+| TasksForCandidateUserAndGroupQuery | Retrieves a list of tasks accessible by the user because listed as candidate and the user's group and applying additional filters | List | yes | yes | no |
+| TaskForIdQuery | Retrieves a task by id (without any other filters) | Task or null | yes | yes | yes |
+| TasksForApplicationQuery | Retrieves all tasks by given application name (without any further filters) | List | yes | yes | yes |
+| AllTasksWithDataEntriesQuery | Retrieves a list of tasks applying additional filters and correlates result with data entries, if available | List<(Task, List) | yes | incubation | no |
+| TasksWithDataEntriesForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters and correlates result with data entries, if available | List<(Task, List) | yes | incubation | no |
+| TasksWithDataEntriesForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters and correlates result with data entries, if available | List<(Task, List) | yes | incubation | yes |
+| TaskWithDataEntriesForIdQuery | Retrieves a task by id and correlates result with data entries, if available | (Task, List) or null | yes | yes | yes |
+| TaskCountByApplicationQuery | Counts tasks grouped by application names, useful for monitoring | List<(ApplicationName, Count)> | yes | no | yes |
### Process Definition API
@@ -90,7 +91,7 @@ for more details. Please note, that not all implementations are implementing thi
## Filtering, Paging and Sorting
-Task API and Data Entries API supports filtering, paging and sorting in queries resulting in multiple results. For Task API these are `AllTasksQuery`, `TasksForGroupQuery`, `TasksForUserQuery`,
+Task API and Data Entries API supports filtering, paging and sorting in queries resulting in multiple results. For Task API these are `AllTasksQuery`, `TasksForGroupQuery`, `TasksForUserQuery`, `TasksForCandidateUserAndGroupQuery`,
`AllTasksWithDataEntriesQuery`, `TasksWithDataEntriesForGroupQuery`, `TasksWithDataEntriesForUserQuery` and for Data Entries API these are `DataEntriesForUserQuery` and `DataEntriesQuery`.
The queries implement the `PageableSortableQuery` interface, allowing to limit the amount of results and provide an optional sorting:
@@ -104,29 +105,41 @@ interface PageableSortableQuery {
The `page` parameter denotes the page number to deliver (starting with `0`). The `size` parameter denotes the number of elements on a page. By default, the `page` is set to `0`
and the size is set to `Int.MAX`.
-An optional `sort` parameter allows to sort the results by a field attribute. The format of the `sort` string is `<+|->filedName`, `+fieldName` means sort by `fieldName` ascending,
-`-fieldName` means sort by `fieldName` descending. The field must be a direct member of the result (`Task`, `TaskWithDataEntries` or `DataEntry`) and must be one of the following type:
+An optional `sort` parameter allows to sort the results by a field attribute. The format of the `sort` string is `<+|->fieldName`, `+fieldName` means sort by `fieldName` ascending,
+`-fieldName` means sort by `fieldName` descending. The field must be a direct member of the result (`Task` for queries on `Task` and `TaskWithDataEntries` or `DataEntry`) and must be one of the following type:
* java.lang.Integer
* java.lang.String
* java.util.Date
* java.time.Instant
-To filter the results, you might supply a list of filters. A filter is an expression in format `fieldNamevalue`, where `fieldName` is addressing the attribute of the search result,
+To filter the results, you can supply a list of filters. A filter is an expression in format `fieldNamevalue`, where `fieldName` is addressing the attribute of the search result,
`` is one of `<`, `=`, `%` and `>` and `value` is a string representation of the values. The `fieldName` can point to an attribute of the result entity itself (`Task`, `TaskWithDataEntries`
-or `DataEntry`) or point to the attribute inside the payload. To avoid a possible name clash, you must prefix the field name with `task.` if you want to filter on direct attributes of a task,
-and you must prefix the field name with `data` if you want to filter on direct attributes of a dta entry. For example, `task.priority=50` would deliver tasks with priority set to 50,
+or `DataEntry`) or point to the attribute inside the payload. To avoid name clashes, you must prefix the field name with `task` if you want to filter on direct attributes of a task,
+and you must prefix the field name with `data` if you want to filter on direct attributes of a data entry. For example, `task.priority=50` would deliver tasks with priority set to 50,
and `data.entryType=info.polyflow.Order` will deliver data entries of type `info.polyflow.Order` only.
Following operations are supported:
-| Filter | Operation | In-Memory | JPA (Task Attributes) | JPA (Data Entries Attributes) | Mongo DB (Task Attributes) | Mongo DB (Data Entries Attributes) |
-|--------|--------------|--------------|-----------------------|--------------------------------------------------------------------|----------------------------|------------------------------------|
-| `<` | Less than | all, payload | none | none | all, payload | all, payload |
-| `>` | Greater than | all, payload | none | none | all, payload | all, payload |
-| `=` | Equals | all, payload | business key, payload | entry id, entry type, type, payload, processing state, user status | all, payload | all, payload |
-| `%` | Like | all, payload | name, description | none | none | none |
+| Filter | Operation | In-Memory | JPA (Task Attributes) | JPA (Data Entries Attributes) | Mongo DB (Task Attributes) | Mongo DB (Data Entries Attributes) |
+|--------|--------------|--------------|-------------------------------------------------------------------|--------------------------------------------------------------------------|----------------------------|------------------------------------|
+| `<` | Less than | all, payload | `followUpDate`, `dueDate` | none | all, payload | all, payload |
+| `>` | Greater than | all, payload | `followUpDate`, `dueDate` | none | all, payload | all, payload |
+| `=` | Equals | all, payload | payload, `businessKey`, `followUpDate`, `dueDate`, `priority` | `entryId`, `entryType`, `type`, payload, `processingState`, `userStatus` | all, payload | all, payload |
+| `%` | Like | all, payload | `businessKey`, `name`, `description`, `processName`, `textSearch` | none | none | none |
+!!! info
+ There are several special reserved filters which can be passed to the task query: `task.processName=` checks equality of the process name, `task.processName%` makes a like-search on
+ process name, `task.textSearch%some-substring` makes a special OR-combined like-search on task name, task description and task process name.
-If the field name has no prefix of above, it is considered as an attribute inside the payload of data entry or enriched variables of a user task. For example, imagine
-you have a data entry with payload attributes `{ "attribute": "value", "another": 45 }`. In order to search for those, just specify `attribute=value` in your filter criteria.
+If the field name does not have one of the above prefixes, it is considered as an attribute inside the payload of data entry or enriched variables of a user task. For example, imagine
+you have a data entry with payload attributes `{ "attribute": "value", "another": 45 }`. In order to search for those, just specify `attribute=value` in your filter criteria.
+
+Filters are composed with logical AND, meaning that all given filters have to match in order for a task to be included in the result of the query. For example, given the filters
+`task.priority=50` and `foo=bar`, the query result would only contain tasks that have a priority of 50 **and** a payload attribute named foo with the value bar.
+
+!!! warning
+ The [JPA View](view-jpa.md) has a different implementation when applying filters. Filters that target the same attribute are OR-composed before being AND-composed
+ with filters that target other attributes. For example, given the filters `customerName=ABC`, `customerName=DEF` and `task.priority=50`, the filters for the
+ customer name would first get OR-composed before being AND-composed with the task priority filter, resulting in a filtering expression logically equivalent to
+ `(customerName=ABC OR customerName=DEF) AND task.priority=50`.
diff --git a/docs/reference-guide/components/view-jpa.md b/docs/reference-guide/components/view-jpa.md
index 5b39fdce1..1df9bf250 100644
--- a/docs/reference-guide/components/view-jpa.md
+++ b/docs/reference-guide/components/view-jpa.md
@@ -5,7 +5,7 @@ pageId: view-jpa
### Purpose
The JPA View is component responsible for creating read-projections of tasks and business data entries. It currently implements
-Datapool View API and Taskpool API and persists the projection as entities and relations in a RDBMS using JPA. It is a useful
+Datapool View API and Taskpool API and persists the projection as entities and relations in a RDBMS using JPA. It is useful
if the JPA persistence is already used in the project setup.
### Features
@@ -59,18 +59,29 @@ configuration of this indexing process by the following configuration options:
```yml
polyflow.view.jpa:
- payload-attribute-level-limit: 2
stored-items: task, data-entry, process-instance, process-definition
+ payload-attribute-level-limit: 2
data-entry-filters:
include: myProperty2.myOtherEmbeddedProperty3, myProperty2.myOtherEmbeddedProperty2
# exclude: myProperty
+ task-filters:
+ exclude: processVariableWithVeryLongText
+
```
-In the example below you see the configuration of the limit of keying depth and usage of include/exclude filters of the keys.
+In the example above you see the configuration of the limit of keying depth and usage of include/exclude filters of the keys.
In addition, the `stored-items` property is holding a set of items to be persisted to the database. The possible values of
stored items are: `task`, `data-entry`, `process-instance` and `process-definition`. By setting this property, you can disable
storage of items not required by your application and save space consumption of your database. The property defaults to `data-entry`.
+The attributes `data-entry-filters` and `task-filters` hold `include` / `exclude` lists of property paths which will be taken in
+consideration during the search index creation.
+
+!!! note
+ Please make sure you understand that the **payload enrichment** performed during collection and **indexing for search** are two different
+ operations. It is perfectly fine to have a large JSON payload attached to the task, but it makes no sense to make the entire payload searchable,
+ at lease using JPA View.
+
### Entity Scan
The JPA View utilizes Spring Data repositories and Hibernate entities inside the persistence layer. As a result, it declares a `@EntityScan`
diff --git a/integration/camunda-bpm/engine-client/pom.xml b/integration/camunda-bpm/engine-client/pom.xml
index b9e2d3fcb..136cf2848 100644
--- a/integration/camunda-bpm/engine-client/pom.xml
+++ b/integration/camunda-bpm/engine-client/pom.xml
@@ -4,7 +4,7 @@
io.holunda.polyflow
polyflow-integration-camunda-bpm-engine-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-camunda-bpm-engine-client
@@ -22,7 +22,7 @@
org.axonframework
- axon-server-conector
+ axon-server-connector
diff --git a/integration/camunda-bpm/pom.xml b/integration/camunda-bpm/pom.xml
index f2fe70199..29b0242f3 100644
--- a/integration/camunda-bpm/pom.xml
+++ b/integration/camunda-bpm/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../../bom/parent/pom.xml
@@ -14,9 +14,9 @@
pom
- 7.18.0
- 7.19.0
- 6.18.0
+ 7.19.0
+ ${camunda-bpm.version}
+ 6.19.0
diff --git a/integration/camunda-bpm/springboot-autoconfigure/pom.xml b/integration/camunda-bpm/springboot-autoconfigure/pom.xml
index fc1e82a76..91c0f4a33 100755
--- a/integration/camunda-bpm/springboot-autoconfigure/pom.xml
+++ b/integration/camunda-bpm/springboot-autoconfigure/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-integration-camunda-bpm-engine-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-camunda-bpm-springboot-autoconfigure
diff --git a/integration/camunda-bpm/springboot-starter/pom.xml b/integration/camunda-bpm/springboot-starter/pom.xml
index b8b9836e2..1aab072eb 100755
--- a/integration/camunda-bpm/springboot-starter/pom.xml
+++ b/integration/camunda-bpm/springboot-starter/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-integration-camunda-bpm-engine-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-camunda-bpm-springboot-starter
diff --git a/integration/camunda-bpm/taskpool-collector/pom.xml b/integration/camunda-bpm/taskpool-collector/pom.xml
index e1adacc01..781c4afa6 100755
--- a/integration/camunda-bpm/taskpool-collector/pom.xml
+++ b/integration/camunda-bpm/taskpool-collector/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-integration-camunda-bpm-engine-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-camunda-bpm-taskpool-collector
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/TaskEntityExtensions.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/TaskEntityExtensions.kt
index 3aaeedc6a..b271bb3c0 100644
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/TaskEntityExtensions.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/TaskEntityExtensions.kt
@@ -1,8 +1,11 @@
package io.holunda.polyflow.taskpool
+import io.holunda.camunda.taskpool.api.task.CreateTaskCommand
import org.apache.commons.lang3.reflect.FieldUtils
+import org.camunda.bpm.engine.delegate.TaskListener
import org.camunda.bpm.engine.impl.persistence.entity.PropertyChange
import org.camunda.bpm.engine.impl.persistence.entity.TaskEntity
+import org.camunda.bpm.engine.task.IdentityLinkType
/**
* Reads identity changes from the task entity.
@@ -22,4 +25,28 @@ fun TaskEntity.isAssigneeChange(): Boolean =
/**
* Checks if the task entity has changed properties.
*/
-fun TaskEntity.hasChangedProperties(): Boolean = this.propertyChanges.isNotEmpty()
\ No newline at end of file
+fun TaskEntity.hasChangedProperties(): Boolean = this.propertyChanges.isNotEmpty()
+
+/**
+ * Creates a new CreateTaskCommand from this task entity. The resulting command has no correlations and is not enriched.
+ * @param applicationName application name.
+ * @return create task command.
+ */
+fun TaskEntity.asCreateCommand(applicationName: String) = CreateTaskCommand(
+ id = this.id,
+ assignee = this.assignee,
+ candidateGroups = this.candidates.filter { it.groupId != null }.map { it.groupId }.toSet(),
+ candidateUsers = this.candidates.filter { it.userId != null && it.type == IdentityLinkType.CANDIDATE }.map { it.userId }.toSet(),
+ createTime = this.createTime,
+ description = this.description,
+ dueDate = this.dueDate,
+ followUpDate = this.followUpDate,
+ eventName = TaskListener.EVENTNAME_CREATE,
+ name = this.name,
+ owner = this.owner,
+ priority = this.priority,
+ formKey = this.formKey(),
+ taskDefinitionKey = this.taskDefinitionKey,
+ businessKey = this.execution.businessKey,
+ sourceReference = this.sourceReference(applicationName)
+ )
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/CamundaTaskpoolCollectorProperties.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/CamundaTaskpoolCollectorProperties.kt
index 078a06b74..c0a696ce7 100755
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/CamundaTaskpoolCollectorProperties.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/CamundaTaskpoolCollectorProperties.kt
@@ -1,5 +1,7 @@
package io.holunda.polyflow.taskpool.collector
+import io.holunda.polyflow.taskpool.collector.task.assigner.ProcessVariableTaskAssignerMapping
+import org.camunda.bpm.engine.delegate.TaskListener
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
@@ -52,11 +54,49 @@ data class CamundaTaskCollectorProperties(
*/
@NestedConfigurationProperty
val enricher: TaskCollectorEnricherProperties = TaskCollectorEnricherProperties(),
+
+ /**
+ * Task assigner properties.
+ */
+ @NestedConfigurationProperty
+ val assigner: TaskAssignerProperties = TaskAssignerProperties(),
+
/**
* Flag to enable or disable the collector.
*/
- val enabled: Boolean = true
-)
+ val enabled: Boolean = true,
+
+ /**
+ * Properties of task importer.
+ */
+ @NestedConfigurationProperty
+ val importer: TaskImporterProperties = TaskImporterProperties(),
+
+ /**
+ * List of task events to be excluded from collector. Defaults to empty list, so all events are collected.
+ * Possible values are constants defined in [TaskListener].
+ */
+ val excludedTaskEventNames: List = listOf(),
+
+ /**
+ * List of history events to restrict (HistoricTaskInstanceEventEntity, HistoricIdentityLinkLogEventEntity). Defaults to empty list, so all events are collected.
+ * Possible values are constants defined in [HistoryEventTypes] + "update".
+ */
+ val excludedHistoryEventNames: List = listOf()
+) {
+ /**
+ * Determines if the provided event name should be collected.
+ * @param eventName event name to check.
+ * @return true if not excluded.
+ */
+ fun collectTaskEvent(eventName: String): Boolean = !excludedTaskEventNames.contains(eventName)
+ /**
+ * Determines if the provided event name should be collected.
+ * @param eventName event name to check.
+ * @return true if not excluded.
+ */
+ fun collectHistoryEvent(eventName: String): Boolean = !excludedHistoryEventNames.contains(eventName)
+}
/**
* Process variable properties.
@@ -100,6 +140,26 @@ enum class TaskCollectorEnricherType {
custom
}
+/**
+ * Type of task assigner.
+ */
+enum class TaskAssignerType {
+ /**
+ * Empty assigner, use information from Camunda task.
+ */
+ no,
+
+ /**
+ * Use process variables for assignment information.
+ */
+ processVariables,
+
+ /**
+ * Custom assigner.
+ */
+ custom
+}
+
/**
* Properties controlling the transfer of process definitions deployments.
*/
@@ -124,3 +184,61 @@ data class CamundaProcessInstanceCollectorProperties(
*/
val enabled: Boolean = true
)
+
+/**
+ * Properties to set up the task assigner.
+ */
+@ConstructorBinding
+data class TaskAssignerProperties(
+ /**
+ * Configures assigner type.
+ */
+ val type: TaskAssignerType = TaskAssignerType.no,
+ /**
+ * Process variable carrying the assignee information used by the process variable task assigner.
+ */
+ val assignee: String? = null,
+ /**
+ * Process variable carrying the candidateUsers information used by the process variable task assigner.
+ */
+ val candidateUsers: String? = null,
+ /**
+ * Process variable carrying the candidateGroups information used by the process variable task assigner.
+ */
+ val candidateGroups: String? = null
+) {
+ /**
+ * Constructs mapping from properties.
+ */
+ fun toMapping(): ProcessVariableTaskAssignerMapping = ProcessVariableTaskAssignerMapping(
+ assignee = assignee,
+ candidateUsers = candidateUsers,
+ candidateGroups = candidateGroups,
+ )
+}
+
+/**
+ * Configuration of the task importer.
+ */
+@ConstructorBinding
+data class TaskImporterProperties(
+ /**
+ * Enables or disabled importer. Defaults to false.
+ */
+ val enabled: Boolean = false,
+
+ /**
+ * Configures the type of engine task command filter.
+ * Defaults to `eventstore` allowing co-located deployed Taskpool Core to be used as a reference to filter commands.
+ */
+ val taskFilterType: EngineTaskCommandFilterType = EngineTaskCommandFilterType.eventstore
+)
+
+/**
+ * Type
+ */
+enum class EngineTaskCommandFilterType {
+ eventstore,
+ custom
+}
+
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/process/definition/ProcessDefinitionService.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/process/definition/ProcessDefinitionService.kt
index aa0c7c404..61846b701 100644
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/process/definition/ProcessDefinitionService.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/process/definition/ProcessDefinitionService.kt
@@ -50,7 +50,15 @@ class ProcessDefinitionService(
this.processDefinitions.clear()
}
this.processDefinitions.addAll(newDefinitions)
- return newDefinitions.map { it.asCommand(applicationName = collectorProperties.applicationName, formKey = formService.getStartFormKey(it.id)) }
+ return newDefinitions.map { it.asCommand(applicationName = collectorProperties.applicationName, formKey = getStartFormKey(it, formService)) }
+ }
+
+ private fun getStartFormKey(processDefinitionEntity: ProcessDefinitionEntity, formService: FormService): String? {
+ return if (processDefinitionEntity.hasStartFormKey) {
+ formService.getStartFormKey(processDefinitionEntity.id)
+ } else {
+ null
+ }
}
/**
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskAssigner.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskAssigner.kt
new file mode 100644
index 000000000..76f4adfa6
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskAssigner.kt
@@ -0,0 +1,15 @@
+package io.holunda.polyflow.taskpool.collector.task
+
+import io.holunda.camunda.taskpool.api.task.EngineTaskCommand
+
+/**
+ * Hook for assignment changes.
+ */
+interface TaskAssigner {
+ /**
+ * Sets assignment of a task command.
+ * @param command task command
+ * @return command with modified assignment information.
+ */
+ fun setAssignment(command: EngineTaskCommand): EngineTaskCommand
+}
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCollectorConfiguration.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCollectorConfiguration.kt
index 926e62ce4..9434e7272 100644
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCollectorConfiguration.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCollectorConfiguration.kt
@@ -1,7 +1,13 @@
package io.holunda.polyflow.taskpool.collector.task
+import io.holunda.camunda.taskpool.api.task.EngineTaskCommandFilter
+import io.holunda.polyflow.taskpool.collector.CamundaTaskpoolCollectorConfiguration
import io.holunda.polyflow.taskpool.collector.CamundaTaskpoolCollectorProperties
+import io.holunda.polyflow.taskpool.collector.TaskAssignerType
import io.holunda.polyflow.taskpool.collector.TaskCollectorEnricherType
+import io.holunda.polyflow.taskpool.collector.task.assigner.EmptyTaskAssigner
+import io.holunda.polyflow.taskpool.collector.task.assigner.ProcessVariableChangeAssigningService
+import io.holunda.polyflow.taskpool.collector.task.assigner.ProcessVariablesTaskAssigner
import io.holunda.polyflow.taskpool.collector.task.enricher.EmptyTaskCommandEnricher
import io.holunda.polyflow.taskpool.collector.task.enricher.ProcessVariablesCorrelator
import io.holunda.polyflow.taskpool.collector.task.enricher.ProcessVariablesFilter
@@ -14,12 +20,12 @@ import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl
import org.camunda.bpm.engine.impl.interceptor.CommandExecutor
import org.camunda.bpm.engine.spring.SpringProcessEnginePlugin
import org.camunda.bpm.spring.boot.starter.property.CamundaBpmProperties
+import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import org.springframework.context.annotation.Lazy
/**
* Constructs the task collector components.
@@ -48,29 +54,67 @@ class TaskCollectorConfiguration(
}
}
+ /**
+ * Task variables loader.
+ */
+ @Bean
+ fun taskVariablesLoader(
+ runtimeService: RuntimeService,
+ taskService: TaskService,
+ commandExecutor: CommandExecutor
+ ): TaskVariableLoader = TaskVariableLoader(runtimeService, taskService, commandExecutor)
+
/**
* Create enricher.
*/
@Bean
@ConditionalOnExpression("'\${polyflow.integration.collector.camunda.task.enricher.type}' != 'custom'")
fun processVariablesEnricher(
- runtimeService: RuntimeService,
- taskService: TaskService,
- commandExecutor: CommandExecutor,
+ taskVariableLoader: TaskVariableLoader,
filter: ProcessVariablesFilter,
correlator: ProcessVariablesCorrelator
): VariablesEnricher =
when (camundaTaskpoolCollectorProperties.task.enricher.type) {
- TaskCollectorEnricherType.processVariables -> ProcessVariablesTaskCommandEnricher(runtimeService, taskService, commandExecutor, filter, correlator)
+ TaskCollectorEnricherType.processVariables -> ProcessVariablesTaskCommandEnricher(
+ processVariablesFilter = filter,
+ processVariablesCorrelator = correlator,
+ taskVariableLoader = taskVariableLoader
+ )
+
TaskCollectorEnricherType.no -> EmptyTaskCommandEnricher()
else -> throw IllegalStateException("Could not initialize task enricher, used unknown ${camundaTaskpoolCollectorProperties.task.enricher.type} type.")
}
+ /**
+ * Creates an empty task assigner if no assigner is defined.
+ */
+ @Bean
+ @ConditionalOnExpression("'\${polyflow.integration.collector.camunda.task.assigner.type}' != 'custom'")
+ fun taskAssigner(taskVariableLoader: TaskVariableLoader): TaskAssigner =
+ when (camundaTaskpoolCollectorProperties.task.assigner.type) {
+ TaskAssignerType.no -> EmptyTaskAssigner()
+ TaskAssignerType.processVariables -> ProcessVariablesTaskAssigner(
+ taskVariableLoader = taskVariableLoader,
+ processVariableTaskAssignerMapping = camundaTaskpoolCollectorProperties.task.assigner.toMapping()
+ )
+
+ else -> throw IllegalStateException("Could not initialize task assigner, used unknown ${camundaTaskpoolCollectorProperties.task.assigner.type} type.")
+ }
+
+ /**
+ * Service responsible for changing assignees on process variable change.
+ */
+ @Bean
+ @ConditionalOnExpression("'\${polyflow.integration.collector.camunda.task.assigner.type}' == 'process-variables' && '\${polyflow.integration.collector.camunda.process-variable.enabled}'")
+ fun processVariableChangeAssigningService(taskService: TaskService) = ProcessVariableChangeAssigningService(
+ taskService = taskService,
+ mapping = camundaTaskpoolCollectorProperties.task.assigner.toMapping()
+ )
/**
* Constructs the task collector service responsible for collecting Camunda Spring events and building commands out of them.
*/
- @Bean
+ @Bean(TaskEventCollectorService.NAME)
fun taskEventCollectorService(repositoryService: RepositoryService) = TaskEventCollectorService(
camundaTaskpoolCollectorProperties = camundaTaskpoolCollectorProperties,
repositoryService = repositoryService
@@ -82,9 +126,37 @@ class TaskCollectorConfiguration(
@Bean
fun taskCommandProcessor(
engineTaskCommandSender: EngineTaskCommandSender,
- variablesEnricher: VariablesEnricher
+ variablesEnricher: VariablesEnricher,
+ taskAssigner: TaskAssigner
) = TaskCommandProcessor(
engineTaskCommandSender = engineTaskCommandSender,
- enricher = variablesEnricher
+ enricher = variablesEnricher,
+ taskAssigner = taskAssigner
)
+
+ /**
+ * Create a task collector service collecting tasks directly from the task service of the engine.
+ */
+ @Bean
+ @ConditionalOnProperty(value = ["polyflow.integration.collector.camunda.task.importer.enabled"], havingValue = "true", matchIfMissing = false)
+ fun taskServiceCollectorService(
+ taskService: TaskService,
+ commandExecutor: CommandExecutor,
+ applicationEventPublisher: ApplicationEventPublisher,
+ @Autowired(required = false) engineTaskCommandFilter: EngineTaskCommandFilter?
+ ): TaskServiceCollectorService {
+
+ if (engineTaskCommandFilter == null) {
+ CamundaTaskpoolCollectorConfiguration.logger.warn { "Task importer is configured, but no task filter is provided. All tasks commands will be rejected." }
+ }
+
+ return TaskServiceCollectorService(
+ taskService = taskService,
+ commandExecutor = commandExecutor,
+ camundaTaskpoolCollectorProperties = camundaTaskpoolCollectorProperties,
+ applicationEventPublisher = applicationEventPublisher,
+ engineTaskCommandFilter = engineTaskCommandFilter ?: object : EngineTaskCommandFilter {}
+ )
+
+ }
}
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCommandProcessor.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCommandProcessor.kt
index ecf00f2c7..0aa86f415 100644
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCommandProcessor.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskCommandProcessor.kt
@@ -14,7 +14,8 @@ import org.springframework.context.event.EventListener
*/
class TaskCommandProcessor(
private val engineTaskCommandSender: EngineTaskCommandSender,
- private val enricher: VariablesEnricher
+ private val enricher: VariablesEnricher,
+ private val taskAssigner: TaskAssigner
) {
companion object : KLogging()
@@ -27,9 +28,11 @@ class TaskCommandProcessor(
when (command) {
is TaskIdentityWithPayloadAndCorrelations -> enricher.enrich(command)
else -> command
+ }.let {
+ taskAssigner.setAssignment(it)
}.also { commandToSend ->
if (logger.isTraceEnabled) {
- logger.trace("COLLECTOR-008: Sending engine task command: $commandToSend.")
+ logger.trace {"COLLECTOR-008: Sending engine task command: $commandToSend." }
}
// enrich and send
engineTaskCommandSender.send(commandToSend)
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskEventCollectorService.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskEventCollectorService.kt
index 9f5776ba9..2978b57e2 100755
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskEventCollectorService.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskEventCollectorService.kt
@@ -17,12 +17,13 @@ import org.springframework.core.annotation.Order
* Collects Camunda events and Camunda historic events (event listener order is {@link TaskEventCollectorService#ORDER}) and emits Commands
*/
class TaskEventCollectorService(
- private val camundaTaskpoolCollectorProperties: CamundaTaskpoolCollectorProperties,
+ val camundaTaskpoolCollectorProperties: CamundaTaskpoolCollectorProperties, // must not be private to access in conditions of event handlers
private val repositoryService: RepositoryService
) {
companion object : KLogging() {
+ const val NAME = "taskEventCollectorService"
// high order to be later than all other listeners and work on changed entity
const val ORDER = Integer.MAX_VALUE - 100
}
@@ -30,7 +31,7 @@ class TaskEventCollectorService(
/**
* Tracing of collector.
*/
- @Order(ORDER)
+ @Order(ORDER - 10)
@EventListener
fun all(task: DelegateTask) {
if (logger.isTraceEnabled) {
@@ -46,7 +47,7 @@ class TaskEventCollectorService(
* Fires create command.
*/
@Order(ORDER)
- @EventListener(condition = "#task.eventName.equals('create')")
+ @EventListener(condition = "#task.eventName.equals('create') && @taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectTaskEvent('create')")
fun create(task: DelegateTask): CreateTaskCommand =
CreateTaskCommand(
id = task.id,
@@ -70,7 +71,7 @@ class TaskEventCollectorService(
* Fires complete.
*/
@Order(ORDER)
- @EventListener(condition = "#task.eventName.equals('complete')")
+ @EventListener(condition = "#task.eventName.equals('complete') && @taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectTaskEvent('complete')")
fun complete(task: DelegateTask): CompleteTaskCommand =
CompleteTaskCommand(
id = task.id,
@@ -81,17 +82,21 @@ class TaskEventCollectorService(
* Fires assign command.
*/
@Order(ORDER)
- @EventListener(condition = "#task.eventName.equals('assignment')")
+ @EventListener(condition = "#task.eventName.equals('assignment') && @taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectTaskEvent('assignment')")
fun assign(task: DelegateTask) {
// this method is intentionally empty to demonstrate that the assign event is captured.
// we hence rely on historic identity link events to capture assignment via API and via listeners more accurately.
+ // see implementation below
+ if (logger.isTraceEnabled) {
+ logger.trace { "Task ${task.id} is assigned to ${task.assignee}." }
+ }
}
/**
* Fires delete command.
*/
@Order(ORDER)
- @EventListener(condition = "#task.eventName.equals('delete')")
+ @EventListener(condition = "#task.eventName.equals('delete') && @taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectTaskEvent('delete')")
fun delete(task: DelegateTask): DeleteTaskCommand =
DeleteTaskCommand(
id = task.id,
@@ -99,12 +104,11 @@ class TaskEventCollectorService(
eventName = task.eventName
)
-
/**
* Fires update command.
*/
@Order(ORDER)
- @EventListener(condition = "#task.eventName.equals('update')")
+ @EventListener(condition = "#task.eventName.equals('update') && @taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectTaskEvent('update')")
fun update(task: DelegateTask): UpdateAttributeTaskCommand? =
if (task is TaskEntity) {
if (task.isAssigneeChange()) {
@@ -123,10 +127,9 @@ class TaskEventCollectorService(
* into the original intent.
*/
@Order(ORDER)
- @EventListener
- fun update(changeEvent: HistoricTaskInstanceEventEntity): UpdateAttributesHistoricTaskCommand? =
- when (changeEvent.eventType) {
- "update" -> UpdateAttributesHistoricTaskCommand(
+ @EventListener(condition = "#changeEvent.eventType.equals('update') && @taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectHistoryEvent('update')")
+ fun update(changeEvent: HistoricTaskInstanceEventEntity): UpdateAttributesHistoricTaskCommand =
+ UpdateAttributesHistoricTaskCommand(
id = changeEvent.taskId,
description = changeEvent.description,
dueDate = changeEvent.dueDate,
@@ -138,15 +141,12 @@ class TaskEventCollectorService(
sourceReference = changeEvent.sourceReference(repositoryService, camundaTaskpoolCollectorProperties.applicationName)
)
- else -> null
- }
-
/**
* Fires update assignment historic command.
* This is the only way to detect changes of identity links (candidate user/group change and remove).
*/
@Order(ORDER)
- @EventListener
+ @EventListener(condition = "@taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectHistoryEvent('add-identity-link') || @taskEventCollectorService.camundaTaskpoolCollectorProperties.task.collectHistoryEvent('delete-identity-link')")
fun update(changeEvent: HistoricIdentityLinkLogEventEntity): Any? =
when {
// user assignment. Is needed because the assignment out of a listener is undetected otherwise.
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskServiceCollectorService.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskServiceCollectorService.kt
new file mode 100644
index 000000000..7265e7a8f
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskServiceCollectorService.kt
@@ -0,0 +1,85 @@
+package io.holunda.polyflow.taskpool.collector.task
+
+import io.holunda.camunda.taskpool.api.task.EngineTaskCommandFilter
+import io.holunda.polyflow.taskpool.asCreateCommand
+import io.holunda.polyflow.taskpool.callInProcessEngineContext
+import io.holunda.polyflow.taskpool.collector.CamundaTaskpoolCollectorProperties
+import io.holunda.polyflow.view.query.PageableSortableQuery
+import org.camunda.bpm.engine.TaskService
+import org.camunda.bpm.engine.impl.db.entitymanager.DbEntityManager
+import org.camunda.bpm.engine.impl.interceptor.CommandContext
+import org.camunda.bpm.engine.impl.interceptor.CommandContextListener
+import org.camunda.bpm.engine.impl.interceptor.CommandExecutor
+import org.camunda.bpm.engine.impl.persistence.entity.TaskEntity
+import org.springframework.context.ApplicationEventPublisher
+
+/**
+ * Service to collect tasks and fire the corresponding commands using Camunda Task Service.
+ */
+class TaskServiceCollectorService(
+ private val taskService: TaskService,
+ private val commandExecutor: CommandExecutor,
+ private val camundaTaskpoolCollectorProperties: CamundaTaskpoolCollectorProperties,
+ private val applicationEventPublisher: ApplicationEventPublisher,
+ private val engineTaskCommandFilter: EngineTaskCommandFilter
+) {
+
+ /**
+ * Collects tasks existing inm the Camunda task service
+ * sends commands for all which are not filtered away by the filter.
+ * In combination with [TaskAggregateEngineTaskCommandFilter] this can be used to initialize
+ * the event store with tasks from the engine.
+ * @param activeOnly parameter controlling if only active tasks are collected.
+ * @param firstResult first result of filtered command list to be sent
+ * @param maxResults last result of filtered command list to be sent
+ */
+ fun collectAndPopulateExistingTasks(activeOnly: Boolean = true, firstResult: Int = 0, maxResults: Int = 1000) {
+ callInProcessEngineContext(newContext = true) {
+ commandExecutor.execute { innerContext ->
+ innerContext.registerCommandContextListener(object : CommandContextListener {
+ override fun onCommandContextClose(commandContext: CommandContext) {
+ // Remove all cached entities from the inner context because we don't want to accidentally flush any changes that could cause
+ // OptimisticLockingExceptions in the outer context.
+ // (We don't change any variables but there are situations where _reading_ a variable also causes a change, e.g. when the serialized form of a
+ // complex variable has changed because properties have been added since creation of the variable. Another example: A variable containing a Set
+ // has been created and serialized from a LinkedHashSet with a specific order, but is deserialized to a HashSet with a different iteration order.
+ // When this is serialized again, the serialized form will look different because the order has changed.
+ (commandContext.sessions[DbEntityManager::class.java] as? DbEntityManager)?.dbEntityCache?.apply { cachedEntities.forEach { remove(it) } }
+ }
+
+ override fun onCommandFailed(commandContext: CommandContext, t: Throwable) {
+ }
+ })
+
+ // query
+ val engineTasks = taskService
+ .createTaskQuery()
+ .initializeFormKeys()
+ .let {
+ if (activeOnly) {
+ it.active()
+ } else {
+ it
+ }
+ }.list()
+
+ // create commands
+ val commands = engineTasks.filterIsInstance().map { task -> task.asCreateCommand(camundaTaskpoolCollectorProperties.applicationName) }
+
+ // filter and limit
+ val filtered = commands.filter { command -> engineTaskCommandFilter.test(command) }.let {
+ if (firstResult < it.size) {
+ it.subList(firstResult, maxResults.coerceAtMost(it.size))
+ } else {
+ it
+ }
+ }
+
+ // publish
+ filtered.forEach {
+ applicationEventPublisher.publishEvent(it)
+ }
+ }
+ }
+ }
+}
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskVariableLoader.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskVariableLoader.kt
new file mode 100644
index 000000000..78ec156b5
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/TaskVariableLoader.kt
@@ -0,0 +1,89 @@
+package io.holunda.polyflow.taskpool.collector.task
+
+import io.holunda.camunda.taskpool.api.task.*
+import io.holunda.polyflow.taskpool.callInProcessEngineContext
+import mu.KLogging
+import org.camunda.bpm.engine.RuntimeService
+import org.camunda.bpm.engine.TaskService
+import org.camunda.bpm.engine.impl.db.entitymanager.DbEntityManager
+import org.camunda.bpm.engine.impl.interceptor.CommandContext
+import org.camunda.bpm.engine.impl.interceptor.CommandContextListener
+import org.camunda.bpm.engine.impl.interceptor.CommandExecutor
+import org.camunda.bpm.engine.variable.VariableMap
+import org.camunda.bpm.engine.variable.Variables
+
+/**
+ * Facility to load variables in a command context.
+ * @param commandExecutor Camunda API executor.
+ * @param taskService Camunda API to access tasks.
+ * @param runtimeService Camunda API to access the execution.
+ */
+class TaskVariableLoader(
+ private val runtimeService: RuntimeService,
+ private val taskService: TaskService,
+ private val commandExecutor: CommandExecutor,
+) {
+ companion object : KLogging()
+
+ /**
+ * Retrieves typed variables from the context of a command.
+ * @param command command context.
+ * @return variable map.
+ */
+ fun getTypeVariables(command: T): VariableMap {
+ return if (command.isHistoric()) {
+ // Task updated
+ // This is a historic command which is processed from a command context listener on command context close. Accessing variables at this point will try to add
+ // another command context listener (some camunda feature about updating mutable variables without explicitly saving them), which in turn causes a
+ // ConcurrentModificationException.
+ // We work around this by opening a new context.
+ // Caution: This will only work if the task is already flushed to the database, e.g. if it was created in a previous transaction.
+ // It will also see only the state of the variables that is flushed to the database
+ callInProcessEngineContext(newContext = true) {
+ commandExecutor.execute { innerContext ->
+ val task = taskService.createTaskQuery().taskId(command.id).singleResult()
+ if (task != null) {
+ taskService.getVariablesTyped(command.id)
+ } else {
+ val execution = runtimeService.createExecutionQuery().executionId(command.sourceReference.executionId).singleResult()
+ if (execution != null) {
+ runtimeService.getVariablesTyped(command.sourceReference.executionId)
+ } else {
+ logger.debug { "ENRICHER-004: Could not enrich variables from running execution ${command.sourceReference.executionId}, since it doesn't exist (anymore)." }
+ Variables.createVariables()
+ }
+ }.also {
+ innerContext.registerCommandContextListener(object : CommandContextListener {
+ override fun onCommandContextClose(commandContext: CommandContext) {
+ // Remove all cached entities from the inner context because we don't want to accidentally flush any changes that could cause
+ // OptimisticLockingExceptions in the outer context.
+ // (We don't change any variables but there are situations where _reading_ a variable also causes a change, e.g. when the serialized form of a
+ // complex variable has changed because properties have been added since creation of the variable. Another example: A variable containing a Set
+ // has been created and serialized from a LinkedHashSet with a specific order, but is deserialized to a HashSet with a different iteration order.
+ // When this is serialized again, the serialized form will look different because the order has changed.
+ (commandContext.sessions[DbEntityManager::class.java] as? DbEntityManager)?.dbEntityCache?.apply { cachedEntities.forEach { remove(it) } }
+ }
+
+ override fun onCommandFailed(commandContext: CommandContext, t: Throwable) {
+ }
+ })
+ }
+ }
+ }
+ } else {
+ // Create task
+ taskService.getVariablesTyped(command.id)
+ }
+ }
+}
+
+/**
+ * Checks if the command is created from a historic Camunda event.
+ */
+fun Any.isHistoric(): Boolean =
+ when (this) {
+ is CreateTaskCommand, is DeleteTaskCommand, is CompleteTaskCommand, is AssignTaskCommand, is UpdateAttributeTaskCommand -> false
+ is UpdateAttributesHistoricTaskCommand, is UpdateAssignmentTaskCommand -> true
+ else -> throw IllegalArgumentException("Unexpected command received: '$this'")
+ }
+
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/EmptyTaskAssigner.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/EmptyTaskAssigner.kt
new file mode 100644
index 000000000..61375a916
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/EmptyTaskAssigner.kt
@@ -0,0 +1,11 @@
+package io.holunda.polyflow.taskpool.collector.task.assigner
+
+import io.holunda.camunda.taskpool.api.task.EngineTaskCommand
+import io.holunda.polyflow.taskpool.collector.task.TaskAssigner
+
+/**
+ * No-op task assigner changing nothing.
+ */
+class EmptyTaskAssigner : TaskAssigner {
+ override fun setAssignment(command: EngineTaskCommand): EngineTaskCommand = command
+}
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariableChangeAssigningService.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariableChangeAssigningService.kt
new file mode 100644
index 000000000..fc34058e8
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariableChangeAssigningService.kt
@@ -0,0 +1,91 @@
+package io.holunda.polyflow.taskpool.collector.task.assigner
+
+import io.holunda.camunda.taskpool.api.task.*
+import io.holunda.polyflow.taskpool.sender.process.variable.CreateSingleProcessVariableCommand
+import io.holunda.polyflow.taskpool.sender.process.variable.UpdateSingleProcessVariableCommand
+import mu.KLogging
+import org.camunda.bpm.engine.TaskService
+import org.springframework.context.event.EventListener
+
+/**
+ * This service bridges the gap between changes in process variables and change of assignments of existing user tasks.
+ * It subscribes to changes to variables configures as assignment process variables and reacts to those changes
+ * emitting task assignment update events if the underlying process instance is waiting in a user task.
+ *
+ * In order to work properly, the collector needs to activate variable collection (even if the sender is disabled).
+ */
+class ProcessVariableChangeAssigningService(
+ private val taskService: TaskService,
+ private val mapping: ProcessVariableTaskAssignerMapping
+) {
+
+ companion object : KLogging()
+
+ /**
+ * React on new variables created.
+ */
+ @EventListener
+ fun on(command: CreateSingleProcessVariableCommand): EngineTaskCommand? {
+ // only relevant if waiting in a user task
+ val taskId = getTaskId(command.sourceReference) ?: return null
+ return when (command.variableName) {
+ mapping.assignee -> {
+ AssignTaskCommand(
+ id = taskId,
+ assignee = command.value.value.asStringValue()
+ )
+ }
+ mapping.candidateUsers -> {
+ if (command.value.value != null) {
+ AddCandidateUsersCommand(
+ id = taskId,
+ candidateUsers = command.value.value.asSetValue()
+ )
+ } else {
+ null
+ }
+ }
+ mapping.candidateGroups -> {
+ null
+ }
+
+ else -> null
+ }
+ }
+
+ /**
+ * React on variables updates.
+ */
+ @EventListener
+ fun on(command: UpdateSingleProcessVariableCommand): EngineTaskCommand? {
+ val taskId = getTaskId(command.sourceReference) ?: return null
+ return when (command.variableName) {
+ mapping.assignee -> {
+ AssignTaskCommand(
+ id = taskId,
+ assignee = command.value.value.asStringValue()
+ )
+ }
+ mapping.candidateUsers -> {
+ if (command.value.value != null) {
+ AddCandidateUsersCommand(
+ id = taskId,
+ candidateUsers = command.value.value.asSetValue()
+ )
+ } else {
+ null
+ }
+ }
+ mapping.candidateGroups -> {
+ null
+ }
+
+ else -> null
+ }
+ }
+
+
+ private fun getTaskId(sourceReference: SourceReference): String? {
+ return taskService.createTaskQuery().executionId(sourceReference.executionId).singleResult()?.id
+ }
+}
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariableTaskAssignerMapping.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariableTaskAssignerMapping.kt
new file mode 100644
index 000000000..ceb99db8c
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariableTaskAssignerMapping.kt
@@ -0,0 +1,73 @@
+package io.holunda.polyflow.taskpool.collector.task.assigner
+
+import org.camunda.bpm.engine.variable.VariableMap
+
+/**
+ * A mapping defining the names of process variables used for assignment.
+ */
+data class ProcessVariableTaskAssignerMapping(
+ val assignee: String?,
+ val candidateUsers: String?,
+ val candidateGroups: String?
+) {
+ /**
+ * Loads assignment from variables.
+ */
+ fun loadAssignmentFromVariables(variables: VariableMap): Assignment =
+ Assignment(
+ assignee = if (assignee != null && variables.containsKey(assignee)) {
+ variables[assignee].asStringValue()
+ } else {
+ null
+ },
+ candidateUsers = if (candidateUsers != null && variables.containsKey(candidateUsers)) {
+ variables[candidateUsers].asSetValue()
+ } else {
+ setOf()
+ },
+ candidateGroups = if (candidateGroups != null && variables.containsKey(candidateGroups)) {
+ variables[candidateGroups].asSetValue()
+ } else {
+ setOf()
+ }
+ )
+
+ /**
+ * Assignment information.
+ */
+ data class Assignment(
+ /**
+ * Assignee.
+ */
+ val assignee: String?,
+ /**
+ * Candidate users.
+ */
+ val candidateUsers: Set,
+ /**
+ * Candidate groups.
+ */
+ val candidateGroups: Set
+ )
+}
+
+/**
+ * Variable value to set or empty set.
+ */
+@Suppress("UNCHECKED_CAST")
+fun Any?.asSetValue() =
+ when (this) {
+ is Collection<*> -> (this as Collection).toSet()
+ is String -> this.split(",").map { it.trim() }.toSet()
+ else -> setOf()
+ }
+
+/**
+ * Variable value to string or null.
+ */
+fun Any?.asStringValue() =
+ when (this) {
+ is String -> this
+ else -> null
+ }
+
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariablesTaskAssigner.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariablesTaskAssigner.kt
new file mode 100644
index 000000000..e61cc0249
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/assigner/ProcessVariablesTaskAssigner.kt
@@ -0,0 +1,44 @@
+package io.holunda.polyflow.taskpool.collector.task.assigner
+
+import io.holunda.camunda.taskpool.api.task.CreateTaskCommand
+import io.holunda.camunda.taskpool.api.task.EngineTaskCommand
+import io.holunda.polyflow.taskpool.collector.task.TaskAssigner
+import io.holunda.polyflow.taskpool.collector.task.TaskVariableLoader
+
+/**
+ * Task assigner retrieving assignment information from process variables.
+ */
+class ProcessVariablesTaskAssigner(
+ private val taskVariableLoader: TaskVariableLoader,
+ private val processVariableTaskAssignerMapping: ProcessVariableTaskAssignerMapping
+) : TaskAssigner {
+ override fun setAssignment(command: EngineTaskCommand): EngineTaskCommand =
+ when (command) {
+ is CreateTaskCommand -> processVariableTaskAssignerMapping
+ .loadAssignmentFromVariables(variables = taskVariableLoader.getTypeVariables(command))
+ .let { assignment ->
+ command.let {
+ if (assignment.assignee != null) {
+ it.copy(assignee = assignment.assignee)
+ } else {
+ it
+ }
+ }.let {
+ if (assignment.candidateUsers.isNotEmpty()) {
+ it.copy(candidateUsers = assignment.candidateUsers)
+ } else {
+ it
+ }
+ }.let {
+ if (assignment.candidateGroups.isNotEmpty()) {
+ it.copy(candidateGroups = assignment.candidateGroups)
+ } else {
+ it
+ }
+ }
+ }
+
+ else -> command
+ }
+
+}
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariableFilter.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariableFilter.kt
index 04c746d5c..ed513073a 100644
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariableFilter.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariableFilter.kt
@@ -6,12 +6,12 @@ package io.holunda.polyflow.taskpool.collector.task.enricher
* If a differentiation between individual user tasks of a process is required, use a {@link TaskVariableFilter} instead.
*/
data class ProcessVariableFilter(
- override val processDefinitionKey: ProcessDefinitionKey?,
- val filterType: FilterType,
- val processVariables: List = emptyList()
-): VariableFilter {
+ override val processDefinitionKey: ProcessDefinitionKey?,
+ val filterType: FilterType,
+ val processVariables: List = emptyList()
+) : VariableFilter {
- constructor(filterType: FilterType, processVariables: List): this(null, filterType, processVariables)
+ constructor(filterType: FilterType, processVariables: List) : this(null, filterType, processVariables)
override fun filter(taskDefinitionKey: TaskDefinitionKey, variableName: VariableName): Boolean {
return (filterType == FilterType.INCLUDE) == processVariables.contains(variableName)
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariablesTaskCommandEnricher.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariablesTaskCommandEnricher.kt
index 704b18ab5..a71388750 100755
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariablesTaskCommandEnricher.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/ProcessVariablesTaskCommandEnricher.kt
@@ -2,6 +2,7 @@ package io.holunda.polyflow.taskpool.collector.task.enricher
import io.holunda.camunda.taskpool.api.task.*
import io.holunda.polyflow.taskpool.callInProcessEngineContext
+import io.holunda.polyflow.taskpool.collector.task.TaskVariableLoader
import io.holunda.polyflow.taskpool.collector.task.VariablesEnricher
import io.holunda.polyflow.taskpool.putAllTyped
import mu.KLogging
@@ -16,76 +17,21 @@ import org.camunda.bpm.engine.variable.Variables.createVariables
/**
* Enriches commands with process variables.
- * @param runtimeService Camunda API to access the execution.
* @param processVariablesFilter filter to whitelist or blacklist the variables which should be added to the task.
*/
open class ProcessVariablesTaskCommandEnricher(
- private val runtimeService: RuntimeService,
- private val taskService: TaskService,
- private val commandExecutor: CommandExecutor,
private val processVariablesFilter: ProcessVariablesFilter,
private val processVariablesCorrelator: ProcessVariablesCorrelator,
+ private val taskVariableLoader: TaskVariableLoader
) : VariablesEnricher {
companion object : KLogging()
- /**
- * Retrieves typed variables to enrich current command.
- * @param command command to enrich.
- * @return variable map.
- */
- open fun getTypeVariables(command: T): VariableMap {
- return if (command.isHistoric()) {
- // Task updated
- // This is a historic command which is processed from a command context listener on command context close. Accessing variables at this point will try to add
- // another command context listener (some camunda feature about updating mutable variables without explicitly saving them), which in turn causes a
- // ConcurrentModificationException.
- // We work around this by opening a new context.
- // Caution: This will only work if the task is already flushed to the database, e.g. if it was created in a previous transaction.
- // It will also see only the state of the variables that is flushed to the database
- callInProcessEngineContext(newContext = true) {
- commandExecutor.execute { innerContext ->
- val task = taskService.createTaskQuery().taskId(command.id).singleResult()
- if (task != null) {
- taskService.getVariablesTyped(command.id)
- } else {
- val execution = runtimeService.createExecutionQuery().executionId(command.sourceReference.executionId).singleResult()
- if (execution != null) {
- runtimeService.getVariablesTyped(command.sourceReference.executionId)
- } else {
- logger.debug { "ENRICHER-004: Could not enrich variables from running execution ${command.sourceReference.executionId}, since it doesn't exist (anymore)." }
- createVariables()
- }
- }
- .also {
- innerContext.registerCommandContextListener(object : CommandContextListener {
- override fun onCommandContextClose(commandContext: CommandContext) {
- // Remove all cached entities from the inner context because we don't want to accidentally flush any changes that could cause
- // OptimisticLockingExceptions in the outer context.
- // (We don't change any variables but there are situations where _reading_ a variable also causes a change, e.g. when the serialized form of a
- // complex variable has changed because properties have been added since creation of the variable. Another example: A variable containing a Set
- // has been created and serialized from a LinkedHashSet with a specific order, but is deserialized to a HashSet with a different iteration order.
- // When this is serialized again, the serialized form will look different because the order has changed.
- (commandContext.sessions[DbEntityManager::class.java] as? DbEntityManager)?.dbEntityCache?.apply { cachedEntities.forEach { remove(it) } }
- }
-
- override fun onCommandFailed(commandContext: CommandContext, t: Throwable) {
- }
- })
- }
- }
- }
- } else {
- // Create task
- taskService.getVariablesTyped(command.id)
- }
- }
-
override fun enrich(command: T): T {
// load variables typed
- val variablesTyped = getTypeVariables(command)
+ val variablesTyped = taskVariableLoader.getTypeVariables(command)
// Payload enrichment
command.payload.putAllTyped(
@@ -111,17 +57,3 @@ open class ProcessVariablesTaskCommandEnricher(
}
}
-/**
- * Checks if the command is created from a historic Camunda event.
- */
-fun Any.isHistoric(): Boolean =
- when (this) {
- is CreateTaskCommand, is DeleteTaskCommand, is CompleteTaskCommand, is AssignTaskCommand, is UpdateAttributeTaskCommand -> false
- is UpdateAttributesHistoricTaskCommand, is UpdateAssignmentTaskCommand -> true
- else -> throw IllegalArgumentException("Unexpected command received: '$this'")
- }
-
-
-
-
-
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/VariableFilter.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/VariableFilter.kt
index e088cc9d9..abebd71fb 100644
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/VariableFilter.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/collector/task/enricher/VariableFilter.kt
@@ -8,7 +8,7 @@ interface VariableFilter {
val processDefinitionKey: ProcessDefinitionKey?
/**
- * Returns whether or not the process variable with the given name shall be contained in the payload of the given task.
+ * Returns whether the process variable with the given name shall be contained in the payload of the given task.
* @param taskDefinitionKey the key of the task to be enriched
* @param variableName the name of the process variable
*/
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/collector/properties/CamundaTaskpoolCollectorPropertiesExtendedTest.kt b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/collector/properties/CamundaTaskpoolCollectorPropertiesExtendedTest.kt
index 3ebe6ac30..d0f1ba439 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/collector/properties/CamundaTaskpoolCollectorPropertiesExtendedTest.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/collector/properties/CamundaTaskpoolCollectorPropertiesExtendedTest.kt
@@ -4,6 +4,7 @@ import com.thoughtworks.xstream.XStream
import io.holunda.polyflow.taskpool.collector.CamundaTaskpoolCollectorConfiguration
import io.holunda.polyflow.taskpool.collector.CamundaTaskpoolCollectorProperties
import io.holunda.polyflow.taskpool.collector.TaskCollectorEnricherType
+import io.holunda.polyflow.taskpool.collector.task.TaskVariableLoader
import io.holunda.polyflow.taskpool.collector.task.VariablesEnricher
import io.holunda.polyflow.taskpool.sender.process.definition.ProcessDefinitionCommandSender
import io.holunda.polyflow.taskpool.sender.process.instance.ProcessInstanceCommandSender
@@ -15,6 +16,9 @@ import org.axonframework.eventhandling.EventBus
import org.axonframework.serialization.Serializer
import org.axonframework.serialization.xml.XStreamSerializer
import org.camunda.bpm.engine.RepositoryService
+import org.camunda.bpm.engine.RuntimeService
+import org.camunda.bpm.engine.TaskService
+import org.camunda.bpm.engine.impl.interceptor.CommandExecutor
import org.camunda.bpm.spring.boot.starter.property.CamundaBpmProperties
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
@@ -79,12 +83,38 @@ internal class CamundaTaskpoolCollectorPropertiesExtendedTest {
}
}
+ @Test
+ fun `loads properties configuration to ignore listeners`() {
+ contextRunner
+ .withUserConfiguration(TestMockConfiguration::class.java)
+ .withUserConfiguration(AdditionalMockConfiguration::class.java)
+ .withPropertyValues(
+ "spring.application.name=my-test-application",
+ "camunda.bpm.eventing.task=false",
+ "polyflow.integration.collector.camunda.task.enabled=true",
+ "polyflow.integration.collector.camunda.task.enricher.type=custom",
+ "polyflow.integration.collector.camunda.task.excluded-task-event-names=assignment",
+ "polyflow.integration.collector.camunda.task.excluded-history-event-names=add-identity-link,delete-identity-link",
+ ).run {
+
+ assertThat(it.getBean(CamundaTaskpoolCollectorProperties::class.java)).isNotNull
+ val props: CamundaTaskpoolCollectorProperties = it.getBean(CamundaTaskpoolCollectorProperties::class.java)
+
+ assertThat(props.task.enabled).isTrue
+ assertThat(props.task.collectTaskEvent("assignment")).isFalse()
+ assertThat(props.task.collectHistoryEvent("add-identity-link")).isFalse()
+ assertThat(props.task.collectHistoryEvent("delete-identity-link")).isFalse()
+ }
+ }
+
+
/**
* Config class without configuration annotation not to confuse others.
*/
private class AdditionalMockConfiguration {
@Bean
fun variablesEnricher(): VariablesEnricher = mock(VariablesEnricher::class.java)
+
}
/**
@@ -118,5 +148,14 @@ internal class CamundaTaskpoolCollectorPropertiesExtendedTest {
@Bean
fun repositoryService(): RepositoryService = mock()
+
+ @Bean
+ fun runtimeService(): RuntimeService = mock()
+
+ @Bean
+ fun taskService(): TaskService = mock()
+
+ @Bean
+ fun commandExecutor(): CommandExecutor = mock()
}
}
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt
index e6f6e4870..7b35e6ff1 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt
@@ -145,8 +145,8 @@ class TestDriver(
}
- fun assertProcessInstanceWaitsInUserTask(instance: ProcessInstance) {
- BpmnAwareTests.assertThat(instance).isWaitingAt(TASK_DEFINITION_KEY)
+ fun assertProcessInstanceWaitsInUserTask(instance: ProcessInstance, taskDefinitionKey: String = TASK_DEFINITION_KEY) {
+ BpmnAwareTests.assertThat(instance).isWaitingAt(taskDefinitionKey)
}
/*
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/ProcessDefinitionServiceITest.kt b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/ProcessDefinitionServiceITest.kt
index 125771b68..f5914dfe8 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/ProcessDefinitionServiceITest.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/ProcessDefinitionServiceITest.kt
@@ -86,6 +86,52 @@ class ProcessDefinitionServiceITest {
assertThat(definitions[0].candidateStarterGroups).containsExactlyElementsOf(listOf("muppetshow"))
}
+ @Test
+ fun `should not deliver process starter if no start form is available`() {
+
+ val processId = "my-id"
+ val startEventId = "start"
+ val modelInstance = Bpmn
+ .createExecutableProcess(processId)
+ .startEvent(startEventId)
+ .endEvent("end")
+ .done().apply {
+ getModelElementById(processId).setAttributeValue("name", "My Process")
+ getModelElementById(processId).setAttributeValueNs(NS_CAMUNDA, "candidateStarterGroups", "muppetshow")
+ }
+
+ repositoryService
+ .createDeployment()
+ .addModelInstance("process-without-start-form.bpmn", modelInstance)
+ .deploy()
+
+
+ val definitions = processDefinitionService.getProcessDefinitions(processEngine.processEngineConfiguration as ProcessEngineConfigurationImpl)
+
+ assertThat(definitions).isNotEmpty
+ assertThat(definitions[0].processName).isEqualTo("My Process")
+ assertThat(definitions[0].processDefinitionKey).isEqualTo("my-id")
+ assertThat(definitions[0].processDefinitionVersion).isEqualTo(1)
+ assertThat(definitions[0].formKey).isNull()
+ assertThat(definitions[0].candidateStarterGroups).containsExactlyElementsOf(listOf("muppetshow"))
+ }
+
+ @Test
+ fun `should not deliver process starter if only two message start events are available`() {
+ repositoryService
+ .createDeployment()
+ .addClasspathResource("itest/message_start_event.bpmn")
+ .deploy()
+
+ val definitions = processDefinitionService.getProcessDefinitions(processEngine.processEngineConfiguration as ProcessEngineConfigurationImpl)
+
+ assertThat(definitions).isNotEmpty
+ assertThat(definitions[0].processName).isEqualTo("My Process")
+ assertThat(definitions[0].processDefinitionKey).isEqualTo("my-id")
+ assertThat(definitions[0].processDefinitionVersion).isEqualTo(1)
+ assertThat(definitions[0].formKey).isNull()
+ }
+
/**
* Internal test application.
*/
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt
index 92dbec1d0..963bf20f6 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.module.kotlin.convertValue
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.holunda.camunda.taskpool.api.task.*
import io.holunda.polyflow.taskpool.EnableCamundaTaskpoolCollector
+import io.holunda.polyflow.taskpool.collector.task.TaskEventCollectorService
import io.holunda.polyflow.taskpool.itest.TestDriver
import io.holunda.polyflow.taskpool.itest.TestDriver.Companion.DEFAULT_VARIABLES
import io.holunda.polyflow.taskpool.itest.TestDriver.Companion.createTaskCommand
@@ -17,6 +18,7 @@ import org.axonframework.commandhandling.gateway.CommandGateway
import org.camunda.bpm.engine.RepositoryService
import org.camunda.bpm.engine.RuntimeService
import org.camunda.bpm.engine.TaskService
+import org.camunda.bpm.engine.delegate.DelegateTask
import org.camunda.bpm.engine.delegate.TaskListener
import org.camunda.bpm.engine.impl.interceptor.Command
import org.camunda.bpm.engine.impl.interceptor.CommandExecutor
@@ -32,6 +34,8 @@ import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Primary
+import org.springframework.context.event.EventListener
+import org.springframework.core.annotation.Order
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ActiveProfiles
import java.time.Instant.now
@@ -647,6 +651,43 @@ internal class TaskCollectorITest {
}
+ /**
+ * The process is started and waits in a user task. The user task has a task listener that changes some local process variables on create.
+ * The create command should contain the local variables.
+ */
+ @Test
+ fun `updates variables with create listener implemented as spring event handler`() {
+
+ // deploy
+ driver.deployProcess(
+ createUserTaskProcess(
+ taskDefinitionKey = "eventing",
+ taskListeners = listOf(
+ "create" to "#{setTaskLocalVariables}" // this one will be ignored, since it is invoked after enrichment
+ )
+ )
+ )
+
+
+ // start
+ val instance = driver.startProcessInstance(variables = Variables.createVariables().putValue("overriddenVariable", "global-value"))
+ driver.assertProcessInstanceWaitsInUserTask(instance, "eventing")
+
+ val createCommand = createTaskCommand(
+ variables = Variables.createVariables()
+ .putValue("taskLocalOnlyVariable", "only-value-by-event-handler")
+ .putValue("overriddenVariable", "local-value-by-event-handler")
+ )
+
+
+ waitAtMost(3, TimeUnit.SECONDS).untilAsserted {
+ verify(commandListGateway).sendToGateway(
+ listOf(createCommand)
+ )
+ }
+
+ verifyNoMoreInteractions(commandListGateway)
+ }
/**
* Internal test application.
@@ -694,6 +735,26 @@ internal class TaskCollectorITest {
delegateTask.dueDate = TestDriver.NOW
delegateTask.followUpDate = TestDriver.NOW
}
+
+ /**
+ * A task listener that sets some local variables.
+ */
+ @Order(TaskEventCollectorService.ORDER - 80)
+ @EventListener(condition = "#delegateTask.taskDefinitionKey.equals('eventing') && #delegateTask.eventName.equals('create')")
+ fun setTaskLocalVariables(delegateTask: DelegateTask) {
+ delegateTask.setVariableLocal("taskLocalOnlyVariable", "only-value-by-event-handler")
+ delegateTask.setVariableLocal("overriddenVariable", "local-value-by-event-handler")
+ }
+
+
+ /**
+ * A task listener that sets some local variables.
+ */
+ @Bean
+ fun setTaskLocalVariables() = TaskListener { delegateTask ->
+ delegateTask.setVariableLocal("taskLocalOnlyVariable", "only-value")
+ delegateTask.setVariableLocal("overriddenVariable", "local-value")
+ }
}
}
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/txjob/TaskTxJobSenderITest.kt b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/txjob/TaskTxJobSenderITest.kt
index bd5b4d011..eeddff42d 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/txjob/TaskTxJobSenderITest.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/txjob/TaskTxJobSenderITest.kt
@@ -7,17 +7,16 @@ import io.holunda.polyflow.taskpool.itest.TestDriver
import io.holunda.polyflow.taskpool.itest.TestDriver.Companion.createUserTaskProcess
import io.holunda.polyflow.taskpool.sender.gateway.CommandListGateway
import org.assertj.core.api.Assertions
+import org.awaitility.Awaitility
import org.axonframework.commandhandling.gateway.CommandGateway
import org.camunda.bpm.engine.ManagementService
import org.camunda.bpm.engine.RepositoryService
import org.camunda.bpm.engine.RuntimeService
-import org.camunda.bpm.engine.TaskService
-import org.camunda.bpm.engine.impl.interceptor.Command
-import org.camunda.bpm.engine.impl.interceptor.CommandExecutor
+import org.camunda.bpm.engine.delegate.TaskListener
+import org.camunda.bpm.engine.impl.persistence.entity.MessageEntity
import org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.*
+import org.camunda.bpm.engine.variable.Variables
import org.camunda.bpm.spring.boot.starter.annotation.EnableProcessApplication
-import org.junit.After
-import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
@@ -30,20 +29,20 @@ import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Primary
-import org.springframework.test.annotation.Commit
import org.springframework.test.annotation.DirtiesContext
-import org.springframework.test.annotation.Rollback
import org.springframework.test.context.ActiveProfiles
-import org.springframework.test.context.transaction.AfterTransaction
+import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
-import org.springframework.transaction.support.TransactionSynchronization
+import org.springframework.transaction.support.TransactionTemplate
import java.util.*
+import java.util.concurrent.TimeUnit
-@Disabled("Understand how to test this")
@SpringBootTest(classes = [TaskTxJobSenderITest.TaskTxJobSenderTestApplication::class], webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@ActiveProfiles("txjob-sender-itest")
@DirtiesContext
+@Transactional
+@Disabled("This test sometimes fails because it can't create a new TX (Entity Manager is closed).")
internal class TaskTxJobSenderITest {
@MockBean
@@ -59,63 +58,111 @@ internal class TaskTxJobSenderITest {
lateinit var managementService: ManagementService
@Autowired
- lateinit var taskServiceService: TaskService
-
-
- @Autowired
- lateinit var commandExecutor: CommandExecutor
+ lateinit var txTemplate: TransactionTemplate
+ lateinit var createCommand: CreateTaskCommand
private val driver: TestDriver by lazy {
TestDriver(repositoryService, runtimeService)
}
-
@BeforeEach
- fun `start process and create user task`() {
-
+ fun `setup tx template`() {
+ txTemplate.propagationBehavior = Propagation.REQUIRES_NEW.value()
}
@Test
- @Transactional
- @Commit
fun `creates task in process`() {
- // deploy
- driver.deployProcess(
- createUserTaskProcess()
- )
-// commandExecutor.execute {
-// Command {
- // start
- val instance = driver.startProcessInstance()
- // instance is started
- assertThat(instance).isStarted
- // user task
- driver.assertProcessInstanceWaitsInUserTask(instance)
-// }
-// }
+ doInTransaction {
+ // deploy
+ driver.deployProcess(
+ createUserTaskProcess()
+ )
+ // start
+ val instance = driver.startProcessInstance()
+ // instance is started
+ assertThat(instance).isStarted
+ // user task
+ driver.assertProcessInstanceWaitsInUserTask(instance)
- /*
+ verifyNoMoreInteractions(commandListGateway)
+
+ createCommand = TestDriver.createTaskCommand()
+ }
+
+ assertAndExecuteCommandSendingJob()
+
+ verify(commandListGateway).sendToGateway(
+ listOf(createCommand)
+ )
+ }
- val createCommand = createTaskCommand(
- candidateUsers = setOf("piggy"),
- candidateGroups = setOf("muppetshow"),
+ /**
+ * The process is started and waits in a user task. The user task has a task listener that changes some local process variables on create.
+ * The create command should contain the local variables.
+ */
+ @Test
+ @Disabled("Find out why the local listener update always gt into the next TX and how to deal with it")
+ fun `updates variables with create listener`() {
+
+ doInTransaction {
+ // deploy
+ driver.deployProcess(
+ createUserTaskProcess(
+ taskListeners = listOf(
+ "create" to "#{setTaskLocalVariables}"
+ )
)
- */
+ )
+
+ // start
+ val instance = driver.startProcessInstance(variables = Variables.createVariables().putValue("overriddenVariable", "global-value"))
+ driver.assertProcessInstanceWaitsInUserTask(instance)
+
+ verifyNoMoreInteractions(commandListGateway)
+ createCommand = TestDriver.createTaskCommand(
+ variables = Variables.createVariables()
+ .putValue("taskLocalOnlyVariable", "only-value")
+ .putValue("overriddenVariable", "local-value")
+ )
+ }
- verifyNoMoreInteractions(commandListGateway)
+ assertAndExecuteCommandSendingJob()
+
+ verify(commandListGateway).sendToGateway(
+ listOf(createCommand)
+ )
}
- @AfterTransaction
- fun `after all`() {
- val jobs = managementService.createJobQuery().list()
- Assertions.assertThat(jobs).hasSize(1)
+ private fun assertAndExecuteCommandSendingJob() {
+ doInTransaction {
+ val jobs = managementService.createJobQuery().list()
+ Assertions.assertThat(jobs).hasSize(1)
+ Assertions.assertThat(jobs[0]).isInstanceOf(MessageEntity::class.java)
+ Assertions.assertThat((jobs[0] as MessageEntity).jobHandlerType).isEqualTo("polyflow-engine-task-command-sending")
+
+ Awaitility.waitAtMost(3, TimeUnit.SECONDS).untilAsserted {
+ try {
+ execute(jobs[0])
+ } catch (e: Exception) {
+ // brute force preventing Optimistic locking exception, IllegalStateException (job doesn't exist)
+ }
+ Assertions.assertThat(managementService.createJobQuery().count()).isEqualTo(0)
+ }
+ }
}
+ private fun doInTransaction(operation: Runnable) {
+ txTemplate.execute {
+ operation.run()
+ null
+ }
+
+ }
@SpringBootApplication
@EnableProcessApplication
@@ -126,5 +173,15 @@ internal class TaskTxJobSenderITest {
@Bean
@Primary
fun testTxJobAxonCommandGateway(): CommandGateway = mock()
+
+ /**
+ * A task listener that sets some local variables.
+ */
+ @Bean
+ fun setTaskLocalVariables() = TaskListener { delegateTask ->
+ delegateTask.setVariableLocal("taskLocalOnlyVariable", "only-value")
+ delegateTask.setVariableLocal("overriddenVariable", "local-value")
+ }
+
}
}
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/resources/application-txjob-sender-itest.yml b/integration/camunda-bpm/taskpool-collector/src/test/resources/application-txjob-sender-itest.yml
index 6c3a453c9..2b0e97a03 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/resources/application-txjob-sender-itest.yml
+++ b/integration/camunda-bpm/taskpool-collector/src/test/resources/application-txjob-sender-itest.yml
@@ -1,5 +1,5 @@
spring:
- application.name: txjob-sender-test
+ application.name: collector-test
jpa:
open-in-view: true # disable JPA warning
show-sql: true
@@ -17,7 +17,16 @@ camunda:
polyflow:
integration:
+ collector:
+ camunda:
+ task:
+ enabled: true
+ process-instance:
+ enabled: false
+ process-definition:
+ enabled: false
sender:
task:
type: txjob
send-within-transaction: true
+
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/resources/itest/message_start_event.bpmn b/integration/camunda-bpm/taskpool-collector/src/test/resources/itest/message_start_event.bpmn
new file mode 100644
index 000000000..67f5ead53
--- /dev/null
+++ b/integration/camunda-bpm/taskpool-collector/src/test/resources/itest/message_start_event.bpmn
@@ -0,0 +1,44 @@
+
+
+
+
+ Flow_0rqm55e
+ Flow_1k4xpiz
+
+
+
+
+ Flow_0rqm55e
+
+
+
+ Flow_1k4xpiz
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/integration/camunda-bpm/taskpool-job-sender/pom.xml b/integration/camunda-bpm/taskpool-job-sender/pom.xml
index 1543cb71f..75e133f72 100755
--- a/integration/camunda-bpm/taskpool-job-sender/pom.xml
+++ b/integration/camunda-bpm/taskpool-job-sender/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-integration-camunda-bpm-engine-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-camunda-bpm-taskpool-job-sender
@@ -33,7 +33,7 @@
camunda-engine
provided
-
+
org.springframework.boot
spring-boot-starter
@@ -121,11 +121,6 @@
jackson-datatype-jsr310
test
-
- org.assertj
- assertj-core
- test
-
diff --git a/integration/camunda-bpm/taskpool-job-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/CamundaJobSenderConfiguration.kt b/integration/camunda-bpm/taskpool-job-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/CamundaJobSenderConfiguration.kt
index 66686c300..cae2ddfa2 100644
--- a/integration/camunda-bpm/taskpool-job-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/CamundaJobSenderConfiguration.kt
+++ b/integration/camunda-bpm/taskpool-job-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/CamundaJobSenderConfiguration.kt
@@ -36,7 +36,6 @@ class CamundaJobSenderConfiguration(
processEngineConfiguration: ProcessEngineConfigurationImpl,
@Qualifier(COMMAND_BYTEARRAY_OBJECT_MAPPER)
objectMapper: ObjectMapper,
- commandListGateway: CommandListGateway,
engineTaskCommandAccumulator: EngineTaskCommandAccumulator
): EngineTaskCommandSender =
TxAwareAccumulatingCamundaJobEngineTaskCommandSender(
diff --git a/integration/common/datapool-sender/pom.xml b/integration/common/datapool-sender/pom.xml
index ad2fd105d..82c96800d 100755
--- a/integration/common/datapool-sender/pom.xml
+++ b/integration/common/datapool-sender/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-integration-common-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-datapool-sender
diff --git a/integration/common/pom.xml b/integration/common/pom.xml
index e621ae3b5..95362a87c 100755
--- a/integration/common/pom.xml
+++ b/integration/common/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
../../bom/parent/pom.xml
diff --git a/integration/common/tasklist-url-resolver/pom.xml b/integration/common/tasklist-url-resolver/pom.xml
index e665658b3..b70ce8c46 100644
--- a/integration/common/tasklist-url-resolver/pom.xml
+++ b/integration/common/tasklist-url-resolver/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-integration-common-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-tasklist-url-resolver
diff --git a/integration/common/taskpool-sender/pom.xml b/integration/common/taskpool-sender/pom.xml
index d8b65aaca..f77540b22 100755
--- a/integration/common/taskpool-sender/pom.xml
+++ b/integration/common/taskpool-sender/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-integration-common-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-taskpool-sender
diff --git a/integration/common/variable-serializer/pom.xml b/integration/common/variable-serializer/pom.xml
index 6afd3649f..ccbdc94b8 100755
--- a/integration/common/variable-serializer/pom.xml
+++ b/integration/common/variable-serializer/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-integration-common-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-variable-serializer
diff --git a/pom.xml b/pom.xml
index 3ca475cc9..563665431 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
io.holunda.polyflow
polyflow-root
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
pom
POM: ${project.artifactId}
@@ -20,7 +20,7 @@
${java.version}
${java.version}
- 1.8.20
+ 1.9.10
${java.version}
true
3.0.5
@@ -41,13 +41,13 @@
org.apache.maven.plugins
maven-clean-plugin
- 3.2.0
+ 3.3.1
com.amashchenko.maven.plugin
gitflow-maven-plugin
- 1.19.0
+ 1.20.0
master
@@ -67,7 +67,7 @@
org.codehaus.mojo
versions-maven-plugin
- 2.15.0
+ 2.16.0
false
@@ -79,7 +79,7 @@
org.apache.maven.plugins
maven-enforcer-plugin
- 3.3.0
+ 3.4.1
enforce-maven
@@ -159,7 +159,7 @@
org.apache.maven.plugins
maven-gpg-plugin
- 3.0.1
+ 3.1.0
diff --git a/view/form-url-resolver/pom.xml b/view/form-url-resolver/pom.xml
index fc5d1b4af..67805e164 100644
--- a/view/form-url-resolver/pom.xml
+++ b/view/form-url-resolver/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-view-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-form-url-resolver
diff --git a/view/jpa/pom.xml b/view/jpa/pom.xml
index 7eb3a252d..50675ed86 100644
--- a/view/jpa/pom.xml
+++ b/view/jpa/pom.xml
@@ -6,7 +6,7 @@
io.holunda.polyflow
polyflow-view-parent
- 3.8.3-SNAPSHOT
+ 3.14.4-SNAPSHOT
polyflow-view-jpa
@@ -51,7 +51,6 @@
com.h2database
h2
- 1.4.200
test
@@ -103,6 +102,12 @@
org.axonframework
axon-spring-boot-starter
test
+
+
+ org.axonframework
+ axon-server-connector
+
+
@@ -228,7 +233,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.2.0.jre11
+ 12.4.1.jre11
org.postgresql
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/JpaPolyflowViewTaskService.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/JpaPolyflowViewTaskService.kt
index 300fd4156..ca56faa6e 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/JpaPolyflowViewTaskService.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/JpaPolyflowViewTaskService.kt
@@ -15,6 +15,7 @@ import io.holunda.polyflow.view.jpa.data.toDataEntry
import io.holunda.polyflow.view.jpa.task.TaskEntity
import io.holunda.polyflow.view.jpa.task.TaskRepository
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasApplication
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.isAssignedTo
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.isAssigneeSet
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.isAuthorizedFor
import io.holunda.polyflow.view.jpa.task.toEntity
@@ -55,12 +56,27 @@ class JpaPolyflowViewTaskService(
val criteria = toCriteria(query.filters)
val taskSpecification = criteria.toTaskSpecification()
val dataEntrySpecification = criteria.toDataEntrySpecification()
- val pageRequest = pageRequest(query.page, query.size, query.sort)
+ val sort = query.apply {
+ sanitizeSort(Task::class)
+ }.mapTaskSort()
+
+ val pageRequest = pageRequest(query.page, query.size, sort)
+
+ val userQuery = if (query.assignedToMeOnly) {
+ isAssignedTo(query.user.username)
+ } else {
+ composeOr(
+ listOf(
+ isAuthorizedFor(authorizedPrincipals),
+ isAssignedTo(query.user.username)
+ )
+ )
+ }
val page = if (taskSpecification != null) {
- taskRepository.findAll(taskSpecification.and(isAuthorizedFor(authorizedPrincipals)), pageRequest)
+ taskRepository.findAll(taskSpecification.and(userQuery), pageRequest)
} else {
- taskRepository.findAll(isAuthorizedFor(authorizedPrincipals), pageRequest)
+ taskRepository.findAll(userQuery, pageRequest)
}
.map { taskEntity ->
TaskWithDataEntries(
@@ -103,7 +119,10 @@ class JpaPolyflowViewTaskService(
val criteria = toCriteria(query.filters)
val taskSpecification = criteria.toTaskSpecification()
val dataEntrySpecification = criteria.toDataEntrySpecification()
- val pageRequest = pageRequest(query.page, query.size, query.sort)
+ val sort = query.apply {
+ sanitizeSort(Task::class)
+ }.mapTaskSort()
+ val pageRequest = pageRequest(query.page, query.size, sort)
val page = if (taskSpecification != null) {
taskRepository.findAll(taskSpecification.and(taskAuthorizationSpecification), pageRequest)
@@ -142,8 +161,10 @@ class JpaPolyflowViewTaskService(
val criteria = toCriteria(query.filters)
val taskSpecification = criteria.toTaskSpecification()
val dataEntrySpecification = criteria.toDataEntrySpecification()
-
- val pageRequest = pageRequest(query.page, query.size, query.sort)
+ val sort = query.apply {
+ sanitizeSort(Task::class)
+ }.mapTaskSort()
+ val pageRequest = pageRequest(query.page, query.size, sort)
val page = if (taskSpecification != null) {
taskRepository.findAll(taskSpecification, pageRequest)
@@ -180,12 +201,25 @@ class JpaPolyflowViewTaskService(
override fun query(query: TasksForUserQuery): TaskQueryResult {
val authorizedPrincipals: Set = setOf(user(query.user.username)).plus(query.user.groups.map { group(it) })
val specification = toCriteria(query.filters).toTaskSpecification()
- val pageRequest = pageRequest(query.page, query.size, query.sort)
+ val sort = query.apply {
+ sanitizeSort(Task::class)
+ }.mapTaskSort()
+ val pageRequest = pageRequest(query.page, query.size, sort)
+ val userQuery = if (query.assignedToMeOnly) {
+ isAssignedTo(query.user.username)
+ } else {
+ composeOr(
+ listOf(
+ isAuthorizedFor(authorizedPrincipals),
+ isAssignedTo(query.user.username)
+ )
+ )
+ }
val page = if (specification != null) {
- taskRepository.findAll(specification.and(isAuthorizedFor(authorizedPrincipals)), pageRequest)
+ taskRepository.findAll(specification.and(userQuery), pageRequest)
} else {
- taskRepository.findAll(isAuthorizedFor(authorizedPrincipals), pageRequest)
+ taskRepository.findAll(userQuery, pageRequest)
}.map { taskEntity -> taskEntity.toTask(objectMapper) }
return TaskQueryResult(
@@ -203,7 +237,41 @@ class JpaPolyflowViewTaskService(
)
)
val taskSpecification = toCriteria(query.filters).toTaskSpecification()
- val pageRequest = pageRequest(query.page, query.size, query.sort)
+ val sort = query.apply {
+ sanitizeSort(Task::class)
+ }.mapTaskSort()
+ val pageRequest = pageRequest(query.page, query.size, sort)
+
+ val page = if (taskSpecification != null) {
+ taskRepository.findAll(taskSpecification.and(authorizationSpecification), pageRequest)
+ } else {
+ taskRepository.findAll(authorizationSpecification, pageRequest)
+ }.map { taskEntity -> taskEntity.toTask(objectMapper) }
+
+ return TaskQueryResult(
+ elements = page.toList(),
+ totalElementCount = page.totalElements.toInt()
+ )
+ }
+
+ @QueryHandler
+ override fun query(query: TasksForCandidateUserAndGroupQuery): TaskQueryResult {
+ val authorizationSpecification =
+ composeAnd(
+ listOf(
+ composeOr(
+ listOf(
+ isAuthorizedFor(setOf(user(query.user.username)).plus(query.user.groups.map { group(it) }.toSet())),
+ )
+ ),
+ isAssigneeSet(query.includeAssigned)
+ )
+ )
+ val taskSpecification = toCriteria(query.filters).toTaskSpecification()
+ val sort = query.apply {
+ sanitizeSort(Task::class)
+ }.mapTaskSort()
+ val pageRequest = pageRequest(query.page, query.size, sort)
val page = if (taskSpecification != null) {
taskRepository.findAll(taskSpecification.and(authorizationSpecification), pageRequest)
@@ -220,7 +288,10 @@ class JpaPolyflowViewTaskService(
@QueryHandler
override fun query(query: AllTasksQuery): TaskQueryResult {
val specification = toCriteria(query.filters).toTaskSpecification()
- val pageRequest = pageRequest(query.page, query.size, query.sort)
+ val sort = query.apply {
+ sanitizeSort(Task::class)
+ }.mapTaskSort()
+ val pageRequest = pageRequest(query.page, query.size, sort)
val page = if (specification != null) {
taskRepository.findAll(specification, pageRequest)
} else {
@@ -302,7 +373,7 @@ class JpaPolyflowViewTaskService(
event.toEntity(
objectMapper,
polyflowJpaViewProperties.payloadAttributeLevelLimit,
- polyflowJpaViewProperties.dataEntryJsonPathFilters()
+ polyflowJpaViewProperties.taskJsonPathFilters()
)
)
emitTaskUpdate(updated)
@@ -396,7 +467,7 @@ class JpaPolyflowViewTaskService(
objectMapper,
entity,
polyflowJpaViewProperties.payloadAttributeLevelLimit,
- polyflowJpaViewProperties.dataEntryJsonPathFilters()
+ polyflowJpaViewProperties.taskJsonPathFilters()
)
)
emitTaskUpdate(updated)
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/PolyflowJpaViewProperties.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/PolyflowJpaViewProperties.kt
index a2d808874..e9ce9b051 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/PolyflowJpaViewProperties.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/PolyflowJpaViewProperties.kt
@@ -26,7 +26,14 @@ data class PolyflowJpaViewProperties(
* Filters for the paths for indexing.
*/
@NestedConfigurationProperty
- private val dataEntryFilters: PayloadAttributeFilterPaths = PayloadAttributeFilterPaths()
+ private val dataEntryFilters: PayloadAttributeFilterPaths = PayloadAttributeFilterPaths(),
+
+ /**
+ * Filters for the path for indexing.
+ */
+ @NestedConfigurationProperty
+ private val taskFilters: PayloadAttributeFilterPaths = PayloadAttributeFilterPaths()
+
) {
/**
* Extracts JSON path filters out of the properties.
@@ -34,6 +41,14 @@ data class PolyflowJpaViewProperties(
fun dataEntryJsonPathFilters(): List> {
return this.dataEntryFilters.include.map { eqInclude(it) }.plus(this.dataEntryFilters.exclude.map { eqExclude(it) })
}
+
+ /**
+ * Extracts JSON path filters out of the properties.
+ */
+ fun taskJsonPathFilters(): List> {
+ return this.taskFilters.include.map { eqInclude(it) }.plus(this.taskFilters.exclude.map { eqExclude(it) })
+ }
+
}
/**
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/SpecificationExt.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/SpecificationExt.kt
index 8cb6747b1..81a3b9081 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/SpecificationExt.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/SpecificationExt.kt
@@ -3,9 +3,8 @@ package io.holunda.polyflow.view.jpa
import io.holunda.camunda.taskpool.api.business.DataEntryState
import io.holunda.camunda.taskpool.api.business.ProcessingType
import io.holunda.polyflow.view.DataEntry
-import io.holunda.polyflow.view.filter.Criterion
-import io.holunda.polyflow.view.filter.EQUALS
-import io.holunda.polyflow.view.filter.LIKE
+import io.holunda.polyflow.view.Task
+import io.holunda.polyflow.view.filter.*
import io.holunda.polyflow.view.jpa.data.DataEntryEntity
import io.holunda.polyflow.view.jpa.data.DataEntryRepository.Companion.hasDataEntryPayloadAttribute
import io.holunda.polyflow.view.jpa.data.DataEntryRepository.Companion.hasEntryId
@@ -15,14 +14,27 @@ import io.holunda.polyflow.view.jpa.data.DataEntryRepository.Companion.hasState
import io.holunda.polyflow.view.jpa.data.DataEntryRepository.Companion.hasType
import io.holunda.polyflow.view.jpa.task.TaskEntity
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasBusinessKey
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasDueDate
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasDueDateAfter
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasDueDateBefore
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasFollowUpDate
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasFollowUpDateAfter
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasFollowUpDateBefore
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasPriority
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasProcessName
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasTaskPayloadAttribute
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.likeBusinessKey
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.likeDescription
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.likeName
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.likeProcessName
+import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.likeTextSearch
+import io.holunda.polyflow.view.query.PageableSortableQuery
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
+import org.springframework.data.domain.Sort.Direction
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.jpa.domain.Specification.where
+import java.time.Instant
/**
* Creates a JPQL specification out of predicate wrapper.
@@ -66,7 +78,12 @@ fun pageRequest(page: Int, size: Int, sort: String?): PageRequest {
val sortCriteria = if (sort.isNullOrBlank()) {
null
} else {
- Sort.by(Sort.Direction.fromOptionalString(sort.substring(0, 1)).orElse(Sort.DEFAULT_DIRECTION), sort.substring(1))
+ val direction = if (sort.substring(0, 1) == "+") {
+ Direction.ASC
+ } else {
+ Direction.DESC
+ }
+ Sort.by(direction, sort.substring(1))
}
return if (sortCriteria != null) {
PageRequest.of(page, size, sortCriteria)
@@ -75,37 +92,81 @@ fun pageRequest(page: Int, size: Int, sort: String?): PageRequest {
}
}
+/**
+ * Map sort string from the view API to implementation sort of the entities.
+ */
+fun PageableSortableQuery.mapTaskSort(): String {
+ return if (this.sort == null) {
+ // no sort is specified, we don't want unsorted results.
+ "-${TaskEntity::createdDate.name}"
+ } else {
+ val direction = sort!!.substring(0, 1)
+ val field = sort!!.substring(1)
+ return when (field) {
+ Task::name.name -> TaskEntity::name.name
+ Task::description.name -> TaskEntity::description.name
+ Task::assignee.name -> TaskEntity::assignee.name
+ Task::createTime.name -> TaskEntity::createdDate.name
+ Task::dueDate.name -> TaskEntity::dueDate.name
+ Task::followUpDate.name -> TaskEntity::followUpDate.name
+ Task::owner.name -> TaskEntity::owner.name
+ Task::priority.name -> TaskEntity::priority.name
+ Task::formKey.name -> TaskEntity::formKey.name
+ Task::businessKey.name -> TaskEntity::businessKey.name
+ Task::id.name -> TaskEntity::taskId.name
+ Task::taskDefinitionKey.name -> TaskEntity::taskDefinitionKey.name
+ else -> throw IllegalArgumentException("'$field' is not supported for sorting in JPA View")
+ }.let { "$direction$it" }
+ }
+}
+
/**
* Specification for query on task attributes.
*/
internal fun List.toTaskAttributeSpecification(): Specification? {
- val relevant = this.filterIsInstance().map { it.toTaskSpecification() }
- return composeAnd(relevant)
+ val relevant = this.filterIsInstance()
+ // compose criteria with same name with OR and criteria with different names with AND
+ val relevantByName = relevant.groupBy { it.name }
+ val orComposedByName = relevantByName.map { (_, criteria) -> composeOr(criteria.map { it.toTaskSpecification() }) }
+
+ return composeAnd(orComposedByName)
}
/**
* Specification for query on data entry attributes.
*/
internal fun List.toDataEntryAttributeSpecification(): Specification? {
- val relevant = this.filterIsInstance().map { it.toDataEntrySpecification() }
- return composeAnd(relevant)
+ val relevant = this.filterIsInstance()
+ // compose criteria with same name with OR and criteria with different names with AND
+ val relevantByName = relevant.groupBy { it.name }
+ val orComposedByName = relevantByName.map { (_, criteria) -> composeOr(criteria.map { it.toDataEntrySpecification() }) }
+
+ return composeAnd(orComposedByName)
}
/**
* Specification on payload.
*/
internal fun List.toDataEntryPayloadSpecification(): Specification? {
- val relevant = this.filterIsInstance().map { it.toDataEntrySpecification() }
- return composeAnd(relevant)
+ val relevant = this.filterIsInstance()
+ // compose criteria with same name with OR and criteria with different names with AND
+ val relevantByName = relevant.groupBy { it.name }
+ val orComposedByName = relevantByName.map { (_, criteria) -> criteria.toOrDataEntrySpecification() }
+
+ return composeAnd(orComposedByName)
}
/**
* Specification on payload.
*/
internal fun List.toTaskPayloadSpecification(): Specification? {
- val relevant = this.filterIsInstance().map { it.toTaskSpecification() }
- return composeAnd(relevant)
+ val relevant = this.filterIsInstance()
+ // compose criteria with same name with OR and criteria with different names with AND
+ val relevantByName = relevant.groupBy { it.name }
+ val orComposedByName = relevantByName.map { (_, criteria) -> criteria.toOrTaskSpecification() }
+
+ return composeAnd(orComposedByName)
}
@@ -116,30 +177,56 @@ internal fun Criterion.TaskCriterion.toTaskSpecification(): Specification {
when (this.name) {
- TaskEntity::businessKey.name -> hasBusinessKey(this.value)
+ "processName" -> hasProcessName(this.value)
+ Task::businessKey.name -> hasBusinessKey(this.value)
+ Task::dueDate.name -> hasDueDate(Instant.parse(this.value))
+ Task::followUpDate.name -> hasFollowUpDate(Instant.parse(this.value))
+ Task::priority.name -> hasPriority(this.value.toInt())
else -> throw IllegalArgumentException("JPA View found unsupported task attribute for equals comparison: ${this.name}.")
}
}
+
LIKE -> {
when (this.name) {
- TaskEntity::name.name -> likeName(this.value)
- TaskEntity::description.name -> likeDescription(this.value)
- TaskEntity::businessKey.name -> likeBusinessKey(this.value)
+ "textSearch" -> likeTextSearch(this.value)
+ Task::name.name -> likeName(this.value)
+ Task::description.name -> likeDescription(this.value)
+ "processName" -> likeProcessName(this.value)
+ Task::businessKey.name -> likeBusinessKey(this.value)
else -> throw IllegalArgumentException("JPA View found unsupported task attribute for like comparison: ${this.name}.")
}
}
+
+ LESS -> {
+ when (this.name) {
+ Task::dueDate.name -> hasDueDateBefore(Instant.parse(this.value))
+ Task::followUpDate.name -> hasFollowUpDateBefore(Instant.parse(this.value))
+ else -> throw IllegalArgumentException("JPA View found unsupported task attribute for < comparison: ${this.name}.")
+ }
+ }
+
+ GREATER -> {
+ when (this.name) {
+ Task::dueDate.name -> hasDueDateAfter(Instant.parse(this.value))
+ Task::followUpDate.name -> hasFollowUpDateAfter(Instant.parse(this.value))
+ else -> throw IllegalArgumentException("JPA View found unsupported task attribute for > comparison: ${this.name}.")
+ }
+ }
+
else -> throw IllegalArgumentException("JPA View found unsupported comparison ${this.operator} for attribute ${this.name}.")
}
}
/**
- * Creates JPA Specification for query of payload attributes based on JSON paths.
+ * Creates JPA Specification for query of payload attributes based on JSON paths. All criteria must have the same path
+ * and will be composed by the logical OR operator.
*/
-internal fun Criterion.PayloadEntryCriterion.toTaskSpecification(): Specification {
- return when (this.operator) {
- EQUALS -> hasTaskPayloadAttribute(this.name, this.value)
- else -> throw IllegalArgumentException("JPA View currently supports only equals as operator for filtering of payload attributes.")
- }
+internal fun List.toOrTaskSpecification(): Specification {
+ require(this.isNotEmpty()) { "List of criteria must not be empty." }
+ require(this.all { it.operator == EQUALS }) { "JPA View currently supports only equals as operator for filtering of payload attributes." }
+ require(this.distinctBy { it.name }.size == 1) { "All criteria must have the same path." }
+
+ return hasTaskPayloadAttribute(this.first().name, this.map { it.value })
}
/**
@@ -157,13 +244,15 @@ internal fun Criterion.DataEntryCriterion.toDataEntrySpecification(): Specificat
}
/**
- * Creates JPA Specification for query of payload attributes based on JSON paths.
+ * Creates JPA Specification for query of payload attributes based on JSON paths. All criteria must have the same path
+ * and will be composed by the logical OR operator.
*/
-internal fun Criterion.PayloadEntryCriterion.toDataEntrySpecification(): Specification {
- return when (this.operator) {
- EQUALS -> hasDataEntryPayloadAttribute(this.name, this.value)
- else -> throw IllegalArgumentException("JPA View currently supports only equals as operator for filtering of payload attributes.")
- }
+internal fun List.toOrDataEntrySpecification(): Specification {
+ require(this.isNotEmpty()) { "List of criteria must not be empty." }
+ require(this.all { it.operator == EQUALS }) { "JPA View currently supports only equals as operator for filtering of payload attributes." }
+ require(this.distinctBy { it.name }.size == 1) { "All criteria must have the same path." }
+
+ return hasDataEntryPayloadAttribute(this.first().name, this.map { it.value })
}
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryEntity.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryEntity.kt
index 23b38f495..d37f65b96 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryEntity.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryEntity.kt
@@ -2,6 +2,8 @@ package io.holunda.polyflow.view.jpa.data
import io.holunda.polyflow.view.jpa.payload.PayloadAttribute
+import org.hibernate.annotations.Fetch
+import org.hibernate.annotations.FetchMode
import java.time.Instant
import javax.persistence.*
@@ -13,21 +15,19 @@ import javax.persistence.*
class DataEntryEntity(
@EmbeddedId
var dataEntryId: DataEntryId,
- @Column(name = "TYPE", nullable = false)
+ @Column(name = "TYPE", length = 255, nullable = false)
var type: String,
- @Column(name = "NAME", nullable = false)
- @Lob
+ @Column(name = "NAME", length = 255, nullable = false)
var name: String,
- @Column(name = "APPLICATION_NAME", nullable = false)
+ @Column(name = "APPLICATION_NAME", length = 64, nullable = false)
var applicationName: String,
- @Column(name = "FORM_KEY")
+ @Column(name = "FORM_KEY", length = 64, nullable = true)
var formKey: String? = null,
@Column(name = "REVISION")
var revision: Long = 0L,
@Embedded
var state: DataEntryStateEmbeddable,
- @Column(name = "DESCRIPTION")
- @Lob
+ @Column(name = "DESCRIPTION", nullable = true, length = 2048)
var description: String? = null,
@Column(name = "DATE_CREATED", nullable = false)
@@ -60,6 +60,7 @@ class DataEntryEntity(
@OneToMany(mappedBy = "dataEntry", orphanRemoval = true, cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
+ @Fetch(FetchMode.SELECT)
var protocol: MutableList = mutableListOf(),
@Column(name = "PAYLOAD")
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryId.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryId.kt
index a6e143968..3228cde99 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryId.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryId.kt
@@ -10,9 +10,9 @@ import javax.persistence.Embeddable
*/
@Embeddable
class DataEntryId(
- @Column(name = "ENTRY_ID", nullable = false)
+ @Column(name = "ENTRY_ID", length = 64, nullable = false)
var entryId: String,
- @Column(name = "ENTRY_TYPE", nullable = false)
+ @Column(name = "ENTRY_TYPE", length = 128, nullable = false)
var entryType: String
) : Serializable {
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryRepository.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryRepository.kt
index 1c6380ae2..2d7354686 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryRepository.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryRepository.kt
@@ -84,20 +84,26 @@ interface DataEntryRepository : CrudRepository, Jp
}
/**
- * Specification for checking the payload attribute.
+ * Specification for checking the payload attribute. If multiple values are given, one of them must match.
+ * payload.name = ? AND (payload.value = ? OR payload.value = ? OR ...)
*/
- fun hasDataEntryPayloadAttribute(name: String, value: String): Specification =
- Specification { dataEntry, _, builder ->
+ fun hasDataEntryPayloadAttribute(name: String, values: List): Specification =
+ Specification { dataEntry, query, builder ->
+ query.distinct(true)
val join = dataEntry.join>(DataEntryEntity::payloadAttributes.name)
val pathEquals = builder.equal(
join.get(PayloadAttribute::path.name),
name
)
- val valueEquals = builder.equal(
- join.get(PayloadAttribute::value.name),
- value
- )
- builder.and(pathEquals, valueEquals)
+
+ val valueAnyOf = values.map {
+ builder.equal(
+ join.get(PayloadAttribute::value.name),
+ it
+ )
+ }.let { builder.or(*it.toTypedArray()) }
+
+ builder.and(pathEquals, valueAnyOf)
}
/**
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryStateEmbeddable.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryStateEmbeddable.kt
index 8a9ef3e05..12e5cd1e3 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryStateEmbeddable.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/DataEntryStateEmbeddable.kt
@@ -10,9 +10,9 @@ import javax.persistence.Embeddable
*/
@Embeddable
class DataEntryStateEmbeddable(
- @Column(name = "PROCESSING_TYPE", nullable = false)
+ @Column(name = "PROCESSING_TYPE", length = 64, nullable = false)
var processingType: String,
- @Column(name = "STATE", nullable = false)
+ @Column(name = "STATE", length = 64, nullable = false)
var state: String
) : Serializable {
companion object {
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/ProtocolElement.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/ProtocolElement.kt
index 7b2de164f..129b45975 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/ProtocolElement.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/data/ProtocolElement.kt
@@ -11,18 +11,18 @@ import javax.persistence.*
@Table(name = "PLF_DATA_ENTRY_PROTOCOL")
class ProtocolElement(
@Id
- @Column(name = "ID")
+ @Column(name = "ID", length = 64, nullable = false)
var id: String = UUID.randomUUID().toString(),
@Column(name = "TIME", nullable = false)
var time: Instant = Instant.now(),
@Embedded
var state: DataEntryStateEmbeddable,
- @Column(name = "USERNAME", nullable = true)
+ @Column(name = "USERNAME", length = 64, nullable = true)
var username: String? = null,
- @Column(name = "LOG_MESSAGE", nullable = true)
+ @Column(name = "LOG_MESSAGE", length = 2048, nullable = true)
var logMessage: String? = null,
- @Column(name = "LOG_DETAILS", nullable = true)
+ @Column(name = "LOG_DETAILS", length = 2048, nullable = true)
var logDetails: String? = null,
@ManyToOne
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessDefinitionEntity.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessDefinitionEntity.kt
index d723e1d86..f1b9fc495 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessDefinitionEntity.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessDefinitionEntity.kt
@@ -9,20 +9,19 @@ import javax.persistence.*
@Table(name = "PLF_PROC_DEF")
class ProcessDefinitionEntity(
@Id
- @Column(name = "PROC_DEF_ID")
+ @Column(name = "PROC_DEF_ID", length = 255)
var processDefinitionId: String,
- @Column(name = "PROC_DEF_KEY", nullable = false)
+ @Column(name = "PROC_DEF_KEY", length = 255, nullable = false)
var processDefinitionKey: String,
@Column(name = "PROC_DEF_VERSION", nullable = false)
var processDefinitionVersion: Int,
- @Column(name = "APPLICATION_NAME", nullable = false)
+ @Column(name = "APPLICATION_NAME", length = 64, nullable = false)
var applicationName: String,
- @Column(name = "NAME", nullable = false)
+ @Column(name = "NAME", length = 255, nullable = false)
var name: String,
@Column(name = "VERSION_TAG")
var versionTag: String? = null,
- @Column(name = "DESCRIPTION")
- @Lob
+ @Column(name = "DESCRIPTION", length = 2048)
var description: String? = null,
@Column(name = "START_FORM_KEY")
var startFormKey: String? = null,
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessInstanceEntity.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessInstanceEntity.kt
index c3d5222e8..95e38d83a 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessInstanceEntity.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/ProcessInstanceEntity.kt
@@ -10,23 +10,23 @@ import javax.persistence.*
@Table(name = "PLF_PROC_INSTANCE")
class ProcessInstanceEntity(
@Id
- @Column(name = "INSTANCE_ID", nullable = true)
+ @Column(name = "INSTANCE_ID", length = 64, nullable = true)
var processInstanceId: String,
- @Column(name = "BUSINESS_KEY", nullable = true)
+ @Column(name = "BUSINESS_KEY", length = 64, nullable = true)
var businessKey: String?,
- @Column(name = "SUPER_INSTANCE_ID", nullable = true)
+ @Column(name = "SUPER_INSTANCE_ID", length = 64, nullable = true)
var superInstanceId: String?,
- @Column(name = "START_ACTIVITY_ID", nullable = true)
+ @Column(name = "START_ACTIVITY_ID", length = 64, nullable = true)
var startActivityId: String?,
- @Column(name = "END_ACTIVITY_ID", nullable = true)
+ @Column(name = "END_ACTIVITY_ID", length = 64, nullable = true)
var endActivityId: String?,
- @Column(name = "DELETE_REASON", nullable = true)
+ @Column(name = "DELETE_REASON", length = 2048, nullable = true)
var deleteReason: String?,
- @Column(name = "START_USER_ID", nullable = true)
+ @Column(name = "START_USER_ID", length = 64, nullable = true)
var startUserId: String?,
@Embedded
var sourceReference: SourceReferenceEmbeddable,
- @Column(name = "RUN_STATE", nullable = false)
+ @Column(name = "RUN_STATE", length = 64, nullable = false)
@Enumerated(EnumType.STRING)
var state: ProcessInstanceState
)
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/SourceReferenceEmbeddable.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/SourceReferenceEmbeddable.kt
index 72dddecbc..50f177bba 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/SourceReferenceEmbeddable.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/process/SourceReferenceEmbeddable.kt
@@ -9,21 +9,21 @@ import javax.persistence.Embeddable
*/
@Embeddable
class SourceReferenceEmbeddable(
- @Column(name = "SOURCE_INSTANCE_ID", nullable = false)
+ @Column(name = "SOURCE_INSTANCE_ID", length = 64, nullable = false)
var instanceId: String,
- @Column(name = "SOURCE_EXECUTION_ID", nullable = false)
+ @Column(name = "SOURCE_EXECUTION_ID", length = 64, nullable = false)
var executionId: String,
- @Column(name = "SOURCE_DEF_ID", nullable = false)
+ @Column(name = "SOURCE_DEF_ID", length = 64, nullable = false)
var definitionId: String,
- @Column(name = "SOURCE_DEF_KEY", nullable = false)
+ @Column(name = "SOURCE_DEF_KEY", length = 64, nullable = false)
var definitionKey: String,
- @Column(name = "SOURCE_NAME", nullable = false)
+ @Column(name = "SOURCE_NAME", length = 255, nullable = false)
var name: String,
- @Column(name = "APPLICATION_NAME", nullable = false)
+ @Column(name = "APPLICATION_NAME", length = 64, nullable = false)
var applicationName: String,
- @Column(name = "SOURCE_TENANT_ID", nullable = true)
+ @Column(name = "SOURCE_TENANT_ID", length = 64, nullable = true)
var tenantId: String? = null,
- @Column(name = "SOURCE_TYPE", nullable = false)
+ @Column(name = "SOURCE_TYPE", length = 64, nullable = false)
var sourceType: String
) : Serializable {
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskEntity.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskEntity.kt
index 4fc26282b..cf62c5f8c 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskEntity.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskEntity.kt
@@ -14,12 +14,11 @@ import javax.persistence.*
@Table(name = "PLF_TASK")
class TaskEntity(
@Id
- @Column(name = "TASK_ID", nullable = false)
+ @Column(name = "TASK_ID", length = 64, nullable = false)
var taskId: String,
@Column(name = "TASK_DEF_KEY", nullable = false)
var taskDefinitionKey: String,
- @Lob
- @Column(name = "NAME", nullable = false)
+ @Column(name = "NAME", length = 128, nullable = false)
var name: String,
@Column(name = "PRIORITY")
var priority: Int,
@@ -50,22 +49,21 @@ class TaskEntity(
]
)
var payloadAttributes: MutableSet = mutableSetOf(),
- @Column(name = "BUSINESS_KEY")
+ @Column(name = "BUSINESS_KEY", length = 255, nullable = true)
var businessKey: String? = null,
- @Lob
- @Column(name = "DESCRIPTION")
+ @Column(name = "DESCRIPTION", length = 2048, nullable = true)
var description: String? = null,
@Column(name = "FORM_KEY")
var formKey: String? = null,
@Column(name = "DATE_CREATED", nullable = false)
var createdDate: Instant = Instant.now(),
- @Column(name = "DATE_DUE")
+ @Column(name = "DATE_DUE", nullable = true)
var dueDate: Instant? = null,
- @Column(name = "DATE_FOLLOW_UP")
+ @Column(name = "DATE_FOLLOW_UP", nullable = true)
var followUpDate: Instant? = null,
- @Column(name = "OWNER_ID")
+ @Column(name = "OWNER_ID", length = 64, nullable = true)
var owner: String? = null,
- @Column(name = "ASSIGNEE_ID")
+ @Column(name = "ASSIGNEE_ID", length = 64, nullable = true)
var assignee: String? = null,
@Column(name = "PAYLOAD")
@Lob
diff --git a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskRepository.kt b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskRepository.kt
index b9fc9ab6c..2cb069c33 100644
--- a/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskRepository.kt
+++ b/view/jpa/src/main/kotlin/io/holunda/polyflow/view/jpa/task/TaskRepository.kt
@@ -1,13 +1,13 @@
package io.holunda.polyflow.view.jpa.task
import io.holunda.polyflow.view.jpa.auth.AuthorizationPrincipal
-import io.holunda.polyflow.view.jpa.data.DataEntryEntity
import io.holunda.polyflow.view.jpa.composeOr
import io.holunda.polyflow.view.jpa.payload.PayloadAttribute
import io.holunda.polyflow.view.jpa.process.SourceReferenceEmbeddable
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.data.repository.CrudRepository
+import java.time.Instant
/**
* Repository for accessing tasks.
@@ -27,6 +27,18 @@ interface TaskRepository : CrudRepository, JpaSpecificationE
builder.isNull(task.get(TaskEntity::assignee.name))
}
}
+
+ /**
+ * Is assignee set to specified user.
+ */
+ fun isAssignedTo(assignee: String): Specification =
+ Specification { task, _, builder ->
+ builder.equal(
+ task.get(TaskEntity::assignee.name),
+ assignee
+ )
+ }
+
/**
* Specification for checking authorization of multiple principals.
*/
@@ -50,6 +62,18 @@ interface TaskRepository : CrudRepository, JpaSpecificationE
application
)
}
+
+ /**
+ * Specification for checking the process name.
+ */
+ fun hasProcessName(processName: String): Specification =
+ Specification { task, _, builder ->
+ builder.equal(
+ task.get(TaskEntity::sourceReference.name).get(SourceReferenceEmbeddable::name.name),
+ processName
+ )
+ }
+
/**
* Specification for checking the business key.
*/
@@ -61,14 +85,105 @@ interface TaskRepository : CrudRepository, JpaSpecificationE
)
}
+ /**
+ * Specification for checking the priority.
+ */
+ fun hasPriority(priority: Int): Specification =
+ Specification { task, _, builder ->
+ builder.equal(
+ task.get(TaskEntity::priority.name),
+ priority
+ )
+ }
+
+ /**
+ * Specification for checking the due date.
+ */
+ fun hasDueDate(dueDate: Instant): Specification =
+ Specification { task, _, builder ->
+ builder.equal(
+ task.get(TaskEntity::dueDate.name),
+ dueDate
+ )
+ }
+
+ /**
+ * Specification for checking the due date.
+ */
+ fun hasDueDateBefore(dueDate: Instant): Specification =
+ Specification { task, _, builder ->
+ builder.or(
+ builder.isNull(task.get(TaskEntity::followUpDate.name)),
+ builder.lessThan(
+ task.get(TaskEntity::dueDate.name),
+ dueDate
+ )
+ )
+ }
+
+ /**
+ * Specification for checking the due date.
+ */
+ fun hasDueDateAfter(dueDate: Instant): Specification =
+ Specification { task, _, builder ->
+ builder.or(
+ builder.isNull(task.get(TaskEntity::followUpDate.name)),
+ builder.greaterThan(
+ task.get(TaskEntity::dueDate.name),
+ dueDate
+ )
+ )
+ }
+
+ /**
+ * Specification for checking the follow-up date.
+ */
+ fun hasFollowUpDate(followUpDate: Instant): Specification =
+ Specification { task, _, builder ->
+ builder.equal(
+ task.get(TaskEntity::followUpDate.name),
+ followUpDate
+ )
+ }
+
+ /**
+ * Specification for checking the follow-up date.
+ */
+ fun hasFollowUpDateBefore(followUpDate: Instant): Specification =
+ Specification { task, _, builder ->
+ builder.or(
+ builder.isNull(task.get(TaskEntity::followUpDate.name)),
+ builder.lessThan(
+ task.get(TaskEntity::followUpDate.name),
+ followUpDate
+ )
+ )
+ }
+
+ /**
+ * Specification for checking the follow-up date.
+ */
+ fun hasFollowUpDateAfter(followUpDate: Instant): Specification =
+ Specification { task, _, builder ->
+ builder.or(
+ builder.isNull(task.get(TaskEntity::followUpDate.name)),
+ builder.greaterThan(
+ task.get(TaskEntity::followUpDate.name),
+ followUpDate
+ )
+ )
+ }
+
/**
* Specification for checking the name likeness.
*/
fun likeName(pattern: String): Specification =
Specification { task, _, builder ->
builder.like(
- task.get(TaskEntity::name.name),
- pattern
+ builder.lower(
+ task.get(TaskEntity::name.name)
+ ),
+ "%${pattern.lowercase()}%"
)
}
@@ -78,8 +193,10 @@ interface TaskRepository : CrudRepository, JpaSpecificationE
fun likeDescription(pattern: String): Specification =
Specification { task, _, builder ->
builder.like(
- task.get(TaskEntity::description.name),
- pattern
+ builder.lower(
+ task.get(TaskEntity::description.name)
+ ),
+ "%${pattern.lowercase()}%"
)
}
@@ -89,27 +206,55 @@ interface TaskRepository : CrudRepository, JpaSpecificationE
fun likeBusinessKey(pattern: String): Specification =
Specification { task, _, builder ->
builder.like(
- task.get(TaskEntity::description.name),
- pattern
+ builder.lower(
+ task.get(TaskEntity::businessKey.name)
+ ),
+ "%${pattern.lowercase()}%"
)
}
-
/**
- * Specification for checking the payload attribute.
+ * Specification for checking the process name likeness.
*/
- fun hasTaskPayloadAttribute(name: String, value: String): Specification =
+ fun likeProcessName(pattern: String): Specification =
Specification { task, _, builder ->
- val join = task.join>(TaskEntity::payloadAttributes.name)
+ builder.like(
+ builder.lower(
+ task.get(TaskEntity::sourceReference.name).get(SourceReferenceEmbeddable::name.name)
+ ),
+ "%${pattern.lowercase()}%"
+ )
+ }
+
+ /**
+ * Specification for checking likeness of multiple task attributes combined by an OR operator.
+ */
+ fun likeTextSearch(pattern: String): Specification =
+ likeName(pattern)
+ .or(likeDescription(pattern))
+ .or(likeProcessName(pattern))
+
+ /**
+ * Specification for checking the payload attribute of a task. If multiple values are given, one of them must match.
+ * payload.name = ? AND (payload.value = ? OR payload.value = ? OR ...)
+ */
+ fun hasTaskPayloadAttribute(name: String, values: List): Specification =
+ Specification { task, query, builder ->
+ query.distinct(true)
+ val join = task.join>(TaskEntity::payloadAttributes.name)
val pathEquals = builder.equal(
join.get(PayloadAttribute::path.name),
name
)
- val valueEquals = builder.equal(
- join.get(PayloadAttribute::value.name),
- value
- )
- builder.and(pathEquals, valueEquals)
+
+ val valueAnyOf = values.map {
+ builder.equal(
+ join.get(PayloadAttribute::value.name),
+ it
+ )
+ }.let { builder.or(*it.toTypedArray()) }
+
+ builder.and(pathEquals, valueAnyOf)
}
}
}
diff --git a/view/jpa/src/sql/persistence.xml b/view/jpa/src/sql/persistence.xml
index 309ea21be..08eb446de 100644
--- a/view/jpa/src/sql/persistence.xml
+++ b/view/jpa/src/sql/persistence.xml
@@ -1,8 +1,8 @@
+