From 1679a8389117ca923f483f4d8625fbda49511b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pere=20Fern=C3=A1ndez?= Date: Thu, 24 Oct 2024 18:51:11 +0200 Subject: [PATCH 01/16] [incubator-kie-issues#1545]: Create JPA persistence layer for Human Tasks (#3735) * incubator-kie-issues#1545: Create JPA persistence layer for Human Tasks * - testing * - springboot and it tests * - cleanup * - fix query * - fix packages *- fix it tests endpoints * - increase priority --- .../common/jbpm-usertask-storage-jpa/pom.xml | 73 +++ .../usertask/jpa/JPAUserTaskInstances.java | 129 +++++ .../jpa/mapper/AttachmentsEntityMapper.java | 81 +++ .../jpa/mapper/CommentsEntityMapper.java | 78 +++ .../jpa/mapper/TaskInputsEntityMapper.java | 77 +++ .../jpa/mapper/TaskMetadataEntityMapper.java | 74 +++ .../jpa/mapper/TaskOutputsEntityMapper.java | 77 +++ .../mapper/UserTaskInstanceEntityMapper.java | 101 ++++ .../jpa/mapper/json/utils/JSONUtils.java | 54 ++ .../usertask/jpa/model/AttachmentEntity.java | 95 ++++ .../usertask/jpa/model/CommentEntity.java | 85 ++++ .../usertask/jpa/model/TaskDataEntity.java | 91 ++++ .../usertask/jpa/model/TaskDataEntityPK.java | 76 +++ .../usertask/jpa/model/TaskInputEntity.java | 34 ++ .../jpa/model/TaskMetadataEntity.java | 35 ++ .../usertask/jpa/model/TaskOutputEntity.java | 35 ++ .../jpa/model/UserTaskInstanceEntity.java | 334 +++++++++++++ .../jpa/repository/AttachmentRepository.java | 34 ++ .../jpa/repository/BaseRepository.java | 63 +++ .../jpa/repository/CommentRepository.java | 34 ++ .../jpa/repository/TaskInputRepository.java | 35 ++ .../repository/TaskMetadataRepository.java | 35 ++ .../jpa/repository/TaskOutputRepository.java | 35 ++ .../UserTaskInstanceRepository.java | 48 ++ .../jpa/repository/UserTaskJPAContext.java | 27 + .../src/main/resources/META-INF/beans.xml | 0 .../resources/META-INF/kie-flyway.properties | 23 + .../h2/V1.0.0__jBPM_user_task_create.sql | 166 +++++++ .../V1.0.0__jBPM_user_task_create.sql | 164 +++++++ .../jpa/JPAUserTaskInstancesTest.java | 220 +++++++++ .../mapper/AttachmentsEntityMapperTest.java | 135 +++++ .../jpa/mapper/CommentsEntityMapperTest.java | 145 ++++++ .../mapper/TaskInputsEntityMapperTest.java | 144 ++++++ .../mapper/TaskMetadataEntityMapperTest.java | 145 ++++++ .../mapper/TaskOutputsEntityMapperTest.java | 143 ++++++ .../UserTaskInstanceEntityMapperTest.java | 101 ++++ .../usertask/jpa/mapper/models/Person.java | 76 +++ .../usertask/jpa/mapper/utils/TestUtils.java | 317 ++++++++++++ addons/common/pom.xml | 1 + .../kie/kogito/auth/IdentityProviders.java | 5 +- .../kie/kogito/usertask/UserTaskInstance.java | 2 +- .../kogito/usertask/UserTaskInstances.java | 2 +- .../UserTaskKogitoWorkItemHandler.java | 13 +- ...kKogitoWorkItemHandlerProcessListener.java | 1 + .../usertask/impl/AbstractUserTask.java | 3 +- .../impl/DefaultUserTaskInstance.java | 34 +- .../impl/InMemoryUserTaskInstances.java | 9 +- .../lifecycle/DefaultUserTaskLifeCycle.java | 6 +- .../DefaultUserTaskTransitionToken.java | 3 +- kogito-bom/pom.xml | 77 ++- kogito-build/kogito-dependencies-bom/pom.xml | 8 + quarkus/addons/flyway/deployment/pom.xml | 1 + .../deployment/pom.xml | 110 +++++ ...MUserTaskStorageJPAExtensionProcessor.java | 40 ++ .../addons/jbpm-usertask-storage-jpa/pom.xml | 45 ++ .../jbpm-usertask-storage-jpa/runtime/pom.xml | 170 +++++++ .../quarkus/QuarkusJPAUserTaskInstances.java | 42 ++ .../QuarkusAttachmentsEntityMapper.java | 39 ++ .../mapper/QuarkusCommentsEntityMapper.java | 39 ++ .../mapper/QuarkusTaskInputsEntityMapper.java | 39 ++ .../QuarkusTaskMetadataEntityMapper.java | 39 ++ .../QuarkusTaskOutputsEntityMapper.java | 39 ++ .../QuarkusUserTaskInstanceEntityMapper.java | 39 ++ .../QuarkusAttachmentRepository.java | 41 ++ .../repository/QuarkusCommentRepository.java | 41 ++ .../QuarkusTaskInputEntityRepository.java | 40 ++ .../QuarkusTaskMetadataEntityRepository.java | 40 ++ .../QuarkusTaskOutputEntityRepository.java | 40 ++ .../QuarkusUserTaskInstanceRepository.java | 41 ++ .../repository/QuarkusUserTaskJPAContext.java | 40 ++ .../src/main/resources/META-INF/beans.xml | 20 + .../resources/META-INF/quarkus-extension.yaml | 37 ++ .../BaseQuarkusJPAUserTaskInstancesTest.java | 462 ++++++++++++++++++ .../H2QuarkusJPAUserTaskInstancesTest.java | 33 ++ .../jpa/quarkus/H2QuarkusTestProfile.java | 30 ++ ...greSQLQuarkusJPAUserTaskInstancesTest.java | 35 ++ .../quarkus/PostgreSQLQuarkusTestProfile.java | 30 ++ .../src/test/resources/META-INF/beans.xml | 0 .../src/test/resources/application.properties | 28 ++ quarkus/addons/pom.xml | 1 + .../pom.xml | 146 ++++++ .../main/java/org/acme/travels/Address.java | 76 +++ .../main/java/org/acme/travels/Traveller.java | 88 ++++ .../src/main/resources/application.properties | 24 + .../src/main/resources/approval.bpmn | 304 ++++++++++++ .../jbpm/userTask/jpa/it/BaseUserTaskIT.java | 69 +++ .../jpa/it/UserTaskAttachmentsIT.java | 212 ++++++++ .../userTask/jpa/it/UserTaskCommentsIT.java | 203 ++++++++ .../jbpm/userTask/jpa/it/UserTaskInputs.java | 107 ++++ .../userTask/jpa/it/UserTaskLifeCycleIT.java | 192 ++++++++ .../userTask/jpa/it/UserTaskOutputsIT.java | 109 +++++ quarkus/integration-tests/pom.xml | 1 + .../addons/jbpm-usertask-storage-jpa/pom.xml | 35 ++ .../SpringBootJPAUserTaskInstances.java | 37 ++ .../SpringBootAttachmentsEntityMapper.java | 34 ++ .../SpringBootCommentsEntityMapper.java | 34 ++ .../SpringBootTaskInputsEntityMapper.java | 34 ++ .../SpringBootTaskMetadataEntityMapper.java | 34 ++ .../SpringBootTaskOutputsEntityMapper.java | 34 ++ ...pringBootUserTaskInstanceEntityMapper.java | 34 ++ .../SpringBootAttachmentRepository.java | 36 ++ .../SpringBootCommentRepository.java | 36 ++ .../SpringBootTaskInputEntityRepository.java | 35 ++ ...pringBootTaskMetadataEntityRepository.java | 35 ++ .../SpringBootTaskOutputEntityRepository.java | 35 ++ .../SpringBootUserTaskInstanceRepository.java | 36 ++ .../SpringBootUserTaskJPAContext.java | 40 ++ .../src/main/resources/META-INF/beans.xml | 0 springboot/addons/pom.xml | 1 + .../pom.xml | 153 ++++++ .../main/java/org/acme/travels/Address.java | 76 +++ .../main/java/org/acme/travels/Traveller.java | 88 ++++ .../it/KogitoSpringbootApplication.java | 30 ++ .../src/main/resources/application.properties | 23 + .../src/main/resources/approval.bpmn | 304 ++++++++++++ .../jbpm/userTask/jpa/it/BaseUserTaskIT.java | 90 ++++ .../jpa/it/UserTaskAttachmentsIT.java | 215 ++++++++ .../userTask/jpa/it/UserTaskCommentsIT.java | 207 ++++++++ .../jbpm/userTask/jpa/it/UserTaskInputs.java | 110 +++++ .../userTask/jpa/it/UserTaskLifeCycleIT.java | 196 ++++++++ .../userTask/jpa/it/UserTaskOutputsIT.java | 112 +++++ springboot/integration-tests/pom.xml | 1 + 122 files changed, 8998 insertions(+), 52 deletions(-) create mode 100644 addons/common/jbpm-usertask-storage-jpa/pom.xml create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/JPAUserTaskInstances.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapper.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapper.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapper.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapper.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapper.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapper.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/json/utils/JSONUtils.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntity.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntityPK.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskInputEntity.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskMetadataEntity.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskOutputEntity.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/AttachmentRepository.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/BaseRepository.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/CommentRepository.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskInputRepository.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskMetadataRepository.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskOutputRepository.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskInstanceRepository.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskJPAContext.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/resources/META-INF/beans.xml create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/resources/META-INF/kie-flyway.properties create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/JPAUserTaskInstancesTest.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapperTest.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapperTest.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapperTest.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapperTest.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapperTest.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapperTest.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/models/Person.java create mode 100644 addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/utils/TestUtils.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/deployment/pom.xml create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/deployment/src/main/java/org/jbpm/usertask/storage/jpa/quarkus/deployment/JBPMUserTaskStorageJPAExtensionProcessor.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/pom.xml create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/pom.xml create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/QuarkusJPAUserTaskInstances.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusAttachmentsEntityMapper.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusCommentsEntityMapper.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskInputsEntityMapper.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskMetadataEntityMapper.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskOutputsEntityMapper.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusUserTaskInstanceEntityMapper.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusAttachmentRepository.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusCommentRepository.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskInputEntityRepository.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskMetadataEntityRepository.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskOutputEntityRepository.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskInstanceRepository.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskJPAContext.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/beans.xml create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/BaseQuarkusJPAUserTaskInstancesTest.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusJPAUserTaskInstancesTest.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusTestProfile.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusJPAUserTaskInstancesTest.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusTestProfile.java create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/resources/META-INF/beans.xml create mode 100644 quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/resources/application.properties create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/pom.xml create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Address.java create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Traveller.java create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/application.properties create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/approval.bpmn create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java create mode 100644 quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/pom.xml create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/SpringBootJPAUserTaskInstances.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootAttachmentsEntityMapper.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootCommentsEntityMapper.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskInputsEntityMapper.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskMetadataEntityMapper.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskOutputsEntityMapper.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootUserTaskInstanceEntityMapper.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootAttachmentRepository.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootCommentRepository.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskInputEntityRepository.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskMetadataEntityRepository.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskOutputEntityRepository.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskInstanceRepository.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskJPAContext.java create mode 100644 springboot/addons/jbpm-usertask-storage-jpa/src/main/resources/META-INF/beans.xml create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/pom.xml create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Address.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Traveller.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/kie/kogito/it/KogitoSpringbootApplication.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/application.properties create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/approval.bpmn create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java create mode 100644 springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java diff --git a/addons/common/jbpm-usertask-storage-jpa/pom.xml b/addons/common/jbpm-usertask-storage-jpa/pom.xml new file mode 100644 index 00000000000..dd28be272d2 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + org.kie + kogito-addons-common-parent + 999-SNAPSHOT + + + org.jbpm + jbpm-addons-usertask-storage-jpa + + jBPM :: Add-Ons :: User Task Storage JPA :: Common + jBPM Add-Ons User Task Storage JPA Common + + + UTF-8 + org.jbpm.usertask.storage.jpa + + + + + jakarta.persistence + jakarta.persistence-api + + + org.slf4j + slf4j-api + + + org.kie.kogito + kogito-api + + + org.kie.kogito + jbpm-usertask + + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + ch.qos.logback + logback-classic + test + + + org.assertj + assertj-core + test + + + + diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/JPAUserTaskInstances.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/JPAUserTaskInstances.java new file mode 100644 index 00000000000..d2f6a5e4cbf --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/JPAUserTaskInstances.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa; + +import java.util.*; +import java.util.function.Function; + +import org.jbpm.usertask.jpa.mapper.UserTaskInstanceEntityMapper; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.UserTaskInstanceRepository; +import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTaskInstances; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JPAUserTaskInstances implements UserTaskInstances { + public static final Logger LOGGER = LoggerFactory.getLogger(JPAUserTaskInstances.class); + + private final UserTaskInstanceRepository userTaskInstanceRepository; + private final UserTaskInstanceEntityMapper userTaskInstanceEntityMapper; + + private Function reconnectUserTaskInstance; + private Function disconnectUserTaskInstance; + + public JPAUserTaskInstances(UserTaskInstanceRepository userTaskInstanceRepository, UserTaskInstanceEntityMapper userTaskInstanceEntityMapper) { + this.userTaskInstanceRepository = userTaskInstanceRepository; + this.userTaskInstanceEntityMapper = userTaskInstanceEntityMapper; + } + + @Override + public Optional findById(String userTaskInstanceId) { + return this.userTaskInstanceRepository.findById(userTaskInstanceId) + .map(userTaskInstanceEntityMapper::mapTaskEntityToInstance) + .map(reconnectUserTaskInstance); + } + + @Override + public List findByIdentity(IdentityProvider identityProvider) { + return userTaskInstanceRepository.findByIdentity(identityProvider) + .stream() + .map(userTaskInstanceEntityMapper::mapTaskEntityToInstance) + .map(reconnectUserTaskInstance) + .toList(); + } + + @Override + public boolean exists(String userTaskInstanceId) { + return userTaskInstanceRepository.findById(userTaskInstanceId).isPresent(); + } + + @Override + public UserTaskInstance create(UserTaskInstance userTaskInstance) { + Optional optional = userTaskInstanceRepository.findById(userTaskInstance.getId()); + + if (optional.isPresent()) { + LOGGER.error("Cannot create userTaskInstance with id {}. Task Already exists.", userTaskInstance.getId()); + throw new IllegalArgumentException("Cannot create userTaskInstance with id " + userTaskInstance.getId() + ". Task Already exists."); + } + + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + entity.setId(userTaskInstance.getId()); + + this.userTaskInstanceRepository.persist(entity); + + userTaskInstanceEntityMapper.mapTaskInstanceToEntity(userTaskInstance, entity); + + return this.reconnectUserTaskInstance.apply(userTaskInstance); + } + + @Override + public UserTaskInstance update(UserTaskInstance userTaskInstance) { + + Optional optional = userTaskInstanceRepository.findById(userTaskInstance.getId()); + + if (optional.isEmpty()) { + LOGGER.error("Could not find userTaskInstance with id {}", userTaskInstance.getId()); + throw new RuntimeException("Could not find userTaskInstance with id " + userTaskInstance.getId()); + } + + UserTaskInstanceEntity userTaskInstanceEntity = optional.get(); + + userTaskInstanceEntityMapper.mapTaskInstanceToEntity(userTaskInstance, userTaskInstanceEntity); + + userTaskInstanceRepository.update(userTaskInstanceEntity); + + return userTaskInstance; + } + + @Override + public UserTaskInstance remove(UserTaskInstance userTaskInstance) { + Optional optional = userTaskInstanceRepository.findById(userTaskInstance.getId()); + + if (optional.isEmpty()) { + LOGGER.warn("Could not remove userTaskInstance with id {}, task cannot be found", userTaskInstance.getId()); + throw new RuntimeException("Could not remove userTaskInstance with id " + userTaskInstance.getId() + ", userTaskInstance cannot be found"); + } + + this.userTaskInstanceRepository.remove(optional.get()); + return this.disconnectUserTaskInstance.apply(userTaskInstance); + } + + @Override + public void setReconnectUserTaskInstance(Function reconnectUserTaskInstance) { + this.reconnectUserTaskInstance = reconnectUserTaskInstance; + } + + @Override + public void setDisconnectUserTaskInstance(Function disconnectUserTaskInstance) { + this.disconnectUserTaskInstance = disconnectUserTaskInstance; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapper.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapper.java new file mode 100644 index 00000000000..210553ed853 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapper.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.jbpm.usertask.jpa.model.AttachmentEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.AttachmentRepository; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.kie.kogito.usertask.model.Attachment; + +import static java.util.stream.Collectors.toCollection; + +public class AttachmentsEntityMapper { + private final AttachmentRepository repository; + + public AttachmentsEntityMapper(AttachmentRepository repository) { + this.repository = repository; + } + + public void mapInstanceToEntity(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + Collection toRemove = userTaskInstanceEntity.getAttachments() + .stream() + .filter(entity -> userTaskInstance.getAttachments().stream().noneMatch(attachment -> attachment.getId().equals(entity.getId()))) + .toList(); + + toRemove.forEach(attachment -> { + repository.remove(attachment); + userTaskInstanceEntity.removeAttachment(attachment); + }); + + userTaskInstance.getAttachments().forEach(attachment -> { + AttachmentEntity attachmentEntity = userTaskInstanceEntity.getAttachments().stream().filter(entity -> entity.getId().equals(attachment.getId())).findFirst().orElseGet(() -> { + AttachmentEntity entity = new AttachmentEntity(); + userTaskInstanceEntity.addAttachment(entity); + return entity; + }); + attachmentEntity.setId(attachment.getId()); + attachmentEntity.setUpdatedBy(attachment.getUpdatedBy()); + attachmentEntity.setName(attachment.getName()); + attachmentEntity.setUrl(attachment.getContent().toString()); + attachmentEntity.setUpdatedAt(attachment.getUpdatedAt()); + }); + } + + public void mapEntityToInstance(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + + List attachments = userTaskInstanceEntity.getAttachments().stream().map(attachmentEntity -> { + Attachment attachment = new Attachment(attachmentEntity.getId(), attachmentEntity.getUpdatedBy()); + attachment.setId(attachmentEntity.getId()); + attachment.setName(attachmentEntity.getName()); + attachment.setContent(URI.create(attachmentEntity.getUrl())); + attachment.setUpdatedAt(attachmentEntity.getUpdatedAt()); + return attachment; + }).collect(toCollection(ArrayList::new)); + + ((DefaultUserTaskInstance) userTaskInstance).setAttachments(attachments); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapper.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapper.java new file mode 100644 index 00000000000..b854ec3510a --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapper.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.jbpm.usertask.jpa.model.CommentEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.CommentRepository; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.kie.kogito.usertask.model.Comment; + +import static java.util.stream.Collectors.toCollection; + +public class CommentsEntityMapper { + + private final CommentRepository repository; + + public CommentsEntityMapper(CommentRepository repository) { + this.repository = repository; + } + + public void mapInstanceToEntity(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + Collection toRemove = userTaskInstanceEntity.getComments() + .stream() + .filter(entity -> userTaskInstance.getComments().stream().noneMatch(comment -> comment.getId().equals(entity.getId()))) + .toList(); + + toRemove.forEach(comment -> { + repository.remove(comment); + userTaskInstanceEntity.removeComment(comment); + }); + + userTaskInstance.getComments().forEach(comment -> { + CommentEntity commentEntity = userTaskInstanceEntity.getComments().stream().filter(entity -> entity.getId().equals(comment.getId())).findFirst().orElseGet(() -> { + CommentEntity entity = new CommentEntity(); + userTaskInstanceEntity.addComment(entity); + return entity; + }); + commentEntity.setId(comment.getId()); + commentEntity.setUpdatedBy(comment.getUpdatedBy()); + commentEntity.setComment(comment.getContent()); + commentEntity.setUpdatedAt(comment.getUpdatedAt()); + }); + } + + public void mapEntityToInstance(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + List comments = userTaskInstanceEntity.getComments().stream().map(commentEntity -> { + Comment comment = new Comment(commentEntity.getId(), commentEntity.getUpdatedBy()); + comment.setId(commentEntity.getId()); + comment.setContent(commentEntity.getComment()); + comment.setUpdatedAt(commentEntity.getUpdatedAt()); + return comment; + }).collect(toCollection(ArrayList::new)); + + ((DefaultUserTaskInstance) userTaskInstance).setComments(comments); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapper.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapper.java new file mode 100644 index 00000000000..12a6c50f986 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapper.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.jbpm.usertask.jpa.mapper.json.utils.JSONUtils; +import org.jbpm.usertask.jpa.model.TaskInputEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.TaskInputRepository; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; + +public class TaskInputsEntityMapper { + + private TaskInputRepository repository; + + public TaskInputsEntityMapper(TaskInputRepository repository) { + this.repository = repository; + } + + public void mapInstanceToEntity(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + Collection toRemove = userTaskInstanceEntity.getInputs() + .stream() + .filter(entity -> !userTaskInstance.getInputs().containsKey(entity.getName())) + .toList(); + + toRemove.forEach(input -> { + repository.remove(input); + userTaskInstanceEntity.removeInput(input); + }); + + userTaskInstance.getInputs().forEach((key, value) -> { + TaskInputEntity inputEntity = userTaskInstanceEntity.getInputs().stream().filter(entity -> entity.getName().equals(key)).findFirst().orElseGet(() -> { + TaskInputEntity entity = new TaskInputEntity(); + entity.setName(key); + userTaskInstanceEntity.addInput(entity); + return entity; + }); + inputEntity.setName(key); + if (Objects.nonNull(value)) { + inputEntity.setValue(JSONUtils.valueToString(value).getBytes(StandardCharsets.UTF_8)); + inputEntity.setJavaType(value.getClass().getName()); + } + }); + } + + public void mapEntityToInstance(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + Map inputs = new HashMap<>(); + userTaskInstanceEntity.getInputs().forEach(taskInputEntity -> { + String value = taskInputEntity.getValue() == null ? null : new String(taskInputEntity.getValue(), StandardCharsets.UTF_8); + inputs.put(taskInputEntity.getName(), JSONUtils.stringTreeToValue(value, taskInputEntity.getJavaType())); + }); + ((DefaultUserTaskInstance) userTaskInstance).setInputs(inputs); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapper.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapper.java new file mode 100644 index 00000000000..999c7357496 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapper.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.jbpm.usertask.jpa.mapper.json.utils.JSONUtils; +import org.jbpm.usertask.jpa.model.TaskMetadataEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.TaskMetadataRepository; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; + +public class TaskMetadataEntityMapper { + + private final TaskMetadataRepository repository; + + public TaskMetadataEntityMapper(TaskMetadataRepository repository) { + this.repository = repository; + } + + public void mapInstanceToEntity(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + Collection toRemove = userTaskInstanceEntity.getMetadata() + .stream() + .filter(entity -> !userTaskInstance.getMetadata().containsKey(entity.getName())) + .toList(); + + toRemove.forEach(metadata -> { + repository.remove(metadata); + userTaskInstanceEntity.removeMetadata(metadata); + }); + + userTaskInstance.getMetadata().forEach((key, value) -> { + TaskMetadataEntity metadataEntity = userTaskInstanceEntity.getMetadata().stream().filter(entity -> entity.getName().equals(key)).findFirst().orElseGet(() -> { + TaskMetadataEntity entity = new TaskMetadataEntity(); + userTaskInstanceEntity.addMetadata(entity); + return entity; + }); + metadataEntity.setName(key); + if (Objects.nonNull(value)) { + metadataEntity.setValue(JSONUtils.valueToString(value)); + metadataEntity.setJavaType(value.getClass().getName()); + } + }); + } + + public void mapEntityToInstance(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + Map metadata = new HashMap<>(); + userTaskInstanceEntity.getMetadata().forEach(metadataEntry -> { + metadata.put(metadataEntry.getName(), JSONUtils.stringTreeToValue(metadataEntry.getValue(), metadataEntry.getJavaType())); + }); + ((DefaultUserTaskInstance) userTaskInstance).setMetadata(metadata); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapper.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapper.java new file mode 100644 index 00000000000..1fa7d4ae282 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapper.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.jbpm.usertask.jpa.mapper.json.utils.JSONUtils; +import org.jbpm.usertask.jpa.model.TaskOutputEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.TaskOutputRepository; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; + +public class TaskOutputsEntityMapper { + + private final TaskOutputRepository repository; + + public TaskOutputsEntityMapper(TaskOutputRepository repository) { + this.repository = repository; + } + + public void mapInstanceToEntity(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + Collection toRemove = userTaskInstanceEntity.getOutputs() + .stream() + .filter(entity -> !userTaskInstance.getOutputs().containsKey(entity.getName())) + .toList(); + + toRemove.forEach(output -> { + repository.remove(output); + userTaskInstanceEntity.removeOutput(output); + }); + + userTaskInstance.getOutputs().forEach((key, value) -> { + TaskOutputEntity outputEntity = userTaskInstanceEntity.getOutputs().stream().filter(entity -> entity.getName().equals(key)).findFirst().orElseGet(() -> { + TaskOutputEntity entity = new TaskOutputEntity(); + entity.setName(key); + userTaskInstanceEntity.addOutput(entity); + return entity; + }); + outputEntity.setName(key); + if (Objects.nonNull(value)) { + outputEntity.setValue(JSONUtils.valueToString(value).getBytes(StandardCharsets.UTF_8)); + outputEntity.setJavaType(value.getClass().getName()); + } + }); + } + + public void mapEntityToInstance(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + Map outputs = new HashMap<>(); + userTaskInstanceEntity.getOutputs().forEach(taskOutputEntity -> { + String value = taskOutputEntity.getValue() == null ? null : new String(taskOutputEntity.getValue(), StandardCharsets.UTF_8); + outputs.put(taskOutputEntity.getName(), JSONUtils.stringTreeToValue(value, taskOutputEntity.getJavaType())); + }); + ((DefaultUserTaskInstance) userTaskInstance).setOutputs(outputs); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapper.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapper.java new file mode 100644 index 00000000000..e03125742ba --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapper.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.util.Set; + +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.kie.kogito.usertask.lifecycle.UserTaskState; + +public class UserTaskInstanceEntityMapper { + + private final AttachmentsEntityMapper attachmentsMapper; + private final CommentsEntityMapper commentMapper; + private final TaskInputsEntityMapper taskInputsMapper; + private final TaskOutputsEntityMapper taskOutputsMapper; + private final TaskMetadataEntityMapper taskMetadataMapper; + + public UserTaskInstanceEntityMapper(AttachmentsEntityMapper attachmentsMapper, CommentsEntityMapper commentsMapper, TaskMetadataEntityMapper taskMetadataMapper, + TaskInputsEntityMapper taskInputsMapper, TaskOutputsEntityMapper taskOutputMapper) { + this.attachmentsMapper = attachmentsMapper; + this.commentMapper = commentsMapper; + this.taskMetadataMapper = taskMetadataMapper; + this.taskInputsMapper = taskInputsMapper; + this.taskOutputsMapper = taskOutputMapper; + } + + public UserTaskInstanceEntity mapTaskInstanceToEntity(UserTaskInstance userTaskInstance, UserTaskInstanceEntity entity) { + entity.setId(userTaskInstance.getId()); + entity.setTaskName(userTaskInstance.getTaskName()); + entity.setTaskDescription(userTaskInstance.getTaskDescription()); + entity.setTaskPriority(userTaskInstance.getTaskPriority()); + entity.setStatus(userTaskInstance.getStatus().getName()); + entity.setTerminationType(userTaskInstance.getStatus().getTerminate() == null ? null : userTaskInstance.getStatus().getTerminate().name()); + entity.setExternalReferenceId(userTaskInstance.getExternalReferenceId()); + entity.setUserTaskId(userTaskInstance.getUserTaskId()); + + entity.setActualOwner(userTaskInstance.getActualOwner()); + entity.setPotentialUsers(Set.copyOf(userTaskInstance.getPotentialUsers())); + entity.setPotentialGroups(Set.copyOf(userTaskInstance.getPotentialGroups())); + entity.setAdminUsers(Set.copyOf(userTaskInstance.getAdminUsers())); + entity.setAdminGroups(Set.copyOf(userTaskInstance.getAdminGroups())); + entity.setExcludedUsers(Set.copyOf(userTaskInstance.getExcludedUsers())); + + attachmentsMapper.mapInstanceToEntity(userTaskInstance, entity); + commentMapper.mapInstanceToEntity(userTaskInstance, entity); + taskInputsMapper.mapInstanceToEntity(userTaskInstance, entity); + taskOutputsMapper.mapInstanceToEntity(userTaskInstance, entity); + taskMetadataMapper.mapInstanceToEntity(userTaskInstance, entity); + + return entity; + } + + public UserTaskInstance mapTaskEntityToInstance(UserTaskInstanceEntity entity) { + + DefaultUserTaskInstance instance = new DefaultUserTaskInstance(); + + instance.setId(entity.getId()); + instance.setUserTaskId(entity.getUserTaskId()); + instance.setExternalReferenceId(entity.getExternalReferenceId()); + instance.setTaskName(entity.getTaskName()); + instance.setTaskDescription(entity.getTaskDescription()); + instance.setTaskPriority(entity.getTaskPriority()); + + UserTaskState.TerminationType terminationType = entity.getTerminationType() == null ? null : UserTaskState.TerminationType.valueOf(entity.getTerminationType()); + instance.setStatus(UserTaskState.of(entity.getStatus(), terminationType)); + + instance.setActualOwner(entity.getActualOwner()); + instance.setPotentialUsers(Set.copyOf(entity.getPotentialUsers())); + instance.setPotentialGroups(Set.copyOf(entity.getPotentialGroups())); + instance.setAdminUsers(Set.copyOf(entity.getAdminUsers())); + instance.setAdminGroups(Set.copyOf(entity.getAdminGroups())); + instance.setExcludedUsers(Set.copyOf(entity.getExcludedUsers())); + + attachmentsMapper.mapEntityToInstance(entity, instance); + commentMapper.mapEntityToInstance(entity, instance); + taskInputsMapper.mapEntityToInstance(entity, instance); + taskOutputsMapper.mapEntityToInstance(entity, instance); + taskMetadataMapper.mapEntityToInstance(entity, instance); + + return instance; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/json/utils/JSONUtils.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/json/utils/JSONUtils.java new file mode 100644 index 00000000000..b7e33589800 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/mapper/json/utils/JSONUtils.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper.json.utils; + +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +public class JSONUtils { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + } + + public static String valueToString(Object value) { + try { + return OBJECT_MAPPER.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static Object stringTreeToValue(String value, String javaType) { + try { + if (Objects.isNull(value) || Objects.isNull(javaType)) { + return null; + } + return OBJECT_MAPPER.readValue(value, Class.forName(javaType)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java new file mode 100644 index 00000000000..a0bff107682 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import java.util.Date; + +import jakarta.persistence.*; + +@Entity +@Table(name = "jbpm_user_tasks_attachments") +public class AttachmentEntity { + + @Id + private String id; + + private String name; + + private String url; + + @Column(name = "updated_by") + private String updatedBy; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "updated_at") + private Date updatedAt; + + @ManyToOne + @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_user_task_attachment_tid")) + private UserTaskInstanceEntity taskInstance; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + + public UserTaskInstanceEntity getTaskInstance() { + return taskInstance; + } + + public void setTaskInstance(UserTaskInstanceEntity taskInstance) { + this.taskInstance = taskInstance; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java new file mode 100644 index 00000000000..cf70f4e7bca --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import java.util.Date; + +import jakarta.persistence.*; + +@Entity +@Table(name = "jbpm_user_tasks_comments") +public class CommentEntity { + + @Id + private String id; + + private String comment; + + @Column(name = "updated_by") + private String updatedBy; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "updated_at") + private Date updatedAt; + + @ManyToOne(optional = false) + @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_user_task_comment_tid")) + private UserTaskInstanceEntity taskInstance; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String user) { + this.updatedBy = user; + } + + public UserTaskInstanceEntity getTaskInstance() { + return taskInstance; + } + + public void setTaskInstance(UserTaskInstanceEntity taskInstance) { + this.taskInstance = taskInstance; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntity.java new file mode 100644 index 00000000000..506f3bf925b --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntity.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import java.util.Objects; + +import jakarta.persistence.*; + +@MappedSuperclass +public abstract class TaskDataEntity { + + @Id + @Column(name = "name") + protected String name; + + @Id + @ManyToOne(optional = false) + @JoinColumn(name = "task_id") + protected UserTaskInstanceEntity taskInstance; + + @Column(name = "value") + protected T value; + + @Column(name = "java_type") + protected String javaType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public UserTaskInstanceEntity getTaskInstance() { + return taskInstance; + } + + public void setTaskInstance(UserTaskInstanceEntity taskInstance) { + this.taskInstance = taskInstance; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + public String getJavaType() { + return javaType; + } + + public void setJavaType(String javaType) { + this.javaType = javaType; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TaskDataEntity that = (TaskDataEntity) o; + return Objects.equals(getName(), that.getName()) && Objects.equals(getTaskInstance(), that.getTaskInstance()) && Objects.equals(getValue(), + that.getValue()) && Objects.equals(getJavaType(), that.getJavaType()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), getTaskInstance(), getValue(), getJavaType()); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntityPK.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntityPK.java new file mode 100644 index 00000000000..4d0f1fab9ed --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskDataEntityPK.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import java.io.Serializable; +import java.util.Objects; + +public class TaskDataEntityPK implements Serializable { + + private String name; + private UserTaskInstanceEntity taskInstance; + + public TaskDataEntityPK() { + } + + public TaskDataEntityPK(String inputName, UserTaskInstanceEntity taskInstance) { + this.taskInstance = taskInstance; + this.name = inputName; + } + + public UserTaskInstanceEntity getTaskInstance() { + return taskInstance; + } + + public void setTaskInstance(UserTaskInstanceEntity taskInstance) { + this.taskInstance = taskInstance; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TaskDataEntityPK that = (TaskDataEntityPK) o; + return Objects.equals(getName(), that.getName()) && Objects.equals(getTaskInstance(), that.getTaskInstance()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), getTaskInstance()); + } + + @Override + public String toString() { + return "TaskInputEntityId{" + + "taskInstance='" + taskInstance + '\'' + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskInputEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskInputEntity.java new file mode 100644 index 00000000000..9c4351cfd24 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskInputEntity.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "jbpm_user_tasks_inputs") +@AttributeOverrides({ + @AttributeOverride(name = "name", column = @Column(name = "input_name")), + @AttributeOverride(name = "value", column = @Column(name = "input_value")) +}) +@AssociationOverride(name = "taskInstance", foreignKey = @ForeignKey(name = "jbpm_user_tasks_inputs_tid")) +@IdClass(TaskDataEntityPK.class) +public class TaskInputEntity extends TaskDataEntity { + +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskMetadataEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskMetadataEntity.java new file mode 100644 index 00000000000..2729ee68716 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskMetadataEntity.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "jbpm_user_tasks_metadata") +@AttributeOverrides({ + @AttributeOverride(name = "name", column = @Column(name = "metadata_name")), + @AttributeOverride(name = "value", column = @Column(name = "metadata_value")) +}) +@AssociationOverride(name = "taskInstance", + joinColumns = @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "jbpm_user_tasks_metadata_tid"))) +@IdClass(TaskDataEntityPK.class) +public class TaskMetadataEntity extends TaskDataEntity { + +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskOutputEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskOutputEntity.java new file mode 100644 index 00000000000..bd7098f2450 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskOutputEntity.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "jbpm_user_tasks_outputs") +@AttributeOverrides({ + @AttributeOverride(name = "name", column = @Column(name = "output_name")), + @AttributeOverride(name = "value", column = @Column(name = "output_value")) +}) +@AssociationOverride(name = "taskInstance", + joinColumns = @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "jbpm_user_tasks_outputs_tid"))) +@IdClass(TaskDataEntityPK.class) +public class TaskOutputEntity extends TaskDataEntity { + +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java new file mode 100644 index 00000000000..8b3cfca0282 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.model; + +import java.util.*; + +import jakarta.persistence.*; + +@Entity +@NamedQuery(name = UserTaskInstanceEntity.GET_INSTANCES_BY_IDENTITY, + query = "select userTask from UserTaskInstanceEntity userTask " + + "left join userTask.adminGroups adminGroups " + + "left join userTask.potentialGroups potentialGroups " + + "where userTask.actualOwner = :userId " + + "or :userId member of userTask.adminUsers " + + "or adminGroups in (:roles) " + + "or (:userId member of userTask.potentialUsers and :userId not member of userTask.excludedUsers) " + + "or potentialGroups in (:roles)") +@Table(name = "jbpm_user_tasks") +public class UserTaskInstanceEntity { + public static final String GET_INSTANCES_BY_IDENTITY = "UserTaskInstanceEntity.GetInstanceByIdentity"; + + @Id + private String id; + + @Column(name = "user_task_id") + private String userTaskId; + + @Column(name = "task_name") + private String taskName; + + @Column(name = "task_description") + private String taskDescription; + + @Column(name = "task_priority") + private String taskPriority; + + private String status; + + @Column(name = "termination_type") + private String terminationType; + + @Column(name = "actual_owner") + private String actualOwner; + + @Column(name = "external_reference_id") + private String externalReferenceId; + + @ElementCollection + @CollectionTable(name = "jbpm_user_tasks_potential_users", joinColumns = @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_jbpm_user_tasks_potential_users_tid"))) + @Column(name = "user_id", nullable = false) + private Set potentialUsers = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "jbpm_user_tasks_potential_groups", joinColumns = @JoinColumn(name = "task_id"), + foreignKey = @ForeignKey(name = "fk_jbpm_user_tasks_potential_groups_tid")) + @Column(name = "group_id") + private Set potentialGroups = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "jbpm_user_tasks_admin_users", joinColumns = @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_jbpm_user_tasks_admin_users_tid"))) + @Column(name = "user_id", nullable = false) + private Set adminUsers = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "jbpm_user_tasks_admin_groups", joinColumns = @JoinColumn(name = "task_id"), + foreignKey = @ForeignKey(name = "fk_jbpm_user_tasks_admin_groups_tid")) + @Column(name = "group_id") + private Set adminGroups = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "jbpm_user_tasks_excluded_users", joinColumns = @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_jbpm_user_tasks_excluded_users_tid"))) + @Column(name = "user_id", nullable = false) + private Set excludedUsers = new HashSet<>(); + + @OneToMany(mappedBy = "taskInstance", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List attachments = new ArrayList<>(); + + @OneToMany(mappedBy = "taskInstance", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List comments = new ArrayList<>(); + + @OneToMany(mappedBy = "taskInstance", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List inputs = new ArrayList<>(); + + @OneToMany(mappedBy = "taskInstance", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List outputs = new ArrayList<>(); + + @OneToMany(mappedBy = "taskInstance", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List metadata = new ArrayList<>(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getActualOwner() { + return actualOwner; + } + + public void setActualOwner(String actualOwner) { + this.actualOwner = actualOwner; + } + + public String getTaskName() { + return taskName; + } + + public void setTaskName(String taskName) { + this.taskName = taskName; + } + + public String getTaskDescription() { + return taskDescription; + } + + public void setTaskDescription(String taskDescription) { + this.taskDescription = taskDescription; + } + + public String getTaskPriority() { + return taskPriority; + } + + public void setTaskPriority(String taskPriority) { + this.taskPriority = taskPriority; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getExternalReferenceId() { + return externalReferenceId; + } + + public void setExternalReferenceId(String externalReferenceId) { + this.externalReferenceId = externalReferenceId; + } + + public void setPotentialUsers(Set potentialUsers) { + this.potentialUsers.clear(); + this.potentialUsers.addAll(potentialUsers); + } + + public Set getPotentialUsers() { + return potentialUsers; + } + + public Set getPotentialGroups() { + return potentialGroups; + } + + public void setPotentialGroups(Set potentialGroups) { + this.potentialGroups.clear(); + this.potentialGroups.addAll(potentialGroups); + } + + public Set getAdminUsers() { + return adminUsers; + } + + public void setAdminUsers(Set adminUsers) { + this.adminUsers.clear(); + this.adminUsers.addAll(adminUsers); + } + + public Collection getAdminGroups() { + return adminGroups; + } + + public void setAdminGroups(Set adminGroups) { + this.adminGroups.clear(); + this.adminGroups.addAll(adminGroups); + } + + public Collection getExcludedUsers() { + return excludedUsers; + } + + public void setExcludedUsers(Set excludedUsers) { + this.excludedUsers.clear(); + this.excludedUsers.addAll(excludedUsers); + } + + public void clearAttachments() { + this.attachments.clear(); + } + + public Collection getAttachments() { + return attachments; + } + + public void addAttachment(AttachmentEntity attachment) { + attachment.setTaskInstance(this); + this.attachments.add(attachment); + } + + public void removeAttachment(AttachmentEntity attachmentEntity) { + this.attachments.remove(attachmentEntity); + } + + public void setAttachments(Collection attachments) { + this.clearAttachments(); + this.attachments.addAll(attachments); + } + + public void clearComments() { + this.comments.clear(); + } + + public void removeComment(CommentEntity comment) { + this.comments.remove(comment); + } + + public Collection getComments() { + return comments; + } + + public void addComment(CommentEntity comment) { + comment.setTaskInstance(this); + this.comments.add(comment); + } + + public void setComments(Collection comments) { + this.clearComments(); + this.comments.addAll(comments); + } + + public void clearInputs() { + this.inputs.clear(); + } + + public Collection getInputs() { + return inputs; + } + + public void setInputs(Collection inputs) { + this.clearInputs(); + this.inputs.addAll(inputs); + } + + public void addInput(TaskInputEntity input) { + input.setTaskInstance(this); + this.inputs.add(input); + } + + public void removeInput(TaskInputEntity input) { + this.inputs.remove(input); + } + + public void clearOutputs() { + this.outputs.clear(); + } + + public Collection getOutputs() { + return outputs; + } + + public void addOutput(TaskOutputEntity output) { + output.setTaskInstance(this); + this.outputs.add(output); + } + + public void removeOutput(TaskOutputEntity output) { + this.outputs.remove(output); + } + + public void setOutputs(Collection outputs) { + this.clearOutputs(); + this.outputs.addAll(outputs); + } + + public void clearMetadata() { + this.metadata.clear(); + } + + public Collection getMetadata() { + return metadata; + } + + public void addMetadata(TaskMetadataEntity metadata) { + metadata.setTaskInstance(this); + this.metadata.add(metadata); + } + + public void removeMetadata(TaskMetadataEntity metadata) { + this.metadata.remove(metadata); + } + + public void setMetadata(Collection metadata) { + this.clearMetadata(); + this.metadata.addAll(metadata); + } + + public void setUserTaskId(String userTaskId) { + this.userTaskId = userTaskId; + } + + public String getUserTaskId() { + return userTaskId; + } + + public void setTerminationType(String terminationType) { + this.terminationType = terminationType; + } + + public String getTerminationType() { + return terminationType; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/AttachmentRepository.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/AttachmentRepository.java new file mode 100644 index 00000000000..92bf5f5a81f --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/AttachmentRepository.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import org.jbpm.usertask.jpa.model.AttachmentEntity; + +public class AttachmentRepository extends BaseRepository { + + public AttachmentRepository(UserTaskJPAContext context) { + super(context); + } + + @Override + public Class getEntityClass() { + return AttachmentEntity.class; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/BaseRepository.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/BaseRepository.java new file mode 100644 index 00000000000..ed144ddbc04 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/BaseRepository.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import java.util.List; +import java.util.Optional; + +import jakarta.persistence.EntityManager; + +public abstract class BaseRepository { + + protected UserTaskJPAContext context; + + public BaseRepository(UserTaskJPAContext context) { + this.context = context; + } + + public Optional findById(K id) { + return Optional.ofNullable(getEntityManager().find(getEntityClass(), id)); + } + + public List findAll() { + return getEntityManager().createQuery("from " + getEntityClass().getName(), getEntityClass()).getResultList(); + } + + public T persist(T entity) { + getEntityManager().persist(entity); + + return entity; + } + + public T update(T entity) { + return this.getEntityManager().merge(entity); + } + + public T remove(T entity) { + this.getEntityManager().remove(entity); + return entity; + } + + public abstract Class getEntityClass(); + + protected EntityManager getEntityManager() { + return context.getEntityManager(); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/CommentRepository.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/CommentRepository.java new file mode 100644 index 00000000000..98b85d6a991 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/CommentRepository.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import org.jbpm.usertask.jpa.model.CommentEntity; + +public class CommentRepository extends BaseRepository { + + public CommentRepository(UserTaskJPAContext context) { + super(context); + } + + @Override + public Class getEntityClass() { + return CommentEntity.class; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskInputRepository.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskInputRepository.java new file mode 100644 index 00000000000..6e85081ff76 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskInputRepository.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import org.jbpm.usertask.jpa.model.TaskDataEntityPK; +import org.jbpm.usertask.jpa.model.TaskInputEntity; + +public class TaskInputRepository extends BaseRepository { + + public TaskInputRepository(UserTaskJPAContext context) { + super(context); + } + + @Override + public Class getEntityClass() { + return TaskInputEntity.class; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskMetadataRepository.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskMetadataRepository.java new file mode 100644 index 00000000000..26796bb38aa --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskMetadataRepository.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import org.jbpm.usertask.jpa.model.TaskDataEntityPK; +import org.jbpm.usertask.jpa.model.TaskMetadataEntity; + +public class TaskMetadataRepository extends BaseRepository { + + public TaskMetadataRepository(UserTaskJPAContext context) { + super(context); + } + + @Override + public Class getEntityClass() { + return TaskMetadataEntity.class; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskOutputRepository.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskOutputRepository.java new file mode 100644 index 00000000000..2b39b8f1bb0 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/TaskOutputRepository.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import org.jbpm.usertask.jpa.model.TaskDataEntityPK; +import org.jbpm.usertask.jpa.model.TaskOutputEntity; + +public class TaskOutputRepository extends BaseRepository { + + public TaskOutputRepository(UserTaskJPAContext context) { + super(context); + } + + @Override + public Class getEntityClass() { + return TaskOutputEntity.class; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskInstanceRepository.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskInstanceRepository.java new file mode 100644 index 00000000000..1be6a4aec71 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskInstanceRepository.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import java.util.List; + +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.kie.kogito.auth.IdentityProvider; + +import jakarta.persistence.TypedQuery; + +import static org.jbpm.usertask.jpa.model.UserTaskInstanceEntity.GET_INSTANCES_BY_IDENTITY; + +public class UserTaskInstanceRepository extends BaseRepository { + + public UserTaskInstanceRepository(UserTaskJPAContext context) { + super(context); + } + + public List findByIdentity(IdentityProvider identityProvider) { + TypedQuery query = getEntityManager().createNamedQuery(GET_INSTANCES_BY_IDENTITY, UserTaskInstanceEntity.class); + query.setParameter("userId", identityProvider.getName()); + query.setParameter("roles", identityProvider.getRoles()); + return query.getResultList(); + } + + @Override + public Class getEntityClass() { + return UserTaskInstanceEntity.class; + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskJPAContext.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskJPAContext.java new file mode 100644 index 00000000000..8606184dd58 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/repository/UserTaskJPAContext.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.repository; + +import jakarta.persistence.EntityManager; + +public interface UserTaskJPAContext { + + EntityManager getEntityManager(); +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/META-INF/beans.xml b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/META-INF/kie-flyway.properties b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/META-INF/kie-flyway.properties new file mode 100644 index 00000000000..67ce94390d9 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/META-INF/kie-flyway.properties @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +module.name=jbpm-user-task-storage + +module.locations.h2=classpath:kie-flyway/db/user-tasks/h2 +module.locations.postgresql=classpath:kie-flyway/db/user-tasks/postgresql \ No newline at end of file diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql new file mode 100644 index 00000000000..058f3599d3b --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +create table jbpm_user_tasks ( + id varchar(50) not null, + user_task_id varchar(255), + task_priority varchar(50), + actual_owner varchar(255), + task_description varchar(255), + status varchar(255), + termination_type varchar(255), + external_reference_id varchar(255), + task_name varchar(255), + primary key (id) +); + +create table jbpm_user_tasks_potential_users( + task_id varchar(50) not null, + user_id varchar(255) not null, + primary key (task_id, user_id) +); + +create table jbpm_user_tasks_potential_groups( + task_id varchar(50) not null, + group_id varchar(255) not null, + primary key (task_id, group_id) +); + +create table jbpm_user_tasks_admin_users ( + task_id varchar(50) not null, + user_id varchar(255) not null, + primary key (task_id, user_id) +); + +create table jbpm_user_tasks_admin_groups ( + task_id varchar(50) not null, + group_id varchar(255) not null, + primary key (task_id, group_id) +); + +create table jbpm_user_tasks_excluded_users( + task_id varchar(50) not null, + user_id varchar(255) not null, + primary key (task_id, user_id) +); + +create table jbpm_user_tasks_attachments ( + id varchar(50) not null, + name varchar(255), + updated_by varchar(255), + updated_at timestamp, + url varchar(255), + task_id varchar(50) not null, + primary key (id) +); + +create table jbpm_user_tasks_comments ( + id varchar(50) not null, + updated_by varchar(255), + updated_at timestamp, + comment varchar(255), + task_id varchar(50) not null, + primary key (id) +); + +create table jbpm_user_tasks_inputs ( + task_id varchar(50) not null, + input_name varchar(255) not null, + input_value varbinary(max), + java_type varchar(255), + primary key (task_id, input_name) +); + +create table jbpm_user_tasks_outputs ( + task_id varchar(50) not null, + output_name varchar(255) not null, + output_value varbinary(max), + java_type varchar(255), + primary key (task_id, output_name) +); + +create table jbpm_user_tasks_metadata ( + task_id varchar(50) not null, + metadata_name varchar(255) not null, + metadata_value varchar(512), + java_type varchar(255), + primary key (task_id, metadata_name) +); + +alter table if exists jbpm_user_tasks_potential_users +drop constraint if exists fk_jbpm_user_tasks_potential_users_tid cascade; + +alter table if exists jbpm_user_tasks_potential_users +add constraint fk_jbpm_user_fk_tasks_potential_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_potential_groups +drop constraint if exists fk_jbpm_user_tasks_potential_groups_tid cascade; + +alter table if exists jbpm_user_tasks_potential_groups +add constraint fk_jbpm_user_tasks_potential_groups_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_admin_users +drop constraint if exists fk_jbpm_user_tasks_admin_users_tid cascade; + +alter table if exists jbpm_user_tasks_admin_users +add constraint fk_jbpm_user_tasks_admin_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_admin_groups +drop constraint if exists fk_jbpm_user_tasks_admin_groups_tid cascade; + +alter table if exists jbpm_user_tasks_admin_groups +add constraint fk_jbpm_user_tasks_admin_groups_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_excluded_users +drop constraint if exists fk_jbpm_user_tasks_excluded_users_tid cascade; + +alter table if exists jbpm_user_tasks_excluded_users +add constraint fk_jbpm_user_tasks_excluded_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_attachments +drop constraint if exists fk_user_task_attachment_tid cascade; + +alter table if exists jbpm_user_tasks_attachments +add constraint fk_user_task_attachment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_comments +drop constraint if exists fk_user_task_comment_tid cascade; + +alter table if exists jbpm_user_tasks_comments +add constraint fk_user_task_comment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_inputs +drop constraint if exists fk_jbpm_user_tasks_inputs_tid cascade; + +alter table if exists jbpm_user_tasks_inputs +add constraint fk_jbpm_user_tasks_inputs_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_outputs +drop constraint if exists fk_jbpm_user_tasks_outputs_tid cascade; + +alter table if exists jbpm_user_tasks_outputs +add constraint fk_jbpm_user_tasks_outputs_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_metadata +drop constraint if exists fk_jbpm_user_tasks_metadata_tid cascade; + +alter table if exists jbpm_user_tasks_metadata +add constraint fk_jbpm_user_tasks_metadata_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + + diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql new file mode 100644 index 00000000000..8daa0437216 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +create table jbpm_user_tasks ( + id varchar(50) not null, + user_task_id varchar(255), + task_priority varchar(50), + actual_owner varchar(255), + task_description varchar(255), + status varchar(255), + termination_type varchar(255), + external_reference_id varchar(255), + task_name varchar(255), + primary key (id) +); + +create table jbpm_user_tasks_potential_users( + task_id varchar(50) not null, + user_id varchar(255) not null, + primary key (task_id, user_id) +); + +create table jbpm_user_tasks_potential_groups( + task_id varchar(50) not null, + group_id varchar(255) not null, + primary key (task_id, group_id) +); + +create table jbpm_user_tasks_admin_users ( + task_id varchar(50) not null, + user_id varchar(255) not null, + primary key (task_id, user_id) +); + +create table jbpm_user_tasks_admin_groups ( + task_id varchar(50) not null, + group_id varchar(255) not null, + primary key (task_id, group_id) +); + +create table jbpm_user_tasks_excluded_users( + task_id varchar(50) not null, + user_id varchar(255) not null, + primary key (task_id, user_id) +); + +create table jbpm_user_tasks_attachments ( + id varchar(50) not null, + name varchar(255), + updated_by varchar(255), + updated_at timestamp(6), + url varchar(255), + task_id varchar(50) not null, + primary key (id) +); + +create table jbpm_user_tasks_comments ( + id varchar(50) not null, + updated_by varchar(255), + updated_at timestamp(6), + comment varchar(255), + task_id varchar(50) not null, + primary key (id) +); + +create table jbpm_user_tasks_inputs ( + task_id varchar(50) not null, + input_name varchar(255) not null, + input_value bytea, + java_type varchar(255), + primary key (task_id, input_name) +); + +create table jbpm_user_tasks_outputs ( + task_id varchar(50) not null, + output_name varchar(255) not null, + output_value bytea, + java_type varchar(255), + primary key (task_id, output_name) +); + +create table jbpm_user_tasks_metadata ( + task_id varchar(50), + metadata_name varchar(255) not null, + metadata_value varchar(512), + java_type varchar(255), + primary key (task_id, metadata_name) +); + +alter table if exists jbpm_user_tasks_potential_users +drop constraint if exists fk_jbpm_user_tasks_potential_users_tid cascade; + +alter table if exists jbpm_user_tasks_potential_users +add constraint fk_jbpm_user_fk_tasks_potential_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_potential_groups +drop constraint if exists fk_jbpm_user_tasks_potential_groups_tid cascade; + +alter table if exists jbpm_user_tasks_potential_groups +add constraint fk_jbpm_user_tasks_potential_groups_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_admin_users +drop constraint if exists fk_jbpm_user_tasks_admin_users_tid cascade; + +alter table if exists jbpm_user_tasks_admin_users +add constraint fk_jbpm_user_tasks_admin_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_admin_groups +drop constraint if exists fk_jbpm_user_tasks_admin_groups_tid cascade; + +alter table if exists jbpm_user_tasks_admin_groups +add constraint fk_jbpm_user_tasks_admin_groups_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_excluded_users +drop constraint if exists fk_jbpm_user_tasks_excluded_users_tid cascade; + +alter table if exists jbpm_user_tasks_excluded_users +add constraint fk_jbpm_user_tasks_excluded_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_attachments +drop constraint if exists fk_user_task_attachment_tid cascade; + +alter table if exists jbpm_user_tasks_attachments +add constraint fk_user_task_attachment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_comments +drop constraint if exists fk_user_task_comment_tid cascade; + +alter table if exists jbpm_user_tasks_comments +add constraint fk_user_task_comment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_inputs +drop constraint if exists fk_jbpm_user_tasks_inputs_tid cascade; + +alter table if exists jbpm_user_tasks_inputs +add constraint fk_jbpm_user_tasks_inputs_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_outputs +drop constraint if exists fk_jbpm_user_tasks_outputs_tid cascade; + +alter table if exists jbpm_user_tasks_outputs +add constraint fk_jbpm_user_tasks_outputs_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; + +alter table if exists jbpm_user_tasks_metadata +drop constraint if exists fk_jbpm_user_tasks_metadata_tid cascade; + +alter table if exists jbpm_user_tasks_metadata +add constraint fk_jbpm_user_tasks_metadata_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; \ No newline at end of file diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/JPAUserTaskInstancesTest.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/JPAUserTaskInstancesTest.java new file mode 100644 index 00000000000..d0ca49f1143 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/JPAUserTaskInstancesTest.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.mapper.*; +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.UserTaskInstanceRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.auth.IdentityProviders; +import org.kie.kogito.usertask.UserTaskInstance; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class JPAUserTaskInstancesTest { + + @Mock + private UserTaskInstanceRepository userTaskInstanceRepository; + + @Mock + private AttachmentsEntityMapper attachmentsEntityMapper; + @Mock + private CommentsEntityMapper commentsEntityMapper; + @Mock + private TaskMetadataEntityMapper metadataEntityMapper; + @Mock + private TaskInputsEntityMapper inputsEntityMapper; + @Mock + private TaskOutputsEntityMapper outputsEntityMapper; + + private UserTaskInstanceEntityMapper userTaskInstanceEntityMapper; + @Mock + private Function reconnectUserTaskInstance; + @Mock + private Function disconnectUserTaskInstance; + + private JPAUserTaskInstances jpaUserTaskInstances; + + @BeforeEach + public void setup() { + userTaskInstanceEntityMapper = spy(new UserTaskInstanceEntityMapper(attachmentsEntityMapper, commentsEntityMapper, metadataEntityMapper, inputsEntityMapper, outputsEntityMapper)); + jpaUserTaskInstances = new JPAUserTaskInstances(userTaskInstanceRepository, userTaskInstanceEntityMapper); + jpaUserTaskInstances.setReconnectUserTaskInstance(reconnectUserTaskInstance); + jpaUserTaskInstances.setDisconnectUserTaskInstance(disconnectUserTaskInstance); + } + + @Test + public void testSuccessfulFindById() { + Optional result = Optional.of(TestUtils.createUserTaskInstanceEntity()); + + when(userTaskInstanceRepository.findById(any())).thenReturn(result); + + jpaUserTaskInstances.findById("1234"); + + verify(userTaskInstanceEntityMapper, times(1)).mapTaskEntityToInstance(any()); + verify(reconnectUserTaskInstance, times(1)).apply(any()); + } + + @Test + public void testUnSuccessfulFindById() { + when(userTaskInstanceRepository.findById(any())).thenReturn(Optional.empty()); + + jpaUserTaskInstances.findById("1234"); + + verify(userTaskInstanceEntityMapper, never()).mapTaskEntityToInstance(any()); + verify(reconnectUserTaskInstance, never()).apply(any()); + } + + @Test + public void testSuccessfulExists() { + Optional result = Optional.of(TestUtils.createUserTaskInstanceEntity()); + + when(userTaskInstanceRepository.findById(any())).thenReturn(result); + + Assertions.assertThat(jpaUserTaskInstances.exists("1234")) + .isTrue(); + + verify(userTaskInstanceEntityMapper, never()).mapTaskEntityToInstance(any()); + verify(reconnectUserTaskInstance, never()).apply(any()); + } + + @Test + public void testUnSuccessfulExists() { + when(userTaskInstanceRepository.findById(any())).thenReturn(Optional.empty()); + + Assertions.assertThat(jpaUserTaskInstances.exists("1234")) + .isFalse(); + + verify(userTaskInstanceEntityMapper, never()).mapTaskEntityToInstance(any()); + verify(reconnectUserTaskInstance, never()).apply(any()); + } + + @Test + public void testSuccessfulFindByIdentity() { + List result = List.of(TestUtils.createUserTaskInstanceEntity(), TestUtils.createUserTaskInstanceEntity()); + + when(userTaskInstanceRepository.findByIdentity(any())).thenReturn(result); + + List instances = jpaUserTaskInstances.findByIdentity(IdentityProviders.of("user", "group")); + + Assertions.assertThat(instances) + .hasSize(2); + + verify(userTaskInstanceEntityMapper, times(2)).mapTaskEntityToInstance(any()); + verify(reconnectUserTaskInstance, times(2)).apply(any()); + + } + + @Test + public void testUnSuccessfulFindByIdentity() { + when(userTaskInstanceRepository.findByIdentity(any())).thenReturn(List.of()); + + List instances = jpaUserTaskInstances.findByIdentity(IdentityProviders.of("user", "group")); + + Assertions.assertThat(instances) + .isEmpty(); + + verify(userTaskInstanceEntityMapper, never()).mapTaskEntityToInstance(any()); + verify(reconnectUserTaskInstance, never()).apply(any()); + } + + @Test + public void testSuccessfulCreate() { + when(userTaskInstanceRepository.findById(any())).thenReturn(Optional.empty()); + + jpaUserTaskInstances.create(TestUtils.createUserTaskInstance()); + + verify(userTaskInstanceRepository, times(1)).persist(any()); + verify(userTaskInstanceEntityMapper, times(1)).mapTaskInstanceToEntity(any(), any()); + verify(reconnectUserTaskInstance, times(1)).apply(any()); + } + + @Test + public void testUnSuccessfulCreate() { + Optional result = Optional.of(TestUtils.createUserTaskInstanceEntity()); + when(userTaskInstanceRepository.findById(any())).thenReturn(result); + + Assertions.assertThatThrownBy(() -> { + jpaUserTaskInstances.create(TestUtils.createUserTaskInstance()); + }).hasMessageContaining("Task Already exists."); + + verify(userTaskInstanceRepository, never()).persist(any()); + verify(userTaskInstanceEntityMapper, never()).mapTaskInstanceToEntity(any(), any()); + verify(reconnectUserTaskInstance, never()).apply(any()); + } + + @Test + public void testSuccessfulUpdate() { + Optional result = Optional.of(TestUtils.createUserTaskInstanceEntity()); + when(userTaskInstanceRepository.findById(any())).thenReturn(result); + + jpaUserTaskInstances.update(TestUtils.createUserTaskInstance()); + + verify(userTaskInstanceRepository, times(1)).update(any()); + verify(userTaskInstanceEntityMapper, times(1)).mapTaskInstanceToEntity(any(), any()); + } + + @Test + public void testUnSuccessfulUpdate() { + when(userTaskInstanceRepository.findById(any())).thenReturn(Optional.empty()); + + Assertions.assertThatThrownBy(() -> { + jpaUserTaskInstances.update(TestUtils.createUserTaskInstance()); + }).hasMessageContaining("Could not find userTaskInstance with id "); + + verify(userTaskInstanceRepository, never()).persist(any()); + verify(userTaskInstanceEntityMapper, never()).mapTaskInstanceToEntity(any(), any()); + } + + @Test + public void testSuccessfulRemove() { + Optional result = Optional.of(TestUtils.createUserTaskInstanceEntity()); + when(userTaskInstanceRepository.findById(any())).thenReturn(result); + + jpaUserTaskInstances.remove(TestUtils.createUserTaskInstance()); + + verify(userTaskInstanceRepository, times(1)).remove(any()); + verify(disconnectUserTaskInstance, times(1)).apply(any()); + } + + @Test + public void testUnSuccessfulRemove() { + when(userTaskInstanceRepository.findById(any())).thenReturn(Optional.empty()); + + Assertions.assertThatThrownBy(() -> { + jpaUserTaskInstances.remove(TestUtils.createUserTaskInstance()); + }).hasMessageContaining("Could not remove userTaskInstance with id"); + + verify(userTaskInstanceRepository, never()).persist(any()); + verify(disconnectUserTaskInstance, never()).apply(any()); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapperTest.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapperTest.java new file mode 100644 index 00000000000..cafdb8e166b --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/AttachmentsEntityMapperTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.net.URI; +import java.util.Date; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.AttachmentEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.AttachmentRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.model.Attachment; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class AttachmentsEntityMapperTest { + + @Mock + private AttachmentRepository repository; + private AttachmentsEntityMapper mapper; + + @BeforeEach + public void setup() { + mapper = new AttachmentsEntityMapper(repository); + } + + @Test + public void testMapAttachmentsFromInstanceToEntity() { + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + Attachment attachment = new Attachment("1", "John"); + attachment.setName("attachment 1"); + attachment.setContent(URI.create("http://localhost:8080/my-attachment.txt")); + attachment.setUpdatedAt(new Date()); + + Attachment attachment2 = new Attachment("2", "Ned"); + attachment2.setName("attachment 2"); + attachment2.setContent(URI.create("http://localhost:8080/my-attachment2.txt")); + attachment2.setUpdatedAt(new Date()); + + instance.addAttachment(attachment); + instance.addAttachment(attachment2); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getAttachments()) + .hasSize(2); + verify(repository, never()) + .remove(any()); + TestUtils.assertUserTaskEntityAttachments(entity.getAttachments(), instance.getAttachments()); + + instance.removeAttachment(attachment); + mapper.mapInstanceToEntity(instance, entity); + + verify(repository, times(1)) + .remove(any()); + Assertions.assertThat(entity.getAttachments()) + .hasSize(1); + TestUtils.assertUserTaskEntityAttachments(entity.getAttachments(), instance.getAttachments()); + + instance.removeAttachment(attachment2); + mapper.mapInstanceToEntity(instance, entity); + + verify(repository, times(2)) + .remove(any()); + Assertions.assertThat(entity.getAttachments()) + .hasSize(0); + } + + @Test + public void testMapAttachmentsFromEntityToInstance() { + + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + + AttachmentEntity attachment = new AttachmentEntity(); + attachment.setId("1"); + attachment.setUpdatedBy("John"); + attachment.setName("attachment 1"); + attachment.setUrl("http://localhost:8080/my-attachment.txt"); + attachment.setUpdatedAt(new Date()); + + AttachmentEntity attachment2 = new AttachmentEntity(); + attachment2.setId("2"); + attachment2.setUpdatedBy("Ned"); + attachment2.setName("attachment 2"); + attachment2.setUrl("http://localhost:8080/my-attachment_2.txt"); + attachment2.setUpdatedAt(new Date()); + + entity.addAttachment(attachment); + entity.addAttachment(attachment2); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getAttachments()) + .hasSize(2); + verify(repository, never()) + .remove(any()); + TestUtils.assertUserTaskInstanceAttachments(instance.getAttachments(), entity.getAttachments()); + + entity.removeAttachment(attachment); + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(entity.getAttachments()) + .hasSize(1); + TestUtils.assertUserTaskInstanceAttachments(instance.getAttachments(), entity.getAttachments()); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapperTest.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapperTest.java new file mode 100644 index 00000000000..62477c2d7ab --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/CommentsEntityMapperTest.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.util.Date; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.CommentEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.CommentRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.model.Comment; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class CommentsEntityMapperTest { + + @Mock + private CommentRepository repository; + private CommentsEntityMapper mapper; + + @BeforeEach + public void setup() { + mapper = new CommentsEntityMapper(repository); + } + + @Test + public void testMapCommentsFromInstanceToEntity() { + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + Comment comment = new Comment("1", "John"); + comment.setContent("This is comment 1"); + comment.setUpdatedAt(new Date()); + + Comment comment2 = new Comment("2", "Ned"); + comment2.setContent("This is comment 2"); + comment2.setUpdatedAt(new Date()); + + instance.addComment(comment); + instance.addComment(comment2); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getComments()) + .hasSize(2); + verify(repository, never()) + .remove(any()); + + TestUtils.assertUserTaskEntityComments(entity.getComments(), instance.getComments()); + + instance.removeComment(comment); + mapper.mapInstanceToEntity(instance, entity); + + verify(repository, times(1)) + .remove(any()); + Assertions.assertThat(entity.getComments()) + .hasSize(1); + TestUtils.assertUserTaskEntityComments(entity.getComments(), instance.getComments()); + + instance.removeComment(comment2); + mapper.mapInstanceToEntity(instance, entity); + + verify(repository, times(2)) + .remove(any()); + Assertions.assertThat(entity.getComments()) + .hasSize(0); + } + + @Test + public void testMapCommentsFromEntityToInstance() { + + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + + CommentEntity comment = new CommentEntity(); + comment.setId("1"); + comment.setUpdatedBy("John"); + comment.setComment("This is comment 1"); + comment.setUpdatedAt(new Date()); + + CommentEntity comment2 = new CommentEntity(); + comment2.setId("2"); + comment2.setUpdatedBy("Ned"); + comment2.setComment("This is comment 2"); + comment2.setUpdatedAt(new Date()); + + entity.addComment(comment); + entity.addComment(comment2); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getComments()) + .hasSize(2); + verify(repository, never()) + .remove(any()); + + TestUtils.assertUserTaskInstanceComments(instance.getComments(), entity.getComments()); + + entity.removeComment(comment); + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(entity.getComments()) + .hasSize(1); + TestUtils.assertUserTaskInstanceComments(instance.getComments(), entity.getComments()); + + entity.removeComment(comment); + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(entity.getComments()) + .hasSize(1); + TestUtils.assertUserTaskInstanceComments(instance.getComments(), entity.getComments()); + + entity.removeComment(comment2); + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(entity.getComments()) + .hasSize(0); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapperTest.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapperTest.java new file mode 100644 index 00000000000..1aaeed39d30 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskInputsEntityMapperTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.TaskInputEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.TaskInputRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TaskInputsEntityMapperTest { + + @Mock + private TaskInputRepository repository; + private TaskInputsEntityMapper mapper; + + @BeforeEach + public void setup() { + mapper = new TaskInputsEntityMapper(repository); + } + + @Test + public void testMapInputsFromInstanceToEntity() { + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getInputs()) + .hasSize(8); + verify(repository, never()) + .remove(any()); + TestUtils.assertUserTaskEntityInputs(entity, instance); + + instance.getInputs().remove("in_string"); + instance.getInputs().remove("in_integer"); + instance.getInputs().remove("in_null"); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getInputs()) + .hasSize(5); + verify(repository, times(3)) + .remove(any()); + + TestUtils.assertUserTaskEntityInputs(entity, instance); + + instance.getInputs().clear(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getInputs()) + .hasSize(0); + verify(repository, times(8)) + .remove(any()); + } + + @Test + public void testMapInputsFromEntityToInstance() { + final String stringValue = "This is the input value"; + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + TaskInputEntity input = new TaskInputEntity(); + input.setName("in_string"); + input.setValue(stringValue.getBytes(StandardCharsets.UTF_8)); + + entity.addInput(input); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getInputs()) + .hasSize(1); + + TestUtils.assertUserTaskInstanceInputs(instance, entity); + + entity.getInputs().clear(); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getInputs()) + .hasSize(0); + } + + @Test + public void testMappingRoundCircle() { + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getInputs()) + .hasSize(8); + verify(repository, never()) + .remove(any()); + + TestUtils.assertUserTaskEntityInputs(entity, instance); + + DefaultUserTaskInstance instance2 = new DefaultUserTaskInstance(); + + mapper.mapEntityToInstance(entity, instance2); + + Assertions.assertThat(instance2.getInputs()) + .hasSize(8); + + TestUtils.assertUserTaskInstanceInputs(instance2, entity); + + Assertions.assertThat(instance2.getInputs()) + .usingRecursiveComparison() + .isEqualTo(instance.getInputs()); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapperTest.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapperTest.java new file mode 100644 index 00000000000..1e6d18de1fd --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskMetadataEntityMapperTest.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.TaskMetadataEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.TaskMetadataRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TaskMetadataEntityMapperTest { + + @Mock + private TaskMetadataRepository repository; + private TaskMetadataEntityMapper mapper; + + @BeforeEach + public void setup() { + mapper = new TaskMetadataEntityMapper(repository); + } + + @Test + public void testMapMetadataFromInstanceToEntity() { + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getMetadata()) + .hasSize(6); + verify(repository, never()) + .remove(any()); + TestUtils.assertUserTaskEntityMetadata(entity, instance); + + instance.getMetadata().remove("ProcessId"); + instance.getMetadata().remove("ProcessType"); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getMetadata()) + .hasSize(4); + verify(repository, times(2)) + .remove(any()); + + TestUtils.assertUserTaskEntityMetadata(entity, instance); + + instance.getMetadata().clear(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getMetadata()) + .hasSize(0); + verify(repository, times(6)) + .remove(any()); + } + + @Test + public void testMapMetadataFromEntityToInstance() { + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + TaskMetadataEntity metadata = new TaskMetadataEntity(); + metadata.setName("ProcessId"); + metadata.setValue("1234"); + + TaskMetadataEntity metadata2 = new TaskMetadataEntity(); + metadata2.setName("CustomMetadata"); + metadata2.setValue("This is the metadata value"); + + entity.addMetadata(metadata); + entity.addMetadata(metadata2); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getMetadata()) + .hasSize(2); + + TestUtils.assertUserTaskInstanceMetadata(instance, entity); + + entity.getMetadata().clear(); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getMetadata()) + .hasSize(0); + } + + @Test + public void testFullMappingCircle() { + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getMetadata()) + .hasSize(6); + verify(repository, never()) + .remove(any()); + + TestUtils.assertUserTaskEntityMetadata(entity, instance); + + DefaultUserTaskInstance instance2 = new DefaultUserTaskInstance(); + + mapper.mapEntityToInstance(entity, instance2); + + Assertions.assertThat(instance2.getMetadata()) + .hasSize(6); + + TestUtils.assertUserTaskInstanceMetadata(instance2, entity); + + Assertions.assertThat(instance2.getMetadata()) + .usingRecursiveComparison() + .isEqualTo(instance.getMetadata()); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapperTest.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapperTest.java new file mode 100644 index 00000000000..52da167c9d1 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/TaskOutputsEntityMapperTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.TaskOutputEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.repository.TaskOutputRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TaskOutputsEntityMapperTest { + + @Mock + private TaskOutputRepository repository; + private TaskOutputsEntityMapper mapper; + + @BeforeEach + public void setup() { + mapper = new TaskOutputsEntityMapper(repository); + } + + @Test + public void testMapOutputsFromInstanceToEntity() { + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getOutputs()) + .hasSize(8); + verify(repository, never()) + .remove(any()); + TestUtils.assertUserTaskEntityOutputs(entity, instance); + + instance.getOutputs().remove("out_string"); + instance.getOutputs().remove("out_integer"); + instance.getOutputs().remove("out_null"); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getOutputs()) + .hasSize(5); + verify(repository, times(3)) + .remove(any()); + + TestUtils.assertUserTaskEntityOutputs(entity, instance); + + instance.getOutputs().clear(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getOutputs()) + .hasSize(0); + verify(repository, times(8)) + .remove(any()); + } + + @Test + public void testMapOutputsFromEntityToInstance() { + final String stringValue = "This is the output value"; + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + TaskOutputEntity output = new TaskOutputEntity(); + output.setName("out_string"); + output.setValue(stringValue.getBytes(StandardCharsets.UTF_8)); + + entity.addOutput(output); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getOutputs()) + .hasSize(1); + + TestUtils.assertUserTaskInstanceOutputs(instance, entity); + + entity.getOutputs().clear(); + + mapper.mapEntityToInstance(entity, instance); + + Assertions.assertThat(instance.getOutputs()) + .hasSize(0); + } + + @Test + public void testMappingRoundCircle() { + + UserTaskInstance instance = TestUtils.createUserTaskInstance(); + UserTaskInstanceEntity entity = new UserTaskInstanceEntity(); + + mapper.mapInstanceToEntity(instance, entity); + + Assertions.assertThat(entity.getOutputs()) + .hasSize(8); + verify(repository, never()) + .remove(any()); + TestUtils.assertUserTaskEntityOutputs(entity, instance); + + DefaultUserTaskInstance instance2 = new DefaultUserTaskInstance(); + + mapper.mapEntityToInstance(entity, instance2); + + Assertions.assertThat(instance2.getOutputs()) + .hasSize(8); + + TestUtils.assertUserTaskInstanceOutputs(instance2, entity); + + Assertions.assertThat(instance2.getOutputs()) + .usingRecursiveComparison() + .isEqualTo(instance.getOutputs()); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapperTest.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapperTest.java new file mode 100644 index 00000000000..3b49d9b445d --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/UserTaskInstanceEntityMapperTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper; + +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.usertask.UserTaskInstance; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserTaskInstanceEntityMapperTest { + + @Mock + private AttachmentsEntityMapper attachmentsEntityMapper; + @Mock + private CommentsEntityMapper commentsEntityMapper; + @Mock + private TaskMetadataEntityMapper metadataEntityMapper; + @Mock + private TaskInputsEntityMapper inputsEntityMapper; + @Mock + private TaskOutputsEntityMapper outputsEntityMapper; + + private UserTaskInstanceEntityMapper userTaskInstanceEntityMapper; + + @BeforeEach + public void setUp() { + this.userTaskInstanceEntityMapper = new UserTaskInstanceEntityMapper(attachmentsEntityMapper, commentsEntityMapper, metadataEntityMapper, inputsEntityMapper, outputsEntityMapper); + } + + @Test + public void testUserTaskInstanceToUserTaskEntityMapper() { + UserTaskInstance userTaskInstance = TestUtils.createUserTaskInstance(); + + UserTaskInstanceEntity userTaskInstanceEntity = new UserTaskInstanceEntity(); + + userTaskInstanceEntityMapper.mapTaskInstanceToEntity(userTaskInstance, userTaskInstanceEntity); + + verify(attachmentsEntityMapper, times(1)) + .mapInstanceToEntity(same(userTaskInstance), same(userTaskInstanceEntity)); + verify(commentsEntityMapper, times(1)) + .mapInstanceToEntity(same(userTaskInstance), same(userTaskInstanceEntity)); + verify(metadataEntityMapper, times(1)) + .mapInstanceToEntity(same(userTaskInstance), same(userTaskInstanceEntity)); + verify(inputsEntityMapper, times(1)) + .mapInstanceToEntity(same(userTaskInstance), same(userTaskInstanceEntity)); + verify(outputsEntityMapper, times(1)) + .mapInstanceToEntity(same(userTaskInstance), same(userTaskInstanceEntity)); + + TestUtils.assertUserTaskEntityData(userTaskInstanceEntity, userTaskInstance); + TestUtils.assertUserTaskEntityPotentialUserAndGroups(userTaskInstanceEntity, userTaskInstance); + TestUtils.assertUserTaskEntityAdminUserAndGroups(userTaskInstanceEntity, userTaskInstance); + TestUtils.assertUserTaskEntityExcludedUsers(userTaskInstanceEntity, userTaskInstance); + } + + @Test + public void testUserTaskEntityToUserTaskInstanceMapper() { + UserTaskInstanceEntity userTaskInstanceEntity = TestUtils.createUserTaskInstanceEntity(); + + UserTaskInstance userTaskInstance = userTaskInstanceEntityMapper.mapTaskEntityToInstance(userTaskInstanceEntity); + + verify(attachmentsEntityMapper, times(1)) + .mapEntityToInstance(same(userTaskInstanceEntity), same(userTaskInstance)); + verify(commentsEntityMapper, times(1)) + .mapEntityToInstance(same(userTaskInstanceEntity), same(userTaskInstance)); + verify(metadataEntityMapper, times(1)) + .mapEntityToInstance(same(userTaskInstanceEntity), same(userTaskInstance)); + verify(inputsEntityMapper, times(1)) + .mapEntityToInstance(same(userTaskInstanceEntity), same(userTaskInstance)); + verify(outputsEntityMapper, times(1)) + .mapEntityToInstance(same(userTaskInstanceEntity), same(userTaskInstance)); + + TestUtils.assertUserTaskInstanceData(userTaskInstance, userTaskInstanceEntity); + TestUtils.assertUserTaskInstancePotentialUserAndGroups(userTaskInstance, userTaskInstanceEntity); + TestUtils.assertUserTaskInstanceAdminUserAndGroups(userTaskInstance, userTaskInstanceEntity); + TestUtils.assertUserTaskInstanceExcludedUsers(userTaskInstance, userTaskInstanceEntity); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/models/Person.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/models/Person.java new file mode 100644 index 00000000000..3c1d911f1e6 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/models/Person.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper.models; + +import java.util.Objects; + +public class Person { + String name; + String lastName; + int age; + + public Person() { + } + + public Person(String name, String lastName, int age) { + this.name = name; + this.lastName = lastName; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Person person = (Person) o; + return age == person.age && Objects.equals(name, person.name) && Objects.equals(lastName, person.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(name, lastName, age); + } +} diff --git a/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/utils/TestUtils.java b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/utils/TestUtils.java new file mode 100644 index 00000000000..b1b4373f369 --- /dev/null +++ b/addons/common/jbpm-usertask-storage-jpa/src/test/java/org/jbpm/usertask/jpa/mapper/utils/TestUtils.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.mapper.utils; + +import java.util.*; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.mapper.models.Person; +import org.jbpm.usertask.jpa.model.AttachmentEntity; +import org.jbpm.usertask.jpa.model.CommentEntity; +import org.jbpm.usertask.jpa.model.TaskDataEntity; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.kie.kogito.usertask.lifecycle.UserTaskState; +import org.kie.kogito.usertask.model.Attachment; +import org.kie.kogito.usertask.model.Comment; + +public class TestUtils { + + private TestUtils() { + } + + public static void assertUserTaskEntityData(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + Assertions.assertThat(userTaskInstanceEntity) + .hasFieldOrPropertyWithValue("id", userTaskInstance.getId()) + .hasFieldOrPropertyWithValue("userTaskId", userTaskInstance.getUserTaskId()) + .hasFieldOrPropertyWithValue("taskName", userTaskInstance.getTaskName()) + .hasFieldOrPropertyWithValue("taskDescription", userTaskInstance.getTaskDescription()) + .hasFieldOrPropertyWithValue("taskPriority", userTaskInstance.getTaskPriority()) + .hasFieldOrPropertyWithValue("status", userTaskInstance.getStatus().getName()) + .hasFieldOrPropertyWithValue("terminationType", userTaskInstance.getStatus().getTerminate().toString()) + .hasFieldOrPropertyWithValue("externalReferenceId", userTaskInstance.getExternalReferenceId()) + .hasFieldOrPropertyWithValue("actualOwner", userTaskInstance.getActualOwner()); + } + + public static void assertUserTaskInstanceData(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + UserTaskState.TerminationType terminationType = + Objects.isNull(userTaskInstanceEntity.getTerminationType()) ? null : UserTaskState.TerminationType.valueOf(userTaskInstanceEntity.getTerminationType()); + UserTaskState state = UserTaskState.of(userTaskInstanceEntity.getStatus(), terminationType); + + Assertions.assertThat(userTaskInstance) + .hasFieldOrPropertyWithValue("id", userTaskInstanceEntity.getId()) + .hasFieldOrPropertyWithValue("userTaskId", userTaskInstanceEntity.getUserTaskId()) + .hasFieldOrPropertyWithValue("taskName", userTaskInstanceEntity.getTaskName()) + .hasFieldOrPropertyWithValue("taskDescription", userTaskInstanceEntity.getTaskDescription()) + .hasFieldOrPropertyWithValue("taskPriority", userTaskInstanceEntity.getTaskPriority()) + .hasFieldOrPropertyWithValue("status", state) + .hasFieldOrPropertyWithValue("externalReferenceId", userTaskInstanceEntity.getExternalReferenceId()) + .hasFieldOrPropertyWithValue("actualOwner", userTaskInstanceEntity.getActualOwner()); + } + + public static void assertUserTaskEntityPotentialUserAndGroups(UserTaskInstanceEntity userTaskEntity, UserTaskInstance userTaskInstance) { + assertUserOrGroupsAssignments(userTaskEntity.getPotentialUsers(), userTaskInstance.getPotentialUsers()); + assertUserOrGroupsAssignments(userTaskEntity.getPotentialGroups(), userTaskInstance.getPotentialGroups()); + } + + public static void assertUserTaskEntityAdminUserAndGroups(UserTaskInstanceEntity userTaskEntity, UserTaskInstance userTaskInstance) { + assertUserOrGroupsAssignments(userTaskEntity.getAdminUsers(), userTaskInstance.getAdminUsers()); + assertUserOrGroupsAssignments(userTaskEntity.getAdminGroups(), userTaskInstance.getAdminGroups()); + } + + public static void assertUserTaskEntityExcludedUsers(UserTaskInstanceEntity userTaskEntity, UserTaskInstance userTaskInstance) { + assertUserOrGroupsAssignments(userTaskEntity.getExcludedUsers(), userTaskInstance.getExcludedUsers()); + } + + public static void assertUserTaskInstancePotentialUserAndGroups(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskEntity) { + assertUserOrGroupsAssignments(userTaskInstance.getPotentialUsers(), userTaskEntity.getPotentialUsers()); + assertUserOrGroupsAssignments(userTaskInstance.getPotentialGroups(), userTaskEntity.getPotentialGroups()); + } + + public static void assertUserTaskInstanceAdminUserAndGroups(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskEntity) { + assertUserOrGroupsAssignments(userTaskInstance.getAdminUsers(), userTaskEntity.getAdminUsers()); + assertUserOrGroupsAssignments(userTaskInstance.getAdminGroups(), userTaskEntity.getAdminGroups()); + } + + public static void assertUserTaskInstanceExcludedUsers(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskEntity) { + assertUserOrGroupsAssignments(userTaskInstance.getExcludedUsers(), userTaskEntity.getExcludedUsers()); + } + + private static void assertUserOrGroupsAssignments(Collection entityAssignments, Collection instanceAssignments) { + Assertions.assertThat(entityAssignments) + .hasSize(instanceAssignments.size()) + .containsExactlyInAnyOrder(instanceAssignments.toArray(new String[0])); + } + + public static void assertUserTaskEntityComments(Collection entityComments, Collection instanceComments) { + Assertions.assertThat(entityComments) + .hasSize(instanceComments.size()); + + entityComments.forEach(entityComment -> { + Optional optional = instanceComments.stream() + .filter(instanceComment -> instanceComment.getId().equals(entityComment.getId())) + .findFirst(); + + Assertions.assertThat(optional) + .isPresent(); + + Comment instanceComment = optional.get(); + + Assertions.assertThat(entityComment) + .hasFieldOrPropertyWithValue("id", instanceComment.getId()) + .hasFieldOrPropertyWithValue("comment", instanceComment.getContent()) + .hasFieldOrPropertyWithValue("updatedBy", instanceComment.getUpdatedBy()) + .matches(entity -> entity.getUpdatedAt().getTime() == instanceComment.getUpdatedAt().getTime()); + }); + } + + public static void assertUserTaskInstanceComments(Collection instanceComments, Collection entityComments) { + Assertions.assertThat(instanceComments) + .hasSize(instanceComments.size()); + + instanceComments.forEach(instanceComment -> { + Optional optional = entityComments.stream() + .filter(entityComment -> entityComment.getId().equals(instanceComment.getId())) + .findFirst(); + + Assertions.assertThat(optional) + .isPresent(); + + CommentEntity entityComment = optional.get(); + + Assertions.assertThat(instanceComment) + .hasFieldOrPropertyWithValue("id", entityComment.getId()) + .hasFieldOrPropertyWithValue("content", entityComment.getComment()) + .hasFieldOrPropertyWithValue("updatedBy", entityComment.getUpdatedBy()) + .matches(entity -> entity.getUpdatedAt().getTime() == entityComment.getUpdatedAt().getTime()); + }); + } + + public static void assertUserTaskEntityAttachments(Collection entityAttachments, Collection instanceAttachments) { + Assertions.assertThat(entityAttachments) + .hasSize(instanceAttachments.size()); + + entityAttachments.forEach(entityAttachment -> { + Optional optional = instanceAttachments.stream() + .filter(instanceAttachment -> instanceAttachment.getId().equals(entityAttachment.getId())) + .findFirst(); + + Assertions.assertThat(optional) + .isPresent(); + + Attachment instanceAttachment = optional.get(); + + Assertions.assertThat(entityAttachment) + .hasFieldOrPropertyWithValue("id", instanceAttachment.getId()) + .hasFieldOrPropertyWithValue("name", instanceAttachment.getName()) + .hasFieldOrPropertyWithValue("updatedBy", instanceAttachment.getUpdatedBy()) + .matches(entity -> entity.getUpdatedAt().getTime() == instanceAttachment.getUpdatedAt().getTime()) + .hasFieldOrPropertyWithValue("url", instanceAttachment.getContent().toString()); + }); + } + + public static void assertUserTaskInstanceAttachments(Collection instanceAttachments, Collection entityAttachments) { + Assertions.assertThat(instanceAttachments) + .hasSize(instanceAttachments.size()); + + instanceAttachments.forEach(instanceAttachment -> { + Optional optional = entityAttachments.stream() + .filter(entityAttachment -> entityAttachment.getId().equals(instanceAttachment.getId())) + .findFirst(); + + Assertions.assertThat(optional) + .isPresent(); + + AttachmentEntity entityAttachment = optional.get(); + + Assertions.assertThat(instanceAttachment) + .hasFieldOrPropertyWithValue("id", entityAttachment.getId()) + .hasFieldOrPropertyWithValue("name", entityAttachment.getName()) + .hasFieldOrPropertyWithValue("updatedBy", entityAttachment.getUpdatedBy()) + .matches(entity -> entity.getUpdatedAt().getTime() == entityAttachment.getUpdatedAt().getTime()) + .matches(entity -> entity.getContent().toString().equals(entityAttachment.getUrl())); + }); + } + + public static void assertUserTaskEntityInputs(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + assertUserTaskEntityMapData(userTaskInstanceEntity.getInputs(), userTaskInstance.getInputs()); + } + + public static void assertUserTaskEntityOutputs(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + assertUserTaskEntityMapData(userTaskInstanceEntity.getOutputs(), userTaskInstance.getOutputs()); + } + + public static void assertUserTaskEntityMetadata(UserTaskInstanceEntity userTaskInstanceEntity, UserTaskInstance userTaskInstance) { + assertUserTaskEntityMapData(userTaskInstanceEntity.getMetadata(), userTaskInstance.getMetadata()); + } + + private static void assertUserTaskEntityMapData(Collection entityData, Map instanceData) { + Assertions.assertThat(entityData.size()) + .isEqualTo(instanceData.size()); + + entityData.stream().forEach(entity -> { + Object value = instanceData.get(entity.getName()); + Assertions.assertThat(entity) + .isNotNull() + .matches(e -> instanceData.containsKey(e.getName())) + .matches(e -> Objects.isNull(e.getValue()) ? Objects.isNull(value) : e.getJavaType().equals(value.getClass().getName())); + }); + } + + public static void assertUserTaskInstanceInputs(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + assertUserTaskInstanceMapData(userTaskInstance.getInputs(), userTaskInstanceEntity.getInputs()); + } + + public static void assertUserTaskInstanceOutputs(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + assertUserTaskInstanceMapData(userTaskInstance.getOutputs(), userTaskInstanceEntity.getOutputs()); + } + + public static void assertUserTaskInstanceMetadata(UserTaskInstance userTaskInstance, UserTaskInstanceEntity userTaskInstanceEntity) { + assertUserTaskInstanceMapData(userTaskInstance.getMetadata(), userTaskInstanceEntity.getMetadata()); + } + + private static void assertUserTaskInstanceMapData(Map instanceData, Collection entityData) { + Assertions.assertThat(instanceData.size()) + .isEqualTo(entityData.size()); + + instanceData.forEach((key, value) -> { + Optional optional = entityData.stream().filter(data -> data.getName().equals(key)).findFirst(); + + Assertions.assertThat(optional) + .isPresent(); + + if (Objects.nonNull(value)) { + TaskDataEntity data = optional.get(); + Assertions.assertThat(value.getClass().getName()) + .isEqualTo(data.getJavaType()); + } + }); + } + + public static DefaultUserTaskInstance createUserTaskInstance() { + DefaultUserTaskInstance instance = new DefaultUserTaskInstance(); + instance.setId(UUID.randomUUID().toString()); + instance.setUserTaskId("user-task-id"); + instance.setTaskName("test-task"); + instance.setTaskDescription("this is a test task description"); + instance.setTaskPriority("1"); + instance.setStatus(UserTaskState.of("Complete", UserTaskState.TerminationType.COMPLETED)); + + instance.setActualOwner("Homer"); + instance.setPotentialUsers(Set.of("Bart", "Liza")); + instance.setPotentialGroups(Set.of("Simpson", "Family")); + instance.setAdminUsers(Set.of("Seymour")); + instance.setAdminGroups(Set.of("Administrators", "Managers")); + instance.setExcludedUsers(Set.of("Ned", "Bart")); + + instance.setExternalReferenceId("external-reference-id"); + + instance.setMetadata("ProcessId", "process-id"); + instance.setMetadata("ProcessType", "BPMN"); + instance.setMetadata("ProcessVersion", "1.0.0"); + instance.setMetadata("boolean", true); + instance.setMetadata("integer", 0); + instance.setMetadata("null", 0); + + instance.setInput("in_string", "hello this is a string"); + instance.setInput("in_integer", 1); + instance.setInput("in_long", 1000L); + instance.setInput("in_float", 1.02f); + instance.setInput("in_boolean", true); + instance.setInput("in_date", new Date()); + instance.setInput("in_person", new Person("Ned", "Stark", 50)); + instance.setInput("in_null", null); + + instance.setOutput("out_string", "hello this is an output string"); + instance.setOutput("out_integer", 12); + instance.setOutput("out_long", 2000L); + instance.setOutput("out_float", 3.5f); + instance.setOutput("out_boolean", false); + instance.setOutput("out_date", new Date()); + instance.setOutput("out_person", new Person("Jon", "Snow", 17)); + instance.setOutput("out_null", null); + + return instance; + } + + public static UserTaskInstanceEntity createUserTaskInstanceEntity() { + UserTaskInstanceEntity instance = new UserTaskInstanceEntity(); + + instance.setId(UUID.randomUUID().toString()); + instance.setUserTaskId("user-task-id"); + instance.setTaskName("test-task"); + instance.setTaskDescription("this is a test task description"); + instance.setTaskPriority("1"); + instance.setStatus("Complete"); + instance.setTerminationType(UserTaskState.TerminationType.COMPLETED.name()); + + instance.setActualOwner("Homer"); + instance.setPotentialUsers(Set.of("Bart")); + instance.setPotentialGroups(Set.of("Simpson", "Family")); + instance.setAdminUsers(Set.of("Seymour")); + instance.setAdminGroups(Set.of("Administrators", "Managers")); + instance.setExcludedUsers(Set.of("Ned")); + + instance.setExternalReferenceId("external-reference-id"); + + return instance; + } +} diff --git a/addons/common/pom.xml b/addons/common/pom.xml index 1a12e57c0c9..fe7ccb1dcf6 100644 --- a/addons/common/pom.xml +++ b/addons/common/pom.xml @@ -55,6 +55,7 @@ task-management marshallers flyway + jbpm-usertask-storage-jpa diff --git a/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviders.java b/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviders.java index 2d1073893f7..8d560bc801f 100644 --- a/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviders.java +++ b/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviders.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; public class IdentityProviders { @@ -52,8 +53,8 @@ public boolean hasRole(String role) { } - public static IdentityProvider of(String name) { - return new DefaultIdentityProvider(name, Collections.emptyList()); + public static IdentityProvider of(String name, String... roles) { + return new DefaultIdentityProvider(name, List.of(roles)); } public static IdentityProvider of(String name, Collection roles) { diff --git a/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstance.java b/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstance.java index 9e101eb9d4b..e28e23befed 100644 --- a/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstance.java +++ b/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstance.java @@ -39,7 +39,7 @@ public interface UserTaskInstance { boolean hasActualOwner(); - void setActuaOwner(String actualOwner); + void setActualOwner(String actualOwner); String getActualOwner(); diff --git a/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstances.java b/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstances.java index 7f294b15fed..8372c4e2747 100644 --- a/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstances.java +++ b/api/kogito-api/src/main/java/org/kie/kogito/usertask/UserTaskInstances.java @@ -40,6 +40,6 @@ public interface UserTaskInstances { UserTaskInstance update(UserTaskInstance userTaskInstance); - UserTaskInstance remove(String userTaskInstanceId); + UserTaskInstance remove(UserTaskInstance userTaskInstanceId); } diff --git a/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandler.java b/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandler.java index 71d34053787..cf30b7ed2bf 100644 --- a/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandler.java +++ b/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandler.java @@ -73,10 +73,14 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage UserTask userTask = userTasks.userTaskById((String) workItem.getParameter(KogitoWorkItem.PARAMETER_UNIQUE_TASK_ID)); DefaultUserTaskInstance instance = (DefaultUserTaskInstance) userTask.createInstance(); + + instance.setExternalReferenceId(workItem.getStringId()); + + userTask.instances().create(instance); + instance.setTaskName((String) workItem.getParameter(TASK_NAME)); instance.setTaskDescription((String) workItem.getParameter(DESCRIPTION)); instance.setTaskPriority(priority != null ? priority.toString() : null); - instance.setExternalReferenceId(workItem.getStringId()); instance.setMetadata("ProcessId", workItem.getProcessInstance().getProcessId()); instance.setMetadata("ProcessType", workItem.getProcessInstance().getProcess().getType()); @@ -87,7 +91,6 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage instance.setMetadata("RootProcessInstanceId", workItem.getProcessInstance().getRootProcessInstanceId()); instance.setMetadata("ParentProcessInstanceId", workItem.getProcessInstance().getParentProcessInstanceId()); - userTask.instances().create(instance); instance.fireInitialStateChange(); workItem.getParameters().entrySet().stream().filter(e -> !HumanTaskNode.TASK_PARAMETERS.contains(e.getKey())).forEach(e -> instance.setInput(e.getKey(), e.getValue())); @@ -109,6 +112,9 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage @Override public Optional completeWorkItemHandler(KogitoWorkItemManager manager, KogitoWorkItemHandler handler, KogitoWorkItem workItem, WorkItemTransition transition) { + if (transition.data().containsKey("Notify")) { + return Optional.empty(); + } UserTasks userTasks = handler.getApplication().get(UserTasks.class); UserTask userTask = userTasks.userTaskById((String) workItem.getParameter(KogitoWorkItem.PARAMETER_UNIQUE_TASK_ID)); userTask.instances().findById(workItem.getExternalReferenceId()).ifPresent(ut -> { @@ -122,6 +128,9 @@ public Optional completeWorkItemHandler(KogitoWorkItemManage @Override public Optional abortWorkItemHandler(KogitoWorkItemManager manager, KogitoWorkItemHandler handler, KogitoWorkItem workItem, WorkItemTransition transition) { + if (transition.data().containsKey("Notify")) { + return Optional.empty(); + } UserTasks userTasks = handler.getApplication().get(UserTasks.class); UserTask userTask = userTasks.userTaskById((String) workItem.getParameter(KogitoWorkItem.PARAMETER_UNIQUE_TASK_ID)); userTask.instances().findById(workItem.getExternalReferenceId()).ifPresent(ut -> { diff --git a/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandlerProcessListener.java b/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandlerProcessListener.java index 7d0bc70557a..116eb8716fd 100644 --- a/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandlerProcessListener.java +++ b/jbpm/jbpm-usertask-workitem/src/main/java/org/kie/kogito/jbpm/usertask/handler/UserTaskKogitoWorkItemHandlerProcessListener.java @@ -59,6 +59,7 @@ public void onUserTaskState(UserTaskStateEvent event) { processes.processById(processId).instances().findById(processInstanceId).ifPresent(pi -> { Map data = new HashMap<>(event.getUserTaskInstance().getOutputs()); data.put("ActorId", event.getUserTaskInstance().getActualOwner()); + data.put("Notify", false); pi.completeWorkItem(event.getUserTaskInstance().getExternalReferenceId(), data); }); diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/AbstractUserTask.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/AbstractUserTask.java index 68f85aceb3f..795c0f25e9c 100644 --- a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/AbstractUserTask.java +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/AbstractUserTask.java @@ -74,8 +74,9 @@ public UserTaskInstance createInstance() { instance.setPotentialUsers(getPotentialUsers()); instance.setPotentialGroups(getPotentialGroups()); instance.setAdminUsers(getAdminUsers()); - instance.setPotentialGroups(getPotentialGroups()); + instance.setAdminGroups(getAdminGroups()); instance.setExcludedUsers(getExcludedUsers()); + instance.setInstances(this.instances()); return instance; } diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java index fd6d3ce21c3..266e674887e 100644 --- a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java @@ -18,15 +18,7 @@ */ package org.kie.kogito.usertask.impl; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.internal.usertask.event.KogitoUserTaskEventSupport; @@ -74,7 +66,7 @@ public class DefaultUserTaskInstance implements UserTaskInstance { @JsonIgnore private KogitoUserTaskEventSupport userTaskEventSupport; @JsonIgnore - private UserTaskLifeCycle setUserTaskLifeCycle; + private UserTaskLifeCycle userTaskLifeCycle; public DefaultUserTaskInstance() { this.inputs = new HashMap<>(); @@ -94,7 +86,6 @@ public DefaultUserTaskInstance(UserTask userTask) { this(); this.id = UUID.randomUUID().toString(); this.userTask = userTask; - this.instances = userTask.instances(); } public void setUserTaskEventSupport(KogitoUserTaskEventSupport userTaskEventSupport) { @@ -102,7 +93,7 @@ public void setUserTaskEventSupport(KogitoUserTaskEventSupport userTaskEventSupp } public void setUserTaskLifeCycle(UserTaskLifeCycle userTaskLifeCycle) { - this.setUserTaskLifeCycle = userTaskLifeCycle; + this.userTaskLifeCycle = userTaskLifeCycle; } public void setInstances(UserTaskInstances instances) { @@ -142,7 +133,7 @@ public boolean hasActualOwner() { } @Override - public void setActuaOwner(String actualOwner) { + public void setActualOwner(String actualOwner) { this.actualOwner = actualOwner; if (this.userTaskEventSupport != null) { this.userTaskEventSupport.fireOneUserTaskStateChange(this, this.status, this.status); @@ -166,15 +157,14 @@ public void setExternalReferenceId(String externalReferenceId) { @Override public void transition(String transitionId, Map data, IdentityProvider identity) { - Optional next = Optional.of(this.setUserTaskLifeCycle.newTransitionToken(transitionId, this, data)); + Optional next = Optional.of(this.userTaskLifeCycle.newTransitionToken(transitionId, this, data)); while (next.isPresent()) { UserTaskTransitionToken transition = next.get(); - next = this.setUserTaskLifeCycle.transition(this, transition, identity); + next = this.userTaskLifeCycle.transition(this, transition, identity); this.status = transition.target(); - this.updatePersistenceOrRemove(); this.userTaskEventSupport.fireOneUserTaskStateChange(this, transition.source(), transition.target()); } - + this.updatePersistenceOrRemove(); } private void updatePersistence() { @@ -185,7 +175,7 @@ private void updatePersistence() { private void updatePersistenceOrRemove() { if (this.status.isTerminate()) { - this.instances.remove(this.id); + this.instances.remove(this); } else { this.instances.update(this); } @@ -205,7 +195,7 @@ public Map getInputs() { } public void setInputs(Map inputs) { - inputs.forEach(this::setInput); + this.inputs = inputs; } public Map getOutputs() { @@ -213,7 +203,7 @@ public Map getOutputs() { } public void setOutputs(Map outputs) { - outputs.forEach(this::setOutput); + this.outputs = outputs; } @Override @@ -451,8 +441,6 @@ public Attachment removeAttachment(Attachment attachment) { public void setAttachments(List attachments) { this.attachments = attachments; - updatePersistence(); - } /** @@ -509,7 +497,6 @@ public Comment removeComment(Comment comment) { public void setComments(List comments) { this.comments = comments; - updatePersistence(); } public void setMetadata(String key, Object value) { @@ -523,7 +510,6 @@ public Map getMetadata() { public void setMetadata(Map metadata) { this.metadata = metadata; - updatePersistence(); } @Override diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/InMemoryUserTaskInstances.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/InMemoryUserTaskInstances.java index def2088da39..256144c4849 100644 --- a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/InMemoryUserTaskInstances.java +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/InMemoryUserTaskInstances.java @@ -161,14 +161,15 @@ public UserTaskInstance update(UserTaskInstance userTaskInstance) { } @Override - public UserTaskInstance remove(String userTaskInstanceId) { + public UserTaskInstance remove(UserTaskInstance userTaskInstance) { try { - if (!userTaskInstances.containsKey(userTaskInstanceId)) { + if (!userTaskInstances.containsKey(userTaskInstance.getId())) { return null; } - return disconnectUserTaskInstance.apply(mapper.readValue(userTaskInstances.remove(userTaskInstanceId), DefaultUserTaskInstance.class)); + userTaskInstances.remove(userTaskInstance.getId()); + return disconnectUserTaskInstance.apply(userTaskInstance); } catch (Exception e) { - LOG.error("during remove {}", userTaskInstanceId, e); + LOG.error("during remove {}", userTaskInstance, e); return null; } } diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskLifeCycle.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskLifeCycle.java index a28a5accfde..6af8a612fcf 100644 --- a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskLifeCycle.java +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskLifeCycle.java @@ -127,9 +127,9 @@ public Optional activate(UserTaskInstance userTaskInsta public Optional claim(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { if (token.data().containsKey(PARAMETER_USER)) { - defaultUserTaskInstance.setActuaOwner((String) token.data().get(PARAMETER_USER)); + defaultUserTaskInstance.setActualOwner((String) token.data().get(PARAMETER_USER)); } else { - defaultUserTaskInstance.setActuaOwner(identityProvider.getName()); + defaultUserTaskInstance.setActualOwner(identityProvider.getName()); } } return Optional.empty(); @@ -137,7 +137,7 @@ public Optional claim(UserTaskInstance userTaskInstance public Optional release(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { - defaultUserTaskInstance.setActuaOwner(null); + defaultUserTaskInstance.setActualOwner(null); } return Optional.empty(); } diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskTransitionToken.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskTransitionToken.java index f8cfd7733fc..56561c9ca51 100644 --- a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskTransitionToken.java +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/DefaultUserTaskTransitionToken.java @@ -19,6 +19,7 @@ package org.kie.kogito.usertask.impl.lifecycle; import java.util.Map; +import java.util.Objects; import org.kie.kogito.usertask.lifecycle.UserTaskState; import org.kie.kogito.usertask.lifecycle.UserTaskTransitionToken; @@ -34,7 +35,7 @@ public DefaultUserTaskTransitionToken(String transition, UserTaskState source, U this.transition = transition; this.source = source; this.target = target; - this.data = data; + this.data = Objects.isNull(data) ? Map.of() : data; } @Override diff --git a/kogito-bom/pom.xml b/kogito-bom/pom.xml index 6e4af07c46d..13acf8c5126 100755 --- a/kogito-bom/pom.xml +++ b/kogito-bom/pom.xml @@ -781,18 +781,6 @@ sources - - org.kie - kie-addons-springboot-flyway - ${project.version} - - - org.kie - kie-addons-springboot-flyway - ${project.version} - sources - - org.kie.kogito @@ -1732,6 +1720,59 @@ sources + + + org.jbpm + jbpm-addons-usertask-storage-jpa + ${project.version} + + + org.jbpm + jbpm-addons-usertask-storage-jpa + ${project.version} + sources + + + org.jbpm + jbpm-addons-usertask-storage-jpa + ${project.version} + test-jar + test + + + org.jbpm + jbpm-addons-quarkus-usertask-storage-jpa + ${project.version} + + + org.jbpm + jbpm-addons-quarkus-usertask-storage-jpa + ${project.version} + sources + + + org.jbpm + jbpm-addons-quarkus-usertask-storage-jpa-deployment + ${project.version} + + + org.jbpm + jbpm-addons-quarkus-usertask-storage-jpa-deployment + ${project.version} + sources + + + org.jbpm + jbpm-addons-springboot-usertask-storage-jpa + ${project.version} + + + org.jbpm + jbpm-addons-springboot-usertask-storage-jpa + ${project.version} + sources + + org.kie @@ -1766,7 +1807,17 @@ ${project.version} sources - + + org.kie + kie-addons-springboot-flyway + ${project.version} + + + org.kie + kie-addons-springboot-flyway + ${project.version} + sources + diff --git a/kogito-build/kogito-dependencies-bom/pom.xml b/kogito-build/kogito-dependencies-bom/pom.xml index 26fae00aa0d..f085d103e0a 100644 --- a/kogito-build/kogito-dependencies-bom/pom.xml +++ b/kogito-build/kogito-dependencies-bom/pom.xml @@ -108,6 +108,8 @@ 7.10.2 3.1.0 + 3.1.0 + 6.2.7.Final 24.0.4 @@ -443,6 +445,12 @@ ${version.jakarta.ws.rs} + + jakarta.persistence + jakarta.persistence-api + ${version.jakarta.persistence-api} + + org.junit.jupiter junit-jupiter-api diff --git a/quarkus/addons/flyway/deployment/pom.xml b/quarkus/addons/flyway/deployment/pom.xml index 1e6eefccdc8..de160bad027 100644 --- a/quarkus/addons/flyway/deployment/pom.xml +++ b/quarkus/addons/flyway/deployment/pom.xml @@ -50,6 +50,7 @@ io.quarkus quarkus-agroal-deployment + org.mockito diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/deployment/pom.xml b/quarkus/addons/jbpm-usertask-storage-jpa/deployment/pom.xml new file mode 100644 index 00000000000..fe131a6e27f --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/deployment/pom.xml @@ -0,0 +1,110 @@ + + + + + jbpm-addon-quarkus-usertask-storage-jpa-parent + org.jbpm + 999-SNAPSHOT + + 4.0.0 + + jbpm-addons-quarkus-usertask-storage-jpa-deployment + jBPM :: Add-Ons :: Quarkus :: User Task Storage JPA :: Deployment + jBPM Add-On Quarkus User Task Storage JPA Deployment + + + UTF-8 + org.jbpm.usertask.storage.jpa.quarkus.deployment + + + + + org.jbpm + jbpm-addons-quarkus-usertask-storage-jpa + + + org.kie + kogito-addons-quarkus-common-deployment + + + org.kie + kie-addons-quarkus-flyway-deployment + + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-agroal-deployment + + + io.quarkus + quarkus-hibernate-orm-deployment + + + + io.quarkus + quarkus-junit5 + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-junit5-mockito + test + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${version.io.quarkus} + + + + + + + \ No newline at end of file diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/deployment/src/main/java/org/jbpm/usertask/storage/jpa/quarkus/deployment/JBPMUserTaskStorageJPAExtensionProcessor.java b/quarkus/addons/jbpm-usertask-storage-jpa/deployment/src/main/java/org/jbpm/usertask/storage/jpa/quarkus/deployment/JBPMUserTaskStorageJPAExtensionProcessor.java new file mode 100644 index 00000000000..6d7087a4716 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/deployment/src/main/java/org/jbpm/usertask/storage/jpa/quarkus/deployment/JBPMUserTaskStorageJPAExtensionProcessor.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.storage.jpa.quarkus.deployment; + +import org.kie.kogito.quarkus.addons.common.deployment.KogitoCapability; +import org.kie.kogito.quarkus.addons.common.deployment.RequireCapabilityKogitoAddOnProcessor; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +public class JBPMUserTaskStorageJPAExtensionProcessor extends RequireCapabilityKogitoAddOnProcessor { + + private static final String FEATURE = "jbpm-addon-usertask-storage-jpa"; + + public JBPMUserTaskStorageJPAExtensionProcessor() { + super(KogitoCapability.PROCESSES); + } + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/pom.xml b/quarkus/addons/jbpm-usertask-storage-jpa/pom.xml new file mode 100644 index 00000000000..1fc1709a355 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/pom.xml @@ -0,0 +1,45 @@ + + + + + kogito-addons-quarkus-parent + org.kie + 999-SNAPSHOT + + 4.0.0 + + org.jbpm + jbpm-addon-quarkus-usertask-storage-jpa-parent + + jBPM :: Add-Ons :: Quarkus :: User Task Storage JPA :: Parent + jBPM Add-On Quarkus User Task Storage JPA Parent + + pom + + + deployment + runtime + + + \ No newline at end of file diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/pom.xml b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/pom.xml new file mode 100644 index 00000000000..e7808950852 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/pom.xml @@ -0,0 +1,170 @@ + + + + + jbpm-addon-quarkus-usertask-storage-jpa-parent + org.jbpm + 999-SNAPSHOT + + 4.0.0 + + jbpm-addons-quarkus-usertask-storage-jpa + jBPM :: Add-Ons :: Quarkus :: User Task Storage JPA :: Runtime + jBPM Add-On Quarkus User Task Storage JPA Runtime + + + UTF-8 + org.jbpm.usertask.storage.jpa.quarkus + + + + + org.jbpm + jbpm-addons-usertask-storage-jpa + + + org.kie + kie-addons-quarkus-flyway + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-hibernate-orm + + + + io.quarkus + quarkus-agroal + test + + + io.quarkus + quarkus-jdbc-postgresql + test + + + io.quarkus + quarkus-jdbc-h2 + test + + + io.quarkus + quarkus-test-h2 + test + + + io.quarkus + quarkus-junit5 + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + io.quarkus + quarkus-junit5-mockito + test + + + org.kie.kogito + kogito-quarkus-test-utils + test + + + org.jbpm + jbpm-addons-usertask-storage-jpa + test-jar + test + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${version.io.quarkus} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + org.jbpm.addons.usertask.storage + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${version.io.quarkus} + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.jboss.logmanager.LogManager + + + + + default + + integration-test + + + + verify + + verify + + + + + + + + \ No newline at end of file diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/QuarkusJPAUserTaskInstances.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/QuarkusJPAUserTaskInstances.java new file mode 100644 index 00000000000..d1cdbef5eb4 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/QuarkusJPAUserTaskInstances.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus; + +import org.jbpm.usertask.jpa.JPAUserTaskInstances; +import org.jbpm.usertask.jpa.mapper.UserTaskInstanceEntityMapper; +import org.jbpm.usertask.jpa.repository.UserTaskInstanceRepository; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +@Transactional +@ApplicationScoped +public class QuarkusJPAUserTaskInstances extends JPAUserTaskInstances { + + QuarkusJPAUserTaskInstances() { + super(null, null); + } + + @Inject + public QuarkusJPAUserTaskInstances(UserTaskInstanceRepository userTaskInstanceRepository, UserTaskInstanceEntityMapper userTaskInstanceEntityMapper) { + super(userTaskInstanceRepository, userTaskInstanceEntityMapper); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusAttachmentsEntityMapper.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusAttachmentsEntityMapper.java new file mode 100644 index 00000000000..cb642a4334c --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusAttachmentsEntityMapper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.mapper; + +import org.jbpm.usertask.jpa.mapper.AttachmentsEntityMapper; +import org.jbpm.usertask.jpa.repository.AttachmentRepository; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusAttachmentsEntityMapper extends AttachmentsEntityMapper { + + QuarkusAttachmentsEntityMapper() { + this(null); + } + + @Inject + public QuarkusAttachmentsEntityMapper(AttachmentRepository attachmentRepository) { + super(attachmentRepository); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusCommentsEntityMapper.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusCommentsEntityMapper.java new file mode 100644 index 00000000000..0024b10a31e --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusCommentsEntityMapper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.mapper; + +import org.jbpm.usertask.jpa.mapper.CommentsEntityMapper; +import org.jbpm.usertask.jpa.repository.CommentRepository; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusCommentsEntityMapper extends CommentsEntityMapper { + + public QuarkusCommentsEntityMapper() { + this(null); + } + + @Inject + public QuarkusCommentsEntityMapper(CommentRepository commentRepository) { + super(commentRepository); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskInputsEntityMapper.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskInputsEntityMapper.java new file mode 100644 index 00000000000..1304468655e --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskInputsEntityMapper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.mapper; + +import org.jbpm.usertask.jpa.mapper.TaskInputsEntityMapper; +import org.jbpm.usertask.jpa.repository.TaskInputRepository; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusTaskInputsEntityMapper extends TaskInputsEntityMapper { + + public QuarkusTaskInputsEntityMapper() { + this(null); + } + + @Inject + public QuarkusTaskInputsEntityMapper(TaskInputRepository repository) { + super(repository); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskMetadataEntityMapper.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskMetadataEntityMapper.java new file mode 100644 index 00000000000..fdc5b94360a --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskMetadataEntityMapper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.mapper; + +import org.jbpm.usertask.jpa.mapper.TaskMetadataEntityMapper; +import org.jbpm.usertask.jpa.repository.TaskMetadataRepository; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusTaskMetadataEntityMapper extends TaskMetadataEntityMapper { + + public QuarkusTaskMetadataEntityMapper() { + this(null); + } + + @Inject + public QuarkusTaskMetadataEntityMapper(TaskMetadataRepository repository) { + super(repository); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskOutputsEntityMapper.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskOutputsEntityMapper.java new file mode 100644 index 00000000000..0d80b25e24b --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusTaskOutputsEntityMapper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.mapper; + +import org.jbpm.usertask.jpa.mapper.TaskOutputsEntityMapper; +import org.jbpm.usertask.jpa.repository.TaskOutputRepository; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusTaskOutputsEntityMapper extends TaskOutputsEntityMapper { + + public QuarkusTaskOutputsEntityMapper() { + this(null); + } + + @Inject + public QuarkusTaskOutputsEntityMapper(TaskOutputRepository repository) { + super(repository); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusUserTaskInstanceEntityMapper.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusUserTaskInstanceEntityMapper.java new file mode 100644 index 00000000000..e3f6c79e1ad --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/mapper/QuarkusUserTaskInstanceEntityMapper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.mapper; + +import org.jbpm.usertask.jpa.mapper.*; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusUserTaskInstanceEntityMapper extends UserTaskInstanceEntityMapper { + + QuarkusUserTaskInstanceEntityMapper() { + this(null, null, null, null, null); + } + + @Inject + public QuarkusUserTaskInstanceEntityMapper(AttachmentsEntityMapper attachmentsMapper, CommentsEntityMapper commentsMapper, TaskMetadataEntityMapper taskMetadataEntityMapper, + TaskInputsEntityMapper taskInputsEntityMapper, TaskOutputsEntityMapper taskOutputsEntityMapper) { + super(attachmentsMapper, commentsMapper, taskMetadataEntityMapper, taskInputsEntityMapper, taskOutputsEntityMapper); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusAttachmentRepository.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusAttachmentRepository.java new file mode 100644 index 00000000000..04a551660d5 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusAttachmentRepository.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.repository; + +import org.jbpm.usertask.jpa.repository.AttachmentRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +@ApplicationScoped +@Transactional +public class QuarkusAttachmentRepository extends AttachmentRepository { + + QuarkusAttachmentRepository() { + this(null); + } + + @Inject + public QuarkusAttachmentRepository(UserTaskJPAContext context) { + super(context); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusCommentRepository.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusCommentRepository.java new file mode 100644 index 00000000000..7bfecdb4c2e --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusCommentRepository.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.repository; + +import org.jbpm.usertask.jpa.repository.CommentRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +@ApplicationScoped +@Transactional +public class QuarkusCommentRepository extends CommentRepository { + + QuarkusCommentRepository() { + this(null); + } + + @Inject + public QuarkusCommentRepository(UserTaskJPAContext context) { + super(context); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskInputEntityRepository.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskInputEntityRepository.java new file mode 100644 index 00000000000..41818d7e0c7 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskInputEntityRepository.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.repository; + +import org.jbpm.usertask.jpa.repository.TaskInputRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusTaskInputEntityRepository extends TaskInputRepository { + + QuarkusTaskInputEntityRepository() { + this(null); + } + + @Inject + public QuarkusTaskInputEntityRepository(UserTaskJPAContext context) { + super(context); + } + +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskMetadataEntityRepository.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskMetadataEntityRepository.java new file mode 100644 index 00000000000..6ff95bdf79c --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskMetadataEntityRepository.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.repository; + +import org.jbpm.usertask.jpa.repository.TaskMetadataRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusTaskMetadataEntityRepository extends TaskMetadataRepository { + + QuarkusTaskMetadataEntityRepository() { + this(null); + } + + @Inject + public QuarkusTaskMetadataEntityRepository(UserTaskJPAContext context) { + super(context); + } + +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskOutputEntityRepository.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskOutputEntityRepository.java new file mode 100644 index 00000000000..7810b987848 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusTaskOutputEntityRepository.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.repository; + +import org.jbpm.usertask.jpa.repository.TaskOutputRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusTaskOutputEntityRepository extends TaskOutputRepository { + + QuarkusTaskOutputEntityRepository() { + this(null); + } + + @Inject + public QuarkusTaskOutputEntityRepository(UserTaskJPAContext context) { + super(context); + } + +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskInstanceRepository.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskInstanceRepository.java new file mode 100644 index 00000000000..2a487fb8bfb --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskInstanceRepository.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.repository; + +import org.jbpm.usertask.jpa.repository.UserTaskInstanceRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +@ApplicationScoped +@Transactional +public class QuarkusUserTaskInstanceRepository extends UserTaskInstanceRepository { + + QuarkusUserTaskInstanceRepository() { + this(null); + } + + @Inject + public QuarkusUserTaskInstanceRepository(UserTaskJPAContext context) { + super(context); + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskJPAContext.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskJPAContext.java new file mode 100644 index 00000000000..bcbfb1e366b --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/java/org/jbpm/usertask/jpa/quarkus/repository/QuarkusUserTaskJPAContext.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus.repository; + +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; + +@ApplicationScoped +@Transactional +public class QuarkusUserTaskJPAContext implements UserTaskJPAContext { + + @PersistenceContext + private EntityManager em; + + @Override + public EntityManager getEntityManager() { + return em; + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/beans.xml b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..a0eb9fbf8cd --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/beans.xml @@ -0,0 +1,20 @@ + diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000..4983be32275 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,37 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +name: jBPM User Task Storage JPS +description: Add-On that enables JPA storage for jBPM User Tasks +metadata: + keywords: + - KIE + - jBPM + - persistence + - JPA + guide: https://quarkus.io/guides/kie + categories: + - "business-automation" + - "cloud" + - "jBPM" + - "persistence" + - "JPA" + status: "stable" + config: + - "org.jbpm" diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/BaseQuarkusJPAUserTaskInstancesTest.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/BaseQuarkusJPAUserTaskInstancesTest.java new file mode 100644 index 00000000000..1bfcc273af6 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/BaseQuarkusJPAUserTaskInstancesTest.java @@ -0,0 +1,462 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.function.Function; + +import org.assertj.core.api.Assertions; +import org.jbpm.usertask.jpa.JPAUserTaskInstances; +import org.jbpm.usertask.jpa.mapper.utils.TestUtils; +import org.jbpm.usertask.jpa.model.UserTaskInstanceEntity; +import org.jbpm.usertask.jpa.quarkus.repository.QuarkusUserTaskJPAContext; +import org.jbpm.usertask.jpa.repository.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kie.kogito.auth.IdentityProviders; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.kie.kogito.usertask.model.Attachment; +import org.kie.kogito.usertask.model.Comment; +import org.mockito.Mockito; + +import jakarta.inject.Inject; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public abstract class BaseQuarkusJPAUserTaskInstancesTest { + + @Inject + QuarkusUserTaskJPAContext context; + + @Inject + JPAUserTaskInstances userTaskInstances; + + @Inject + UserTaskInstanceRepository userTaskInstanceRepository; + + @Inject + AttachmentRepository attachmentRepository; + + @Inject + CommentRepository commentRepository; + + private Function connect; + private Function disconnect; + + @BeforeEach + public void init() { + connect = Mockito.mock(Function.class); + disconnect = Mockito.mock(Function.class); + + when(connect.apply(any(UserTaskInstance.class))).thenAnswer(i -> i.getArgument(0)); + when(disconnect.apply(any(UserTaskInstance.class))).thenAnswer(i -> i.getArgument(0)); + + userTaskInstances.setReconnectUserTaskInstance(connect); + userTaskInstances.setDisconnectUserTaskInstance(disconnect); + } + + @Test + public void testCreateUserTask() { + UserTaskInstance instance = createUserTaskInstance(); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + + Assertions.assertThat(userTaskInstances.findById(instance.getId())) + .isNotNull() + .isEmpty(); + + userTaskInstances.create(instance); + + verify(connect, times(1)).apply(any(UserTaskInstance.class)); + + Optional entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + Assertions.assertThat(entityOptional) + .isNotNull() + .isPresent(); + + UserTaskInstanceEntity entity = entityOptional.get(); + + assertEntityAndInstance(entity, instance); + + Optional persistedInstanceOptional = userTaskInstances.findById(instance.getId()); + + Assertions.assertThat(persistedInstanceOptional) + .isNotNull() + .isPresent(); + + assertEntityAndInstance(entity, persistedInstanceOptional.get()); + } + + @Test + public void testCreateExistingTask() { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + Assertions.assertThatThrownBy(() -> userTaskInstances.create(instance)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Task Already exists."); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testEditTaskInputOutputs() { + + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + Optional entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + Assertions.assertThat(entityOptional) + .isNotNull() + .isPresent(); + + UserTaskInstanceEntity entity = entityOptional.get(); + + Assertions.assertThat(entity.getInputs()) + .hasSize(instance.getInputs().size()); + + Assertions.assertThat(entity.getOutputs()) + .hasSize(instance.getOutputs().size()); + + instance.getInputs().clear(); + instance.setInput("new_input", "this is a new input"); + + instance.getOutputs().clear(); + instance.setOutput("new_output", "this is a new output"); + + userTaskInstances.update(instance); + + entity = userTaskInstanceRepository.findById(instance.getId()).get(); + + Assertions.assertThat(entity.getInputs()) + .hasSize(1); + + Assertions.assertThat(entity.getOutputs()) + .hasSize(1); + + TestUtils.assertUserTaskEntityInputs(entity, instance); + TestUtils.assertUserTaskEntityOutputs(entity, instance); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + + } + + @Test + public void testFindByIdentityByActualOwner() { + + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + List result = userTaskInstances.findByIdentity(IdentityProviders.of("Homer", "Group")); + + Assertions.assertThat(result) + .hasSize(1); + + verify(connect, times(2)).apply(any(UserTaskInstance.class)); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testFindByIdentityByPotentialOwners() { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + List result = userTaskInstances.findByIdentity(IdentityProviders.of("Liza", "Group")); + + Assertions.assertThat(result) + .hasSize(1); + + verify(connect, times(2)).apply(any(UserTaskInstance.class)); + + List result2 = userTaskInstances.findByIdentity(IdentityProviders.of("Bart", "Simpson")); + + Assertions.assertThat(result2) + .hasSize(1); + + verify(connect, times(3)).apply(any(UserTaskInstance.class)); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testFindByIdentityByPotentialGroups() { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + List result = userTaskInstances.findByIdentity(IdentityProviders.of("Abraham", "Admin", "Simpson")); + + Assertions.assertThat(result) + .hasSize(1); + + verify(connect, times(2)).apply(any(UserTaskInstance.class)); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testFindByIdentityByAdminUsers() { + + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + List result = userTaskInstances.findByIdentity(IdentityProviders.of("Seymour", "Group")); + + Assertions.assertThat(result) + .hasSize(1); + + verify(connect, times(2)).apply(any(UserTaskInstance.class)); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testFindByIdentityByAdminGroups() { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + List result = userTaskInstances.findByIdentity(IdentityProviders.of("Abraham", "Administrator", "Managers")); + + Assertions.assertThat(result) + .hasSize(1); + + verify(connect, times(2)).apply(any(UserTaskInstance.class)); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testFindByIdentityByExcludedUser() { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + List result = userTaskInstances.findByIdentity(IdentityProviders.of("Ned")); + + Assertions.assertThat(result) + .hasSize(0); + + verify(connect, times(1)).apply(any(UserTaskInstance.class)); + + result = userTaskInstances.findByIdentity(IdentityProviders.of("Bart")); + + Assertions.assertThat(result) + .hasSize(0); + + verify(connect, times(1)).apply(any(UserTaskInstance.class)); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testFindByIdentityByUnknownUser() { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + List result = userTaskInstances.findByIdentity(IdentityProviders.of("Someone", "Group")); + + Assertions.assertThat(result) + .hasSize(0); + + verify(connect, times(1)).apply(any(UserTaskInstance.class)); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testAttachments() throws URISyntaxException { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + Optional entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + Assertions.assertThat(entityOptional) + .isNotNull() + .isPresent(); + + assertEntityAndInstance(entityOptional.get(), instance); + + Attachment attachment = new Attachment("1", "Admin"); + attachment.setName("attachment 1"); + attachment.setContent(new URI("http://url.com/to/my/attachment")); + attachment.setUpdatedAt(new Date()); + + instance.addAttachment(attachment); + + entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + TestUtils.assertUserTaskEntityAttachments(entityOptional.get().getAttachments(), instance.getAttachments()); + + Attachment attachment2 = new Attachment("2", "Admin"); + attachment2.setName("attachment 2"); + attachment2.setContent(new URI("http://url.com/to/my/attachment2")); + attachment2.setUpdatedAt(new Date()); + + instance.addAttachment(attachment2); + + entityOptional = userTaskInstanceRepository.findById(instance.getId()); + TestUtils.assertUserTaskEntityAttachments(entityOptional.get().getAttachments(), instance.getAttachments()); + + instance.removeAttachment(attachment); + instance.removeAttachment(attachment2); + + userTaskInstances.update(instance); + + entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + Assertions.assertThat(entityOptional.get().getAttachments()) + .isEmpty(); + + Assertions.assertThat(attachmentRepository.findAll()) + .isEmpty(); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + @Test + public void testComments() { + UserTaskInstance instance = createUserTaskInstance(); + + userTaskInstances.create(instance); + + Optional entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + Assertions.assertThat(entityOptional) + .isNotNull() + .isPresent(); + + assertEntityAndInstance(entityOptional.get(), instance); + + Comment comment = new Comment("1", "Admin"); + comment.setContent("This the comment 1"); + comment.setUpdatedAt(new Date()); + + instance.addComment(comment); + + entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + UserTaskInstanceEntity userTaskInstanceEntity = entityOptional.get(); + + Assertions.assertThat(userTaskInstanceEntity.getComments()) + .hasSize(1); + + TestUtils.assertUserTaskEntityComments(entityOptional.get().getComments(), instance.getComments()); + + Comment comment2 = new Comment("2", "Admin"); + comment2.setContent("This the comment 2"); + comment2.setUpdatedAt(new Date()); + + instance.addComment(comment2); + + entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + userTaskInstanceEntity = entityOptional.get(); + + Assertions.assertThat(userTaskInstanceEntity.getComments()) + .hasSize(2); + + TestUtils.assertUserTaskEntityComments(userTaskInstanceEntity.getComments(), instance.getComments()); + + instance.removeComment(comment); + instance.removeComment(comment2); + + entityOptional = userTaskInstanceRepository.findById(instance.getId()); + + Assertions.assertThat(entityOptional.get().getComments()) + .isEmpty(); + + Assertions.assertThat(commentRepository.findAll()) + .isEmpty(); + + userTaskInstances.remove(instance); + + Assertions.assertThat(userTaskInstances.exists(instance.getId())) + .isFalse(); + } + + private void assertEntityAndInstance(UserTaskInstanceEntity entity, UserTaskInstance instance) { + TestUtils.assertUserTaskEntityData(entity, instance); + + TestUtils.assertUserTaskEntityPotentialUserAndGroups(entity, instance); + TestUtils.assertUserTaskEntityAdminUserAndGroups(entity, instance); + TestUtils.assertUserTaskEntityExcludedUsers(entity, instance); + + TestUtils.assertUserTaskEntityInputs(entity, instance); + TestUtils.assertUserTaskEntityOutputs(entity, instance); + + TestUtils.assertUserTaskEntityAttachments(entity.getAttachments(), instance.getAttachments()); + TestUtils.assertUserTaskEntityComments(entity.getComments(), instance.getComments()); + TestUtils.assertUserTaskEntityMetadata(entity, instance); + } + + private UserTaskInstance createUserTaskInstance() { + DefaultUserTaskInstance instance = TestUtils.createUserTaskInstance(); + + instance.setInstances(userTaskInstances); + + return instance; + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusJPAUserTaskInstancesTest.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusJPAUserTaskInstancesTest.java new file mode 100644 index 00000000000..f314b629be5 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusJPAUserTaskInstancesTest.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus; + +import io.quarkus.test.TestTransaction; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.h2.H2DatabaseTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestTransaction +@QuarkusTestResource(value = H2DatabaseTestResource.class, restrictToAnnotatedClass = true) +@TestProfile(H2QuarkusTestProfile.class) +public class H2QuarkusJPAUserTaskInstancesTest extends BaseQuarkusJPAUserTaskInstancesTest { +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusTestProfile.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusTestProfile.java new file mode 100644 index 00000000000..4a1b05ffec8 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/H2QuarkusTestProfile.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class H2QuarkusTestProfile implements QuarkusTestProfile { + + @Override + public String getConfigProfile() { + return "test-h2"; + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusJPAUserTaskInstancesTest.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusJPAUserTaskInstancesTest.java new file mode 100644 index 00000000000..7174ee048c3 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusJPAUserTaskInstancesTest.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus; + +import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource; + +import io.quarkus.test.TestTransaction; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestTransaction +@QuarkusTestResource(value = PostgreSqlQuarkusTestResource.class, restrictToAnnotatedClass = true) +@TestProfile(PostgreSQLQuarkusTestProfile.class) +public class PostgreSQLQuarkusJPAUserTaskInstancesTest extends BaseQuarkusJPAUserTaskInstancesTest { + +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusTestProfile.java b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusTestProfile.java new file mode 100644 index 00000000000..dda90d34319 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/java/org/jbpm/usertask/jpa/quarkus/PostgreSQLQuarkusTestProfile.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.quarkus; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class PostgreSQLQuarkusTestProfile implements QuarkusTestProfile { + + @Override + public String getConfigProfile() { + return "test-postgresql"; + } +} diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/resources/META-INF/beans.xml b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/resources/application.properties b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/resources/application.properties new file mode 100644 index 00000000000..62677c92319 --- /dev/null +++ b/quarkus/addons/jbpm-usertask-storage-jpa/runtime/src/test/resources/application.properties @@ -0,0 +1,28 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + + +%test-postgresql.quarkus.datasource.db-kind=postgresql +%test-postgresql.quarkus.datasource.devservices.enabled=false + +%test-h2.quarkus.datasource.db-kind=h2 +%test-h2.quarkus.datasource.username=kogito +%test-h2.quarkus.datasource.jdbc.url=jdbc:h2:mem:default + +kie.flyway.enabled=true diff --git a/quarkus/addons/pom.xml b/quarkus/addons/pom.xml index 2a51d08ca82..de5f6685ed0 100644 --- a/quarkus/addons/pom.xml +++ b/quarkus/addons/pom.xml @@ -60,6 +60,7 @@ marshallers process-definitions dynamic + jbpm-usertask-storage-jpa diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/pom.xml b/quarkus/integration-tests/integration-tests-quarkus-usertasks/pom.xml new file mode 100644 index 00000000000..e648118be0c --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/pom.xml @@ -0,0 +1,146 @@ + + + + + org.kie.kogito + kogito-quarkus-integration-tests + 999-SNAPSHOT + + 4.0.0 + integration-tests-quarkus-usertasks + Kogito :: Integration Tests :: Quarkus :: Processes :: Source Files + + + org.jbpm.usertask.storage.jpa.quarkus.it + + + + + + org.kie.kogito + kogito-quarkus-bom + ${project.version} + pom + import + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-jackson + + + org.jbpm + jbpm-quarkus + + + org.kie + kie-addons-quarkus-persistence-jdbc + + + org.jbpm + jbpm-addons-quarkus-usertask-storage-jpa + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-agroal + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + org.kie.kogito + kogito-quarkus-test-utils + test + + + + + org.jbpm + jbpm-quarkus-deployment + ${project.version} + pom + test + + + * + * + + + + + org.jbpm + jbpm-addons-quarkus-usertask-storage-jpa-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + \ No newline at end of file diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Address.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Address.java new file mode 100644 index 00000000000..662dfa9b9f7 --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Address.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.acme.travels; + +public class Address { + + private String street; + private String city; + private String zipCode; + private String country; + + public Address() { + + } + + public Address(String street, String city, String zipCode, String country) { + super(); + this.street = street; + this.city = city; + this.zipCode = zipCode; + this.country = country; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + @Override + public String toString() { + return "Address [street=" + street + ", city=" + city + ", zipCode=" + zipCode + ", country=" + country + "]"; + } +} diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Traveller.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Traveller.java new file mode 100644 index 00000000000..c24685803d2 --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/java/org/acme/travels/Traveller.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.acme.travels; + +public class Traveller { + + private String firstName; + private String lastName; + private String email; + private String nationality; + private Address address; + + public Traveller() { + + } + + public Traveller(String firstName, String lastName, String email, String nationality, Address address) { + super(); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.nationality = nationality; + this.address = address; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getNationality() { + return nationality; + } + + public void setNationality(String nationality) { + this.nationality = nationality; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + @Override + public String toString() { + return "Traveller [firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + ", nationality=" + + nationality + ", address=" + address + "]"; + } + +} diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/application.properties b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/application.properties new file mode 100644 index 00000000000..5458fa57f9c --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/application.properties @@ -0,0 +1,24 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +quarkus.datasource.db-kind=postgresql + +kogito.persistence.type=jdbc + +kie.flyway.enabled=true \ No newline at end of file diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/approval.bpmn b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/approval.bpmn new file mode 100644 index 00000000000..275d148f7ac --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/main/resources/approval.bpmn @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _9EAFE6C1-69B4-4908-B764-EF3C4A55BEE3 + _C13522F1-230A-4C26-B5A9-533A5D9FEE9D + + + + + + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_TaskNameInputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_travellerInputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_SkippableInputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_GroupIdInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_ActorIdOutputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_approvedOutputX + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_TaskNameInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_TaskNameInputX + + + + traveller + _8B62D3CA-5D03-4B2B-832B-126469288BB4_travellerInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_SkippableInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_SkippableInputX + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_GroupIdInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_GroupIdInputX + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_ActorIdOutputX + approver + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_approvedOutputX + firstLineApproval + + + + manager + + + + + + + + + + _C13522F1-230A-4C26-B5A9-533A5D9FEE9D + _078F46FB-B7A1-4DBB-BE9A-75C7CB0CCD03 + + + + + + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_TaskNameInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_ExcludedOwnerIdInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_travellerInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_SkippableInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_GroupIdInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_approvedOutputX + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_TaskNameInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_TaskNameInputX + + + + approver + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_ExcludedOwnerIdInputX + + + traveller + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_travellerInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_SkippableInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_SkippableInputX + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_GroupIdInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_GroupIdInputX + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_approvedOutputX + secondLineApproval + + + + + + + + + _9EAFE6C1-69B4-4908-B764-EF3C4A55BEE3 + + + + + + + + _078F46FB-B7A1-4DBB-BE9A-75C7CB0CCD03 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _F0jB8En5EeqlfsIhq1UCRQ + _F0jB8En5EeqlfsIhq1UCRQ + + diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java new file mode 100644 index 00000000000..1d75f4c0c5a --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Traveller; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.emptyOrNullString; + +public abstract class BaseUserTaskIT { + public static final String PROCESS_ID = "approvals"; + public static final String USER_TASKS_ENDPOINT = "/usertasks/instance"; + public static final String USER_TASKS_INSTANCE_ENDPOINT = USER_TASKS_ENDPOINT + "/{taskId}"; + + static { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + } + + public String startProcessInstance(Traveller traveller) { + final String pid = given().contentType(ContentType.JSON) + .when() + .body(Map.of("traveller", traveller)) + .post("/{processId}", PROCESS_ID) + .then() + .statusCode(201) + .header("Location", not(emptyOrNullString())) + .body("id", not(emptyOrNullString())) + .extract() + .path("id"); + + given() + .accept(ContentType.JSON) + .when() + .get("/{processId}/{id}", PROCESS_ID, pid) + .then() + .statusCode(200) + .body("id", equalTo(pid)) + .body("traveller.firstName", equalTo(traveller.getFirstName())) + .body("traveller.lastName", equalTo(traveller.getLastName())) + .body("traveller.email", equalTo(traveller.getEmail())) + .body("traveller.nationality", equalTo(traveller.getNationality())); + + return pid; + } +} diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java new file mode 100644 index 00000000000..630e207040b --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.net.URI; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource; +import org.kie.kogito.usertask.model.AttachmentInfo; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.emptyOrNullString; + +@QuarkusIntegrationTest +@QuarkusTestResource(value = PostgreSqlQuarkusTestResource.class, restrictToAnnotatedClass = true) +public class UserTaskAttachmentsIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/attachments"; + public static final String USER_TASKS_INSTANCE_ATTACHMENT = USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT + "/{attachmentId}"; + + @Test + public void testUserTaskAttachments() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + AttachmentInfo attachment1 = new AttachmentInfo(URI.create("http://localhost:8080/attachment_1.txt"), "Attachment 1"); + + String attachment1Id = addAndVerifyAttachment(taskId, attachment1); + + AttachmentInfo attachment2 = new AttachmentInfo(URI.create("http://localhost:8080/attachment_2.txt"), "Attachment 2"); + + String attachment2Id = addAndVerifyAttachment(taskId, attachment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)); + + attachment1 = new AttachmentInfo(URI.create("http://localhost:8080/new_attachment_1.txt"), "NEW Attachment 1"); + + updateAndVerifyAttachment(taskId, attachment1Id, attachment1); + + attachment2 = new AttachmentInfo(URI.create("http://localhost:8080/new_attachment_2.txt"), "NEW Attachment 2"); + + updateAndVerifyAttachment(taskId, attachment2Id, attachment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].id", not(emptyOrNullString())) + .body("[0].content", equalTo(attachment1.getUri().toString())) + .body("[0].name", equalTo(attachment1.getName())) + .body("[0].updatedBy", not(emptyOrNullString())) + .body("[0].updatedAt", not(emptyOrNullString())) + .body("[1].id", not(emptyOrNullString())) + .body("[1].content", equalTo(attachment2.getUri().toString())) + .body("[1].name", equalTo(attachment2.getName())) + .body("[1].updatedBy", not(emptyOrNullString())) + .body("[1].updatedAt", not(emptyOrNullString())); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachment1Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(1)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachment2Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + } + + private String addAndVerifyAttachment(String taskId, AttachmentInfo attachment) { + String id = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(attachment) + .post(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENT, taskId, id) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + + return id; + } + + private void updateAndVerifyAttachment(String taskId, String attachmentId, AttachmentInfo attachment) { + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(attachment) + .put(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachmentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachmentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + } +} diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java new file mode 100644 index 00000000000..638c7e30266 --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource; +import org.kie.kogito.usertask.model.CommentInfo; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.emptyOrNullString; + +@QuarkusIntegrationTest +@QuarkusTestResource(value = PostgreSqlQuarkusTestResource.class, restrictToAnnotatedClass = true) +public class UserTaskCommentsIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_COMMENTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/comments"; + public static final String USER_TASKS_INSTANCE_COMMENT = USER_TASKS_INSTANCE_COMMENTS_ENDPOINT + "/{commentId}"; + + @Test + public void testUserTaskComments() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + CommentInfo comment1 = new CommentInfo("This is my second comment."); + + String comment1Id = addAndVerifyComment(taskId, comment1); + + CommentInfo comment2 = new CommentInfo("This is my second comment."); + + String comment2Id = addAndVerifyComment(taskId, comment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)); + + comment1 = new CommentInfo("This is the first comment modified"); + + updateAndVerifyComment(taskId, comment1Id, comment1); + + comment2 = new CommentInfo("This is the second comment modified"); + + updateAndVerifyComment(taskId, comment2Id, comment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].id", not(emptyOrNullString())) + .body("[0].content", equalTo(comment1.getComment())) + .body("[0].updatedBy", not(emptyOrNullString())) + .body("[0].updatedAt", not(emptyOrNullString())) + .body("[1].id", not(emptyOrNullString())) + .body("[1].content", equalTo(comment2.getComment())) + .body("[1].updatedBy", not(emptyOrNullString())) + .body("[1].updatedAt", not(emptyOrNullString())); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_COMMENT, taskId, comment1Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(1)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_COMMENT, taskId, comment2Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + } + + private String addAndVerifyComment(String taskId, CommentInfo comment) { + String id = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(comment) + .post(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENT, taskId, id) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + + return id; + } + + private void updateAndVerifyComment(String taskId, String commentId, CommentInfo comment) { + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(comment) + .put(USER_TASKS_INSTANCE_COMMENT, taskId, commentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENT, taskId, commentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + } +} diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java new file mode 100644 index 00000000000..13084ed8399 --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; + +@QuarkusIntegrationTest +@QuarkusTestResource(value = PostgreSqlQuarkusTestResource.class, restrictToAnnotatedClass = true) +public class UserTaskInputs extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_INPUTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/inputs"; + + @Test + public void testUserTaskInputs() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")) + .body("taskName", equalTo("firstLineApproval")) + .body("potentialUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + Traveller newTraveller = new Traveller("Ned", "Stark", "n.stark@winterfell.com", "Northern", new Address("main street", "Winterfell", "10005", "WF")); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(Map.of("traveller", newTraveller)) + .put(USER_TASKS_INSTANCE_INPUTS_ENDPOINT, taskId) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("inputs.traveller.firstName", equalTo(newTraveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(newTraveller.getLastName())) + .body("inputs.traveller.email", equalTo(newTraveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(newTraveller.getNationality())) + .body("inputs.traveller.address.street", equalTo(newTraveller.getAddress().getStreet())) + .body("inputs.traveller.address.city", equalTo(newTraveller.getAddress().getCity())) + .body("inputs.traveller.address.zipCode", equalTo(newTraveller.getAddress().getZipCode())) + .body("inputs.traveller.address.country", equalTo(newTraveller.getAddress().getCountry())); + } +} diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java new file mode 100644 index 00000000000..b2c7b29e11d --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource; +import org.kie.kogito.usertask.model.TransitionInfo; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; + +@QuarkusIntegrationTest +@QuarkusTestResource(value = PostgreSqlQuarkusTestResource.class, restrictToAnnotatedClass = true) +public class UserTaskLifeCycleIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_TRANSITION_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/transition"; + + @Test + public void testUserTaskLifeCycle() { + + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + final String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")) + .body("taskName", equalTo("firstLineApproval")) + .body("potentialUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + given() + .contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(new TransitionInfo("complete", Map.of("approved", true))) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Completed")) + .body("status.terminate", equalTo("COMPLETED")) + .body("outputs.approved", equalTo(true)); + + // Manager is excluded for the secondLineApproval Task, he shouldn't be allowed to see the task + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", not(taskId)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Ready")) + .body("taskName", equalTo("secondLineApproval")) + .body("excludedUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + // Manager is excluded for the secondLineApproval Task, he shouldn't be able to work on the task + given() + .contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .body(new TransitionInfo("claim")) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(500); + + given() + .contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .body(new TransitionInfo("claim")) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")); + + given() + .contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .body(new TransitionInfo("complete", Map.of("approved", true))) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Completed")) + .body("status.terminate", equalTo("COMPLETED")) + .body("outputs.approved", equalTo(true)); + + given() + .accept(ContentType.JSON) + .when() + .get("/{processId}/{id}", PROCESS_ID, pid) + .then() + .statusCode(404); + } +} diff --git a/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java new file mode 100644 index 00000000000..1192545ac61 --- /dev/null +++ b/quarkus/integration-tests/integration-tests-quarkus-usertasks/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; + +@QuarkusIntegrationTest +@QuarkusTestResource(value = PostgreSqlQuarkusTestResource.class, restrictToAnnotatedClass = true) +public class UserTaskOutputsIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_OUTPUTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/outputs"; + + @Test + public void testUserTaskOutputs() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")) + .body("taskName", equalTo("firstLineApproval")) + .body("potentialUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + Traveller newTraveller = new Traveller("Ned", "Stark", "n.stark@winterfell.com", "Northern", new Address("main street", "Winterfell", "10005", "WF")); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(Map.of("traveller", newTraveller, "approved", true)) + .put(USER_TASKS_INSTANCE_OUTPUTS_ENDPOINT, taskId) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("outputs.approved", is(true)) + .body("outputs.traveller.firstName", equalTo(newTraveller.getFirstName())) + .body("outputs.traveller.lastName", equalTo(newTraveller.getLastName())) + .body("outputs.traveller.email", equalTo(newTraveller.getEmail())) + .body("outputs.traveller.nationality", equalTo(newTraveller.getNationality())) + .body("outputs.traveller.address.street", equalTo(newTraveller.getAddress().getStreet())) + .body("outputs.traveller.address.city", equalTo(newTraveller.getAddress().getCity())) + .body("outputs.traveller.address.zipCode", equalTo(newTraveller.getAddress().getZipCode())) + .body("outputs.traveller.address.country", equalTo(newTraveller.getAddress().getCountry())); + } +} diff --git a/quarkus/integration-tests/pom.xml b/quarkus/integration-tests/pom.xml index 1fb26d5ec8d..48ba322a8bd 100644 --- a/quarkus/integration-tests/pom.xml +++ b/quarkus/integration-tests/pom.xml @@ -48,6 +48,7 @@ integration-tests-quarkus-processes integration-tests-quarkus-processes-reactive integration-tests-quarkus-processes-persistence + integration-tests-quarkus-usertasks integration-tests-quarkus-source-files integration-tests-quarkus-gradle diff --git a/springboot/addons/jbpm-usertask-storage-jpa/pom.xml b/springboot/addons/jbpm-usertask-storage-jpa/pom.xml new file mode 100644 index 00000000000..36bed41e5cc --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/pom.xml @@ -0,0 +1,35 @@ + + + + kogito-addons-springboot-parent + org.kie + 999-SNAPSHOT + + 4.0.0 + + org.jbpm + jbpm-addons-springboot-usertask-storage-jpa + jBPM :: Add-Ons :: Spring Boot :: User Task Storage JPA + jBPM Add-On Spring Boot User Task Storage JPA + + + UTF-8 + org.jbpm.usertask.storage.jpa.springboot + + + + + org.springframework.boot + spring-boot-starter-web + + + org.jbpm + jbpm-addons-usertask-storage-jpa + + + org.kie + kie-addons-springboot-flyway + + + diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/SpringBootJPAUserTaskInstances.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/SpringBootJPAUserTaskInstances.java new file mode 100644 index 00000000000..aadef95e407 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/SpringBootJPAUserTaskInstances.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot; + +import org.jbpm.usertask.jpa.JPAUserTaskInstances; +import org.jbpm.usertask.jpa.mapper.UserTaskInstanceEntityMapper; +import org.jbpm.usertask.jpa.repository.UserTaskInstanceRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Component +public class SpringBootJPAUserTaskInstances extends JPAUserTaskInstances { + + @Autowired + public SpringBootJPAUserTaskInstances(UserTaskInstanceRepository userTaskInstanceRepository, UserTaskInstanceEntityMapper userTaskInstanceEntityMapper) { + super(userTaskInstanceRepository, userTaskInstanceEntityMapper); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootAttachmentsEntityMapper.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootAttachmentsEntityMapper.java new file mode 100644 index 00000000000..6375f27ec8e --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootAttachmentsEntityMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.mapper; + +import org.jbpm.usertask.jpa.mapper.AttachmentsEntityMapper; +import org.jbpm.usertask.jpa.repository.AttachmentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootAttachmentsEntityMapper extends AttachmentsEntityMapper { + + @Autowired + public SpringBootAttachmentsEntityMapper(AttachmentRepository attachmentRepository) { + super(attachmentRepository); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootCommentsEntityMapper.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootCommentsEntityMapper.java new file mode 100644 index 00000000000..2a8c4cb8215 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootCommentsEntityMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.mapper; + +import org.jbpm.usertask.jpa.mapper.CommentsEntityMapper; +import org.jbpm.usertask.jpa.repository.CommentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootCommentsEntityMapper extends CommentsEntityMapper { + + @Autowired + public SpringBootCommentsEntityMapper(CommentRepository commentRepository) { + super(commentRepository); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskInputsEntityMapper.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskInputsEntityMapper.java new file mode 100644 index 00000000000..c0520b805b3 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskInputsEntityMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.mapper; + +import org.jbpm.usertask.jpa.mapper.TaskInputsEntityMapper; +import org.jbpm.usertask.jpa.repository.TaskInputRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootTaskInputsEntityMapper extends TaskInputsEntityMapper { + + @Autowired + public SpringBootTaskInputsEntityMapper(TaskInputRepository repository) { + super(repository); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskMetadataEntityMapper.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskMetadataEntityMapper.java new file mode 100644 index 00000000000..9d52d325084 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskMetadataEntityMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.mapper; + +import org.jbpm.usertask.jpa.mapper.TaskMetadataEntityMapper; +import org.jbpm.usertask.jpa.repository.TaskMetadataRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootTaskMetadataEntityMapper extends TaskMetadataEntityMapper { + + @Autowired + public SpringBootTaskMetadataEntityMapper(TaskMetadataRepository repository) { + super(repository); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskOutputsEntityMapper.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskOutputsEntityMapper.java new file mode 100644 index 00000000000..83f9634990a --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootTaskOutputsEntityMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.mapper; + +import org.jbpm.usertask.jpa.mapper.TaskOutputsEntityMapper; +import org.jbpm.usertask.jpa.repository.TaskOutputRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootTaskOutputsEntityMapper extends TaskOutputsEntityMapper { + + @Autowired + public SpringBootTaskOutputsEntityMapper(TaskOutputRepository repository) { + super(repository); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootUserTaskInstanceEntityMapper.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootUserTaskInstanceEntityMapper.java new file mode 100644 index 00000000000..6c753b1b6a0 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/mapper/SpringBootUserTaskInstanceEntityMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.mapper; + +import org.jbpm.usertask.jpa.mapper.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootUserTaskInstanceEntityMapper extends UserTaskInstanceEntityMapper { + + @Autowired + public SpringBootUserTaskInstanceEntityMapper(AttachmentsEntityMapper attachmentsMapper, CommentsEntityMapper commentsMapper, TaskMetadataEntityMapper taskMetadataEntityMapper, + TaskInputsEntityMapper taskInputsEntityMapper, TaskOutputsEntityMapper taskOutputsEntityMapper) { + super(attachmentsMapper, commentsMapper, taskMetadataEntityMapper, taskInputsEntityMapper, taskOutputsEntityMapper); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootAttachmentRepository.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootAttachmentRepository.java new file mode 100644 index 00000000000..d9fad245177 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootAttachmentRepository.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.repository; + +import org.jbpm.usertask.jpa.repository.AttachmentRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Transactional +public class SpringBootAttachmentRepository extends AttachmentRepository { + + @Autowired + public SpringBootAttachmentRepository(UserTaskJPAContext context) { + super(context); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootCommentRepository.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootCommentRepository.java new file mode 100644 index 00000000000..6181e505e7e --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootCommentRepository.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.repository; + +import org.jbpm.usertask.jpa.repository.CommentRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Transactional +public class SpringBootCommentRepository extends CommentRepository { + + @Autowired + public SpringBootCommentRepository(UserTaskJPAContext context) { + super(context); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskInputEntityRepository.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskInputEntityRepository.java new file mode 100644 index 00000000000..468301e813f --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskInputEntityRepository.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.repository; + +import org.jbpm.usertask.jpa.repository.TaskInputRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootTaskInputEntityRepository extends TaskInputRepository { + + @Autowired + public SpringBootTaskInputEntityRepository(UserTaskJPAContext context) { + super(context); + } + +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskMetadataEntityRepository.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskMetadataEntityRepository.java new file mode 100644 index 00000000000..c5064422a82 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskMetadataEntityRepository.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.repository; + +import org.jbpm.usertask.jpa.repository.TaskMetadataRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootTaskMetadataEntityRepository extends TaskMetadataRepository { + + @Autowired + public SpringBootTaskMetadataEntityRepository(UserTaskJPAContext context) { + super(context); + } + +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskOutputEntityRepository.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskOutputEntityRepository.java new file mode 100644 index 00000000000..3eefcd20778 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootTaskOutputEntityRepository.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.repository; + +import org.jbpm.usertask.jpa.repository.TaskOutputRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SpringBootTaskOutputEntityRepository extends TaskOutputRepository { + + @Autowired + public SpringBootTaskOutputEntityRepository(UserTaskJPAContext context) { + super(context); + } + +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskInstanceRepository.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskInstanceRepository.java new file mode 100644 index 00000000000..b84df9dad37 --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskInstanceRepository.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.repository; + +import org.jbpm.usertask.jpa.repository.UserTaskInstanceRepository; +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Transactional +public class SpringBootUserTaskInstanceRepository extends UserTaskInstanceRepository { + + @Autowired + public SpringBootUserTaskInstanceRepository(UserTaskJPAContext context) { + super(context); + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskJPAContext.java b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskJPAContext.java new file mode 100644 index 00000000000..a208f7ece9c --- /dev/null +++ b/springboot/addons/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/springboot/repository/SpringBootUserTaskJPAContext.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.usertask.jpa.springboot.repository; + +import org.jbpm.usertask.jpa.repository.UserTaskJPAContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@Component +@Transactional +public class SpringBootUserTaskJPAContext implements UserTaskJPAContext { + + @PersistenceContext + private EntityManager em; + + @Override + public EntityManager getEntityManager() { + return em; + } +} diff --git a/springboot/addons/jbpm-usertask-storage-jpa/src/main/resources/META-INF/beans.xml b/springboot/addons/jbpm-usertask-storage-jpa/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/springboot/addons/pom.xml b/springboot/addons/pom.xml index d6221ca7a6b..3aee177ac61 100644 --- a/springboot/addons/pom.xml +++ b/springboot/addons/pom.xml @@ -51,6 +51,7 @@ kubernetes flyway persistence + jbpm-usertask-storage-jpa diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/pom.xml b/springboot/integration-tests/integration-tests-springboot-usertasks-it/pom.xml new file mode 100644 index 00000000000..d3a7645e5f9 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/pom.xml @@ -0,0 +1,153 @@ + + + + org.kie.kogito + kogito-spring-boot-integration-tests + 999-SNAPSHOT + + + 4.0.0 + + integration-tests-springboot-usertasks-it + Kogito :: Integration Tests :: Spring Boot :: UserTasks + + + integration.tests.springboot.userTasks.it + false + + + + + + org.kie.kogito + kogito-spring-boot-bom + ${project.version} + pom + import + + + + + + + org.jbpm + jbpm-spring-boot-starter + + + org.kie + kie-addons-springboot-persistence-jdbc + + + org.jbpm + jbpm-addons-springboot-usertask-storage-jpa + + + + + org.postgresql + postgresql + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.kie + kie-addons-springboot-process-management + + + org.springframework.boot + spring-boot-starter-test + test + + + io.rest-assured + json-schema-validator + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + io.rest-assured + rest-assured + test + + + org.kie.kogito + kogito-spring-boot-test-utils + test + + + org.awaitility + awaitility + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + maven-compiler-plugin + ${version.compiler.plugin} + + ${maven.compiler.release} + + + + pre-kogito-generate-model + process-resources + + compile + + + + + + org.kie.kogito + kogito-maven-plugin + ${project.version} + + + kogito-generate-model + process-resources + + generateModel + + + + kogito-process-model-classes + process-classes + + process-model-classes + + + + + + + \ No newline at end of file diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Address.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Address.java new file mode 100644 index 00000000000..662dfa9b9f7 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Address.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.acme.travels; + +public class Address { + + private String street; + private String city; + private String zipCode; + private String country; + + public Address() { + + } + + public Address(String street, String city, String zipCode, String country) { + super(); + this.street = street; + this.city = city; + this.zipCode = zipCode; + this.country = country; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + @Override + public String toString() { + return "Address [street=" + street + ", city=" + city + ", zipCode=" + zipCode + ", country=" + country + "]"; + } +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Traveller.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Traveller.java new file mode 100644 index 00000000000..c24685803d2 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/acme/travels/Traveller.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.acme.travels; + +public class Traveller { + + private String firstName; + private String lastName; + private String email; + private String nationality; + private Address address; + + public Traveller() { + + } + + public Traveller(String firstName, String lastName, String email, String nationality, Address address) { + super(); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.nationality = nationality; + this.address = address; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getNationality() { + return nationality; + } + + public void setNationality(String nationality) { + this.nationality = nationality; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + @Override + public String toString() { + return "Traveller [firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + ", nationality=" + + nationality + ", address=" + address + "]"; + } + +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/kie/kogito/it/KogitoSpringbootApplication.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/kie/kogito/it/KogitoSpringbootApplication.java new file mode 100644 index 00000000000..8cc2437b71d --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/java/org/kie/kogito/it/KogitoSpringbootApplication.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.it; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = { "org.kie.kogito.**", "org.acme.travels.**" }) +public class KogitoSpringbootApplication { + + public static void main(String[] args) { + SpringApplication.run(KogitoSpringbootApplication.class, args); + } +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/application.properties b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/application.properties new file mode 100644 index 00000000000..0e459353843 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/application.properties @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +kogito.persistence.type=jdbc + +kie.flyway.enabled=true +spring.flyway.enabled=false \ No newline at end of file diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/approval.bpmn b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/approval.bpmn new file mode 100644 index 00000000000..275d148f7ac --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/main/resources/approval.bpmn @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _9EAFE6C1-69B4-4908-B764-EF3C4A55BEE3 + _C13522F1-230A-4C26-B5A9-533A5D9FEE9D + + + + + + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_TaskNameInputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_travellerInputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_SkippableInputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_GroupIdInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_ActorIdOutputX + _8B62D3CA-5D03-4B2B-832B-126469288BB4_approvedOutputX + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_TaskNameInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_TaskNameInputX + + + + traveller + _8B62D3CA-5D03-4B2B-832B-126469288BB4_travellerInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_SkippableInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_SkippableInputX + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_GroupIdInputX + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_GroupIdInputX + + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_ActorIdOutputX + approver + + + _8B62D3CA-5D03-4B2B-832B-126469288BB4_approvedOutputX + firstLineApproval + + + + manager + + + + + + + + + + _C13522F1-230A-4C26-B5A9-533A5D9FEE9D + _078F46FB-B7A1-4DBB-BE9A-75C7CB0CCD03 + + + + + + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_TaskNameInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_ExcludedOwnerIdInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_travellerInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_SkippableInputX + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_GroupIdInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_approvedOutputX + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_TaskNameInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_TaskNameInputX + + + + approver + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_ExcludedOwnerIdInputX + + + traveller + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_travellerInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_SkippableInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_SkippableInputX + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_GroupIdInputX + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_GroupIdInputX + + + + _0DBFABE8-92B0-46E6-B52E-A9593AFA4371_approvedOutputX + secondLineApproval + + + + + + + + + _9EAFE6C1-69B4-4908-B764-EF3C4A55BEE3 + + + + + + + + _078F46FB-B7A1-4DBB-BE9A-75C7CB0CCD03 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _F0jB8En5EeqlfsIhq1UCRQ + _F0jB8En5EeqlfsIhq1UCRQ + + diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java new file mode 100644 index 00000000000..3d8f3075882 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/BaseUserTaskIT.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Traveller; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.web.server.LocalServerPort; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.emptyOrNullString; + +public abstract class BaseUserTaskIT { + public static final String PROCESS_ID = "approvals"; + public static final String PROCESS_INSTANCE = "approvals/{id}"; + public static final String USER_TASKS_ENDPOINT = "usertasks/instance"; + public static final String USER_TASKS_INSTANCE_ENDPOINT = USER_TASKS_ENDPOINT + "/{taskId}"; + + @LocalServerPort + int httpPort; + + static { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + } + + @BeforeEach + void setPort() { + RestAssured.port = httpPort; + } + + public String startProcessInstance(Traveller traveller) { + final String pid = given().contentType(ContentType.JSON) + .when() + .body(Map.of("traveller", traveller)) + .post("/{processId}", PROCESS_ID) + .then() + .statusCode(201) + .header("Location", not(emptyOrNullString())) + .body("id", not(emptyOrNullString())) + .extract() + .path("id"); + + given() + .accept(ContentType.JSON) + .when() + .get("/{processId}/{id}", PROCESS_ID, pid) + .then() + .statusCode(200) + .body("id", equalTo(pid)) + .body("traveller.firstName", equalTo(traveller.getFirstName())) + .body("traveller.lastName", equalTo(traveller.getLastName())) + .body("traveller.email", equalTo(traveller.getEmail())) + .body("traveller.nationality", equalTo(traveller.getNationality())); + + return pid; + } + + public void abortProcessInstance(String pid) { + given().contentType(ContentType.JSON) + .when() + .body(Map.of()) + .delete(PROCESS_INSTANCE, pid) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())); + } +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java new file mode 100644 index 00000000000..60887b11ffb --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskAttachmentsIT.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.net.URI; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.it.KogitoSpringbootApplication; +import org.kie.kogito.testcontainers.springboot.PostgreSqlSpringBootTestResource; +import org.kie.kogito.usertask.model.AttachmentInfo; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.emptyOrNullString; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KogitoSpringbootApplication.class) +@ContextConfiguration(initializers = PostgreSqlSpringBootTestResource.class) +public class UserTaskAttachmentsIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/attachments"; + public static final String USER_TASKS_INSTANCE_ATTACHMENT = USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT + "/{attachmentId}"; + + @Test + public void testUserTaskAttachments() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + final String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + AttachmentInfo attachment1 = new AttachmentInfo(URI.create("http://localhost:8080/attachment_1.txt"), "Attachment 1"); + + String attachment1Id = addAndVerifyAttachment(taskId, attachment1); + + AttachmentInfo attachment2 = new AttachmentInfo(URI.create("http://localhost:8080/attachment_2.txt"), "Attachment 2"); + + String attachment2Id = addAndVerifyAttachment(taskId, attachment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)); + + attachment1 = new AttachmentInfo(URI.create("http://localhost:8080/new_attachment_1.txt"), "NEW Attachment 1"); + + updateAndVerifyAttachment(taskId, attachment1Id, attachment1); + + attachment2 = new AttachmentInfo(URI.create("http://localhost:8080/new_attachment_2.txt"), "NEW Attachment 2"); + + updateAndVerifyAttachment(taskId, attachment2Id, attachment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].id", not(emptyOrNullString())) + .body("[0].content", equalTo(attachment1.getUri().toString())) + .body("[0].name", equalTo(attachment1.getName())) + .body("[0].updatedBy", not(emptyOrNullString())) + .body("[0].updatedAt", not(emptyOrNullString())) + .body("[1].id", not(emptyOrNullString())) + .body("[1].content", equalTo(attachment2.getUri().toString())) + .body("[1].name", equalTo(attachment2.getName())) + .body("[1].updatedBy", not(emptyOrNullString())) + .body("[1].updatedAt", not(emptyOrNullString())); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachment1Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(1)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachment2Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + abortProcessInstance(pid); + } + + private String addAndVerifyAttachment(String taskId, AttachmentInfo attachment) { + String id = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(attachment) + .post(USER_TASKS_INSTANCE_ATTACHMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENT, taskId, id) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + + return id; + } + + private void updateAndVerifyAttachment(String taskId, String attachmentId, AttachmentInfo attachment) { + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(attachment) + .put(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachmentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ATTACHMENT, taskId, attachmentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(attachment.getUri().toString())) + .body("name", equalTo(attachment.getName())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + } +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java new file mode 100644 index 00000000000..07f7f91c4ac --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskCommentsIT.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.it.KogitoSpringbootApplication; +import org.kie.kogito.testcontainers.springboot.PostgreSqlSpringBootTestResource; +import org.kie.kogito.usertask.model.CommentInfo; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.emptyOrNullString; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KogitoSpringbootApplication.class) +@ContextConfiguration(initializers = PostgreSqlSpringBootTestResource.class) +public class UserTaskCommentsIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_COMMENTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/comments"; + public static final String USER_TASKS_INSTANCE_COMMENT = USER_TASKS_INSTANCE_COMMENTS_ENDPOINT + "/{commentId}"; + + @Test + public void testUserTaskComments() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + final String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + CommentInfo comment1 = new CommentInfo("This is my second comment."); + + String comment1Id = addAndVerifyComment(taskId, comment1); + + CommentInfo comment2 = new CommentInfo("This is my second comment."); + + String comment2Id = addAndVerifyComment(taskId, comment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)); + + comment1 = new CommentInfo("This is the first comment modified"); + + updateAndVerifyComment(taskId, comment1Id, comment1); + + comment2 = new CommentInfo("This is the second comment modified"); + + updateAndVerifyComment(taskId, comment2Id, comment2); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].id", not(emptyOrNullString())) + .body("[0].content", equalTo(comment1.getComment())) + .body("[0].updatedBy", not(emptyOrNullString())) + .body("[0].updatedAt", not(emptyOrNullString())) + .body("[1].id", not(emptyOrNullString())) + .body("[1].content", equalTo(comment2.getComment())) + .body("[1].updatedBy", not(emptyOrNullString())) + .body("[1].updatedAt", not(emptyOrNullString())); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_COMMENT, taskId, comment1Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(1)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .delete(USER_TASKS_INSTANCE_COMMENT, taskId, comment2Id) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + abortProcessInstance(pid); + } + + private String addAndVerifyComment(String taskId, CommentInfo comment) { + String id = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(comment) + .post(USER_TASKS_INSTANCE_COMMENTS_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENT, taskId, id) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + + return id; + } + + private void updateAndVerifyComment(String taskId, String commentId, CommentInfo comment) { + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(comment) + .put(USER_TASKS_INSTANCE_COMMENT, taskId, commentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())) + .extract() + .path("id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_COMMENT, taskId, commentId) + .then() + .statusCode(200) + .body("id", not(emptyOrNullString())) + .body("content", equalTo(comment.getComment())) + .body("updatedBy", not(emptyOrNullString())) + .body("updatedAt", not(emptyOrNullString())); + } +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java new file mode 100644 index 00000000000..0357f040233 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskInputs.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.it.KogitoSpringbootApplication; +import org.kie.kogito.testcontainers.springboot.PostgreSqlSpringBootTestResource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KogitoSpringbootApplication.class) +@ContextConfiguration(initializers = PostgreSqlSpringBootTestResource.class) +public class UserTaskInputs extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_INPUTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/inputs"; + + @Test + public void testUserTaskInputs() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")) + .body("taskName", equalTo("firstLineApproval")) + .body("potentialUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + Traveller newTraveller = new Traveller("Ned", "Stark", "n.stark@winterfell.com", "Northern", new Address("main street", "Winterfell", "10005", "WF")); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(Map.of("traveller", newTraveller)) + .put(USER_TASKS_INSTANCE_INPUTS_ENDPOINT, taskId) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("inputs.traveller.firstName", equalTo(newTraveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(newTraveller.getLastName())) + .body("inputs.traveller.email", equalTo(newTraveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(newTraveller.getNationality())) + .body("inputs.traveller.address.street", equalTo(newTraveller.getAddress().getStreet())) + .body("inputs.traveller.address.city", equalTo(newTraveller.getAddress().getCity())) + .body("inputs.traveller.address.zipCode", equalTo(newTraveller.getAddress().getZipCode())) + .body("inputs.traveller.address.country", equalTo(newTraveller.getAddress().getCountry())); + + abortProcessInstance(pid); + } +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java new file mode 100644 index 00000000000..0f04acf9565 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskLifeCycleIT.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.it.KogitoSpringbootApplication; +import org.kie.kogito.testcontainers.springboot.PostgreSqlSpringBootTestResource; +import org.kie.kogito.usertask.model.TransitionInfo; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KogitoSpringbootApplication.class) +@ContextConfiguration(initializers = PostgreSqlSpringBootTestResource.class) +public class UserTaskLifeCycleIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_TRANSITION_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/transition"; + + @Test + public void testUserTaskLifeCycle() { + + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + final String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")) + .body("taskName", equalTo("firstLineApproval")) + .body("potentialUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + given() + .contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(new TransitionInfo("complete", Map.of("approved", true))) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Completed")) + .body("status.terminate", equalTo("COMPLETED")) + .body("outputs.approved", equalTo(true)); + + // Manager is excluded for the secondLineApproval Task, he shouldn't be allowed to see the task + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(0)); + + taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", not(taskId)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Ready")) + .body("taskName", equalTo("secondLineApproval")) + .body("excludedUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + // Manager is excluded for the secondLineApproval Task, he shouldn't be able to work on the task + given() + .contentType(ContentType.JSON) + .when() + .body(Map.of()) + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(new TransitionInfo("claim")) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(500); + + given() + .contentType(ContentType.JSON) + .when() + .body(Map.of()) + .queryParam("user", "john") + .queryParam("group", "managers") + .body(new TransitionInfo("claim")) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")); + + given() + .contentType(ContentType.JSON) + .when() + .queryParam("user", "john") + .queryParam("group", "managers") + .body(new TransitionInfo("complete", Map.of("approved", true))) + .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Completed")) + .body("status.terminate", equalTo("COMPLETED")) + .body("outputs.approved", equalTo(true)); + + given() + .accept(ContentType.JSON) + .when() + .get("/{processId}/{id}", PROCESS_ID, pid) + .then() + .statusCode(404); + } +} diff --git a/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java new file mode 100644 index 00000000000..b89e19989a1 --- /dev/null +++ b/springboot/integration-tests/integration-tests-springboot-usertasks-it/src/test/java/org/jbpm/userTask/jpa/it/UserTaskOutputsIT.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.userTask.jpa.it; + +import java.util.Map; + +import org.acme.travels.Address; +import org.acme.travels.Traveller; +import org.junit.jupiter.api.Test; +import org.kie.kogito.it.KogitoSpringbootApplication; +import org.kie.kogito.testcontainers.springboot.PostgreSqlSpringBootTestResource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KogitoSpringbootApplication.class) +@ContextConfiguration(initializers = PostgreSqlSpringBootTestResource.class) +public class UserTaskOutputsIT extends BaseUserTaskIT { + public static final String USER_TASKS_INSTANCE_OUTPUTS_ENDPOINT = USER_TASKS_INSTANCE_ENDPOINT + "/outputs"; + + @Test + public void testUserTaskOutputs() { + Traveller traveller = new Traveller("John", "Doe", "john.doe@example.com", "American", new Address("main street", "Boston", "10005", "US")); + + String pid = startProcessInstance(traveller); + + String taskId = given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_ENDPOINT) + .then() + .statusCode(200) + .body("$.size()", is(1)) + .extract() + .path("[0].id"); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("status.name", equalTo("Reserved")) + .body("taskName", equalTo("firstLineApproval")) + .body("potentialUsers", hasItem("manager")) + .body("potentialGroups", hasItem("managers")) + .body("inputs.traveller.firstName", equalTo(traveller.getFirstName())) + .body("inputs.traveller.lastName", equalTo(traveller.getLastName())) + .body("inputs.traveller.email", equalTo(traveller.getEmail())) + .body("inputs.traveller.nationality", equalTo(traveller.getNationality())) + .body("metadata.ProcessType", equalTo("BPMN")) + .body("metadata.ProcessVersion", equalTo("1.0")) + .body("metadata.ProcessId", equalTo(PROCESS_ID)) + .body("metadata.ProcessInstanceId", equalTo(pid)) + .body("metadata.ProcessInstanceState", equalTo(1)); + + Traveller newTraveller = new Traveller("Ned", "Stark", "n.stark@winterfell.com", "Northern", new Address("main street", "Winterfell", "10005", "WF")); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .body(Map.of("traveller", newTraveller, "approved", true)) + .put(USER_TASKS_INSTANCE_OUTPUTS_ENDPOINT, taskId) + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "manager") + .queryParam("group", "department-managers") + .get(USER_TASKS_INSTANCE_ENDPOINT, taskId) + .then() + .statusCode(200) + .body("id", equalTo(taskId)) + .body("outputs.approved", is(true)) + .body("outputs.traveller.firstName", equalTo(newTraveller.getFirstName())) + .body("outputs.traveller.lastName", equalTo(newTraveller.getLastName())) + .body("outputs.traveller.email", equalTo(newTraveller.getEmail())) + .body("outputs.traveller.nationality", equalTo(newTraveller.getNationality())) + .body("outputs.traveller.address.street", equalTo(newTraveller.getAddress().getStreet())) + .body("outputs.traveller.address.city", equalTo(newTraveller.getAddress().getCity())) + .body("outputs.traveller.address.zipCode", equalTo(newTraveller.getAddress().getZipCode())) + .body("outputs.traveller.address.country", equalTo(newTraveller.getAddress().getCountry())); + + abortProcessInstance(pid); + } +} diff --git a/springboot/integration-tests/pom.xml b/springboot/integration-tests/pom.xml index 4bf6bb95a02..4e76b109677 100644 --- a/springboot/integration-tests/pom.xml +++ b/springboot/integration-tests/pom.xml @@ -41,6 +41,7 @@ integration-tests-springboot-norest-it integration-tests-springboot-processes-it integration-tests-springboot-processes-persistence-it + integration-tests-springboot-usertasks-it From 5175f11ef9efe503954ef394dc5b0e794273257b Mon Sep 17 00:00:00 2001 From: Gabriele Cardosi Date: Fri, 25 Oct 2024 06:56:32 +0200 Subject: [PATCH 02/16] [incubator-kie-issues#1574] Fix CVE-2024-47561 (#3751) --- kogito-build/kogito-dependencies-bom/pom.xml | 2 +- .../serverless/workflow/dmn/SWFDMNTest.java | 20 ------------------- .../workflow/dmn/SWFDecisionEngine.java | 20 ------------------- 3 files changed, 1 insertion(+), 41 deletions(-) diff --git a/kogito-build/kogito-dependencies-bom/pom.xml b/kogito-build/kogito-dependencies-bom/pom.xml index f085d103e0a..6e3de9522a3 100644 --- a/kogito-build/kogito-dependencies-bom/pom.xml +++ b/kogito-build/kogito-dependencies-bom/pom.xml @@ -120,7 +120,7 @@ 2.0.4 13.4-alpine3.14 - 1.11.3 + 1.11.4 3.24.2 4.0.4 2.9.0 diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-dmn-parser/src/test/java/org/kie/kogito/serverless/workflow/dmn/SWFDMNTest.java b/kogito-serverless-workflow/kogito-serverless-workflow-dmn-parser/src/test/java/org/kie/kogito/serverless/workflow/dmn/SWFDMNTest.java index bdacd9ad463..48e899f80da 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-dmn-parser/src/test/java/org/kie/kogito/serverless/workflow/dmn/SWFDMNTest.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-dmn-parser/src/test/java/org/kie/kogito/serverless/workflow/dmn/SWFDMNTest.java @@ -1,22 +1,3 @@ -/** -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ - /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -35,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - package org.kie.kogito.serverless.workflow.dmn; import java.io.IOException; diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-dmn/src/main/java/org/kie/kogito/serverless/workflow/dmn/SWFDecisionEngine.java b/kogito-serverless-workflow/kogito-serverless-workflow-dmn/src/main/java/org/kie/kogito/serverless/workflow/dmn/SWFDecisionEngine.java index fb0e36aa410..ab76278f8a4 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-dmn/src/main/java/org/kie/kogito/serverless/workflow/dmn/SWFDecisionEngine.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-dmn/src/main/java/org/kie/kogito/serverless/workflow/dmn/SWFDecisionEngine.java @@ -1,22 +1,3 @@ -/** -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ - /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -35,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - package org.kie.kogito.serverless.workflow.dmn; import java.util.HashMap; From e3352dd41e57a8c4bb8bcd3165dea50b211e1508 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:36:14 +0200 Subject: [PATCH 03/16] [Fix #3721] Optimize event grouping (#3739) * [Fix #3721] Optimize event grouping * [Fix #3721] Adding addons * [Fix #3721] Refactoring KogitoIndexConverter --- api/kogito-events-api/pom.xml | 8 +- .../event/process/CloudEventVisitor.java | 25 ++ .../KogitoEventBodySerializationHelper.java | 296 ++++++++++++++++++ .../process/KogitoMarshallEventSupport.java | 30 ++ .../ProcessInstanceErrorEventBody.java | 32 +- .../process/ProcessInstanceNodeEventBody.java | 47 ++- .../process/ProcessInstanceSLAEventBody.java | 41 ++- .../ProcessInstanceStateEventBody.java | 43 ++- .../ProcessInstanceVariableEventBody.java | 37 ++- .../kie/kogito/event/DataEventFactory.java | 17 + .../JacksonTypeCloudEventDataConverter.java | 44 +++ .../MultipleProcessInstanceDataEvent.java | 19 +- .../ProcessInstanceErrorDataEvent.java | 5 +- .../process/ProcessInstanceNodeDataEvent.java | 5 +- .../process/ProcessInstanceSLADataEvent.java | 5 +- .../ProcessInstanceStateDataEvent.java | 5 +- .../ProcessInstanceVariableDataEvent.java | 5 +- ...nProcessInstanceDataEventDeserializer.java | 79 +++++ ...UserTaskInstanceDataEventDeserializer.java | 82 +++++ .../KogitoDataEventSerializationHelper.java | 74 +++++ .../serializer/KogitoSerializationModule.java | 37 +++ ...sDataInstanceBeanDeserializerModifier.java | 40 +++ ...essDataInstanceBeanSerializerModifier.java | 40 +++ ...leProcessDataInstanceConverterFactory.java | 65 ++++ ...eProcessInstanceDataEventDeserializer.java | 184 +++++++++++ ...pleProcessInstanceDataEventSerializer.java | 115 +++++++ ...ocessInstanceDataEventExtensionRecord.java | 168 ++++++++++ .../com.fasterxml.jackson.databind.Module | 1 + .../event/process/ProcessEventsTest.java | 221 +++++++++++-- .../GlobalObjectMapperQuarkusTemplate.java | 2 +- .../GroupingMessagingEventPublisher.java | 15 +- 31 files changed, 1747 insertions(+), 40 deletions(-) create mode 100644 api/kogito-events-api/src/main/java/org/kie/kogito/event/process/CloudEventVisitor.java create mode 100644 api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoEventBodySerializationHelper.java create mode 100644 api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoMarshallEventSupport.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/impl/JacksonTypeCloudEventDataConverter.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonProcessInstanceDataEventDeserializer.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonUserTaskInstanceDataEventDeserializer.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoDataEventSerializationHelper.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoSerializationModule.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanDeserializerModifier.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanSerializerModifier.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceConverterFactory.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventDeserializer.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java create mode 100644 api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/ProcessInstanceDataEventExtensionRecord.java create mode 100644 api/kogito-events-core/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module diff --git a/api/kogito-events-api/pom.xml b/api/kogito-events-api/pom.xml index 63726ae8252..1bd9097e94e 100644 --- a/api/kogito-events-api/pom.xml +++ b/api/kogito-events-api/pom.xml @@ -47,10 +47,6 @@ - - io.cloudevents - cloudevents-core - org.junit.jupiter @@ -72,6 +68,10 @@ slf4j-simple test + + org.kie.kogito + kogito-jackson-utils + diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/CloudEventVisitor.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/CloudEventVisitor.java new file mode 100644 index 00000000000..1d6f2c1c599 --- /dev/null +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/CloudEventVisitor.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.process; + +import org.kie.kogito.event.DataEvent; + +public interface CloudEventVisitor { + void visit(DataEvent cloudEvent); +} diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoEventBodySerializationHelper.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoEventBodySerializationHelper.java new file mode 100644 index 00000000000..d00146eb9c2 --- /dev/null +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoEventBodySerializationHelper.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.process; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Collection; +import java.util.Date; + +import org.kie.kogito.jackson.utils.ObjectMapperFactory; + +import com.fasterxml.jackson.databind.JsonNode; + +public class KogitoEventBodySerializationHelper { + + private KogitoEventBodySerializationHelper() { + } + + public static String readUTF(DataInput in) throws IOException { + boolean isNotNull = in.readBoolean(); + return isNotNull ? in.readUTF() : null; + } + + public static void writeUTF(DataOutput out, String string) throws IOException { + if (string == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeUTF(string); + } + } + + public static void writeDate(DataOutput out, Date date) throws IOException { + if (date == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeLong(date.getTime()); + } + } + + public static Date readDate(DataInput in) throws IOException { + boolean isNotNull = in.readBoolean(); + return isNotNull ? new Date(in.readLong()) : null; + } + + public static void writeTime(DataOutput out, OffsetDateTime date) throws IOException { + if (date == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeLong(date.toInstant().toEpochMilli()); + } + } + + public static OffsetDateTime readTime(DataInput in) throws IOException { + boolean isNotNull = in.readBoolean(); + return isNotNull ? Instant.ofEpochMilli(in.readLong()).atOffset(ZoneOffset.UTC) : null; + } + + public static void writeUTFCollection(DataOutput out, Collection collection) throws IOException { + if (collection == null) { + writeInt(out, -1); + } else { + writeInt(out, collection.size()); + for (String item : collection) { + writeUTF(out, item); + } + } + } + + public static > T readUTFCollection(DataInput in, T holder) throws IOException { + int size = readInt(in); + if (size == -1) { + return null; + } + while (size-- > 0) { + holder.add(readUTF(in)); + } + return holder; + } + + private enum SerType { + + NULL(KogitoEventBodySerializationHelper::writeNull, KogitoEventBodySerializationHelper::readNull), + JSON(KogitoEventBodySerializationHelper::writeJson, KogitoEventBodySerializationHelper::readJson), + DEFAULT(KogitoEventBodySerializationHelper::writeJson, KogitoEventBodySerializationHelper::readDefault), + STRING(KogitoEventBodySerializationHelper::writeString, DataInput::readUTF), + INT(KogitoEventBodySerializationHelper::writeInt, DataInput::readInt), + SHORT(KogitoEventBodySerializationHelper::writeShort, DataInput::readShort), + LONG(KogitoEventBodySerializationHelper::writeLong, DataInput::readLong), + BYTE(KogitoEventBodySerializationHelper::writeByte, DataInput::readByte), + BOOLEAN(KogitoEventBodySerializationHelper::writeBoolean, DataInput::readBoolean), + FLOAT(KogitoEventBodySerializationHelper::writeFloat, DataInput::readFloat), + DOUBLE(KogitoEventBodySerializationHelper::writeDouble, DataInput::readDouble); + + final ObjectWriter writer; + final ObjectReader reader; + + SerType(ObjectWriter writer, ObjectReader reader) { + this.writer = writer; + this.reader = reader; + } + + ObjectWriter writer() { + return writer; + } + + ObjectReader reader() { + return reader; + } + + static SerType fromType(Class type) { + if (JsonNode.class.isAssignableFrom(type)) { + return JSON; + } else if (String.class.isAssignableFrom(type)) { + return STRING; + } else if (Boolean.class.isAssignableFrom(type)) { + return BOOLEAN; + } else if (Integer.class.isAssignableFrom(type)) { + return INT; + } else if (Short.class.isAssignableFrom(type)) { + return SHORT; + } else if (Byte.class.isAssignableFrom(type)) { + return BYTE; + } else if (Long.class.isAssignableFrom(type)) { + return LONG; + } else if (Float.class.isAssignableFrom(type)) { + return FLOAT; + } else if (Double.class.isAssignableFrom(type)) { + return DOUBLE; + } else { + return DEFAULT; + } + } + + static SerType fromObject(Object obj) { + return obj == null ? NULL : fromType(obj.getClass()); + } + } + + private static void writeType(DataOutput out, SerType type) throws IOException { + out.writeByte(type.ordinal()); + } + + private static SerType readType(DataInput in) throws IOException { + return SerType.values()[in.readByte()]; + } + + public static void writeObject(DataOutput out, Object obj) throws IOException { + SerType type = SerType.fromObject(obj); + writeType(out, type); + type.writer().accept(out, obj); + } + + public static Object readObject(DataInput in) throws IOException { + return readType(in).reader().apply(in); + } + + @FunctionalInterface + private static interface ObjectWriter { + void accept(DataOutput out, Object obj) throws IOException; + } + + private static interface ObjectReader { + Object apply(DataInput out) throws IOException; + } + + private static void writeString(DataOutput out, Object obj) throws IOException { + out.writeUTF((String) obj); + } + + private static void writeBoolean(DataOutput out, Object obj) throws IOException { + out.writeBoolean((Boolean) obj); + } + + private static void writeInt(DataOutput out, Object obj) throws IOException { + out.writeInt((Integer) obj); + } + + private static void writeLong(DataOutput out, Object obj) throws IOException { + out.writeInt((Integer) obj); + } + + private static void writeShort(DataOutput out, Object obj) throws IOException { + out.writeShort((Short) obj); + } + + private static void writeByte(DataOutput out, Object obj) throws IOException { + out.writeByte((Byte) obj); + } + + private static void writeFloat(DataOutput out, Object obj) throws IOException { + out.writeFloat((Float) obj); + } + + private static void writeDouble(DataOutput out, Object obj) throws IOException { + out.writeDouble((Double) obj); + } + + private static void writeNull(DataOutput out, Object obj) { + // do nothing + } + + private static Object readNull(DataInput in) { + return null; + } + + public static void writeInteger(DataOutput out, Integer integer) throws IOException { + if (integer == null) { + writeType(out, SerType.NULL); + } else { + writeInt(out, integer.intValue()); + } + } + + public static Integer readInteger(DataInput in) throws IOException { + SerType type = readType(in); + return type == SerType.NULL ? null : readInt(in, type); + } + + public static void writeInt(DataOutput out, int size) throws IOException { + if (size < Byte.MAX_VALUE) { + writeType(out, SerType.BYTE); + out.writeByte((byte) size); + } else if (size < Short.MAX_VALUE) { + writeType(out, SerType.SHORT); + out.writeShort((short) size); + } else { + writeType(out, SerType.INT); + out.writeInt(size); + } + } + + public static int readInt(DataInput in) throws IOException { + SerType type = readType(in); + return readInt(in, type); + } + + private static int readInt(DataInput in, SerType type) throws IOException { + switch (type) { + case INT: + return in.readInt(); + case SHORT: + return in.readShort(); + case BYTE: + return in.readByte(); + default: + throw new IOException("Stream corrupted. Read unrecognized type " + type); + } + } + + private static void writeJson(DataOutput out, Object obj) throws IOException { + byte[] bytes = ObjectMapperFactory.get().writeValueAsBytes(obj); + out.writeInt(bytes.length); + out.write(bytes); + } + + private static Object readJson(DataInput in) throws IOException { + return readJson(in, JsonNode.class); + } + + private static Object readDefault(DataInput in) throws IOException { + return readJson(in, Object.class); + } + + private static Object readJson(DataInput in, Class type) throws IOException { + byte[] bytes = new byte[in.readInt()]; + in.readFully(bytes); + return ObjectMapperFactory.get().readValue(bytes, type); + } + + public static Date toDate(OffsetDateTime time) { + return time == null ? null : Date.from(time.toInstant()); + } +} diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoMarshallEventSupport.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoMarshallEventSupport.java new file mode 100644 index 00000000000..76693a64c6c --- /dev/null +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/KogitoMarshallEventSupport.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.process; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public interface KogitoMarshallEventSupport { + + void writeEvent(DataOutput out) throws IOException; + + void readEvent(DataInput in) throws IOException; +} diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorEventBody.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorEventBody.java index a6ea8e77855..e20807a3d0a 100644 --- a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorEventBody.java +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorEventBody.java @@ -19,9 +19,16 @@ package org.kie.kogito.event.process; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; import java.util.Date; -public class ProcessInstanceErrorEventBody { +import org.kie.kogito.event.DataEvent; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.*; + +public class ProcessInstanceErrorEventBody implements KogitoMarshallEventSupport, CloudEventVisitor { // common fields for events private Date eventDate; @@ -138,4 +145,27 @@ public ProcessInstanceErrorEventBody build() { return instance; } } + + @Override + public void readEvent(DataInput in) throws IOException { + nodeDefinitionId = in.readUTF(); + nodeInstanceId = in.readUTF(); + errorMessage = in.readUTF(); + } + + @Override + public void writeEvent(DataOutput out) throws IOException { + out.writeUTF(nodeDefinitionId); + out.writeUTF(nodeInstanceId); + out.writeUTF(errorMessage); + } + + @Override + public void visit(DataEvent dataEvent) { + this.processId = dataEvent.getKogitoProcessId(); + this.processInstanceId = dataEvent.getKogitoProcessInstanceId(); + this.processVersion = dataEvent.getKogitoProcessInstanceVersion(); + this.eventDate = toDate(dataEvent.getTime()); + this.eventUser = dataEvent.getKogitoIdentity(); + } } diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeEventBody.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeEventBody.java index ca6621c0b14..349a8280b86 100644 --- a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeEventBody.java +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeEventBody.java @@ -18,12 +18,19 @@ */ package org.kie.kogito.event.process; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Objects; -public class ProcessInstanceNodeEventBody { +import org.kie.kogito.event.DataEvent; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.*; + +public class ProcessInstanceNodeEventBody implements KogitoMarshallEventSupport, CloudEventVisitor { public static final int EVENT_TYPE_ENTER = 1; @@ -71,7 +78,42 @@ public class ProcessInstanceNodeEventBody { private Map data; - private ProcessInstanceNodeEventBody() { + @Override + public void writeEvent(DataOutput out) throws IOException { + writeInteger(out, eventType); + writeUTF(out, connectionNodeDefinitionId); + out.writeUTF(nodeDefinitionId); + writeUTF(out, nodeName); + out.writeUTF(nodeType); + out.writeUTF(nodeInstanceId); + writeUTF(out, workItemId); + writeDate(out, slaDueDate); + writeObject(out, data); + } + + @Override + public void readEvent(DataInput in) throws IOException { + eventType = readInteger(in); + connectionNodeDefinitionId = readUTF(in); + nodeDefinitionId = in.readUTF(); + nodeName = readUTF(in); + nodeType = in.readUTF(); + nodeInstanceId = in.readUTF(); + workItemId = readUTF(in); + slaDueDate = readDate(in); + data = (Map) readObject(in); + } + + @Override + public void visit(DataEvent dataEvent) { + this.processId = dataEvent.getKogitoProcessId(); + this.processInstanceId = dataEvent.getKogitoProcessInstanceId(); + this.processVersion = dataEvent.getKogitoProcessInstanceVersion(); + this.eventDate = toDate(dataEvent.getTime()); + this.eventUser = dataEvent.getKogitoIdentity(); + } + + public ProcessInstanceNodeEventBody() { this.data = new HashMap<>(); } @@ -246,5 +288,4 @@ public ProcessInstanceNodeEventBody build() { } } - } diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLAEventBody.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLAEventBody.java index 133c0e57155..d054a490b1f 100644 --- a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLAEventBody.java +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLAEventBody.java @@ -18,9 +18,20 @@ */ package org.kie.kogito.event.process; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; import java.util.Date; -public class ProcessInstanceSLAEventBody { +import org.kie.kogito.event.DataEvent; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.readDate; +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.readUTF; +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.toDate; +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.writeDate; +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.writeUTF; + +public class ProcessInstanceSLAEventBody implements KogitoMarshallEventSupport, CloudEventVisitor { // common fields for events private Date eventDate; @@ -47,6 +58,34 @@ public class ProcessInstanceSLAEventBody { private Date slaDueDate; + @Override + public void writeEvent(DataOutput out) throws IOException { + out.writeUTF(nodeDefinitionId); + writeUTF(out, nodeName); + out.writeUTF(nodeType); + out.writeUTF(nodeInstanceId); + writeDate(out, slaDueDate); + + } + + @Override + public void readEvent(DataInput in) throws IOException { + nodeDefinitionId = in.readUTF(); + nodeName = readUTF(in); + nodeType = in.readUTF(); + nodeInstanceId = in.readUTF(); + slaDueDate = readDate(in); + } + + @Override + public void visit(DataEvent dataEvent) { + this.processId = dataEvent.getKogitoProcessId(); + this.processInstanceId = dataEvent.getKogitoProcessInstanceId(); + this.processVersion = dataEvent.getKogitoProcessInstanceVersion(); + this.eventDate = toDate(dataEvent.getTime()); + this.eventUser = dataEvent.getKogitoIdentity(); + } + public Date getSlaDueDate() { return slaDueDate; } diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateEventBody.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateEventBody.java index b9bb4fd9ad3..8c7c291110e 100644 --- a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateEventBody.java +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateEventBody.java @@ -18,13 +18,21 @@ */ package org.kie.kogito.event.process; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; import java.util.Set; -public class ProcessInstanceStateEventBody { +import org.kie.kogito.event.DataEvent; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.*; + +public class ProcessInstanceStateEventBody implements KogitoMarshallEventSupport, CloudEventVisitor { public static final int EVENT_TYPE_STARTED = 1; public static final int EVENT_TYPE_ENDED = 2; @@ -65,6 +73,38 @@ public class ProcessInstanceStateEventBody { public Date slaDueDate; + @Override + public void writeEvent(DataOutput out) throws IOException { + writeInteger(out, eventType); + writeUTF(out, processName); + writeInteger(out, state); + writeUTFCollection(out, roles); + writeDate(out, slaDueDate); + } + + @Override + public void readEvent(DataInput in) throws IOException { + eventType = readInteger(in); + processName = readUTF(in); + state = readInteger(in); + roles = readUTFCollection(in, new LinkedHashSet<>()); + slaDueDate = readDate(in); + } + + @Override + public void visit(DataEvent dataEvent) { + this.processId = dataEvent.getKogitoProcessId(); + this.processInstanceId = dataEvent.getKogitoProcessInstanceId(); + this.processVersion = dataEvent.getKogitoProcessInstanceVersion(); + this.eventDate = toDate(dataEvent.getTime()); + this.eventUser = dataEvent.getKogitoIdentity(); + this.parentInstanceId = dataEvent.getKogitoParentProcessInstanceId(); + this.rootProcessId = dataEvent.getKogitoRootProcessId(); + this.rootProcessInstanceId = dataEvent.getKogitoRootProcessInstanceId(); + this.processType = dataEvent.getKogitoProcessType(); + this.businessKey = dataEvent.getKogitoBusinessKey(); + } + public Date getEventDate() { return eventDate; } @@ -262,4 +302,5 @@ public ProcessInstanceStateEventBody build() { } } + } diff --git a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableEventBody.java b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableEventBody.java index 00a55b9cd86..2008f264f6d 100644 --- a/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableEventBody.java +++ b/api/kogito-events-api/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableEventBody.java @@ -18,12 +18,19 @@ */ package org.kie.kogito.event.process; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Objects; -public class ProcessInstanceVariableEventBody { +import org.kie.kogito.event.DataEvent; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.*; + +public class ProcessInstanceVariableEventBody implements KogitoMarshallEventSupport, CloudEventVisitor { // common fields for events private Date eventDate; @@ -46,6 +53,33 @@ public class ProcessInstanceVariableEventBody { private String variableName; private Object variableValue; + @Override + public void writeEvent(DataOutput out) throws IOException { + writeUTF(out, nodeContainerDefinitionId); + writeUTF(out, nodeContainerInstanceId); + writeUTF(out, variableId); + out.writeUTF(variableName); + writeObject(out, variableValue); + } + + @Override + public void readEvent(DataInput in) throws IOException { + nodeContainerDefinitionId = readUTF(in); + nodeContainerInstanceId = readUTF(in); + variableId = readUTF(in); + variableName = in.readUTF(); + variableValue = readObject(in); + } + + @Override + public void visit(DataEvent dataEvent) { + this.processId = dataEvent.getKogitoProcessId(); + this.processInstanceId = dataEvent.getKogitoProcessInstanceId(); + this.processVersion = dataEvent.getKogitoProcessInstanceVersion(); + this.eventDate = toDate(dataEvent.getTime()); + this.eventUser = dataEvent.getKogitoIdentity(); + } + public Date getEventDate() { return eventDate; } @@ -184,4 +218,5 @@ public ProcessInstanceVariableEventBody build() { return instance; } } + } diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/DataEventFactory.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/DataEventFactory.java index a52df39f9df..f2317a5d667 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/DataEventFactory.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/DataEventFactory.java @@ -18,6 +18,7 @@ */ package org.kie.kogito.event; +import java.io.IOException; import java.net.URI; import java.time.OffsetDateTime; import java.util.Optional; @@ -43,6 +44,22 @@ public static DataEvent from(CloudEvent event, Converter(event, dataUnmarshaller); } + public static , V> T from(T dataEvent, CloudEvent cloudEvent, Converter dataUnmarshaller) throws IOException { + dataEvent.setSpecVersion(cloudEvent.getSpecVersion()); + dataEvent.setId(cloudEvent.getId()); + dataEvent.setType(cloudEvent.getType()); + dataEvent.setSource(cloudEvent.getSource()); + dataEvent.setDataContentType(cloudEvent.getDataContentType()); + dataEvent.setDataSchema(cloudEvent.getDataSchema()); + dataEvent.setSubject(cloudEvent.getSubject()); + dataEvent.setTime(cloudEvent.getTime()); + cloudEvent.getExtensionNames().forEach(extensionName -> dataEvent.addExtensionAttribute(extensionName, cloudEvent.getExtension(extensionName))); + if (cloudEvent.getData() != null) { + dataEvent.setData(dataUnmarshaller.convert(cloudEvent.getData())); + } + return dataEvent; + } + public static DataEvent from(T eventData, String trigger, KogitoProcessInstance pi) { return from(eventData, trigger, URI.create("/process/" + pi.getProcessId()), Optional.empty(), ProcessMeta.fromKogitoProcessInstance(pi)); } diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/impl/JacksonTypeCloudEventDataConverter.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/impl/JacksonTypeCloudEventDataConverter.java new file mode 100644 index 00000000000..3e64b44a5db --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/impl/JacksonTypeCloudEventDataConverter.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.impl; + +import java.io.IOException; + +import org.kie.kogito.event.Converter; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.cloudevents.CloudEventData; + +public class JacksonTypeCloudEventDataConverter implements Converter { + + private ObjectMapper objectMapper; + private TypeReference outputType; + + public JacksonTypeCloudEventDataConverter(ObjectMapper objectMapper, TypeReference outputType) { + this.objectMapper = objectMapper; + this.outputType = outputType; + } + + @Override + public O convert(CloudEventData value) throws IOException { + return objectMapper.readValue(value.toBytes(), outputType); + } +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java index 7db8c0e7659..f29a920c132 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java @@ -21,14 +21,25 @@ import java.net.URI; import java.util.Collection; -public class MultipleProcessInstanceDataEvent extends ProcessInstanceDataEvent>> { +public class MultipleProcessInstanceDataEvent extends ProcessInstanceDataEvent>> { - public static final String TYPE = "MultipleProcessInstanceDataEvent"; + public static final String MULTIPLE_TYPE = "MultipleProcessInstanceDataEvent"; + public static final String BINARY_CONTENT_TYPE = "application/octet-stream"; + public static final String COMPRESS_DATA = "compressdata"; public MultipleProcessInstanceDataEvent() { } - public MultipleProcessInstanceDataEvent(URI source, Collection> body) { - super(TYPE, source, body); + public MultipleProcessInstanceDataEvent(URI source, Collection> body) { + super(MULTIPLE_TYPE, source, body); + } + + public boolean isCompressed() { + Object extension = getExtension(MultipleProcessInstanceDataEvent.COMPRESS_DATA); + return extension instanceof Boolean ? ((Boolean) extension).booleanValue() : false; + } + + public void setCompressed(boolean compressed) { + addExtensionAttribute(COMPRESS_DATA, compressed); } } diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorDataEvent.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorDataEvent.java index 1427e917f23..2c50f8e2262 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorDataEvent.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceErrorDataEvent.java @@ -22,11 +22,14 @@ public class ProcessInstanceErrorDataEvent extends ProcessInstanceDataEvent { + public static final String ERROR_TYPE = "ProcessInstanceErrorDataEvent"; + public ProcessInstanceErrorDataEvent() { + this.setType(ERROR_TYPE); } public ProcessInstanceErrorDataEvent(String source, String addons, String identity, Map metaData, ProcessInstanceErrorEventBody body) { - super("ProcessInstanceErrorDataEvent", + super(ERROR_TYPE, source, body, (String) metaData.get(ProcessInstanceEventMetadata.PROCESS_INSTANCE_ID_META_DATA), diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeDataEvent.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeDataEvent.java index db118aa009f..e1e6a74e76d 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeDataEvent.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceNodeDataEvent.java @@ -22,11 +22,14 @@ public class ProcessInstanceNodeDataEvent extends ProcessInstanceDataEvent { + public static final String NODE_TYPE = "ProcessInstanceNodeDataEvent"; + public ProcessInstanceNodeDataEvent() { + this.setType(NODE_TYPE); } public ProcessInstanceNodeDataEvent(String source, String addons, String identity, Map metaData, ProcessInstanceNodeEventBody body) { - super("ProcessInstanceNodeDataEvent", + super(NODE_TYPE, source, body, (String) metaData.get(ProcessInstanceEventMetadata.PROCESS_INSTANCE_ID_META_DATA), diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLADataEvent.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLADataEvent.java index 90139ce0022..e5b743aeac9 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLADataEvent.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceSLADataEvent.java @@ -22,11 +22,14 @@ public class ProcessInstanceSLADataEvent extends ProcessInstanceDataEvent { + public static final String SLA_TYPE = "ProcessInstanceSLADataEvent"; + public ProcessInstanceSLADataEvent() { + this.setType(SLA_TYPE); } public ProcessInstanceSLADataEvent(String source, String addons, String identity, Map metaData, ProcessInstanceSLAEventBody body) { - super("ProcessInstanceSLADataEvent", + super(SLA_TYPE, source, body, (String) metaData.get(ProcessInstanceEventMetadata.PROCESS_INSTANCE_ID_META_DATA), diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateDataEvent.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateDataEvent.java index 38d30defdab..a0ce3e1003b 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateDataEvent.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceStateDataEvent.java @@ -22,11 +22,14 @@ public class ProcessInstanceStateDataEvent extends ProcessInstanceDataEvent { + public static final String STATE_TYPE = "ProcessInstanceStateDataEvent"; + public ProcessInstanceStateDataEvent() { + this.setType(STATE_TYPE); } public ProcessInstanceStateDataEvent(String source, String addons, String identity, Map metaData, ProcessInstanceStateEventBody body) { - super("ProcessInstanceStateDataEvent", + super(STATE_TYPE, source, body, (String) metaData.get(ProcessInstanceEventMetadata.PROCESS_INSTANCE_ID_META_DATA), diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableDataEvent.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableDataEvent.java index b7c83d367fc..1acf471d5ba 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableDataEvent.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/ProcessInstanceVariableDataEvent.java @@ -32,15 +32,18 @@ public class ProcessInstanceVariableDataEvent extends ProcessInstanceDataEvent

INTERNAL_EXTENSION_ATTRIBUTES = Collections.singleton(CloudEventExtensionConstants.KOGITO_VARIABLE_NAME); + public static final String VAR_TYPE = "ProcessInstanceVariableDataEvent"; + @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonProperty(CloudEventExtensionConstants.KOGITO_VARIABLE_NAME) private String kogitoVariableName; public ProcessInstanceVariableDataEvent() { + this.setType(VAR_TYPE); } public ProcessInstanceVariableDataEvent(String source, String addons, String identity, Map metaData, ProcessInstanceVariableEventBody body) { - super("ProcessInstanceVariableDataEvent", + super(VAR_TYPE, source, body, (String) metaData.get(ProcessInstanceEventMetadata.PROCESS_INSTANCE_ID_META_DATA), diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonProcessInstanceDataEventDeserializer.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonProcessInstanceDataEventDeserializer.java new file mode 100644 index 00000000000..d97b2512506 --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonProcessInstanceDataEventDeserializer.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import java.io.IOException; + +import org.kie.kogito.event.process.MultipleProcessInstanceDataEvent; +import org.kie.kogito.event.process.ProcessInstanceDataEvent; +import org.kie.kogito.event.process.ProcessInstanceErrorDataEvent; +import org.kie.kogito.event.process.ProcessInstanceNodeDataEvent; +import org.kie.kogito.event.process.ProcessInstanceSLADataEvent; +import org.kie.kogito.event.process.ProcessInstanceStateDataEvent; +import org.kie.kogito.event.process.ProcessInstanceVariableDataEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +public class JsonProcessInstanceDataEventDeserializer extends StdDeserializer> { + + private static final Logger LOGGER = LoggerFactory.getLogger(JsonProcessInstanceDataEventDeserializer.class); + + private static final long serialVersionUID = 6152014726577574241L; + + public JsonProcessInstanceDataEventDeserializer() { + this(JsonProcessInstanceDataEventDeserializer.class); + } + + public JsonProcessInstanceDataEventDeserializer(Class vc) { + super(vc); + } + + @Override + public ProcessInstanceDataEvent deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode node = jp.getCodec().readTree(jp); + LOGGER.debug("Deserialize process instance data event: {}", node); + String type = node.get("type").asText(); + + switch (type) { + case MultipleProcessInstanceDataEvent.MULTIPLE_TYPE: + return jp.getCodec().treeToValue(node, MultipleProcessInstanceDataEvent.class); + case ProcessInstanceErrorDataEvent.ERROR_TYPE: + return (ProcessInstanceDataEvent) jp.getCodec().treeToValue(node, ProcessInstanceErrorDataEvent.class); + case ProcessInstanceNodeDataEvent.NODE_TYPE: + return (ProcessInstanceDataEvent) jp.getCodec().treeToValue(node, ProcessInstanceNodeDataEvent.class); + case ProcessInstanceSLADataEvent.SLA_TYPE: + return (ProcessInstanceDataEvent) jp.getCodec().treeToValue(node, ProcessInstanceSLADataEvent.class); + case ProcessInstanceStateDataEvent.STATE_TYPE: + return (ProcessInstanceDataEvent) jp.getCodec().treeToValue(node, ProcessInstanceStateDataEvent.class); + case ProcessInstanceVariableDataEvent.VAR_TYPE: + return (ProcessInstanceDataEvent) jp.getCodec().treeToValue(node, ProcessInstanceVariableDataEvent.class); + default: + LOGGER.warn("Unknown type {} in json data {}", type, node); + return (ProcessInstanceDataEvent) jp.getCodec().treeToValue(node, ProcessInstanceDataEvent.class); + + } + } +} \ No newline at end of file diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonUserTaskInstanceDataEventDeserializer.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonUserTaskInstanceDataEventDeserializer.java new file mode 100644 index 00000000000..7e8bf5458ad --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/JsonUserTaskInstanceDataEventDeserializer.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import java.io.IOException; + +import org.kie.kogito.event.usertask.MultipleUserTaskInstanceDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceAssignmentDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceAttachmentDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceCommentDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceDeadlineDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceStateDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceVariableDataEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +public class JsonUserTaskInstanceDataEventDeserializer extends StdDeserializer> { + + private static final Logger LOGGER = LoggerFactory.getLogger(JsonUserTaskInstanceDataEventDeserializer.class); + + private static final long serialVersionUID = -6626663191296012306L; + + public JsonUserTaskInstanceDataEventDeserializer() { + this(null); + } + + public JsonUserTaskInstanceDataEventDeserializer(Class vc) { + super(vc); + } + + @Override + public UserTaskInstanceDataEvent deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode node = jp.getCodec().readTree(jp); + LOGGER.debug("Deserialize user task instance data event: {}", node); + String type = node.get("type").asText(); + + switch (type) { + case MultipleUserTaskInstanceDataEvent.TYPE: + return jp.getCodec().treeToValue(node, MultipleUserTaskInstanceDataEvent.class); + case "UserTaskInstanceAssignmentDataEvent": + return (UserTaskInstanceDataEvent) jp.getCodec().treeToValue(node, UserTaskInstanceAssignmentDataEvent.class); + case "UserTaskInstanceAttachmentDataEvent": + return (UserTaskInstanceDataEvent) jp.getCodec().treeToValue(node, UserTaskInstanceAttachmentDataEvent.class); + case "UserTaskInstanceCommentDataEvent": + return (UserTaskInstanceDataEvent) jp.getCodec().treeToValue(node, UserTaskInstanceCommentDataEvent.class); + case "UserTaskInstanceDeadlineDataEvent": + return (UserTaskInstanceDataEvent) jp.getCodec().treeToValue(node, UserTaskInstanceDeadlineDataEvent.class); + case "UserTaskInstanceStateDataEvent": + return (UserTaskInstanceDataEvent) jp.getCodec().treeToValue(node, UserTaskInstanceStateDataEvent.class); + case "UserTaskInstanceVariableDataEvent": + return (UserTaskInstanceDataEvent) jp.getCodec().treeToValue(node, UserTaskInstanceVariableDataEvent.class); + default: + LOGGER.warn("Unknown type {} in json data {}", type, node); + return (UserTaskInstanceDataEvent) jp.getCodec().treeToValue(node, UserTaskInstanceDataEvent.class); + + } + } +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoDataEventSerializationHelper.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoDataEventSerializationHelper.java new file mode 100644 index 00000000000..f4e512eb223 --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoDataEventSerializationHelper.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.net.URI; + +import org.kie.kogito.event.AbstractDataEvent; +import org.kie.kogito.event.DataEvent; +import org.kie.kogito.event.process.ProcessInstanceDataEvent; + +import io.cloudevents.SpecVersion; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.*; + +class KogitoDataEventSerializationHelper { + + private KogitoDataEventSerializationHelper() { + } + + static void writeCloudEventAttrs(DataOutput out, DataEvent data) throws IOException { + out.writeUTF(data.getSpecVersion().toString()); + out.writeUTF(data.getId()); + writeUTF(out, data.getSubject()); + writeUTF(out, data.getDataContentType()); + writeUTF(out, data.getDataSchema() != null ? data.getDataSchema().toString() : null); + } + + static > T readCloudEventAttrs(DataInput in, T data) throws IOException { + data.setSpecVersion(SpecVersion.parse(in.readUTF())); + data.setId(in.readUTF()); + data.setSubject(readUTF(in)); + data.setDataContentType(readUTF(in)); + String dataSchema = readUTF(in); + if (dataSchema != null) { + data.setDataSchema(URI.create(dataSchema)); + } + return data; + } + + static void populateCloudEvent(ProcessInstanceDataEvent event, ProcessInstanceDataEventExtensionRecord info) { + event.setKogitoBusinessKey(info.getBusinessKey()); + event.setKogitoProcessId(info.getId()); + event.setKogitoProcessInstanceId(info.getInstanceId()); + event.setKogitoParentProcessInstanceId(info.getParentInstanceId()); + event.setKogitoProcessInstanceState(info.getState()); + event.setKogitoProcessInstanceVersion(info.getVersion()); + event.setKogitoProcessType(info.getType()); + event.setKogitoRootProcessId(info.getRootId()); + event.setKogitoRootProcessInstanceId(info.getRootInstanceId()); + event.setKogitoIdentity(info.getIdentity()); + event.setSource(info.getSource()); + event.setKogitoAddons(info.getAddons()); + } + +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoSerializationModule.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoSerializationModule.java new file mode 100644 index 00000000000..381e0b93d2a --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/KogitoSerializationModule.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import org.kie.kogito.event.process.ProcessInstanceDataEvent; +import org.kie.kogito.event.usertask.UserTaskInstanceDataEvent; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class KogitoSerializationModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public KogitoSerializationModule() { + super("KogitoSerialization"); + setSerializerModifier(new MultipleProcessDataInstanceBeanSerializerModifier()); + setDeserializerModifier(new MultipleProcessDataInstanceBeanDeserializerModifier()); + addDeserializer(ProcessInstanceDataEvent.class, new JsonProcessInstanceDataEventDeserializer()); + addDeserializer(UserTaskInstanceDataEvent.class, new JsonUserTaskInstanceDataEventDeserializer()); + } +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanDeserializerModifier.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanDeserializerModifier.java new file mode 100644 index 00000000000..d72be4e5b57 --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanDeserializerModifier.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import org.kie.kogito.event.process.MultipleProcessInstanceDataEvent; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; + +public class MultipleProcessDataInstanceBeanDeserializerModifier extends BeanDeserializerModifier { + + private static final long serialVersionUID = 1L; + + @Override + public JsonDeserializer modifyDeserializer( + DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer deserializer) { + if (beanDesc.getBeanClass().equals(MultipleProcessInstanceDataEvent.class)) { + return new MultipleProcessInstanceDataEventDeserializer((JsonDeserializer) deserializer); + } + return deserializer; + } +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanSerializerModifier.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanSerializerModifier.java new file mode 100644 index 00000000000..3c62d932a33 --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceBeanSerializerModifier.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import org.kie.kogito.event.process.MultipleProcessInstanceDataEvent; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; + +public class MultipleProcessDataInstanceBeanSerializerModifier extends BeanSerializerModifier { + + private static final long serialVersionUID = 1L; + + @Override + public JsonSerializer modifySerializer( + SerializationConfig config, BeanDescription beanDesc, JsonSerializer serializer) { + if (beanDesc.getBeanClass().equals(MultipleProcessInstanceDataEvent.class)) { + return new MultipleProcessInstanceDataEventSerializer((JsonSerializer) serializer); + } + return serializer; + } +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceConverterFactory.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceConverterFactory.java new file mode 100644 index 00000000000..4f8a6205c20 --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessDataInstanceConverterFactory.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import java.io.IOException; +import java.util.Base64; +import java.util.Collection; + +import org.kie.kogito.event.Converter; +import org.kie.kogito.event.impl.JacksonTypeCloudEventDataConverter; +import org.kie.kogito.event.process.KogitoMarshallEventSupport; +import org.kie.kogito.event.process.MultipleProcessInstanceDataEvent; +import org.kie.kogito.event.process.ProcessInstanceDataEvent; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; + +public class MultipleProcessDataInstanceConverterFactory { + + private MultipleProcessDataInstanceConverterFactory() { + } + + public static Converter>> fromCloudEvent(CloudEvent cloudEvent, ObjectMapper objectMapper) { + if (MultipleProcessInstanceDataEvent.BINARY_CONTENT_TYPE.equals(cloudEvent.getDataContentType())) { + return isCompressed(cloudEvent) ? compressedConverter : binaryConverter; + } else { + return new JacksonTypeCloudEventDataConverter<>(objectMapper, new TypeReference>>() { + }); + } + } + + private static boolean isCompressed(CloudEvent event) { + Object value = event.getExtension(MultipleProcessInstanceDataEvent.COMPRESS_DATA); + return value instanceof Boolean ? ((Boolean) value).booleanValue() : false; + } + + private static Converter>> binaryConverter = + data -> deserialize(data, false); + + private static Converter>> compressedConverter = + data -> deserialize(data, true); + + private static Collection> deserialize(CloudEventData data, boolean compress) throws IOException { + return MultipleProcessInstanceDataEventDeserializer.readFromBytes(Base64.getDecoder().decode(data.toBytes()), compress); + } +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventDeserializer.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventDeserializer.java new file mode 100644 index 00000000000..6e0b0f262c0 --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventDeserializer.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import java.io.ByteArrayInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; +import java.util.zip.GZIPInputStream; + +import org.kie.kogito.event.process.CloudEventVisitor; +import org.kie.kogito.event.process.KogitoMarshallEventSupport; +import org.kie.kogito.event.process.MultipleProcessInstanceDataEvent; +import org.kie.kogito.event.process.ProcessInstanceDataEvent; +import org.kie.kogito.event.process.ProcessInstanceErrorDataEvent; +import org.kie.kogito.event.process.ProcessInstanceErrorEventBody; +import org.kie.kogito.event.process.ProcessInstanceNodeDataEvent; +import org.kie.kogito.event.process.ProcessInstanceNodeEventBody; +import org.kie.kogito.event.process.ProcessInstanceSLADataEvent; +import org.kie.kogito.event.process.ProcessInstanceSLAEventBody; +import org.kie.kogito.event.process.ProcessInstanceStateDataEvent; +import org.kie.kogito.event.process.ProcessInstanceStateEventBody; +import org.kie.kogito.event.process.ProcessInstanceVariableDataEvent; +import org.kie.kogito.event.process.ProcessInstanceVariableEventBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; + +import io.cloudevents.SpecVersion; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.readInt; + +public class MultipleProcessInstanceDataEventDeserializer extends JsonDeserializer implements ResolvableDeserializer { + + private static final Logger logger = LoggerFactory.getLogger(MultipleProcessInstanceDataEventDeserializer.class); + + private JsonDeserializer defaultDeserializer; + + public MultipleProcessInstanceDataEventDeserializer(JsonDeserializer deserializer) { + this.defaultDeserializer = deserializer; + } + + @Override + public void resolve(DeserializationContext ctxt) throws JsonMappingException { + ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt); + } + + @Override + public MultipleProcessInstanceDataEvent deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + JsonNode node = p.getCodec().readTree(p); + JsonNode dataContentType = node.get("datacontenttype"); + if (dataContentType != null && MultipleProcessInstanceDataEvent.BINARY_CONTENT_TYPE.equals(dataContentType.asText())) { + MultipleProcessInstanceDataEvent event = new MultipleProcessInstanceDataEvent(); + event.setDataContentType(dataContentType.asText()); + event.setSource(URI.create(node.get("source").asText())); + event.setType(node.get("type").asText()); + event.setSpecVersion(SpecVersion.parse(node.get("specversion").asText())); + event.setId(node.get("id").asText()); + JsonNode data = node.get("data"); + if (data != null) { + event.setData(readFromBytes(data.binaryValue(), isCompressed(node))); + } + return event; + } else { + JsonParser newParser = node.traverse(p.getCodec()); + newParser.nextToken(); + return (MultipleProcessInstanceDataEvent) defaultDeserializer.deserialize(newParser, ctxt); + } + } + + private static boolean isCompressed(JsonNode node) { + JsonNode compress = node.get(MultipleProcessInstanceDataEvent.COMPRESS_DATA); + return compress != null && compress.isBoolean() ? compress.asBoolean() : false; + } + + static Collection> readFromBytes(byte[] binaryValue, boolean compressed) throws IOException { + InputStream wrappedIn = new ByteArrayInputStream(binaryValue); + if (compressed) { + logger.trace("Gzip compressed byte array"); + wrappedIn = new GZIPInputStream(wrappedIn); + } + try (DataInputStream in = new DataInputStream(wrappedIn)) { + int size = readInt(in); + logger.trace("Reading collection of size {}", size); + Collection> result = new ArrayList<>(size); + List infos = new ArrayList<>(); + while (size-- > 0) { + byte readInfo = in.readByte(); + logger.trace("Info ordinal is {}", readInfo); + ProcessInstanceDataEventExtensionRecord info; + if (readInfo == -1) { + info = new ProcessInstanceDataEventExtensionRecord(); + info.readEvent(in); + logger.trace("Info readed is {}", info); + infos.add(info); + } else { + info = infos.get(readInfo); + logger.trace("Info cached is {}", info); + } + String type = in.readUTF(); + logger.trace("Type is {}", info); + result.add(getCloudEvent(in, type, info)); + logger.trace("{} events remaining", size); + } + return result; + } + } + + private static ProcessInstanceDataEvent getCloudEvent(DataInputStream in, String type, ProcessInstanceDataEventExtensionRecord info) throws IOException { + switch (type) { + case ProcessInstanceVariableDataEvent.VAR_TYPE: + ProcessInstanceVariableDataEvent item = buildDataEvent(in, new ProcessInstanceVariableDataEvent(), ProcessInstanceVariableEventBody::new, info); + item.setKogitoVariableName(item.getData().getVariableName()); + return item; + case ProcessInstanceStateDataEvent.STATE_TYPE: + return buildDataEvent(in, new ProcessInstanceStateDataEvent(), ProcessInstanceStateEventBody::new, info); + case ProcessInstanceNodeDataEvent.NODE_TYPE: + return buildDataEvent(in, new ProcessInstanceNodeDataEvent(), ProcessInstanceNodeEventBody::new, info); + case ProcessInstanceErrorDataEvent.ERROR_TYPE: + return buildDataEvent(in, new ProcessInstanceErrorDataEvent(), ProcessInstanceErrorEventBody::new, info); + case ProcessInstanceSLADataEvent.SLA_TYPE: + return buildDataEvent(in, new ProcessInstanceSLADataEvent(), ProcessInstanceSLAEventBody::new, info); + default: + throw new UnsupportedOperationException("Unrecognized event type " + type); + } + } + + private static , V extends KogitoMarshallEventSupport & CloudEventVisitor> T buildDataEvent(DataInput in, T cloudEvent, Supplier bodySupplier, + ProcessInstanceDataEventExtensionRecord info) throws IOException { + int delta = readInt(in); + logger.trace("Time delta is {}", delta); + cloudEvent.setTime(info.getTime().plus(delta, ChronoUnit.MILLIS)); + KogitoDataEventSerializationHelper.readCloudEventAttrs(in, cloudEvent); + logger.trace("Cloud event before population {}", cloudEvent); + KogitoDataEventSerializationHelper.populateCloudEvent(cloudEvent, info); + logger.trace("Cloud event after population {}", cloudEvent); + + boolean isNotNull = in.readBoolean(); + if (isNotNull) { + logger.trace("Data is not null"); + V body = bodySupplier.get(); + body.readEvent(in); + logger.trace("Event body before population {}", body); + body.visit(cloudEvent); + logger.trace("Event body after population {}", body); + cloudEvent.setData(body); + } else { + logger.trace("Data is null"); + } + return cloudEvent; + } + +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java new file mode 100644 index 00000000000..42825e9679c --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import org.kie.kogito.event.process.KogitoMarshallEventSupport; +import org.kie.kogito.event.process.MultipleProcessInstanceDataEvent; +import org.kie.kogito.event.process.ProcessInstanceDataEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.writeInt; + +public class MultipleProcessInstanceDataEventSerializer extends JsonSerializer { + + private static final Logger logger = LoggerFactory.getLogger(MultipleProcessInstanceDataEventDeserializer.class); + + private JsonSerializer defaultSerializer; + + public MultipleProcessInstanceDataEventSerializer(JsonSerializer serializer) { + this.defaultSerializer = serializer; + } + + @Override + public void serialize(MultipleProcessInstanceDataEvent value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + if (MultipleProcessInstanceDataEvent.BINARY_CONTENT_TYPE.equals(value.getDataContentType())) { + gen.writeStartObject(); + gen.writeStringField("datacontenttype", value.getDataContentType()); + gen.writeStringField("source", value.getSource().toString()); + gen.writeStringField("id", value.getId()); + gen.writeStringField("specversion", value.getSpecVersion().toString()); + gen.writeStringField("type", value.getType()); + boolean compress = value.isCompressed(); + if (compress) { + gen.writeBooleanField(MultipleProcessInstanceDataEvent.COMPRESS_DATA, true); + } + gen.writeBinaryField("data", dataAsBytes(gen, value.getData(), compress)); + gen.writeEndObject(); + } else { + defaultSerializer.serialize(value, gen, serializers); + } + } + + private byte[] dataAsBytes(JsonGenerator gen, Collection> data, boolean compress) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(compress ? new GZIPOutputStream(bytesOut) : bytesOut)) { + logger.trace("Writing size {}", data.size()); + writeInt(out, data.size()); + Map infos = new HashMap<>(); + for (ProcessInstanceDataEvent cloudEvent : data) { + String key = cloudEvent.getKogitoProcessInstanceId(); + ProcessInstanceDataEventExtensionRecord info = infos.get(key); + if (info == null) { + logger.trace("Writing marker byte -1"); + out.writeByte((byte) -1); + info = new ProcessInstanceDataEventExtensionRecord(infos.size(), cloudEvent); + logger.trace("Writing info", info); + info.writeEvent(out); + infos.put(key, info); + } else { + logger.trace("Writing marker byte {}", info.getOrdinal()); + out.writeByte((byte) info.getOrdinal()); + } + logger.trace("Writing type {}", cloudEvent.getType()); + out.writeUTF(cloudEvent.getType()); + int timeDelta = cloudEvent.getTime().compareTo(info.getTime()); + logger.trace("Writing time delta {}", timeDelta); + writeInt(out, timeDelta); + logger.trace("Writing cloud event attrs {}", cloudEvent); + KogitoDataEventSerializationHelper.writeCloudEventAttrs(out, cloudEvent); + KogitoMarshallEventSupport itemData = cloudEvent.getData(); + if (itemData != null) { + logger.trace("Writing data not null boolean"); + out.writeBoolean(true); + logger.trace("Writing cloud event body {}", itemData); + itemData.writeEvent(out); + } else { + logger.trace("Writing data null boolean"); + out.writeBoolean(false); + } + logger.trace("individual event writing completed"); + } + } + return bytesOut.toByteArray(); + } + +} diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/ProcessInstanceDataEventExtensionRecord.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/ProcessInstanceDataEventExtensionRecord.java new file mode 100644 index 00000000000..28abac1a281 --- /dev/null +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/ProcessInstanceDataEventExtensionRecord.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.event.serializer; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.net.URI; +import java.time.OffsetDateTime; + +import org.kie.kogito.event.process.KogitoMarshallEventSupport; +import org.kie.kogito.event.process.ProcessInstanceDataEvent; + +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.*; + +class ProcessInstanceDataEventExtensionRecord implements KogitoMarshallEventSupport { + + // referenceId and startFromNode are not used by process instance events + private String id; + private String instanceId; + private String version; + private String state; + private String type; + private String parentInstanceId; + private String rootId; + private String rootInstanceId; + private String businessKey; + private String identity; + private URI source; + private OffsetDateTime time; + private String addons; + private transient int ordinal; + + public ProcessInstanceDataEventExtensionRecord() { + } + + public ProcessInstanceDataEventExtensionRecord(int ordinal, ProcessInstanceDataEvent dataEvent) { + this.ordinal = ordinal; + id = dataEvent.getKogitoProcessId(); + instanceId = dataEvent.getKogitoProcessInstanceId(); + version = dataEvent.getKogitoProcessInstanceVersion(); + state = dataEvent.getKogitoProcessInstanceState(); + type = dataEvent.getKogitoProcessType(); + parentInstanceId = dataEvent.getKogitoParentProcessInstanceId(); + rootId = dataEvent.getKogitoRootProcessId(); + rootInstanceId = dataEvent.getKogitoRootProcessInstanceId(); + businessKey = dataEvent.getKogitoBusinessKey(); + identity = dataEvent.getKogitoIdentity(); + time = dataEvent.getTime(); + source = dataEvent.getSource(); + addons = dataEvent.getKogitoAddons(); + } + + public int getOrdinal() { + return ordinal; + } + + public String getId() { + return id; + } + + public String getBusinessKey() { + return businessKey; + } + + public String getInstanceId() { + return instanceId; + } + + public String getVersion() { + return version; + } + + public String getState() { + return state; + } + + public String getType() { + return type; + } + + public String getParentInstanceId() { + return parentInstanceId; + } + + public String getRootId() { + return rootId; + } + + public String getRootInstanceId() { + return rootInstanceId; + } + + public String getIdentity() { + return identity; + } + + public OffsetDateTime getTime() { + return time; + } + + public URI getSource() { + return source; + } + + public String getAddons() { + return addons; + } + + @Override + public void writeEvent(DataOutput out) throws IOException { + out.writeUTF(id); + out.writeUTF(instanceId); + out.writeUTF(version); + out.writeUTF(state); + writeUTF(out, type); + writeUTF(out, parentInstanceId); + writeUTF(out, rootId); + writeUTF(out, rootInstanceId); + writeUTF(out, businessKey); + writeUTF(out, identity); + writeTime(out, time); + out.writeUTF(source.toString()); + writeUTF(out, addons); + } + + @Override + public void readEvent(DataInput in) throws IOException { + id = in.readUTF(); + instanceId = in.readUTF(); + version = in.readUTF(); + state = in.readUTF(); + type = readUTF(in); + parentInstanceId = readUTF(in); + rootId = readUTF(in); + rootInstanceId = readUTF(in); + businessKey = readUTF(in); + identity = readUTF(in); + time = readTime(in); + source = URI.create(in.readUTF()); + addons = readUTF(in); + } + + @Override + public String toString() { + return "ProcessInstanceDataEventExtensionRecord [id=" + id + ", instanceId=" + instanceId + ", version=" + + version + ", state=" + state + ", type=" + type + ", parentInstanceId=" + parentInstanceId + + ", rootId=" + rootId + ", rootInstanceId=" + rootInstanceId + ", businessKey=" + businessKey + + ", identity=" + identity + ", source=" + source + ", time=" + time + ", addons=" + addons + "]"; + } + +} diff --git a/api/kogito-events-core/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/api/kogito-events-core/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 00000000000..85fd599eb48 --- /dev/null +++ b/api/kogito-events-core/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +org.kie.kogito.event.serializer.KogitoSerializationModule \ No newline at end of file diff --git a/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java b/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java index 0d4fb4fafa9..d278e2c4279 100644 --- a/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java +++ b/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java @@ -18,31 +18,41 @@ */ package org.kie.kogito.event.process; +import java.io.IOException; import java.net.URI; import java.time.OffsetDateTime; import java.util.Arrays; +import java.util.Iterator; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.kie.kogito.event.AbstractDataEvent; +import org.kie.kogito.event.DataEventFactory; import org.kie.kogito.event.cloudevents.CloudEventExtensionConstants; +import org.kie.kogito.event.serializer.MultipleProcessDataInstanceConverterFactory; import org.kie.kogito.event.usertask.UserTaskInstanceStateDataEvent; +import org.kie.kogito.jackson.utils.JsonObjectUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.cloudevents.CloudEvent; import io.cloudevents.SpecVersion; import io.cloudevents.jackson.JsonFormat; import static org.assertj.core.api.Assertions.assertThat; +import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.toDate; class ProcessEventsTest { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .registerModule(new JavaTimeModule()) .registerModule(JsonFormat.getCloudEventJacksonModule()) - .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .findAndRegisterModules(); + + private static final Logger logger = LoggerFactory.getLogger(ProcessEventsTest.class); private static final Set BASE_EXTENSION_NAMES = Arrays.stream(new String[] { CloudEventExtensionConstants.PROCESS_INSTANCE_ID, @@ -53,10 +63,9 @@ class ProcessEventsTest { CloudEventExtensionConstants.PROCESS_INSTANCE_VERSION, CloudEventExtensionConstants.PROCESS_PARENT_PROCESS_INSTANCE_ID, CloudEventExtensionConstants.PROCESS_INSTANCE_STATE, - CloudEventExtensionConstants.PROCESS_REFERENCE_ID, - CloudEventExtensionConstants.PROCESS_START_FROM_NODE, CloudEventExtensionConstants.BUSINESS_KEY, - CloudEventExtensionConstants.PROCESS_TYPE }).collect(Collectors.toSet()); + CloudEventExtensionConstants.PROCESS_TYPE, + CloudEventExtensionConstants.IDENTITY }).collect(Collectors.toSet()); private static final String PROCESS_INSTANCE_EVENT_TYPE = "ProcessInstanceEvent"; private static final String USER_TASK_INSTANCE_EVENT_TYPE = "UserTaskInstanceEvent"; @@ -77,16 +86,22 @@ class ProcessEventsTest { private static final String ROOT_PROCESS_ID = "ROOT_PROCESS_ID"; private static final String PROCESS_PARENT_PROCESS_INSTANCE_ID = "PROCESS_PARENT_PROCESS_INSTANCE_ID"; private static final String PROCESS_INSTANCE_STATE = "PROCESS_INSTANCE_STATE"; - private static final String PROCESS_REFERENCE_ID = "PROCESS_REFERENCE_ID"; - private static final String PROCESS_START_FROM_NODE = "PROCESS_START_FROM_NODE"; private static final String BUSINESS_KEY = "BUSINESS_KEY"; private static final String PROCESS_TYPE = "PROCESS_TYPE"; private static final String ADDONS = "ADDONS"; - + private static final int PROCESS_STATE = 1; + private static final String NODE_CONTAINER_ID = "323"; + private static final String NODE_CONTAINER_INSTANCEID = "323-3232-3232"; private static final String EXTENSION_1 = "EXTENSION_1"; private static final String EXTENSION_1_VALUE = "EXTENSION_1_VALUE"; private static final String EXTENSION_2 = "EXTENSION_2"; private static final String EXTENSION_2_VALUE = "EXTENSION_2_VALUE"; + private static final String ERROR_MESSAGE = "AAAAAAHHHHH!!!!!"; + + private static final int EVENT_TYPE = 1; + + private static final String NODE_NAME = "NODE_NAME"; + private static final String NODE_TYPE = "NODE_TYPE"; private static final String VARIABLE_NAME = "VARIABLE_NAME"; @@ -112,6 +127,178 @@ void processInstanceDataEvent() throws Exception { assertExtensionNames(deserializedEvent, BASE_EXTENSION_NAMES, EXTENSION_1, EXTENSION_2); } + @Test + void multipleInstanceDataEvent() throws IOException { + JsonNode expectedVarValue = OBJECT_MAPPER.createObjectNode().put("name", "John Doe"); + int standard = processMultipleInstanceDataEvent(expectedVarValue, false, false); + int binary = processMultipleInstanceDataEvent(expectedVarValue, true, false); + int binaryCompressed = processMultipleInstanceDataEvent(expectedVarValue, true, true); + assertThat(standard).isGreaterThan(binary); + assertThat(binary).isGreaterThan(binaryCompressed); + } + + private int processMultipleInstanceDataEvent(JsonNode expectedVarValue, boolean binary, boolean compress) throws IOException { + ProcessInstanceStateDataEvent stateEvent = new ProcessInstanceStateDataEvent(); + setBaseEventValues(stateEvent, ProcessInstanceStateDataEvent.STATE_TYPE); + stateEvent.setData(ProcessInstanceStateEventBody.create().eventDate(toDate(TIME)).eventType(EVENT_TYPE).eventUser(SUBJECT) + .businessKey(BUSINESS_KEY).processId(PROCESS_ID).processInstanceId(PROCESS_INSTANCE_ID).state(PROCESS_STATE) + .processVersion(PROCESS_INSTANCE_VERSION).parentInstanceId(PROCESS_PARENT_PROCESS_INSTANCE_ID).processName(PROCESS_ID) + .processType(PROCESS_TYPE).rootProcessId(ROOT_PROCESS_ID).rootProcessInstanceId(ROOT_PROCESS_INSTANCE_ID).build()); + + ProcessInstanceVariableDataEvent varEvent = new ProcessInstanceVariableDataEvent(); + setBaseEventValues(varEvent, ProcessInstanceVariableDataEvent.VAR_TYPE); + varEvent.addExtensionAttribute(CloudEventExtensionConstants.KOGITO_VARIABLE_NAME, VARIABLE_NAME); + varEvent.setData(ProcessInstanceVariableEventBody.create().eventDate(toDate(TIME)).eventUser(SUBJECT) + .processId(PROCESS_ID).processInstanceId(PROCESS_INSTANCE_ID).processVersion(PROCESS_INSTANCE_VERSION) + .nodeContainerDefinitionId(NODE_CONTAINER_ID).nodeContainerInstanceId(NODE_CONTAINER_INSTANCEID) + .variableName(VARIABLE_NAME) + .variableId(VARIABLE_NAME) + .variableValue(expectedVarValue) + .build()); + + ProcessInstanceErrorDataEvent errorEvent = new ProcessInstanceErrorDataEvent(); + setBaseEventValues(errorEvent, ProcessInstanceErrorDataEvent.ERROR_TYPE); + errorEvent.setData(ProcessInstanceErrorEventBody.create().errorMessage(ERROR_MESSAGE).eventDate(toDate(TIME)).eventUser(SUBJECT) + .processId(PROCESS_ID).processInstanceId(PROCESS_INSTANCE_ID).processVersion(PROCESS_INSTANCE_VERSION).nodeDefinitionId(NODE_CONTAINER_ID) + .nodeInstanceId(NODE_CONTAINER_INSTANCEID).build()); + + ProcessInstanceNodeDataEvent nodeEvent = new ProcessInstanceNodeDataEvent(); + setBaseEventValues(nodeEvent, ProcessInstanceNodeDataEvent.NODE_TYPE); + nodeEvent + .setData(ProcessInstanceNodeEventBody.create().processId(PROCESS_ID).processInstanceId(PROCESS_INSTANCE_ID).processVersion(PROCESS_INSTANCE_VERSION).nodeDefinitionId(NODE_CONTAINER_ID) + .nodeInstanceId(NODE_CONTAINER_INSTANCEID).eventDate(toDate(TIME)).eventUser(SUBJECT).connectionNodeDefinitionId(NODE_CONTAINER_ID).workItemId(NODE_CONTAINER_ID) + .nodeType(NODE_TYPE).nodeName(NODE_NAME) + .eventType(EVENT_TYPE).slaDueDate(toDate(TIME)).build()); + + ProcessInstanceSLADataEvent slaEvent = new ProcessInstanceSLADataEvent(); + setBaseEventValues(slaEvent, ProcessInstanceSLADataEvent.SLA_TYPE); + slaEvent + .setData(ProcessInstanceSLAEventBody.create().processId(PROCESS_ID).processInstanceId(PROCESS_INSTANCE_ID).processVersion(PROCESS_INSTANCE_VERSION).nodeDefinitionId(NODE_CONTAINER_ID) + .nodeInstanceId(NODE_CONTAINER_INSTANCEID).eventDate(toDate(TIME)).eventUser(SUBJECT) + .nodeType(NODE_TYPE).nodeName(NODE_NAME).slaDueDate(toDate(TIME)).build()); + + MultipleProcessInstanceDataEvent event = new MultipleProcessInstanceDataEvent(SOURCE, Arrays.asList(stateEvent, varEvent, errorEvent, nodeEvent, slaEvent)); + if (binary) { + event.setDataContentType(MultipleProcessInstanceDataEvent.BINARY_CONTENT_TYPE); + } + if (compress) { + event.setCompressed(compress); + } + + byte[] json = OBJECT_MAPPER.writeValueAsBytes(event); + logger.info("Serialized chunk size is {}", json.length); + + // cloud event structured mode check + MultipleProcessInstanceDataEvent deserializedEvent = OBJECT_MAPPER.readValue(json, MultipleProcessInstanceDataEvent.class); + assertThat(deserializedEvent.getData()).hasSize(event.getData().size()); + assertMultipleIntance(deserializedEvent, expectedVarValue); + + // cloud event binary mode check + CloudEvent cloudEvent = OBJECT_MAPPER.readValue(json, CloudEvent.class); + deserializedEvent = DataEventFactory.from(new MultipleProcessInstanceDataEvent(), cloudEvent, MultipleProcessDataInstanceConverterFactory.fromCloudEvent(cloudEvent, OBJECT_MAPPER)); + assertThat(deserializedEvent.getData()).hasSize(event.getData().size()); + assertMultipleIntance(deserializedEvent, expectedVarValue); + return json.length; + } + + private void assertMultipleIntance(MultipleProcessInstanceDataEvent deserializedEvent, JsonNode expectedVarValue) { + + Iterator> iter = deserializedEvent.getData().iterator(); + ProcessInstanceStateDataEvent deserializedStateEvent = (ProcessInstanceStateDataEvent) iter.next(); + assertBaseEventValues(deserializedStateEvent, ProcessInstanceStateDataEvent.STATE_TYPE); + assertExtensionNames(deserializedStateEvent, BASE_EXTENSION_NAMES); + assertStateBody(deserializedStateEvent.getData()); + + ProcessInstanceVariableDataEvent deserializedVariableEvent = (ProcessInstanceVariableDataEvent) iter.next(); + assertBaseEventValues(deserializedVariableEvent, ProcessInstanceVariableDataEvent.VAR_TYPE); + assertExtensionNames(deserializedVariableEvent, BASE_EXTENSION_NAMES, CloudEventExtensionConstants.KOGITO_VARIABLE_NAME); + assertThat(deserializedVariableEvent.getExtension(CloudEventExtensionConstants.KOGITO_VARIABLE_NAME)).isEqualTo(VARIABLE_NAME); + assertVarBody(deserializedVariableEvent.getData(), expectedVarValue); + + ProcessInstanceErrorDataEvent deserializedErrorEvent = (ProcessInstanceErrorDataEvent) iter.next(); + assertBaseEventValues(deserializedErrorEvent, ProcessInstanceErrorDataEvent.ERROR_TYPE); + assertExtensionNames(deserializedErrorEvent, BASE_EXTENSION_NAMES); + assertErrorBody(deserializedErrorEvent.getData()); + + ProcessInstanceNodeDataEvent deserializedNodeEvent = (ProcessInstanceNodeDataEvent) iter.next(); + assertBaseEventValues(deserializedNodeEvent, ProcessInstanceNodeDataEvent.NODE_TYPE); + assertExtensionNames(deserializedNodeEvent, BASE_EXTENSION_NAMES); + assertNodeBody(deserializedNodeEvent.getData()); + + ProcessInstanceSLADataEvent deserializedSLAEvent = (ProcessInstanceSLADataEvent) iter.next(); + assertBaseEventValues(deserializedSLAEvent, ProcessInstanceSLADataEvent.SLA_TYPE); + assertExtensionNames(deserializedSLAEvent, BASE_EXTENSION_NAMES); + assertSLABody(deserializedSLAEvent.getData()); + } + + private void assertSLABody(ProcessInstanceSLAEventBody data) { + assertThat(data.getNodeDefinitionId()).isEqualTo(NODE_CONTAINER_ID); + assertThat(data.getNodeInstanceId()).isEqualTo(NODE_CONTAINER_INSTANCEID); + assertThat(data.getProcessId()).isEqualTo(PROCESS_ID); + assertThat(data.getProcessInstanceId()).isEqualTo(PROCESS_INSTANCE_ID); + assertThat(data.getProcessVersion()).isEqualTo(PROCESS_INSTANCE_VERSION); + assertThat(data.getEventUser()).isEqualTo(SUBJECT); + assertThat(data.getEventDate()).isEqualTo(toDate(TIME)); + assertThat(data.getSlaDueDate()).isEqualTo(toDate(TIME)); + assertThat(data.getNodeName()).isEqualTo(NODE_NAME); + assertThat(data.getNodeType()).isEqualTo(NODE_TYPE); + } + + private void assertNodeBody(ProcessInstanceNodeEventBody data) { + assertThat(data.getNodeDefinitionId()).isEqualTo(NODE_CONTAINER_ID); + assertThat(data.getNodeInstanceId()).isEqualTo(NODE_CONTAINER_INSTANCEID); + assertThat(data.getProcessId()).isEqualTo(PROCESS_ID); + assertThat(data.getProcessInstanceId()).isEqualTo(PROCESS_INSTANCE_ID); + assertThat(data.getProcessVersion()).isEqualTo(PROCESS_INSTANCE_VERSION); + assertThat(data.getEventUser()).isEqualTo(SUBJECT); + assertThat(data.getEventDate()).isEqualTo(toDate(TIME)); + assertThat(data.getEventType()).isEqualTo(EVENT_TYPE); + assertThat(data.getConnectionNodeDefinitionId()).isEqualTo(NODE_CONTAINER_ID); + assertThat(data.getWorkItemId()).isEqualTo(NODE_CONTAINER_ID); + assertThat(data.getSlaDueDate()).isEqualTo(toDate(TIME)); + assertThat(data.getNodeName()).isEqualTo(NODE_NAME); + assertThat(data.getNodeType()).isEqualTo(NODE_TYPE); + } + + private void assertErrorBody(ProcessInstanceErrorEventBody data) { + assertThat(data.getNodeDefinitionId()).isEqualTo(NODE_CONTAINER_ID); + assertThat(data.getNodeInstanceId()).isEqualTo(NODE_CONTAINER_INSTANCEID); + assertThat(data.getErrorMessage()).isEqualTo(ERROR_MESSAGE); + assertThat(data.getProcessId()).isEqualTo(PROCESS_ID); + assertThat(data.getProcessInstanceId()).isEqualTo(PROCESS_INSTANCE_ID); + assertThat(data.getProcessVersion()).isEqualTo(PROCESS_INSTANCE_VERSION); + assertThat(data.getEventUser()).isEqualTo(SUBJECT); + assertThat(data.getEventDate()).isEqualTo(toDate(TIME)); + } + + private static void assertVarBody(ProcessInstanceVariableEventBody data, JsonNode expectedVarValue) { + assertThat(data.getVariableId()).isEqualTo(VARIABLE_NAME); + assertThat(data.getVariableName()).isEqualTo(VARIABLE_NAME); + assertThat(JsonObjectUtils.fromValue(data.getVariableValue())).isEqualTo(expectedVarValue); + assertThat(data.getNodeContainerDefinitionId()).isEqualTo(NODE_CONTAINER_ID); + assertThat(data.getNodeContainerInstanceId()).isEqualTo(NODE_CONTAINER_INSTANCEID); + assertThat(data.getProcessId()).isEqualTo(PROCESS_ID); + assertThat(data.getProcessInstanceId()).isEqualTo(PROCESS_INSTANCE_ID); + assertThat(data.getProcessVersion()).isEqualTo(PROCESS_INSTANCE_VERSION); + assertThat(data.getEventUser()).isEqualTo(SUBJECT); + assertThat(data.getEventDate()).isEqualTo(toDate(TIME)); + } + + private static void assertStateBody(ProcessInstanceStateEventBody data) { + assertThat(data.getBusinessKey()).isEqualTo(BUSINESS_KEY); + assertThat(data.getParentInstanceId()).isEqualTo(PROCESS_PARENT_PROCESS_INSTANCE_ID); + assertThat(data.getRootProcessId()).isEqualTo(ROOT_PROCESS_ID); + assertThat(data.getProcessType()).isEqualTo(PROCESS_TYPE); + assertThat(data.getState()).isEqualTo(PROCESS_STATE); + assertThat(data.getRootProcessInstanceId()).isEqualTo(ROOT_PROCESS_INSTANCE_ID); + assertThat(data.getEventType()).isEqualTo(EVENT_TYPE); + assertThat(data.getProcessId()).isEqualTo(PROCESS_ID); + assertThat(data.getProcessInstanceId()).isEqualTo(PROCESS_INSTANCE_ID); + assertThat(data.getProcessVersion()).isEqualTo(PROCESS_INSTANCE_VERSION); + assertThat(data.getEventUser()).isEqualTo(SUBJECT); + assertThat(data.getEventDate()).isEqualTo(toDate(TIME)); + } + @Test void userTaskInstanceDataEvent() throws Exception { UserTaskInstanceStateDataEvent event = new UserTaskInstanceStateDataEvent(); @@ -136,7 +323,6 @@ void userTaskInstanceDataEvent() throws Exception { assertExtensionNames(deserializedEvent, BASE_EXTENSION_NAMES, CloudEventExtensionConstants.PROCESS_USER_TASK_INSTANCE_ID, CloudEventExtensionConstants.PROCESS_USER_TASK_INSTANCE_STATE, EXTENSION_1, EXTENSION_2); - } @Test @@ -175,12 +361,11 @@ private static void setBaseEventValues(AbstractDataEvent event, String eventT event.setKogitoRootProcessInstanceId(ROOT_PROCESS_INSTANCE_ID); event.setKogitoRootProcessId(ROOT_PROCESS_ID); event.setKogitoParentProcessInstanceId(PROCESS_PARENT_PROCESS_INSTANCE_ID); - event.setKogitoReferenceId(PROCESS_REFERENCE_ID); event.setKogitoProcessInstanceState(PROCESS_INSTANCE_STATE); - event.setKogitoStartFromNode(PROCESS_START_FROM_NODE); event.setKogitoBusinessKey(BUSINESS_KEY); event.setKogitoProcessType(PROCESS_TYPE); event.setKogitoAddons(ADDONS); + event.setKogitoIdentity(SUBJECT); } private static void setAdditionalExtensions(AbstractDataEvent event) { @@ -197,25 +382,25 @@ private static void assertBaseEventValues(AbstractDataEvent deserializedEvent assertThat(deserializedEvent.getSubject()).isEqualTo(SUBJECT); assertThat(deserializedEvent.getDataContentType()).isEqualTo(DATA_CONTENT_TYPE); assertThat(deserializedEvent.getDataSchema()).isEqualTo(DATA_SCHEMA); - assertThat(deserializedEvent.getKogitoProcessInstanceId()).isEqualTo(PROCESS_INSTANCE_ID); assertThat(deserializedEvent.getKogitoProcessId()).isEqualTo(PROCESS_ID); assertThat(deserializedEvent.getKogitoRootProcessInstanceId()).isEqualTo(ROOT_PROCESS_INSTANCE_ID); assertThat(deserializedEvent.getKogitoRootProcessId()).isEqualTo(ROOT_PROCESS_ID); assertThat(deserializedEvent.getKogitoParentProcessInstanceId()).isEqualTo(PROCESS_PARENT_PROCESS_INSTANCE_ID); - assertThat(deserializedEvent.getKogitoReferenceId()).isEqualTo(PROCESS_REFERENCE_ID); assertThat(deserializedEvent.getKogitoProcessInstanceState()).isEqualTo(PROCESS_INSTANCE_STATE); - assertThat(deserializedEvent.getKogitoStartFromNode()).isEqualTo(PROCESS_START_FROM_NODE); assertThat(deserializedEvent.getKogitoBusinessKey()).isEqualTo(BUSINESS_KEY); assertThat(deserializedEvent.getKogitoProcessType()).isEqualTo(PROCESS_TYPE); + assertThat(deserializedEvent.getKogitoIdentity()).isEqualTo(SUBJECT); assertThat(deserializedEvent.getKogitoAddons()).isEqualTo(ADDONS); } private static void assertExtensionNames(AbstractDataEvent event, Set baseNames, String... names) { Set extensionNames = event.getExtensionNames(); assertThat(extensionNames).hasSize(baseNames.size() + names.length) - .containsAll(baseNames) - .contains(names); + .containsAll(baseNames); + if (names.length > 0) { + assertThat(extensionNames).contains(names); + } } private static void assertExtensionsNotDuplicated(String json, Set extensionNames) { diff --git a/kogito-codegen-modules/kogito-codegen-core/src/main/resources/class-templates/config/GlobalObjectMapperQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-core/src/main/resources/class-templates/config/GlobalObjectMapperQuarkusTemplate.java index 050097b1d13..c670e699f90 100644 --- a/kogito-codegen-modules/kogito-codegen-core/src/main/resources/class-templates/config/GlobalObjectMapperQuarkusTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-core/src/main/resources/class-templates/config/GlobalObjectMapperQuarkusTemplate.java @@ -43,6 +43,6 @@ public void customize(ObjectMapper mapper) { mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); } mapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true).withTimeZone(TimeZone.getDefault())); - mapper.registerModule(new JavaTimeModule()).registerModule(JsonFormat.getCloudEventJacksonModule()); + mapper.registerModule(JsonFormat.getCloudEventJacksonModule()).findAndRegisterModules(); } } \ No newline at end of file diff --git a/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java b/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java index 66e007d0394..ee30e0dc36d 100644 --- a/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java +++ b/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java @@ -26,7 +26,9 @@ import java.util.Map; import java.util.Map.Entry; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.kie.kogito.event.DataEvent; +import org.kie.kogito.event.process.KogitoMarshallEventSupport; import org.kie.kogito.event.process.MultipleProcessInstanceDataEvent; import org.kie.kogito.event.process.ProcessInstanceDataEvent; import org.kie.kogito.event.usertask.MultipleUserTaskInstanceDataEvent; @@ -45,6 +47,12 @@ public void publish(DataEvent event) { publish(Collections.singletonList(event)); } + @ConfigProperty(name = "kogito.events.grouping.binary", defaultValue = "false") + private boolean binary; + + @ConfigProperty(name = "kogito.events.grouping.compress", defaultValue = "false") + private boolean compress; + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void publish(Collection> events) { @@ -62,7 +70,12 @@ private void publishEvents(Map.Entry entry) if (firstEvent instanceof UserTaskInstanceDataEvent) { publishToTopic(entry.getKey(), new MultipleUserTaskInstanceDataEvent(source, (Collection>) entry.getValue())); } else if (firstEvent instanceof ProcessInstanceDataEvent) { - publishToTopic(entry.getKey(), new MultipleProcessInstanceDataEvent(source, (Collection>) entry.getValue())); + MultipleProcessInstanceDataEvent sent = new MultipleProcessInstanceDataEvent(source, (Collection>) entry.getValue()); + if (binary) { + sent.setDataContentType(MultipleProcessInstanceDataEvent.BINARY_CONTENT_TYPE); + sent.setCompressed(compress); + } + publishToTopic(entry.getKey(), sent); } else { for (DataEvent event : (Collection>) entry.getValue()) { publishToTopic(entry.getKey(), event); From 25adf7d0d2f042e3baafb7d25fc52514a2ed6a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pere=20Fern=C3=A1ndez?= Date: Fri, 25 Oct 2024 20:31:11 +0200 Subject: [PATCH 04/16] [NO_ISSUE] fix constraints names (#3753) * [NO_ISSUE] fix constraints names --- .../org/jbpm/usertask/jpa/model/AttachmentEntity.java | 2 +- .../java/org/jbpm/usertask/jpa/model/CommentEntity.java | 2 +- .../db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql | 8 ++++---- .../postgresql/V1.0.0__jBPM_user_task_create.sql | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java index a0bff107682..57b6fb8f024 100644 --- a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java @@ -42,7 +42,7 @@ public class AttachmentEntity { private Date updatedAt; @ManyToOne - @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_user_task_attachment_tid")) + @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_user_tasks_attachments_tid")) private UserTaskInstanceEntity taskInstance; public String getId() { diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java index cf70f4e7bca..83132a3c35a 100644 --- a/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java @@ -40,7 +40,7 @@ public class CommentEntity { private Date updatedAt; @ManyToOne(optional = false) - @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_user_task_comment_tid")) + @JoinColumn(name = "task_id", foreignKey = @ForeignKey(name = "fk_user_tasks_comments_tid")) private UserTaskInstanceEntity taskInstance; public String getId() { diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql index 058f3599d3b..ba5b12e5fb3 100644 --- a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/h2/V1.0.0__jBPM_user_task_create.sql @@ -134,16 +134,16 @@ alter table if exists jbpm_user_tasks_excluded_users add constraint fk_jbpm_user_tasks_excluded_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; alter table if exists jbpm_user_tasks_attachments -drop constraint if exists fk_user_task_attachment_tid cascade; +drop constraint if exists fk_user_tasks_attachments_tid cascade; alter table if exists jbpm_user_tasks_attachments -add constraint fk_user_task_attachment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; +add constraint fk_user_tasks_attachments_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; alter table if exists jbpm_user_tasks_comments -drop constraint if exists fk_user_task_comment_tid cascade; +drop constraint if exists fk_user_tasks_comments_tid cascade; alter table if exists jbpm_user_tasks_comments -add constraint fk_user_task_comment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; +add constraint fk_user_tasks_comments_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; alter table if exists jbpm_user_tasks_inputs drop constraint if exists fk_jbpm_user_tasks_inputs_tid cascade; diff --git a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql index 8daa0437216..acc1ca28b0d 100644 --- a/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql +++ b/addons/common/jbpm-usertask-storage-jpa/src/main/resources/kie-flyway/db/user-tasks/postgresql/V1.0.0__jBPM_user_task_create.sql @@ -134,16 +134,16 @@ alter table if exists jbpm_user_tasks_excluded_users add constraint fk_jbpm_user_tasks_excluded_users_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; alter table if exists jbpm_user_tasks_attachments -drop constraint if exists fk_user_task_attachment_tid cascade; +drop constraint if exists fk_user_tasks_attachments_tid cascade; alter table if exists jbpm_user_tasks_attachments -add constraint fk_user_task_attachment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; +add constraint fk_user_tasks_attachments_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; alter table if exists jbpm_user_tasks_comments -drop constraint if exists fk_user_task_comment_tid cascade; +drop constraint if exists fk_user_tasks_comments_tid cascade; alter table if exists jbpm_user_tasks_comments -add constraint fk_user_task_comment_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; +add constraint fk_user_tasks_comments_tid foreign key (task_id) references jbpm_user_tasks(id) on delete cascade; alter table if exists jbpm_user_tasks_inputs drop constraint if exists fk_jbpm_user_tasks_inputs_tid cascade; From 5d6874373c396038e1243239f5b475d6fbae8caf Mon Sep 17 00:00:00 2001 From: Abhiram Gundala <164050036+Abhitocode@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:19:51 -0400 Subject: [PATCH 05/16] [incubator-kie-issues#1554] Fix Business Calendar Usage (#3686) * business-calendar * business-calendar * business-calendar * business-calendar * business-calendar * business-calendar * [gitgabrio_business-calendar] Minor refactoring AbstractProcessRuntime and some tests * [gitgabrio_business-calendar] Minor refactoring AbstractProcessRuntime and some tests * business-calendar * business-calendar * business-calendar --------- Co-authored-by: Gabriele-Cardosi --- .../kogito/calendar}/BusinessCalendar.java | 2 +- .../org/kie/kogito/process/ProcessConfig.java | 3 + .../core/constants/CalendarConstants.java | 24 +++ .../core/timer/BusinessCalendarImpl.java | 50 +++-- .../instance/AbstractProcessRuntime.java | 90 +++++++++ .../instance/DummyKnowledgeRuntime.java | 9 + .../process/instance/LightProcessRuntime.java | 80 +------- .../process/instance/ProcessRuntimeImpl.java | 80 +------- .../impl/WorkflowProcessInstanceImpl.java | 7 +- .../instance/node/StateBasedNodeInstance.java | 9 +- .../process/impl/AbstractProcessConfig.java | 11 +- .../process/impl/StaticProcessConfig.java | 16 +- .../core/timer/BusinessCalendarImplTest.java | 12 +- .../instance/LightProcessRuntimeTest.java | 6 + .../instance/RuleFlowProcessInstanceTest.java | 3 +- .../impl/AbstractProcessConfigTest.java | 2 +- .../BPMN2-BusinessCalendarTimer.bpmn2 | 172 ++++++++++++++++++ .../org/jbpm/bpmn2/ProcessFactoryTest.java | 6 + .../bpmn2/calendar/BusinessCalendarTest.java | 115 ++++++++++++ .../src/test/resources/calendar.properties | 6 + .../codegen/process/ProcessCodegen.java | 16 +- ...cDependencyInjectionProducerGenerator.java | 12 +- .../config/ProcessConfigQuarkusTemplate.java | 11 +- .../config/ProcessConfigSpringTemplate.java | 7 +- ...sinessCalendarProducerQuarkusTemplate.java | 36 ++++ ...usinessCalendarProducerSpringTemplate.java | 38 ++++ .../codegen/process/ProcessCodegenTest.java | 43 ++++- ...sinessCalendarProducerQuarkusTemplate.java | 36 ++++ ...usinessCalendarProducerSpringTemplate.java | 37 ++++ 29 files changed, 715 insertions(+), 224 deletions(-) rename {jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer => api/kogito-api/src/main/java/org/kie/kogito/calendar}/BusinessCalendar.java (97%) mode change 100755 => 100644 create mode 100644 jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/constants/CalendarConstants.java create mode 100644 jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/calendar/BPMN2-BusinessCalendarTimer.bpmn2 create mode 100644 jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTest.java create mode 100644 jbpm/jbpm-tests/src/test/resources/calendar.properties create mode 100644 kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java create mode 100644 kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java create mode 100644 kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java create mode 100644 kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendar.java b/api/kogito-api/src/main/java/org/kie/kogito/calendar/BusinessCalendar.java old mode 100755 new mode 100644 similarity index 97% rename from jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendar.java rename to api/kogito-api/src/main/java/org/kie/kogito/calendar/BusinessCalendar.java index 544fbcb6ef3..6e15a6cc0e6 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendar.java +++ b/api/kogito-api/src/main/java/org/kie/kogito/calendar/BusinessCalendar.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.jbpm.process.core.timer; +package org.kie.kogito.calendar; import java.util.Date; diff --git a/api/kogito-api/src/main/java/org/kie/kogito/process/ProcessConfig.java b/api/kogito-api/src/main/java/org/kie/kogito/process/ProcessConfig.java index 40549bbcd90..b365c3d7647 100644 --- a/api/kogito-api/src/main/java/org/kie/kogito/process/ProcessConfig.java +++ b/api/kogito-api/src/main/java/org/kie/kogito/process/ProcessConfig.java @@ -20,6 +20,7 @@ import org.kie.kogito.KogitoConfig; import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.jobs.JobsService; import org.kie.kogito.signal.SignalManagerHub; import org.kie.kogito.uow.UnitOfWorkManager; @@ -38,4 +39,6 @@ public interface ProcessConfig extends KogitoConfig { ProcessVersionResolver versionResolver(); IdentityProvider identityProvider(); + + BusinessCalendar getBusinessCalendar(); } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/constants/CalendarConstants.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/constants/CalendarConstants.java new file mode 100644 index 00000000000..036902eabb9 --- /dev/null +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/constants/CalendarConstants.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jbpm.process.core.constants; + +public class CalendarConstants { + public static final String BUSINESS_CALENDAR_PATH = "calendar.properties"; + public static final String BUSINESS_CALENDAR_ENVIRONMENT_KEY = "jbpm.business.calendar"; +} diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java index 29017a62307..fbb650e3c35 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.OffsetDateTime; @@ -30,15 +31,19 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.List; +import java.util.Objects; import java.util.Properties; import java.util.TimeZone; import java.util.regex.Matcher; import org.jbpm.util.PatternConstants; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.timer.SessionClock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_PATH; + /** * Default implementation of BusinessCalendar interface that is configured with properties. * Following are supported properties: @@ -103,46 +108,35 @@ public class BusinessCalendarImpl implements BusinessCalendar { public static final String WEEKEND_DAYS = "business.weekend.days"; public static final String TIMEZONE = "business.cal.timezone"; - private static final String DEFAULT_PROPERTIES_NAME = "/jbpm.business.calendar.properties"; - public BusinessCalendarImpl() { - String propertiesLocation = System.getProperty("jbpm.business.calendar.properties"); - - if (propertiesLocation == null) { - propertiesLocation = DEFAULT_PROPERTIES_NAME; - } - businessCalendarConfiguration = new Properties(); - - InputStream in = this.getClass().getResourceAsStream(propertiesLocation); - if (in != null) { - - try { - businessCalendarConfiguration.load(in); - } catch (IOException e) { - logger.error("Error while loading properties for business calendar", e); - - } - } - init(); - + this(null); } public BusinessCalendarImpl(Properties configuration) { - this.businessCalendarConfiguration = configuration; - init(); + this(configuration, null); } public BusinessCalendarImpl(Properties configuration, SessionClock clock) { - this.businessCalendarConfiguration = configuration; this.clock = clock; + if (configuration == null) { + businessCalendarConfiguration = new Properties(); + URL resource = Thread.currentThread().getContextClassLoader().getResource(BUSINESS_CALENDAR_PATH); + if (Objects.nonNull(resource)) { + try (InputStream is = resource.openStream()) { + businessCalendarConfiguration.load(is); + } catch (IOException e) { + logger.error("Error while loading properties for business calendar", e); + throw new RuntimeException("Error while loading properties for business calendar", e); + } + } + + } else { + this.businessCalendarConfiguration = configuration; + } init(); } protected void init() { - if (this.businessCalendarConfiguration == null) { - throw new IllegalArgumentException("BusinessCalendar configuration was not provided."); - } - daysPerWeek = getPropertyAsInt(DAYS_PER_WEEK, "5"); hoursInDay = getPropertyAsInt(HOURS_PER_DAY, "8"); startHour = getPropertyAsInt(START_HOUR, "9"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/AbstractProcessRuntime.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/AbstractProcessRuntime.java index 4ef63f4376e..cb388bb3610 100644 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/AbstractProcessRuntime.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/AbstractProcessRuntime.java @@ -18,22 +18,43 @@ */ package org.jbpm.process.instance; +import java.util.Collection; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import org.drools.base.time.TimeUtils; +import org.drools.core.common.InternalKnowledgeRuntime; +import org.jbpm.process.core.timer.DateTimeUtils; +import org.jbpm.process.core.timer.Timer; import org.jbpm.process.instance.event.KogitoProcessEventListenerAdapter; import org.jbpm.process.instance.event.KogitoProcessEventSupportImpl; +import org.jbpm.ruleflow.core.RuleFlowProcess; +import org.jbpm.workflow.core.node.StartNode; +import org.kie.api.definition.process.Process; import org.kie.api.event.process.ProcessEventListener; import org.kie.kogito.Application; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.internal.process.event.KogitoProcessEventListener; import org.kie.kogito.internal.process.event.KogitoProcessEventSupport; import org.kie.kogito.internal.process.runtime.KogitoProcessRuntime; +import org.kie.kogito.jobs.DurationExpirationTime; +import org.kie.kogito.jobs.ExactExpirationTime; +import org.kie.kogito.jobs.ExpirationTime; +import org.kie.kogito.jobs.JobsService; +import org.kie.kogito.jobs.ProcessJobDescription; +import org.kie.kogito.signal.SignalManager; + +import static org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_ENVIRONMENT_KEY; public abstract class AbstractProcessRuntime implements InternalProcessRuntime { protected KogitoProcessEventSupport processEventSupport; protected KogitoProcessRuntimeImpl kogitoProcessRuntime = new KogitoProcessRuntimeImpl(this); + + protected SignalManager signalManager; + protected JobsService jobService; + private final Application application; private final Map listenersMap = new IdentityHashMap<>(); @@ -72,6 +93,20 @@ public List getProcessEventListeners() { return (List) (Object) ((KogitoProcessEventSupportImpl) this.processEventSupport).getEventListeners(); } + protected void initStartTimers(Collection processes, InternalKnowledgeRuntime kruntime) { + for (Process process : processes) { + RuleFlowProcess p = (RuleFlowProcess) process; + List startNodes = p.getTimerStart(); + if (startNodes != null && !startNodes.isEmpty()) { + for (StartNode startNode : startNodes) { + if (startNode != null && startNode.getTimer() != null) { + jobService.scheduleProcessJob(ProcessJobDescription.of(createTimerInstance(startNode.getTimer(), kruntime), p.getId())); + } + } + } + } + } + private KogitoProcessEventListener asKogitoProcessEventListener(ProcessEventListener processEventListener) { if (processEventListener instanceof KogitoProcessEventListener) { return ((KogitoProcessEventListener) processEventListener); @@ -85,4 +120,59 @@ private KogitoProcessEventListener removeKogitoProcessEventListener(ProcessEvent } return listenersMap.remove(processEventListener); } + + protected ExpirationTime createTimerInstance(Timer timer, InternalKnowledgeRuntime kruntime) { + + if (kruntime != null && kruntime.getEnvironment().get(BUSINESS_CALENDAR_ENVIRONMENT_KEY) != null) { + BusinessCalendar businessCalendar = (BusinessCalendar) kruntime.getEnvironment().get(BUSINESS_CALENDAR_ENVIRONMENT_KEY); + + long delay = businessCalendar.calculateBusinessTimeAsDuration(timer.getDelay()); + + if (timer.getPeriod() == null) { + return DurationExpirationTime.repeat(delay); + } else { + long period = businessCalendar.calculateBusinessTimeAsDuration(timer.getPeriod()); + + return DurationExpirationTime.repeat(delay, period); + } + } else { + return configureTimerInstance(timer); + } + } + + private ExpirationTime configureTimerInstance(Timer timer) { + long duration = -1; + switch (timer.getTimeType()) { + case Timer.TIME_CYCLE: + // when using ISO date/time period is not set + long[] repeatValues = DateTimeUtils.parseRepeatableDateTime(timer.getDelay()); + if (repeatValues.length == 3) { + return DurationExpirationTime.repeat(repeatValues[1], repeatValues[2]); + } else { + long delay = repeatValues[0]; + long period = -1; + try { + period = TimeUtils.parseTimeString(timer.getPeriod()); + + } catch (RuntimeException e) { + period = repeatValues[0]; + } + + return DurationExpirationTime.repeat(delay, period); + } + + case Timer.TIME_DURATION: + + duration = DateTimeUtils.parseDuration(timer.getDelay()); + return DurationExpirationTime.repeat(duration); + + case Timer.TIME_DATE: + + return ExactExpirationTime.of(timer.getDate()); + + default: + throw new UnsupportedOperationException("Not supported timer definition"); + } + + } } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/DummyKnowledgeRuntime.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/DummyKnowledgeRuntime.java index 2f7da3c086c..1ae292bbd33 100644 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/DummyKnowledgeRuntime.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/DummyKnowledgeRuntime.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.Objects; import org.drools.core.common.EndOperationListener; import org.drools.core.common.InternalAgenda; @@ -53,11 +54,15 @@ import org.kie.api.runtime.rule.ViewChangedEventListener; import org.kie.api.time.SessionClock; import org.kie.kogito.Application; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.internal.process.event.KogitoProcessEventSupport; import org.kie.kogito.internal.process.runtime.KogitoProcessInstance; import org.kie.kogito.internal.process.runtime.KogitoProcessRuntime; import org.kie.kogito.internal.process.workitem.KogitoWorkItemManager; import org.kie.kogito.jobs.JobsService; +import org.kie.kogito.process.ProcessConfig; + +import static org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_ENVIRONMENT_KEY; /** * A severely limited implementation of the WorkingMemory interface. @@ -72,6 +77,10 @@ class DummyKnowledgeRuntime implements InternalKnowledgeRuntime, KogitoProcessRu this.processRuntime = processRuntime; this.environment = new EnvironmentImpl(); // register codegen-based node instances factories + BusinessCalendar calendar = processRuntime.getApplication().config().get(ProcessConfig.class).getBusinessCalendar(); + if (Objects.nonNull(calendar)) { + environment.set(BUSINESS_CALENDAR_ENVIRONMENT_KEY, calendar); + } environment.set("NodeInstanceFactoryRegistry", new CodegenNodeInstanceFactoryRegistry()); } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/LightProcessRuntime.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/LightProcessRuntime.java index df1b7c5ba69..a972d72daf2 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/LightProcessRuntime.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/LightProcessRuntime.java @@ -24,16 +24,12 @@ import java.util.Map; import org.drools.base.definitions.rule.impl.RuleImpl; -import org.drools.base.time.TimeUtils; import org.drools.core.common.InternalKnowledgeRuntime; import org.drools.core.common.ReteEvaluator; import org.drools.core.common.WorkingMemoryAction; import org.drools.core.phreak.PropagationEntry; import org.jbpm.process.core.event.EventFilter; import org.jbpm.process.core.event.EventTypeFilter; -import org.jbpm.process.core.timer.BusinessCalendar; -import org.jbpm.process.core.timer.DateTimeUtils; -import org.jbpm.process.core.timer.Timer; import org.jbpm.ruleflow.core.RuleFlowProcess; import org.jbpm.workflow.core.impl.DataAssociation; import org.jbpm.workflow.core.impl.NodeIoHelper; @@ -54,11 +50,7 @@ import org.kie.kogito.Application; import org.kie.kogito.internal.process.runtime.KogitoProcessInstance; import org.kie.kogito.internal.process.workitem.KogitoWorkItemManager; -import org.kie.kogito.jobs.DurationExpirationTime; -import org.kie.kogito.jobs.ExactExpirationTime; -import org.kie.kogito.jobs.ExpirationTime; import org.kie.kogito.jobs.JobsService; -import org.kie.kogito.jobs.ProcessJobDescription; import org.kie.kogito.process.Processes; import org.kie.kogito.services.jobs.impl.InMemoryJobService; import org.kie.kogito.signal.SignalManager; @@ -71,8 +63,6 @@ public class LightProcessRuntime extends AbstractProcessRuntime { private final InternalKnowledgeRuntime knowledgeRuntime; private ProcessInstanceManager processInstanceManager; - private SignalManager signalManager; - private JobsService jobService; private final KogitoWorkItemManager workItemManager; private UnitOfWorkManager unitOfWorkManager; @@ -102,20 +92,7 @@ protected LightProcessRuntime(ProcessRuntimeContext runtimeContext, ProcessRunti } public void initStartTimers() { - Collection processes = runtimeContext.getProcesses(); - for (Process process : processes) { - RuleFlowProcess p = (RuleFlowProcess) process; - List startNodes = p.getTimerStart(); - if (startNodes != null && !startNodes.isEmpty()) { - for (StartNode startNode : startNodes) { - if (startNode != null && startNode.getTimer() != null) { - - jobService.scheduleProcessJob(ProcessJobDescription.of(createTimerInstance(startNode.getTimer(), knowledgeRuntime), p.getId())); - - } - } - } - } + initStartTimers(runtimeContext.getProcesses(), knowledgeRuntime); } @Override @@ -424,61 +401,6 @@ public boolean isActive() { return runtimeContext.isActive(); } - protected ExpirationTime createTimerInstance(Timer timer, InternalKnowledgeRuntime kruntime) { - - if (kruntime != null && kruntime.getEnvironment().get("jbpm.business.calendar") != null) { - BusinessCalendar businessCalendar = (BusinessCalendar) kruntime.getEnvironment().get("jbpm.business.calendar"); - - long delay = businessCalendar.calculateBusinessTimeAsDuration(timer.getDelay()); - - if (timer.getPeriod() == null) { - return DurationExpirationTime.repeat(delay); - } else { - long period = businessCalendar.calculateBusinessTimeAsDuration(timer.getPeriod()); - - return DurationExpirationTime.repeat(delay, period); - } - } else { - return configureTimerInstance(timer); - } - } - - private ExpirationTime configureTimerInstance(Timer timer) { - long duration = -1; - switch (timer.getTimeType()) { - case Timer.TIME_CYCLE: - // when using ISO date/time period is not set - long[] repeatValues = DateTimeUtils.parseRepeatableDateTime(timer.getDelay()); - if (repeatValues.length == 3) { - return DurationExpirationTime.repeat(repeatValues[1], repeatValues[2]); - } else { - long delay = repeatValues[0]; - long period = -1; - try { - period = TimeUtils.parseTimeString(timer.getPeriod()); - - } catch (RuntimeException e) { - period = repeatValues[0]; - } - - return DurationExpirationTime.repeat(delay, period); - } - - case Timer.TIME_DURATION: - - duration = DateTimeUtils.parseDuration(timer.getDelay()); - return DurationExpirationTime.repeat(duration); - - case Timer.TIME_DATE: - - return ExactExpirationTime.of(timer.getDate()); - - default: - throw new UnsupportedOperationException("Not supported timer definition"); - } - - } - public class SignalManagerSignalAction extends PropagationEntry.AbstractPropagationEntry implements WorkingMemoryAction { private String type; diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/ProcessRuntimeImpl.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/ProcessRuntimeImpl.java index 3eaec2fce36..0cd50cde8d8 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/ProcessRuntimeImpl.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/ProcessRuntimeImpl.java @@ -24,7 +24,6 @@ import java.util.Map; import org.drools.base.definitions.rule.impl.RuleImpl; -import org.drools.base.time.TimeUtils; import org.drools.core.common.InternalKnowledgeRuntime; import org.drools.core.common.InternalWorkingMemory; import org.drools.core.common.ReteEvaluator; @@ -35,9 +34,6 @@ import org.drools.core.time.impl.ThreadSafeTrackableTimeJobFactoryManager; import org.jbpm.process.core.event.EventFilter; import org.jbpm.process.core.event.EventTypeFilter; -import org.jbpm.process.core.timer.BusinessCalendar; -import org.jbpm.process.core.timer.DateTimeUtils; -import org.jbpm.process.core.timer.Timer; import org.jbpm.process.instance.event.DefaultSignalManagerFactory; import org.jbpm.process.instance.event.KogitoProcessEventSupportImpl; import org.jbpm.process.instance.impl.DefaultProcessInstanceManagerFactory; @@ -47,7 +43,6 @@ import org.jbpm.workflow.core.node.EventTrigger; import org.jbpm.workflow.core.node.StartNode; import org.jbpm.workflow.core.node.Trigger; -import org.kie.api.KieBase; import org.kie.api.command.ExecutableCommand; import org.kie.api.definition.process.Node; import org.kie.api.definition.process.Process; @@ -66,11 +61,7 @@ import org.kie.kogito.Application; import org.kie.kogito.internal.process.runtime.KogitoProcessInstance; import org.kie.kogito.internal.process.runtime.KogitoProcessRuntime; -import org.kie.kogito.jobs.DurationExpirationTime; -import org.kie.kogito.jobs.ExactExpirationTime; -import org.kie.kogito.jobs.ExpirationTime; import org.kie.kogito.jobs.JobsService; -import org.kie.kogito.jobs.ProcessJobDescription; import org.kie.kogito.services.identity.NoOpIdentityProvider; import org.kie.kogito.services.jobs.impl.LegacyInMemoryJobService; import org.kie.kogito.services.uow.CollectingUnitOfWorkFactory; @@ -84,8 +75,6 @@ public class ProcessRuntimeImpl extends AbstractProcessRuntime { private InternalKnowledgeRuntime kruntime; private ProcessInstanceManager processInstanceManager; - private SignalManager signalManager; - private JobsService jobService; private UnitOfWorkManager unitOfWorkManager; public ProcessRuntimeImpl(Application application, InternalWorkingMemory workingMemory) { @@ -109,20 +98,7 @@ public ProcessRuntimeImpl(Application application, InternalWorkingMemory working } public void initStartTimers() { - KieBase kbase = kruntime.getKieBase(); - Collection processes = kbase.getProcesses(); - for (Process process : processes) { - RuleFlowProcess p = (RuleFlowProcess) process; - List startNodes = p.getTimerStart(); - if (startNodes != null && !startNodes.isEmpty()) { - - for (StartNode startNode : startNodes) { - if (startNode != null && startNode.getTimer() != null) { - jobService.scheduleProcessJob(ProcessJobDescription.of(createTimerInstance(startNode.getTimer(), kruntime), p.getId())); - } - } - } - } + initStartTimers(kruntime.getKieBase().getProcesses(), kruntime); } private void initProcessInstanceManager() { @@ -408,60 +384,6 @@ public boolean isActive() { return active.booleanValue(); } - protected ExpirationTime createTimerInstance(Timer timer, InternalKnowledgeRuntime kruntime) { - if (kruntime != null && kruntime.getEnvironment().get("jbpm.business.calendar") != null) { - BusinessCalendar businessCalendar = (BusinessCalendar) kruntime.getEnvironment().get("jbpm.business.calendar"); - - long delay = businessCalendar.calculateBusinessTimeAsDuration(timer.getDelay()); - - if (timer.getPeriod() == null) { - return DurationExpirationTime.repeat(delay); - } else { - long period = businessCalendar.calculateBusinessTimeAsDuration(timer.getPeriod()); - - return DurationExpirationTime.repeat(delay, period); - } - } else { - return configureTimerInstance(timer); - } - } - - private ExpirationTime configureTimerInstance(Timer timer) { - long duration = -1; - switch (timer.getTimeType()) { - case Timer.TIME_CYCLE: - // when using ISO date/time period is not set - long[] repeatValues = DateTimeUtils.parseRepeatableDateTime(timer.getDelay()); - if (repeatValues.length == 3) { - int parsedReapedCount = (int) repeatValues[0]; - - return DurationExpirationTime.repeat(repeatValues[1], repeatValues[2], parsedReapedCount); - } else { - long delay = repeatValues[0]; - long period = -1; - try { - period = TimeUtils.parseTimeString(timer.getPeriod()); - } catch (RuntimeException e) { - period = repeatValues[0]; - } - - return DurationExpirationTime.repeat(delay, period); - } - - case Timer.TIME_DURATION: - - duration = DateTimeUtils.parseDuration(timer.getDelay()); - return DurationExpirationTime.after(duration); - - case Timer.TIME_DATE: - - return ExactExpirationTime.of(timer.getDate()); - - default: - throw new UnsupportedOperationException("Not supported timer definition"); - } - } - @Override public InternalKnowledgeRuntime getInternalKieRuntime() { return this.kruntime; diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java index 93554930fa8..25a0cd0467f 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java @@ -44,7 +44,6 @@ import org.jbpm.process.core.ContextResolver; import org.jbpm.process.core.context.variable.Variable; import org.jbpm.process.core.context.variable.VariableScope; -import org.jbpm.process.core.timer.BusinessCalendar; import org.jbpm.process.core.timer.DateTimeUtils; import org.jbpm.process.core.timer.Timer; import org.jbpm.process.instance.ContextInstance; @@ -81,6 +80,7 @@ import org.kie.api.definition.process.WorkflowElementIdentifier; import org.kie.api.runtime.rule.AgendaFilter; import org.kie.internal.process.CorrelationKey; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.internal.process.event.KogitoEventListener; import org.kie.kogito.internal.process.runtime.KogitoNodeInstance; import org.kie.kogito.internal.process.runtime.KogitoNodeInstanceContainer; @@ -102,6 +102,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_ENVIRONMENT_KEY; import static org.jbpm.ruleflow.core.Metadata.COMPENSATION; import static org.jbpm.ruleflow.core.Metadata.CONDITION; import static org.jbpm.ruleflow.core.Metadata.CORRELATION_KEY; @@ -565,8 +566,8 @@ public TimerInstance configureSLATimer(String slaDueDateExpression) { logger.debug("SLA due date is set to {}", slaDueDateExpression); InternalKnowledgeRuntime kruntime = getKnowledgeRuntime(); long duration; - if (kruntime.getEnvironment().get("jbpm.business.calendar") != null) { - BusinessCalendar businessCalendar = (BusinessCalendar) kruntime.getEnvironment().get("jbpm.business.calendar"); + if (kruntime.getEnvironment().get(BUSINESS_CALENDAR_ENVIRONMENT_KEY) != null) { + BusinessCalendar businessCalendar = (BusinessCalendar) kruntime.getEnvironment().get(BUSINESS_CALENDAR_ENVIRONMENT_KEY); duration = businessCalendar.calculateBusinessTimeAsDuration(slaDueDateExpression); } else { duration = DateTimeUtils.parseDuration(slaDueDateExpression); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java index 554da5acc51..6b69126d81c 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java @@ -29,7 +29,6 @@ import org.drools.core.common.InternalAgenda; import org.drools.core.common.ReteEvaluator; import org.drools.core.rule.consequence.InternalMatch; -import org.jbpm.process.core.timer.BusinessCalendar; import org.jbpm.process.core.timer.DateTimeUtils; import org.jbpm.process.core.timer.Timer; import org.jbpm.process.instance.InternalProcessRuntime; @@ -44,6 +43,7 @@ import org.jbpm.workflow.instance.impl.WorkflowProcessInstanceImpl; import org.kie.api.event.rule.MatchCreatedEvent; import org.kie.api.runtime.KieRuntime; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.internal.process.event.KogitoEventListener; import org.kie.kogito.internal.process.runtime.KogitoNodeInstance; import org.kie.kogito.internal.process.runtime.KogitoProcessContext; @@ -60,6 +60,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_ENVIRONMENT_KEY; import static org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE; import static org.jbpm.workflow.instance.node.TimerNodeInstance.TIMER_TRIGGERED_EVENT; @@ -145,8 +146,8 @@ protected void configureSla() { protected ExpirationTime createTimerInstance(Timer timer) { KieRuntime kruntime = getProcessInstance().getKnowledgeRuntime(); - if (kruntime != null && kruntime.getEnvironment().get("jbpm.business.calendar") != null) { - BusinessCalendar businessCalendar = (BusinessCalendar) kruntime.getEnvironment().get("jbpm.business.calendar"); + if (kruntime != null && kruntime.getEnvironment().get(BUSINESS_CALENDAR_ENVIRONMENT_KEY) != null) { + BusinessCalendar businessCalendar = (BusinessCalendar) kruntime.getEnvironment().get(BUSINESS_CALENDAR_ENVIRONMENT_KEY); String delay = null; switch (timer.getTimeType()) { case Timer.TIME_CYCLE: @@ -183,7 +184,7 @@ protected ExpirationTime createTimerInstance(Timer timer) { case Timer.TIME_DURATION: delay = resolveTimerExpression(timer.getDelay()); - return DurationExpirationTime.repeat(businessCalendar.calculateBusinessTimeAsDuration(delay)); + return DurationExpirationTime.after(businessCalendar.calculateBusinessTimeAsDuration(delay)); case Timer.TIME_DATE: // even though calendar is available concrete date was provided so it shall be used return ExactExpirationTime.of(timer.getDate()); diff --git a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessConfig.java b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessConfig.java index 25d278f628c..ac13e659424 100644 --- a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessConfig.java +++ b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessConfig.java @@ -28,6 +28,7 @@ import org.kie.api.event.process.ProcessEventListener; import org.kie.kogito.Addons; import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.event.EventPublisher; import org.kie.kogito.jobs.JobsService; import org.kie.kogito.process.ProcessConfig; @@ -51,6 +52,7 @@ public abstract class AbstractProcessConfig implements ProcessConfig { private final JobsService jobsService; private final ProcessVersionResolver versionResolver; private final IdentityProvider identityProvider; + private final BusinessCalendar businessCalendar; protected AbstractProcessConfig( Iterable workItemHandlerConfig, @@ -62,7 +64,8 @@ protected AbstractProcessConfig( String kogitoService, Iterable unitOfWorkListeners, Iterable versionResolver, - Iterable identityProvider) { + Iterable identityProvider, + Iterable businessCalendar) { this.workItemHandlerConfig = mergeWorkItemHandler(workItemHandlerConfig, DefaultWorkItemHandlerConfig::new); this.processEventListenerConfig = merge(processEventListenerConfigs, processEventListeners); @@ -72,6 +75,7 @@ protected AbstractProcessConfig( this.jobsService = orDefault(jobsService, () -> null); this.versionResolver = orDefault(versionResolver, () -> null); this.identityProvider = orDefault(identityProvider, NoOpIdentityProvider::new); + this.businessCalendar = orDefault(businessCalendar, () -> null); eventPublishers.forEach(publisher -> unitOfWorkManager().eventManager().addPublisher(publisher)); unitOfWorkListeners.forEach(listener -> unitOfWorkManager().register(listener)); @@ -124,6 +128,11 @@ public IdentityProvider identityProvider() { return identityProvider; } + @Override + public BusinessCalendar getBusinessCalendar() { + return this.businessCalendar; + } + public org.kie.kogito.Addons addons() { return Addons.EMTPY; } diff --git a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/StaticProcessConfig.java b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/StaticProcessConfig.java index c540407c4f9..343f232a042 100644 --- a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/StaticProcessConfig.java +++ b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/StaticProcessConfig.java @@ -19,6 +19,7 @@ package org.kie.kogito.process.impl; import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.jobs.JobsService; import org.kie.kogito.process.ProcessConfig; import org.kie.kogito.process.ProcessEventListenerConfig; @@ -41,12 +42,13 @@ public class StaticProcessConfig implements ProcessConfig { private final ProcessVersionResolver versionResolver; private final IdentityProvider identityProvider; + private final BusinessCalendar businessCalendar; public StaticProcessConfig( WorkItemHandlerConfig workItemHandlerConfig, ProcessEventListenerConfig processEventListenerConfig, UnitOfWorkManager unitOfWorkManager) { - this(workItemHandlerConfig, processEventListenerConfig, unitOfWorkManager, null, null, new NoOpIdentityProvider()); + this(workItemHandlerConfig, processEventListenerConfig, unitOfWorkManager, null, null, new NoOpIdentityProvider(), null); } public StaticProcessConfig( @@ -55,7 +57,8 @@ public StaticProcessConfig( UnitOfWorkManager unitOfWorkManager, JobsService jobsService, ProcessVersionResolver versionResolver, - IdentityProvider identityProvider) { + IdentityProvider identityProvider, + BusinessCalendar calendar) { this.unitOfWorkManager = unitOfWorkManager; this.workItemHandlerConfig = workItemHandlerConfig; this.processEventListenerConfig = processEventListenerConfig; @@ -63,6 +66,7 @@ public StaticProcessConfig( this.jobsService = jobsService; this.versionResolver = versionResolver; this.identityProvider = identityProvider; + this.businessCalendar = calendar; } public StaticProcessConfig() { @@ -71,7 +75,8 @@ public StaticProcessConfig() { new DefaultUnitOfWorkManager(new CollectingUnitOfWorkFactory()), null, null, - new NoOpIdentityProvider()); + new NoOpIdentityProvider(), + null); } @Override @@ -108,4 +113,9 @@ public ProcessVersionResolver versionResolver() { public IdentityProvider identityProvider() { return identityProvider; } + + @Override + public BusinessCalendar getBusinessCalendar() { + return this.businessCalendar; + } } diff --git a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java index 1fc0852da9b..3d398d753d1 100755 --- a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java +++ b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java @@ -31,7 +31,7 @@ import org.slf4j.LoggerFactory; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; public class BusinessCalendarImplTest extends AbstractBaseTest { @@ -327,14 +327,8 @@ public void testCalculateMinutesPassingAfterHour() { } @Test - public void testMissingConfigurationDualArgConstructor() { - SessionPseudoClock clock = new StaticPseudoClock(parseToDateWithTime("2012-05-04 13:45").getTime()); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new BusinessCalendarImpl(null, clock)); - } - - @Test - public void testMissingConfigurationSingleArgConstructor() { - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new BusinessCalendarImpl(null)); + public void testBusinessCalendarWithoutProvidedConfiguration() { + assertDoesNotThrow(() -> new BusinessCalendarImpl()); } @Test diff --git a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/instance/LightProcessRuntimeTest.java b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/instance/LightProcessRuntimeTest.java index 61a3c1f28a4..7e3b18e691a 100644 --- a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/instance/LightProcessRuntimeTest.java +++ b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/instance/LightProcessRuntimeTest.java @@ -26,9 +26,12 @@ import org.junit.jupiter.api.Test; import org.kie.api.definition.process.WorkflowElementIdentifier; import org.kie.kogito.Application; +import org.kie.kogito.Config; import org.kie.kogito.process.Processes; +import org.kie.kogito.process.impl.AbstractProcessConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -66,6 +69,9 @@ void testInstantiation() { LightProcessRuntimeContext rtc = new LightProcessRuntimeContext(Collections.singletonList(myProcess.process)); Application application = mock(Application.class); + Config config = mock(Config.class); + when(application.config()).thenReturn(config); + when(config.get(any())).thenReturn(mock(AbstractProcessConfig.class)); when(application.get(Processes.class)).thenReturn(mock(Processes.class)); LightProcessRuntime rt = new LightProcessRuntime(rtc, services, application); diff --git a/jbpm/jbpm-flow/src/test/java/org/jbpm/ruleflow/instance/RuleFlowProcessInstanceTest.java b/jbpm/jbpm-flow/src/test/java/org/jbpm/ruleflow/instance/RuleFlowProcessInstanceTest.java index d0b8ced9e98..68d2e177e4d 100755 --- a/jbpm/jbpm-flow/src/test/java/org/jbpm/ruleflow/instance/RuleFlowProcessInstanceTest.java +++ b/jbpm/jbpm-flow/src/test/java/org/jbpm/ruleflow/instance/RuleFlowProcessInstanceTest.java @@ -27,7 +27,8 @@ import org.kie.kogito.internal.process.runtime.KogitoProcessRuntime; import org.slf4j.LoggerFactory; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class RuleFlowProcessInstanceTest extends AbstractBaseTest { diff --git a/jbpm/jbpm-flow/src/test/java/org/kie/kogito/process/impl/AbstractProcessConfigTest.java b/jbpm/jbpm-flow/src/test/java/org/kie/kogito/process/impl/AbstractProcessConfigTest.java index 2f0b75255dd..6a3f26da38b 100644 --- a/jbpm/jbpm-flow/src/test/java/org/kie/kogito/process/impl/AbstractProcessConfigTest.java +++ b/jbpm/jbpm-flow/src/test/java/org/kie/kogito/process/impl/AbstractProcessConfigTest.java @@ -34,7 +34,7 @@ public class AbstractProcessConfigTest { private static class MockProcessConfig extends AbstractProcessConfig { protected MockProcessConfig(Iterable workItemHandlerConfig) { super(workItemHandlerConfig, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); } } diff --git a/jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/calendar/BPMN2-BusinessCalendarTimer.bpmn2 b/jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/calendar/BPMN2-BusinessCalendarTimer.bpmn2 new file mode 100644 index 00000000000..490924b1f18 --- /dev/null +++ b/jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/calendar/BPMN2-BusinessCalendarTimer.bpmn2 @@ -0,0 +1,172 @@ + + + + + _BD039EC9-BD1C-44EB-8DB7-347E412D0F7A + + + _BD039EC9-BD1C-44EB-8DB7-347E412D0F7A + _AA98E9A1-3635-40E0-8991-471E24785D03 + + + + + + + _7C1AEE0C-F9CC-46D8-BFC0-554A3046F3BF_TaskNameInputX + + HumanTask + _7C1AEE0C-F9CC-46D8-BFC0-554A3046F3BF_TaskNameInputX + + + + + john + + + + + + _AA98E9A1-3635-40E0-8991-471E24785D03 + + + + + _9C180400-8EAA-4C89-A148-AE8872CFD116 + + + + _9C180400-8EAA-4C89-A148-AE8872CFD116 + + 1s + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _02EK0ABVEeSwDJQJw6Rb7Q + _02EK0ABVEeSwDJQJw6Rb7Q + + diff --git a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ProcessFactoryTest.java b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ProcessFactoryTest.java index 2a78663004c..5739164f833 100755 --- a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ProcessFactoryTest.java +++ b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ProcessFactoryTest.java @@ -41,12 +41,14 @@ import org.kie.api.runtime.process.ProcessInstance; import org.kie.internal.io.ResourceFactory; import org.kie.kogito.Application; +import org.kie.kogito.Config; import org.kie.kogito.internal.process.event.DefaultKogitoProcessEventListener; import org.kie.kogito.internal.process.event.KogitoProcessEventListener; import org.kie.kogito.internal.process.runtime.KogitoProcessInstance; import org.kie.kogito.process.Processes; import org.kie.kogito.process.bpmn2.BpmnProcess; import org.kie.kogito.process.bpmn2.BpmnProcesses; +import org.kie.kogito.process.impl.AbstractProcessConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.jbpm.ruleflow.core.Metadata.CANCEL_ACTIVITY; @@ -56,6 +58,7 @@ import static org.jbpm.ruleflow.core.Metadata.HAS_ERROR_EVENT; import static org.jbpm.ruleflow.core.Metadata.TIME_CYCLE; import static org.jbpm.ruleflow.core.Metadata.TIME_DURATION; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -417,6 +420,9 @@ public void testBoundaryErrorEvent(String errorCode) throws Exception { final RuleFlowProcess process = factory.validate().getProcess(); Application application = mock(Application.class); + Config config = mock(Config.class); + when(application.config()).thenReturn(config); + when(config.get(any())).thenReturn(mock(AbstractProcessConfig.class)); when(application.get(Processes.class)).thenReturn(new BpmnProcesses().addProcess(new BpmnProcess(process))); final LightProcessRuntime processRuntime = LightProcessRuntime.of(application, Collections.singletonList(process), new LightProcessRuntimeServiceProvider()); diff --git a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTest.java b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTest.java new file mode 100644 index 00000000000..c89cc7a824d --- /dev/null +++ b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTest.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jbpm.bpmn2.calendar; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import org.jbpm.bpmn2.objects.TestWorkItemHandler; +import org.jbpm.process.core.timer.BusinessCalendarImpl; +import org.jbpm.test.utils.ProcessTestHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.kie.kogito.Application; +import org.kie.kogito.StaticApplication; +import org.kie.kogito.StaticConfig; +import org.kie.kogito.calendar.BusinessCalendar; +import org.kie.kogito.process.ProcessConfig; +import org.kie.kogito.process.ProcessInstance; +import org.kie.kogito.process.bpmn2.BpmnProcesses; +import org.kie.kogito.process.impl.AbstractProcessConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BusinessCalendarTest { + + private static BusinessCalendar workingDayCalendar; + private static BusinessCalendar notWorkingDayCalendar; + + @BeforeAll + public static void createCalendars() { + workingDayCalendar = configureBusinessCalendar(true); + notWorkingDayCalendar = configureBusinessCalendar(false); + } + + @Test + public void testTimerWithWorkingDayCalendar() throws InterruptedException { + BpmnProcesses bpmnProcesses = new BpmnProcesses(); + ProcessConfig config = new MockProcessConfig(workingDayCalendar); + Application app = new StaticApplication(new StaticConfig(null, config), bpmnProcesses); + TestWorkItemHandler workItemHandler = new TestWorkItemHandler(); + ProcessTestHelper.registerHandler(app, "Human Task", workItemHandler); + org.kie.kogito.process.Process processDefinition = BusinessCalendarTimerProcess.newProcess(app); + BusinessCalendarTimerModel model = processDefinition.createModel(); + org.kie.kogito.process.ProcessInstance instance = processDefinition.createInstance(model); + instance.start(); + assertThat(instance.status()).isEqualTo(ProcessInstance.STATE_ACTIVE); + Thread.sleep(2000); + assertThat(instance.status()).isEqualTo(ProcessInstance.STATE_COMPLETED); + } + + @Test + public void testTimerWithNotWorkingDayCalendar() throws InterruptedException { + BpmnProcesses bpmnProcesses = new BpmnProcesses(); + ProcessConfig config = new MockProcessConfig(notWorkingDayCalendar); + Application app = new StaticApplication(new StaticConfig(null, config), bpmnProcesses); + TestWorkItemHandler workItemHandler = new TestWorkItemHandler(); + ProcessTestHelper.registerHandler(app, "Human Task", workItemHandler); + org.kie.kogito.process.Process processDefinition = BusinessCalendarTimerProcess.newProcess(app); + BusinessCalendarTimerModel model = processDefinition.createModel(); + org.kie.kogito.process.ProcessInstance instance = processDefinition.createInstance(model); + instance.start(); + assertThat(instance.status()).isEqualTo(ProcessInstance.STATE_ACTIVE); + Thread.sleep(2000); + assertThat(instance.status()).isEqualTo(ProcessInstance.STATE_ACTIVE); + } + + private static BusinessCalendar configureBusinessCalendar(boolean isWorkingDayCalendar) { + Properties businessCalendarConfiguration = new Properties(); + if (isWorkingDayCalendar) { + businessCalendarConfiguration.setProperty(BusinessCalendarImpl.START_HOUR, "0"); + businessCalendarConfiguration.setProperty(BusinessCalendarImpl.END_HOUR, "24"); + businessCalendarConfiguration.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, "24"); + businessCalendarConfiguration.setProperty(BusinessCalendarImpl.DAYS_PER_WEEK, "7"); + businessCalendarConfiguration.setProperty(BusinessCalendarImpl.WEEKEND_DAYS, "8,9"); + } else { + Calendar currentCalendar = Calendar.getInstance(); + Date today = new Date(); + currentCalendar.add(Calendar.DATE, 1); + Date tomorrow = currentCalendar.getTime(); + String dateFormat = "yyyy-MM-dd"; + SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); + businessCalendarConfiguration.setProperty(BusinessCalendarImpl.HOLIDAYS, sdf.format(today) + "," + sdf.format(tomorrow)); + businessCalendarConfiguration.setProperty(BusinessCalendarImpl.HOLIDAY_DATE_FORMAT, dateFormat); + } + return new BusinessCalendarImpl(businessCalendarConfiguration); + } + + private static class MockProcessConfig extends AbstractProcessConfig { + private MockProcessConfig(BusinessCalendar businessCalendar) { + super(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), List.of(businessCalendar)); + } + } +} diff --git a/jbpm/jbpm-tests/src/test/resources/calendar.properties b/jbpm/jbpm-tests/src/test/resources/calendar.properties new file mode 100644 index 00000000000..e2ab07c9f05 --- /dev/null +++ b/jbpm/jbpm-tests/src/test/resources/calendar.properties @@ -0,0 +1,6 @@ +business.end.hour=24 +business.hours.per.day=24 +business.start.hour=0 +business.holiday.date.format=yyyy-MM-dd +business.days.per.week =7 +#business.cal.timezone= system default timezone.. diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java index 528a9e081a0..14bab62579a 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -77,6 +78,7 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toList; +import static org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_PATH; import static org.kie.kogito.grafana.GrafanaConfigurationWriter.buildDashboardName; import static org.kie.kogito.grafana.GrafanaConfigurationWriter.generateOperationalDashboard; import static org.kie.kogito.internal.utils.ConversionUtils.sanitizeClassName; @@ -100,7 +102,8 @@ public class ProcessCodegen extends AbstractGenerator { private static final String GLOBAL_OPERATIONAL_DASHBOARD_TEMPLATE = "/grafana-dashboard-template/processes/global-operational-dashboard-template.json"; private static final String PROCESS_OPERATIONAL_DASHBOARD_TEMPLATE = "/grafana-dashboard-template/processes/process-operational-dashboard-template.json"; - + public static final String BUSINESS_CALENDAR_PRODUCER_TEMPLATE = "BusinessCalendarProducer"; + private static final String IS_BUSINESS_CALENDAR_PRESENT = "isBusinessCalendarPresent"; static { ProcessValidatorRegistry.getInstance().registerAdditonalValidator(JavaRuleFlowProcessValidator.getInstance()); BPMN_SEMANTIC_MODULES.addSemanticModule(new BPMNSemanticModule()); @@ -144,6 +147,7 @@ public static ProcessCodegen ofCollectedResources(KogitoBuildContext context, Co if (useSvgAddon) { context.addContextAttribute(ContextAttributesConstants.PROCESS_AUTO_SVG_MAPPING, processSVGMap); } + context.addContextAttribute(IS_BUSINESS_CALENDAR_PRESENT, resources.stream().anyMatch(resource -> resource.resource().getSourcePath().endsWith(BUSINESS_CALENDAR_PATH))); handleValidation(context, processesErrors); @@ -436,10 +440,16 @@ protected Collection internalGenerate() { } //Generating the Producer classes for Dependency Injection - StaticDependencyInjectionProducerGenerator.of(context()) - .generate() + StaticDependencyInjectionProducerGenerator staticDependencyInjectionProducerGenerator = StaticDependencyInjectionProducerGenerator.of(context()); + + staticDependencyInjectionProducerGenerator.generate() .entrySet() .forEach(entry -> storeFile(PRODUCER_TYPE, entry.getKey(), entry.getValue())); + Boolean isBusinessCalendarPresent = context().getContextAttribute(IS_BUSINESS_CALENDAR_PRESENT, Boolean.class); + if (Objects.nonNull(isBusinessCalendarPresent) && isBusinessCalendarPresent) { + staticDependencyInjectionProducerGenerator.generate(List.of(BUSINESS_CALENDAR_PRODUCER_TEMPLATE)) + .forEach((key, value) -> storeFile(PRODUCER_TYPE, key, value)); + } if (context().hasRESTForGenerator(this)) { for (ProcessResourceGenerator resourceGenerator : rgs) { diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/StaticDependencyInjectionProducerGenerator.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/StaticDependencyInjectionProducerGenerator.java index 9c147303e7f..9529b99cbc9 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/StaticDependencyInjectionProducerGenerator.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/StaticDependencyInjectionProducerGenerator.java @@ -49,11 +49,19 @@ public static StaticDependencyInjectionProducerGenerator of(KogitoBuildContext c * @return Map with the generated resources */ public Map generate() { + return generate(producerTemplates); + } + + /** + * Key is the FilePath, Value is the content + * + * @return Map with the generated resources + */ + public Map generate(List templates) { if (!context.hasDI()) { return Collections.emptyMap(); } - return producerTemplates.stream() - .map(this::buildProducerTemplatedGenerator) + return templates.stream().map(this::buildProducerTemplatedGenerator) .collect(Collectors.toMap(TemplatedGenerator::generatedFilePath, generator -> generator.compilationUnitOrThrow().toString())); } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigQuarkusTemplate.java index 19491e3b91b..98daf8fbdca 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigQuarkusTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigQuarkusTemplate.java @@ -18,10 +18,9 @@ */ package $Package$; -import jakarta.enterprise.inject.Instance; - import org.kie.api.event.process.ProcessEventListener; import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.event.EventPublisher; import org.kie.kogito.jobs.JobsService; import org.kie.kogito.process.ProcessEventListenerConfig; @@ -30,6 +29,8 @@ import org.kie.kogito.uow.UnitOfWorkManager; import org.kie.kogito.uow.events.UnitOfWorkEventListener; +import jakarta.enterprise.inject.Instance; + @jakarta.inject.Singleton public class ProcessConfig extends org.kie.kogito.process.impl.AbstractProcessConfig { @@ -44,7 +45,8 @@ public ProcessConfig( org.kie.kogito.config.ConfigBean configBean, Instance unitOfWorkEventListeners, Instance versionResolver, - Instance identityProvider) { + Instance identityProvider, + Instance businessCalendar) { super(workItemHandlerConfig, processEventListenerConfigs, @@ -55,7 +57,8 @@ public ProcessConfig( configBean.getServiceUrl(), unitOfWorkEventListeners, versionResolver, - identityProvider); + identityProvider, + businessCalendar); } } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigSpringTemplate.java index df77c11433b..8152091578b 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigSpringTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/config/ProcessConfigSpringTemplate.java @@ -22,6 +22,7 @@ import org.kie.api.event.process.ProcessEventListener; import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.calendar.BusinessCalendar; import org.kie.kogito.event.EventPublisher; import org.kie.kogito.jobs.JobsService; import org.kie.kogito.process.ProcessEventListenerConfig; @@ -44,7 +45,8 @@ public ProcessConfig( org.kie.kogito.config.ConfigBean configBean, List unitOfWorkEventListeners, List versionResolver, - List identityProvider) { + List identityProvider, + List businessCalendar) { super(workItemHandlerConfig, processEventListenerConfigs, @@ -55,6 +57,7 @@ public ProcessConfig( configBean.getServiceUrl(), unitOfWorkEventListeners, versionResolver, - identityProvider); + identityProvider, + businessCalendar); } } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java new file mode 100644 index 00000000000..4504af36e14 --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package $Package$; + +import org.jbpm.process.core.timer.BusinessCalendarImpl; +import org.kie.kogito.calendar.BusinessCalendar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.enterprise.inject.Produces; + +public class BusinessCalendarProducer { + + private static final Logger logger = LoggerFactory.getLogger(BusinessCalendarProducer.class); + + @Produces + public BusinessCalendar createBusinessCalendar() { + return new BusinessCalendarImpl(); + } +} \ No newline at end of file diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java new file mode 100644 index 00000000000..8fd9f766a1f --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package $Package$; + +import org.kie.kogito.calendar.BusinessCalendar; +import org.jbpm.process.core.timer.BusinessCalendarImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BusinessCalendarProducer { + + private static final Logger logger = LoggerFactory.getLogger(BusinessCalendarProducer.class); + + @Bean + public BusinessCalendar createBusinessCalendar() { + + return new BusinessCalendarImpl(); + } +} \ No newline at end of file diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java index b2472ec7580..d1d930a1166 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java @@ -23,17 +23,24 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.drools.codegen.common.GeneratedFile; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.kie.kogito.codegen.api.AddonsConfig; import org.kie.kogito.codegen.api.context.KogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.JavaKogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.QuarkusKogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.SpringBootKogitoBuildContext; import org.kie.kogito.codegen.core.DashboardGeneratedFileUtils; import org.kie.kogito.codegen.core.io.CollectedResourceProducer; import static org.assertj.core.api.Assertions.assertThat; +import static org.kie.kogito.codegen.process.ProcessCodegen.BUSINESS_CALENDAR_PRODUCER_TEMPLATE; import static org.kie.kogito.grafana.utils.GrafanaDashboardUtils.DISABLED_OPERATIONAL_DASHBOARDS; class ProcessCodegenTest { @@ -99,16 +106,44 @@ public void whenMonitoringAndPrometheusEnabledGrafanaDashboardsAreNotGenerated(K generateTestDashboards(codeGenerator, 0); } - private List generateTestDashboards(ProcessCodegen codeGenerator, int expectedDashboards) { + @ParameterizedTest + @MethodSource("contextBuildersForBusinessCalendar") + public void whenCalendarPropertiesFoundGenerateBusinessCalendar(KogitoBuildContext.Builder contextBuilder, String dependencyAnnotation) { + KogitoBuildContext context = contextBuilder.build(); + StaticDependencyInjectionProducerGenerator staticDependencyInjectionProducerGenerator = StaticDependencyInjectionProducerGenerator.of(context); + Map businessCalendarProducerSourcesMap = staticDependencyInjectionProducerGenerator.generate(List.of(BUSINESS_CALENDAR_PRODUCER_TEMPLATE)); + String expectedKey = "org/kie/kogito/app/BusinessCalendarProducer.java"; + assertThat(businessCalendarProducerSourcesMap).hasSize(1).containsKey(expectedKey); + String generatedContent = businessCalendarProducerSourcesMap.get(expectedKey); + assertThat(generatedContent).isNotNull().isNotEmpty().contains(dependencyAnnotation); + } - Collection generatedFiles = codeGenerator.generate(); + @ParameterizedTest + @MethodSource("contextBuildersNotDI") + public void whenBuildingNotDIContext(KogitoBuildContext.Builder contextBuilder) { + KogitoBuildContext context = contextBuilder.build(); + StaticDependencyInjectionProducerGenerator staticDependencyInjectionProducerGenerator = StaticDependencyInjectionProducerGenerator.of(context); + Map businessCalendarProducer = staticDependencyInjectionProducerGenerator.generate(List.of(BUSINESS_CALENDAR_PRODUCER_TEMPLATE)); + assertThat(businessCalendarProducer).isEmpty(); + } + private List generateTestDashboards(ProcessCodegen codeGenerator, int expectedDashboards) { + Collection generatedFiles = codeGenerator.generate(); List dashboards = generatedFiles.stream() .filter(x -> x.type().equals(DashboardGeneratedFileUtils.DASHBOARD_TYPE)) .collect(Collectors.toList()); - assertThat(dashboards).hasSize(expectedDashboards); - return dashboards; } + + private static Stream contextBuildersForBusinessCalendar() { + return Stream.of( + Arguments.of(QuarkusKogitoBuildContext.builder(), "@Produces"), + Arguments.of(SpringBootKogitoBuildContext.builder(), "@Bean")); + } + + private static Stream contextBuildersNotDI() { + return Stream.of( + Arguments.of(JavaKogitoBuildContext.builder())); + } } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java new file mode 100644 index 00000000000..dd5e5f18ea9 --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package $Package$; + +import org.kie.kogito.calendar.BusinessCalendar; +import org.kie.kogito.calendar.BusinessCalendarImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.enterprise.inject.Produces; + +public class BusinessCalendarProducer { + + private static final Logger logger = LoggerFactory.getLogger(BusinessCalendarProducer.class); + + @Produces + public BusinessCalendar createBusinessCalendar() { + return new BusinessCalendarImpl(); + } +} \ No newline at end of file diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java new file mode 100644 index 00000000000..e80c7cb10b8 --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package $Package$; + +import org.kie.kogito.calendar.BusinessCalendar; +import org.kie.kogito.calendar.BusinessCalendarImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BusinessCalendarProducer { + + private static final Logger logger = LoggerFactory.getLogger(BusinessCalendarProducer.class); + + @Bean + public BusinessCalendar createBusinessCalendar() { + return new BusinessCalendarImpl(); + } +} \ No newline at end of file From 07c4639d1643893b0661cfe7a526dac1edee0085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20Zim=C3=A1nyi?= Date: Tue, 29 Oct 2024 07:39:54 +0100 Subject: [PATCH 06/16] [kie-issues#1564] Do not overwrite original jar with Spring Boot executable jar in integration tests (#3748) * Don't overwrite base jar with Spring Boot jar. * Move unpack jars profile to an earlier Maven phase. --- kogito-build/kogito-build-no-bom-parent/pom.xml | 2 +- .../integration-tests-springboot-decisions-it/pom.xml | 3 +++ .../integration-tests-springboot-kafka-it/pom.xml | 3 +++ .../integration-tests-springboot-norest-it/pom.xml | 3 +++ .../integration-tests-springboot-processes-it/pom.xml | 3 +++ .../pom.xml | 3 +++ 6 files changed, 16 insertions(+), 1 deletion(-) diff --git a/kogito-build/kogito-build-no-bom-parent/pom.xml b/kogito-build/kogito-build-no-bom-parent/pom.xml index 8b3101c356f..94e930a526e 100644 --- a/kogito-build/kogito-build-no-bom-parent/pom.xml +++ b/kogito-build/kogito-build-no-bom-parent/pom.xml @@ -1109,7 +1109,7 @@ unpack-jar - compile + process-resources unpack diff --git a/springboot/integration-tests/integration-tests-springboot-decisions-it/pom.xml b/springboot/integration-tests/integration-tests-springboot-decisions-it/pom.xml index ccfd8d904d3..844e657acd3 100644 --- a/springboot/integration-tests/integration-tests-springboot-decisions-it/pom.xml +++ b/springboot/integration-tests/integration-tests-springboot-decisions-it/pom.xml @@ -164,6 +164,9 @@ repackage + + executable + diff --git a/springboot/integration-tests/integration-tests-springboot-kafka-it/pom.xml b/springboot/integration-tests/integration-tests-springboot-kafka-it/pom.xml index 890b7513937..b40463110b7 100644 --- a/springboot/integration-tests/integration-tests-springboot-kafka-it/pom.xml +++ b/springboot/integration-tests/integration-tests-springboot-kafka-it/pom.xml @@ -166,6 +166,9 @@ repackage + + executable + diff --git a/springboot/integration-tests/integration-tests-springboot-norest-it/pom.xml b/springboot/integration-tests/integration-tests-springboot-norest-it/pom.xml index cb2a4795bea..eaa58d571bf 100644 --- a/springboot/integration-tests/integration-tests-springboot-norest-it/pom.xml +++ b/springboot/integration-tests/integration-tests-springboot-norest-it/pom.xml @@ -154,6 +154,9 @@ repackage + + executable + diff --git a/springboot/integration-tests/integration-tests-springboot-processes-it/pom.xml b/springboot/integration-tests/integration-tests-springboot-processes-it/pom.xml index e5f9ff3402b..66bd8d48053 100644 --- a/springboot/integration-tests/integration-tests-springboot-processes-it/pom.xml +++ b/springboot/integration-tests/integration-tests-springboot-processes-it/pom.xml @@ -184,6 +184,9 @@ repackage + + executable + diff --git a/springboot/integration-tests/integration-tests-springboot-processes-persistence-it/pom.xml b/springboot/integration-tests/integration-tests-springboot-processes-persistence-it/pom.xml index 6d09004c22a..ac43bc7bec8 100644 --- a/springboot/integration-tests/integration-tests-springboot-processes-persistence-it/pom.xml +++ b/springboot/integration-tests/integration-tests-springboot-processes-persistence-it/pom.xml @@ -128,6 +128,9 @@ repackage + + executable + From f9edb64c2e7996963f8db7cb44c2bfe7434a1e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pere=20Fern=C3=A1ndez?= Date: Thu, 31 Oct 2024 10:18:22 +0100 Subject: [PATCH 07/16] [NO_ISSUE] Fix User Task variable events. (#3755) --- .../org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java index 266e674887e..757bf30edec 100644 --- a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/DefaultUserTaskInstance.java @@ -210,7 +210,7 @@ public void setOutputs(Map outputs) { public void setInput(String key, Object newValue) { Object oldValue = this.inputs.put(key, newValue); if (this.userTaskEventSupport != null) { - this.userTaskEventSupport.fireOnUserTaskInputVariableChange(this, key, oldValue, newValue); + this.userTaskEventSupport.fireOnUserTaskInputVariableChange(this, key, newValue, oldValue); } updatePersistence(); } @@ -219,7 +219,7 @@ public void setInput(String key, Object newValue) { public void setOutput(String key, Object newValue) { Object oldValue = this.outputs.put(key, newValue); if (this.userTaskEventSupport != null) { - this.userTaskEventSupport.fireOnUserTaskOutputVariableChange(this, key, oldValue, newValue); + this.userTaskEventSupport.fireOnUserTaskOutputVariableChange(this, key, newValue, oldValue); } updatePersistence(); } From b750843d35dd1296687b79513be2265adcdb230d Mon Sep 17 00:00:00 2001 From: Enrique Date: Thu, 31 Oct 2024 11:25:38 +0100 Subject: [PATCH 08/16] [incubator-kie-issues-1557] Marshalling POJO Input/output in user task (#3749) * [incubator-kie-issues-1557] Marshalling POJO Input/output in user task --- .../SimpleDeserializationProblemHandler.java | 33 ++++++++++ .../json/SimplePolymorphicTypeValidator.java | 44 +++++++++++++ .../codegen/usertask/UserTaskCodegen.java | 2 +- .../RestResourceUserTaskQuarkusTemplate.java | 41 ++++++++++-- .../RestResourceUserTaskSpringTemplate.java | 45 +++++++++++-- .../integrationtests/quarkus/TaskIT.java | 65 ++++++++++++++++++ .../integrationtests/springboot/TaskTest.java | 66 +++++++++++++++++++ 7 files changed, 284 insertions(+), 12 deletions(-) create mode 100644 jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimpleDeserializationProblemHandler.java create mode 100644 jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimplePolymorphicTypeValidator.java diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimpleDeserializationProblemHandler.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimpleDeserializationProblemHandler.java new file mode 100644 index 00000000000..f3c447b6fc1 --- /dev/null +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimpleDeserializationProblemHandler.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.usertask.impl.json; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; + +public class SimpleDeserializationProblemHandler extends DeserializationProblemHandler { + @Override + public JavaType handleMissingTypeId(DeserializationContext ctxt, JavaType baseType, TypeIdResolver idResolver, String failureMsg) throws IOException { + return baseType; + } +} diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimplePolymorphicTypeValidator.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimplePolymorphicTypeValidator.java new file mode 100644 index 00000000000..6940b1a8e1b --- /dev/null +++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/json/SimplePolymorphicTypeValidator.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.usertask.impl.json; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; + +public class SimplePolymorphicTypeValidator extends PolymorphicTypeValidator { + + private static final long serialVersionUID = 6608109163132613995L; + + @Override + public Validity validateBaseType(MapperConfig config, JavaType baseType) { + return Validity.ALLOWED; + } + + @Override + public Validity validateSubClassName(MapperConfig config, JavaType baseType, String subClassName) throws JsonMappingException { + return Validity.ALLOWED; + } + + @Override + public Validity validateSubType(MapperConfig config, JavaType baseType, JavaType subType) throws JsonMappingException { + return Validity.ALLOWED; + } +} diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/usertask/UserTaskCodegen.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/usertask/UserTaskCodegen.java index 9c2fa55b1ac..7fd6603aa04 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/usertask/UserTaskCodegen.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/usertask/UserTaskCodegen.java @@ -198,7 +198,7 @@ public List generateUserTask() { ConstructorDeclaration declaration = clazzDeclaration.findFirst(ConstructorDeclaration.class).get(); declaration.setName(className); - String taskNodeName = (String) info.getParameter(NODE_NAME); + String taskNodeName = (String) info.getParameter("TaskName"); Expression taskNameExpression = taskNodeName != null ? new StringLiteralExpr(taskNodeName) : new NullLiteralExpr(); BlockStmt block = declaration.getBody(); diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java index 8a7039263d0..7369db60545 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java @@ -20,8 +20,11 @@ import java.util.Map; import java.util.List; +import java.io.IOException; import java.util.Collection; +import jakarta.inject.Inject; + import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; @@ -47,12 +50,24 @@ import org.kie.kogito.services.uow.UnitOfWorkExecutor; import org.kie.kogito.usertask.UserTaskInstanceNotFoundException; import org.kie.kogito.usertask.UserTaskService; +import org.kie.kogito.usertask.impl.json.SimpleDeserializationProblemHandler; +import org.kie.kogito.usertask.impl.json.SimplePolymorphicTypeValidator; import org.kie.kogito.usertask.view.UserTaskView; import org.kie.kogito.usertask.view.UserTaskTransitionView; import org.kie.kogito.usertask.model.*; -import jakarta.inject.Inject; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator.Validity; +import com.fasterxml.jackson.databind.module.SimpleModule; @Path("/usertasks/instance") public class UserTasksResource { @@ -60,6 +75,20 @@ public class UserTasksResource { @Inject UserTaskService userTaskService; + @Inject + ObjectMapper objectMapper; + + ObjectMapper mapper; + + @jakarta.annotation.PostConstruct + public void init() { + mapper = objectMapper.copy(); + SimpleModule module = new SimpleModule(); + mapper.addHandler(new SimpleDeserializationProblemHandler()); + mapper.registerModule(module); + mapper.activateDefaultTypingAsProperty(new SimplePolymorphicTypeValidator(), DefaultTyping.NON_FINAL, "@type"); + } + @GET @Produces(MediaType.APPLICATION_JSON) public List list(@QueryParam("user") String user, @QueryParam("group") List groups) { @@ -80,7 +109,7 @@ public UserTaskView find(@PathParam("taskId") String taskId, @QueryParam("user") public UserTaskView transition( @PathParam("taskId") String taskId, @QueryParam("user") String user, - @QueryParam("group") List groups, + @QueryParam("group") List groups, TransitionInfo transitionInfo) { return userTaskService.transition(taskId, transitionInfo.getTransitionId(), transitionInfo.getData(), IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } @@ -102,18 +131,20 @@ public UserTaskView setOutput( @PathParam("taskId") String taskId, @QueryParam("user") String user, @QueryParam("group") List groups, - Map data) { + String body) throws Exception { + Map data = mapper.readValue(body, Map.class); return userTaskService.setOutputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } @PUT @Path("/{taskId}/inputs") @Consumes(MediaType.APPLICATION_JSON) - public UserTaskView setOutput(@PathParam("id") String id, + public UserTaskView setInputs( @PathParam("taskId") String taskId, @QueryParam("user") String user, @QueryParam("group") List groups, - Map data) { + String body) throws Exception { + Map data = mapper.readValue(body, Map.class); return userTaskService.setInputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java index 4e0e7580276..ea5b3873931 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import java.io.IOException; import java.util.Collection; import org.jbpm.util.JsonSchemaUtil; @@ -30,6 +31,8 @@ import org.kie.kogito.process.impl.Sig; import org.kie.kogito.services.uow.UnitOfWorkExecutor; import org.kie.kogito.usertask.UserTaskService; +import org.kie.kogito.usertask.impl.json.SimpleDeserializationProblemHandler; +import org.kie.kogito.usertask.impl.json.SimplePolymorphicTypeValidator; import org.kie.kogito.usertask.view.UserTaskTransitionView; import org.kie.kogito.usertask.view.UserTaskView; import org.springframework.http.HttpStatus; @@ -47,6 +50,19 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.util.UriComponentsBuilder; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator.Validity; +import com.fasterxml.jackson.databind.module.SimpleModule; + import org.springframework.beans.factory.annotation.Autowired; import org.kie.kogito.usertask.model.*; @@ -58,6 +74,20 @@ public class UserTasksResource { @Autowired UserTaskService userTaskService; + @Autowired + ObjectMapper objectMapper; + + ObjectMapper mapper; + + @jakarta.annotation.PostConstruct + public void init() { + mapper = objectMapper.copy(); + SimpleModule module = new SimpleModule(); + mapper.addHandler(new SimpleDeserializationProblemHandler()); + mapper.registerModule(module); + mapper.activateDefaultTypingAsProperty(new SimplePolymorphicTypeValidator(), DefaultTyping.NON_FINAL, "@type"); + } + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List list(@RequestParam("user") String user, @RequestParam("group") List groups) { return userTaskService.list(IdentityProviders.of(user, groups)); @@ -72,9 +102,10 @@ public UserTaskView find(@PathVariable("taskId") String taskId, @RequestParam("u public UserTaskView transition( @PathVariable("taskId") String taskId, @RequestParam("user") String user, - @RequestParam("group") List groups, + @RequestParam("group") List groups, @RequestBody TransitionInfo transitionInfo) { - return userTaskService.transition(taskId, transitionInfo.getTransitionId(), transitionInfo.getData(), IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.transition(taskId, transitionInfo.getTransitionId(), transitionInfo.getData(), IdentityProviders.of(user, groups)) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @GetMapping(value = "/{taskId}/transition", produces = MediaType.APPLICATION_JSON_VALUE) @@ -84,22 +115,24 @@ public Collection transition( @RequestParam("group") List groups) { return userTaskService.allowedTransitions(taskId, IdentityProviders.of(user, groups)); } - + @PutMapping("/{taskId}/outputs") public UserTaskView setOutput( @PathVariable("taskId") String taskId, @RequestParam("user") String user, @RequestParam("group") List groups, - @RequestBody Map data) { + @RequestBody String body) throws Exception { + Map data = mapper.readValue(body, Map.class); return userTaskService.setOutputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @PutMapping("/{taskId}/inputs") - public UserTaskView setOutput(@PathVariable("id") String id, + public UserTaskView setInputs( @PathVariable("taskId") String taskId, @RequestParam("user") String user, @RequestParam("group") List groups, - @RequestBody Map data) { + @RequestBody String body) throws Exception { + Map data = mapper.readValue(body, Map.class); return userTaskService.setInputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } diff --git a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/TaskIT.java b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/TaskIT.java index 0e5449be866..0388d2827ef 100644 --- a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/TaskIT.java +++ b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/TaskIT.java @@ -31,6 +31,10 @@ import org.kie.kogito.usertask.model.AttachmentInfo; import org.kie.kogito.usertask.model.CommentInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + import io.quarkus.test.junit.QuarkusIntegrationTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; @@ -39,6 +43,7 @@ import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchema; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @QuarkusIntegrationTest @@ -105,6 +110,66 @@ void testJsonSchema() throws IOException { } } + @Test + public void testInputOutputsViaJsonTypeProperty() throws Exception { + Traveller traveller = new Traveller("pepe", "rubiales", "pepe.rubiales@gmail.com", "Spanish", null); + + given() + .contentType(ContentType.JSON) + .when() + .body(Collections.singletonMap("traveller", traveller)) + .post("/approvals") + .then() + .statusCode(201) + .extract() + .path("id"); + + String taskId = given() + .contentType(ContentType.JSON) + .queryParam("user", "admin") + .queryParam("group", "managers") + .when() + .get("/usertasks/instance") + .then() + .statusCode(200) + .extract() + .path("[0].id"); + + traveller = new Traveller("pepe2", "rubiales2", "pepe.rubiales@gmail.com", "Spanish2", null); + ObjectMapper mapper = new ObjectMapper(); + mapper.activateDefaultTypingAsProperty(BasicPolymorphicTypeValidator.builder().build(), DefaultTyping.NON_FINAL, "@type"); + String jsonBody = mapper.writeValueAsString(Map.of("traveller", traveller)); + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "admin") + .queryParam("group", "managers") + .pathParam("taskId", taskId) + .body(jsonBody) + .put("/usertasks/instance/{taskId}/inputs") + .then() + .log().body() + .statusCode(200) + .body("inputs.traveller.firstName", is(traveller.getFirstName())) + .body("inputs.traveller.lastName", is(traveller.getLastName())) + .body("inputs.traveller.email", is(traveller.getEmail())) + .body("inputs.traveller.nationality", is(traveller.getNationality())); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "admin") + .queryParam("group", "managers") + .pathParam("taskId", taskId) + .body(jsonBody) + .put("/usertasks/instance/{taskId}/outputs") + .then() + .log().body() + .statusCode(200) + .body("outputs.traveller.firstName", is(traveller.getFirstName())) + .body("outputs.traveller.lastName", is(traveller.getLastName())) + .body("outputs.traveller.email", is(traveller.getEmail())) + .body("outputs.traveller.nationality", is(traveller.getNationality())); + } + @Test void testSaveTask() { Traveller traveller = new Traveller("pepe", "rubiales", "pepe.rubiales@gmail.com", "Spanish"); diff --git a/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/TaskTest.java b/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/TaskTest.java index f00a5267e0f..66821bd1c0b 100644 --- a/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/TaskTest.java +++ b/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/TaskTest.java @@ -40,12 +40,17 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + import io.restassured.http.ContentType; import static io.restassured.RestAssured.given; import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(SpringExtension.class) @@ -111,6 +116,66 @@ void testJsonSchemaFiles() { } } + @Test + public void testInputOutputsViaJsonTypeProperty() throws Exception { + Traveller traveller = new Traveller("pepe", "rubiales", "pepe.rubiales@gmail.com", "Spanish", null); + + given() + .contentType(ContentType.JSON) + .when() + .body(Collections.singletonMap("traveller", traveller)) + .post("/approvals") + .then() + .statusCode(201) + .extract() + .path("id"); + + String taskId = given() + .contentType(ContentType.JSON) + .queryParam("user", "admin") + .queryParam("group", "managers") + .when() + .get("/usertasks/instance") + .then() + .statusCode(200) + .extract() + .path("[0].id"); + + traveller = new Traveller("pepe2", "rubiales2", "pepe.rubiales@gmail.com", "Spanish2", null); + ObjectMapper mapper = new ObjectMapper(); + mapper.activateDefaultTypingAsProperty(BasicPolymorphicTypeValidator.builder().build(), DefaultTyping.NON_FINAL, "@type"); + String jsonBody = mapper.writeValueAsString(Map.of("traveller", traveller)); + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "admin") + .queryParam("group", "managers") + .pathParam("taskId", taskId) + .body(jsonBody) + .put("/usertasks/instance/{taskId}/inputs") + .then() + .log().body() + .statusCode(200) + .body("inputs.traveller.firstName", is(traveller.getFirstName())) + .body("inputs.traveller.lastName", is(traveller.getLastName())) + .body("inputs.traveller.email", is(traveller.getEmail())) + .body("inputs.traveller.nationality", is(traveller.getNationality())); + + given().contentType(ContentType.JSON) + .when() + .queryParam("user", "admin") + .queryParam("group", "managers") + .pathParam("taskId", taskId) + .body(jsonBody) + .put("/usertasks/instance/{taskId}/outputs") + .then() + .log().body() + .statusCode(200) + .body("outputs.traveller.firstName", is(traveller.getFirstName())) + .body("outputs.traveller.lastName", is(traveller.getLastName())) + .body("outputs.traveller.email", is(traveller.getEmail())) + .body("outputs.traveller.nationality", is(traveller.getNationality())); + } + @Test void testCommentAndAttachment() { Traveller traveller = new Traveller("pepe", "rubiales", "pepe.rubiales@gmail.com", "Spanish", null); @@ -371,4 +436,5 @@ void testUpdateTaskInfo() { assertThat(downTaskInfo.getInputParams()).isNotNull(); assertThat(downTaskInfo.getInputParams().get("traveller")).isNull(); } + } From 1e930714e66245b12aa331ad22de19131ce09257 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:32:36 +0100 Subject: [PATCH 09/16] [Fix #3733] Collecting more than one error message (#3756) * [Fix #3733] Collecting more than one error message * [Fix #3733] Including process validation check * [Fix #3733] Adding transition check --- .../validation/RuleFlowProcessValidator.java | 10 +++- .../AbstractWorkflowOperationIdFactory.java | 21 ++++--- .../workflow/parser/ParserContext.java | 12 +++- .../parser/ServerlessWorkflowParser.java | 22 +++++-- .../parser/handlers/ActionNodeUtils.java | 17 ++++-- .../handlers/ActionResourceFactory.java | 38 ++++++++----- .../handlers/CompositeContextNodeHandler.java | 4 +- .../parser/handlers/StateHandler.java | 36 +++++++++--- .../parser/handlers/SwitchHandler.java | 2 +- .../handlers/validation/SwitchValidator.java | 20 +++---- .../validation/WorkflowValidator.java | 6 +- .../parser/types/ScriptTypeHandler.java | 4 +- .../parser/types/SysOutTypeHandler.java | 16 +++++- .../validation/SwitchValidatorTest.java | 54 +++++++++++------- .../StaticFluentWorkflowApplicationTest.java | 5 +- .../StaticWorkflowApplicationTest.java | 13 +++++ .../src/test/resources/wrong.sw.json | 57 +++++++++++++++++++ .../workflow/executor/StaticRPCRegister.java | 4 +- 18 files changed, 253 insertions(+), 88 deletions(-) create mode 100644 kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java index 5319367af5d..ae89938bbae 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java @@ -115,9 +115,7 @@ public static RuleFlowProcessValidator getInstance() { return INSTANCE; } - public ProcessValidationError[] validateProcess(final RuleFlowProcess process) { - final List errors = new ArrayList<>(); - + public List validateProcess(final RuleFlowProcess process, List errors) { if (process.getName() == null) { errors.add(new ProcessValidationErrorImpl(process, "Process has no name.")); @@ -151,6 +149,12 @@ public ProcessValidationError[] validateProcess(final RuleFlowProcess process) { errors, process); + return errors; + + } + + public ProcessValidationError[] validateProcess(final RuleFlowProcess process) { + final List errors = validateProcess(process, new ArrayList<>()); return errors.toArray(new ProcessValidationError[errors.size()]); } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java index 5f4278c92d6..6a71a72b583 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java @@ -44,7 +44,7 @@ public abstract class AbstractWorkflowOperationIdFactory implements WorkflowOper @Override public WorkflowOperationId from(Workflow workflow, FunctionDefinition function, Optional context) { - ActionResource actionResource = ActionResourceFactory.getActionResource(function); + ActionResource actionResource = ActionResourceFactory.getActionResource(function, context); Optional convertedUri = convertURI(workflow, context, actionResource.getUri()); final String fileName; final String uri; @@ -56,14 +56,19 @@ public WorkflowOperationId from(Workflow workflow, FunctionDefinition function, fileName = getFileName(workflow, function, context, uri, actionResource.getOperation(), actionResource.getService()); } if (fileName == null || fileName.isBlank()) { - throw new IllegalArgumentException( - format("Empty file name for function '%s', please review uri '%s' or consider using a different strategy defined in the kogito.sw.operationIdStrategy property", - function.getName(), uri)); + String msg = format("Empty file name for function '%s', please review uri '%s' or consider using a different strategy defined in the kogito.sw.operationIdStrategy property", + function.getName(), uri); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } String packageName = onlyChars(removeExt(fileName.toLowerCase())); if (packageName.isBlank()) { - throw new IllegalArgumentException( - format("Empty package for file '%s'. A file name should contain at least one letter which is not part of the extension", fileName)); + String msg = + format("Empty package for file '%s'. A file name should contain at least one letter which is not part of the extension", fileName); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } return new WorkflowOperationId(uri, actionResource.getOperation(), actionResource.getService(), fileName, packageName); } @@ -80,7 +85,9 @@ private JsonNode getUriDefinitions(Workflow workflow, Optional co try { definitions = uri == null ? NullNode.instance : ObjectMapperFactory.get().readTree(readBytes(uri, workflow, context)); } catch (IOException e) { - throw new UncheckedIOException(e); + context.ifPresentOrElse(c -> c.addValidationError(e.getMessage()), () -> { + throw new UncheckedIOException(e); + }); } uriDefinitions.setDefinitions(definitions); } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java index 16dee79f093..6b86c08c753 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java @@ -43,8 +43,9 @@ public class ParserContext { private final NodeIdGenerator idGenerator; private final WorkflowOperationIdFactory operationIdFactory; private final KogitoBuildContext context; - private final Collection generatedFiles; + private final Collection generatedFiles = new ArrayList<>(); private final AsyncInfoResolver asyncInfoResolver; + private final Collection validationErrors = new ArrayList<>(); public static final String ASYNC_CONVERTER_KEY = "asyncInfoConverter"; @@ -60,7 +61,6 @@ public ParserContext(NodeIdGenerator idGenerator, RuleFlowProcessFactory factory this.context = context; this.operationIdFactory = operationIdFactory; this.asyncInfoResolver = asyncInfoResolver; - this.generatedFiles = new ArrayList<>(); } public void add(StateHandler stateHandler) { @@ -114,4 +114,12 @@ public void setCompensation() { public KogitoBuildContext getContext() { return context; } + + public void addValidationError(String message) { + validationErrors.add(message); + } + + public Collection validationErrors() { + return validationErrors; + } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java index 8fd92f82f35..49e16462cc3 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java @@ -20,24 +20,28 @@ import java.io.IOException; import java.io.Reader; -import java.io.UncheckedIOException; import java.net.URI; import java.net.URL; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import org.jbpm.process.core.datatype.impl.type.ObjectDataType; +import org.jbpm.process.core.validation.ProcessValidationError; +import org.jbpm.process.core.validation.impl.ProcessValidationErrorImpl; import org.jbpm.ruleflow.core.Metadata; import org.jbpm.ruleflow.core.RuleFlowProcessFactory; +import org.jbpm.ruleflow.core.validation.RuleFlowProcessValidator; import org.jbpm.workflow.core.WorkflowModelValidator; import org.kie.kogito.codegen.api.GeneratedInfo; import org.kie.kogito.codegen.api.context.KogitoBuildContext; import org.kie.kogito.internal.process.runtime.KogitoWorkflowProcess; import org.kie.kogito.internal.utils.ConversionUtils; import org.kie.kogito.jackson.utils.ObjectMapperFactory; +import org.kie.kogito.process.validation.ValidationException; import org.kie.kogito.serverless.workflow.SWFConstants; import org.kie.kogito.serverless.workflow.extensions.OutputSchema; import org.kie.kogito.serverless.workflow.operationid.WorkflowOperationIdFactoryProvider; @@ -121,7 +125,7 @@ private ServerlessWorkflowParser(Workflow workflow, KogitoBuildContext context) } private GeneratedInfo parseProcess() { - WorkflowValidator.validateStart(workflow); + RuleFlowProcessFactory factory = RuleFlowProcessFactory.createProcess(workflow.getId(), !workflow.isKeepActive()) .name(workflow.getName() == null ? DEFAULT_NAME : workflow.getName()) .version(workflow.getVersion() == null ? DEFAULT_VERSION : workflow.getVersion()) @@ -134,6 +138,7 @@ private GeneratedInfo parseProcess() { .type(KogitoWorkflowProcess.SW_TYPE); ParserContext parserContext = new ParserContext(idGenerator, factory, context, WorkflowOperationIdFactoryProvider.getFactory(context.getApplicationProperty(WorkflowOperationIdFactoryProvider.PROPERTY_NAME))); + WorkflowValidator.validateStart(workflow, parserContext); modelValidator(parserContext, Optional.ofNullable(workflow.getDataInputSchema())).ifPresent(factory::inputValidator); modelValidator(parserContext, ServerlessWorkflowUtils.getExtension(workflow, OutputSchema.class).map(OutputSchema::getOutputSchema)).ifPresent(factory::outputValidator); loadConstants(factory, parserContext); @@ -145,6 +150,7 @@ private GeneratedInfo parseProcess() { handlers.forEach(StateHandler::handleState); handlers.forEach(StateHandler::handleTransitions); handlers.forEach(StateHandler::handleConnections); + if (parserContext.isCompensation()) { factory.metaData(Metadata.COMPENSATION, true); factory.metaData(Metadata.COMPENSATE_WHEN_ABORTED, true); @@ -170,8 +176,13 @@ private GeneratedInfo parseProcess() { if (!annotations.isEmpty()) { factory.metaData(Metadata.ANNOTATIONS, annotations); } - - return new GeneratedInfo<>(factory.validate().getProcess(), parserContext.generatedFiles()); + factory.link(); + List errors = RuleFlowProcessValidator.getInstance().validateProcess(factory.getProcess(), new ArrayList<>()); + parserContext.validationErrors().forEach(m -> errors.add(new ProcessValidationErrorImpl(factory.getProcess(), m))); + if (!errors.isEmpty()) { + throw new ValidationException(factory.getProcess().getId(), errors); + } + return new GeneratedInfo<>(factory.getProcess(), parserContext.generatedFiles()); } private Optional modelValidator(ParserContext parserContext, Optional schema) { @@ -194,7 +205,8 @@ private void loadConstants(RuleFlowProcessFactory factory, ParserContext parserC try { constants.setConstantsDef(ObjectMapperFactory.get().readValue(readBytes(constants.getRefValue(), workflow, parserContext), JsonNode.class)); } catch (IOException e) { - throw new UncheckedIOException("Invalid file " + constants.getRefValue(), e); + parserContext.addValidationError("Invalid file " + constants.getRefValue() + e); + return; } } factory.metaData(Metadata.CONSTANTS, constants.getConstantsDef()); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java index 5ecad8e9429..fc0e7468067 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java @@ -34,16 +34,21 @@ public class ActionNodeUtils { return embeddedSubProcess.actionNode(context.newId()).name(functionDef.getName()); } - public static void checkArgs(FunctionRef functionRef, String... requiredArgs) { + public static boolean checkArgs(ParserContext context, FunctionRef functionRef, String... requiredArgs) { JsonNode args = functionRef.getArguments(); + boolean isOk = true; if (args == null) { - throw new IllegalArgumentException("Arguments cannot be null for function " + functionRef.getRefName()); - } - for (String arg : requiredArgs) { - if (!args.has(arg)) { - throw new IllegalArgumentException("Missing mandatory " + arg + " argument for function " + functionRef.getRefName()); + context.addValidationError("Arguments cannot be null for function " + functionRef.getRefName()); + isOk = false; + } else { + for (String arg : requiredArgs) { + if (!args.has(arg)) { + context.addValidationError("Missing mandatory " + arg + " argument for function " + functionRef.getRefName()); + isOk = false; + } } } + return isOk; } private ActionNodeUtils() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java index ee3fbdb47a6..6f62d751b44 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java @@ -19,7 +19,10 @@ package org.kie.kogito.serverless.workflow.parser.handlers; import java.util.Map; -import java.util.function.Function; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.kie.kogito.serverless.workflow.parser.ParserContext; import io.serverlessworkflow.api.functions.FunctionDefinition; import io.serverlessworkflow.api.functions.FunctionDefinition.Type; @@ -28,38 +31,47 @@ public class ActionResourceFactory { - private static final Map> map = + private static final Map, ActionResource>> map = Map.of(FunctionDefinition.Type.REST, ActionResourceFactory::justOperation, FunctionDefinition.Type.ASYNCAPI, ActionResourceFactory::justOperation, FunctionDefinition.Type.RPC, ActionResourceFactory::withService); - private static ActionResource justOperation(String operationStr) { - String[] tokens = getTokens(operationStr, 2); + private static ActionResource justOperation(String operationStr, Optional context) { + String[] tokens = getTokens(operationStr, 2, context); return new ActionResource(tokens[0], tokens[1], null); } - private static ActionResource withService(String operationStr) { - String[] tokens = getTokens(operationStr, 3); + private static ActionResource withService(String operationStr, Optional context) { + String[] tokens = getTokens(operationStr, 3, context); return new ActionResource(tokens[0], tokens[2], tokens[1]); } - private static String[] getTokens(String operationStr, int expectedTokens) { + private static String[] getTokens(String operationStr, int expectedTokens, Optional context) { String[] tokens = operationStr.split(OPERATION_SEPARATOR); if (tokens.length != expectedTokens) { - throw new IllegalArgumentException(String.format("%s should have just %d %s", operationStr, expectedTokens - 1, OPERATION_SEPARATOR)); + String msg = String.format("%s should have just %d %s", operationStr, expectedTokens - 1, OPERATION_SEPARATOR); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } return tokens; } - public static ActionResource getActionResource(FunctionDefinition function) { - Function factory = map.get(function.getType()); + public static ActionResource getActionResource(FunctionDefinition function, Optional context) { + BiFunction, ActionResource> factory = map.get(function.getType()); if (factory == null) { - throw new UnsupportedOperationException(function.getType() + " does not support action resources"); + String msg = function.getType() + " does not support action resources"; + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new UnsupportedOperationException(msg); + }); } String operation = function.getOperation(); if (operation == null) { - throw new IllegalArgumentException("operation string must not be null for function " + function.getName()); + String msg = "operation string must not be null for function " + function.getName(); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } - return factory.apply(operation); + return factory.apply(operation, context); } private ActionResourceFactory() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java index 755f00e21aa..49deb2d8e29 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java @@ -165,7 +165,7 @@ private MakeNodeResult processActionFilter(RuleFlowNodeContainerFactory em return filterAndMergeNode(embeddedSubProcess, collectVar, fromExpr, resultExpr, toExpr, useData, shouldMerge, (factory, inputVar, outputVar) -> addActionMetadata(getActionNode(factory, action.getSubFlowRef(), inputVar, outputVar), action)); } else { - throw new IllegalArgumentException("Action node " + action.getName() + " of state " + state.getName() + " does not have function or event defined"); + return faultyNodeResult(embeddedSubProcess, "Action node " + action.getName() + " of state " + state.getName() + " does not have function or event defined"); } } @@ -199,7 +199,7 @@ private TimerNodeFactory createTimerNode(RuleFlowNodeContainerFactory f .findFirst() .map(functionDef -> fromFunctionDefinition(embeddedSubProcess, functionDef, functionRef, varInfo)) .or(() -> fromPredefinedFunction(embeddedSubProcess, functionRef, varInfo)) - .orElseThrow(() -> new IllegalArgumentException("Cannot find function " + functionName)); + .orElseGet(() -> faultyNode(embeddedSubProcess, "Cannot find function " + functionName)); } private Stream getFunctionDefStream() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java index 5c9caf6f451..e0dbbf5ee99 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -148,7 +149,7 @@ public void handleEnd() { private void handleCompensation(RuleFlowNodeContainerFactory factory) { StateHandler compensation = parserContext.getStateHandler(state.getCompensatedBy()); if (compensation == null) { - throw new IllegalArgumentException("State " + getState().getName() + " refers to a compensation " + state.getCompensatedBy() + " which cannot be found"); + parserContext.addValidationError("State " + getState().getName() + " refers to a compensation " + state.getCompensatedBy() + " which cannot be found"); } parserContext.setCompensation(); WorkflowElementIdentifier eventCompensationId = parserContext.newId(); @@ -164,7 +165,7 @@ private void handleCompensation(RuleFlowNodeContainerFactory factory) { compensation = parserContext.getStateHandler(compensation); while (compensation != null) { if (!compensation.usedForCompensation()) { - throw new IllegalArgumentException( + parserContext.addValidationError( "Compensation state can only have transition to other compensation state. State " + compensation.getState().getName() + " is not used for compensation"); } lastNodeId = handleCompensation(embeddedSubProcess, compensation); @@ -177,7 +178,7 @@ private void handleCompensation(RuleFlowNodeContainerFactory factory) { private WorkflowElementIdentifier handleCompensation(RuleFlowNodeContainerFactory embeddedSubProcess, StateHandler compensation) { if (compensation.getState().getCompensatedBy() != null) { - throw new IllegalArgumentException("Serverless workflow specification forbids nested compensations, hence state " + compensation.getState().getName() + " is not valid"); + parserContext.addValidationError("Serverless workflow specification forbids nested compensations, hence state " + compensation.getState().getName() + " is not valid"); } compensation.handleState(embeddedSubProcess); Transition transition = compensation.getState().getTransition(); @@ -256,11 +257,13 @@ private boolean hasCode(ErrorDefinition errorDef) { protected final Collection getErrorDefinitions(Error error) { Errors errors = workflow.getErrors(); if (errors == null) { - throw new IllegalArgumentException("workflow should contain errors property"); + parserContext.addValidationError("workflow should contain errors property"); + return Collections.emptyList(); } List errorDefs = errors.getErrorDefs(); if (errorDefs == null) { - throw new IllegalArgumentException("workflow errors property must contain errorDefs property"); + parserContext.addValidationError("workflow errors property must contain errorDefs property"); + return Collections.emptyList(); } if (error.getErrorRef() != null) { @@ -268,15 +271,16 @@ protected final Collection getErrorDefinitions(Error error) { } else if (error.getErrorRefs() != null) { return getErrorsDefinitions(errorDefs, error.getErrorRefs()); } else { - throw new IllegalArgumentException("state errors should contain either errorRef or errorRefs property"); + parserContext.addValidationError("state errors should contain either errorRef or errorRefs property"); + return Collections.emptyList(); } } private Collection getErrorsDefinitions(List errorDefs, List errorRefs) { Collection result = new ArrayList<>(); for (String errorRef : errorRefs) { - result.add(errorDefs.stream().filter(errorDef -> errorDef.getName().equals(errorRef) && hasCode(errorDef)).findAny() - .orElseThrow(() -> new IllegalArgumentException("Cannot find any error definition for errorRef" + errorRef))); + errorDefs.stream().filter(errorDef -> errorDef.getName().equals(errorRef) && hasCode(errorDef)).findAny().ifPresentOrElse(result::add, + () -> parserContext.addValidationError("Cannot find any error definition for errorRef" + errorRef)); } return result; } @@ -382,7 +386,12 @@ protected final void handleTransition(RuleFlowNodeContainerFactory factory targetState.connectSource(actionNode); } } else { - callback.ifPresent(HandleTransitionCallBack::onEmptyTarget); + callback.ifPresentOrElse(HandleTransitionCallBack::onEmptyTarget, + () -> { + if (transition != null) { + parserContext.addValidationError(String.format("There is no state for transition %s originated in %s", transition.getNextState(), state.getName())); + } + }); } } @@ -582,4 +591,13 @@ default void onEmptyTarget() { protected static WorkflowElementIdentifier concatId(WorkflowElementIdentifier start, WorkflowElementIdentifier end) { return WorkflowElementIdentifierFactory.fromExternalFormat(start.toSanitizeString() + "_" + end.toSanitizeString()); } + + public MakeNodeResult faultyNodeResult(RuleFlowNodeContainerFactory embeddedSubProcess, String message) { + return new MakeNodeResult(faultyNode(embeddedSubProcess, message)); + } + + public NodeFactory faultyNode(RuleFlowNodeContainerFactory embeddedSubProcess, String message) { + parserContext.addValidationError(message); + return embeddedSubProcess.actionNode(parserContext.newId()); + } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java index 581bf0d410f..bbf9536938e 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java @@ -58,7 +58,7 @@ public boolean usedForCompensation() { @Override public MakeNodeResult makeNode(RuleFlowNodeContainerFactory factory) { - validateConditions(state, workflow); + validateConditions(state, workflow, parserContext); SplitFactory splitNode = factory.splitNode(parserContext.newId()); splitNode = isDataBased() ? exclusiveSplitNode(splitNode) : eventBasedExclusiveSplitNode(splitNode); return new MakeNodeResult(splitNode.name(state.getName())); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java index 09cd5c26773..11f07410eb2 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java @@ -50,12 +50,11 @@ public class SwitchValidator { private SwitchValidator() { } - public static void validateConditions(SwitchState state, Workflow workflow) { + public static void validateConditions(SwitchState state, Workflow workflow, ParserContext context) { if (state.getDataConditions().isEmpty() && state.getEventConditions().isEmpty()) { - throw new IllegalArgumentException(String.format(CONDITIONS_NOT_FOUND_ERROR, state.getName(), workflow.getName())); - } - if (!state.getDataConditions().isEmpty() && !state.getEventConditions().isEmpty()) { - throw new IllegalArgumentException(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, state.getName(), workflow.getName())); + context.addValidationError(String.format(CONDITIONS_NOT_FOUND_ERROR, state.getName(), workflow.getName())); + } else if (!state.getDataConditions().isEmpty() && !state.getEventConditions().isEmpty()) { + context.addValidationError(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, state.getName(), workflow.getName())); } } @@ -67,18 +66,17 @@ public static void validateDefaultCondition(DefaultConditionDefinition defaultCo if (transition != null) { String nextState = transition.getNextState(); if (nextState == null || nextState.isEmpty()) { - throw new IllegalArgumentException(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); - } - if (parserContext.getStateHandler(nextState) == null) { - throw new IllegalArgumentException(String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, nextState, state.getName(), workflow.getName())); + parserContext.addValidationError(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); + } else if (parserContext.getStateHandler(nextState) == null) { + parserContext.addValidationError(String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, nextState, state.getName(), workflow.getName())); } } else if (defaultCondition.getEnd() == null) { - throw new IllegalArgumentException(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); + parserContext.addValidationError(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); } if (!state.getEventConditions().isEmpty()) { String eventTimeout = resolveEventTimeout(state, workflow); if (eventTimeout == null) { - throw new IllegalArgumentException(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, state.getName(), workflow.getName())); + parserContext.addValidationError(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, state.getName(), workflow.getName())); } } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java index a0c5a15c031..82de55e0af7 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.serverless.workflow.parser.handlers.validation; +import org.kie.kogito.serverless.workflow.parser.ParserContext; + import io.serverlessworkflow.api.Workflow; import static org.kie.kogito.internal.utils.ConversionUtils.isEmpty; @@ -27,9 +29,9 @@ public class WorkflowValidator { private WorkflowValidator() { } - public static void validateStart(Workflow workflow) { + public static void validateStart(Workflow workflow, ParserContext context) { if (workflow.getStart() == null || isEmpty(workflow.getStart().getStateName())) { - throw new IllegalArgumentException("Workflow does not define a starting state"); + context.addValidationError("Workflow does not define a starting state"); } } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java index c8a8cb74ab8..b9301b5b2bd 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java @@ -48,7 +48,9 @@ public String type() { public NodeFactory getActionNode(Workflow workflow, ParserContext context, RuleFlowNodeContainerFactory embeddedSubProcess, FunctionDefinition functionDef, FunctionRef functionRef, VariableInfo varInfo) { - checkArgs(functionRef, SCRIPT); + if (!checkArgs(context, functionRef, SCRIPT)) { + return embeddedSubProcess.actionNode(context.newId()); + } String lang = trimCustomOperation(functionDef); if (PYTHON.equalsIgnoreCase(lang)) { return addFunctionArgs(workflow, diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java index 9f8d597d736..3c0ec2936a1 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java @@ -20,7 +20,9 @@ import org.jbpm.ruleflow.core.RuleFlowNodeContainerFactory; import org.jbpm.ruleflow.core.factory.ActionNodeFactory; +import org.jbpm.ruleflow.core.factory.NodeFactory; import org.kie.kogito.serverless.workflow.parser.FunctionTypeHandlerFactory; +import org.kie.kogito.serverless.workflow.parser.ParserContext; import org.kie.kogito.serverless.workflow.parser.VariableInfo; import org.kie.kogito.serverless.workflow.suppliers.SysoutActionSupplier; @@ -35,13 +37,25 @@ public class SysOutTypeHandler extends ActionTypeHandler { public static final String SYSOUT_TYPE = "sysout"; public static final String SYSOUT_TYPE_PARAM = "message"; + @Override + public NodeFactory getActionNode(Workflow workflow, + ParserContext context, + RuleFlowNodeContainerFactory embeddedSubProcess, + FunctionDefinition functionDef, + FunctionRef functionRef, + VariableInfo varInfo) { + if (!checkArgs(context, functionRef, SYSOUT_TYPE_PARAM)) { + return embeddedSubProcess.actionNode(context.newId()); + } + return super.getActionNode(workflow, context, embeddedSubProcess, functionDef, functionRef, varInfo); + } + @Override protected > ActionNodeFactory fillAction(Workflow workflow, ActionNodeFactory node, FunctionDefinition functionDef, FunctionRef functionRef, VariableInfo varInfo) { - checkArgs(functionRef, SYSOUT_TYPE_PARAM); return node.action(new SysoutActionSupplier(workflow.getExpressionLang(), functionRef.getArguments().get(SYSOUT_TYPE_PARAM).asText(), varInfo.getInputVar(), FunctionTypeHandlerFactory.trimCustomOperation(functionDef))); } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java index aed7bb78acd..1d2221122f2 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.kie.kogito.serverless.workflow.parser.ParserContext; import org.kie.kogito.serverless.workflow.parser.handlers.StateHandler; +import org.mockito.ArgumentCaptor; import io.serverlessworkflow.api.Workflow; import io.serverlessworkflow.api.defaultdef.DefaultConditionDefinition; @@ -34,7 +35,7 @@ import io.serverlessworkflow.api.switchconditions.EventCondition; import io.serverlessworkflow.api.transitions.Transition; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThat; import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.CONDITIONS_NOT_FOUND_ERROR; import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR; import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.EVENT_TIMEOUT_REQUIRED_ERROR; @@ -43,6 +44,7 @@ import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; class SwitchValidatorTest { @@ -63,26 +65,30 @@ void setUp() { @Test void validateConditionsNoConditionsFoundError() { - assertThatThrownBy(() -> SwitchValidator.validateConditions(switchState, workflow)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(CONDITIONS_NOT_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateConditions(switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(CONDITIONS_NOT_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test void validateConditionsBothConditionsFoundError() { switchState.getDataConditions().add(mock(DataCondition.class)); switchState.getEventConditions().add(mock(EventCondition.class)); - assertThatThrownBy(() -> SwitchValidator.validateConditions(switchState, workflow)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateConditions(switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test void validateDefaultConditionTransitionWithoutNextError() { DefaultConditionDefinition defaultCondition = mockDefaultConditionWithTransition(); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test @@ -90,17 +96,19 @@ void validateDefaultConditionTransitionNextStateNotFoundError() { DefaultConditionDefinition defaultCondition = mockDefaultConditionWithTransition(); Transition transition = defaultCondition.getTransition(); doReturn(NEXT_STATE).when(transition).getNextState(); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, NEXT_STATE, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format((String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, NEXT_STATE, SWITCH_STATE_NAME, WORKFLOW_NAME)))); } @Test void validateDefaultConditionWithoutTransitionAndEndIsNullError() { DefaultConditionDefinition defaultCondition = mock(DefaultConditionDefinition.class); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test @@ -111,9 +119,10 @@ void validateDefaultConditionWithEventConditionsTransitionButTimeoutNotSetError( doReturn(NEXT_STATE).when(transition).getNextState(); StateHandler stateHandler = mock(StateHandler.class); doReturn(stateHandler).when(parserContext).getStateHandler(NEXT_STATE); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME); } @Test @@ -122,9 +131,10 @@ void validateDefaultConditionWithEventConditionsEndButTimeoutNotSetError() { DefaultConditionDefinition defaultCondition = mock(DefaultConditionDefinition.class); End end = mock(End.class); doReturn(end).when(defaultCondition).getEnd(); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } private SwitchState mockSwitchState() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java index 1cc91bde1a2..5cf827f291c 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java @@ -29,6 +29,7 @@ import org.kie.api.event.process.ProcessCompletedEvent; import org.kie.kogito.internal.process.event.DefaultKogitoProcessEventListener; import org.kie.kogito.process.Process; +import org.kie.kogito.process.validation.ValidationException; import org.kie.kogito.serverless.workflow.actions.SysoutAction; import org.kie.kogito.serverless.workflow.actions.WorkflowLogLevel; import org.kie.kogito.serverless.workflow.fluent.FunctionBuilder; @@ -260,7 +261,7 @@ void testMissingMessageException() { try (StaticWorkflowApplication application = StaticWorkflowApplication.create()) { Workflow workflow = workflow("Testing logs").function(FunctionBuilder.def(funcName, Type.CUSTOM, SysOutTypeHandler.SYSOUT_TYPE)).start(operation().action( call(funcName))).end().build(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("message"); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("message"); } } @@ -270,7 +271,7 @@ void testNoArgsMessageException() { try (StaticWorkflowApplication application = StaticWorkflowApplication.create()) { Workflow workflow = workflow("Testing logs").function(FunctionBuilder.def(funcName, Type.CUSTOM, SysOutTypeHandler.SYSOUT_TYPE)).start(operation().action( call(funcName, null))).end().build(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("Arguments cannot be null"); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("Arguments cannot be null"); } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java index 4309f18156c..bed9c81d532 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.kie.kogito.process.validation.ValidationException; import org.kie.kogito.serverless.workflow.actions.SysoutAction; import org.kie.kogito.serverless.workflow.utils.ServerlessWorkflowUtils; import org.kie.kogito.serverless.workflow.utils.WorkflowFormat; @@ -50,6 +51,7 @@ import ch.qos.logback.core.read.ListAppender; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; import static org.kie.kogito.serverless.workflow.utils.ServerlessWorkflowUtils.getWorkflow; import static org.kie.kogito.serverless.workflow.utils.ServerlessWorkflowUtils.writeWorkflow; @@ -107,6 +109,17 @@ void interpolationFile(String fileName, WorkflowFormat format) throws IOExceptio } } + @Test + void testValidationError() throws IOException { + try (Reader reader = new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream("wrong.sw.json")); + StaticWorkflowApplication application = StaticWorkflowApplication.create()) { + Workflow workflow = getWorkflow(reader, WorkflowFormat.JSON); + ValidationException validationException = catchThrowableOfType(() -> application.process(workflow), ValidationException.class); + assertThat(validationException.getErrors()).hasSizeGreaterThanOrEqualTo(4); + assertThat(validationException).hasMessageContaining("error").hasMessageContaining("function").hasMessageContaining("connect").hasMessageContaining("transition"); + } + } + private static Stream interpolationParameters() { return Stream.of(Arguments.of("interpolation.sw.json", WorkflowFormat.JSON), Arguments.of("interpolation.sw.yml", WorkflowFormat.YAML)); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json new file mode 100644 index 00000000000..41c48f309a9 --- /dev/null +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json @@ -0,0 +1,57 @@ +{ + "id": "hello", + "version": "1.0", + "name": "Hello Workflow", + "description": "Inject Hello World", + "start": "Hello", + "functions": [ + { + "name": "getIP", + "type": "custom", + "operation": "rest:get:https://ipinfo.io/json" + }, + { + "name": "pushData", + "type": "custom", + "operation": "rest:post:https://httpbin.org/post" + }, + { + "name": "logInfo", + "type": "custom", + "operation": "sysout:INFO" + } + ], + "errors":[], + "states": [ + { + "name": "Hello", + "type": "inject", + "data": { + "greeting": "Hello World", + "mantra": "Serverless Workflow is awesome!" + }, + "end": true + }, + { + "name": "Get public IP", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "getIPO" + }, + "actionDataFilter": { + "toStateData": ".ip_info" + } + } + ], + "onErrors": [ + { + "errorRef": "notAvailable", + "transition": "logError" + } + ], + "transition": "push_host_data" + } + ] +} \ No newline at end of file diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java b/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java index 0fed7511b53..372f86edcc3 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.serverless.workflow.executor; +import java.util.Optional; + import org.kie.kogito.serverless.workflow.parser.handlers.ActionResource; import org.kie.kogito.serverless.workflow.parser.handlers.ActionResourceFactory; import org.kie.kogito.serverless.workflow.utils.RPCWorkflowUtils; @@ -36,7 +38,7 @@ public void register(StaticWorkflowApplication application, Workflow workflow) { } private void registerHandler(StaticWorkflowApplication application, FunctionDefinition function) { - ActionResource actionResource = ActionResourceFactory.getActionResource(function); + ActionResource actionResource = ActionResourceFactory.getActionResource(function, Optional.empty()); application.registerHandler(new StaticRPCWorkItemHandler(RPCWorkflowUtils.getRPCClassName(actionResource.getService()))); } } From 1d7107cf26eac24cab266d228aa2bddea8777ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pere=20Fern=C3=A1ndez?= Date: Fri, 1 Nov 2024 14:46:50 +0100 Subject: [PATCH 10/16] [incubator-kie-issues-1597] Enforce authentication in User Tasks rest endpoints (#3758) * [incubator-kie-issues-1597] Enforce authentication in User Tasks rest endpoints * Added configs * - formatting * - fix config --- .../org/kie/kogito/auth/IdentityProvider.java | 2 +- .../kogito/auth/IdentityProviderFactory.java | 40 +++++++++++ .../impl/IdentityProviderFactoryImpl.java | 52 ++++++++++++++ .../kogito/auth/impl/KogitoAuthConfig.java | 41 +++++++++++ .../impl/IdentityProviderFactoryImplTest.java | 70 +++++++++++++++++++ .../RestResourceQuarkusTemplate.java | 14 ++-- .../RestResourceSpringTemplate.java | 15 ++-- .../RestResourceWorkItemQuarkusTemplate.java | 15 ++-- .../RestResourceWorkItemSpringTemplate.java | 15 ++-- .../RestResourceUserTaskQuarkusTemplate.java | 44 ++++++------ .../RestResourceUserTaskSpringTemplate.java | 37 +++++----- ...uarkusIdentityProviderFactoryProducer.java | 56 +++++++++++++++ .../config/KogitoAuthRuntimeConfig.java | 41 +++++++++++ .../quarkus/config/KogitoRuntimeConfig.java | 6 ++ ...SpringIdentityProviderFactoryProducer.java | 43 ++++++++++++ 15 files changed, 414 insertions(+), 77 deletions(-) create mode 100644 api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviderFactory.java create mode 100644 api/kogito-api/src/main/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImpl.java create mode 100644 api/kogito-api/src/main/java/org/kie/kogito/auth/impl/KogitoAuthConfig.java create mode 100644 api/kogito-api/src/test/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImplTest.java create mode 100644 quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/auth/QuarkusIdentityProviderFactoryProducer.java create mode 100644 quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoAuthRuntimeConfig.java create mode 100644 springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/spring/auth/SpringIdentityProviderFactoryProducer.java diff --git a/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProvider.java b/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProvider.java index 1bbbdc6719e..5a837667ac3 100644 --- a/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProvider.java +++ b/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProvider.java @@ -27,7 +27,7 @@ public interface IdentityProvider { /** - * Returns name assigned to the current context, usually refers to user name + * Returns name assigned to the current context, usually refers to the username * * @return assigned name taken from security context */ diff --git a/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviderFactory.java b/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviderFactory.java new file mode 100644 index 00000000000..0d44e9274a1 --- /dev/null +++ b/api/kogito-api/src/main/java/org/kie/kogito/auth/IdentityProviderFactory.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.auth; + +import java.util.Collection; + +/** + * Factory that resolves the {@link IdentityProvider} + */ +public interface IdentityProviderFactory { + + /** + * Enables (true) using the application security context when resolving current User Identity. Defaults to false. + */ + String KOGITO_SECURITY_AUTH_ENABLED = "kogito.security.auth.enabled"; + + /** + * Comma-separated list of roles that allow identity impersonation when resolving the actual User Identity. + */ + String KOGITO_SECURITY_AUTH_IMPERSONATION_ALLOWED_FOR_ROLES = "kogito.security.auth.impersonation.allowed-for-roles"; + + IdentityProvider getOrImpersonateIdentity(String user, Collection roles); +} diff --git a/api/kogito-api/src/main/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImpl.java b/api/kogito-api/src/main/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImpl.java new file mode 100644 index 00000000000..655ed5daa38 --- /dev/null +++ b/api/kogito-api/src/main/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImpl.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.auth.impl; + +import java.util.Collection; + +import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.auth.IdentityProviderFactory; +import org.kie.kogito.auth.IdentityProviders; + +public class IdentityProviderFactoryImpl implements IdentityProviderFactory { + + private final IdentityProvider identityProvider; + private final KogitoAuthConfig config; + + public IdentityProviderFactoryImpl(IdentityProvider identityProvider, KogitoAuthConfig config) { + this.identityProvider = identityProvider; + this.config = config; + } + + @Override + public IdentityProvider getOrImpersonateIdentity(String user, Collection roles) { + + if (!config.isEnabled()) { + return IdentityProviders.of(user, roles); + } + + Collection identityRoles = identityProvider.getRoles(); + if (config.getRolesThatAllowImpersonation().stream().anyMatch(identityRoles::contains)) { + return IdentityProviders.of(user, roles); + } + + return identityProvider; + } +} diff --git a/api/kogito-api/src/main/java/org/kie/kogito/auth/impl/KogitoAuthConfig.java b/api/kogito-api/src/main/java/org/kie/kogito/auth/impl/KogitoAuthConfig.java new file mode 100644 index 00000000000..c66687fd029 --- /dev/null +++ b/api/kogito-api/src/main/java/org/kie/kogito/auth/impl/KogitoAuthConfig.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.auth.impl; + +import java.util.Collection; + +public class KogitoAuthConfig { + + private final boolean enabled; + private final Collection rolesThatAllowImpersonation; + + public KogitoAuthConfig(boolean enabled, Collection rolesThatAllowImpersonation) { + this.enabled = enabled; + this.rolesThatAllowImpersonation = rolesThatAllowImpersonation; + } + + public boolean isEnabled() { + return enabled; + } + + public Collection getRolesThatAllowImpersonation() { + return rolesThatAllowImpersonation; + } +} diff --git a/api/kogito-api/src/test/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImplTest.java b/api/kogito-api/src/test/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImplTest.java new file mode 100644 index 00000000000..967d85a4789 --- /dev/null +++ b/api/kogito-api/src/test/java/org/kie/kogito/auth/impl/IdentityProviderFactoryImplTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.auth.impl; + +import java.util.Collection; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.kie.kogito.auth.IdentityProviders; + +public class IdentityProviderFactoryImplTest { + + private static final String KOGITO_IDENTITY_USER = "john"; + private static final Collection KOGITO_IDENTITY_ROLES = List.of("IT", "task-operator"); + private static final Collection KOGITO_IDENTITY_IMPERSONATOR_ROLES = List.of("root", "task-admin"); + private static final String TEST_USER = "katty"; + private static final Collection TEST_ROLES = List.of("HR", "task-operator"); + + @Test + public void testResolveIdentityWithAuthDisabled() { + KogitoAuthConfig config = new KogitoAuthConfig(false, KOGITO_IDENTITY_IMPERSONATOR_ROLES); + IdentityProviderFactoryImpl identityProviderFactory = new IdentityProviderFactoryImpl(IdentityProviders.of(KOGITO_IDENTITY_USER, KOGITO_IDENTITY_ROLES), config); + + Assertions.assertThat(identityProviderFactory.getOrImpersonateIdentity(TEST_USER, TEST_ROLES)) + .isNotNull() + .hasFieldOrPropertyWithValue("name", TEST_USER) + .matches(identityProvider -> identityProvider.getRoles().containsAll(TEST_ROLES)); + } + + @Test + public void testResolveIdentityWithAuthEnabled() { + KogitoAuthConfig config = new KogitoAuthConfig(true, KOGITO_IDENTITY_IMPERSONATOR_ROLES); + IdentityProviderFactoryImpl identityProviderFactory = new IdentityProviderFactoryImpl(IdentityProviders.of(KOGITO_IDENTITY_USER, KOGITO_IDENTITY_ROLES), config); + + Assertions.assertThat(identityProviderFactory.getOrImpersonateIdentity(TEST_USER, TEST_ROLES)) + .isNotNull() + .hasFieldOrPropertyWithValue("name", KOGITO_IDENTITY_USER) + .matches(identityProvider -> identityProvider.getRoles().containsAll(KOGITO_IDENTITY_ROLES)); + } + + @Test + public void testResolveImpersonatedIdentityWithAuthEnabled() { + KogitoAuthConfig config = new KogitoAuthConfig(true, KOGITO_IDENTITY_IMPERSONATOR_ROLES); + IdentityProviderFactoryImpl identityProviderFactory = new IdentityProviderFactoryImpl(IdentityProviders.of(KOGITO_IDENTITY_USER, KOGITO_IDENTITY_IMPERSONATOR_ROLES), config); + + Assertions.assertThat(identityProviderFactory.getOrImpersonateIdentity(TEST_USER, TEST_ROLES)) + .isNotNull() + .hasFieldOrPropertyWithValue("name", TEST_USER) + .matches(identityProvider -> identityProvider.getRoles().containsAll(TEST_ROLES)); + } + +} diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java index 0bbf0b10a6c..130628cd576 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java @@ -18,7 +18,6 @@ */ package com.myspace.demo; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -50,17 +49,11 @@ import org.jbpm.util.JsonSchemaUtil; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; -import org.kie.kogito.process.WorkItem; import org.kie.kogito.process.ProcessService; import org.kie.kogito.process.workitem.TaskModel; -import org.kie.kogito.auth.IdentityProvider; -import org.kie.kogito.auth.IdentityProviders; +import org.kie.kogito.auth.IdentityProviderFactory; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.usertask.model.Attachment; -import org.kie.kogito.usertask.model.AttachmentInfo; -import org.kie.kogito.usertask.model.Comment; - @Path("/$name$") public class $Type$Resource { @@ -69,6 +62,9 @@ public class $Type$Resource { @Inject ProcessService processService; + @Inject + IdentityProviderFactory identityProviderFactory; + @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @@ -144,7 +140,7 @@ public class $Type$Resource { public List getTasks_$name$(@PathParam("id") String id, @QueryParam("user") final String user, @QueryParam("group") final List groups) { - return processService.getWorkItems(process, id, SecurityPolicy.of(IdentityProviders.of(user, groups))) + return processService.getWorkItems(process, id, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups))) .orElseThrow(NotFoundException::new) .stream() .map($TaskModelFactory$::from) diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java index 190a38af3b8..0c5cb82f6fe 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java @@ -18,8 +18,6 @@ */ package com.myspace.demo; -import java.net.URI; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,17 +26,11 @@ import org.jbpm.util.JsonSchemaUtil; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; -import org.kie.kogito.process.WorkItem; import org.kie.kogito.process.ProcessService; import org.kie.kogito.process.workitem.TaskModel; -import org.kie.kogito.auth.IdentityProvider; -import org.kie.kogito.auth.IdentityProviders; +import org.kie.kogito.auth.IdentityProviderFactory; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.usertask.model.Attachment; -import org.kie.kogito.usertask.model.AttachmentInfo; -import org.kie.kogito.usertask.model.Comment; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -70,6 +62,9 @@ public class $Type$Resource { @Autowired ProcessService processService; + @Autowired + IdentityProviderFactory identityProviderFactory; + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "$documentation$", description = "$processInstanceDescription$") public ResponseEntity<$Type$Output> createResource_$name$(@RequestHeader HttpHeaders httpHeaders, @@ -128,7 +123,7 @@ public class $Type$Resource { public List getTasks_$name$(@PathVariable("id") String id, @RequestParam(value = "user", required = false) final String user, @RequestParam(value = "group", required = false) final List groups) { - return processService.getWorkItems(process, id, SecurityPolicy.of(IdentityProviders.of(user, groups))) + return processService.getWorkItems(process, id, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups))) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)) .stream() .map($TaskModelFactory$::from) diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemQuarkusTemplate.java index 2bf6dd3bc69..cff68fad0fb 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemQuarkusTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemQuarkusTemplate.java @@ -25,7 +25,6 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; -import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; @@ -47,7 +46,7 @@ public Response signal(@PathParam("id") final String id, @QueryParam("user") final String user, @QueryParam("group") final List groups, @Context UriInfo uriInfo) { - return processService.signalWorkItem(process, id, "$taskName$", SecurityPolicy.of(user, groups)) + return processService.signalWorkItem(process, id, "$taskName$", SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups))) .map(task -> Response .created(uriInfo.getAbsolutePathBuilder().path(task.getId()).build()) .entity(task.getResults()) @@ -65,7 +64,7 @@ public Response signal(@PathParam("id") final String id, @QueryParam("user") final String user, @QueryParam("group") final List groups, final $TaskOutput$ model) { - return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(user, groups), model) + return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), model) .orElseThrow(NotFoundException::new); } @@ -77,7 +76,7 @@ public Response signal(@PathParam("id") final String id, @QueryParam("user") final String user, @QueryParam("group") final List groups, final $TaskOutput$ model) { - return processService.setWorkItemOutput(process, id, taskId, SecurityPolicy.of(user, groups), model, $TaskOutput$::fromMap) + return processService.setWorkItemOutput(process, id, taskId, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), model, $TaskOutput$::fromMap) .orElseThrow(NotFoundException::new); } @@ -92,7 +91,7 @@ public Response signal(@PathParam("id") final String id, @QueryParam("user") final String user, @QueryParam("group") final List groups, final $TaskOutput$ model) { - return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(user, groups), model) + return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), model) .orElseThrow(NotFoundException::new); } @@ -103,7 +102,7 @@ public Response signal(@PathParam("id") final String id, @PathParam("taskId") String taskId, @QueryParam("user") final String user, @QueryParam("group") final List groups) { - return processService.getWorkItem(process, id, taskId, SecurityPolicy.of(user, groups), $TaskModel$::from) + return processService.getWorkItem(process, id, taskId, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), $TaskModel$::from) .orElseThrow(NotFoundException::new); } @@ -115,7 +114,7 @@ public Response signal(@PathParam("id") final String id, @QueryParam("phase") @DefaultValue("abort") final String phase, @QueryParam("user") final String user, @QueryParam("group") final List groups) { - return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(user, groups), null) + return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), null) .orElseThrow(NotFoundException::new); } @@ -133,7 +132,7 @@ public Map getSchemaAndPhases(@PathParam("id") final String id, @PathParam("taskId") final String taskId, @QueryParam("user") final String user, @QueryParam("group") final List groups) { - return processService.getWorkItemSchemaAndPhases(process, id, taskId, "$taskName$", SecurityPolicy.of(user, groups)); + return processService.getWorkItemSchemaAndPhases(process, id, taskId, "$taskName$", SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups))); } } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemSpringTemplate.java index 7a5e65aea26..88659fb8004 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemSpringTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceWorkItemSpringTemplate.java @@ -22,7 +22,6 @@ import java.util.Map; import org.jbpm.util.JsonSchemaUtil; -import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; @@ -52,7 +51,7 @@ public ResponseEntity signal(@PathVariable("id") final String id, @RequestParam("group") final List groups, final UriComponentsBuilder uriComponentsBuilder) { - return processService.signalWorkItem(process, id, "$taskName$", SecurityPolicy.of(user, groups)) + return processService.signalWorkItem(process, id, "$taskName$", SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups))) .map(task -> ResponseEntity .created(uriComponentsBuilder .path("/$name$/{id}/$taskName$/{taskId}") @@ -69,7 +68,7 @@ public ResponseEntity signal(@PathVariable("id") final String id, @RequestParam("user") final String user, @RequestParam("group") final List groups, @RequestBody(required = false) final $TaskOutput$ model) { - return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(user, groups), model) + return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), model) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @@ -79,7 +78,7 @@ public ResponseEntity signal(@PathVariable("id") final String id, @RequestParam(value = "user", required = false) final String user, @RequestParam(value = "group", required = false) final List groups, @RequestBody(required = false) final $TaskOutput$ model) { - return processService.setWorkItemOutput(process, id, taskId, SecurityPolicy.of(user, groups), model, $TaskOutput$::fromMap) + return processService.setWorkItemOutput(process, id, taskId, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), model, $TaskOutput$::fromMap) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @@ -94,7 +93,7 @@ public ResponseEntity signal(@PathVariable("id") final String id, @RequestParam(value = "group", required = false) final List groups, @RequestBody(required = false) final $TaskOutput$ model) { - return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(user, groups), model) + return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), model) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @@ -104,7 +103,7 @@ public ResponseEntity signal(@PathVariable("id") final String id, @RequestParam(value = "user", required = false) final String user, @RequestParam(value = "group", required = false) final List groups) { - return processService.getWorkItem(process, id, taskId, SecurityPolicy.of(user, groups), $TaskModel$::from) + return processService.getWorkItem(process, id, taskId, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), $TaskModel$::from) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @@ -116,7 +115,7 @@ public ResponseEntity signal(@PathVariable("id") final String id, @RequestParam(value = "user", required = false) final String user, @RequestParam(value = "group", required = false) final List groups) { - return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(user, groups), null) + return processService.transitionWorkItem(process, id, taskId, phase, SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups)), null) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @@ -131,7 +130,7 @@ public Map getSchemaAndPhases(@PathVariable("id") final String i @RequestParam(value = "user", required = false) final String user, @RequestParam(value = "group", required = false) final List groups) { - return processService.getWorkItemSchemaAndPhases(process, id, taskId, "$taskName$", SecurityPolicy.of(user, groups)); + return processService.getWorkItemSchemaAndPhases(process, id, taskId, "$taskName$", SecurityPolicy.of(identityProviderFactory.getOrImpersonateIdentity(user, groups))); } } \ No newline at end of file diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java index 7369db60545..9970e3c28bb 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskQuarkusTemplate.java @@ -40,14 +40,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.Consumes; -import org.kie.kogito.auth.IdentityProviders; -import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.process.Process; -import org.kie.kogito.process.ProcessInstance; -import org.kie.kogito.process.ProcessInstanceReadMode; -import org.kie.kogito.process.WorkItem; -import org.kie.kogito.process.impl.Sig; -import org.kie.kogito.services.uow.UnitOfWorkExecutor; +import org.kie.kogito.auth.IdentityProviderFactory; import org.kie.kogito.usertask.UserTaskInstanceNotFoundException; import org.kie.kogito.usertask.UserTaskService; import org.kie.kogito.usertask.impl.json.SimpleDeserializationProblemHandler; @@ -75,6 +68,9 @@ public class UserTasksResource { @Inject UserTaskService userTaskService; + @Inject + IdentityProviderFactory identityProviderFactory; + @Inject ObjectMapper objectMapper; @@ -92,14 +88,14 @@ public void init() { @GET @Produces(MediaType.APPLICATION_JSON) public List list(@QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.list(IdentityProviders.of(user, groups)); + return userTaskService.list(identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @GET @Path("/{taskId}") @Produces(MediaType.APPLICATION_JSON) public UserTaskView find(@PathParam("taskId") String taskId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.getUserTaskInstance(taskId, IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); + return userTaskService.getUserTaskInstance(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } @POST @@ -111,7 +107,7 @@ public UserTaskView transition( @QueryParam("user") String user, @QueryParam("group") List groups, TransitionInfo transitionInfo) { - return userTaskService.transition(taskId, transitionInfo.getTransitionId(), transitionInfo.getData(), IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); + return userTaskService.transition(taskId, transitionInfo.getTransitionId(), transitionInfo.getData(), identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } @GET @@ -121,7 +117,7 @@ public Collection transition( @PathParam("taskId") String taskId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.allowedTransitions(taskId, IdentityProviders.of(user, groups)); + return userTaskService.allowedTransitions(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @PUT @@ -133,7 +129,7 @@ public UserTaskView setOutput( @QueryParam("group") List groups, String body) throws Exception { Map data = mapper.readValue(body, Map.class); - return userTaskService.setOutputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); + return userTaskService.setOutputs(taskId, data, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } @PUT @@ -145,7 +141,7 @@ public UserTaskView setInputs( @QueryParam("group") List groups, String body) throws Exception { Map data = mapper.readValue(body, Map.class); - return userTaskService.setInputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); + return userTaskService.setInputs(taskId, data, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } @GET @@ -155,7 +151,7 @@ public Collection getComments( @PathParam("taskId") String taskId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.getComments(taskId, IdentityProviders.of(user, groups)); + return userTaskService.getComments(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @POST @@ -169,7 +165,7 @@ public Comment addComment( CommentInfo commentInfo) { Comment comment = new Comment(null, user); comment.setContent(commentInfo.getComment()); - return userTaskService.addComment(taskId, comment, IdentityProviders.of(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); + return userTaskService.addComment(taskId, comment, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(UserTaskInstanceNotFoundException::new); } @GET @@ -180,7 +176,7 @@ public Comment getComment( @PathParam("commentId") String commentId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.getComment(taskId, commentId, IdentityProviders.of(user, groups)) + return userTaskService.getComment(taskId, commentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(() -> new UserTaskInstanceNotFoundException("Comment " + commentId + " not found")); } @@ -196,7 +192,7 @@ public Comment updateComment( CommentInfo commentInfo) { Comment comment = new Comment(commentId, user); comment.setContent(commentInfo.getComment()); - return userTaskService.updateComment(taskId, comment, IdentityProviders.of(user, groups)) + return userTaskService.updateComment(taskId, comment, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(UserTaskInstanceNotFoundException::new); } @@ -207,7 +203,7 @@ public Comment deleteComment( @PathParam("commentId") String commentId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.removeComment(taskId, commentId, IdentityProviders.of(user, groups)) + return userTaskService.removeComment(taskId, commentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(UserTaskInstanceNotFoundException::new); } @@ -218,7 +214,7 @@ public Collection getAttachments( @PathParam("taskId") String taskId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.getAttachments(taskId, IdentityProviders.of(user, groups)); + return userTaskService.getAttachments(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @POST @@ -233,7 +229,7 @@ public Attachment addAttachment( Attachment attachment = new Attachment(null, user); attachment.setName(attachmentInfo.getName()); attachment.setContent(attachmentInfo.getUri()); - return userTaskService.addAttachment(taskId, attachment, IdentityProviders.of(user, groups)) + return userTaskService.addAttachment(taskId, attachment, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(UserTaskInstanceNotFoundException::new); } @@ -250,7 +246,7 @@ public Attachment updateAttachment( Attachment attachment = new Attachment(attachmentId, user); attachment.setName(attachmentInfo.getName()); attachment.setContent(attachmentInfo.getUri()); - return userTaskService.updateAttachment(taskId, attachment, IdentityProviders.of(user, groups)) + return userTaskService.updateAttachment(taskId, attachment, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(UserTaskInstanceNotFoundException::new); } @@ -261,7 +257,7 @@ public Attachment deleteAttachment( @PathParam("attachmentId") String attachmentId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.removeAttachment(taskId, attachmentId, IdentityProviders.of(user, groups)) + return userTaskService.removeAttachment(taskId, attachmentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(UserTaskInstanceNotFoundException::new); } @@ -273,7 +269,7 @@ public Attachment getAttachment( @PathParam("attachmentId") String attachmentId, @QueryParam("user") String user, @QueryParam("group") List groups) { - return userTaskService.getAttachment(taskId, attachmentId, IdentityProviders.of(user, groups)) + return userTaskService.getAttachment(taskId, attachmentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(() -> new UserTaskInstanceNotFoundException("Attachment " + attachmentId + " not found")); } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java index ea5b3873931..83c8bf3bbb5 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/usertask/RestResourceUserTaskSpringTemplate.java @@ -24,7 +24,7 @@ import java.util.Collection; import org.jbpm.util.JsonSchemaUtil; -import org.kie.kogito.auth.IdentityProviders; +import org.kie.kogito.auth.IdentityProviderFactory; import org.kie.kogito.auth.SecurityPolicy; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; @@ -74,6 +74,9 @@ public class UserTasksResource { @Autowired UserTaskService userTaskService; + @Autowired + IdentityProviderFactory identityProviderFactory; + @Autowired ObjectMapper objectMapper; @@ -90,12 +93,12 @@ public void init() { @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List list(@RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.list(IdentityProviders.of(user, groups)); + return userTaskService.list(identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @GetMapping(value = "/{taskId}", produces = MediaType.APPLICATION_JSON_VALUE) public UserTaskView find(@PathVariable("taskId") String taskId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.getUserTaskInstance(taskId, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.getUserTaskInstance(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @PostMapping(value = "/{taskId}/transition") @@ -104,7 +107,7 @@ public UserTaskView transition( @RequestParam("user") String user, @RequestParam("group") List groups, @RequestBody TransitionInfo transitionInfo) { - return userTaskService.transition(taskId, transitionInfo.getTransitionId(), transitionInfo.getData(), IdentityProviders.of(user, groups)) + return userTaskService.transition(taskId, transitionInfo.getTransitionId(), transitionInfo.getData(), identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @@ -113,7 +116,7 @@ public Collection transition( @PathVariable("taskId") String taskId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.allowedTransitions(taskId, IdentityProviders.of(user, groups)); + return userTaskService.allowedTransitions(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @PutMapping("/{taskId}/outputs") @@ -123,7 +126,7 @@ public UserTaskView setOutput( @RequestParam("group") List groups, @RequestBody String body) throws Exception { Map data = mapper.readValue(body, Map.class); - return userTaskService.setOutputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.setOutputs(taskId, data, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @PutMapping("/{taskId}/inputs") @@ -133,7 +136,7 @@ public UserTaskView setInputs( @RequestParam("group") List groups, @RequestBody String body) throws Exception { Map data = mapper.readValue(body, Map.class); - return userTaskService.setInputs(taskId, data, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.setInputs(taskId, data, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @GetMapping("/{taskId}/comments") @@ -141,7 +144,7 @@ public Collection getComments( @PathVariable("taskId") String taskId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.getComments(taskId, IdentityProviders.of(user, groups)); + return userTaskService.getComments(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @PostMapping("/{taskId}/comments") @@ -152,7 +155,7 @@ public Comment addComment( @RequestBody CommentInfo commentInfo) { Comment comment = new Comment(null, user); comment.setContent(commentInfo.getComment()); - return userTaskService.addComment(taskId, comment, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.addComment(taskId, comment, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @GetMapping("/{taskId}/comments/{commentId}") @@ -161,7 +164,7 @@ public Comment getComment( @PathVariable("commentId") String commentId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.getComment(taskId, commentId, IdentityProviders.of(user, groups)) + return userTaskService.getComment(taskId, commentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Comment " + commentId + " not found")); } @@ -174,7 +177,7 @@ public Comment updateComment( @RequestBody CommentInfo commentInfo) { Comment comment = new Comment(commentId, user); comment.setContent(commentInfo.getComment()); - return userTaskService.updateComment(taskId, comment, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.updateComment(taskId, comment, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @DeleteMapping(value = "/{taskId}/comments/{commentId}", consumes = MediaType.ALL_VALUE) @@ -183,7 +186,7 @@ public Comment deleteComment( @PathVariable("commentId") String commentId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.removeComment(taskId, commentId, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.removeComment(taskId, commentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @GetMapping("/{taskId}/attachments") @@ -191,7 +194,7 @@ public Collection getAttachments( @PathVariable("taskId") String taskId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.getAttachments(taskId, IdentityProviders.of(user, groups)); + return userTaskService.getAttachments(taskId, identityProviderFactory.getOrImpersonateIdentity(user, groups)); } @PostMapping("/{taskId}/attachments") @@ -203,7 +206,7 @@ public Attachment addAttachment( Attachment attachment = new Attachment(null, user); attachment.setName(attachmentInfo.getName()); attachment.setContent(attachmentInfo.getUri()); - return userTaskService.addAttachment(taskId, attachment, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.addAttachment(taskId, attachment, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @PutMapping("/{taskId}/attachments/{attachmentId}") @@ -216,7 +219,7 @@ public Attachment updateAttachment( Attachment attachment = new Attachment(attachmentId, user); attachment.setName(attachmentInfo.getName()); attachment.setContent(attachmentInfo.getUri()); - return userTaskService.updateAttachment(taskId, attachment, IdentityProviders.of(user, groups)) + return userTaskService.updateAttachment(taskId, attachment, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @@ -226,7 +229,7 @@ public Attachment deleteAttachment( @PathVariable("attachmentId") String attachmentId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.removeAttachment(taskId, attachmentId, IdentityProviders.of(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return userTaskService.removeAttachment(taskId, attachmentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @GetMapping("/{taskId}/attachments/{attachmentId}") @@ -235,7 +238,7 @@ public Attachment getAttachment( @PathVariable("attachmentId") String attachmentId, @RequestParam("user") String user, @RequestParam("group") List groups) { - return userTaskService.getAttachment(taskId, attachmentId, IdentityProviders.of(user, groups)) + return userTaskService.getAttachment(taskId, attachmentId, identityProviderFactory.getOrImpersonateIdentity(user, groups)) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment " + attachmentId + " not found")); } diff --git a/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/auth/QuarkusIdentityProviderFactoryProducer.java b/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/auth/QuarkusIdentityProviderFactoryProducer.java new file mode 100644 index 00000000000..ea33b3b6b82 --- /dev/null +++ b/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/auth/QuarkusIdentityProviderFactoryProducer.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.quarkus.auth; + +import java.util.List; + +import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.auth.IdentityProviderFactory; +import org.kie.kogito.auth.impl.IdentityProviderFactoryImpl; +import org.kie.kogito.auth.impl.KogitoAuthConfig; +import org.kie.kogito.quarkus.config.KogitoRuntimeConfig; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +@ApplicationScoped +public class QuarkusIdentityProviderFactoryProducer { + + private final KogitoRuntimeConfig config; + private final IdentityProvider identityProvider; + + QuarkusIdentityProviderFactoryProducer() { + this(null, null); + } + + @Inject + public QuarkusIdentityProviderFactoryProducer(KogitoRuntimeConfig config, IdentityProvider identityProvider) { + this.config = config; + this.identityProvider = identityProvider; + } + + @Produces + public IdentityProviderFactory get() { + String[] rolesThatAllowImpersonation = config.authConfig.rolesThatAllowImpersonation.map(value -> value.split(",")).orElse(new String[] {}); + + return new IdentityProviderFactoryImpl(identityProvider, new KogitoAuthConfig(config.authConfig.enabled, List.of(rolesThatAllowImpersonation))); + } +} diff --git a/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoAuthRuntimeConfig.java b/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoAuthRuntimeConfig.java new file mode 100644 index 00000000000..917439b188c --- /dev/null +++ b/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoAuthRuntimeConfig.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.quarkus.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class KogitoAuthRuntimeConfig { + + /** + * Enables using the application security context when resolving current User Identity. + */ + @ConfigItem(name = "enabled", defaultValue = "false") + public boolean enabled; + + /** + * Comma-separated list of roles that allow identity impersonation when resolving the actual User Identity. + */ + @ConfigItem(name = "impersonation.allowed-for-roles") + public Optional rolesThatAllowImpersonation; +} diff --git a/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoRuntimeConfig.java b/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoRuntimeConfig.java index df8e45d3ec7..451eae92a0e 100644 --- a/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoRuntimeConfig.java +++ b/quarkus/extensions/kogito-quarkus-extension-common/kogito-quarkus-common/src/main/java/org/kie/kogito/quarkus/config/KogitoRuntimeConfig.java @@ -45,4 +45,10 @@ public class KogitoRuntimeConfig { */ @ConfigItem(name = "process.instances.limit", defaultValue = "1000") public short processInstanceLimit; + + /** + * Auth Configuration + */ + @ConfigItem(name = "security.auth") + public KogitoAuthRuntimeConfig authConfig; } diff --git a/springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/spring/auth/SpringIdentityProviderFactoryProducer.java b/springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/spring/auth/SpringIdentityProviderFactoryProducer.java new file mode 100644 index 00000000000..88e2c7cbdb3 --- /dev/null +++ b/springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/spring/auth/SpringIdentityProviderFactoryProducer.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.spring.auth; + +import java.util.List; +import java.util.Optional; + +import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.auth.IdentityProviderFactory; +import org.kie.kogito.auth.impl.IdentityProviderFactoryImpl; +import org.kie.kogito.auth.impl.KogitoAuthConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SpringIdentityProviderFactoryProducer { + + @Bean + public IdentityProviderFactory produce(IdentityProvider identityProvider, + @Value("${" + IdentityProviderFactory.KOGITO_SECURITY_AUTH_ENABLED + ":false}") boolean enabled, + @Value("${" + IdentityProviderFactory.KOGITO_SECURITY_AUTH_IMPERSONATION_ALLOWED_FOR_ROLES + ":#{null}}") Optional configRolesThatAllowImpersonation) { + String[] rolesThatAllowImpersonation = configRolesThatAllowImpersonation.map(roles -> roles.split(",")).orElse(new String[] {}); + return new IdentityProviderFactoryImpl(identityProvider, new KogitoAuthConfig(enabled, List.of(rolesThatAllowImpersonation))); + } +} From 97d526c04dc823076746332657bd5d698a465395 Mon Sep 17 00:00:00 2001 From: Enrique Date: Fri, 1 Nov 2024 16:40:01 +0100 Subject: [PATCH 11/16] [incubator-kie-issues-1555] Multiple sub process instances cancelled by the same timer. (#3745) * [incubator-kie-issues-1555] Multiple sub process instances cancelled by the same timer. * fix tests * fix Composite rest --- .../DefaultCountDownProcessEventListener.java | 14 + .../process/core/event/EventTypeFilter.java | 112 ++++++-- .../actions/AbstractNodeInstanceAction.java | 4 +- .../ruleflow/core/RuleFlowProcessFactory.java | 7 +- .../impl/WorkflowProcessInstanceImpl.java | 197 +++++++++++--- .../node/BoundaryEventNodeInstance.java | 2 +- .../instance/node/CompositeNodeInstance.java | 15 +- .../instance/node/StateBasedNodeInstance.java | 7 +- .../instance/node/TimerNodeInstance.java | 3 +- ...iInstanceLoopSubprocessBoundaryTimer.bpmn2 | 245 ++++++++++++++++++ .../org/jbpm/bpmn2/IntermediateEventTest.java | 34 ++- 11 files changed, 566 insertions(+), 74 deletions(-) create mode 100644 jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/loop/BPMN2-MultiInstanceLoopSubprocessBoundaryTimer.bpmn2 diff --git a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/test/util/DefaultCountDownProcessEventListener.java b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/test/util/DefaultCountDownProcessEventListener.java index b00a151e446..777a972eac0 100644 --- a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/test/util/DefaultCountDownProcessEventListener.java +++ b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/test/util/DefaultCountDownProcessEventListener.java @@ -43,6 +43,20 @@ public boolean waitTillCompleted() { return waitTillCompleted(10000); } + public boolean await() { + try { + latch.await(); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("Interrputed thread while waiting for all triggers", e); + return false; + } catch (Exception e) { + logger.error("Error during waiting state", e); + return false; + } + } + public boolean waitTillCompleted(long timeOut) { try { return latch.await(timeOut, TimeUnit.MILLISECONDS); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/event/EventTypeFilter.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/event/EventTypeFilter.java index 01ee3388e2a..6868325465f 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/event/EventTypeFilter.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/event/EventTypeFilter.java @@ -19,10 +19,19 @@ package org.jbpm.process.core.event; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Function; +import java.util.regex.Matcher; import org.jbpm.process.core.correlation.CorrelationInstance; import org.jbpm.process.core.correlation.CorrelationManager; +import org.jbpm.util.PatternConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,37 +64,106 @@ public void setType(String type) { this.type = type; } + public void setMessageRef(String messageRef) { + this.messageRef = messageRef; + } + public String toString() { return "Event filter: [" + this.type + "]"; } @Override public boolean acceptsEvent(String type, Object event, Function resolver) { - logger.debug("This event is subscribed to a message type {} with payload {}", type, event); + if (this.type == null) { + return false; + } + if (resolver == null) { - return this.type != null && this.type.equals(type); + return this.type.equals(type); } - if (this.type != null && this.type.equals(type)) { - if (correlationManager != null && correlationManager.isSubscribe(messageRef)) { - if (event == null) { - logger.debug("This event is subscribed to a message ref {}", type); - return false; - } - CorrelationInstance messageCorrelation = correlationManager.computeCorrelationInstance(messageRef, event); - CorrelationInstance processCorrelation = correlationManager.computeSubscription(messageRef, resolver); - logger.debug("The event type {} is correlated, computing correlations. Message correlation is {}; process correlation is: {} ", type, messageCorrelation, processCorrelation); - return messageCorrelation.equals(processCorrelation); + if (this.type.equals(type) && correlationManager != null && correlationManager.isSubscribe(messageRef)) { + logger.debug("This event is subscribed to a message type {} with payload {}", type, event); + if (event == null) { + logger.debug("Cannot compute subscription for messageref {} and type {}", messageRef, type); + return false; } - return true; + CorrelationInstance messageCorrelation = correlationManager.computeCorrelationInstance(messageRef, event); + CorrelationInstance processCorrelation = correlationManager.computeSubscription(messageRef, resolver); + logger.debug("The event type {} is correlated, computing correlations. Message correlation is {}; process correlation is: {} ", type, messageCorrelation, processCorrelation); + return messageCorrelation.equals(processCorrelation); } - String resolvedType = (String) resolver.apply(this.type); - return resolvedType != null && resolvedType.equals(type); + return isAccepted(type, resolver); } - public void setMessageRef(String messageRef) { - this.messageRef = messageRef; + public boolean isAccepted(String type, Function resolver) { + return resolveVariable(this.type, resolver).contains(type); } + + private List resolveVariable(String varExpression, Function resolver) { + if (varExpression == null) { + return Collections.emptyList(); + } + Map replacements = new HashMap<>(); + Matcher matcher = PatternConstants.PARAMETER_MATCHER.matcher(varExpression); + while (matcher.find()) { + String paramName = matcher.group(1); + Object value = resolver.apply(paramName); + if (value == null) { + logger.warn("expression {} in dynamic signal {} not resolved", paramName, varExpression); + continue; + } else if (value instanceof Object[]) { + replacements.put(paramName, (Object[]) value); + } else { + replacements.put(paramName, new Object[] { value }); + } + } + List acceptedTypes = new ArrayList<>(); + List> data = generateCombinations(replacements.keySet(), replacements); + for (Map combination : data) { + String tmp = varExpression; + for (Map.Entry replacement : combination.entrySet()) { + tmp = tmp.replace("#{" + replacement.getKey() + "}", replacement.getValue()); + } + acceptedTypes.add(tmp); + } + if (acceptedTypes.isEmpty()) { + acceptedTypes.add(varExpression); + } + return acceptedTypes; + } + + private List> generateCombinations(Set keys, Map data) { + List> combinations = new ArrayList<>(); + for (String key : keys) { + Set remaining = new HashSet<>(keys); + remaining.remove(key); + List> subCombinations = generateCombinations(remaining, data); + if (subCombinations.isEmpty()) { + for (Object value : data.get(key)) { + Map combination = new HashMap<>(); + combination.put(key, value.toString()); + if (!combinations.contains(combination)) { + combinations.add(combination); + } + } + } else { + for (Map subCombination : subCombinations) { + for (Object value : data.get(key)) { + Map combination = new HashMap<>(); + combination.putAll(subCombination); + combination.put(key, value.toString()); + if (!combinations.contains(combination)) { + combinations.add(combination); + } + } + } + } + } + + return combinations; + } + } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/impl/actions/AbstractNodeInstanceAction.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/impl/actions/AbstractNodeInstanceAction.java index 569a3faceb3..aa1d5756e7d 100644 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/impl/actions/AbstractNodeInstanceAction.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/instance/impl/actions/AbstractNodeInstanceAction.java @@ -24,7 +24,7 @@ import org.jbpm.process.instance.impl.Action; import org.jbpm.workflow.instance.node.CompositeNodeInstance; import org.kie.api.runtime.process.NodeInstance; -import org.kie.api.runtime.process.WorkflowProcessInstance; +import org.kie.api.runtime.process.NodeInstanceContainer; import org.kie.kogito.internal.process.runtime.KogitoProcessContext; public abstract class AbstractNodeInstanceAction implements Action, Serializable { @@ -39,7 +39,7 @@ protected AbstractNodeInstanceAction(String attachedToNodeId) { @Override public void execute(KogitoProcessContext context) throws Exception { - WorkflowProcessInstance pi = context.getNodeInstance().getProcessInstance(); + NodeInstanceContainer pi = context.getNodeInstance().getNodeInstanceContainer(); NodeInstance nodeInstance = findNodeByUniqueId(pi.getNodeInstances(), attachedToNodeId); if (nodeInstance != null) { execute(nodeInstance); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/RuleFlowProcessFactory.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/RuleFlowProcessFactory.java index ea9a67f0a9f..2234646210d 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/RuleFlowProcessFactory.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/RuleFlowProcessFactory.java @@ -42,6 +42,7 @@ import org.jbpm.process.instance.impl.ReturnValueEvaluator; import org.jbpm.process.instance.impl.actions.CancelNodeInstanceAction; import org.jbpm.process.instance.impl.actions.SignalProcessInstanceAction; +import org.jbpm.process.instance.impl.util.VariableUtil; import org.jbpm.ruleflow.core.validation.RuleFlowProcessValidator; import org.jbpm.workflow.core.DroolsAction; import org.jbpm.workflow.core.WorkflowModelValidator; @@ -412,8 +413,10 @@ protected void linkBoundaryErrorEvent(Node node, String attachedTo, Node attache protected DroolsAction timerAction(String type) { DroolsAction signal = new DroolsAction(); - - Action action = kcontext -> kcontext.getProcessInstance().signalEvent(type, kcontext.getNodeInstance().getStringId()); + Action action = kcontext -> { + String eventType = VariableUtil.resolveVariable(type, kcontext.getNodeInstance()); + kcontext.getProcessInstance().signalEvent(eventType, kcontext.getNodeInstance().getStringId()); + }; signal.wire(action); return signal; diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java index 25a0cd0467f..48a7276e269 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java @@ -34,11 +34,13 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.stream.Collectors; import java.util.stream.Stream; import org.drools.core.common.InternalKnowledgeRuntime; +import org.drools.mvel.MVELSafeHelper; import org.drools.mvel.util.MVELEvaluator; import org.jbpm.process.core.ContextContainer; import org.jbpm.process.core.ContextResolver; @@ -57,16 +59,19 @@ import org.jbpm.workflow.core.Node; import org.jbpm.workflow.core.impl.NodeImpl; import org.jbpm.workflow.core.node.BoundaryEventNode; +import org.jbpm.workflow.core.node.CompositeContextNode; import org.jbpm.workflow.core.node.CompositeNode; import org.jbpm.workflow.core.node.DynamicNode; import org.jbpm.workflow.core.node.EventNode; import org.jbpm.workflow.core.node.EventNodeInterface; import org.jbpm.workflow.core.node.EventSubProcessNode; +import org.jbpm.workflow.core.node.ForEachNode; import org.jbpm.workflow.core.node.MilestoneNode; import org.jbpm.workflow.core.node.StartNode; import org.jbpm.workflow.core.node.StateNode; import org.jbpm.workflow.instance.NodeInstance; import org.jbpm.workflow.instance.WorkflowProcessInstance; +import org.jbpm.workflow.instance.node.CompositeContextNodeInstance; import org.jbpm.workflow.instance.node.CompositeNodeInstance; import org.jbpm.workflow.instance.node.EndNodeInstance; import org.jbpm.workflow.instance.node.EventBasedNodeInstanceInterface; @@ -74,6 +79,7 @@ import org.jbpm.workflow.instance.node.EventNodeInstanceInterface; import org.jbpm.workflow.instance.node.EventSubProcessNodeInstance; import org.jbpm.workflow.instance.node.FaultNodeInstance; +import org.jbpm.workflow.instance.node.ForEachNodeInstance; import org.jbpm.workflow.instance.node.StateBasedNodeInstance; import org.jbpm.workflow.instance.node.WorkItemNodeInstance; import org.kie.api.definition.process.NodeContainer; @@ -99,6 +105,7 @@ import org.kie.kogito.process.flexible.Milestone; import org.kie.kogito.timer.TimerInstance; import org.mvel2.integration.VariableResolverFactory; +import org.mvel2.integration.impl.ImmutableDefaultFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -291,12 +298,12 @@ public List getNodeInstances(WorkflowElementIdentifier nodeId) { return result; } - public List getNodeInstances(WorkflowElementIdentifier nodeId, final List currentView) { + public List getNodeInstances(WorkflowElementIdentifier nodeId, final List currentView) { if (nodeId == null) { return Collections.emptyList(); } - List result = new ArrayList<>(); - for (final NodeInstance nodeInstance : currentView) { + List result = new ArrayList<>(); + for (org.kie.api.runtime.process.NodeInstance nodeInstance : currentView) { if (nodeId.equals(nodeInstance.getNodeId())) { result.add(nodeInstance); } @@ -677,8 +684,6 @@ public void signalEvent(String type, Object event) { return; } - List currentView = new ArrayList<>(this.nodeInstances); - try { this.activatingNodeIds = new ArrayList<>(); List listeners = eventListeners.get(type); @@ -693,32 +698,8 @@ public void signalEvent(String type, Object event) { listener.signalEvent(type, event); } } - for (org.kie.api.definition.process.Node node : getWorkflowProcess().getNodes()) { - if (node instanceof EventNodeInterface && ((EventNodeInterface) node).acceptsEvent(type, event, getResolver(node, currentView))) { - if (node instanceof BoundaryEventNode boundaryEventNode) { - WorkflowElementIdentifier id = WorkflowElementIdentifierFactory.fromExternalFormat(boundaryEventNode.getAttachedToNodeId()); - if (!getNodeInstances(id, currentView).isEmpty()) { - EventNodeInstance eventNodeInstance = (EventNodeInstance) getNodeInstance(node); - eventNodeInstance.signalEvent(type, event, getResolver(node, currentView)); - } else if (type.startsWith("Error-") || type.startsWith("Compensation-") || type.startsWith("implicit:compensation")) { - EventNodeInstance eventNodeInstance = (EventNodeInstance) getNodeInstance(node); - eventNodeInstance.signalEvent(type, event, getResolver(node, currentView)); - } - } else { - if (node instanceof EventSubProcessNode && (resolveVariables(((EventSubProcessNode) node).getEvents()).contains(type))) { - EventSubProcessNodeInstance eventNodeInstance = (EventSubProcessNodeInstance) getNodeInstance(node); - eventNodeInstance.signalEvent(type, event); - } else { - List nodeInstances = getNodeInstances(node.getId(), currentView); - if (nodeInstances != null && !nodeInstances.isEmpty()) { - for (NodeInstance nodeInstance : nodeInstances) { - ((EventNodeInstanceInterface) nodeInstance).signalEvent(type, event, getResolver(node, currentView)); - } - } - } - } - } - } + + signal(this, (node) -> this.getNodeInstance(node), () -> this.getWorkflowProcess().getNodes(), type, event); if (((org.jbpm.workflow.core.WorkflowProcess) getWorkflowProcess()).isDynamic()) { for (org.kie.api.definition.process.Node node : getWorkflowProcess().getNodes()) { @@ -749,28 +730,168 @@ public void signalEvent(String type, Object event) { } } - private Function getResolver(org.kie.api.definition.process.Node node, List currentView) { + private void signal(org.kie.api.runtime.process.NodeInstanceContainer container, Function nodeInstanceSupplier, + Supplier resolveNodes, String type, Object event) { + + List currentView = container.getNodeInstances().stream().map(NodeInstance.class::cast).collect(Collectors.toList()); + for (org.kie.api.definition.process.Node node : resolveNodes.get()) { + if (node instanceof EventNodeInterface && ((EventNodeInterface) node).acceptsEvent(type, event, getEventFilterResolver(container, node, currentView))) { + if (node instanceof BoundaryEventNode boundaryEventNode) { + WorkflowElementIdentifier id = WorkflowElementIdentifierFactory.fromExternalFormat(boundaryEventNode.getAttachedToNodeId()); + if (!getNodeInstances(id, currentView).isEmpty()) { + EventNodeInstance eventNodeInstance = (EventNodeInstance) nodeInstanceSupplier.apply(node); + eventNodeInstance.signalEvent(type, event, getEventFilterResolver(container, node, currentView)); + } else if (type.startsWith("Error-") || type.startsWith("Compensation-") || type.startsWith("implicit:compensation")) { + EventNodeInstance eventNodeInstance = (EventNodeInstance) nodeInstanceSupplier.apply(node); + eventNodeInstance.signalEvent(type, event, getEventFilterResolver(container, node, currentView)); + } + } else { + if (node instanceof EventSubProcessNode && (resolveVariables(((EventSubProcessNode) node).getEvents()).contains(type))) { + EventSubProcessNodeInstance eventNodeInstance = (EventSubProcessNodeInstance) getNodeInstance(node); + eventNodeInstance.signalEvent(type, event); + } else { + List nodeInstances = getNodeInstances(node.getId(), currentView); + if (nodeInstances != null && !nodeInstances.isEmpty()) { + for (org.kie.api.runtime.process.NodeInstance nodeInstance : nodeInstances) { + ((EventNodeInstanceInterface) nodeInstance).signalEvent(type, event, getEventFilterResolver(container, node, currentView)); + } + } + } + } + } + } + } + + public Function getEventFilterResolver(org.kie.api.runtime.process.NodeInstanceContainer container, org.kie.api.definition.process.Node node, + List currentView) { if (node instanceof DynamicNode) { // special handling for dynamic node to allow to resolve variables from individual node instances of the dynamic node // instead of just relying on process instance's variables - return e -> { - List nodeInstances = getNodeInstances(node.getId(), currentView); + return (varExpresion) -> { + List nodeInstances = getNodeInstances(node.getId(), currentView); if (nodeInstances != null && !nodeInstances.isEmpty()) { StringBuilder st = new StringBuilder(); - for (NodeInstance ni : nodeInstances) { - Object result = resolveVariable(e, new NodeInstanceResolverFactory(ni)); + for (org.kie.api.runtime.process.NodeInstance ni : nodeInstances) { + Object result = resolveExpressionVariable(varExpresion, new NodeInstanceResolverFactory((NodeInstance) ni)); st.append(result).append("###"); } return st.toString(); } else { - return resolveVariable(e); + NodeInstanceImpl instance = (NodeInstanceImpl) getNodeInstance(node.getId().toExternalFormat(), true); + if (instance != null) { + return instance.getVariable(varExpresion); + } + return null; + } + }; + } else if (node instanceof BoundaryEventNode) { + return (varExpresion) -> { + Function getScopedVariable; + if (container instanceof CompositeContextNodeInstance) { + getScopedVariable = (name) -> getVariable(name, ((CompositeContextNodeInstance) container).getContextInstances(VariableScope.VARIABLE_SCOPE)); + } else if (container instanceof WorkflowProcessInstanceImpl) { + getScopedVariable = (name) -> ((WorkflowProcessInstanceImpl) container).getVariable(name); + } else { + getScopedVariable = null; + } + Object value = getScopedVariable.apply(varExpresion); + if (value != null) { + return value; } + VariableResolverFactory resolverFactory = new ImmutableDefaultFactory() { + @Override + public boolean isResolveable(String varName) { + return getScopedVariable.apply(varName) != null; + } + + @Override + public org.mvel2.integration.VariableResolver getVariableResolver(String varName) { + return new org.mvel2.integration.impl.SimpleValueResolver(getScopedVariable.apply(varName)); + } + }; + return resolveExpressionVariable(varExpresion, resolverFactory).orElse(null); + }; + } else if (node instanceof ForEachNode) { + return (varExpression) -> { + try { + // for each can have multiple outcomes 1 per item of the list so it should be computed like that + ForEachNodeInstance forEachNodeInstance = (ForEachNodeInstance) getNodeInstanceByNodeId(node.getId(), true); + if (forEachNodeInstance == null) { + return new Object[0]; + } + List data = forEachNodeInstance.getNodeInstances().stream().filter(e -> e instanceof CompositeContextNodeInstance) + .map(e -> (CompositeContextNodeInstance) e).collect(Collectors.toList()); + List outcome = new ArrayList<>(); + for (CompositeContextNodeInstance nodeInstance : data) { + Object resolvedValue = resolveExpressionVariable(varExpression, new NodeInstanceResolverFactory(nodeInstance)).orElse(null); + if (resolvedValue != null) { + outcome.add(resolvedValue); + } + } + return outcome.toArray(); + } catch (Throwable t) { + return new Object[0]; + } + }; + } else if (node instanceof EventSubProcessNode || node instanceof StateNode) { + return (varName) -> { + return resolveExpressionVariable(varName, new ProcessInstanceResolverFactory(this)).orElse(null); + }; + } else if (node instanceof CompositeContextNode) { + return (varExpression) -> { + List nodeInstances = getNodeInstances(node.getId(), currentView); + List outcome = new ArrayList<>(); + if (nodeInstances != null && !nodeInstances.isEmpty()) { + for (org.kie.api.runtime.process.NodeInstance nodeInstance : nodeInstances) { + Object resolvedValue = resolveExpressionVariable(varExpression, new NodeInstanceResolverFactory((NodeInstance) nodeInstance)).orElse(null); + if (resolvedValue != null) { + outcome.add(resolvedValue); + } + } + } + return outcome.toArray(); }; } else { - return this::resolveVariable; + return (varName) -> { + return resolveExpressionVariable(varName, new ProcessInstanceResolverFactory(this)).orElse(null); + }; + } + } + + public Object getVariable(String name, List variableScopeInstances) { + if (variableScopeInstances != null) { + for (ContextInstance contextInstance : variableScopeInstances) { + Object value = ((VariableScopeInstance) contextInstance).getVariable(name); + if (value != null) { + return value; + } + } + } + return null; + } + + private Optional resolveExpressionVariable(String paramName, VariableResolverFactory factory) { + try { + // just in case is not an expression + if (factory.isResolveable(paramName)) { + return Optional.of(factory.getVariableResolver(paramName).getValue()); + } + return Optional.ofNullable(MVELSafeHelper.getEvaluator().eval(paramName, factory)); + } catch (Throwable t) { + logger.error("Could not find variable scope for variable {}", paramName); + return Optional.empty(); } } + public NodeInstance getNodeInstanceByNodeId(WorkflowElementIdentifier nodeId, boolean recursive) { + for (NodeInstance nodeInstance : getNodeInstances(recursive)) { + if (nodeInstance.getNodeId().equals(nodeId)) { + return nodeInstance; + } + } + return null; + } + protected List resolveVariables(List events) { return events.stream().map(this::resolveVariable).map(Object::toString).collect(Collectors.toList()); } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/BoundaryEventNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/BoundaryEventNodeInstance.java index e4fbc359594..40e911c01be 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/BoundaryEventNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/BoundaryEventNodeInstance.java @@ -74,7 +74,7 @@ private boolean isAttachedToNodeActive(Collection nodeInstances, S if (attachedTo.equals(nodeUniqueId) && !isActivating) { // in case this is timer event make sure it corresponds to the proper node instance if (type.startsWith("Timer-")) { - if (nInstance.getStringId().equals(event)) { + if (nInstance.getId().equals(event)) { return true; } } else { diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java index ea9a6774629..c926ba21a32 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java @@ -41,6 +41,7 @@ import org.jbpm.workflow.instance.impl.NodeInstanceFactory; import org.jbpm.workflow.instance.impl.NodeInstanceFactoryRegistry; import org.jbpm.workflow.instance.impl.NodeInstanceImpl; +import org.jbpm.workflow.instance.impl.WorkflowProcessInstanceImpl; import org.kie.api.definition.process.Connection; import org.kie.api.definition.process.NodeContainer; import org.kie.api.definition.process.WorkflowElementIdentifier; @@ -305,19 +306,19 @@ private NodeInstance buildCompositeNodeInstance(NodeInstanceImpl nodeInstance, o @Override public void signalEvent(String type, Object event, Function varResolver) { - List currentView = new ArrayList<>(this.nodeInstances); + List currentView = new ArrayList<>(this.nodeInstances); super.signalEvent(type, event); for (org.kie.api.definition.process.Node node : getCompositeNode().internalGetNodes()) { if (node instanceof EventNodeInterface - && ((EventNodeInterface) node).acceptsEvent(type, event, varName -> this.getVariable(varName))) { + && ((EventNodeInterface) node).acceptsEvent(type, event, ((WorkflowProcessInstanceImpl) this.getProcessInstance()).getEventFilterResolver(this, node, currentView))) { if (node instanceof EventNode && ((EventNode) node).getFrom() == null || node instanceof EventSubProcessNode) { EventNodeInstanceInterface eventNodeInstance = (EventNodeInstanceInterface) getNodeInstance(node); eventNodeInstance.signalEvent(type, event, varResolver); } else { - List nodeInstances = getNodeInstances(node.getId(), currentView); + List nodeInstances = getNodeInstances(node.getId(), currentView); if (nodeInstances != null && !nodeInstances.isEmpty()) { - for (NodeInstance nodeInstance : nodeInstances) { + for (org.kie.api.runtime.process.NodeInstance nodeInstance : nodeInstances) { ((EventNodeInstanceInterface) nodeInstance).signalEvent(type, event, varResolver); } } @@ -354,9 +355,9 @@ public List getNodeInstances(WorkflowElementIdentifier nodeId) { return result; } - public List getNodeInstances(WorkflowElementIdentifier nodeId, List currentView) { - List result = new ArrayList<>(); - for (final NodeInstance nodeInstance : currentView) { + public List getNodeInstances(WorkflowElementIdentifier nodeId, List currentView) { + List result = new ArrayList<>(); + for (org.kie.api.runtime.process.NodeInstance nodeInstance : currentView) { if (nodeInstance.getNodeId().equals(nodeId)) { result.add(nodeInstance); } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java index 6b69126d81c..2b0517718c6 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/StateBasedNodeInstance.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import org.drools.core.common.InternalAgenda; import org.drools.core.common.ReteEvaluator; @@ -100,11 +99,11 @@ public void internalTrigger(KogitoNodeInstance from, String type) { .generateId() .timerId(Long.toString(timer.getId())) .expirationTime(createTimerInstance(timer)) - .processInstanceId(getProcessInstance().getStringId()) + .rootProcessId(getProcessInstance().getRootProcessId()) .rootProcessInstanceId(getProcessInstance().getRootProcessInstanceId()) .processId(getProcessInstance().getProcessId()) - .rootProcessId(getProcessInstance().getRootProcessId()) - .nodeInstanceId(Optional.ofNullable(from).map(KogitoNodeInstance::getStringId).orElse(null)) + .processInstanceId(getProcessInstance().getStringId()) + .nodeInstanceId(this.getId()) .build(); String jobId = jobService.scheduleProcessInstanceJob(jobDescription); timerInstances.add(jobId); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/TimerNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/TimerNodeInstance.java index 054ef0ef8de..452e9785287 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/TimerNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/TimerNodeInstance.java @@ -23,7 +23,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -91,7 +90,7 @@ public void internalTrigger(KogitoNodeInstance from, String type) { .rootProcessInstanceId(getProcessInstance().getRootProcessInstanceId()) .processId(getProcessInstance().getProcessId()) .rootProcessId(getProcessInstance().getRootProcessId()) - .nodeInstanceId(Optional.ofNullable(from).map(KogitoNodeInstance::getStringId).orElse(null)) + .nodeInstanceId(this.getId()) .build(); JobsService jobService = processRuntime.getJobsService(); String jobId = jobService.scheduleProcessInstanceJob(jobDescription); diff --git a/jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/loop/BPMN2-MultiInstanceLoopSubprocessBoundaryTimer.bpmn2 b/jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/loop/BPMN2-MultiInstanceLoopSubprocessBoundaryTimer.bpmn2 new file mode 100644 index 00000000000..5afcff04cbd --- /dev/null +++ b/jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/loop/BPMN2-MultiInstanceLoopSubprocessBoundaryTimer.bpmn2 @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _E51B394B-06A3-4AF6-BC86-157D9039A0FB + + + _D296A998-04B5-4CB2-9CFE-1D19C729D5B8 + + + + + + + + _D296A998-04B5-4CB2-9CFE-1D19C729D5B8 + _E51B394B-06A3-4AF6-BC86-157D9039A0FB + + + + + _EEFE61C3-C4A3-48D3-9971-D499F41E88CC_IN_COLLECTIONInputX + _EEFE61C3-C4A3-48D3-9971-D499F41E88CC_itemInputX + + + + + mi_input + _EEFE61C3-C4A3-48D3-9971-D499F41E88CC_IN_COLLECTIONInputX + + + _EEFE61C3-C4A3-48D3-9971-D499F41E88CC_IN_COLLECTIONInputX + + + + + + + + + + + + + + + + + + + + + + + + + + + + _A75E00C6-6FBD-48DE-9750-462DC521483A + _DB8BA8C1-DE12-479A-AF7D-7A562718926C + _CA5FFA7C-D186-4CCB-BC1C-134145DD3C0A + + + + + + + + _DFAE59C0-EE21-4991-AF31-BEBFD37BEB98 + _DB8BA8C1-DE12-479A-AF7D-7A562718926C + System.out.println("Script Timer Task!"); + + + + + + + + _72B473CD-9F5C-461D-AC1B-DCEE85A38365 + _A75E00C6-6FBD-48DE-9750-462DC521483A + + + + + _DEF3687C-875E-4F22-B764-AB6C5626902D_TaskNameInputX + _DEF3687C-875E-4F22-B764-AB6C5626902D_SkippableInputX + + + + _DEF3687C-875E-4F22-B764-AB6C5626902D_TaskNameInputX + + + + + + + _DEF3687C-875E-4F22-B764-AB6C5626902D_SkippableInputX + + + + + + + + admin + + + + + + + + + + _2FC3736F-E7E6-446C-87B5-5B018FF20372 + _72B473CD-9F5C-461D-AC1B-DCEE85A38365 + System.out.println("Script Task: " + item); + + + _CA5FFA7C-D186-4CCB-BC1C-134145DD3C0A + + + _2FC3736F-E7E6-446C-87B5-5B018FF20372 + + + _DFAE59C0-EE21-4991-AF31-BEBFD37BEB98 + + #{item} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _Nw1jAGpCED2xyqiTIWNwhw + _Nw1jAGpCED2xyqiTIWNwhw + + diff --git a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/IntermediateEventTest.java b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/IntermediateEventTest.java index 9d2ec7cc3b5..bace4169053 100755 --- a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/IntermediateEventTest.java +++ b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/IntermediateEventTest.java @@ -111,6 +111,8 @@ import org.jbpm.bpmn2.loop.MultiInstanceLoopCharacteristicsTaskSequentialProcess; import org.jbpm.bpmn2.loop.MultiInstanceLoopCharacteristicsTaskWithOutputCmpCondSequentialModel; import org.jbpm.bpmn2.loop.MultiInstanceLoopCharacteristicsTaskWithOutputCmpCondSequentialProcess; +import org.jbpm.bpmn2.loop.MultiInstanceLoopSubprocessBoundaryTimerModel; +import org.jbpm.bpmn2.loop.MultiInstanceLoopSubprocessBoundaryTimerProcess; import org.jbpm.bpmn2.objects.Person; import org.jbpm.bpmn2.objects.TestUserTaskWorkItemHandler; import org.jbpm.bpmn2.objects.TestWorkItemHandler; @@ -144,6 +146,7 @@ import org.jbpm.test.utils.ProcessTestHelper.CompletionKogitoEventListener; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.kie.api.command.ExecutableCommand; import org.kie.api.event.process.ProcessCompletedEvent; import org.kie.api.event.process.ProcessNodeLeftEvent; @@ -481,6 +484,7 @@ public void testEventBasedSplitAfter() { } @Test + @Timeout(10000L) public void testEventBasedSplit2() { ProcessCompletedCountDownProcessEventListener countDownListener = new ProcessCompletedCountDownProcessEventListener(1); Application app = ProcessTestHelper.newApplication(); @@ -511,7 +515,7 @@ public void testEventBasedSplit2() { instance = processDefinition.createInstance(model); instance.start(); assertThat(instance.status()).isEqualTo(org.kie.kogito.process.ProcessInstance.STATE_ACTIVE); - countDownListener.waitTillCompleted(); + countDownListener.await(); assertThat(instance.status()).isEqualTo(org.kie.kogito.process.ProcessInstance.STATE_COMPLETED); } @@ -2037,6 +2041,34 @@ public void testMultiInstanceLoopBoundaryTimer() throws Exception { assertThat(instance.status()).isEqualTo(org.kie.kogito.process.ProcessInstance.STATE_COMPLETED); } + @Test + @Timeout(10000L) + public void testMultiInstanceLoopSubprocessBoundaryTimer() throws Exception { + Application app = ProcessTestHelper.newApplication(); + NodeLeftCountDownProcessEventListener countDownListener = new NodeLeftCountDownProcessEventListener("Script2", 1); + ProcessTestHelper.registerProcessEventListener(app, countDownListener); + TestUserTaskWorkItemHandler handler = new TestUserTaskWorkItemHandler(); + ProcessTestHelper.registerHandler(app, "Human Task", handler); + + org.kie.kogito.process.Process definition = MultiInstanceLoopSubprocessBoundaryTimerProcess.newProcess(app); + MultiInstanceLoopSubprocessBoundaryTimerModel model = definition.createModel(); + model.setMi_input(List.of("PT1S", "PT2S", "PT3S")); + org.kie.kogito.process.ProcessInstance instance = definition.createInstance(model); + instance.start(); + + countDownListener.reset(1); + assertThat(countDownListener.await()).isTrue(); + assertThat(instance.status()).isEqualTo(org.kie.kogito.process.ProcessInstance.STATE_ACTIVE); + + countDownListener.reset(1); + assertThat(countDownListener.await()).isTrue(); + assertThat(instance.status()).isEqualTo(org.kie.kogito.process.ProcessInstance.STATE_ACTIVE); + + countDownListener.reset(1); + assertThat(countDownListener.await()).isTrue(); + assertThat(instance.status()).isEqualTo(org.kie.kogito.process.ProcessInstance.STATE_COMPLETED); + } + @Test public void testMultiInstanceLoopCharacteristicsProcessSequential() throws Exception { Application app = ProcessTestHelper.newApplication(); From 54875f805ad0fc8c3bbd6dcc3c248c5267652956 Mon Sep 17 00:00:00 2001 From: Enrique Date: Fri, 1 Nov 2024 20:02:34 +0100 Subject: [PATCH 12/16] [incubator-kie-issues-1550] Add Transaction error handling when transaction is enabled (#3740) * [incubator-kie-issues-1550] Add Transaction error handling when transaction is enabled * ProcessGenerationIT fix tests * clean up exception handling * add exception handler * handlers creation for spring boot and quarkus * fix throwable * fix compilation errors * fix ProcessGenerationIT * fix java standalone no tx * fix quarkus exceptions handling * ProcessResourceGeneratorTest fix * fix resources * fix quarkus error handling * fix spring boot unitof work * fix transactions templates * fix template class name * fix * fix composed messages based on the exceptions processed --- .../exceptions/AbstractExceptionsHandler.java | 142 ++++++++++++ .../exceptions/BaseExceptionsHandler.java | 203 ------------------ .../exceptions/ExceptionBodyMessage.java | 57 +++++ .../ExceptionBodyMessageFunctions.java | 124 +++++++++++ .../exceptions/RestExceptionHandler.java | 58 +++++ .../exceptions/BaseExceptionHandlerTest.java | 14 +- .../kie/kogito/handler/ExceptionHandler.java | 25 +++ .../org/jbpm/bpmn2/xml/ProcessHandler.java | 4 +- .../org/jbpm/workflow/core/impl/NodeImpl.java | 6 +- .../jbpm/workflow/core/node/ActionNode.java | 6 +- .../org/jbpm/workflow/core/node/EndNode.java | 4 +- .../jbpm/workflow/core/node/EventNode.java | 6 +- .../workflow/core/node/MilestoneNode.java | 6 +- .../jbpm/workflow/core/node/RuleSetNode.java | 5 +- .../org/jbpm/workflow/core/node/Split.java | 4 +- .../jbpm/workflow/core/node/StartNode.java | 4 +- .../workflow/core/node/SubProcessNode.java | 6 +- .../jbpm/workflow/core/node/TimerNode.java | 6 +- .../jbpm/workflow/core/node/WorkItemNode.java | 6 +- .../instance/WorkflowProcessParameters.java | 56 +++++ .../instance/impl/NodeInstanceImpl.java | 18 +- .../impl/WorkflowProcessInstanceImpl.java | 10 + .../instance/node/ForEachNodeInstance.java | 4 +- .../codegen/process/ProcessGenerationIT.java | 16 +- .../codegen/process/ProcessCodegen.java | 29 ++- .../codegen/process/util/CodegenUtil.java | 12 +- ...tionHandlerTransactionQuarkusTemplate.java | 77 +++++++ ...ptionHandlerTransactionSpringTemplate.java | 73 +++++++ quarkus/addons/rest-exception-handler/pom.xml | 4 + .../exceptions/BaseExceptionMapper.java | 7 - .../exceptions/ExceptionsHandler.java | 28 ++- .../exceptions/ExceptionsHandlerProducer.java | 32 +++ .../IllegalArgumentExceptionMapper.java | 4 + .../InvalidLifeCyclePhaseExceptionMapper.java | 4 + .../InvalidTransitionExceptionMapper.java | 4 + .../NodeInstanceNotFoundExceptionMapper.java | 4 + .../NodeNotFoundExceptionMapper.java | 4 + .../NotAuthorizedExceptionMapper.java | 4 + ...cessInstanceDuplicatedExceptionMapper.java | 4 + ...ocessInstanceExecutionExceptionMapper.java | 4 + ...rocessInstanceNotFoundExceptionMapper.java | 4 + ...kInstanceNotAuthorizedExceptionMapper.java | 4 + ...erTaskInstanceNotFoundExceptionMapper.java | 4 + .../UserTaskTransitionExceptionMapper.java | 4 + .../VariableViolationExceptionMapper.java | 4 + .../WorkItemExecutionExceptionMapper.java | 4 + .../WorkItemNotFoundExceptionMapper.java | 4 + .../exceptions/ExceptionsHandlerTest.java | 8 +- .../source/files/SourceFilesResource.java | 47 ++-- .../source/files/SourceFilesResourceTest.java | 4 +- .../springboot/ExceptionsHandler.java | 59 ++--- .../springboot/ExceptionsHandlerTest.java | 22 +- .../kogito/process/KogitoBeanProducer.java | 9 + 53 files changed, 933 insertions(+), 328 deletions(-) create mode 100644 addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/AbstractExceptionsHandler.java delete mode 100644 addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionsHandler.java create mode 100644 addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessage.java create mode 100644 addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessageFunctions.java create mode 100644 addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/RestExceptionHandler.java create mode 100644 api/kogito-api/src/main/java/org/kie/kogito/handler/ExceptionHandler.java create mode 100644 jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/WorkflowProcessParameters.java create mode 100644 kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionQuarkusTemplate.java create mode 100644 kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionSpringTemplate.java create mode 100644 quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerProducer.java diff --git a/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/AbstractExceptionsHandler.java b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/AbstractExceptionsHandler.java new file mode 100644 index 00000000000..26923edcd21 --- /dev/null +++ b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/AbstractExceptionsHandler.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.resource.exceptions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.kie.kogito.handler.ExceptionHandler; +import org.kie.kogito.internal.process.runtime.MessageException; +import org.kie.kogito.internal.process.workitem.InvalidLifeCyclePhaseException; +import org.kie.kogito.internal.process.workitem.InvalidTransitionException; +import org.kie.kogito.internal.process.workitem.NotAuthorizedException; +import org.kie.kogito.internal.process.workitem.WorkItemExecutionException; +import org.kie.kogito.internal.process.workitem.WorkItemNotFoundException; +import org.kie.kogito.process.NodeInstanceNotFoundException; +import org.kie.kogito.process.NodeNotFoundException; +import org.kie.kogito.process.ProcessInstanceDuplicatedException; +import org.kie.kogito.process.ProcessInstanceExecutionException; +import org.kie.kogito.process.ProcessInstanceNotFoundException; +import org.kie.kogito.process.VariableViolationException; +import org.kie.kogito.usertask.UserTaskInstanceNotAuthorizedException; +import org.kie.kogito.usertask.UserTaskInstanceNotFoundException; +import org.kie.kogito.usertask.lifecycle.UserTaskTransitionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.nodeInstanceNotFoundMessageException; +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.nodeNotFoundMessageException; +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.processInstanceDuplicatedMessageException; +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.processInstanceExecutionMessageException; +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.processInstanceNotFoundExceptionMessageException; +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.variableViolationMessageException; +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.workItemExecutionMessageException; +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.workItemNotFoundMessageException; +import static org.kie.kogito.resource.exceptions.RestExceptionHandler.newExceptionHandler; + +public abstract class AbstractExceptionsHandler { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractExceptionsHandler.class); + + RestExceptionHandler DEFAULT_HANDLER = newExceptionHandler(Exception.class, this::badRequest); + + private Map, RestExceptionHandler> mapper; + + private List errorHandlers; + + protected AbstractExceptionsHandler() { + this(Collections.emptyList()); + } + + protected AbstractExceptionsHandler(Iterable errorHandlers) { + List> handlers = List.> of( + newExceptionHandler(InvalidLifeCyclePhaseException.class, this::badRequest), + newExceptionHandler(UserTaskTransitionException.class, this::badRequest), + newExceptionHandler(UserTaskInstanceNotFoundException.class, this::notFound), + newExceptionHandler(UserTaskInstanceNotAuthorizedException.class, this::forbidden), + newExceptionHandler(InvalidTransitionException.class, this::badRequest), + newExceptionHandler(NodeInstanceNotFoundException.class, nodeInstanceNotFoundMessageException(), this::notFound), + newExceptionHandler(NodeNotFoundException.class, nodeNotFoundMessageException(), this::notFound), + newExceptionHandler(NotAuthorizedException.class, this::forbidden), + newExceptionHandler(ProcessInstanceDuplicatedException.class, processInstanceDuplicatedMessageException(), this::conflict), + newExceptionHandler(ProcessInstanceExecutionException.class, processInstanceExecutionMessageException(), this::internalError), + newExceptionHandler(ProcessInstanceNotFoundException.class, processInstanceNotFoundExceptionMessageException(), this::notFound), + newExceptionHandler(WorkItemNotFoundException.class, workItemNotFoundMessageException(), this::notFound), + newExceptionHandler(VariableViolationException.class, variableViolationMessageException(), this::badRequest), + newExceptionHandler(WorkItemExecutionException.class, workItemExecutionMessageException(), this::fromErrorCode), + newExceptionHandler(IllegalArgumentException.class, this::badRequest), + newExceptionHandler(MessageException.class, this::badRequest)); + + this.mapper = new HashMap<>(); + for (RestExceptionHandler handler : handlers) { + this.mapper.put(handler.getType(), handler); + } + this.errorHandlers = new ArrayList<>(); + errorHandlers.iterator().forEachRemaining(this.errorHandlers::add); + + } + + private T fromErrorCode(ExceptionBodyMessage message) { + switch (message.getErrorCode()) { + case "400": + return badRequest(message); + case "403": + return forbidden(message); + case "404": + return notFound(message); + case "409": + return conflict(message); + default: + return internalError(message); + } + } + + protected abstract T badRequest(ExceptionBodyMessage body); + + protected abstract T conflict(ExceptionBodyMessage body); + + protected abstract T internalError(ExceptionBodyMessage body); + + protected abstract T notFound(ExceptionBodyMessage body); + + protected abstract T forbidden(ExceptionBodyMessage body); + + public T mapException(Exception exceptionThrown) { + + var handler = mapper.getOrDefault(exceptionThrown.getClass(), DEFAULT_HANDLER); + ExceptionBodyMessage message = handler.getContent(exceptionThrown); + + Throwable rootCause = exceptionThrown.getCause(); + while (rootCause != null) { + if (mapper.containsKey(rootCause.getClass())) { + handler = mapper.get(rootCause.getClass()); + message.merge(handler.getContent(rootCause)); + } + rootCause = rootCause.getCause(); + } + // we invoked the error handlers + errorHandlers.forEach(e -> e.handle(exceptionThrown)); + T response = handler.buildResponse(message); + LOG.debug("mapping exception {} with response {}", exceptionThrown, message.getBody()); + return response; + } +} diff --git a/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionsHandler.java b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionsHandler.java deleted file mode 100644 index c326eeebe76..00000000000 --- a/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionsHandler.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.kie.kogito.resource.exceptions; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import org.kie.kogito.internal.process.runtime.MessageException; -import org.kie.kogito.internal.process.workitem.InvalidLifeCyclePhaseException; -import org.kie.kogito.internal.process.workitem.InvalidTransitionException; -import org.kie.kogito.internal.process.workitem.NotAuthorizedException; -import org.kie.kogito.internal.process.workitem.WorkItemExecutionException; -import org.kie.kogito.internal.process.workitem.WorkItemNotFoundException; -import org.kie.kogito.process.NodeInstanceNotFoundException; -import org.kie.kogito.process.NodeNotFoundException; -import org.kie.kogito.process.ProcessInstanceDuplicatedException; -import org.kie.kogito.process.ProcessInstanceExecutionException; -import org.kie.kogito.process.ProcessInstanceNotFoundException; -import org.kie.kogito.process.VariableViolationException; -import org.kie.kogito.usertask.UserTaskInstanceNotAuthorizedException; -import org.kie.kogito.usertask.UserTaskInstanceNotFoundException; -import org.kie.kogito.usertask.lifecycle.UserTaskTransitionException; - -public abstract class BaseExceptionsHandler { - - public static final String MESSAGE = "message"; - public static final String PROCESS_INSTANCE_ID = "processInstanceId"; - private static final String TASK_ID = "taskId"; - public static final String VARIABLE = "variable"; - public static final String NODE_INSTANCE_ID = "nodeInstanceId"; - public static final String NODE_ID = "nodeId"; - public static final String FAILED_NODE_ID = "failedNodeId"; - public static final String ID = "id"; - private final Map, FunctionHolder> mapper; - - private static class FunctionHolder { - private final Function contentGenerator; - private final Function> responseGenerator; - - public FunctionHolder(Function contentGenerator, Function> responseGenerator) { - this.contentGenerator = contentGenerator; - this.responseGenerator = responseGenerator; - } - - public Function getContentGenerator() { - return contentGenerator; - } - - public Function> getResponseGenerator() { - return responseGenerator; - } - } - - private final FunctionHolder defaultHolder = new FunctionHolder<>(ex -> ex, ex -> BaseExceptionsHandler.this::internalError); - private final FunctionHolder messageFunctionHolder = new FunctionHolder<>(ex -> Collections.singletonMap(MESSAGE, ex.getMessage()), ex -> BaseExceptionsHandler.this::badRequest); - - protected BaseExceptionsHandler() { - mapper = new HashMap<>(); - mapper.put(InvalidLifeCyclePhaseException.class, new FunctionHolder<>( - ex -> Collections.singletonMap(MESSAGE, ex.getMessage()), ex -> BaseExceptionsHandler.this::badRequest)); - - mapper.put(UserTaskTransitionException.class, new FunctionHolder<>( - ex -> Collections.singletonMap(MESSAGE, ex.getMessage()), ex -> BaseExceptionsHandler.this::badRequest)); - - mapper.put(UserTaskInstanceNotFoundException.class, new FunctionHolder<>( - ex -> Collections.singletonMap(MESSAGE, ex.getMessage()), ex -> BaseExceptionsHandler.this::notFound)); - - mapper.put(UserTaskInstanceNotAuthorizedException.class, new FunctionHolder<>( - ex -> Collections.singletonMap(MESSAGE, ex.getMessage()), ex -> BaseExceptionsHandler.this::forbidden)); - - mapper.put(InvalidTransitionException.class, new FunctionHolder<>( - ex -> Collections.singletonMap(MESSAGE, ex.getMessage()), ex -> BaseExceptionsHandler.this::badRequest)); - - mapper.put(NodeInstanceNotFoundException.class, new FunctionHolder<>( - ex -> { - NodeInstanceNotFoundException exception = (NodeInstanceNotFoundException) ex; - Map response = new HashMap<>(); - response.put(MESSAGE, exception.getMessage()); - response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); - response.put(NODE_INSTANCE_ID, exception.getNodeInstanceId()); - return response; - }, ex -> BaseExceptionsHandler.this::notFound)); - - mapper.put(NodeNotFoundException.class, new FunctionHolder<>( - ex -> { - NodeNotFoundException exception = (NodeNotFoundException) ex; - Map response = new HashMap<>(); - response.put(MESSAGE, exception.getMessage()); - response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); - response.put(NODE_ID, exception.getNodeId()); - return response; - }, ex -> BaseExceptionsHandler.this::notFound)); - - mapper.put(NotAuthorizedException.class, new FunctionHolder<>( - ex -> Collections.singletonMap(MESSAGE, ex.getMessage()), ex -> BaseExceptionsHandler.this::forbidden)); - - mapper.put(ProcessInstanceDuplicatedException.class, new FunctionHolder<>( - ex -> { - ProcessInstanceDuplicatedException exception = (ProcessInstanceDuplicatedException) ex; - Map response = new HashMap<>(); - response.put(MESSAGE, exception.getMessage()); - response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); - return response; - }, ex -> BaseExceptionsHandler.this::conflict)); - - mapper.put(ProcessInstanceExecutionException.class, new FunctionHolder<>( - ex -> { - ProcessInstanceExecutionException exception = (ProcessInstanceExecutionException) ex; - Map response = new HashMap<>(); - response.put(ID, exception.getProcessInstanceId()); - response.put(FAILED_NODE_ID, exception.getFailedNodeId()); - response.put(MESSAGE, exception.getErrorMessage()); - return response; - }, ex -> BaseExceptionsHandler.this::internalError)); - - mapper.put(ProcessInstanceNotFoundException.class, new FunctionHolder<>( - ex -> { - ProcessInstanceNotFoundException exception = (ProcessInstanceNotFoundException) ex; - Map response = new HashMap<>(); - response.put(MESSAGE, exception.getMessage()); - response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); - return response; - }, ex -> BaseExceptionsHandler.this::notFound)); - - mapper.put(WorkItemNotFoundException.class, new FunctionHolder<>(ex -> { - WorkItemNotFoundException exception = (WorkItemNotFoundException) ex; - return Map.of(MESSAGE, exception.getMessage(), TASK_ID, exception.getWorkItemId()); - }, ex -> BaseExceptionsHandler.this::notFound)); - - mapper.put(VariableViolationException.class, new FunctionHolder<>( - ex -> { - VariableViolationException exception = (VariableViolationException) ex; - Map response = new HashMap<>(); - response.put(MESSAGE, exception.getMessage() + " : " + exception.getErrorMessage()); - response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); - response.put(VARIABLE, exception.getVariableName()); - return response; - }, ex -> BaseExceptionsHandler.this::badRequest)); - - mapper.put(WorkItemExecutionException.class, new FunctionHolder<>( - ex -> Map.of(MESSAGE, ex.getMessage()), - ex -> fromErrorCode(((WorkItemExecutionException) ex).getErrorCode()))); - mapper.put(IllegalArgumentException.class, messageFunctionHolder); - mapper.put(MessageException.class, messageFunctionHolder); - } - - private Function fromErrorCode(String errorCode) { - switch (errorCode) { - case "400": - return this::badRequest; - case "403": - return this::forbidden; - case "404": - return this::notFound; - case "409": - return this::conflict; - default: - return this::internalError; - } - } - - protected abstract T badRequest(R body); - - protected abstract T conflict(R body); - - protected abstract T internalError(R body); - - protected abstract T notFound(R body); - - protected abstract T forbidden(R body); - - public T mapException(R exception) { - FunctionHolder holder = (FunctionHolder) mapper.getOrDefault(exception.getClass(), defaultHolder); - U body = holder.getContentGenerator().apply(exception); - Throwable rootCause = exception.getCause(); - while (rootCause != null) { - if (mapper.containsKey(rootCause.getClass())) { - holder = (FunctionHolder) mapper.get(rootCause.getClass()); - exception = (R) rootCause; - } - rootCause = rootCause.getCause(); - } - return holder.getResponseGenerator().apply(exception).apply(body); - } -} diff --git a/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessage.java b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessage.java new file mode 100644 index 00000000000..b491b111f40 --- /dev/null +++ b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessage.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.resource.exceptions; + +import java.util.HashMap; +import java.util.Map; + +public class ExceptionBodyMessage { + + public static final String MESSAGE = "message"; + public static final String PROCESS_INSTANCE_ID = "processInstanceId"; + public static final String TASK_ID = "taskId"; + public static final String VARIABLE = "variable"; + public static final String NODE_INSTANCE_ID = "nodeInstanceId"; + public static final String NODE_ID = "nodeId"; + public static final String FAILED_NODE_ID = "failedNodeId"; + public static final String ID = "id"; + public static final String ERROR_CODE = "errorCode"; + + private Map body; + + public ExceptionBodyMessage() { + body = new HashMap<>(); + } + + public ExceptionBodyMessage(Map body) { + this.body = new HashMap<>(body); + } + + public Map getBody() { + return body; + } + + public String getErrorCode() { + return body.getOrDefault(ERROR_CODE, ""); + } + + public void merge(ExceptionBodyMessage content) { + this.body.putAll(content.body); + } +} diff --git a/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessageFunctions.java b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessageFunctions.java new file mode 100644 index 00000000000..0bc1e6fd817 --- /dev/null +++ b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionBodyMessageFunctions.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.resource.exceptions; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.kie.kogito.internal.process.workitem.WorkItemExecutionException; +import org.kie.kogito.internal.process.workitem.WorkItemNotFoundException; +import org.kie.kogito.process.NodeInstanceNotFoundException; +import org.kie.kogito.process.NodeNotFoundException; +import org.kie.kogito.process.ProcessInstanceDuplicatedException; +import org.kie.kogito.process.ProcessInstanceExecutionException; +import org.kie.kogito.process.ProcessInstanceNotFoundException; +import org.kie.kogito.process.VariableViolationException; + +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessage.ERROR_CODE; + +public class ExceptionBodyMessageFunctions { + + public static final String MESSAGE = "message"; + public static final String PROCESS_INSTANCE_ID = "processInstanceId"; + public static final String TASK_ID = "taskId"; + public static final String VARIABLE = "variable"; + public static final String NODE_INSTANCE_ID = "nodeInstanceId"; + public static final String NODE_ID = "nodeId"; + public static final String FAILED_NODE_ID = "failedNodeId"; + public static final String ID = "id"; + + public static Function defaultMessageException() { + return ex -> new ExceptionBodyMessage(Collections.singletonMap(MESSAGE, ex.getMessage())); + } + + public static Function nodeInstanceNotFoundMessageException() { + return ex -> { + Map response = new HashMap<>(); + response.put(MESSAGE, ex.getMessage()); + response.put(PROCESS_INSTANCE_ID, ex.getProcessInstanceId()); + response.put(NODE_INSTANCE_ID, ex.getNodeInstanceId()); + return new ExceptionBodyMessage(response); + }; + } + + public static Function nodeNotFoundMessageException() { + return exception -> { + Map response = new HashMap<>(); + response.put(MESSAGE, exception.getMessage()); + response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); + response.put(NODE_ID, exception.getNodeId()); + return new ExceptionBodyMessage(response); + }; + } + + public static Function processInstanceDuplicatedMessageException() { + return exception -> { + Map response = new HashMap<>(); + response.put(MESSAGE, exception.getMessage()); + response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); + return new ExceptionBodyMessage(response); + }; + } + + public static Function processInstanceExecutionMessageException() { + return exception -> { + Map response = new HashMap<>(); + response.put(ID, exception.getProcessInstanceId()); + response.put(FAILED_NODE_ID, exception.getFailedNodeId()); + response.put(MESSAGE, exception.getErrorMessage()); + return new ExceptionBodyMessage(response); + }; + } + + public static Function processInstanceNotFoundExceptionMessageException() { + return exception -> { + Map response = new HashMap<>(); + response.put(MESSAGE, exception.getMessage()); + response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); + return new ExceptionBodyMessage(response); + }; + } + + public static Function workItemNotFoundMessageException() { + return exception -> { + return new ExceptionBodyMessage(Map.of(MESSAGE, exception.getMessage(), TASK_ID, exception.getWorkItemId())); + }; + } + + public static Function workItemExecutionMessageException() { + return exception -> { + Map response = new HashMap<>(); + response.put(MESSAGE, exception.getMessage()); + response.put(ERROR_CODE, exception.getErrorCode()); + return new ExceptionBodyMessage(response); + }; + } + + public static Function variableViolationMessageException() { + return exception -> { + Map response = new HashMap<>(); + response.put(MESSAGE, exception.getMessage() + " : " + exception.getErrorMessage()); + response.put(PROCESS_INSTANCE_ID, exception.getProcessInstanceId()); + response.put(VARIABLE, exception.getVariableName()); + return new ExceptionBodyMessage(response); + }; + } +} diff --git a/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/RestExceptionHandler.java b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/RestExceptionHandler.java new file mode 100644 index 00000000000..083e4efe834 --- /dev/null +++ b/addons/common/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/RestExceptionHandler.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.resource.exceptions; + +import java.util.function.Function; + +import static org.kie.kogito.resource.exceptions.ExceptionBodyMessageFunctions.defaultMessageException; + +public class RestExceptionHandler { + private final Function messageConverter; + + private final Function responseConverter; + + private Class type; + + public RestExceptionHandler(Class type, Function messageConverter, Function responseConverter) { + this.type = type; + this.messageConverter = messageConverter; + this.responseConverter = responseConverter; + } + + public Class getType() { + return type; + } + + public ExceptionBodyMessage getContent(Throwable exception) { + return messageConverter.apply(getType().cast(exception)); + } + + public RESPONSE buildResponse(ExceptionBodyMessage exceptionBodyMessage) { + return responseConverter.apply(exceptionBodyMessage); + } + + public static RestExceptionHandler newExceptionHandler(Class type, Function contentGenerator, + Function responseGenerator) { + return new RestExceptionHandler(type, contentGenerator, responseGenerator); + } + + public static RestExceptionHandler newExceptionHandler(Class type, Function responseGenerator) { + return new RestExceptionHandler(type, defaultMessageException(), responseGenerator); + } +} \ No newline at end of file diff --git a/addons/common/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/BaseExceptionHandlerTest.java b/addons/common/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/BaseExceptionHandlerTest.java index 01f244915df..bf8f28fc314 100644 --- a/addons/common/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/BaseExceptionHandlerTest.java +++ b/addons/common/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/BaseExceptionHandlerTest.java @@ -40,7 +40,7 @@ @ExtendWith(MockitoExtension.class) class BaseExceptionHandlerTest { - private BaseExceptionsHandler tested; + private AbstractExceptionsHandler tested; @Mock private Object badRequestResponse; @@ -59,29 +59,29 @@ class BaseExceptionHandlerTest { @BeforeEach void setUp() { - tested = spy(new BaseExceptionsHandler() { + tested = spy(new AbstractExceptionsHandler() { @Override - protected Object badRequest(Object body) { + protected Object badRequest(ExceptionBodyMessage body) { return badRequestResponse; } @Override - protected Object conflict(Object body) { + protected Object conflict(ExceptionBodyMessage body) { return conflictResponse; } @Override - protected Object internalError(Object body) { + protected Object internalError(ExceptionBodyMessage body) { return internalErrorResponse; } @Override - protected Object notFound(Object body) { + protected Object notFound(ExceptionBodyMessage body) { return notFoundResponse; } @Override - protected Object forbidden(Object body) { + protected Object forbidden(ExceptionBodyMessage body) { return forbiddenResponse; } }); diff --git a/api/kogito-api/src/main/java/org/kie/kogito/handler/ExceptionHandler.java b/api/kogito-api/src/main/java/org/kie/kogito/handler/ExceptionHandler.java new file mode 100644 index 00000000000..7bfbdf37ef0 --- /dev/null +++ b/api/kogito-api/src/main/java/org/kie/kogito/handler/ExceptionHandler.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.handler; + +public interface ExceptionHandler { + + void handle(Exception th); + +} diff --git a/jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/ProcessHandler.java b/jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/ProcessHandler.java index 644f9fe25a7..72180a45c1e 100755 --- a/jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/ProcessHandler.java +++ b/jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/ProcessHandler.java @@ -107,6 +107,8 @@ import org.xml.sax.Attributes; import org.xml.sax.SAXException; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + public class ProcessHandler extends BaseAbstractHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(ProcessHandler.class); @@ -412,7 +414,7 @@ public static void linkConnections(RuleFlowProcess process, NodeContainer nodeCo result.setMetaData("bendpoints", connection.getBendpoints()); result.setMetaData(Metadata.UNIQUE_ID, connection.getId()); - if (source instanceof NodeImpl nodeImpl && Boolean.parseBoolean((String) process.getMetaData().get("jbpm.enable.multi.con"))) { + if (source instanceof NodeImpl nodeImpl && WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(process)) { Constraint constraint = buildConstraint(connection, nodeImpl); if (constraint != null) { nodeImpl.addConstraint(new ConnectionRef(connection.getId(), target.getId(), org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE), diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/NodeImpl.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/NodeImpl.java index 0ea5dee5509..13a21788e1b 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/NodeImpl.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/NodeImpl.java @@ -40,6 +40,8 @@ import org.kie.api.definition.process.NodeContainer; import org.kie.api.definition.process.WorkflowElementIdentifier; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of a node. */ @@ -297,7 +299,7 @@ public Connection getFrom() { if (list.size() == 1) { return list.get(0); } - if (Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { return list.get(0); } else { throw new IllegalArgumentException( @@ -317,7 +319,7 @@ public Connection getTo() { if (list.size() == 1) { return list.get(0); } - if (Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { return list.get(0); } else { throw new IllegalArgumentException( diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/ActionNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/ActionNode.java index 9144d42833a..53f74b41436 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/ActionNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/ActionNode.java @@ -27,6 +27,8 @@ import org.jbpm.workflow.core.impl.ExtendedNodeImpl; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of an action node. * @@ -53,7 +55,7 @@ public void validateAddIncomingConnection(final String type, final Connection co "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); @@ -68,7 +70,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] cannot have more than one outgoing connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EndNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EndNode.java index ae88f439561..222f13d1c75 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EndNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EndNode.java @@ -22,6 +22,8 @@ import org.jbpm.workflow.core.impl.ExtendedNodeImpl; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of an end node. * @@ -58,7 +60,7 @@ public void validateAddIncomingConnection(final String type, final Connection co "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EventNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EventNode.java index 42d216f4835..0ce70b86f88 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EventNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/EventNode.java @@ -28,6 +28,8 @@ import org.jbpm.workflow.core.impl.ExtendedNodeImpl; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + public class EventNode extends ExtendedNodeImpl implements EventNodeInterface { private static final long serialVersionUID = 510l; @@ -104,7 +106,7 @@ public void validateAddIncomingConnection(final String type, final Connection co "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); @@ -119,7 +121,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] cannot have more than one outgoing connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/MilestoneNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/MilestoneNode.java index 7ffabedcda2..1a1ba697ff6 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/MilestoneNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/MilestoneNode.java @@ -24,6 +24,8 @@ import org.kie.api.definition.process.Connection; import org.kie.api.runtime.process.ProcessContext; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of a milestone node. */ @@ -59,7 +61,7 @@ public void validateAddIncomingConnection(final String type, final Connection co if (!Node.CONNECTION_DEFAULT_TYPE.equals(type)) { throwValidationException(connection, "only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throwValidationException(connection, "cannot have more than one incoming connection!"); } } @@ -70,7 +72,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co if (!Node.CONNECTION_DEFAULT_TYPE.equals(type)) { throwValidationException(connection, "only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throwValidationException(connection, "cannot have more than one outgoing connection!"); } } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/RuleSetNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/RuleSetNode.java index 3d129f3b228..b7d9f872073 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/RuleSetNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/RuleSetNode.java @@ -35,6 +35,7 @@ import org.kie.api.runtime.KieRuntime; import org.kie.kogito.decision.DecisionModel; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; import static org.jbpm.workflow.instance.rule.RuleType.DRL_LANG; /** @@ -105,7 +106,7 @@ public void validateAddIncomingConnection(final String type, final Connection co "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); @@ -120,7 +121,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] cannot have more than one outgoing connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/Split.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/Split.java index fe2114905c0..77c4a8e7179 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/Split.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/Split.java @@ -26,6 +26,8 @@ import org.jbpm.workflow.core.impl.NodeImpl; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of a split node. * @@ -143,7 +145,7 @@ public void validateAddIncomingConnection(final String type, final Connection co + "] only accepts default incoming connection type!"); } - if (!getIncomingConnections(Node.CONNECTION_DEFAULT_TYPE).isEmpty() && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (!getIncomingConnections(Node.CONNECTION_DEFAULT_TYPE).isEmpty() && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/StartNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/StartNode.java index 3743bbd1573..e6e43a2bb35 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/StartNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/StartNode.java @@ -27,6 +27,8 @@ import org.jbpm.workflow.core.impl.ExtendedNodeImpl; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of a start node. * @@ -92,7 +94,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co throw new IllegalArgumentException( "A start node [" + this.getUniqueId() + ", " + this.getName() + "] only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "A start node [" + this.getUniqueId() + ", " + this.getName() + "] cannot have more than one outgoing connection!"); } diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/SubProcessNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/SubProcessNode.java index 77f873ddc76..7dcad360219 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/SubProcessNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/SubProcessNode.java @@ -27,6 +27,8 @@ import org.jbpm.workflow.core.Node; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of a sub-flow node. * @@ -77,7 +79,7 @@ public void validateAddIncomingConnection(final String type, final Connection co "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); @@ -92,7 +94,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] cannot have more than one outgoing connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/TimerNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/TimerNode.java index e8ce9a54957..5096a47f0ee 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/TimerNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/TimerNode.java @@ -23,6 +23,8 @@ import org.jbpm.workflow.core.impl.ExtendedNodeImpl; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + public class TimerNode extends ExtendedNodeImpl { private static final long serialVersionUID = 510l; @@ -45,7 +47,7 @@ public void validateAddIncomingConnection(final String type, final Connection co "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); @@ -60,7 +62,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] cannot have more than one outgoing connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/WorkItemNode.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/WorkItemNode.java index d7cd3c06c19..5eb00999d0c 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/WorkItemNode.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/WorkItemNode.java @@ -28,6 +28,8 @@ import org.jbpm.workflow.core.Node; import org.kie.api.definition.process.Connection; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Default implementation of a task node. * @@ -66,7 +68,7 @@ public void validateAddIncomingConnection(final String type, final Connection co "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] only accepts default incoming connection type!"); } - if (getFrom() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getFrom() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getTo().getUniqueId() + ", " + connection.getTo().getName() + "] cannot have more than one incoming connection!"); @@ -81,7 +83,7 @@ public void validateAddOutgoingConnection(final String type, final Connection co "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] only accepts default outgoing connection type!"); } - if (getTo() != null && !Boolean.parseBoolean((String) getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (getTo() != null && !WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcess())) { throw new IllegalArgumentException( "This type of node [" + connection.getFrom().getUniqueId() + ", " + connection.getFrom().getName() + "] cannot have more than one outgoing connection!"); diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/WorkflowProcessParameters.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/WorkflowProcessParameters.java new file mode 100644 index 00000000000..cf5c1bd611f --- /dev/null +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/WorkflowProcessParameters.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jbpm.workflow.instance; + +import java.util.function.Function; + +public class WorkflowProcessParameters { + + /** + * Allows activities to have multiple outgoing connections + */ + public static final WorkflowProcessParameter WORKFLOW_PARAM_MULTIPLE_CONNECTIONS = newBooleanParameter("jbpm.enable.multi.con"); + public static final WorkflowProcessParameter WORKFLOW_PARAM_TRANSACTIONS = newBooleanParameter("jbpm.transactions.enable"); + + public static WorkflowProcessParameter newStringParameter(String name) { + return new WorkflowProcessParameter(name, Function.identity()); + } + + public static WorkflowProcessParameter newBooleanParameter(String name) { + return new WorkflowProcessParameter(name, Boolean::parseBoolean); + } + + public static class WorkflowProcessParameter { + private String name; + private Function converter; + + WorkflowProcessParameter(String name, Function converter) { + this.name = name; + this.converter = converter; + } + + public String getName() { + return name; + } + + public T get(org.kie.api.definition.process.Process workflowProcess) { + return converter.apply((String) workflowProcess.getMetaData().get(name)); + } + } +} diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/NodeInstanceImpl.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/NodeInstanceImpl.java index fb73d73c005..fd78e89418f 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/NodeInstanceImpl.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/NodeInstanceImpl.java @@ -61,12 +61,15 @@ import org.kie.kogito.internal.process.runtime.KogitoNodeInstanceContainer; import org.kie.kogito.internal.process.runtime.KogitoProcessContext; import org.kie.kogito.internal.process.runtime.KogitoProcessInstance; +import org.kie.kogito.process.ProcessInstanceExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.jbpm.ruleflow.core.Metadata.HIDDEN; import static org.jbpm.ruleflow.core.Metadata.INCOMING_CONNECTION; import static org.jbpm.ruleflow.core.Metadata.OUTGOING_CONNECTION; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_TRANSACTIONS; import static org.kie.kogito.internal.process.runtime.KogitoProcessInstance.STATE_ACTIVE; /** @@ -248,10 +251,15 @@ public final void trigger(KogitoNodeInstance from, String type) { try { internalTrigger(from, type); } catch (Exception e) { - logger.debug("Node instance causing process instance error in id {}", this.getStringId(), e); - captureError(e); + if (!WORKFLOW_PARAM_TRANSACTIONS.get(getProcessInstance().getProcess())) { + logger.error("Node instance causing process instance error in id {} in a non transactional environment", this.getStringId()); + captureError(e); + return; + } else { + logger.error("Node instance causing process instance error in id {} in a transactional environment (Wrapping)", this.getStringId()); + throw new ProcessInstanceExecutionException(this.getProcessInstance().getId(), this.getNodeDefinitionId(), e.getMessage(), e); + } // stop after capturing error - return; } if (!hidden) { ((InternalProcessRuntime) kruntime.getProcessRuntime()) @@ -260,8 +268,6 @@ public final void trigger(KogitoNodeInstance from, String type) { } protected void captureError(Exception e) { - logger.error("capture error", e); - e.printStackTrace(); getProcessInstance().setErrorState(this, e); } @@ -317,7 +323,7 @@ public void triggerCompleted(String type, boolean remove) { List connections = null; if (node != null) { - if (Boolean.parseBoolean((String) getProcessInstance().getProcess().getMetaData().get("jbpm.enable.multi.con")) && !((NodeImpl) node).getConstraints().isEmpty()) { + if (WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcessInstance().getProcess()) && !((NodeImpl) node).getConstraints().isEmpty()) { int priority; connections = ((NodeImpl) node).getDefaultOutgoingConnections(); boolean found = false; diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java index 48a7276e269..e185f6d0197 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/impl/WorkflowProcessInstanceImpl.java @@ -100,6 +100,7 @@ import org.kie.kogito.process.EventDescription; import org.kie.kogito.process.NamedDataType; import org.kie.kogito.process.ProcessInstance; +import org.kie.kogito.process.ProcessInstanceExecutionException; import org.kie.kogito.process.flexible.AdHocFragment; import org.kie.kogito.process.flexible.ItemDescription; import org.kie.kogito.process.flexible.Milestone; @@ -1316,6 +1317,15 @@ public void internalSetErrorMessage(String errorMessage) { this.errorCause = Optional.empty(); } + public void internalSetError(ProcessInstanceExecutionException e) { + this.nodeIdInError = e.getFailedNodeId(); + Throwable rootException = getRootException(e); + this.errorMessage = rootException instanceof MessageException ? rootException.getMessage() : rootException.getClass().getCanonicalName() + " - " + rootException.getMessage(); + this.errorCause = Optional.of(e); + setState(STATE_ERROR); + ((InternalProcessRuntime) getKnowledgeRuntime().getProcessRuntime()).getProcessEventSupport().fireOnError(this, null, getKnowledgeRuntime(), e); + } + @Override public Collection adHocFragments() { return Stream.of(getNodeContainer().getNodes()) diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java index 9b17022e460..4749aee86ed 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java @@ -54,6 +54,8 @@ import org.mvel2.integration.VariableResolver; import org.mvel2.integration.impl.SimpleValueResolver; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_MULTIPLE_CONNECTIONS; + /** * Runtime counterpart of a for each node. */ @@ -297,7 +299,7 @@ public void internalTrigger(KogitoNodeInstance from, String type) { ((NodeInstanceContainer) getNodeInstanceContainer()).removeNodeInstance(this); if (getForEachNode().isWaitForCompletion()) { - if (!Boolean.parseBoolean((String) getForEachNode().getProcess().getMetaData().get("jbpm.enable.multi.con"))) { + if (!WORKFLOW_PARAM_MULTIPLE_CONNECTIONS.get(getProcessInstance().getProcess())) { triggerConnection(getForEachJoinNode().getTo()); } else { List connections = getForEachJoinNode().getOutgoingConnections(Node.CONNECTION_DEFAULT_TYPE); diff --git a/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/process/ProcessGenerationIT.java b/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/process/ProcessGenerationIT.java index 9c84008fc5c..7737a67acac 100644 --- a/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/process/ProcessGenerationIT.java +++ b/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/process/ProcessGenerationIT.java @@ -34,6 +34,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -82,6 +83,7 @@ import static org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE; import static org.jbpm.workflow.core.impl.ExtendedNodeImpl.EVENT_NODE_ENTER; import static org.jbpm.workflow.core.impl.ExtendedNodeImpl.EVENT_NODE_EXIT; +import static org.jbpm.workflow.instance.WorkflowProcessParameters.WORKFLOW_PARAM_TRANSACTIONS; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -108,7 +110,7 @@ public class ProcessGenerationIT extends AbstractCodegenIT { private static final Collection IGNORED_PROCESS_META = - Arrays.asList("Definitions", "BPMN.Connections", "BPMN.Associations", "ItemDefinitions"); + Arrays.asList("Definitions", "BPMN.Connections", "BPMN.Associations", "ItemDefinitions", WORKFLOW_PARAM_TRANSACTIONS.getName()); private static final Path BASE_PATH = Paths.get("src/test/resources"); static Stream processesProvider() throws IOException { @@ -405,13 +407,15 @@ private static void assertMetadata(Map expected, Map ignoredKeys == null || !ignoredKeys.contains(k)) - .count()); + Predicate precicateIgnoredKeys = Predicate.not(ignoredKeys::contains); + + List currentKeys = current.keySet().stream().filter(precicateIgnoredKeys).toList(); + List expectedKeys = expected.keySet().stream().filter(precicateIgnoredKeys).toList(); + assertThat(currentKeys).containsExactlyElementsOf(expectedKeys); + expected.keySet() .stream() - .filter(k -> ignoredKeys == null || !ignoredKeys.contains(k)) + .filter(precicateIgnoredKeys) .forEach(k -> assertThat(current).as("Metadata " + k).containsEntry(k, expected.get(k))); } diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java index 14bab62579a..20c7189198a 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/ProcessCodegen.java @@ -48,6 +48,8 @@ import org.jbpm.compiler.xml.core.SemanticModules; import org.jbpm.process.core.impl.ProcessImpl; import org.jbpm.process.core.validation.ProcessValidatorRegistry; +import org.jbpm.workflow.core.impl.WorkflowProcessImpl; +import org.jbpm.workflow.instance.WorkflowProcessParameters; import org.kie.api.definition.process.Process; import org.kie.api.definition.process.WorkflowProcess; import org.kie.api.io.Resource; @@ -57,7 +59,9 @@ import org.kie.kogito.codegen.api.SourceFileCodegenBindEvent; import org.kie.kogito.codegen.api.context.ContextAttributesConstants; import org.kie.kogito.codegen.api.context.KogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.JavaKogitoBuildContext; import org.kie.kogito.codegen.api.io.CollectedResource; +import org.kie.kogito.codegen.api.template.TemplatedGenerator; import org.kie.kogito.codegen.core.AbstractGenerator; import org.kie.kogito.codegen.core.DashboardGeneratedFileUtils; import org.kie.kogito.codegen.process.config.ProcessConfigGenerator; @@ -74,11 +78,13 @@ import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; +import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import static java.lang.String.format; import static java.util.stream.Collectors.toList; import static org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_PATH; +import static org.kie.kogito.codegen.process.util.CodegenUtil.isTransactionEnabled; import static org.kie.kogito.grafana.GrafanaConfigurationWriter.buildDashboardName; import static org.kie.kogito.grafana.GrafanaConfigurationWriter.generateOperationalDashboard; import static org.kie.kogito.internal.utils.ConversionUtils.sanitizeClassName; @@ -297,6 +303,12 @@ protected Collection internalGenerate() { // first we generate all the data classes from variable declarations for (WorkflowProcess workFlowProcess : processes.values()) { + // transaction is disabled by default for SW types + boolean defaultTransactionEnabled = !KogitoWorkflowProcess.SW_TYPE.equals(workFlowProcess.getType()); + if (isTransactionEnabled(this, context(), defaultTransactionEnabled)) { + ((WorkflowProcessImpl) workFlowProcess).setMetaData(WorkflowProcessParameters.WORKFLOW_PARAM_TRANSACTIONS.getName(), "true"); + } + if (!skipModelGeneration(workFlowProcess)) { ModelClassGenerator mcg = new ModelClassGenerator(context(), workFlowProcess); processIdToModelGenerator.put(workFlowProcess.getId(), mcg); @@ -308,9 +320,10 @@ protected Collection internalGenerate() { processIdToOutputModelGenerator.put(workFlowProcess.getId(), omcg); } } - + boolean isServerless = false; // then we generate work items task inputs and outputs if any for (WorkflowProcess workFlowProcess : processes.values()) { + isServerless |= KogitoWorkflowProcess.SW_TYPE.equals(workFlowProcess.getType()); if (KogitoWorkflowProcess.SW_TYPE.equals(workFlowProcess.getType())) { continue; } @@ -339,6 +352,7 @@ protected Collection internalGenerate() { } // generate Process, ProcessInstance classes and the REST resource + for (ProcessExecutableModelGenerator execModelGen : processExecutableModelGenerators) { String classPrefix = sanitizeClassName(execModelGen.extractedProcessId()); KogitoWorkflowProcess workFlowProcess = execModelGen.process(); @@ -373,7 +387,7 @@ protected Collection internalGenerate() { .withWorkItems(processIdToWorkItemModel.get(workFlowProcess.getId())) .withSignals(metaData.getSignals()) .withTriggers(metaData.isStartable(), metaData.isDynamic(), metaData.getTriggers()) - .withTransaction(CodegenUtil.isTransactionEnabled(this, context())); + .withTransaction(isTransactionEnabled(this, context())); rgs.add(processResourceGenerator); } @@ -451,6 +465,17 @@ protected Collection internalGenerate() { .forEach((key, value) -> storeFile(PRODUCER_TYPE, key, value)); } + if (CodegenUtil.isTransactionEnabled(this, context()) && !isServerless) { + String template = "ExceptionHandlerTransaction"; + TemplatedGenerator generator = TemplatedGenerator.builder() + .withTemplateBasePath("/class-templates/transaction/") + .withFallbackContext(JavaKogitoBuildContext.CONTEXT_NAME) + .withTargetTypeName(template) + .build(context(), template); + CompilationUnit handler = generator.compilationUnitOrThrow(); + storeFile(MODEL_TYPE, generator.generatedFilePath(), handler.toString()); + } + if (context().hasRESTForGenerator(this)) { for (ProcessResourceGenerator resourceGenerator : rgs) { storeFile(REST_TYPE, resourceGenerator.generatedFilePath(), diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/util/CodegenUtil.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/util/CodegenUtil.java index ecf9737e170..1e33a958615 100644 --- a/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/util/CodegenUtil.java +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/java/org/kie/kogito/codegen/process/util/CodegenUtil.java @@ -22,6 +22,7 @@ import org.kie.kogito.codegen.api.Generator; import org.kie.kogito.codegen.api.context.KogitoBuildContext; +import org.kie.kogito.codegen.api.context.impl.JavaKogitoBuildContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,9 +66,14 @@ public static String globalProperty(String propertyName) { * @see CodegenUtil#getProperty */ public static boolean isTransactionEnabled(Generator generator, KogitoBuildContext context) { - boolean propertyValue = getProperty(generator, context, TRANSACTION_ENABLED, Boolean::parseBoolean, true); - LOG.debug("trying to compute property {} for generator {} property with value {}", TRANSACTION_ENABLED, generator.name(), propertyValue); - return propertyValue; + return isTransactionEnabled(generator, context, true); + } + + public static boolean isTransactionEnabled(Generator generator, KogitoBuildContext context, boolean defaultValue) { + boolean propertyValue = getProperty(generator, context, TRANSACTION_ENABLED, Boolean::parseBoolean, defaultValue); + LOG.debug("Compute property {} for generator {} property with value {}", TRANSACTION_ENABLED, generator.name(), propertyValue); + // java implementation does not have transactions + return !JavaKogitoBuildContext.CONTEXT_NAME.equals(context.name()) && propertyValue; } /** diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionQuarkusTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionQuarkusTemplate.java new file mode 100644 index 00000000000..3230f0d71bc --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionQuarkusTemplate.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.quarkus.workflow.handler; + +import org.jbpm.workflow.instance.impl.WorkflowProcessInstanceImpl; +import org.kie.kogito.Model; +import org.kie.kogito.handler.ExceptionHandler; +import org.kie.kogito.process.MutableProcessInstances; +import org.kie.kogito.process.ProcessInstanceExecutionException; +import org.kie.kogito.process.Processes; +import org.kie.kogito.process.impl.AbstractProcessInstance; +import org.kie.kogito.services.uow.UnitOfWorkExecutor; +import org.kie.kogito.uow.UnitOfWorkManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.transaction.Transactional.TxType; + +@ApplicationScoped +public class ExceptionHandlerTransaction implements ExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandlerTransaction.class); + + @Inject + UnitOfWorkManager unitOfWorkManager; + + @Inject + Instance processesContainer; + + @Override + @Transactional(value = TxType.REQUIRES_NEW) + public void handle(Exception th) { + if (processesContainer.isResolvable()) { + return; + } + + Processes processes = processesContainer.get(); + if (th instanceof ProcessInstanceExecutionException) { + ProcessInstanceExecutionException processInstanceExecutionException = (ProcessInstanceExecutionException) th; + LOG.info("handling exception {} by the handler {}", th, this.getClass().getName()); + UnitOfWorkExecutor.executeInUnitOfWork(unitOfWorkManager, () -> { + String processInstanceId = processInstanceExecutionException.getProcessInstanceId(); + processes.processByProcessInstanceId(processInstanceId).ifPresent(processDefinition -> { + processDefinition.instances().findById(processInstanceId).ifPresent(instance -> { + AbstractProcessInstance processInstance = ((AbstractProcessInstance) instance); + ((WorkflowProcessInstanceImpl) processInstance.internalGetProcessInstance()).internalSetError(processInstanceExecutionException); + ((MutableProcessInstances) processDefinition.instances()).update(processInstanceId, processInstance); + }); + + }); + + return null; + }); + } + } + +} diff --git a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionSpringTemplate.java b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionSpringTemplate.java new file mode 100644 index 00000000000..d9996d62f16 --- /dev/null +++ b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/transaction/ExceptionHandlerTransactionSpringTemplate.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.process.handler; + +import org.jbpm.workflow.instance.impl.WorkflowProcessInstanceImpl; +import org.kie.kogito.Model; +import org.kie.kogito.handler.ExceptionHandler; +import org.kie.kogito.process.MutableProcessInstances; +import org.kie.kogito.process.ProcessInstanceExecutionException; +import org.kie.kogito.process.Processes; +import org.kie.kogito.process.impl.AbstractProcessInstance; +import org.kie.kogito.services.uow.UnitOfWorkExecutor; +import org.kie.kogito.uow.UnitOfWorkManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class ExceptionHandlerTransaction implements ExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandlerTransaction.class); + + @Autowired + UnitOfWorkManager unitOfWorkManager; + + @Autowired(required = false) + Processes processes; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void handle(Exception th) { + if (processes == null) { + return; + } + if (th instanceof ProcessInstanceExecutionException) { + ProcessInstanceExecutionException processInstanceExecutionException = (ProcessInstanceExecutionException) th; + LOG.info("handling exception {} by the handler {}", th, this.getClass().getName()); + UnitOfWorkExecutor.executeInUnitOfWork(unitOfWorkManager, () -> { + String processInstanceId = processInstanceExecutionException.getProcessInstanceId(); + processes.processByProcessInstanceId(processInstanceId).ifPresent(processDefinition -> { + processDefinition.instances().findById(processInstanceId).ifPresent(instance -> { + AbstractProcessInstance processInstance = ((AbstractProcessInstance) instance); + ((WorkflowProcessInstanceImpl) processInstance.internalGetProcessInstance()).internalSetError(processInstanceExecutionException); + ((MutableProcessInstances) processDefinition.instances()).update(processInstanceId, processInstance); + }); + + }); + + return null; + }); + } + } + +} diff --git a/quarkus/addons/rest-exception-handler/pom.xml b/quarkus/addons/rest-exception-handler/pom.xml index a5419789ab6..03f28e92666 100644 --- a/quarkus/addons/rest-exception-handler/pom.xml +++ b/quarkus/addons/rest-exception-handler/pom.xml @@ -51,6 +51,10 @@ jakarta.inject-api provided + + jakarta.enterprise + jakarta.enterprise.cdi-api + org.mockito mockito-junit-jupiter diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionMapper.java index 21b70e76d74..09a44058b31 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/BaseExceptionMapper.java @@ -23,13 +23,6 @@ public abstract class BaseExceptionMapper implements ExceptionMapper { - protected ExceptionsHandler exceptionsHandler; - - protected BaseExceptionMapper() { - this.exceptionsHandler = new ExceptionsHandler(); - } - @Override - @SuppressWarnings("squid:S3038") public abstract Response toResponse(E e); } diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandler.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandler.java index ec1bd6679dd..2d1afaf5780 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandler.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandler.java @@ -18,54 +18,60 @@ */ package org.kie.kogito.resource.exceptions; +import org.kie.kogito.handler.ExceptionHandler; + import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -public class ExceptionsHandler extends BaseExceptionsHandler { +public class ExceptionsHandler extends AbstractExceptionsHandler { + + public ExceptionsHandler(Iterable handlers) { + super(handlers); + } @Override - protected Response badRequest(R body) { + protected Response badRequest(ExceptionBodyMessage body) { return Response .status(Response.Status.BAD_REQUEST) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .entity(body) + .entity(body.getBody()) .build(); } @Override - protected Response conflict(R body) { + protected Response conflict(ExceptionBodyMessage body) { return Response .status(Response.Status.CONFLICT) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .entity(body) + .entity(body.getBody()) .build(); } @Override - protected Response internalError(R body) { + protected Response internalError(ExceptionBodyMessage body) { return Response .status(Response.Status.INTERNAL_SERVER_ERROR) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .entity(body) + .entity(body.getBody()) .build(); } @Override - protected Response notFound(R body) { + protected Response notFound(ExceptionBodyMessage body) { return Response .status(Response.Status.NOT_FOUND) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .entity(body) + .entity(body.getBody()) .build(); } @Override - protected Response forbidden(R body) { + protected Response forbidden(ExceptionBodyMessage body) { return Response .status(Response.Status.FORBIDDEN) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .entity(body) + .entity(body.getBody()) .build(); } } diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerProducer.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerProducer.java new file mode 100644 index 00000000000..a207c680723 --- /dev/null +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerProducer.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.resource.exceptions; + +import org.kie.kogito.handler.ExceptionHandler; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; + +public class ExceptionsHandlerProducer { + + @Produces + public ExceptionsHandler newExceptionsHandler(Instance handlers) { + return new ExceptionsHandler(handlers); + } +} diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/IllegalArgumentExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/IllegalArgumentExceptionMapper.java index 56a734a511a..e019fd12f96 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/IllegalArgumentExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/IllegalArgumentExceptionMapper.java @@ -18,12 +18,16 @@ */ package org.kie.kogito.resource.exceptions; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class IllegalArgumentExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(IllegalArgumentException e) { return exceptionsHandler.mapException(e); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidLifeCyclePhaseExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidLifeCyclePhaseExceptionMapper.java index 82341b6eecd..7d5b5a938a7 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidLifeCyclePhaseExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidLifeCyclePhaseExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.internal.process.workitem.InvalidLifeCyclePhaseException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class InvalidLifeCyclePhaseExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(InvalidLifeCyclePhaseException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidTransitionExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidTransitionExceptionMapper.java index ff04b15e15f..abd80e367db 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidTransitionExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/InvalidTransitionExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.internal.process.workitem.InvalidTransitionException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class InvalidTransitionExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(InvalidTransitionException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeInstanceNotFoundExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeInstanceNotFoundExceptionMapper.java index 698a1155e7d..891c60fb7c0 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeInstanceNotFoundExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeInstanceNotFoundExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.process.NodeInstanceNotFoundException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class NodeInstanceNotFoundExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(NodeInstanceNotFoundException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeNotFoundExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeNotFoundExceptionMapper.java index 182e1df839f..c41b0397fef 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeNotFoundExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NodeNotFoundExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.process.NodeNotFoundException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class NodeNotFoundExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(NodeNotFoundException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NotAuthorizedExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NotAuthorizedExceptionMapper.java index 030783daa6e..2831677feb8 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NotAuthorizedExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/NotAuthorizedExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.internal.process.workitem.NotAuthorizedException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class NotAuthorizedExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(NotAuthorizedException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceDuplicatedExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceDuplicatedExceptionMapper.java index ed9bb35c967..6c54bf486e0 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceDuplicatedExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceDuplicatedExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.process.ProcessInstanceDuplicatedException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class ProcessInstanceDuplicatedExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(ProcessInstanceDuplicatedException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceExecutionExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceExecutionExceptionMapper.java index 9e3f41ef953..3ff0d6449c5 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceExecutionExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceExecutionExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.process.ProcessInstanceExecutionException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class ProcessInstanceExecutionExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(ProcessInstanceExecutionException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceNotFoundExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceNotFoundExceptionMapper.java index 46b511d6b35..d0e31c3fc52 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceNotFoundExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/ProcessInstanceNotFoundExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.process.ProcessInstanceNotFoundException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class ProcessInstanceNotFoundExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(ProcessInstanceNotFoundException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotAuthorizedExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotAuthorizedExceptionMapper.java index 98710328810..be350a1d6df 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotAuthorizedExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotAuthorizedExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.usertask.UserTaskInstanceNotFoundException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class UserTaskInstanceNotAuthorizedExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(UserTaskInstanceNotFoundException e) { return exceptionsHandler.mapException(e); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotFoundExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotFoundExceptionMapper.java index d74ddf1fc5c..196bd85535b 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotFoundExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskInstanceNotFoundExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.usertask.UserTaskInstanceNotAuthorizedException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class UserTaskInstanceNotFoundExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(UserTaskInstanceNotAuthorizedException e) { return exceptionsHandler.mapException(e); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java index 233d3cd679a..063439ead6e 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.usertask.UserTaskInstanceNotFoundException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class UserTaskTransitionExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(UserTaskInstanceNotFoundException e) { return exceptionsHandler.mapException(e); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/VariableViolationExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/VariableViolationExceptionMapper.java index 85ab766ee42..fdea3253351 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/VariableViolationExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/VariableViolationExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.process.VariableViolationException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class VariableViolationExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(VariableViolationException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemExecutionExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemExecutionExceptionMapper.java index 1ffb619cebb..bcd9f18ede5 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemExecutionExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemExecutionExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.internal.process.workitem.WorkItemExecutionException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class WorkItemExecutionExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(WorkItemExecutionException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemNotFoundExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemNotFoundExceptionMapper.java index a0d0731dcc3..000e0f748a7 100644 --- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemNotFoundExceptionMapper.java +++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/WorkItemNotFoundExceptionMapper.java @@ -20,12 +20,16 @@ import org.kie.kogito.internal.process.workitem.WorkItemNotFoundException; +import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; @Provider public class WorkItemNotFoundExceptionMapper extends BaseExceptionMapper { + @Inject + ExceptionsHandler exceptionsHandler; + @Override public Response toResponse(WorkItemNotFoundException exception) { return exceptionsHandler.mapException(exception); diff --git a/quarkus/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerTest.java b/quarkus/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerTest.java index 028a91a91b5..156d8aae68c 100644 --- a/quarkus/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerTest.java +++ b/quarkus/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/ExceptionsHandlerTest.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.resource.exceptions; +import java.util.ArrayList; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,7 +42,7 @@ class ExceptionsHandlerTest { private ExceptionsHandler tested; @Mock - private Object body; + private ExceptionBodyMessage body; @Mock private RuntimeDelegate runtimeDelegate; @@ -53,7 +55,7 @@ class ExceptionsHandlerTest { @BeforeEach void setUp() { - tested = new ExceptionsHandler(); + tested = new ExceptionsHandler(new ArrayList<>()); RuntimeDelegate.setInstance(runtimeDelegate); when(runtimeDelegate.createResponseBuilder()).thenReturn(builder); when(builder.status(any(Response.StatusType.class))).thenReturn(builder); @@ -71,7 +73,7 @@ void testBadRequest() { private void assertRequest(Response.Status status) { verify(builder).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); verify(builder).status((Response.StatusType) status); - verify(builder).entity(body); + verify(builder).entity(body.getBody()); } @Test diff --git a/quarkus/addons/source-files/runtime/src/main/java/org/kie/kogito/addon/source/files/SourceFilesResource.java b/quarkus/addons/source-files/runtime/src/main/java/org/kie/kogito/addon/source/files/SourceFilesResource.java index 6165d773ab2..29ca134938c 100644 --- a/quarkus/addons/source-files/runtime/src/main/java/org/kie/kogito/addon/source/files/SourceFilesResource.java +++ b/quarkus/addons/source-files/runtime/src/main/java/org/kie/kogito/addon/source/files/SourceFilesResource.java @@ -19,11 +19,10 @@ package org.kie.kogito.addon.source.files; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.util.Collection; +import java.util.Optional; -import org.kie.kogito.resource.exceptions.ExceptionsHandler; import org.kie.kogito.source.files.SourceFile; import org.kie.kogito.source.files.SourceFilesProvider; @@ -41,24 +40,24 @@ @Path("/management/processes/") public final class SourceFilesResource { - private static final ExceptionsHandler EXCEPTIONS_HANDLER = new ExceptionsHandler(); - SourceFilesProvider sourceFilesProvider; @GET @Path("sources") @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response getSourceFileByUri(@QueryParam("uri") String uri) { - return sourceFilesProvider.getSourceFilesByUri(uri) - .map(sourceFile -> { - try (InputStream file = new ByteArrayInputStream(sourceFile.readContents())) { - return Response.ok(file, MediaType.APPLICATION_OCTET_STREAM) - .header("Content-Disposition", "inline; filename=\"" + java.nio.file.Path.of(sourceFile.getUri()).getFileName() + "\"") - .build(); - } catch (Exception e) { - return EXCEPTIONS_HANDLER.mapException(e); - } - }).orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build()); + public Response getSourceFileByUri(@QueryParam("uri") String uri) throws Exception { + Optional sourceFile = sourceFilesProvider.getSourceFilesByUri(uri); + + if (sourceFile.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + try (InputStream file = new ByteArrayInputStream(sourceFile.get().readContents())) { + return Response.ok(file, MediaType.APPLICATION_OCTET_STREAM) + .header("Content-Disposition", "inline; filename=\"" + java.nio.file.Path.of(sourceFile.get().getUri()).getFileName() + "\"") + .build(); + } + } @GET @@ -71,15 +70,15 @@ public Collection getSourceFilesByProcessId(@PathParam("processId") @GET @Path("{processId}/source") @Produces(MediaType.TEXT_PLAIN) - public Response getSourceFileByProcessId(@PathParam("processId") String processId) { - return sourceFilesProvider.getProcessSourceFile(processId) - .map(sourceFile -> { - try { - return Response.ok(sourceFile.readContents()).build(); - } catch (IOException e) { - return EXCEPTIONS_HANDLER.mapException(e); - } - }).orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build()); + public Response getSourceFileByProcessId(@PathParam("processId") String processId) throws Exception { + Optional sourceFile = sourceFilesProvider.getProcessSourceFile(processId); + + if (sourceFile.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return Response.ok(sourceFile.get().readContents()).build(); + } @Inject diff --git a/quarkus/addons/source-files/runtime/src/test/java/org/kie/kogito/addon/source/files/SourceFilesResourceTest.java b/quarkus/addons/source-files/runtime/src/test/java/org/kie/kogito/addon/source/files/SourceFilesResourceTest.java index 642bf95be8a..2968fe78d64 100644 --- a/quarkus/addons/source-files/runtime/src/test/java/org/kie/kogito/addon/source/files/SourceFilesResourceTest.java +++ b/quarkus/addons/source-files/runtime/src/test/java/org/kie/kogito/addon/source/files/SourceFilesResourceTest.java @@ -58,14 +58,14 @@ void getSourceFilesByProcessIdTest() { } @Test - void getEmptySourceFileByProcessIdTest() { + void getEmptySourceFileByProcessIdTest() throws Exception { when(mockSourceFileProvider.getProcessSourceFile(PROCESS_ID)).thenReturn(Optional.empty()); assertThat(sourceFilesTestResource.getSourceFileByProcessId(PROCESS_ID).getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); verify(mockSourceFileProvider).getProcessSourceFile(PROCESS_ID); } @Test - void getValidSourceFileByProcessIdTest() { + void getValidSourceFileByProcessIdTest() throws Exception { when(mockSourceFileProvider.getProcessSourceFile(PROCESS_ID)).thenReturn(Optional.of(new SourceFile("petstore.sw.json"))); assertThat(sourceFilesTestResource.getSourceFileByProcessId(PROCESS_ID).getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); verify(mockSourceFileProvider).getProcessSourceFile(PROCESS_ID); diff --git a/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java b/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java index d5d5dfa96e4..9872513141e 100644 --- a/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java +++ b/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java @@ -18,6 +18,9 @@ */ package org.kie.kogito.resource.exceptions.springboot; +import java.util.List; +import java.util.Map; + import org.kie.kogito.internal.process.workitem.InvalidLifeCyclePhaseException; import org.kie.kogito.internal.process.workitem.InvalidTransitionException; import org.kie.kogito.internal.process.workitem.NotAuthorizedException; @@ -29,7 +32,9 @@ import org.kie.kogito.process.ProcessInstanceExecutionException; import org.kie.kogito.process.ProcessInstanceNotFoundException; import org.kie.kogito.process.VariableViolationException; -import org.kie.kogito.resource.exceptions.BaseExceptionsHandler; +import org.kie.kogito.resource.exceptions.AbstractExceptionsHandler; +import org.kie.kogito.resource.exceptions.ExceptionBodyMessage; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -37,105 +42,111 @@ import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice -public class ExceptionsHandler extends BaseExceptionsHandler { +public class ExceptionsHandler extends AbstractExceptionsHandler>> { + + @Autowired + public ExceptionsHandler(List handlers) { + super(handlers); + } @Override - protected ResponseEntity badRequest(R body) { + protected ResponseEntity> badRequest(ExceptionBodyMessage body) { return ResponseEntity .badRequest() .contentType(MediaType.APPLICATION_JSON) - .body(body); + .body(body.getBody()); } @Override - protected ResponseEntity conflict(R body) { + protected ResponseEntity> conflict(ExceptionBodyMessage body) { return ResponseEntity .status(HttpStatus.CONFLICT) .contentType(MediaType.APPLICATION_JSON) - .body(body); + .body(body.getBody()); } @Override - protected ResponseEntity internalError(R body) { + protected ResponseEntity> internalError(ExceptionBodyMessage body) { return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .contentType(MediaType.APPLICATION_JSON) - .body(body); + .body(body.getBody()); } @Override - protected ResponseEntity notFound(R body) { + protected ResponseEntity> notFound(ExceptionBodyMessage body) { return ResponseEntity .status(HttpStatus.NOT_FOUND) .contentType(MediaType.APPLICATION_JSON) - .body(body); + .body(body.getBody()); } @Override - protected ResponseEntity forbidden(R body) { + protected ResponseEntity> forbidden(ExceptionBodyMessage body) { return ResponseEntity .status(HttpStatus.FORBIDDEN) .contentType(MediaType.APPLICATION_JSON) - .body(body); + .body(body.getBody()); } @ExceptionHandler(InvalidLifeCyclePhaseException.class) - public ResponseEntity toResponse(InvalidLifeCyclePhaseException exception) { + public ResponseEntity> toResponse(InvalidLifeCyclePhaseException exception) { return mapException(exception); } @ExceptionHandler(InvalidTransitionException.class) - public ResponseEntity toResponse(InvalidTransitionException exception) { + public ResponseEntity> toResponse(InvalidTransitionException exception) { return mapException(exception); } @ExceptionHandler(NodeInstanceNotFoundException.class) - public ResponseEntity toResponse(NodeInstanceNotFoundException exception) { + public ResponseEntity> toResponse(NodeInstanceNotFoundException exception) { return mapException(exception); } @ExceptionHandler(NodeNotFoundException.class) - public ResponseEntity toResponse(NodeNotFoundException exception) { + public ResponseEntity> toResponse(NodeNotFoundException exception) { return mapException(exception); } @ExceptionHandler(NotAuthorizedException.class) - public ResponseEntity toResponse(NotAuthorizedException exception) { + public ResponseEntity> toResponse(NotAuthorizedException exception) { return mapException(exception); } @ExceptionHandler(ProcessInstanceDuplicatedException.class) - public ResponseEntity toResponse(ProcessInstanceDuplicatedException exception) { + public ResponseEntity> toResponse(ProcessInstanceDuplicatedException exception) { return mapException(exception); } @ExceptionHandler(ProcessInstanceExecutionException.class) - public ResponseEntity toResponse(ProcessInstanceExecutionException exception) { + public ResponseEntity> toResponse(ProcessInstanceExecutionException exception) { return mapException(exception); } @ExceptionHandler(ProcessInstanceNotFoundException.class) - public ResponseEntity toResponse(ProcessInstanceNotFoundException exception) { + public ResponseEntity> toResponse(ProcessInstanceNotFoundException exception) { return mapException(exception); } @ExceptionHandler(WorkItemNotFoundException.class) - public ResponseEntity toResponse(WorkItemNotFoundException exception) { + public ResponseEntity> toResponse(WorkItemNotFoundException exception) { return mapException(exception); } @ExceptionHandler(WorkItemExecutionException.class) - public ResponseEntity toResponse(WorkItemExecutionException exception) { + public ResponseEntity> toResponse(WorkItemExecutionException exception) { return mapException(exception); } @ExceptionHandler(VariableViolationException.class) - public ResponseEntity toResponse(VariableViolationException exception) { + public ResponseEntity> toResponse(VariableViolationException exception) { return mapException(exception); } @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity toResponse(IllegalArgumentException exception) { + public ResponseEntity> toResponse(IllegalArgumentException exception) { return mapException(exception); } + } diff --git a/springboot/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandlerTest.java b/springboot/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandlerTest.java index 4512a30b7fc..10c82906203 100644 --- a/springboot/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandlerTest.java +++ b/springboot/addons/rest-exception-handler/src/test/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandlerTest.java @@ -18,6 +18,9 @@ */ package org.kie.kogito.resource.exceptions.springboot; +import java.util.ArrayList; +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +32,7 @@ import org.kie.kogito.process.ProcessInstanceExecutionException; import org.kie.kogito.process.ProcessInstanceNotFoundException; import org.kie.kogito.process.VariableViolationException; +import org.kie.kogito.resource.exceptions.ExceptionBodyMessage; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; @@ -44,45 +48,45 @@ class ExceptionsHandlerTest { private ExceptionsHandler tested; @Mock - private Object body; + private ExceptionBodyMessage body; @BeforeEach void setUp() { - tested = spy(new ExceptionsHandler()); + tested = spy(new ExceptionsHandler(new ArrayList<>())); } @Test void testBadRequest() { - ResponseEntity responseEntity = tested.badRequest(body); + ResponseEntity> responseEntity = tested.badRequest(body); assertResponse(responseEntity, HttpStatus.BAD_REQUEST); } - private void assertResponse(ResponseEntity responseEntity, HttpStatus status) { + private void assertResponse(ResponseEntity> responseEntity, HttpStatus status) { assertThat(responseEntity.getStatusCode()).isEqualTo(status); - assertThat(responseEntity.getBody()).isEqualTo(body); + assertThat(responseEntity.getBody()).isEqualTo(body.getBody()); } @Test void testConflict() { - ResponseEntity responseEntity = tested.conflict(body); + ResponseEntity> responseEntity = tested.conflict(body); assertResponse(responseEntity, HttpStatus.CONFLICT); } @Test void testIternalError() { - ResponseEntity responseEntity = tested.internalError(body); + ResponseEntity> responseEntity = tested.internalError(body); assertResponse(responseEntity, HttpStatus.INTERNAL_SERVER_ERROR); } @Test void testNotFound() { - ResponseEntity responseEntity = tested.badRequest(body); + ResponseEntity> responseEntity = tested.badRequest(body); assertResponse(responseEntity, HttpStatus.BAD_REQUEST); } @Test void testForbidden() { - ResponseEntity responseEntity = tested.forbidden(body); + ResponseEntity> responseEntity = tested.forbidden(body); assertResponse(responseEntity, HttpStatus.FORBIDDEN); } diff --git a/springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java b/springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java index b823c16d26b..d1875057a9d 100644 --- a/springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java +++ b/springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java @@ -22,6 +22,9 @@ import org.kie.kogito.correlation.CorrelationService; import org.kie.kogito.event.correlation.DefaultCorrelationService; import org.kie.kogito.process.version.ProjectVersionProcessVersionResolver; +import org.kie.kogito.services.uow.CollectingUnitOfWorkFactory; +import org.kie.kogito.services.uow.DefaultUnitOfWorkManager; +import org.kie.kogito.uow.UnitOfWorkManager; import org.kogito.workitem.rest.RestWorkItemHandlerUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -53,6 +56,12 @@ ProcessVersionResolver projectVersionResolver() { return new ProjectVersionProcessVersionResolver(configBean.getGav().orElseThrow(() -> new RuntimeException("Unable to use kogito.workflow.version-strategy without a project GAV"))); } + @Bean + @ConditionalOnMissingBean(UnitOfWorkManager.class) + UnitOfWorkManager unitOfWorkManager() { + return new DefaultUnitOfWorkManager(new CollectingUnitOfWorkFactory()); + } + @Bean @ConditionalOnMissingBean(WebClientOptions.class) WebClientOptions sslDefaultOptions() { From a2e562392f576d1783021c8a09ddc5bff432382b Mon Sep 17 00:00:00 2001 From: Matheus Cruz <56329339+mcruzdev@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:19:58 -0300 Subject: [PATCH 13/16] [incubator-kie-issues#1534] Remove gradle IT tests (#3724) --- .mvn/maven.config | 3 - .mvn/wrapper/MavenWrapperDownloader.java | 117 --------- .mvn/wrapper/maven-wrapper.jar | Bin 50710 -> 0 bytes .mvn/wrapper/maven-wrapper.properties | 21 -- .../README.md | 26 -- .../.gitignore | 56 ----- .../build.gradle | 58 ----- .../gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 24 -- .../gradlew | 234 ------------------ .../gradlew.bat | 89 ------- .../settings.gradle | 30 --- .../resources/org/acme/travels/scripts.bpmn | 180 -------------- .../src/main/resources/gradle.properties | 27 -- quarkus/integration-tests/pom.xml | 1 - 15 files changed, 866 deletions(-) delete mode 100644 .mvn/maven.config delete mode 100644 .mvn/wrapper/MavenWrapperDownloader.java delete mode 100644 .mvn/wrapper/maven-wrapper.jar delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/README.md delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/.gitignore delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/build.gradle delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradle/wrapper/gradle-wrapper.jar delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradle/wrapper/gradle-wrapper.properties delete mode 100755 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew delete mode 100755 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew.bat delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/settings.gradle delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/src/main/resources/org/acme/travels/scripts.bpmn delete mode 100644 quarkus/integration-tests/integration-tests-quarkus-gradle/src/main/resources/gradle.properties diff --git a/.mvn/maven.config b/.mvn/maven.config deleted file mode 100644 index 0ea53cd508b..00000000000 --- a/.mvn/maven.config +++ /dev/null @@ -1,3 +0,0 @@ --Dmaven.wagon.httpconnectionManager.ttlSeconds=120 --Dmaven.wagon.http.retryHandler.requestSentEnabled=true --Dmaven.wagon.http.retryHandler.count=10 \ No newline at end of file diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index b901097f2db..00000000000 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50710 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 EfAhh=DF6Tf diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 661b1c52443..00000000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,21 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/README.md b/quarkus/integration-tests/integration-tests-quarkus-gradle/README.md deleted file mode 100644 index 8b75c6300ea..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/README.md +++ /dev/null @@ -1,26 +0,0 @@ - - -Quarkus Gradle Integration Test -=============================== - -This project will run `./gradlew --no-daemon quarkusBuild` in a specific subdirectory, -configured in the Maven property `${gradle.project.dir}` using the Maven exec plugin. - -You may duplicate this project to add more test cases. diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/.gitignore b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/.gitignore deleted file mode 100644 index 91886e2401b..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/.gitignore +++ /dev/null @@ -1,56 +0,0 @@ -### -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -### - -# Gradle -.gradle/ -build/ -gradle.properties - -# Eclipse -.project -.classpath -.settings/ -bin/ - -# IntelliJ -.idea -*.ipr -*.iml -*.iws - -# NetBeans -nb-configuration.xml - -# Visual Studio Code -.vscode -.factorypath - -# OSX -.DS_Store - -# Vim -*.swp -*.swo - -# patch -*.orig -*.rej - -# Local environment -.env diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/build.gradle b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/build.gradle deleted file mode 100644 index 7aba25d9855..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -plugins { - id 'java' - id 'io.quarkus' -} - -repositories { - mavenCentral() - mavenLocal() -} - -dependencies { - implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") - implementation "org.kie.kogito:kogito-bom:${kogitoVersion}" - - implementation "org.jbpm:jbpm-quarkus:${kogitoVersion}" - implementation 'io.quarkus:quarkus-arc' - implementation 'io.quarkus:quarkus-resteasy' - implementation 'io.quarkus:quarkus-resteasy-jackson' - implementation 'org.eclipse.microprofile.openapi:microprofile-openapi-api' - testImplementation 'io.quarkus:quarkus-junit5' - testImplementation 'io.rest-assured:rest-assured' -} - -group 'org.kie.kogito.examples' -version '1.0.0-SNAPSHOT' - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -compileJava { - options.encoding = 'UTF-8' - options.compilerArgs << '-parameters' -} - -compileTestJava { - options.encoding = 'UTF-8' -} diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradle/wrapper/gradle-wrapper.jar b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927a4d4fb3f96a785543079b8df6723c946b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59821 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 diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradle/wrapper/gradle-wrapper.properties b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a6a3d0f4258..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,24 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew deleted file mode 100755 index 1b6c787337f..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew.bat b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew.bat deleted file mode 100755 index ac1b06f9382..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/settings.gradle b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/settings.gradle deleted file mode 100644 index 003e9b8fb8d..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/settings.gradle +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -pluginManagement { - repositories { - mavenLocal() - mavenCentral() - gradlePluginPortal() - } - plugins { - id "${quarkusPluginId}" version "${quarkusPluginVersion}" - } -} -rootProject.name='integration-tests-quarkus-gradle-project' diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/src/main/resources/org/acme/travels/scripts.bpmn b/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/src/main/resources/org/acme/travels/scripts.bpmn deleted file mode 100644 index 9b546ebf679..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/src/main/resources/org/acme/travels/scripts.bpmn +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _FEBCC225-139C-47B0-BA16-7878EEE3532D - - - - - - - - _80DA9F1E-743F-4BAF-BFF3-67DDC905E3B7 - - - - - - - - _80DA9F1E-743F-4BAF-BFF3-67DDC905E3B7 - _B4A85C14-EA42-43CA-9BA2-CB92E32FACF5 - - - - - - - - - _B4A85C14-EA42-43CA-9BA2-CB92E32FACF5 - _FEBCC225-139C-47B0-BA16-7878EEE3532D - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _xvWdMEemEeq1Q5UMtA4RFA - _xvWdMEemEeq1Q5UMtA4RFA - - diff --git a/quarkus/integration-tests/integration-tests-quarkus-gradle/src/main/resources/gradle.properties b/quarkus/integration-tests/integration-tests-quarkus-gradle/src/main/resources/gradle.properties deleted file mode 100644 index 5f93ba51cbe..00000000000 --- a/quarkus/integration-tests/integration-tests-quarkus-gradle/src/main/resources/gradle.properties +++ /dev/null @@ -1,27 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -# Gradle properties -quarkusPluginId=io.quarkus -quarkusPluginVersion=${version.io.quarkus} -quarkusPlatformGroupId=io.quarkus -quarkusPlatformArtifactId=quarkus-bom -quarkusPlatformVersion=${version.io.quarkus} -kogitoVersion=${project.version} -org.gradle.jvmargs=-Xmx512m -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError -Djava.awt.headless=true diff --git a/quarkus/integration-tests/pom.xml b/quarkus/integration-tests/pom.xml index 48ba322a8bd..bd9d50e860e 100644 --- a/quarkus/integration-tests/pom.xml +++ b/quarkus/integration-tests/pom.xml @@ -50,7 +50,6 @@ integration-tests-quarkus-processes-persistence integration-tests-quarkus-usertasks integration-tests-quarkus-source-files - integration-tests-quarkus-gradle \ No newline at end of file From db23327dd4cf10ff11a04d7be7d4c2e4b06dba6c Mon Sep 17 00:00:00 2001 From: Matheus Cruz <56329339+mcruzdev@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:26:12 -0300 Subject: [PATCH 14/16] [Fix #3723] Add resteasy-multipart-provider (#3743) Co-authored-by: Matheus Cruz --- .../kogito-quarkus-serverless-workflow/pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml index 8da03ff0f7b..50fc7162730 100644 --- a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml @@ -59,22 +59,22 @@ io.quarkus quarkus-rest-client - io.quarkus quarkus-rest-client-jackson - io.quarkus quarkus-smallrye-openapi - io.quarkus quarkus-grpc - + + org.jboss.resteasy + resteasy-multipart-provider + org.kie From cf64c1d95efb422d2b6583befab3f7afc6dc77fd Mon Sep 17 00:00:00 2001 From: Walter Medvedeo Date: Tue, 5 Nov 2024 17:56:51 +0100 Subject: [PATCH 15/16] [NO_ISSUE] Set the kie.flyway.enabled to false by default. (#3762) --- .../org/kie/flyway/quarkus/KieFlywayQuarkusRuntimeConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quarkus/addons/flyway/runtime/src/main/java/org/kie/flyway/quarkus/KieFlywayQuarkusRuntimeConfig.java b/quarkus/addons/flyway/runtime/src/main/java/org/kie/flyway/quarkus/KieFlywayQuarkusRuntimeConfig.java index 3f79c9643e5..5819fa3a7cc 100644 --- a/quarkus/addons/flyway/runtime/src/main/java/org/kie/flyway/quarkus/KieFlywayQuarkusRuntimeConfig.java +++ b/quarkus/addons/flyway/runtime/src/main/java/org/kie/flyway/quarkus/KieFlywayQuarkusRuntimeConfig.java @@ -35,7 +35,7 @@ public class KieFlywayQuarkusRuntimeConfig implements KieFlywayConfiguration Date: Wed, 6 Nov 2024 14:41:26 +0100 Subject: [PATCH 16/16] [Fix #3763] Properly reading compressdata string header (#3764) * [Fix #3763] Properly reading compressdata string header * [Fix #3763] Adding unit test --- .../MultipleProcessInstanceDataEvent.java | 12 +- ...leProcessDataInstanceConverterFactory.java | 22 ++- ...pleProcessInstanceDataEventSerializer.java | 4 +- .../event/process/ProcessEventsTest.java | 126 +++++++++++++++--- 4 files changed, 139 insertions(+), 25 deletions(-) diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java index f29a920c132..a2783038b2a 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/process/MultipleProcessInstanceDataEvent.java @@ -35,8 +35,16 @@ public MultipleProcessInstanceDataEvent(URI source, Collection>> toCloudEvent(MultipleProcessInstanceDataEvent event, ObjectMapper objectMapper) { + if (MultipleProcessInstanceDataEvent.BINARY_CONTENT_TYPE.equals(event.getDataContentType())) { + return event.isCompressed() ? compressedToBytes : binaryToBytes; + } else { + return objectMapper::writeValueAsBytes; + } + } + public static Converter>> fromCloudEvent(CloudEvent cloudEvent, ObjectMapper objectMapper) { if (MultipleProcessInstanceDataEvent.BINARY_CONTENT_TYPE.equals(cloudEvent.getDataContentType())) { return isCompressed(cloudEvent) ? compressedConverter : binaryConverter; @@ -49,10 +57,13 @@ public static Converter>> compressedToBytes = data -> serialize(data, true); + + private static ToBytes>> binaryToBytes = data -> serialize(data, false); + private static Converter>> binaryConverter = data -> deserialize(data, false); @@ -62,4 +73,9 @@ private static boolean isCompressed(CloudEvent event) { private static Collection> deserialize(CloudEventData data, boolean compress) throws IOException { return MultipleProcessInstanceDataEventDeserializer.readFromBytes(Base64.getDecoder().decode(data.toBytes()), compress); } + + private static byte[] serialize(Collection> data, + boolean compress) throws IOException { + return Base64.getEncoder().encode(MultipleProcessInstanceDataEventSerializer.dataAsBytes(data, compress)); + } } diff --git a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java index 42825e9679c..42b219b46f1 100644 --- a/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java +++ b/api/kogito-events-core/src/main/java/org/kie/kogito/event/serializer/MultipleProcessInstanceDataEventSerializer.java @@ -62,14 +62,14 @@ public void serialize(MultipleProcessInstanceDataEvent value, JsonGenerator gen, if (compress) { gen.writeBooleanField(MultipleProcessInstanceDataEvent.COMPRESS_DATA, true); } - gen.writeBinaryField("data", dataAsBytes(gen, value.getData(), compress)); + gen.writeBinaryField("data", dataAsBytes(value.getData(), compress)); gen.writeEndObject(); } else { defaultSerializer.serialize(value, gen, serializers); } } - private byte[] dataAsBytes(JsonGenerator gen, Collection> data, boolean compress) throws IOException { + static byte[] dataAsBytes(Collection> data, boolean compress) throws IOException { ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); try (DataOutputStream out = new DataOutputStream(compress ? new GZIPOutputStream(bytesOut) : bytesOut)) { logger.trace("Writing size {}", data.size()); diff --git a/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java b/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java index d278e2c4279..50d2f6e8674 100644 --- a/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java +++ b/api/kogito-events-core/src/test/java/org/kie/kogito/event/process/ProcessEventsTest.java @@ -22,8 +22,11 @@ import java.net.URI; import java.time.OffsetDateTime; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -40,8 +43,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; import io.cloudevents.SpecVersion; +import io.cloudevents.core.data.BytesCloudEventData; +import io.cloudevents.core.data.PojoCloudEventData; +import io.cloudevents.core.format.EventFormat; +import io.cloudevents.core.message.MessageWriter; +import io.cloudevents.core.message.impl.BaseGenericBinaryMessageReaderImpl; import io.cloudevents.jackson.JsonFormat; +import io.cloudevents.rw.CloudEventContextWriter; +import io.cloudevents.rw.CloudEventRWException; +import io.cloudevents.rw.CloudEventWriter; import static org.assertj.core.api.Assertions.assertThat; import static org.kie.kogito.event.process.KogitoEventBodySerializationHelper.toDate; @@ -130,14 +142,103 @@ void processInstanceDataEvent() throws Exception { @Test void multipleInstanceDataEvent() throws IOException { JsonNode expectedVarValue = OBJECT_MAPPER.createObjectNode().put("name", "John Doe"); - int standard = processMultipleInstanceDataEvent(expectedVarValue, false, false); - int binary = processMultipleInstanceDataEvent(expectedVarValue, true, false); - int binaryCompressed = processMultipleInstanceDataEvent(expectedVarValue, true, true); - assertThat(standard).isGreaterThan(binary); - assertThat(binary).isGreaterThan(binaryCompressed); + processMultipleInstanceDataEvent(expectedVarValue, false, false, this::serializeAsStructured); + processMultipleInstanceDataEvent(expectedVarValue, true, false, this::serializeAsStructured); + processMultipleInstanceDataEvent(expectedVarValue, true, true, this::serializeAsStructured); + processMultipleInstanceDataEvent(expectedVarValue, false, false, this::serializeAsBinary); + processMultipleInstanceDataEvent(expectedVarValue, true, false, this::serializeAsBinary); + processMultipleInstanceDataEvent(expectedVarValue, true, true, this::serializeAsBinary); } - private int processMultipleInstanceDataEvent(JsonNode expectedVarValue, boolean binary, boolean compress) throws IOException { + private MultipleProcessInstanceDataEvent serializeAsStructured(MultipleProcessInstanceDataEvent event) throws IOException { + return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsBytes(event), MultipleProcessInstanceDataEvent.class); + } + + private record CloudEventHolder(Map headers, byte[] data) { + } + + private static class TestMessageWriter implements CloudEventWriter, MessageWriter, CloudEventHolder> { + + private Map headers = new HashMap<>(); + private byte[] value; + + @Override + public TestMessageWriter create(SpecVersion version) throws CloudEventRWException { + headers.put("specversion", version.toString()); + return this; + } + + @Override + public CloudEventHolder setEvent(EventFormat format, byte[] value) throws CloudEventRWException { + this.value = value; + return this.end(); + } + + @Override + public CloudEventContextWriter withContextAttribute(String name, String value) throws CloudEventRWException { + headers.put(name, value); + return this; + } + + @Override + public CloudEventHolder end(CloudEventData data) throws CloudEventRWException { + this.value = data.toBytes(); + return this.end(); + } + + @Override + public CloudEventHolder end() throws CloudEventRWException { + return new CloudEventHolder(headers, value); + } + } + + private class TestMessageReader extends BaseGenericBinaryMessageReaderImpl { + private Map headers; + + protected TestMessageReader(SpecVersion version, CloudEventHolder body) { + super(version, body.data() == null ? null : BytesCloudEventData.wrap(body.data())); + this.headers = body.headers(); + } + + @Override + protected boolean isContentTypeHeader(String key) { + return false; + } + + @Override + protected boolean isCloudEventsHeader(String key) { + return true; + } + + @Override + protected String toCloudEventsKey(String key) { + return key; + } + + @Override + protected void forEachHeader(BiConsumer fn) { + headers.forEach(fn); + } + + @Override + protected String toCloudEventsValue(String value) { + return value; + } + + } + + private MultipleProcessInstanceDataEvent serializeAsBinary(MultipleProcessInstanceDataEvent event) throws IOException { + CloudEvent toSerialize = event.asCloudEvent(value -> PojoCloudEventData.wrap(value, MultipleProcessDataInstanceConverterFactory.toCloudEvent(event, OBJECT_MAPPER))); + CloudEventHolder holder = new TestMessageWriter().writeBinary(toSerialize); + CloudEvent deserialized = new TestMessageReader(SpecVersion.V1, holder).toEvent(); + return DataEventFactory.from(new MultipleProcessInstanceDataEvent(), deserialized, MultipleProcessDataInstanceConverterFactory.fromCloudEvent(deserialized, OBJECT_MAPPER)); + } + + private static interface CheckedUnaryOperator { + T apply(T obj) throws IOException; + } + + private void processMultipleInstanceDataEvent(JsonNode expectedVarValue, boolean binary, boolean compress, CheckedUnaryOperator operator) throws IOException { ProcessInstanceStateDataEvent stateEvent = new ProcessInstanceStateDataEvent(); setBaseEventValues(stateEvent, ProcessInstanceStateDataEvent.STATE_TYPE); stateEvent.setData(ProcessInstanceStateEventBody.create().eventDate(toDate(TIME)).eventType(EVENT_TYPE).eventUser(SUBJECT) @@ -185,20 +286,9 @@ private int processMultipleInstanceDataEvent(JsonNode expectedVarValue, boolean event.setCompressed(compress); } - byte[] json = OBJECT_MAPPER.writeValueAsBytes(event); - logger.info("Serialized chunk size is {}", json.length); - - // cloud event structured mode check - MultipleProcessInstanceDataEvent deserializedEvent = OBJECT_MAPPER.readValue(json, MultipleProcessInstanceDataEvent.class); - assertThat(deserializedEvent.getData()).hasSize(event.getData().size()); - assertMultipleIntance(deserializedEvent, expectedVarValue); - - // cloud event binary mode check - CloudEvent cloudEvent = OBJECT_MAPPER.readValue(json, CloudEvent.class); - deserializedEvent = DataEventFactory.from(new MultipleProcessInstanceDataEvent(), cloudEvent, MultipleProcessDataInstanceConverterFactory.fromCloudEvent(cloudEvent, OBJECT_MAPPER)); + MultipleProcessInstanceDataEvent deserializedEvent = operator.apply(event); assertThat(deserializedEvent.getData()).hasSize(event.getData().size()); assertMultipleIntance(deserializedEvent, expectedVarValue); - return json.length; } private void assertMultipleIntance(MultipleProcessInstanceDataEvent deserializedEvent, JsonNode expectedVarValue) {