commit 82717986a796290f89a2120d6600e885e4149c7c Author: Andreas Isler Date: Tue Jun 23 08:04:38 2026 +0200 Gitlab migration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75aff64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Maven / NPM +target/ +dist/ +node_modules/ +*.js.map + +# IntelliJ IDEA +.idea/ +*.iml + +# Visual Studio Code +*.code-workspace +.project +.classpath +.settings/ +.vscode/ + +# MacOS +.DS_Store + +# Docker +docker/data/ + +# Flowable +h2-database/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..57c8d49 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..e78f99b --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,17 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/postgres + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..0809a3a --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..5279de9 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c0f20bb --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0f2aac6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..737ef2c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,307 @@ +# Flowable Initializr Template Changelog + +This file is a changelog for the template used by the Flowable Initializr to create this projects. +It contains information about template changes made in the past for better traceability. Versions +mentioned in this file refer to the template itself and are not represented withing the any project +generated by the Flowable Initializr. + +It is totally safe and intended that this file will be deleted after project generation. + +## Change Log + +### 0.14.2 +- Upgrade to Flowable 3.14.4 +- Upgrade to Spring Boot 2.7.18 +- Added password properties to Work/Design/Control application.properties files to encourage users to change them. + +### 0.14.1 +- Upgrade to Flowable 3.14.1 +- Upgrade to Spring Boot 2.7.14 + +### 0.14.0 +- Upgrade to Flowable 3.14.0 +- Upgrade to Spring Boot 2.7.13 +- Changed Angular Design palette dependencies from -legacy to -angular + +### 0.14.0.RC1.1 +- Renamed design-legacy to design-angular to stick to the suggested naming convention. +- Added warning message to README.md about design and design-angular sharing the same DB schema. + +### 0.14.0 +- Upgrade to Flowable 3.14.0.RC1 +- Upgrade to Spring Boot 2.7.12 +- Updated documentation to use `docker compose` instead of `docker-compose` command. +- Updated namespace for Flowable Design properties. +- Added SecurityConfiguration and SecurityActuatorConfiguration classes for new Flowable Design. +- Added flowable-spring-boot-starter-design-actuator dependency for new Flowable Design. +- Added new maven submodule representing the angular version of Flowable Design (running on port 8093). +- Updated README.md with documentation about angular Flowable Design. +- Removed redundant spring.database.username and .password from application-infraless.properties + +### 0.13.5 +- Upgrade to Flowable 3.13.2 + +### 0.13.4 +- Upgrade to Flowable 3.13.1 +- Upgrade to Spring Boot 2.7.8 +- Re-added the micrometer elasticsearch registry dependency that is not part of the Spring Boot Starter anymore. (see https://documentation.flowable.com/latest/admin/release-notes/3.13.0-release/#upgrade-information) + +### 0.13.3 +- Upgrade to Flowable 3.13.0 +- Upgrade to Spring Boot 2.7.6 + +### 0.13.1 +- Updated the `SecurityConfiguration` to get rid of deprecated `WebSecurityConfigurerAdapter`. +- Updated the `SecurityActuatorConfiguration` to get rid of deprecated `WebSecurityConfigurerAdapter`. +- Removed the `StaticResourceConfiguration` as it is not needed anymore. + +### 0.13.0 +- Upgrade to Flowable 3.13.0.RC1 +- Upgrade to Spring Boot 2.7.5 + +### 0.12.3 +- Upgrade to Flowable 3.12.4 + +### 0.12.2 +- Upgrade to Flowable 3.12.3 +- Added deprecation suppress warning annotation to SecurityActuatorConfiguration +- Infraless H2 database is now stored within the project (`h2-database` directory) instead of the users profile. + +### 0.12.1 +- Upgrade to Flowable 3.12.2 +- Readded missing maven-wrapper.properties + +### 0.12.0 +- Upgrade to Flowable 3.12.0 +- Upgrade to Spring Boot 2.7.2 +- Upgrade to Elasticsearch 7.17.5 +- Updated SecurityConfiguration +- Updated docker compose file to use heathcheck/depends_on to enforce service start order and timing + +### 0.11.8 +- Upgrade to Flowable 3.11.7 + +### 0.11.7 +- Added maven wrapper +- Removed Flowable maven repo from parent pom.xml and added it to settings-flowable.xml +- Updated test to work elastic-less and FlowableSpringExtension + +### 0.11.6 +- Changed `training` profile to `infraless` (also in Readme) + +### 0.11.5 +- Removed custom tenant, filter and user definitions - instead added the default-idm-models dependency +- Added `flowable.platform.idm.minimal-setup=true` property (commented so sample Engage users get created) +- Added application-training.properties file for Flowable Academy (Embedded H2 DB, Elastic-less) +- Changed H2 DB dependency from test to default scope. +- Updated README.md to reflect the idm model changes. + +### 0.11.4 +- Upgrade to Flowable 3.11.6 +- Upgrade to Spring Boot 2.6.7 + +### 0.11.3 +- Upgrade to Flowable 3.11.5 +- Upgrade to Spring Boot 2.6.6 + +### 0.11.2 +- Upgrade to Flowable 3.11.2 +- Upgrade to Spring Boot 2.6.2 +- Fixed XPack disable property without quotes that caused errors on some systems. +- Upgrade of Elasticsearch version to 7.16.2 (because of Log4Shell vulnerability fix) + +### 0.11.1 +- Upgrade to Flowable 3.11.1 +- Disabled xpack security in elasticsearch to prevent ugly warnings from RestClient + +### 0.11.0 +- Upgrade to Java 11 +- Upgrade to Flowable 3.11.0 +- Upgrade to Spring Boot 2.6.1 +- Upgrade to Elasticsearch 7.15.2 +- Upgrade to Postgres 14 +- Renamed the *-app module to *-work for both the Flowable Work and Flowable Engage templates (as Engage is mainly just an extension of work) +- Streamlined the naming in the entire template to represent the app module renaming. + - Added additional package level 'work' for main and test classes + - Renamed the Application class + - Renamed the Test Classes +- Renamed the 'elasticsearch' service in docker-compose.yml to 'flowable-index'. +- Renamed docker-compose data volume 'data_es' to 'data_index' +- Fixed elasticsearch URL in flowable-engage service in docker-compose.yml +- Prepared possibility to run elasticless. +- Fixed some typos +- Added themeManagement allowdFeature to admin user definitions. +- Added tenantKey property to tenant setup + +### 0.10.2 +- Upgrade to Flowable 3.10.2 + +### 0.10.1 +- Upgrade to Flowable 3.10.1 +- Removed Flowable License bugfix +- Added impersonateUser feature to admin user definitions + +### 0.10.0 +- Upgrade to Flowable 3.10.0 +- Upgrade Spring Boot to 2.5.2 to be in line with Flowable 3.10.0 +- Set DB Store for Flowable License as default for PROD configuration +- Added sample settings.xml to project root +- Improved README documentation with information on Flowable Enterprise Maven repository access +- Added additional hyphen to index prefix + +### 0.9.2 +- Upgrade to Flowable 3.9.2 +- Added customized Flowable Design and Control as submodules +- Updated docker-compose.yml also allows running vanilla work as a container +- Updated several properties +- Updated README.md for new Design/Control submodules +- Bugfixes + +### 0.9.1 +- Upgrade to Flowable 3.9.1 + +### 0.9.0 +- Upgrade to Flowable 3.9.0 +- Fixed password property for Flowable Control +- Removed MVCC from H2 DB String (true is default) +- Disabled Elasticsearch persistence for Flowable Metrics by default (re-enabled in application-prod.properties) + +### 0.8.2 +- Upgrade to Flowable 3.8.2 +- Switched from yaml list to map format for environment properties in docker-compose + +### 0.8.1 +- Upgrade to Flowable 3.8.1 +- Added cluster type property to Flowable Control config in docker-compose.yml (set to work) +- Improved docker-compose services layout + +### 0.8.0 +- Upgrade to Flowable 3.8.0 +- Upgrade Spring Boot to 2.3.5.RELEASE to be in line with Flowable 3.8.0 +- Added `info.env.name` property to application configuration to show stage label in UI +- Added `management.metrics.export.elastic.enabled=true` property to persist metrics in elasticsearch. + +### 0.7.2 +- Upgrade to FLowable 3.7.2 +- Updated repositories for new Flowable Artifactory + +### 0.7.1 +- Upgrade to Flowable 3.7.1 + +### 0.7.0 +- Upgrade to Flowable 3.7.0 +- Upgrade Spring Boot to 2.3.1.RELEASE to be in line with Flowable 3.7.0 +- Added Flowable Inspect as a dependency. +- Removed entrypoints from docker-compose definition for engage and design as the images now are based on executables jars. +- Updated configuration classes to be in sync with the product. +- Updated Postgres DB in docker-compose to 12.3 + +### 0.6.5 +- Upgrade to Flowable 3.6.5 +- Changed maven packaging from executable .war to executable .jar file as the executable .war might cause issues on + Kubernetes setups (file access delaying servlet initialization). + +### 0.6.1 +- Upgrade to Flowable 3.6.1 +- Disabled DB store for Flowable Design license (active by default since 3.6) to use mounted license file instead +- Updated user definitions with newest allowedFeatures + +### 0.6.0 +- Upgrade to Flowable 3.6.0 +- Upgrade Spring Boot to 2.2.5.RELEASE to be in line with Flowable 3.6.0 +- Updated documentation links in README.md for Flowable 3.6.0 +- Removed JUnit Jupiter dependency as it is now part of spring-boot-starter-test + +### 0.5.1 +- Upgrade to Flowable 3.5.1 +- Minor comment improvements in pom.xml + +### 0.5.0 +- Upgrade to Flowable 3.5.0 +- Upgrade Spring Boot to 2.1.9.RELEASE to be in line with Flowable 3.5.0 +- Maven repository config now points to Flowable Repo (instead of edrorasware). +- Added additional dash character to elasticsearch index prefix property to make the index names more readable. +- Added Info about Windows issues with the host.docker.internal URL. +- Upgrade Elasticsearch image in docker-compose to 7.5.0 + +### 0.4.1 +- Upgrade to Flowable 3.4.1 +- Removed unneeded static logger from SecurityActuatorConfiguration + +### 0.4.0 +- Upgrade to Flowable 3.4.0 + +### 0.3.0.1 +- Upgrade Flowable Design Docker image to 3.3.0.1 +- Configured Flowable Design to be able to get users/groups from Work/Engage +- Configured Flowable Control to allow user/group creation +- Minor .gitignore corrections + +### 0.3.0 +- Upgrade to Flowable 3.3.0 +- Extended .gitignore with Visual Studio Code files +- Updated docu links +- Updated Docker image versions to 3.3.0 +- Refactored Test classes + +### 0.2.1 +- Upgrade to Flowable 3.2.2 + +### 0.2.0 +- Upgrade to Flowable 3.2.1 + +### 0.1.6 +- Added static resource configuration class to configure browser caching for static resources. + +### 0.1.5 +- Upgrade to Flowable 3.1.2 + +### 0.1.4 +- Added CHANGELOG.md file +- Changed bind mounts in docker-compose.yml to volumes to achieve better Docker for Windows compatibility - YOU WILL USE YOUR EXISTING DATA WITH THIS CHANGE!! +- Updated tenant and user definition files to reintroduce needed minimal setup (added `flowableUser` group) +- Updated README.md docu +- Re-added H2 dependency as in testing scope + +### 0.1.3 +- added docker/data folder to gitignore +- Corrected contact filters +- Added property for actuator pretty json + +### 0.1.2 +- Removed frontend title +- Added workaround for FLW-523 + +### 0.1.1 +- Updated allowed features +- Added contact filter to the template + +### 0.1.0 +- Upgrade to Flowable 3.1.0 +- Docu update +- Added data bind mount directory + +### 0.0.5 +- Added additional documentation on how to setup infrastructure manually +- Reconfigured maven to create an executable war file +- Docu update + +### 0.0.4 +- Fixed docker images for design and control to stable version 3.0.0, updated docu accordingly +- Readded flowableAdministrator group and user-definition as these seem to be references directly +- Removed certain properties that are already set by default by the product +- Removed embedded profile + +### 0.0.3 +- Fixed wrong port for control in README.md +- Fixed license file mapping that caused write error for uploadCache directory +- Fixed flowable-design URL in README.md + +### 0.0.2 +- Added template specific tenant files and user definitions +- Fixed main and test class names +- Fixed pox description +- Added context root property for flowable control + +### 0.0.1 +- initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..831ab98 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# flowable local Flowable Work + +## WARNING! +**This project contains the new version of Flowable Design (flowable-local-design) as well as the older angular version of Flowable Design (flowable-local-design-angular). +Both versions use the same database. flowable-local-design will install/upgrade to a new DB schema version as soon as being run. +After this, flowable-local-design-angular will NOT work anymore! The DB schema has to be deleted and recreated by starting flowable-local-design-angular +for it to work again. Use the `infraless` profile to run both design versions concurrently.** + +## Preparation +### Building the project artifacts +Java and Maven are needed as a prerequisite to build this code. +The Maven configuration of the project tries to get the Flowable dependencies form the Flowable Enterprise Repository. +That Repository needs a Flowable enterprise account to be accessible. Get in contact with Flowable to get your Flowable subscription. + +As soon as you got your username and password you can add it to the `settings-flowable.xml` file in the root of the project. +(Keep in mind, that using an unencrypted password in this file is indeed a fast but also not a suggested approach. +Check https://maven.apache.org/guides/mini/guide-encryption.html on how to encrypt your password.) + +You can then create the Flowable artifacts by executing `mvn -s settings-flowable.xml clean package`. + +### Flowable `infraless` profile +For a setup of a decent development infrastructure dedicated Postgres and Elasticsearch instances are heavily suggested +(e.g. by using Docker, see next chapter). + +For participating a Flowable Training course it is possible to run Flowable with an `infraless` profile with an embedded H2 +database and in a mode that works without Elasticsearch. +You can activate this `infraless` profile by adding it to the list of activated Spring profiles for Flowable Work, Design +and Control by either using the start configuration of your IDE or by setting `spring.profiles.active=infraless` as a +JVM parameter or environment variable. + +**ATTENTION**: Keep in mind that the `infraless` profiles does not provide the full Flowable functionality and is absolutely +not suggested for development, test or even production purposes. + +### Setting up the needed Infrastructure +Please check the following links on how to setup the infrastructure for Flowable manually without +using Docker: + +- [Postgres Database](https://documentation.flowable.com/latest/admin/installs/engage-full/#database-1) +- [Elasticsearch](https://documentation.flowable.com/latest/admin/installs/engage-full/#elasticsearch-1) + +For an easier setup with help of Docker navigate to `/docker`, then execute `docker compose up`. Needed services such as +Elasticsearch and a database will be started. + +## Starting the project +Afterwards you can start the Spring Boot application defined in `com.flowable.local.work.FlowableLocalWorkApplication`. In order to achieve this, +you can build the executable war file with maven and execute `java -jar` pointing to the built jar or create a new IDE Run Configuration. +Then open `http://localhost:8090` in the browser and use one of the users specified below in this document. + +You can start the Flowable Design and Flowable Control applications accordingly. + +## Angular Version of Flowable Design +This project contains the new version of Flowable Design (flowable-local-design) available since 3.14 as well as the older +angular version of Flowable Design (flowable-local-design-angular). + +**Usually only 1 of these 2 versions are needed. The other one can (and most probably should) be deleted.** Please keep in +mind, that both versions cannot be run on the same DB schema as the new Flowable Design will update the schema which is then +not compatible with the angular version anymore. + +This restriction does not apply to the infraless profile that uses different databases for the versions. +With this profile active, both the new and the angular version of Flowable Design can be started and used concurrently. + +## Helpful links +After both docker-compose and the application are started, these are the links for the different applications: + +- http://localhost:8090 - Flowable Work +- http://localhost:8091 - Flowable Design +- http://localhost:8092 - Flowable Control +- http://localhost:8093 - Flowable Design Angular + +## Setting up Flowable vanilla apps with Docker +The docker-compose.yml file also contains (commented out) preconfigured service definitions to run all Flowable apps as +vanilla docker images. Just uncomment the appropriate parts to start the apps together with the other infrastructure. +These vanilla services are preconfigured to run as a replacement of any of the customized apps within this project. + +To be able to use them, log in to docker `docker login artifacts.flowable.com` by using your Flowable login. +You will also need an appropriate Flowable license stored in `~/.flowable/flowable.license`. +Also, a decent amount of memory is needed for your docker VM. At least 4 GB are suggested. +Navigate to `/docker`, then execute `docker compose up`. + +## Infrastructure Containers +If you need to recreate the containers, perform the following actions: +- Make sure the application and the docker containers are stopped. +- Execute `docker compose down` inside the `/docker` directory. This will remove the created containers. + +## Data +Data created by the database and Elasticsearch are stored in docker volumes `data_db` and `data_es`. +This allows you to purge and recreate the containers without loosing any data. + +If you need a clean state for the database and elasticsearch just execute `docker compose down -v`inside the `/docker` directory. +The volumes will be recreated as soon as you restart the containers. +BE CAREFUL AS WITH THIS YOU WILL LOSE ALL YOUR DATA STORED IN THE DATABASE AND ELASTICSEARCH! + +## Sample users +| User | User Definition Key | Login | Password | +| -------------| ------------- | ------------- | ------------- | +| Flowable Administrator | admin-flowable | admin | test | \ No newline at end of file diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000..ac94e42 --- /dev/null +++ b/docker/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=3-14 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..fdac91a --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,154 @@ +services: + flowable-db: + image: postgres:14 + environment: + POSTGRES_DB: flowable + POSTGRES_USER: flowable + POSTGRES_PASSWORD: flowable + ports: + - 5432:5432 + volumes: + - data_db:/var/lib/postgresql/data + healthcheck: + test: [ "CMD", "pg_isready", "-q", "-d", "flowable", "-U", "flowable" ] + interval: 5s + timeout: 5s + retries: 3 + start_period: 10s + + flowable-index: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5 + environment: + discovery.type: single-node + node.name: flowable-node-01 + cluster.name: flowable-cluster + xpack.security.enabled: "false" + ports: + - 9300:9300 + - 9200:9200 + mem_limit: 1024m + volumes: + - data_index:/usr/share/elasticsearch/data + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: ["CMD", "curl", "-f", "http://host.docker.internal:9200"] + interval: 5s + timeout: 5s + retries: 3 + start_period: 20s + + # Vanilla Flowable Work, uncomment to activate + # This image requires to login to the Flowable Docker Repo (docker login artifacts.flowable.com) +# flowable-work: +# image: artifacts.flowable.com/flowable/flowable-work:3.14.4 +# environment: +# flowable.license.db-store-enabled: "false" +# flowable.inspect.enabled: "true" +# flowable.security.impersonate.allowed: "true" +# flowable.platform.enable-latest-form-definition-lookup: "true" +# flowable.platform.idm.minimal-setup: "true" +# flowable.example.deploy-apps: "false" +# info.env.name: Development +# logging.file: flowable-work.log +# management.metrics.export.elastic.enabled: "true" +# management.endpoints.web.exposure.include: "*" +# management.endpoint.health.show-details: ALWAYS +# server.servlet.context-path: / +# spring.elasticsearch.rest.uris: http://host.docker.internal:9200 +# spring.datasource.driver-class-name: org.postgresql.Driver +# spring.datasource.url: jdbc:postgresql://host.docker.internal:5432/flowable +# spring.datasource.username: flowable +# spring.datasource.password: flowable +# ports: +# - 8090:8080 +# volumes: +# - ~/.flowable/flowable.license:/root/.flowable/flowable.license:ro +# depends_on: +# flowable-db: +# condition: service_healthy +# flowable-index: +# condition: service_healthy + + # Vanilla Flowable Design, uncomment to activate + # This image requires to login to the Flowable Docker Repo (docker login artifacts.flowable.com) +# flowable-design: +# image: artifacts.flowable.com/docker-local/flowable/flowable-design:3.14.4 +# environment: +# flowable.design.remote.authentication.user: admin +# flowable.design.remote.authentication.password: test +# flowable.design.remote.idm-url: http://host.docker.internal:8090 +# flowable.design.deployment-api-url: http://host.docker.internal:8090/app-api # not yet working for linux systems, check https://github.com/docker/for-linux/issues/264 for PR state and workarounds +# flowable.design.undeployment-api-url: http://host.docker.internal:8090/platform-api/app-deployments # not yet working for linux systems, check https://github.com/docker/for-linux/issues/264 for PR state and workarounds +# flowable.design.db-store-enabled: "false" +# logging.file: flowable-design.log +# server.servlet.context-path: / +# spring.datasource.driver-class-name: org.postgresql.Driver +# spring.datasource.url: jdbc:postgresql://host.docker.internal:5432/flowable +# spring.datasource.username: flowable +# spring.datasource.password: flowable +# ports: +# - 8091:8080 +# volumes: +# - ~/.flowable/flowable.license:/root/.flowable/flowable.license:ro +# depends_on: +# flowable-db: +# condition: service_healthy + + # Vanilla Flowable Design Angular, uncomment to activate + # This image requires to login to the Flowable Docker Repo (docker login artifacts.flowable.com) +# flowable-design-angular: +# image: artifacts.flowable.com/docker-local/flowable/flowable-design-angular:3.14.4 +# environment: +# flowable.common.app.idm-admin.user: admin +# flowable.common.app.idm-admin.password: test +# flowable.common.app.idm-url: http://host.docker.internal:8090 +# flowable.modeler.app.deployment-api-url: http://host.docker.internal:8090/app-api # not yet working for linux systems, check https://github.com/docker/for-linux/issues/264 for PR state and workarounds +# flowable.modeler.app.undeployment-api-url: http://host.docker.internal:8090/platform-api/app-deployments # not yet working for linux systems, check https://github.com/docker/for-linux/issues/264 for PR state and workarounds +# flowable.modeler.app.db-store-enabled: "false" +# logging.file: flowable-design.log +# server.servlet.context-path: / +# spring.datasource.driver-class-name: org.postgresql.Driver +# spring.datasource.url: jdbc:postgresql://host.docker.internal:5432/flowable +# spring.datasource.username: flowable +# spring.datasource.password: flowable +# ports: +# - 8091:8080 +# volumes: +# - ~/.flowable/flowable.license:/root/.flowable/flowable.license:ro +# depends_on: +# flowable-db: +# condition: service_healthy + + # Vanilla Flowable Control, uncomment to activate + # This image requires to login to the Flowable Docker Repo (docker login artifacts.flowable.com) +# flowable-control: +# image: artifacts.flowable.com/docker-local/flowable/flowable-control:3.14.4 +# environment: +# flowable.common.app.idm-admin.user: admin +# flowable.common.app.idm-admin.password: test +# flowable.control.app.cluster-config.server-address: http://host.docker.internal # not yet working for linux systems, check https://github.com/docker/for-linux/issues/264 for pr state and workarounds +# flowable.control.app.cluster-config.server-port: 8090 +# flowable.control.app.cluster-config.context-root: / +# flowable.control.app.cluster-config.password: test +# flowable.control.app.db-store-enabled: "false" +# logging.file: flowable-control.log +# server.servlet.context-path: / +# spring.datasource.driver-class-name: org.postgresql.Driver +# spring.datasource.url: jdbc:postgresql://host.docker.internal:5432/flowable +# spring.datasource.username: flowable +# spring.datasource.password: flowable +# ports: +# - 8092:8080 +# volumes: +# - ~/.flowable/flowable.license:/root/.flowable/flowable.license:ro +# depends_on: +# flowable-db: +# condition: service_healthy + +# Volumes will get a prefix based on the `.env` file and data can be cleared by `docker-compose down -v` +volumes: + data_db: + data_index: \ No newline at end of file diff --git a/flowable-local-control/pom.xml b/flowable-local-control/pom.xml new file mode 100644 index 0000000..e2b609c --- /dev/null +++ b/flowable-local-control/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + flowable-local-control + jar + + flowable-local-control + flowable local Flowable Control + + + com.flowable.local + 14 + 0.0.1-SNAPSHOT + + + + + + + + org.postgresql + postgresql + ${org.postgresql.version} + + + + + + com.flowable.control + flowable-spring-boot-starter-control + ${com.flowable.platform.version} + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + ${com.h2database.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/flowable-local-control/src/main/java/com/flowable/local/control/FlowableLocalControlApplication.java b/flowable-local-control/src/main/java/com/flowable/local/control/FlowableLocalControlApplication.java new file mode 100644 index 0000000..db48e4b --- /dev/null +++ b/flowable-local-control/src/main/java/com/flowable/local/control/FlowableLocalControlApplication.java @@ -0,0 +1,12 @@ +package com.flowable.local.control; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FlowableLocalControlApplication { + + public static void main(String[] args) { + SpringApplication.run(FlowableLocalControlApplication.class, args); + } +} diff --git a/flowable-local-control/src/main/resources/application-infraless.properties b/flowable-local-control/src/main/resources/application-infraless.properties new file mode 100644 index 0000000..488d3d1 --- /dev/null +++ b/flowable-local-control/src/main/resources/application-infraless.properties @@ -0,0 +1,3 @@ +# == Embedded DB == +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:file:./h2-database/flowable-control;DB_CLOSE_ON_EXIT=FALSE diff --git a/flowable-local-control/src/main/resources/application.properties b/flowable-local-control/src/main/resources/application.properties new file mode 100644 index 0000000..f40e452 --- /dev/null +++ b/flowable-local-control/src/main/resources/application.properties @@ -0,0 +1,34 @@ +server.port=8092 +server.servlet.context-path=/ + +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/flowable +spring.datasource.username=flowable +spring.datasource.password=flowable + +# In order for hot swapping to work for the custom.js and custom.css +spring.thymeleaf.cache=false +spring.web.resources.chain.cache=false + +# Enable all endpoints over HTTP +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=ALWAYS +# Pretty-print JSON responses +spring.jackson.serialization.indent_output=true + +# TODO Change these to an other random string with a length of 16 characters +# Used to encrypt your passwords. After a change existing password become invalid! +flowable.control.app.security.encryption.credentials-i-v-spec=fh39chqoxjDF3fhb +flowable.control.app.security.encryption.credentials-secret-spec=lw91VtkPq84nGqiJ + +flowable.control.app.cluster-config.name=Default Cluster +flowable.control.app.cluster-config.description=Default Flowable Cluster +flowable.control.app.cluster-config.server-address=http://localhost +flowable.control.app.cluster-config.port=8090 +flowable.control.app.cluster-config.context-root=/ +flowable.control.app.cluster-config.user-name=admin +flowable.control.app.cluster-config.password=test + +flowable.control.app.db-store-enabled=false +flowable.control.app.cluster-type=work +flowable.control.app.user-store.password=test \ No newline at end of file diff --git a/flowable-local-design-angular/pom.xml b/flowable-local-design-angular/pom.xml new file mode 100644 index 0000000..5a01190 --- /dev/null +++ b/flowable-local-design-angular/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + flowable-local-design-angular + jar + + flowable-local-design-angular + flowable local Flowable Design Angular + + + com.flowable.local + 14 + 0.0.1-SNAPSHOT + + + + + + + + org.postgresql + postgresql + ${org.postgresql.version} + + + + + + com.flowable.design.angular + flowable-spring-boot-starter-design + ${com.flowable.platform.version} + + + com.flowable.platform + flowable-platform-palette-angular + ${com.flowable.platform.version} + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + ${com.h2database.version} + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/flowable-local-design-angular/src/main/java/com/flowable/local/design/angular/FlowableLocalDesignAngularApplication.java b/flowable-local-design-angular/src/main/java/com/flowable/local/design/angular/FlowableLocalDesignAngularApplication.java new file mode 100644 index 0000000..7e266c0 --- /dev/null +++ b/flowable-local-design-angular/src/main/java/com/flowable/local/design/angular/FlowableLocalDesignAngularApplication.java @@ -0,0 +1,12 @@ +package com.flowable.local.design.angular; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FlowableLocalDesignAngularApplication { + + public static void main(String[] args) { + SpringApplication.run(FlowableLocalDesignAngularApplication.class, args); + } +} diff --git a/flowable-local-design-angular/src/main/resources/application-infraless.properties b/flowable-local-design-angular/src/main/resources/application-infraless.properties new file mode 100644 index 0000000..3b0c591 --- /dev/null +++ b/flowable-local-design-angular/src/main/resources/application-infraless.properties @@ -0,0 +1,3 @@ +# == Embedded DB == +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:file:./h2-database/flowable-design-angular;DB_CLOSE_ON_EXIT=FALSE diff --git a/flowable-local-design-angular/src/main/resources/application.properties b/flowable-local-design-angular/src/main/resources/application.properties new file mode 100644 index 0000000..54ccdc3 --- /dev/null +++ b/flowable-local-design-angular/src/main/resources/application.properties @@ -0,0 +1,26 @@ +server.port=8091 +server.servlet.context-path=/ + +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/flowable +spring.datasource.username=flowable +spring.datasource.password=flowable + +# In order for hot swapping to work for the custom.js and custom.css +spring.thymeleaf.cache=false +spring.web.resources.chain.cache=false + +# Enable all endpoints over HTTP +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=ALWAYS +# Pretty-print JSON responses +spring.jackson.serialization.indent_output=true + +flowable.modeler.app.user-store.password=test +flowable.common.app.idm-url=http://localhost:8090 +flowable.common.app.idm-admin.user=admin +flowable.common.app.idm-admin.password=test + +flowable.modeler.app.db-store-enabled=false +flowable.modeler.app.deployment-api-url=http://localhost:8090/app-api +flowable.modeler.app.undeployment-api-url=http://localhost:8090/platform-api/app-deployments diff --git a/flowable-local-design/pom.xml b/flowable-local-design/pom.xml new file mode 100644 index 0000000..93a5f04 --- /dev/null +++ b/flowable-local-design/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + flowable-local-design + jar + + flowable-local-design + flowable local Flowable Design + + + com.flowable.local + 14 + 0.0.1-SNAPSHOT + + + + + + + + org.postgresql + postgresql + ${org.postgresql.version} + + + + + + com.flowable.design + flowable-spring-boot-starter-design + ${com.flowable.platform.version} + + + com.flowable.design + flowable-spring-boot-starter-design-actuator + ${com.flowable.platform.version} + + + com.flowable.platform + flowable-platform-palette + ${com.flowable.platform.version} + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + ${com.h2database.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/flowable-local-design/src/main/java/com/flowable/local/design/FlowableLocalDesignApplication.java b/flowable-local-design/src/main/java/com/flowable/local/design/FlowableLocalDesignApplication.java new file mode 100644 index 0000000..19137da --- /dev/null +++ b/flowable-local-design/src/main/java/com/flowable/local/design/FlowableLocalDesignApplication.java @@ -0,0 +1,12 @@ +package com.flowable.local.design; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FlowableLocalDesignApplication { + + public static void main(String[] args) { + SpringApplication.run(FlowableLocalDesignApplication.class, args); + } +} diff --git a/flowable-local-design/src/main/java/com/flowable/local/design/configuration/SecurityActuatorConfiguration.java b/flowable-local-design/src/main/java/com/flowable/local/design/configuration/SecurityActuatorConfiguration.java new file mode 100644 index 0000000..c60960a --- /dev/null +++ b/flowable-local-design/src/main/java/com/flowable/local/design/configuration/SecurityActuatorConfiguration.java @@ -0,0 +1,38 @@ +package com.flowable.local.design.configuration; + +import com.flowable.design.actuate.autoconfigure.security.servlet.ActuatorRequestMatcher; +import com.flowable.design.engine.api.idm.DesignPrivileges; +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.info.InfoEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityActuatorConfiguration { + + @Bean + @Order(6) + public SecurityFilterChain basicActuatorSecurity(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .csrf() + .disable(); + + http + .requestMatcher(new ActuatorRequestMatcher()) + .authorizeRequests() + .requestMatchers(EndpointRequest.to(InfoEndpoint.class, HealthEndpoint.class)).permitAll() + .requestMatchers(EndpointRequest.toAnyEndpoint()).hasAuthority(DesignPrivileges.ACCESS_ADMIN) + .anyRequest().denyAll() + .and().httpBasic(); + + return http.build(); + } +} diff --git a/flowable-local-design/src/main/java/com/flowable/local/design/configuration/SecurityConfiguration.java b/flowable-local-design/src/main/java/com/flowable/local/design/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..2b8f60f --- /dev/null +++ b/flowable-local-design/src/main/java/com/flowable/local/design/configuration/SecurityConfiguration.java @@ -0,0 +1,63 @@ +package com.flowable.local.design.configuration; + +import com.flowable.autoconfigure.design.security.DesignHttpSecurityCustomizer; +import com.flowable.design.security.spring.web.authentication.AjaxAuthenticationFailureHandler; +import com.flowable.design.security.spring.web.authentication.AjaxAuthenticationSuccessHandler; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; + +import java.util.stream.Collectors; + +@Configuration(proxyBeanMethods = false) +@EnableWebSecurity +public class SecurityConfiguration { + + @Bean + @Order(10) + public SecurityFilterChain basicDefaultSecurity(HttpSecurity http, ObjectProvider httpSecurityCustomizers) throws Exception { + for (DesignHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream() + .collect(Collectors.toList())) { + customizer.customize(http); + } + + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + http + .logout() + .logoutUrl("/auth/logout") + .logoutSuccessUrl("/"); + + http + .exceptionHandling() + .defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), AnyRequestMatcher.INSTANCE) + .and() + .formLogin() + .loginProcessingUrl("/auth/login") + .successHandler(new AjaxAuthenticationSuccessHandler()) + .failureHandler(new AjaxAuthenticationFailureHandler()) + .and() + .authorizeRequests() + // allow context root for all (it triggers the loading of the initial page) + .antMatchers("/").permitAll() + .antMatchers( + "/**/*.svg", "/**/*.ico", "/**/*.png", "/**/*.woff2", "/**/*.css", + "/**/*.woff", "/**/*.html", "/**/*.js", + "/**/flowable-frontend-configuration", + "/**/index.html").permitAll() + .anyRequest().authenticated() + .and() + .httpBasic(); + + return http.build(); + } +} diff --git a/flowable-local-design/src/main/resources/application-infraless.properties b/flowable-local-design/src/main/resources/application-infraless.properties new file mode 100644 index 0000000..6c88321 --- /dev/null +++ b/flowable-local-design/src/main/resources/application-infraless.properties @@ -0,0 +1,3 @@ +# == Embedded DB == +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:file:./h2-database/flowable-design;DB_CLOSE_ON_EXIT=FALSE diff --git a/flowable-local-design/src/main/resources/application.properties b/flowable-local-design/src/main/resources/application.properties new file mode 100644 index 0000000..85ac0a2 --- /dev/null +++ b/flowable-local-design/src/main/resources/application.properties @@ -0,0 +1,30 @@ +server.port=8093 +server.servlet.context-path=/ + +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/flowable +spring.datasource.username=flowable +spring.datasource.password=flowable + +# Local Elasticsearch +spring.elasticsearch.uris=http://localhost:9200 + +# In order for hot swapping to work for the custom.js and custom.css +spring.thymeleaf.cache=false +spring.web.resources.chain.cache=false + +# Enable all endpoints over HTTP +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=ALWAYS +# Pretty-print JSON responses +spring.jackson.serialization.indent_output=true + +flowable.design.user-store.password=test +flowable.design.remote.idm-url=http://localhost:8090 +flowable.design.remote.authentication.user=admin +flowable.design.remote.authentication.password=test + +flowable.design.db-store-enabled=false +flowable.design.deployment-api-url=http://localhost:8090/app-api +flowable.design.undeployment-api-url=http://localhost:8090/platform-api/app-deployments + diff --git a/flowable-local-work/pom.xml b/flowable-local-work/pom.xml new file mode 100644 index 0000000..98d0e6b --- /dev/null +++ b/flowable-local-work/pom.xml @@ -0,0 +1,139 @@ + + + 4.0.0 + + flowable-local-work + jar + + flowable-local-work + flowable local Flowable Work + + + com.flowable.local + 14 + 0.0.1-SNAPSHOT + + + + + + + + org.postgresql + postgresql + ${org.postgresql.version} + + + + + + com.flowable.work + flowable-work-frontend + + + + + + com.flowable.platform + flowable-spring-boot-starter-platform-rest + + + com.flowable.platform + flowable-tenant-setup + + + com.flowable.platform + flowable-platform-default-models + + + com.flowable.platform + flowable-platform-default-idm-models + + + + + + com.flowable.inspect + flowable-spring-boot-starter-inspect-rest + + + + + + com.flowable.platform + flowable-spring-boot-starter-platform-actuator + + + io.micrometer + micrometer-registry-elastic + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + ${com.h2database.version} + + + com.github.wnameless.json + json-flattener + 0.16.6 + + + com.icegreen + greenmail + 2.1.8 + + + jakarta.activation + jakarta.activation-api + 2.1.4 + + + + + + javax.validation + validation-api + + + org.apache.poi + poi-ooxml + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/FlowableLocalWorkApplication.java b/flowable-local-work/src/main/java/com/flowable/local/work/FlowableLocalWorkApplication.java new file mode 100644 index 0000000..df4fcb6 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/FlowableLocalWorkApplication.java @@ -0,0 +1,13 @@ +package com.flowable.local.work; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; + +@SpringBootApplication(exclude = {FreeMarkerAutoConfiguration.class}) +public class FlowableLocalWorkApplication { + + public static void main(String[] args) { + SpringApplication.run(FlowableLocalWorkApplication.class, args); + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/configuration/SecurityActuatorConfiguration.java b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/SecurityActuatorConfiguration.java new file mode 100644 index 0000000..734beae --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/SecurityActuatorConfiguration.java @@ -0,0 +1,39 @@ +package com.flowable.local.work.configuration; + +import com.flowable.actuate.autoconfigure.security.servlet.ActuatorRequestMatcher; +import com.flowable.platform.common.security.SecurityConstants; +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.info.InfoEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityActuatorConfiguration { + + @Bean + @Order(6) + public SecurityFilterChain basicActuatorSecurity(HttpSecurity http) throws Exception { + + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .csrf() + .disable(); + + http + .requestMatcher(new ActuatorRequestMatcher()) + .authorizeRequests() + .requestMatchers(EndpointRequest.to(InfoEndpoint.class, HealthEndpoint.class)).permitAll() + .requestMatchers(EndpointRequest.toAnyEndpoint()).hasAuthority(SecurityConstants.ACCESS_ACTUATORS) + .anyRequest().denyAll() + .and().httpBasic(); + + return http.build(); + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/configuration/SecurityConfiguration.java b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..5df37a8 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/SecurityConfiguration.java @@ -0,0 +1,79 @@ +package com.flowable.local.work.configuration; + +import com.flowable.autoconfigure.frontend.FrontendProperties; +import com.flowable.autoconfigure.security.FlowableHttpSecurityCustomizer; +import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationFailureHandler; +import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationSuccessHandler; +import com.flowable.platform.common.security.SecurityConstants; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; + +import java.util.stream.Collectors; + +@Configuration(proxyBeanMethods = false) +@EnableWebSecurity +public class SecurityConfiguration { + + @Autowired + protected ObjectProvider frontendPropertiesProvider; + + @Bean + @Order(10) + public SecurityFilterChain basicDefaultSecurity(HttpSecurity http, ObjectProvider httpSecurityCustomizers) throws Exception { + for (FlowableHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream() + .collect(Collectors.toList())) { + customizer.customize(http); + } + + http + .logout() + .logoutUrl("/auth/logout"); + + FrontendProperties frontendProperties = frontendPropertiesProvider.getIfAvailable(); + if (frontendProperties != null && frontendProperties.getFeatures().containsKey("formBasedLogout") + && frontendProperties.getFeatures().get("formBasedLogout")) { + http + .logout() + .logoutSuccessUrl("/"); + } else { + http + .logout() + .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); + } + + http + .exceptionHandling() + .defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), AnyRequestMatcher.INSTANCE) + .and() + .formLogin() + .loginProcessingUrl("/auth/login") + .successHandler(new AjaxAuthenticationSuccessHandler()) + .failureHandler(new AjaxAuthenticationFailureHandler()) + .and() + .authorizeRequests() + .antMatchers("/analytics-api/**").hasAuthority(SecurityConstants.ACCESS_REPORTS_METRICS) + .antMatchers("/work-object-api/**").hasAuthority(SecurityConstants.ACCESS_WORKOBJECT_API) + // allow context root for all (it triggers the loading of the initial page) + .antMatchers("/").permitAll() + .antMatchers( + "/**/*.svg", "/**/*.ico", "/**/*.png", "/**/*.woff2", "/**/*.css", + "/**/*.woff", "/**/*.html", "/**/*.js", + "/**/flowable-frontend-configuration", + "/**/index.html").permitAll() + .anyRequest().authenticated() + .and() + .httpBasic(); + + return http.build(); + } +} \ No newline at end of file diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/configuration/ServiceConfiguration.java b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/ServiceConfiguration.java new file mode 100755 index 0000000..563127c --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/ServiceConfiguration.java @@ -0,0 +1,20 @@ +package com.flowable.local.work.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.flowable.local.work.service.StartProcessBot; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class ServiceConfiguration { + + @Bean + public StartProcessBot activateTaskBot(RuntimeService runtimeService, + ObjectMapper objectMapper, + HistoryService historyService) { + return new StartProcessBot(runtimeService, objectMapper, historyService); + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/configuration/StaticResourceConfiguration.java b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/StaticResourceConfiguration.java new file mode 100644 index 0000000..439d17b --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/configuration/StaticResourceConfiguration.java @@ -0,0 +1,65 @@ +package com.flowable.local.work.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.concurrent.TimeUnit; + +@Configuration(proxyBeanMethods = false) +public class StaticResourceConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.setOrder(10) + .addResourceHandler("/ext/*.js", "/*/ext/*.js") + .addResourceLocations("classpath:/static/ext/", "classpath:/public/ext/") + .setCacheControl(CacheControl.noCache()); + + registry.setOrder(20) + .addResourceHandler("/ext/*.css", "/*/ext/*.css") + .addResourceLocations("classpath:/static/ext/", "classpath:/public/ext/") + .setCacheControl(CacheControl.noCache()); + + registry.setOrder(30) + .addResourceHandler("/*.js") + .addResourceLocations("classpath:/public/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + + registry.setOrder(40) + .addResourceHandler("/*.css") + .addResourceLocations("classpath:/public/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + + registry.setOrder(50) + .addResourceHandler("/*.woff") + .addResourceLocations("classpath:/public/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + + registry.setOrder(60) + .addResourceHandler("/*.woff2") + .addResourceLocations("classpath:/public/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + + registry.setOrder(70) + .addResourceHandler("/*.svg") + .addResourceLocations("classpath:/public/", "classpath:/public/twemoji/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + + registry.setOrder(80) + .addResourceHandler("/*.map") + .addResourceLocations("classpath:/public/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + + registry.setOrder(90) + .addResourceHandler("/*.png") + .addResourceLocations("classpath:/public/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + + registry.setOrder(100) + .addResourceHandler("/*.ico") + .addResourceLocations("classpath:/public/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + } +} \ No newline at end of file diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/events/CustomDataObjectEventHandler.java b/flowable-local-work/src/main/java/com/flowable/local/work/events/CustomDataObjectEventHandler.java new file mode 100644 index 0000000..8a31889 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/events/CustomDataObjectEventHandler.java @@ -0,0 +1,95 @@ +package com.flowable.local.work.events; + +import com.flowable.audit.api.AuditService; +import com.flowable.dataobject.api.event.FlowableDataObjectEventType; +import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainer; +import com.flowable.dataobject.api.runtime.DataObjectRuntimeService; +import com.flowable.dataobject.engine.delegate.event.impl.FlowableDataObjectInstanceDeletedEventImpl; +import com.flowable.dataobject.engine.delegate.event.impl.FlowableDataObjectInstanceUpdatedEventImpl; +import org.flowable.common.engine.api.delegate.event.FlowableEvent; +import org.flowable.common.engine.api.delegate.event.FlowableEventListener; +import org.flowable.common.engine.api.delegate.event.FlowableEventType; +import org.flowable.common.engine.impl.cfg.TransactionState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.*; + + +@Component +public class CustomDataObjectEventHandler implements FlowableEventListener { + private static final Logger LOGGER = LoggerFactory.getLogger(CustomDataObjectEventHandler.class); + protected final AuditService auditService; + protected final DataObjectRuntimeService dataObjectRuntimeService; + + public CustomDataObjectEventHandler(AuditService auditService, DataObjectRuntimeService dataObjectRuntimeService) { + this.auditService = auditService; + this.dataObjectRuntimeService = dataObjectRuntimeService; + } + + @Override + public void onEvent(FlowableEvent flowableEvent) { + DataObjectInstanceVariableContainer dataObjectInstance; + String subType; + if (flowableEvent instanceof FlowableDataObjectInstanceUpdatedEventImpl) { + dataObjectInstance = ((FlowableDataObjectInstanceUpdatedEventImpl) flowableEvent).getDataObjectInstance(); + subType = "updated"; + } else if (flowableEvent instanceof FlowableDataObjectInstanceDeletedEventImpl) { + dataObjectInstance = ((FlowableDataObjectInstanceDeletedEventImpl) flowableEvent).getDataObjectInstance(); + subType = "deleted"; + } else { + LOGGER.warn("Unhandled FlowableDataObjectEvent type: {}", flowableEvent.getClass().getName()); + return; + } + + HashMap data = (HashMap) dataObjectInstance.getData(); + if (data == null) { + LOGGER.warn("FlowableDataObjectEvent: No data"); + return; + } + + // If an instance of TST_DO001 is deactivated, delete all references in other data objects. + String scopeDefinitionId = dataObjectInstance.getDefinitionKey(); + if (scopeDefinitionId == null || !scopeDefinitionId.equals("TST_DO001")) return; + Object active = data.get("active"); + if (!(active instanceof Boolean)) { + LOGGER.warn("FlowableDataObjectEvent: 'active' is not boolean"); + return; + } + if (subType.equals("updated") && !((Boolean) active) || subType.equals("deleted")) { + String key = (String) data.get("key"); + LOGGER.warn("delete all references of {}", key); + dataObjectRuntimeService.createDataObjectModificationBuilder() + .tenantId(dataObjectInstance.getTenantId()) + .definitionKey("TST_DO002") + .operation("updateTeam") + .value("teamKey", key) + .value("team", null) + .modify(); + } + } + + @Override + public boolean isFailOnException() { + return true; + } + + @Override + public boolean isFireOnTransactionLifecycleEvent() { + return false; + } + + @Override + public String getOnTransaction() { + return TransactionState.COMMITTING.name(); + } + + @Override + public Collection getTypes() { + List eventTypes = new ArrayList<>(); + eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_UPDATED); + eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_DELETED); + return eventTypes; + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/events/GenericDataObjectAuditTrailEventHandler.java b/flowable-local-work/src/main/java/com/flowable/local/work/events/GenericDataObjectAuditTrailEventHandler.java new file mode 100644 index 0000000..9720cf3 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/events/GenericDataObjectAuditTrailEventHandler.java @@ -0,0 +1,156 @@ +package com.flowable.local.work.events; + +import com.flowable.audit.api.AuditService; +import com.flowable.core.common.api.security.SecurityScope; +import com.flowable.core.spring.security.SecurityUtils; +import com.flowable.dataobject.api.event.FlowableDataObjectEventType; +import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainer; +import com.flowable.dataobject.engine.delegate.event.impl.*; +import org.flowable.common.engine.api.delegate.event.FlowableEvent; +import org.flowable.common.engine.api.delegate.event.FlowableEventListener; +import org.flowable.common.engine.api.delegate.event.FlowableEventType; +import org.flowable.common.engine.impl.cfg.TransactionState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.*; + +/* + Writes audit trail entries on changes of data objects. + These entries must be retrieved by a specific REST endpoint (implemented in AuditTrailController), + because the flowable audit-trail API supports only audit trail entries with scopeType cmmn and bpmn. + The class writes the following data into the table FLW_AUDIT_INSTANCE: + - ID_ autogenerated ID + - CREATION_TIME_ now + - CREATOR_ID_ current user ID + - TYPE_ "user" or "system" + - SUB_TYPE_ "created" / "updated" / "deleted" + - SCOPE_DEFINITION_ID_ data object model key + - SCOPE_ID_ data object instance ID + - SCOPE_TYPE_ "dataObject" + - PAYLOAD_ { + : + : + etc. + } + - TENANT_ID_ tenant ID + - SUB_SCOPE_ID_ content of data object field in column "key", if column "key" exists + -> this mapping makes "key" filterable in a data table + */ +@Component +public class GenericDataObjectAuditTrailEventHandler implements FlowableEventListener { + private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataObjectAuditTrailEventHandler.class); + protected final AuditService auditService; + + public GenericDataObjectAuditTrailEventHandler(AuditService auditService) { + this.auditService = auditService; + } + + @Override + public void onEvent(FlowableEvent flowableEvent) { + DataObjectInstanceVariableContainer dataObjectInstance = null; + String scopeDefinitionId = null; + String operation = null; + Map modificationData = Map.of(); + String subType; + if (flowableEvent instanceof FlowableDataObjectInstanceCreatedEventImpl) { + dataObjectInstance = ((FlowableDataObjectInstanceCreatedEventImpl) flowableEvent).getDataObjectInstance(); + subType = "created"; + } else if (flowableEvent instanceof FlowableDataObjectInstanceUpdatedEventImpl) { + dataObjectInstance = ((FlowableDataObjectInstanceUpdatedEventImpl) flowableEvent).getDataObjectInstance(); + subType = "updated"; + } else if (flowableEvent instanceof FlowableDataObjectInstanceDeletedEventImpl) { + dataObjectInstance = ((FlowableDataObjectInstanceDeletedEventImpl) flowableEvent).getDataObjectInstance(); + subType = "deleted"; + } else if (flowableEvent instanceof FlowableDataObjectInstancesBulkUpdatedEventImpl) { + scopeDefinitionId = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getDataObjectDefinition().getKey(); + operation = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getOperation(); + modificationData = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getModificationData(); + subType = "updatedBulk"; + } else if (flowableEvent instanceof FlowableDataObjectInstancesBulkDeletedEventImpl) { + scopeDefinitionId = ((FlowableDataObjectInstancesBulkDeletedEventImpl) flowableEvent).getDataObjectDefinition().getKey(); + operation = ((FlowableDataObjectInstancesBulkDeletedEventImpl) flowableEvent).getOperation(); + subType = "deletedBulk"; + } else { + LOGGER.warn("Unhandled FlowableDataObjectEvent type: {}", flowableEvent.getClass().getName()); + return; + } + + Map payload = new HashMap<>(); + String scopeId = null; + String subScopeId = null; + SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe(); + + if (subType.equals("created") || subType.equals("updated") || subType.equals("deleted")) { + HashMap data = (HashMap) dataObjectInstance.getData(); + if (data == null) { + LOGGER.warn("FlowableDataObjectEvent: No data"); + return; + } + + scopeDefinitionId = dataObjectInstance.getDefinitionKey(); + + for (Map.Entry entry : data.entrySet()) { + if (entry.getKey().equals("id")) { + scopeId = (String) entry.getValue(); + } else if (entry.getKey().equals("key")) { + // If there is a "key" in the data object, expose it as subScopeId for making it filterable. + subScopeId = (String) entry.getValue(); + } else { + payload.put(entry.getKey(), entry.getValue()); + } + } + } else { + payload.put("operation", operation); + payload.putAll(modificationData); + } + if (currentUserSecurityScope == null) { + auditService.createAuditInstanceBuilder() + .type("system") + .subType(subType) + .scopeType("dataObject") + .scopeDefinitionId(scopeDefinitionId) + .scopeId(scopeId) + .subScopeId(subScopeId) + .payload(payload) + .create(); + } else { + auditService.createAuditInstanceBuilder() + .type("user") + .creatorId(currentUserSecurityScope.getUserId()) + .tenantId(currentUserSecurityScope.getTenantId()) + .subType(subType) + .scopeType("dataObject") + .scopeDefinitionId(scopeDefinitionId) + .scopeId(scopeId) + .subScopeId(subScopeId) + .payload(payload) + .create(); + } + } + + @Override + public boolean isFailOnException() { + return true; + } + + @Override + public boolean isFireOnTransactionLifecycleEvent() { + return false; + } + + @Override + public String getOnTransaction() { + return TransactionState.COMMITTING.name(); + } + + @Override + public Collection getTypes() { + List eventTypes = new ArrayList<>(); + eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_CREATED); + eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_UPDATED); + eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_DELETED); + return eventTypes; + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/rest/ApplicationPropertyController.java b/flowable-local-work/src/main/java/com/flowable/local/work/rest/ApplicationPropertyController.java new file mode 100644 index 0000000..ce796a5 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/rest/ApplicationPropertyController.java @@ -0,0 +1,66 @@ +package com.flowable.local.work.rest; + +import com.flowable.autoconfigure.platform.PropertyConfigurationService; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/* +http://localhost:8090/custom-api/application-property/keyDoesNotExist -> null +http://localhost:8090/custom-api/application-property/string/keyDoesNotExist -> null +http://localhost:8090/custom-api/application-property/keyDoesNotExist?default=abc -> abc +http://localhost:8090/custom-api/application-property/string/keyDoesNotExist?default=abc -> abc +http://localhost:8090/custom-api/application-property/server.port -> 8090 +http://localhost:8090/custom-api/application-property/string/server.port -> 8090 +http://localhost:8090/custom-api/application-property/server.port?default=abc -> 8090 +http://localhost:8090/custom-api/application-property/string/server.port?default=abc -> 8090 +http://localhost:8090/custom-api/application-property/boolean/keyDoesNotExist -> false +http://localhost:8090/custom-api/application-property/boolean/keyDoesNotExist?default=true -> true +http://localhost:8090/custom-api/application-property/boolean/flowable.platform.idm.minimal-setup -> true +http://localhost:8090/custom-api/application-property/boolean/flowable.platform.idm.minimal-setup?default=false -> true +http://localhost:8090/custom-api/application-property/integer/keyDoesNotExist -> 0 +http://localhost:8090/custom-api/application-property/integer/keyDoesNotExist?default=123 -> true +http://localhost:8090/custom-api/application-property/integer/server.port -> 8090 +http://localhost:8090/custom-api/application-property/integer/server.port?default=123 -> 8090 + +http://localhost:8090/custom-api/application-property/integer/flowable.platform.idm.minimal-setup -> ConversionFailedException + */ + +@RestController +@RequestMapping("/custom-api") +@Validated +public class ApplicationPropertyController { + + protected final PropertyConfigurationService propertyConfigurationService; + + public ApplicationPropertyController(PropertyConfigurationService propertyConfigurationService) { + this.propertyConfigurationService = propertyConfigurationService; + } + + @GetMapping(value = {"/application-property/{key}", "/application-property/string/{key}"}) + public ResponseEntity getStringVariable( + @PathVariable String key, + @RequestParam(name = "default", required = false) String defaultValue) { + return ResponseEntity.ok(propertyConfigurationService.getProperty(key, defaultValue)); + } + + @GetMapping("/application-property/boolean/{key}") + public ResponseEntity getBooleanVariable( + @PathVariable String key, + @RequestParam(name = "default", required = false) Boolean defaultValue) { + if (defaultValue == null) { + defaultValue = false; + } + return ResponseEntity.ok(propertyConfigurationService.getBooleanProperty(key, defaultValue)); + } + + @GetMapping("/application-property/integer/{key}") + public ResponseEntity getIntegerVariable( + @PathVariable String key, + @RequestParam(name = "default", required = false) Integer defaultValue) { + if (defaultValue == null) { + defaultValue = 0; + } + return ResponseEntity.ok(propertyConfigurationService.getIntegerProperty(key, defaultValue)); + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/rest/AuditTrailController.java b/flowable-local-work/src/main/java/com/flowable/local/work/rest/AuditTrailController.java new file mode 100644 index 0000000..12e5316 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/rest/AuditTrailController.java @@ -0,0 +1,52 @@ +package com.flowable.local.work.rest; + +import com.flowable.local.work.service.AuditTrailService; +import org.flowable.common.rest.api.DataResponse; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.HashMap; + +/* +Examples: +http://localhost:8090/custom-api/audit-trail?scopeType=dataObject&dataObjectModelKey=TST_DO001 +http://localhost:8090/custom-api/audit-trail?scopeType=dataObject&dataObjectId=d05bd02b-3f8a-11f0-9e89-2e48f5a806de +http://localhost:8090/custom-api/audit-trail?scopeType=dataObject&scopeId=d05bd02b-3f8a-11f0-9e89-2e48f5a806de +http://localhost:8090/custom-api/audit-trail?scopeType=cmmn&instanceName=TST-000001 +http://localhost:8090/custom-api/audit-trail?scopeType=cmmn&rootId=CAS-9270a424-3fa3-11f0-8c07-2e48f5a806de +http://localhost:8090/custom-api/audit-trail?scopeType=cmmn&scopeId=CAS-9270a424-3fa3-11f0-8c07-2e48f5a806de +*/ + +@RestController +@RequestMapping("/custom-api") +@Validated +public class AuditTrailController { + + protected final AuditTrailService auditTrailService; + + public AuditTrailController(AuditTrailService auditTrailService) { + this.auditTrailService = auditTrailService; + } + + @GetMapping(value = {"/audit-trail"}, produces = {"application/json"}) + public DataResponse> getAuditTrail( + @RequestParam(name = "scopeType") @NotNull @NotEmpty String scopeType, + @RequestParam(name = "scopeId", required = false) String scopeId, + @RequestParam(name = "rootId", required = false) String rootId, + @RequestParam(name = "instanceName", required = false) String instanceName, + @RequestParam(name = "dataObjectId", required = false) String dataObjectId, + @RequestParam(name = "dataObjectModelKey", required = false) String dataObjectModelKey, + @RequestParam(name = "start", required = false) Integer start, + @RequestParam(name = "size", required = false) Integer size, + @RequestParam(name = "createdAfter", required = false) String createdAfter, + @RequestParam(name = "createdBefore", required = false) String createdBefore, + @RequestParam(name = "creatorId", required = false) String creatorId, + @RequestParam(name = "subType", required = false) String subType, + @RequestParam(name = "subScopeId", required = false) String subScopeId) { + + return this.auditTrailService.getAuditTrail(scopeType, scopeId, rootId, instanceName, dataObjectId, dataObjectModelKey, + start, size, createdAfter, createdBefore, creatorId, subType, subScopeId); + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/rest/UserController.java b/flowable-local-work/src/main/java/com/flowable/local/work/rest/UserController.java new file mode 100644 index 0000000..8bfae10 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/rest/UserController.java @@ -0,0 +1,38 @@ +package com.flowable.local.work.rest; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.flowable.local.work.service.UserService; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/* +Examples: +http://localhost:8090/custom-api/current-user-groups +http://localhost:8090/custom-api/user-groups/{userId} +*/ + +@RestController +@RequestMapping("/custom-api") +@Validated +public class UserController { + + protected final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping(value = {"/current-user-groups"}, produces = {"application/json"}) + public ResponseEntity getCurrentUserGroups() { + return ResponseEntity.ok(this.userService.getCurrentUserGroups()); + } + + @GetMapping(value = {"/user-groups/{userId}"}, produces = {"application/json"}) + public ResponseEntity getUserGroups(@PathVariable String userId) { + return ResponseEntity.ok(this.userService.getUserGroups(userId)); + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/rest/VariableController.java b/flowable-local-work/src/main/java/com/flowable/local/work/rest/VariableController.java new file mode 100644 index 0000000..575a587 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/rest/VariableController.java @@ -0,0 +1,44 @@ +package com.flowable.local.work.rest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.flowable.local.work.service.VariableService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.*; + +/* +http://localhost:8090/custom-api/variable/CAS-e5d25542-0947-11f0-8b0d-e6be909bfdcd/caseNname +http://localhost:8090/custom-api/variable/CAS-e5d25542-0947-11f0-8b0d-e6be909bfdcd +http://localhost:8090/custom-api/variable/root + +http://localhost:8090/platform-api/case-instances/CAS-5dc9c774-42d4-11f0-b269-2e48f5a806de/work-form/variables +http://localhost:8090/custom-api/variable/CAS-5dc9c774-42d4-11f0-b269-2e48f5a806de/name + */ + +@RestController +@RequestMapping("/custom-api") +public class VariableController { + + protected final VariableService variableService; + + public VariableController(VariableService variableService) { + this.variableService = variableService; + } + + @GetMapping("/variable/{caseInstanceId}") + public ResponseEntity getRootVariable(@PathVariable String caseInstanceId) { + return ResponseEntity.ok(variableService.getVariable(caseInstanceId, "root")); + } + + @GetMapping("/variable/{caseInstanceId}/{variableName}") + public ResponseEntity getSpecificVariable(@PathVariable String caseInstanceId, @PathVariable String variableName) { + return ResponseEntity.ok(variableService.getVariable(caseInstanceId, variableName)); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleMissingServletRequestParameterException() { + return "Variable not found"; + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/service/AuditTrailService.java b/flowable-local-work/src/main/java/com/flowable/local/work/service/AuditTrailService.java new file mode 100644 index 0000000..93007ab --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/service/AuditTrailService.java @@ -0,0 +1,153 @@ +package com.flowable.local.work.service; + +import com.flowable.audit.api.AuditService; +import com.flowable.audit.api.runtime.AuditInstance; +import com.flowable.audit.api.runtime.AuditInstanceQuery; +import com.flowable.audit.engine.impl.persistence.entity.AuditInstanceEntity; +import org.flowable.cmmn.api.history.HistoricCaseInstance; +import org.flowable.cmmn.api.runtime.CaseInstance; +import org.flowable.cmmn.engine.CmmnEngine; +import org.flowable.common.rest.api.DataResponse; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.*; + +/* + Returns audit trail entries based on: + - scopeType cmmn + - rootId + -> audit trail entries for case with specified id + - instanceName + -> audit trail entries for case with specified name + - scopeType dataObject + - dataObjectId + -> audit trail entries for data object with specified instance id, provided by DataObjectAuditTrailEventHandler + - dataObjectModelKey + -> audit trail entries for data object with specified model key, provided by DataObjectAuditTrailEventHandler + */ +@Component +public class AuditTrailService { + protected static final Logger LOGGER = LoggerFactory.getLogger(AuditTrailService.class); + protected final CmmnEngine cmmnEngine; + protected final AuditService auditService; + + public AuditTrailService(CmmnEngine cmmnEngine, AuditService auditService) { + this.cmmnEngine = cmmnEngine; + this.auditService = auditService; + } + + public DataResponse> getAuditTrail(String scopeType, String scopeId, String rootId, String instanceName, + String dataObjectId, String dataObjectModelKey, + Integer start, Integer size, + String createdAfter, String createdBefore, + String creatorId, String subType, String subScopeId) { + String scopeDefinitionId = null; + switch (scopeType) { + case "cmmn": + // Check for rootId or instanceName if scopeId is not provided + if (scopeId == null || scopeId.trim().isEmpty()) { + if (rootId != null && !rootId.trim().isEmpty()) { + scopeId = rootId; + } else if (instanceName != null && !instanceName.trim().isEmpty()) { + CaseInstance caseInstance = this.cmmnEngine.getCmmnRuntimeService().createCaseInstanceQuery().caseInstanceBusinessKey("asdf").singleResult(); +// CaseInstance caseInstance = this.cmmnEngine.getCmmnRuntimeService().createCaseInstanceQuery().caseInstanceName(instanceName).singleResult(); + if (caseInstance != null) { + scopeId = caseInstance.getId(); + } else { + HistoricCaseInstance historicCaseInstance = this.cmmnEngine.getCmmnHistoryService().createHistoricCaseInstanceQuery().caseInstanceName(instanceName).singleResult(); + if (historicCaseInstance != null) { + scopeId = historicCaseInstance.getId(); + } + } + } else { + LOGGER.warn("Missing parameter rootId or instanceName for scopeType=cmmn"); + return createAuditEntryDataResponse(null, 0, 0); + } + } + break; + case "dataObject": + // Check for dataObjectId or dataObjectModelKey if scopeId is not provided + if (scopeId == null || scopeId.trim().isEmpty()) { + if (dataObjectId != null && !dataObjectId.trim().isEmpty()) { + scopeId = dataObjectId; + } else if (dataObjectModelKey != null && !dataObjectModelKey.trim().isEmpty()) { + scopeDefinitionId = dataObjectModelKey; + } else { + LOGGER.warn("Missing parameter dataObjectId or dataObjectModelKey for scopeType=dataObject"); + return createAuditEntryDataResponse(null, 0, 0); + } + } + break; + default: + LOGGER.warn("Invalid scopeType: {}", scopeType); + return createAuditEntryDataResponse(null, 0, 0); + } + return queryAuditTrail(scopeType, scopeId, scopeDefinitionId, start, size, createdAfter, createdBefore, creatorId, subType, subScopeId); + } + + protected DataResponse> queryAuditTrail(String scopeType, String scopeId, + String scopeDefinitionId, + Integer start, Integer size, + String createdAfter, String createdBefore, + String creatorId, String subType, String subScopeId) { + // Create query from parameters + AuditInstanceQuery query = this.auditService.createAuditInstanceQuery().orderByCreationTime().desc(); + query.scopeType(scopeType); + query.scopeId(scopeId); + query.scopeDefinitionId(scopeDefinitionId); + if (createdAfter != null && !createdAfter.trim().isEmpty()) { + query.createdAfter(DateTime.parse(createdAfter).toDate()); + } + if (createdBefore != null && !createdBefore.trim().isEmpty()) { + query.createdBefore(DateTime.parse(createdBefore).toDate()); + } + query.creatorId(creatorId); + query.subType(subType); + query.subScopeId(subScopeId); + + // Run paged query + int startIndex = start == null ? 0 : start; + List auditInstances; + if (size != null) { + auditInstances = query.listPage(startIndex, size); + } else { + auditInstances = query.list(); + } + + // Map query result into proper list + List> entries = new ArrayList<>(); + for (AuditInstance auditInstance : auditInstances) { + entries.add(convertAuditInstance(auditInstance)); + } + return createAuditEntryDataResponse(entries, query.count(), startIndex); + } + + protected DataResponse> createAuditEntryDataResponse(List> entries, long totalCount, int startIndex) { + DataResponse> auditTrail = new DataResponse<>(); + if (entries != null && !entries.isEmpty()) { + auditTrail.setData(entries); + auditTrail.setTotal(totalCount); + auditTrail.setStart(startIndex); + auditTrail.setSize(entries.size()); + } + auditTrail.setSort("creationTime"); + auditTrail.setOrder("desc"); + return auditTrail; + } + + protected HashMap convertAuditInstance(AuditInstance auditInstance) { + HashMap auditEntry = new HashMap<>(); + AuditInstanceEntity auditInst = (AuditInstanceEntity) auditInstance; + auditEntry.put("id", auditInst.getId()); + auditEntry.putAll((Map) auditInst.getPersistentState()); + + HashMap auditInstPayload = (HashMap) auditInst.getPayload(); + if (auditInstPayload != null) { + auditEntry.put("payload", new HashMap<>(auditInstPayload)); + } + return auditEntry; + } +} \ No newline at end of file diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/service/DataObjectUtils.java b/flowable-local-work/src/main/java/com/flowable/local/work/service/DataObjectUtils.java new file mode 100644 index 0000000..bbe46ad --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/service/DataObjectUtils.java @@ -0,0 +1,43 @@ +package com.flowable.local.work.service; + +import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainer; +import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainerBuilder; +import com.flowable.dataobject.api.runtime.DataObjectRuntimeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class DataObjectUtils { + + protected final DataObjectRuntimeService dataObjectRuntimeService; + + public DataObjectUtils(DataObjectRuntimeService dataObjectRuntimeService) { + this.dataObjectRuntimeService = dataObjectRuntimeService; + } + + public void create(String tenantId, String definitionKey) { + DataObjectInstanceVariableContainerBuilder dataObjectValueInstanceBuilder = dataObjectRuntimeService.createDataObjectValueInstanceBuilder(); + dataObjectValueInstanceBuilder.tenantId(tenantId); + dataObjectValueInstanceBuilder.definitionKey(definitionKey); + /*TODO: iterate list + for (entry : list) { + dataObjectValueInstanceBuilder.value(entry.key(), entry.value()); + } + */ + dataObjectValueInstanceBuilder.create(); + } + + public void delete(String tenantId, String definitionKey) { + List dataObjects = dataObjectRuntimeService.createDataObjectInstanceQuery() + .tenantId(tenantId) + .definitionKey(definitionKey) + .operation("searchAll") + .list(); + for (DataObjectInstanceVariableContainer dataObjectInstanceVariableContainer : dataObjects) { + dataObjectRuntimeService.deleteDataObject(dataObjectInstanceVariableContainer.getLookupId().toString(), + dataObjectInstanceVariableContainer.getDefinitionId()); + } + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/service/JsonUtils.java b/flowable-local-work/src/main/java/com/flowable/local/work/service/JsonUtils.java new file mode 100644 index 0000000..dcb2a06 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/service/JsonUtils.java @@ -0,0 +1,107 @@ +package com.flowable.local.work.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.github.wnameless.json.flattener.JsonFlattener; +import com.github.wnameless.json.unflattener.JsonUnflattener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Component +public class JsonUtils { + + protected static final Logger LOGGER = LoggerFactory.getLogger(JsonUtils.class); + protected final ObjectMapper objectMapper; + protected final JavaTimeModule javaTimeModule; + + public JsonUtils(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + this.javaTimeModule = new JavaTimeModule(); + + // Enable ObjectMapper for handling Instant as string + this.objectMapper.registerModule(javaTimeModule); + this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + } + + public JsonNode convertListToJsonNode(List list) { + return objectMapper.valueToTree(list); + } + + public JsonNode convertMapToJsonNode(Map map) { + return objectMapper.valueToTree(map); + } + + public Map convertJsonNodeToMap(JsonNode jsonNode) { + return objectMapper.convertValue(jsonNode, new TypeReference>() {}); + } + + public Map flatten(Object payload) { + try { + return JsonFlattener.flattenAsMap(objectMapper.writeValueAsString(payload)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public Map unflatten(Map flatVars) { + try { + return objectMapper.readValue(JsonUnflattener.unflatten(flatVars), Map.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public JsonNode convertJsonStringToJsonNode(String jsonString) { + try { + return objectMapper.readTree(jsonString); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public Map convertObjectNodeToMap(ObjectNode objectNode) { + return objectMapper.convertValue(objectNode, new TypeReference>() {}); + } + + public ObjectNode getEmptyObjectNode() { + return objectMapper.createObjectNode(); + } + + public ObjectNode loadObjectNodeFromFile(String path) { + try { + return (ObjectNode) this.objectMapper.readTree(new File(path)); + } catch (IOException e) { + LOGGER.debug(e.getMessage()); + return null; + } + } + + public ArrayNode convertObjectToArrayNode(Object object) { + if (object == null) { + LOGGER.debug("{}: Argument is null", this.getClass().getName()); + return null; + } + String obyTypeName = object.getClass().getName(); + if (obyTypeName.equals("java.util.Collections$EmptyList") || obyTypeName.equals("java.util.ArrayList")) { + return objectMapper.convertValue(object, ArrayNode.class); + } else if (obyTypeName.equals("com.fasterxml.jackson.databind.node.ArrayNode")) { + return (ArrayNode) object; + } else { + LOGGER.debug("{}: {} is not implemented}", this.getClass().getName(), obyTypeName); + return null; + } + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/service/StartProcessBot.java b/flowable-local-work/src/main/java/com/flowable/local/work/service/StartProcessBot.java new file mode 100755 index 0000000..18dba2f --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/service/StartProcessBot.java @@ -0,0 +1,77 @@ +package com.flowable.local.work.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.flowable.action.api.bot.BaseBotActionResult; +import com.flowable.action.api.bot.BotActionResult; +import com.flowable.action.api.bot.BotService; +import com.flowable.action.api.history.HistoricActionInstance; +import com.flowable.action.api.intents.Intent; +import com.flowable.action.api.repository.ActionDefinition; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.runtime.ProcessInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +public class StartProcessBot implements BotService { + protected static final Logger LOGGER = LoggerFactory.getLogger(StartProcessBot.class); + public static final String BOT_KEY = "start-process-bot"; + public static final String BOT_NAME = "start process bot"; + public static final String BOT_DESCRIPTION = "This bot starts a process."; + + private final RuntimeService runtimeService; + private final ObjectMapper objectMapper; + private final HistoryService historyService; + + public StartProcessBot(RuntimeService runtimeService, + ObjectMapper objectMapper, + HistoryService historyService) { + this.runtimeService = runtimeService; + this.objectMapper = objectMapper; + this.historyService = historyService; + } + + @Override + public String getKey() { return BOT_KEY; } + + @Override + public String getName() { return BOT_NAME; } + + @Override + public String getDescription() { return BOT_DESCRIPTION; } + + @Override + public BotActionResult invokeBot(HistoricActionInstance actionInstance, ActionDefinition actionDefinition, Map payload) { + LOGGER.info(getKey() + " starting..."); + String rootCaseId = actionInstance.getScopeId(); + if (rootCaseId == null) { + throw new RuntimeException(getKey() + " cannot get Scope Id from action button"); + } + String taskModelId = null; + try { + taskModelId = payload.get("taskModelId").toString(); + } catch (Exception e) { + } + if (taskModelId == null) { + throw new RuntimeException(getKey() + " cannot get taskModelId from payload"); + } + LOGGER.info(getKey() + " starting task instance with taskModelId " + taskModelId + "..."); + ProcessInstance processInstance = this.runtimeService.createProcessInstanceBuilder() + .processDefinitionKey(taskModelId) + .tenantId(actionInstance.getTenantId()) + .variables(payload) + .start(); + if (processInstance == null) { + throw new RuntimeException(getKey() + " cannot find enabled task instance with taskModelId " + taskModelId); + } + LOGGER.info(getKey() + " task instance with taskModelId " + taskModelId + " successfully started"); + + Map processVariables = processInstance.getProcessVariables(); + ObjectNode responsePayload = this.objectMapper.convertValue(processVariables, ObjectNode.class); + responsePayload.put("id", processInstance.getId()); + return new BaseBotActionResult(responsePayload, Intent.NOOP); + } +} diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/service/UserService.java b/flowable-local-work/src/main/java/com/flowable/local/work/service/UserService.java new file mode 100644 index 0000000..dbda793 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/service/UserService.java @@ -0,0 +1,101 @@ +package com.flowable.local.work.service; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.flowable.core.common.api.security.SecurityScope; +import com.flowable.core.idm.api.*; +import com.flowable.core.spring.security.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.validation.constraints.NotNull; +import java.util.*; +import java.util.stream.Collectors; + + +/* + Returns user information: + - getCurrentUserGroups() + - getUserGroups(userId) + */ +@Component +public class UserService { + + protected static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); + protected final PlatformIdentityService platformIdentityService; + + public UserService(PlatformIdentityService platformIdentityService) { + this.platformIdentityService = platformIdentityService; + } + + @NotNull + public ArrayNode getCurrentUserGroups() { + LOGGER.debug("Getting current user groups"); + SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe(); + List stringList = new ArrayList<>(currentUserSecurityScope.getGroupKeys()); + return createSortedArrayNode(stringList); + } + + @NotNull + protected static ArrayNode createSortedArrayNode(List stringList) { + Collections.sort(stringList); + ArrayNode roles = JsonNodeFactory.instance.arrayNode(); + for(String role : stringList) { + // Apply a role filter here if necessary + roles.add(role); + } + return roles; + } + + @NotNull + public ArrayNode getUserGroups(String userId) { + LOGGER.debug("Getting groups of user {}", userId); + SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe(); + // Get groups via User Query + List groupStringList = getLdapEntitlements(userId, currentUserSecurityScope.getTenantId()); + if (groupStringList.isEmpty()) { + // Get groups via Group Query + groupStringList = getPlatformGroups(userId, currentUserSecurityScope); + } + return createSortedArrayNode(groupStringList); + } + + @NotNull + protected List getPlatformGroups(String userId, SecurityScope currentUserSecurityScope) { + List platformGroups = new ArrayList<>(); + try { + platformGroups = platformIdentityService.createPlatformGroupQuery() + .groupTenantId(currentUserSecurityScope.getTenantId()) + .groupMember(userId) + .list(); + } catch (Exception ignored) { + } + List stringList = new ArrayList<>(); + for(PlatformGroup platformGroup : platformGroups) { + stringList.add(platformGroup.getKey()); + } + return stringList; + } + + @NotNull + protected List getLdapEntitlements(String userId, String tenantId) { + PlatformUser platformUser = platformIdentityService.createPlatformUserQuery() + .tenantId(tenantId) + .userId(userId) + .includeIdentityInfo() + .singleResult(); + return Optional.ofNullable(platformUser) + .map(PlatformUser::getIdentityInfo) + .flatMap(identityInfos -> identityInfos.stream() + .filter(ifInfo -> "ldap_entitlement".equals(ifInfo.getName())) + .findFirst()) + .map(PlatformIdentityInfo::getValue) + .filter(value -> value instanceof String) // Ensure that value is String + .map(value -> (String) value) + .map(value -> Arrays.stream(value.split(";")) + .filter(val -> !val.trim().isEmpty()) + .collect(Collectors.toList())) + .orElseGet(List::of); + } +} \ No newline at end of file diff --git a/flowable-local-work/src/main/java/com/flowable/local/work/service/VariableService.java b/flowable-local-work/src/main/java/com/flowable/local/work/service/VariableService.java new file mode 100644 index 0000000..a113b85 --- /dev/null +++ b/flowable-local-work/src/main/java/com/flowable/local/work/service/VariableService.java @@ -0,0 +1,37 @@ +package com.flowable.local.work.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.flowable.cmmn.api.CmmnRuntimeService; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.*; + +@Component +public class VariableService { + protected final ObjectMapper objectMapper; + protected final CmmnRuntimeService cmmnRuntimeService; + + public VariableService(ObjectMapper objectMapper, CmmnRuntimeService cmmnRuntimeService) { + this.objectMapper = objectMapper; + this.objectMapper.registerModule(new JavaTimeModule()); + this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + this.cmmnRuntimeService = cmmnRuntimeService; + } + + public JsonNode getVariable(@PathVariable String caseInstanceId, String variableName) { + // Check parameters + + Map map = new LinkedHashMap<>(); + map.put("caseInstanceId", caseInstanceId); + map.put("variableName", variableName); + map.put("value", this.cmmnRuntimeService.getVariables(caseInstanceId)); + return this.objectMapper.convertValue(map, JsonNode.class); + } + + +} \ No newline at end of file diff --git a/flowable-local-work/src/main/resources/application-infraless.properties b/flowable-local-work/src/main/resources/application-infraless.properties new file mode 100644 index 0000000..7fada9c --- /dev/null +++ b/flowable-local-work/src/main/resources/application-infraless.properties @@ -0,0 +1,8 @@ +# == Embedded DB == +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:file:./h2-database/flowable-work;DB_CLOSE_ON_EXIT=FALSE + +# == Elasticless == +flowable.indexing.enabled=false +management.health.elasticsearch.enabled=false +management.metrics.export.elastic.enabled=false diff --git a/flowable-local-work/src/main/resources/application-prod.properties b/flowable-local-work/src/main/resources/application-prod.properties new file mode 100644 index 0000000..dd6723f --- /dev/null +++ b/flowable-local-work/src/main/resources/application-prod.properties @@ -0,0 +1,11 @@ +flowable.license.db-store-enabled=true +flowable.inspect.enabled=false +# Pretty-print JSON responses +spring.jackson.serialization.indent_output=false +# Forms will update even if an old process/case/task definition will be used +flowable.platform.enable-latest-form-definition-lookup=false +# Environment identifier +info.env.name= +# In order for hot swapping to work for the custom.js and custom.css +spring.thymeleaf.cache=true +spring.web.resources.chain.cache=true \ No newline at end of file diff --git a/flowable-local-work/src/main/resources/application.properties b/flowable-local-work/src/main/resources/application.properties new file mode 100644 index 0000000..8ab1790 --- /dev/null +++ b/flowable-local-work/src/main/resources/application.properties @@ -0,0 +1,40 @@ +server.port=8090 + +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/flowable +spring.datasource.username=flowable +spring.datasource.password=flowable + +# Enable all endpoints over HTTP +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=ALWAYS + +# Set this false, to disable metrics export to elasticsearch +# management.metrics.export.elastic.enabled=false + +flowable.indexing.index-name-prefix=local- +flowable.security.impersonate.allowed=true + +# Set this to true to get a clean IDM setup with only an admin user +flowable.platform.idm.minimal-setup=true +flowable.platform.idm.default-password=test + +# == Development Properties == +flowable.license.db-store-enabled=false +flowable.inspect.enabled=true +# Pretty-print JSON responses +spring.jackson.serialization.indent_output=true +# Forms will update even if an old process/case/task definition will be used +flowable.platform.enable-latest-form-definition-lookup=true +# Environment identifier +info.env.name=Development +# In order for hot swapping to work for the custom.js and custom.css +spring.thymeleaf.cache=false +spring.web.resources.chain.cache=false + +# Path to auto-deploy-apps +flowable.app.resource-location=classpath*:/auto-deploy-apps/ + +# Email +flowable.mail.server.host=localhost +flowable.mail.server.port=2525 diff --git a/flowable-local-work/src/main/resources/com/flowable/dataobject/custom/md-country.data b/flowable-local-work/src/main/resources/com/flowable/dataobject/custom/md-country.data new file mode 100644 index 0000000..b84e99a --- /dev/null +++ b/flowable-local-work/src/main/resources/com/flowable/dataobject/custom/md-country.data @@ -0,0 +1,21 @@ +{ + "key": "md-country", + "name": "Country", + "description": "The master data definition for countries.", + + "dataObjectType": "masterData", + "type": "internal", + "subType": "country", + + "sourceId": "JSON", + "supportsNameFiltering": true, + + "keyField": "alpha3Code", + "idField": "alpha3Code", + "nameField": "name", + + "variables": { + "alpha2Code": "alpha2Code", + "numericCode": "numeric" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/main/resources/com/flowable/master-data/custom/md-country.json b/flowable-local-work/src/main/resources/com/flowable/master-data/custom/md-country.json new file mode 100644 index 0000000..b34f14e --- /dev/null +++ b/flowable-local-work/src/main/resources/com/flowable/master-data/custom/md-country.json @@ -0,0 +1,47 @@ +{ + "dataObjectDefinitionKey": "md-country", + "masterData": [ + { + "name": "Australia default", + "alpha2Code": "AU", + "alpha3Code": "AUS", + "numeric": 36, + "translations": { + "de": { + "name": "Australien de" + }, + "en": { + "name": "Australia en" + } + } + }, + { + "name": "Bermuda default", + "alpha2Code": "BM", + "alpha3Code": "BMU", + "numeric": 60, + "translations": { + "de": { + "name": "Bermuda de" + }, + "en": { + "name": "Bermuda en" + } + } + }, + { + "name": "Haiti default", + "alpha2Code": "HT", + "alpha3Code": "HTI", + "numeric": 332, + "translations": { + "de": { + "name": "Haiti de" + }, + "en": { + "name": "Haiti en" + } + } + } + ] +} \ No newline at end of file diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/FlowableLocalWorkApplicationIT.java b/flowable-local-work/src/test/java/com/flowable/local/work/FlowableLocalWorkApplicationIT.java new file mode 100644 index 0000000..d18b861 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/FlowableLocalWorkApplicationIT.java @@ -0,0 +1,23 @@ +package com.flowable.local.work; + +import org.flowable.spring.impl.test.FlowableSpringExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Because executing a Spring Boot Test will actually start Flowable and therefore additional + * infrastructure like elasticsearch is needed to run this test it is not a Unit Test but an + * Integration Test and should therefore not executed by maven surefire. Therefore the ending IT + * (for IntegrationTest) instead of Test. + */ +@SpringBootTest +@ExtendWith(FlowableSpringExtension.class) +//@Deployment(resources = {"my-process.bmpn20.xml"}) +class FlowableLocalWorkApplicationIT { + + @Test + void canStartApplication() { + + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/FlowableLocalWorkApplicationTest.java b/flowable-local-work/src/test/java/com/flowable/local/work/FlowableLocalWorkApplicationTest.java new file mode 100644 index 0000000..c9915c2 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/FlowableLocalWorkApplicationTest.java @@ -0,0 +1,14 @@ +package com.flowable.local.work; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +class FlowableLocalWorkApplicationTest { + + @Test + void canStartContext() { + + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/config/TestConfiguration.java b/flowable-local-work/src/test/java/com/flowable/local/work/config/TestConfiguration.java new file mode 100644 index 0000000..46c0772 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/config/TestConfiguration.java @@ -0,0 +1,7 @@ +package com.flowable.local.work.config; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TestConfiguration { +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/EmailDto.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/EmailDto.java new file mode 100644 index 0000000..913708a --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/EmailDto.java @@ -0,0 +1,49 @@ +package com.flowable.local.work.model; + +import java.util.List; + +public class EmailDto { + private String subject; + private List receiverList; + private String content; + private Object contentRaw; + + public EmailDto(String subject, List receiverList, String content, Object contentRaw) { + this.subject = subject; + this.receiverList = receiverList; + this.content = content; + this.contentRaw = contentRaw; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public List getReceiverList() { + return receiverList; + } + + public void setReceiverList(List receiverList) { + this.receiverList = receiverList; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Object getContentRaw() { + return contentRaw; + } + + public void setContentRaw(Object contentRaw) { + this.contentRaw = contentRaw; + } +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableExcelMapper.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableExcelMapper.java new file mode 100644 index 0000000..e484214 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableExcelMapper.java @@ -0,0 +1,335 @@ +package com.flowable.local.work.model; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.stereotype.Component; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + + +@Component +public class FlowableExcelMapper { + + protected static ObjectMapper objectMapper = new ObjectMapper(); + protected static JavaTimeModule javaTimeModule = new JavaTimeModule(); + protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser(); + + public FlowableExcelMapper() { + // Enable ObjectMapper for handling Instant as string + objectMapper.registerModule(javaTimeModule); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + } + + // excelPath defines the location of the Excel file + // FlowableExcelParser is used to create an ArrayList from the Excel file + // The 1st row of every sheet contains the path of the variables to be created in the JsonNode + // Delimiter is "." for an ObjectNode and "*" for an ArrayNode + // The 2nd row contains tht type of the variable. Supported types are: + // - String + // - Boolean + // - Integer + // - Double + // - Date + // All sheets are added to an ArrayNode + // All rows are added to an ArrayNode in the sheets ArrayNode + // All cell values are added to an ObjectNode in the rows ArrayNode + public ArrayNode excelBookResourceToJsonNode(String resourcePath) { + ArrayList>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath); + return bookToJsonNode(book); + } + + protected ArrayNode bookToJsonNode(ArrayList>> book) { + ArrayNode bookNode = objectMapper.createArrayNode(); + for (ArrayList> sheet : book) { + bookNode.add(sheetToJsonNode(sheet)); + } + return bookNode; + } + + protected ArrayNode sheetToJsonNode(ArrayList> rows) { + ArrayList paths = new ArrayList<>(); + ArrayList types = new ArrayList<>(); + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (int rowNum = 0; rowNum < rows.size(); rowNum++) { + if (rowNum == 0) { + // The 1st row contains the paths + paths.addAll(rows.get(rowNum)); + } else if (rowNum == 1) { + // The 2nd row contains the types of the variables + types.addAll(rows.get(rowNum)); + } else { + // All other rows contain the values of the variables + arrayNode.add(rowToJsonNode(types, paths, rows.get(rowNum), null)); + } + } + return arrayNode; + } + + protected JsonNode rowToJsonNode(ArrayList types, ArrayList paths, ArrayList row, JsonNode jsonNode) { + for (int cellNum = 0; cellNum < row.size(); cellNum++) { + String cellValue = row.get(cellNum); + // Empty cells are not added to node + if (cellValue != null && !cellValue.isEmpty()) { + String type = types.get(cellNum); + Object content; + switch (cellValue) { + case "__NULL": content = null; break; + case "__N_A": content = "__N_A"; break; + default: + switch (type) { + case "String": + switch (cellValue) { + case "__EMPTY": content = ""; break; + case "__BLANK": content = " "; break; + default: content = cellValue; + } + break; + case "Boolean": content = Boolean.parseBoolean(cellValue); break; + case "Integer": content = Integer.parseInt(cellValue); break; + case "Double": content = Double.parseDouble(cellValue); break; + case "Long": content = Long.parseLong(cellValue); break; + case "Date": + // Excel considers dates as local dates, but we want them as UTC + Instant instant; + try { + LocalDateTime localDateTime = LocalDateTime.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant(); + } catch (DateTimeParseException e) { + LocalDate localDate = LocalDate.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + instant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); + } + content = instant.plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds()); + break; + default: + throw new RuntimeException("Variable type not supported: " + type); + } + } + jsonNode = addNode(jsonNode, paths.get(cellNum), content); + } + } + return jsonNode; + } + + protected JsonNode addNode(JsonNode parent, String path, Object value) { + if (parent == null) { + parent = objectMapper.createObjectNode(); + } + + int delimiterPosition = indexOfFirstDelimiter(path, ".*"); + if (delimiterPosition <= 0) { + if (parent instanceof ObjectNode) { + // Key of a value in an ObjectNode + ((ObjectNode) parent).set(path, objectMapper.valueToTree(value)); + } else { + // Index of a value in an ArrayNode + int index = Integer.parseInt(path); + while (parent.size() <= index) { + // Create empty entries to parent ArrayNode + ((ArrayNode) parent).add(objectMapper.createObjectNode()); + } + ((ArrayNode) parent).set(index, objectMapper.valueToTree(value)); + } + return parent; + } + + String key = path.substring(0, delimiterPosition); + String newPath = path.substring(delimiterPosition + 1); + String delimiter = path.substring(delimiterPosition, delimiterPosition + 1); + JsonNode child = null; + if (delimiter.equals(".")) { + child = objectMapper.createObjectNode(); + } else if (delimiter.equals("*")) { + child = objectMapper.createArrayNode(); + } + + JsonNode node; + if (parent instanceof ObjectNode) { + // Key of a JsonNode in an ObjectNode + node = parent.get(key); + if (node != null) { + // Take the existing ObjectNode as child + child = node; + } + ((ObjectNode) parent).set(key, addNode(child, newPath, value)); + } else { + // Index of a JsonNode in an ArrayNode + int index = Integer.parseInt(key); + node = parent.get(index); + if (node != null) { + // Take the existing ArrayNode as child + child = node; + } + while (parent.size() <= index) { + // Create empty entries to parent ArrayNode + ((ArrayNode) parent).add(objectMapper.createObjectNode()); + } + ((ArrayNode) parent).set(index, addNode(child, newPath, value)); + } + return parent; + } + + protected int indexOfFirstDelimiter(String str, String delimiters) { + for (int i = 0; i < str.length(); i++) { + for (char c : delimiters.toCharArray()) { + if (str.charAt(i) == c) { + return i; + } + } + } + return -1; + } + + public ArrayList> excelBookResourceToObj(String resourcePath) { + ArrayList>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath); + return bookToObj(book); + } + + protected ArrayList> bookToObj(ArrayList>> book) { + ArrayList> bookNode = new ArrayList<>(); + for (ArrayList> sheet : book) { + bookNode.add(sheetToObj(sheet)); + } + return bookNode; + } + + protected ArrayList sheetToObj(ArrayList> rows) { + ArrayList paths = new ArrayList<>(); + ArrayList types = new ArrayList<>(); + ArrayList arrayNode = new ArrayList<>(); + for (int rowNum = 0; rowNum < rows.size(); rowNum++) { + if (rowNum == 0) { + // The 1st row contains the paths + paths.addAll(rows.get(rowNum)); + } else if (rowNum == 1) { + // The 2nd row contains the types of the variables + types.addAll(rows.get(rowNum)); + } else { + // All other rows contain the values of the variables + arrayNode.add(rowToObj(types, paths, rows.get(rowNum), null)); + } + } + return arrayNode; + } + + protected Object rowToObj(ArrayList types, ArrayList paths, ArrayList row, Object node) { + for (int cellNum = 0; cellNum < row.size(); cellNum++) { + String cellValue = row.get(cellNum); + // Empty cells are not added to node + if (cellValue != null && !cellValue.isEmpty()) { + String type = types.get(cellNum); + Object content; + switch (cellValue) { + case "__NULL": content = null; break; + case "__N_A": content = "__N_A"; break; + default: + switch (type) { + case "String": + switch (cellValue) { + case "__EMPTY": content = ""; break; + case "__BLANK": content = " "; break; + default: content = cellValue; + } + break; + case "Boolean": content = Boolean.parseBoolean(cellValue); break; + case "Integer": content = Integer.parseInt(cellValue); break; + case "Double": content = Double.parseDouble(cellValue); break; + case "Long": content = Long.parseLong(cellValue); break; + case "Date": + // Excel considers dates as local dates, but we want them as UTC + Instant instant; + try { + LocalDateTime localDateTime = LocalDateTime.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant(); + } catch (DateTimeParseException e) { + LocalDate localDate = LocalDate.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + instant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); + } + content = instant.plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds()); + break; + default: + throw new RuntimeException("Variable type not supported: " + type); + } + } + node = addObj(node, paths.get(cellNum), content); + } + } + return node; + } + + @SuppressWarnings("unchecked") + protected Object addObj(Object parent, String path, Object value) { + if (parent == null) { + parent = new LinkedHashMap(); + } + + int delimiterPosition = indexOfFirstDelimiter(path, ".*"); + if (delimiterPosition <= 0) { + if (parent instanceof LinkedHashMap) { + // Key of a value in an ObjectNode + ((Map) parent).put(path, value); + } else if (parent instanceof ArrayList) { + // Index of a value in an ArrayNode + int index = Integer.parseInt(path); + while (((ArrayList) parent).size() <= index) { + // Create empty entries to parent ArrayNode + ((ArrayList) parent).add(new LinkedHashMap ()); + } + ((ArrayList) parent).set(index, value); + } else { + throw new RuntimeException("Parent type not supported: " + parent.getClass().getName()); + } + return parent; + } + + String key = path.substring(0, delimiterPosition); + String newPath = path.substring(delimiterPosition + 1); + String delimiter = path.substring(delimiterPosition, delimiterPosition + 1); + Object child = null; + if (delimiter.equals(".")) { + child = new LinkedHashMap(); + } else if (delimiter.equals("*")) { + child = new ArrayList<>(); + } + + Object node; + if (parent instanceof LinkedHashMap) { + // Key of a JsonNode in an ObjectNode + node = ((Map) parent).get(key); + if (node != null) { + // Take the existing ObjectNode as child + child = node; + } + ((Map) parent).put(key, addObj(child, newPath, value)); + } else if (parent instanceof ArrayList) { + // Index of a JsonNode in an ArrayNode + int index = Integer.parseInt(key); + if (((ArrayList) parent).size() > index) { + node = ((ArrayList) parent).get(index); + } else { + node = null; + } + if (node != null) { + // Take the existing ArrayNode as child + child = node; + } + while (((ArrayList) parent).size() <= index) { + // Create empty entries to parent ArrayNode + ((ArrayList) parent).add(new LinkedHashMap ()); + } + ((ArrayList) parent).set(index, addObj(child, newPath, value)); + } else { + throw new RuntimeException("Parent type not supported: " + parent.getClass().getName()); + } + return parent; + } + +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableExcelParser.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableExcelParser.java new file mode 100644 index 0000000..294eb50 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableExcelParser.java @@ -0,0 +1,138 @@ +package com.flowable.local.work.model; + +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Component; + +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.LinkedHashMap; + + +@Component +public class FlowableExcelParser { + + // excelPath defines the location of the Excel file + // Return all cells in all sheets with the following restrictions: + // - Max column count per sheet is defined in the first row + // - Max column count starts with the 1st cell and ends with the 1st empty cell + // - Parsing the sheet stops after the 1st empty row + // All sheets are added to an ArrayList + // All rows are added to an ArrayList in the sheets ArrayList + // All cell values are added to an ArrayList in the rows ArrayList + // Formulas in cells are evaluated + // All values are returned as String + public ArrayList>> parseExcelBookFromResource(String excelResourcePath) { + try (FileInputStream fileInputStream = new FileInputStream(getResourcePath(excelResourcePath))) { + Workbook workBook = new XSSFWorkbook(fileInputStream); + return parseBook(workBook); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String getResourcePath(String resourcePath) { + ClassLoader classLoader = FlowableExcelParser.class.getClassLoader(); + URL resourceUrl = classLoader.getResource(resourcePath); + if (resourceUrl == null) { + throw new RuntimeException("Resource not found: " + resourcePath); + } + return resourceUrl.getPath(); + } + + private ArrayList>> parseBook(Workbook workBook ) { + ArrayList>> book = new ArrayList<>(); + FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator(); + for (Sheet workSheet : workBook) { + book.add(parseSheet(workSheet, formulaEvaluator)); + } + return book; + } + + private ArrayList> parseSheet(Sheet workSheet, FormulaEvaluator formulaEvaluator) { + ArrayList> sheet = new ArrayList<>(); + // Count columns of first row + int maxCols = getMaxCols(workSheet.getRow(0)); + DataFormatter dataFormatter = new DataFormatter(); + int firstRow = workSheet.getFirstRowNum(); + int lastRow = workSheet.getLastRowNum(); + // For (Row workRow : workSheet) sometimes ignores empty rows, then the first empty row as an exit criterion wouldn't work + for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) { + ArrayList row = parseRow(workSheet.getRow(rowIndex), maxCols, formulaEvaluator, dataFormatter); + if (row == null) { + // Take all rows until the first empty row + break; + } else { + sheet.add(row); + } + } + return sheet; + } + + private ArrayList parseRow(Row workRow, int maxCols, FormulaEvaluator formulaEvaluator, DataFormatter dataFormatter) { + if (workRow == null) { + // Row is empty + return null; + } + ArrayList row = new ArrayList<>(); + boolean allCellsNull = true; + for (int colIndex = 0; colIndex < maxCols; colIndex++) { + Cell workCell = workRow.getCell(colIndex); + formulaEvaluator.evaluate(workCell); + String content = dataFormatter.formatCellValue(workCell, formulaEvaluator); + if (content != null && !content.isEmpty()) { + allCellsNull = false; + } + row.add(content); + } + if (allCellsNull) { + // Row is empty + return null; + } else { + return row; + } + } + + private int getMaxCols(Row workRow) { + int maxCol = workRow.getLastCellNum(); + for (int colIndex = 0; colIndex < maxCol; colIndex++) { + Cell workCell = workRow.getCell(colIndex); + if (workCell == null || workCell.getCellType() == CellType.BLANK || workCell.getCellType() == CellType._NONE) { + // Count cells until the first empty cell + return colIndex; + } + } + return maxCol; + } + + @Deprecated + public ArrayList> parseExcelFromResource(String resourcePath) { + try (FileInputStream fileInputStream = new FileInputStream(getResourcePath(resourcePath))) { + Workbook workBook = new XSSFWorkbook(fileInputStream); + FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator(); + ArrayList> sheet = parseSheet(workBook.getSheetAt(0), formulaEvaluator); + ArrayList colHeaders = new ArrayList<>(); + ArrayList> rowList = new ArrayList<>(); + for (int rowIndex = 0; rowIndex < sheet.size(); rowIndex++) { + ArrayList row = sheet.get(rowIndex); + if (rowIndex == 0) { + // Header row + for (Object cell : row) { + colHeaders.add(String.valueOf(cell)); + } + continue; + } + LinkedHashMap rowMap = new LinkedHashMap<>(); + for (int colIndex = 0; colIndex < row.size(); colIndex++) { + rowMap.put(colHeaders.get(colIndex), row.get(colIndex)); + } + rowList.add(rowMap); + } + return rowList; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableJsonParser.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableJsonParser.java new file mode 100644 index 0000000..a48b3d1 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableJsonParser.java @@ -0,0 +1,110 @@ +package com.flowable.local.work.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +@Component +public class FlowableJsonParser { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final JavaTimeModule javaTimeModule = new JavaTimeModule(); + + public static final String BOOLEAN = "Boolean"; + public static final String STRING = "String"; + public static final String INTEGER = "Integer"; + public static final String DOUBLE = "Double"; + public static final String LONG = "Long"; + public static final String INSTANT = "Instant"; + public static final String JSON_OBJECT_NODE = "JsonObjectNode"; + public static final String JSON_ARRAY_NODE = "JsonArrayNode"; + public static final String MAP = "Map"; + public static final String ARRAY_LIST = "ArrayList"; + + + public FlowableJsonParser() { + objectMapper.registerModule(javaTimeModule); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + } + + public Map parseMap(Object map) throws JsonProcessingException, ClassCastException { + return (Map) parseObject(map); + } + + public Object parseObject(Object object) throws JsonProcessingException, ClassCastException { + if (object instanceof Map) { + Map resultMap = new LinkedHashMap<>(Map.of()); + for (Map.Entry mapEntry : ((Map) object).entrySet()) { + String key = mapEntry.getKey(); + resultMap.put(key, parseMapEntry(key, mapEntry.getValue())); + } + return resultMap; + } + if (object instanceof ArrayList) { + ArrayList resultList = new ArrayList<>(); + for (Object arrayEntry : (ArrayList) object) { + resultList.add(parseArrayEntry(arrayEntry)); + } + return resultList; + } + return null; + } + + private Object parseMapEntry(String key, Object value) throws JsonProcessingException { + if (value instanceof Map) { + Map valueMap = (Map) value; + if (valueMap.containsKey("__TYPE") && valueMap.containsKey("__VALUE") && valueMap.size() == 2) { + // if Map in Map, check if inner Map has explicit type (__TYPE and __VALUE) + return convertObjectExplicit(valueMap.get("__VALUE"), (String) valueMap.get("__TYPE")); + } + if (Arrays.asList("__IN", "__OUT").contains(key)) { + // convert __IN and __OUT maps as Map + return parseObject(value); + } + // convert Map to JsonObjectNode (default in models) + return convertObjectExplicit(value, JSON_OBJECT_NODE); + } + if (value instanceof ArrayList) { + // convert ArrayList to JsonArrayNode (default in models) + return convertObjectExplicit(value, JSON_ARRAY_NODE); + } + // take json supported type (Boolean, String, Integer, Double) + return value; + } + + private Object parseArrayEntry(Object value) throws JsonProcessingException { + if (value instanceof Map) { + // convert Map to JsonObjectNode (default in models) + return convertObjectExplicit(value, JSON_OBJECT_NODE); + } + if (value instanceof ArrayList) { + // convert ArrayList to JsonArrayNode (default in models) + return convertObjectExplicit(value, JSON_ARRAY_NODE); + } + // take json supported type (Boolean, String, Integer, Double) + return value; + } + + private Object convertObjectExplicit(Object value, String explicitType) throws JsonProcessingException { + switch (explicitType) { + case BOOLEAN: return Boolean.parseBoolean(value.toString()); + case STRING: return value.toString(); + case INTEGER: return Integer.parseInt(value.toString()); + case DOUBLE: return Double.parseDouble(value.toString()); + case LONG: return Long.parseLong(value.toString()); + case INSTANT: return Instant.parse(value.toString()); + case MAP: case ARRAY_LIST: return parseObject(value); + case JSON_OBJECT_NODE: case JSON_ARRAY_NODE: return objectMapper.convertValue(parseObject(value), JsonNode.class); + default: return value; + } + } +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableModelTest.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableModelTest.java new file mode 100644 index 0000000..69e2895 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableModelTest.java @@ -0,0 +1,54 @@ +package com.flowable.local.work.model; + +import com.flowable.action.engine.test.ActionExtension; +import com.flowable.app.engine.test.FlowableAppExtension; +import com.flowable.dataobject.engine.test.DataObjectExtension; +import com.flowable.form.spring.impl.test.FlowableFormSpringExtension; +import com.flowable.idm.engine.test.PlatformIdmExtension; +import com.flowable.platform.tenant.test.TenantSetupExtension; +import com.flowable.policy.engine.test.PolicyExtension; +import com.flowable.serviceregistry.engine.test.ServiceRegistryExtension; +import com.flowable.template.engine.test.TemplateExtension; +import org.flowable.cmmn.spring.impl.test.FlowableCmmnSpringExtension; +import org.flowable.dmn.spring.impl.test.FlowableDmnSpringExtension; +import org.flowable.spring.impl.test.FlowableSpringExtension; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.annotation.AliasFor; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited + +@ExtendWith(SpringExtension.class) +@ExtendWith(ActionExtension.class) +@ExtendWith(DataObjectExtension.class) +//@ExtendWith(EngageExtension.class) +@ExtendWith(FlowableAppExtension.class) +@ExtendWith(FlowableSpringExtension.class) +@ExtendWith(FlowableCmmnSpringExtension.class) +@ExtendWith(FlowableFormSpringExtension.class) +@ExtendWith(FlowableDmnSpringExtension.class) +@ExtendWith(PlatformIdmExtension.class) +@ExtendWith(PolicyExtension.class) +@ExtendWith(ServiceRegistryExtension.class) +@ExtendWith(TemplateExtension.class) +@ExtendWith(TenantSetupExtension.class) +@ExtendWith(TestMailServerExtension.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional +@SpringBootTest +public @interface FlowableModelTest { + /* + @AliasFor(annotation = SpringBootTest.class, attribute = "webEnvironment") + SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.MOCK; + + */ +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableModelTestUtils.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableModelTestUtils.java new file mode 100644 index 0000000..6436065 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/FlowableModelTestUtils.java @@ -0,0 +1,655 @@ +package com.flowable.local.work.model; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.*; +import com.flowable.audit.api.AuditService; +import com.flowable.audit.api.runtime.AuditInstance; +import com.flowable.core.spring.security.SecurityUtils; +import com.flowable.local.work.service.JsonUtils; +import com.flowable.platform.service.task.CompleteFormRepresentation; +import com.flowable.platform.service.task.PlatformTaskService; +import jakarta.mail.Address; +import org.apache.commons.lang3.tuple.Pair; +import org.assertj.core.api.Assertions; +import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; +import org.flowable.cmmn.api.runtime.CaseInstance; +import org.flowable.cmmn.engine.CmmnEngine; +import org.flowable.common.engine.api.identity.AuthenticationContext; +import org.flowable.common.engine.impl.identity.Authentication; +import org.flowable.engine.ManagementService; +import org.flowable.engine.ProcessEngine; +import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; +import org.flowable.spring.security.SpringSecurityAuthenticationContext; +import org.flowable.task.api.Task; +import org.flowable.variable.api.history.HistoricVariableInstance; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; +import org.springframework.web.client.RestTemplate; + +import java.io.File; +import java.io.FileOutputStream; +import java.net.URI; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@Component +public class FlowableModelTestUtils { + + protected final ProcessEngine processEngine; + protected final CmmnEngine cmmnEngine; + protected final TaskService taskService; + protected final PlatformTaskService platformTaskService; + protected final ManagementService managementService; + protected final JsonUtils jsonUtils; + protected final FlowableExcelMapper flowableExcelMapper; + protected static final Logger logger = LoggerFactory.getLogger(FlowableModelTestUtils.class); + protected final TestMailServer testMailServer; + protected final AuditService auditService; + + + public static String TENANT_ID = null; + public static String ROOT_PROCESS_ID = "ROOT_PROCESS_ID"; + public static String TEST_PROCESS_ID = "TEST_PROCESS_ID"; + + + public FlowableModelTestUtils(ProcessEngine processEngine, + CmmnEngine cmmnEngine, + TaskService taskService, + PlatformTaskService platformTaskService, + ManagementService managementService, + JsonUtils jsonUtils, + FlowableExcelMapper flowableExcelMapper, + TestMailServer testMailServer, + AuditService auditService) { + this.processEngine = processEngine; + this.cmmnEngine = cmmnEngine; + this.taskService = taskService; + this.platformTaskService = platformTaskService; + this.managementService = managementService; + this.jsonUtils = jsonUtils; + this.flowableExcelMapper = flowableExcelMapper; + this.testMailServer = testMailServer; + this.auditService = auditService; + } + + public void checkAndAndAssertAuditRecord(Map map, int auditNumber, String hint, String message, String category, String type) { + if (map != null) { + Object check = map.get("audit"); + if (check instanceof Integer && (Integer) check > 0) { + assertAuditRecord(auditNumber, hint, message, category, type); + } + } + } + + protected void assertAuditRecord(int auditNumber, String hint, String message, String category, String type) { + List auditTrail = getAuditTrail(); + int auditTrailSize = auditTrail.size(); + Assertions.assertThat(auditTrailSize).as(hint + ": invalid auditNumber " + auditTrailSize).isGreaterThanOrEqualTo(auditNumber); + if (auditTrailSize == 0) return; + + AuditInstance auditInstance = auditTrail.get(auditNumber - 1); + Assertions.assertThat(auditInstance.getPayload().get("message")).as(hint).isEqualTo(message); + Assertions.assertThat(auditInstance.getPayload().get("category")).as(hint).isEqualTo(category); + Assertions.assertThat(auditInstance.getType()).as(hint).isEqualTo(type); + } + + public void checkAndAssertEmail(Map map, int emailNumber, String hint, String subject, String receivers) { + if (map != null) { + Object check = map.get("email"); + if (check instanceof Integer && (Integer) check > 0) { + assertEmail(emailNumber, hint, subject, receivers); + } + } + } + + protected void assertEmail(int emailNumber, String hint, String subject, String receivers) { + List emails = getMailList(); + int emailListSize = emails.size(); + Assertions.assertThat(emailListSize).as(hint + ": invalid email number " + emailListSize).isGreaterThanOrEqualTo(emailNumber); + if (emailNumber == 0) return; + + EmailDto email = emails.get(emailNumber - 1); + String emailSubject = email.getSubject(); + Assertions.assertThat(emailSubject).as(hint + ": invalid subject " + emailSubject).endsWith(subject); + + List> receiverCheckList = new ArrayList<>(); + for (String receiver : receivers.split("[,\\s]+")) { + receiverCheckList.add(Pair.of(receiver, false)); + } + List emailReceiverList = email.getReceiverList(); + Assertions.assertThat(emailReceiverList.size()).as(hint + ": invalid number of receivers " + emailReceiverList).isEqualTo(receiverCheckList.size()); + + for (String emailReceiver : emailReceiverList) { + for (int i = 0; i < receiverCheckList.size(); i++) { + Pair checkReceiver = receiverCheckList.get(i); + if (checkReceiver.getLeft().equals(emailReceiver) && !checkReceiver.getRight()) { + receiverCheckList.set(i, Pair.of(emailReceiver, true)); + break; + } else if (i == receiverCheckList.size() - 1) { + Assertions.fail(hint + ": invalid email receiver " + emailReceiverList); + } + } + } + } + + public Map getHistoryCasePayload(String caseInstanceId) { + List historicVariableInstanceList = cmmnEngine.getCmmnHistoryService() + .createHistoricVariableInstanceQuery() + .caseInstanceId(caseInstanceId) + .list(); + return convertHistVariableListToMap(historicVariableInstanceList); + } + + public Map getRuntimeCasePayload(String caseId) { + return cmmnEngine.getCmmnRuntimeService().getVariables(caseId); + } + + public CaseInstance startCaseInstance(String key, Map variables) { + return cmmnEngine.getCmmnRuntimeService() + .createCaseInstanceBuilder() + .caseDefinitionKey(key) + .tenantId(TENANT_ID) // Must not be "default" + .variables(variables) + .start(); + } + + public ProcessInstance startProcessInstance(String key, Map variables) { + return processEngine.getRuntimeService() + .createProcessInstanceBuilder() + .processDefinitionKey(key) + .tenantId(TENANT_ID) // Must not be "default" + .variables(variables) + .start(); + } + + public Task getOpenTask(String taskKey) { + Task task = taskService.createTaskQuery().taskDefinitionKey(taskKey).singleResult(); + Assertions.assertThat(task).as("No open task with key {} found", taskKey).isNotNull(); + return task; + } + + public Task getOpenTaskByName(String taskName) { + Task task = taskService.createTaskQuery().taskName(taskName).singleResult(); + Assertions.assertThat(task).as("No open task with name {} found", taskName).isNotNull(); + return task; + } + + public void claimOpenTask(String taskKey, String userId) { + setTestAuthenticatedUser(userId, TENANT_ID); + taskService.claim(getOpenTask(taskKey).getId(), userId); + } + + public List getAuditTrail() { + return auditService.createAuditInstanceQuery() + //.tenantId(TENANT_ID) TODO: Must not be null, should work with default tenant + .list(); + } + + public List getMailList() { + return Arrays.stream(testMailServer.getMessages()).map(mimeMessage -> { + try { + return new EmailDto(mimeMessage.getSubject(), Arrays.stream(mimeMessage.getAllRecipients()) + .map(Address::toString).collect(Collectors.toList()), mimeMessage.getContent().toString(), mimeMessage.getContent()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toList()); + } + + public boolean isSubset(JsonNode root, JsonNode test) { + // If test is null, it is always a subset of root + if (test == null || test.isNull()) { + return true; + } + // If test is a value node, compare values + if (test.isValueNode()) { + if (test.isTextual() && "__N_A".equals(test.asText())) { + return false; + } + if (root == null) { + return false; + } + return root.isValueNode() && root.asText().equals(test.asText()); + } + // If test is an array node, check if all elements of test exist in root + if(test.isArray()){ + for (int i = 0; i < test.size(); i++) { + JsonNode testElement = test.get(i); + JsonNode rootElement = null; + if (root != null) { + rootElement = root.get(i); + } + if (testElement.isTextual() && "__N_A".equals(testElement.asText())) { + if (rootElement != null && !rootElement.isNull()) { + return false; + } + continue; + } + if (!isSubset(rootElement, testElement)) { + return false; + } + } + return true; + } + // If test is an object node, check if all fields in test exist in root + if (test.isObject()){ + for (Iterator iterator = test.fieldNames(); iterator.hasNext(); ) { + String fieldName = iterator.next(); + JsonNode testValue = test.get(fieldName); + JsonNode rootValue = null; + if (root != null) { + rootValue = root.get(fieldName); + } + if (testValue.isTextual() && "__N_A".equals(testValue.asText())) { + if (root != null && root.has(fieldName)) { + return false; + } + continue; + } + if (!isSubset(rootValue, testValue)){ + return false; + } + } + return true; + } + // If none of the above matches, return false + return false; + } + + public Stream getJsonArgumentsFromExcel(String path) { + ArrayList argumentList = new ArrayList<>(); + JsonNode book = flowableExcelMapper.excelBookResourceToJsonNode(path); + for (JsonNode sheet : book) { + for (JsonNode row : sheet) { + argumentList.add(Arguments.of(path, row)); + } + } + return argumentList.stream(); + } + + public Stream getObjArgumentsFromExcel(String path) { + ArrayList argumentList = new ArrayList<>(); + ArrayList> book = flowableExcelMapper.excelBookResourceToObj(path); + for (ArrayList sheet : book) { + for (Object row : sheet) { + argumentList.add(Arguments.of(path, row)); + } + } + return argumentList.stream(); + } + + public ObjectNode testJsonExcelRow(String path, JsonNode row) { + ObjectNode vars = jsonUtils.getEmptyObjectNode(); + JsonNode rootParam = row.get("root"); + if (rootParam != null) { + Iterator> rootVars = rootParam.fields(); + while (rootVars.hasNext()) { + Map.Entry rootVar = rootVars.next(); + String key = rootVar.getKey(); + JsonNode value = rootVar.getValue(); + vars.set(key, value); + } + } + JsonNode inParam = row.get("in"); + if (inParam != null) { + vars.set("__IN", inParam); + } + JsonNode outParam = row.get("out"); + if (outParam != null) { + vars.set("__OUT", outParam); + } + JsonNode idParam = row.get("id"); + JsonNode test = row.get("test"); + int timerCount = 0; + JsonNode timerNode = row.get("timer"); + if (timerNode != null) timerCount = timerNode.asInt(); + Integer auditRecordCount = null; + JsonNode auditNode = row.get("audit"); + if (auditNode != null) auditRecordCount = auditNode.asInt(); + Integer emailCount = null; + JsonNode emailNode = row.get("email"); + if (emailNode != null) emailCount = emailNode.asInt(); + + logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ", + path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount); + ObjectNode processIds = createRootTestProcessInstance(idParam.asText(), jsonUtils.convertJsonNodeToMap(vars)); + for (int i = 0; i < timerCount; i++) { + executeTimer(processIds.get(TEST_PROCESS_ID).asText()); + } + + ObjectNode result = jsonUtils.getEmptyObjectNode(); + Map rootProcessPayload = getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText()); + JsonNode root = jsonUtils.convertMapToJsonNode(rootProcessPayload); + if (root != null) { + Assertions.assertThat(isSubset(root, test)) + .withFailMessage(System.lineSeparator() + "test :" + test + System.lineSeparator() + "root :" + root).isTrue(); + result.set("root", root); + } + if (auditRecordCount != null) { +// List auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText()); + List auditTrail = getAuditTrail(); + int auditTrailSize = auditTrail.size(); + Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount); + result.set("auditTrail", jsonUtils.convertListToJsonNode(Collections.singletonList(auditTrail))); + } + if (emailCount != null) { + List emailList = getMailList(); + int emailListSize = emailList.size(); + Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount); + result.set("emails", jsonUtils.convertListToJsonNode(Collections.singletonList(emailList))); + } + return result; + } + + public Map testObjExcelRow(String path, Map row) { + Map vars = new LinkedHashMap<>(); + Map rootParam = (Map) row.get("root"); + if (rootParam != null) { + for (Map.Entry entry : rootParam.entrySet()) { + vars.put(entry.getKey(), entry.getValue()); + } + } + Map inParam = (Map) row.get("in"); + if (inParam != null) { + vars.put("__IN", inParam); + } + Map outParam = (Map) row.get("out"); + if (outParam != null) { + vars.put("__OUT", outParam); + } + String idParam = (String) row.get("id"); + Map test =(Map) row.get("test"); + int timerCount = 0; + Object timerNode = row.get("timer"); + if (timerNode != null) timerCount = (Integer) timerNode; + Integer auditRecordCount = null; + Object auditNode = row.get("audit"); + if (auditNode != null) auditRecordCount = (Integer) auditNode; + Integer emailCount = null; + Object emailNode = row.get("email"); + if (emailNode != null) emailCount = (Integer) emailNode; + + logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ", + path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount); + ObjectNode processIds = createRootTestProcessInstance(idParam, vars); + for (int i = 0; i < timerCount; i++) { + executeTimer(processIds.get(TEST_PROCESS_ID).asText()); + } + + Map result = new LinkedHashMap<>(); + Map rootProcessPayload = getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText()); + JsonNode root = jsonUtils.convertMapToJsonNode(rootProcessPayload); + if (root != null) { + Assertions.assertThat(isSubset(root, jsonUtils.convertMapToJsonNode(test))) + .withFailMessage(System.lineSeparator() + "test :" + test + System.lineSeparator() + "root :" + root).isTrue(); + result.put("root", rootProcessPayload); + } + if (auditRecordCount != null) { +// List auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText()); + List auditTrail = getAuditTrail(); + int auditTrailSize = auditTrail.size(); + Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount); + result.put("audit", auditRecordCount); + } + if (emailCount != null) { + List emailList = getMailList(); + int emailListSize = emailList.size(); + Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount); + result.put("email", emailCount); + } + return result; + } + + public void exportApp(String rootUrl, String appModelKey, String username, String password) { + RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + + RestTemplate restTemplate = restTemplateBuilder + .basicAuthentication(username, password) + .rootUri(rootUrl) + .build(); + + ResponseEntity responseEntity = restTemplate.postForEntity(URI.create(rootUrl + "/app/authentication?j_username=" + username + "&j_password=" + password + "&spring_security_remember_me=true&submit=Login"), null, JsonNode.class); + + String designCookieRaw = responseEntity.getHeaders().get("Set-Cookie").stream().collect(Collectors.joining(";")); + String flowableDesignRememberMeTokenValue = Arrays.stream(designCookieRaw.split(";")).filter(part -> part.contains("FLOWABLE_DESIGN_REMEMBER_ME")).findAny().get(); + String csrfToken = Arrays.stream(designCookieRaw.split(";")).filter(part -> part.contains("FLOWABLE_DESIGN_CSRF_TOKEN")).findAny().get(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("Cookie", flowableDesignRememberMeTokenValue + ";" + csrfToken); + + ArrayNode apps = (ArrayNode) restTemplate.exchange(URI.create(rootUrl + "/app/models?filter=apps&modelType=3&sort=modifiedDesc"), HttpMethod.GET, new HttpEntity<>(httpHeaders), JsonNode.class).getBody().path("data"); + String appModelId = StreamSupport.stream(apps.spliterator(), false).filter(app -> app.path("key").asText().equals(appModelKey)).map( app -> app.path("id").asText()).findAny().get(); + + File file = new File(Paths.get("src/test/resources/test-auto-deploy-apps", appModelKey + ".zip").toString()); + restTemplate.execute( + URI.create(rootUrl + "/app/app-definitions/" + appModelId + "/export-bar?includeChildReferences=true"), + HttpMethod.GET, + clientHttpRequest -> clientHttpRequest.getHeaders().set( + "Cookie", flowableDesignRememberMeTokenValue + ";" + csrfToken), + clientHttpResponse -> { + StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(file, false)); + return file; + }); + } + + public ObjectNode emptyNode() { + return jsonUtils.getEmptyObjectNode(); + } + + public Map emptyMap() { + return new LinkedHashMap<>(); + } + + public ObjectNode loadObjectNodeFromResources(String path) { + if (path.isEmpty()) return jsonUtils.getEmptyObjectNode(); + else return jsonUtils.loadObjectNodeFromFile(Paths.get("src","test", "resources", path).toString()); + } + + public Map loadMapFromResources(String path) { + return jsonUtils.convertJsonNodeToMap(loadObjectNodeFromResources(path)); + } + + public void executeTimer(String processId) { + TimerJobEntity timerJobEntity = (TimerJobEntity) managementService.createTimerJobQuery().processInstanceId(processId).singleResult(); + Assertions.assertThat(timerJobEntity).withFailMessage("No active timer").isNotNull(); + managementService.moveTimerToExecutableJob(timerJobEntity.getId()); + managementService.executeJob(timerJobEntity.getId()); + } + public void setTestAuthenticatedUser(String userId, String tenantId) { + if (userId == null) { + this.resetTestAuthenticatedUser(); + } else { + List grantedAuthorities = new ArrayList<>(); + if (StringUtils.isNotBlank(tenantId)) { + grantedAuthorities.add(SecurityUtils.createTenantAuthority(tenantId)); + } + SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(userId, "", grantedAuthorities)); + AuthenticationContext authenticationContext = Authentication.getAuthenticationContext(); + if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) { + Authentication.setAuthenticatedUserId(userId); + } + } + } + + public void setTestAuthenticatedUser(String userId, String tenantId, String... groupKeys) { + if (userId == null) { + this.resetTestAuthenticatedUser(); + } else { + List grantedAuthorities = new ArrayList<>(); + if (StringUtils.isNotBlank(tenantId)) { + grantedAuthorities.add(SecurityUtils.createTenantAuthority(tenantId)); + } + + for (String groupKey : groupKeys) { + grantedAuthorities.add(SecurityUtils.createGroupAuthority(groupKey)); + } + + SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(userId, "", grantedAuthorities)); + AuthenticationContext authenticationContext = Authentication.getAuthenticationContext(); + if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) { + Authentication.setAuthenticatedUserId(userId); + } + } + } + + public void resetTestAuthenticatedUser() { + SecurityContextHolder.getContext().setAuthentication(null); + AuthenticationContext authenticationContext = Authentication.getAuthenticationContext(); + if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) { + Authentication.setAuthenticatedUserId(null); + } + } + + public void completeTaskWithFlatVars(String taskId, Map variables, String outcome) { + Map taskVariables = platformTaskService.getTaskVariables(taskId); + Map taskVariablesFlat = jsonUtils.flatten(taskVariables); + taskVariablesFlat.putAll(variables); + Map completionVars = jsonUtils.unflatten(taskVariablesFlat); + + completeTask(taskId, completionVars, outcome); + } + + public void completeTask(String taskId, Map completionVariables, String outcome) { + CompleteFormRepresentation form = new CompleteFormRepresentation(); + for (String key : completionVariables.keySet()) { + form.setValues(key, completionVariables.get(key)); + } + form.setOutcome(outcome); + platformTaskService.completeTaskForm(taskId, form); + } + + public void completeOpenTask(String taskKey, ObjectNode vars, String outcome) { + Task task = getOpenTask(taskKey); + Map flatVars = jsonUtils.convertJsonNodeToMap(vars); + completeTaskWithFlatVars(task.getId(), flatVars, outcome); + } + + public Map getHistProcessPayload(String processInstanceId) { + List historicVariableInstanceList = processEngine.getHistoryService() + .createHistoricVariableInstanceQuery() + .processInstanceId(processInstanceId) + .list(); + return convertHistVariableListToMap(historicVariableInstanceList); + } + + private HashMap convertHistVariableListToMap(List historicVariableInstanceList) { + HashMap variableMap = new HashMap<>(); + for (HistoricVariableInstance historicVariableInstance : historicVariableInstanceList) { + variableMap.put(historicVariableInstance.getVariableName(), historicVariableInstance.getValue()); + } + return variableMap; + } + + public ObjectNode createRootTestProcessInstance(String testKey, Map variables) { + // Create root process with test process as call activity + String wrapperProcessKey = testKey + "_T"; + BpmnModel bpmnModel = createWrapperTestProcessModel(testKey, (ObjectNode) jsonUtils.convertMapToJsonNode(variables)); + processEngine.getProcessEngineConfiguration() + .getRepositoryService() + .createDeployment() + .addBpmnModel(wrapperProcessKey + ".bpmn20.xml", bpmnModel) + .tenantId(TENANT_ID) // Must not be "default" + .deploy(); + + // Start root process + ProcessInstance rootProcessInstance = startProcessInstance(wrapperProcessKey, variables); + + // Store the process IDs for using them in assertions + ProcessInstance runtimeTestProcessInstance = processEngine.getRuntimeService() + .createProcessInstanceQuery() + .superProcessInstanceId(rootProcessInstance.getId()) + .singleResult(); + String testProcessId; + if (runtimeTestProcessInstance != null) { + testProcessId = runtimeTestProcessInstance.getId(); + } else { + HistoricProcessInstance historicTestProcessInstance = processEngine.getHistoryService() + .createHistoricProcessInstanceQuery() + .superProcessInstanceId(rootProcessInstance.getId()) + .singleResult(); + // if history level is none, testProcessId is not available + testProcessId = historicTestProcessInstance != null ? historicTestProcessInstance.getId() : null; + } + ObjectNode rootNode = jsonUtils.getEmptyObjectNode(); + rootNode.put(ROOT_PROCESS_ID, rootProcessInstance.getProcessInstanceId()); + rootNode.put(TEST_PROCESS_ID, testProcessId); + return rootNode; + } + + protected BpmnModel createWrapperTestProcessModel(String testKey, ObjectNode variables) { + BpmnModel bpmnModel = new BpmnModel(); + Process process = new Process(); + process.setId(testKey + "_T"); + process.setName(testKey + "_T"); + + StartEvent startEvent = new StartEvent(); + startEvent.setId(testKey + "_Start"); + startEvent.setName(testKey + "_Start"); + process.addFlowElement(startEvent); + + CallActivity callActivity = new CallActivity(); + callActivity.setId(testKey); + callActivity.setCalledElement(testKey); + JsonNode inNode = variables.get("__IN"); + if (inNode instanceof ObjectNode) { + Map inMap = jsonUtils.convertObjectNodeToMap((ObjectNode) inNode); + ArrayList inParameters = new ArrayList<>(); + for (Map.Entry entry : inMap.entrySet()) { + IOParameter ioParameter = new IOParameter(); + ioParameter.setSourceExpression("${__IN." + entry.getKey() + "}"); + ioParameter.setTarget(entry.getKey()); + inParameters.add(ioParameter); + } + callActivity.setInParameters(inParameters); + } + JsonNode outNode = variables.get("__OUT"); + if (outNode instanceof ObjectNode) { + Map outMap = (Map) jsonUtils.convertJsonNodeToMap(variables).get("__OUT"); + ArrayList outParameters = new ArrayList<>(); + for (Map.Entry entry : outMap.entrySet()) { + IOParameter ioParameter = new IOParameter(); + ioParameter.setSource(entry.getKey()); + ioParameter.setTarget(entry.getValue()); + outParameters.add(ioParameter); + } + callActivity.setOutParameters(outParameters); + } + process.addFlowElement(callActivity); + + EndEvent endEvent = new EndEvent(); + endEvent.setId(testKey + "_End"); + endEvent.setName(testKey + "_End"); + process.addFlowElement(endEvent); + + SequenceFlow start = new SequenceFlow(testKey + "_Start", testKey); + start.setId("Start_Sequence_Flow"); + process.addFlowElement(start); + + SequenceFlow end = new SequenceFlow(testKey, testKey + "_End"); + end.setId("End_Sequence_Flow"); + process.addFlowElement(end); + + bpmnModel.addProcess(process); + return bpmnModel; + } + +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/TestMailServer.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/TestMailServer.java new file mode 100644 index 0000000..594aa51 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/TestMailServer.java @@ -0,0 +1,33 @@ +package com.flowable.local.work.model; + +import com.flowable.spring.boot.properties.FlowableMailProperties; +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.ServerSetup; + +import jakarta.mail.internet.MimeMessage; +import org.springframework.stereotype.Component; + +import static com.icegreen.greenmail.util.ServerSetup.PROTOCOL_SMTP; + +@Component +public class TestMailServer { + private GreenMail greenMail; + + private final FlowableMailProperties flowableMailProperties; + public TestMailServer(FlowableMailProperties flowableMailProperties) { + this.flowableMailProperties = flowableMailProperties; + } + + public void setup() { + greenMail = new GreenMail(new ServerSetup(flowableMailProperties.getPort(), flowableMailProperties.getHost(), PROTOCOL_SMTP)); + greenMail.start(); + } + + public void stop() { + greenMail.stop(); + } + + public MimeMessage[] getMessages() { + return greenMail.getReceivedMessages(); + } +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/TestMailServerExtension.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/TestMailServerExtension.java new file mode 100644 index 0000000..1e96814 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/TestMailServerExtension.java @@ -0,0 +1,22 @@ +package com.flowable.local.work.model; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +public class TestMailServerExtension implements BeforeEachCallback, AfterEachCallback { + @Override + public void beforeEach(ExtensionContext context) { + getTestMailServer(context).setup(); + } + + @Override + public void afterEach(ExtensionContext context) { + getTestMailServer(context).stop(); + } + + protected TestMailServer getTestMailServer(ExtensionContext context) { + return SpringExtension.getApplicationContext(context).getBean(TestMailServer.class); + } +} diff --git a/flowable-local-work/src/test/java/com/flowable/local/work/model/test/ModelTest.java b/flowable-local-work/src/test/java/com/flowable/local/work/model/test/ModelTest.java new file mode 100644 index 0000000..52ca532 --- /dev/null +++ b/flowable-local-work/src/test/java/com/flowable/local/work/model/test/ModelTest.java @@ -0,0 +1,122 @@ +package com.flowable.local.work.model.test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.*; + +import com.flowable.local.work.model.EmailDto; +import com.flowable.local.work.model.FlowableModelTest; +import com.flowable.local.work.model.FlowableModelTestUtils; +import org.assertj.core.api.Assertions; +import org.flowable.cmmn.api.runtime.CaseInstance; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.validation.constraints.NotNull; +import java.util.*; +import java.util.stream.Stream; + +@FlowableModelTest +public class ModelTest { + + @Autowired + protected FlowableModelTestUtils flowableModelTest; + + @Test + @Disabled + public void exportApp() { + flowableModelTest.exportApp("http://localhost:8093", "TST_APP", "admin", "test"); + } + + @Test + public void c001Test() { + flowableModelTest.setTestAuthenticatedUser("admin", null); + CaseInstance caseInstance = flowableModelTest.startCaseInstance("TST_C001", Map.of()); + String caseInstanceId = caseInstance.getId(); + flowableModelTest.completeOpenTask("TST_P001_T001", flowableModelTest.loadObjectNodeFromResources("model/test/C001/T001.json"), "COMPLETE"); + Assertions.assertThat(flowableModelTest.getRuntimeCasePayload(caseInstanceId).get("testText")).isEqualTo("my test text"); + + flowableModelTest.completeOpenTask("TST_C001_T002", flowableModelTest.emptyNode(), "COMPLETE"); + Assertions.assertThat(flowableModelTest.getHistoryCasePayload(caseInstanceId).get("testText")).isEqualTo("my test text"); + + } + + @Test + public void p005HappyPathTest() { + ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P005", flowableModelTest.emptyMap()); + flowableModelTest.claimOpenTask("TST_P005_T001", "admin"); + flowableModelTest.completeOpenTask("TST_P005_T001", flowableModelTest.loadObjectNodeFromResources("model/test/P005/T001.json"), null); + Map rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText()); + Assertions.assertThat(rootProcessPayload.get("dataEntry")).isEqualTo("my root text"); + } + + @NotNull + private Stream p001TestData() { + return Stream.of( + Arguments.of("admin", "model/test/P001/initiator.json") + ); + } + @ParameterizedTest + @MethodSource("p001TestData") + public void p001Test(String initiator, String path) { + flowableModelTest.setTestAuthenticatedUser(initiator, null); + ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P001", + flowableModelTest.loadMapFromResources(path)); + flowableModelTest.completeOpenTask("TST_P001_T001", flowableModelTest.emptyNode(), "complete"); + Map rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText()); + + Assertions.assertThat(rootProcessPayload.get("rootResult")).isEqualTo(null); + } + + @NotNull + private Stream p002TestData() { + return Stream.of( + Arguments.of("model/test/P002/boolean1.json", true, false), + Arguments.of("model/test/P002/boolean2.json", false, true), + Arguments.of("model/test/P002/string1.json", "hello root", "hello"), + Arguments.of("model/test/P002/string2.json", "123", "456"), + Arguments.of("model/test/P002/string3.json", "123.456,", "456.789"), + Arguments.of("model/test/P002/int.json", 123, 456), + Arguments.of("model/test/P002/double.json", 123.456, 456.789), + Arguments.of("model/test/P002/date.json", "2025-11-12", "2025-11-11") + ); + } + @ParameterizedTest + @MethodSource("p002TestData") + public void p002Test(String path, Object result, Object out) { + ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P002", + flowableModelTest.loadMapFromResources(path)); + Map rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText()); + + List mail = flowableModelTest.getMailList(); + Assertions.assertThat(mail.size()).isEqualTo(0); + + Assertions.assertThat(rootProcessPayload.get("result")).isEqualTo(result); + Assertions.assertThat(rootProcessPayload.get("out")).isEqualTo(out); + } + + private Stream p002ExcelTestData() { + return flowableModelTest.getJsonArgumentsFromExcel("model/test/P002/p002Test.xlsx"); + } + @ParameterizedTest + @MethodSource("p002ExcelTestData") + public void p002ExcelTest(String path, JsonNode argument) { + ObjectNode result = flowableModelTest.testJsonExcelRow(path, argument); + } + + private Stream p002ExcelTestData2() { + return flowableModelTest.getObjArgumentsFromExcel("model/test/P002/p002Test.xlsx"); + } + @ParameterizedTest + @MethodSource("p002ExcelTestData2") + public void p002ExcelTest2(String path, Map argument) { + Map result = flowableModelTest.testObjExcelRow(path, argument); + flowableModelTest.checkAndAssertEmail(result, 1, + "Email", "Test", "test@flowable.com"); + flowableModelTest.checkAndAndAssertAuditRecord(result, 1, + "Audit record", "TST_P002 Audit trail entry", "system", null); + } +} diff --git a/flowable-local-work/src/test/resources/application.properties b/flowable-local-work/src/test/resources/application.properties new file mode 100644 index 0000000..9fd4faf --- /dev/null +++ b/flowable-local-work/src/test/resources/application.properties @@ -0,0 +1,24 @@ +spring.datasource.url=jdbc:h2:mem:flowable-work-db;DB_CLOSE_DELAY=1000 +spring.datasource.username=admin +spring.datasource.password=test + +# To make our life easier in tests we are disabling the async executor and elasticsearch +flowable.async-executor-activate=false +flowable.async-history-executor-activate=false +flowable.indexing.enabled=false +management.health.elasticsearch.enabled=false +management.metrics.export.elastic.enabled=false + +# Set debug level in tests +logging.level.com.flowable=INFO +logging.level.com.flowable.local.work.model.FlowableModelTestUtils=INFO + +# Disable the timeout process in the tests +flowable.external-system.wechat.timeout.process-definition-key= + +# Path to test-auto-deploy-apps +flowable.app.resource-location=classpath*:/test-auto-deploy-apps/ + +# Email +flowable.mail.server.host=localhost +flowable.mail.server.port=3025 \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/C001/T001.json b/flowable-local-work/src/test/resources/model/test/C001/T001.json new file mode 100644 index 0000000..a48859a --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/C001/T001.json @@ -0,0 +1,5 @@ +{ + "root": { + "testText": "my test text" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P001/initiator.json b/flowable-local-work/src/test/resources/model/test/P001/initiator.json new file mode 100644 index 0000000..fbc534b --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P001/initiator.json @@ -0,0 +1,5 @@ +{ + "__IN": { + "initiator": "admin" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/boolean1.json b/flowable-local-work/src/test/resources/model/test/P002/boolean1.json new file mode 100644 index 0000000..6f84512 --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/boolean1.json @@ -0,0 +1,9 @@ +{ + "param": true, + "__IN": { + "param": false + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/boolean2.json b/flowable-local-work/src/test/resources/model/test/P002/boolean2.json new file mode 100644 index 0000000..3ebab25 --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/boolean2.json @@ -0,0 +1,9 @@ +{ + "param": false, + "__IN": { + "param": true + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/date.json b/flowable-local-work/src/test/resources/model/test/P002/date.json new file mode 100644 index 0000000..2829b47 --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/date.json @@ -0,0 +1,9 @@ +{ + "param": "2025-11-12", + "__IN": { + "param": "2025-11-11" + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/double.json b/flowable-local-work/src/test/resources/model/test/P002/double.json new file mode 100644 index 0000000..38cd1a2 --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/double.json @@ -0,0 +1,9 @@ +{ + "param": 123.456, + "__IN": { + "param": 456.789 + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/int.json b/flowable-local-work/src/test/resources/model/test/P002/int.json new file mode 100644 index 0000000..cd503ec --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/int.json @@ -0,0 +1,9 @@ +{ + "param": 123, + "__IN": { + "param": 456 + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/p002Test.xlsx b/flowable-local-work/src/test/resources/model/test/P002/p002Test.xlsx new file mode 100644 index 0000000..b80f353 Binary files /dev/null and b/flowable-local-work/src/test/resources/model/test/P002/p002Test.xlsx differ diff --git a/flowable-local-work/src/test/resources/model/test/P002/string1.json b/flowable-local-work/src/test/resources/model/test/P002/string1.json new file mode 100644 index 0000000..862081d --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/string1.json @@ -0,0 +1,9 @@ +{ + "param": "hello root", + "__IN": { + "param": "hello" + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/string2.json b/flowable-local-work/src/test/resources/model/test/P002/string2.json new file mode 100644 index 0000000..d0b0429 --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/string2.json @@ -0,0 +1,9 @@ +{ + "param": "123", + "__IN": { + "param": "456" + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P002/string3.json b/flowable-local-work/src/test/resources/model/test/P002/string3.json new file mode 100644 index 0000000..91626b3 --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P002/string3.json @@ -0,0 +1,9 @@ +{ + "param": "123.456,", + "__IN": { + "param": "456.789" + }, + "__OUT": { + "result": "out" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/model/test/P005/T001.json b/flowable-local-work/src/test/resources/model/test/P005/T001.json new file mode 100644 index 0000000..e5451ad --- /dev/null +++ b/flowable-local-work/src/test/resources/model/test/P005/T001.json @@ -0,0 +1,5 @@ +{ + "root": { + "dataEntry": "my root text" + } +} \ No newline at end of file diff --git a/flowable-local-work/src/test/resources/test-auto-deploy-apps/TST_APP.zip b/flowable-local-work/src/test/resources/test-auto-deploy-apps/TST_APP.zip new file mode 100644 index 0000000..62086df Binary files /dev/null and b/flowable-local-work/src/test/resources/test-auto-deploy-apps/TST_APP.zip differ diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..47b0fd1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.flowable.local + 14 + 0.0.1-SNAPSHOT + pom + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + + flowable-local-work + flowable-local-design + flowable-local-design-angular + flowable-local-control + + + + UTF-8 + UTF-8 + 11 + 3.14.10 + 42.7.8 + 2.2.224 + false + + + + + + + com.flowable + flowable-platform-bom + ${com.flowable.platform.version} + pom + import + + + + + \ No newline at end of file diff --git a/settings-flowable.xml b/settings-flowable.xml new file mode 100644 index 0000000..d672d85 --- /dev/null +++ b/settings-flowable.xml @@ -0,0 +1,29 @@ + + + + + flowable-repo + USER + PASSWORD + + + + + flowable-artifacts + + + flowable-repo + https://artifacts.flowable.com/artifactory/flowable-maven-all + + false + + + + + + + flowable-artifacts + + \ No newline at end of file diff --git a/tools/fakeSMTP-2.0.jar b/tools/fakeSMTP-2.0.jar new file mode 100644 index 0000000..2bb5330 Binary files /dev/null and b/tools/fakeSMTP-2.0.jar differ diff --git a/tools/startFakeSmtp.sh b/tools/startFakeSmtp.sh new file mode 100755 index 0000000..dbf079f --- /dev/null +++ b/tools/startFakeSmtp.sh @@ -0,0 +1,2 @@ + #!/bin/bash + java --add-exports java.desktop/com.apple.eawt=ALL-UNNAMED -jar ./fakeSMTP-2.0.jar -o ./received-emails -s -p 2525 \ No newline at end of file