Gitlab migration

This commit is contained in:
Andreas Isler
2026-06-23 08:04:38 +02:00
commit 82717986a7
82 changed files with 4872 additions and 0 deletions
+25
View File
@@ -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/
+8
View File
@@ -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
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="flowable-local-design" />
<module name="flowable-local-work" />
<module name="flowable-local-control" />
<module name="flowable-local-design-angular" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="flowable-local-control" options="-parameters" />
<module name="flowable-local-design" options="-parameters" />
<module name="flowable-local-design-angular" options="-parameters" />
<module name="flowable-local-work" options="-parameters" />
</option>
</component>
</project>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgres@localhost" uuid="2a1f6969-a9f0-4067-bce4-109ebe8e85d6">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/flowable-local-control/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flowable-local-design-angular/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flowable-local-design/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flowable-local-work/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="flowable-repo" />
<option name="name" value="flowable-repo" />
<option name="url" value="https://repo.flowable.com/artifactory/flowable-maven-all" />
</remote-repository>
<remote-repository>
<option name="id" value="central_flowable" />
<option name="name" value="flowable-maven-all" />
<option name="url" value="https://artifacts.flowable.com/artifactory/maven-all" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://artifacts.flowable.com/artifactory/maven-all" />
</remote-repository>
<remote-repository>
<option name="id" value="snapshots_flowable" />
<option name="name" value="flowable-maven-all" />
<option name="url" value="https://artifacts.flowable.com/artifactory/maven-all" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="homebrew-11" project-jdk-type="JavaSDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/3.14.iml" filepath="$PROJECT_DIR$/.idea/3.14.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+307
View File
@@ -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
+96
View File
@@ -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 |
+1
View File
@@ -0,0 +1 @@
COMPOSE_PROJECT_NAME=3-14
+154
View File
@@ -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:
+77
View File
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-local-control</artifactId>
<packaging>jar</packaging>
<name>flowable-local-control</name>
<description>flowable local Flowable Control</description>
<parent>
<groupId>com.flowable.local</groupId>
<artifactId>14</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<!-- flowable local -->
<!-- =============== -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${org.postgresql.version}</version>
</dependency>
<!-- Flowable Control -->
<!-- ================ -->
<dependency>
<groupId>com.flowable.control</groupId>
<artifactId>flowable-spring-boot-starter-control</artifactId>
<version>${com.flowable.platform.version}</version>
</dependency>
<!-- Spring Boot -->
<!-- =========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<!-- ======= -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${com.h2database.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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
@@ -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
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-local-design-angular</artifactId>
<packaging>jar</packaging>
<name>flowable-local-design-angular</name>
<description>flowable local Flowable Design Angular</description>
<parent>
<groupId>com.flowable.local</groupId>
<artifactId>14</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<!-- flowable local -->
<!-- =============== -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${org.postgresql.version}</version>
</dependency>
<!-- Flowable Design -->
<!-- =============== -->
<dependency>
<groupId>com.flowable.design.angular</groupId>
<artifactId>flowable-spring-boot-starter-design</artifactId>
<version>${com.flowable.platform.version}</version>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-platform-palette-angular</artifactId>
<version>${com.flowable.platform.version}</version>
</dependency>
<!-- Spring Boot -->
<!-- =========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<!-- ======= -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${com.h2database.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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
@@ -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
+83
View File
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-local-design</artifactId>
<packaging>jar</packaging>
<name>flowable-local-design</name>
<description>flowable local Flowable Design</description>
<parent>
<groupId>com.flowable.local</groupId>
<artifactId>14</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<!-- flowable local -->
<!-- =============== -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${org.postgresql.version}</version>
</dependency>
<!-- Flowable Design -->
<!-- =============== -->
<dependency>
<groupId>com.flowable.design</groupId>
<artifactId>flowable-spring-boot-starter-design</artifactId>
<version>${com.flowable.platform.version}</version>
</dependency>
<dependency>
<groupId>com.flowable.design</groupId>
<artifactId>flowable-spring-boot-starter-design-actuator</artifactId>
<version>${com.flowable.platform.version}</version>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-platform-palette</artifactId>
<version>${com.flowable.platform.version}</version>
</dependency>
<!-- Spring Boot -->
<!-- =========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<!-- ======= -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${com.h2database.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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();
}
}
@@ -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<DesignHttpSecurityCustomizer> 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();
}
}
@@ -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
@@ -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
+139
View File
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-local-work</artifactId>
<packaging>jar</packaging>
<name>flowable-local-work</name>
<description>flowable local Flowable Work</description>
<parent>
<groupId>com.flowable.local</groupId>
<artifactId>14</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<!-- flowable local -->
<!-- =============== -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${org.postgresql.version}</version>
</dependency>
<!-- Flowable Frontend -->
<!-- ================= -->
<dependency>
<groupId>com.flowable.work</groupId>
<artifactId>flowable-work-frontend</artifactId>
</dependency>
<!-- Flowable Platform -->
<!-- ================= -->
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-spring-boot-starter-platform-rest</artifactId>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-tenant-setup</artifactId>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-platform-default-models</artifactId>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-platform-default-idm-models</artifactId>
</dependency>
<!-- Flowable Inspect -->
<!-- ================ -->
<dependency>
<groupId>com.flowable.inspect</groupId>
<artifactId>flowable-spring-boot-starter-inspect-rest</artifactId>
</dependency>
<!-- Flowable Actuators -->
<!-- ================== -->
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-spring-boot-starter-platform-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-elastic</artifactId>
</dependency>
<!-- Spring Boot -->
<!-- =========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<!-- ======= -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${com.h2database.version}</version>
</dependency>
<dependency>
<groupId>com.github.wnameless.json</groupId>
<artifactId>json-flattener</artifactId>
<version>0.16.6</version>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>2.1.8</version>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.1.4</version>
</dependency>
<!-- AuditTrailController -->
<!-- ======= -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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();
}
}
@@ -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<FrontendProperties> frontendPropertiesProvider;
@Bean
@Order(10)
public SecurityFilterChain basicDefaultSecurity(HttpSecurity http, ObjectProvider<FlowableHttpSecurityCustomizer> 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();
}
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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<String, Object> data = (HashMap<String, Object>) 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<? extends FlowableEventType> getTypes() {
List<FlowableEventType> eventTypes = new ArrayList<>();
eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_UPDATED);
eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_DELETED);
return eventTypes;
}
}
@@ -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_ {
<data object column name 1>: <content of data object field in column 1>
<data object column name 2>: <content of data object field in column 1>
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<String, Object> 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<String, Object> payload = new HashMap<>();
String scopeId = null;
String subScopeId = null;
SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe();
if (subType.equals("created") || subType.equals("updated") || subType.equals("deleted")) {
HashMap<String, Object> data = (HashMap<String, Object>) dataObjectInstance.getData();
if (data == null) {
LOGGER.warn("FlowableDataObjectEvent: No data");
return;
}
scopeDefinitionId = dataObjectInstance.getDefinitionKey();
for (Map.Entry<String, Object> 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<? extends FlowableEventType> getTypes() {
List<FlowableEventType> 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;
}
}
@@ -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<String> 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<Boolean> 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<Integer> getIntegerVariable(
@PathVariable String key,
@RequestParam(name = "default", required = false) Integer defaultValue) {
if (defaultValue == null) {
defaultValue = 0;
}
return ResponseEntity.ok(propertyConfigurationService.getIntegerProperty(key, defaultValue));
}
}
@@ -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<HashMap<String, Object>> 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);
}
}
@@ -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<ArrayNode> getCurrentUserGroups() {
return ResponseEntity.ok(this.userService.getCurrentUserGroups());
}
@GetMapping(value = {"/user-groups/{userId}"}, produces = {"application/json"})
public ResponseEntity<ArrayNode> getUserGroups(@PathVariable String userId) {
return ResponseEntity.ok(this.userService.getUserGroups(userId));
}
}
@@ -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<JsonNode> getRootVariable(@PathVariable String caseInstanceId) {
return ResponseEntity.ok(variableService.getVariable(caseInstanceId, "root"));
}
@GetMapping("/variable/{caseInstanceId}/{variableName}")
public ResponseEntity<JsonNode> 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";
}
}
@@ -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 <root.id>
-> audit trail entries for case with specified id
- instanceName <root.name>
-> audit trail entries for case with specified name
- scopeType dataObject
- dataObjectId <data object instance id>
-> audit trail entries for data object with specified instance id, provided by DataObjectAuditTrailEventHandler
- dataObjectModelKey <data object model key>
-> 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<HashMap<String, Object>> 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<HashMap<String, Object>> 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<AuditInstance> auditInstances;
if (size != null) {
auditInstances = query.listPage(startIndex, size);
} else {
auditInstances = query.list();
}
// Map query result into proper list
List<HashMap<String, Object>> entries = new ArrayList<>();
for (AuditInstance auditInstance : auditInstances) {
entries.add(convertAuditInstance(auditInstance));
}
return createAuditEntryDataResponse(entries, query.count(), startIndex);
}
protected DataResponse<HashMap<String, Object>> createAuditEntryDataResponse(List<HashMap<String, Object>> entries, long totalCount, int startIndex) {
DataResponse<HashMap<String, Object>> 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<String, Object> convertAuditInstance(AuditInstance auditInstance) {
HashMap<String, Object> auditEntry = new HashMap<>();
AuditInstanceEntity auditInst = (AuditInstanceEntity) auditInstance;
auditEntry.put("id", auditInst.getId());
auditEntry.putAll((Map<String, String>) auditInst.getPersistentState());
HashMap<String, Object> auditInstPayload = (HashMap<String, Object>) auditInst.getPayload();
if (auditInstPayload != null) {
auditEntry.put("payload", new HashMap<>(auditInstPayload));
}
return auditEntry;
}
}
@@ -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<DataObjectInstanceVariableContainer> dataObjects = dataObjectRuntimeService.createDataObjectInstanceQuery()
.tenantId(tenantId)
.definitionKey(definitionKey)
.operation("searchAll")
.list();
for (DataObjectInstanceVariableContainer dataObjectInstanceVariableContainer : dataObjects) {
dataObjectRuntimeService.deleteDataObject(dataObjectInstanceVariableContainer.getLookupId().toString(),
dataObjectInstanceVariableContainer.getDefinitionId());
}
}
}
@@ -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<Object> list) {
return objectMapper.valueToTree(list);
}
public JsonNode convertMapToJsonNode(Map<String, Object> map) {
return objectMapper.valueToTree(map);
}
public Map<String, Object> convertJsonNodeToMap(JsonNode jsonNode) {
return objectMapper.convertValue(jsonNode, new TypeReference<Map<String, Object>>() {});
}
public Map<String, Object> flatten(Object payload) {
try {
return JsonFlattener.flattenAsMap(objectMapper.writeValueAsString(payload));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public Map<String, Object> unflatten(Map<String, Object> 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<String, Object> convertObjectNodeToMap(ObjectNode objectNode) {
return objectMapper.convertValue(objectNode, new TypeReference<Map<String, Object>>() {});
}
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;
}
}
}
@@ -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<String, Object> 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<String, Object> processVariables = processInstance.getProcessVariables();
ObjectNode responsePayload = this.objectMapper.convertValue(processVariables, ObjectNode.class);
responsePayload.put("id", processInstance.getId());
return new BaseBotActionResult(responsePayload, Intent.NOOP);
}
}
@@ -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<String> stringList = new ArrayList<>(currentUserSecurityScope.getGroupKeys());
return createSortedArrayNode(stringList);
}
@NotNull
protected static ArrayNode createSortedArrayNode(List<String> 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<String> groupStringList = getLdapEntitlements(userId, currentUserSecurityScope.getTenantId());
if (groupStringList.isEmpty()) {
// Get groups via Group Query
groupStringList = getPlatformGroups(userId, currentUserSecurityScope);
}
return createSortedArrayNode(groupStringList);
}
@NotNull
protected List<String> getPlatformGroups(String userId, SecurityScope currentUserSecurityScope) {
List<PlatformGroup> platformGroups = new ArrayList<>();
try {
platformGroups = platformIdentityService.createPlatformGroupQuery()
.groupTenantId(currentUserSecurityScope.getTenantId())
.groupMember(userId)
.list();
} catch (Exception ignored) {
}
List<String> stringList = new ArrayList<>();
for(PlatformGroup platformGroup : platformGroups) {
stringList.add(platformGroup.getKey());
}
return stringList;
}
@NotNull
protected List<String> 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);
}
}
@@ -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<String, Object> 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);
}
}
@@ -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
@@ -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
@@ -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
@@ -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"
}
}
@@ -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"
}
}
}
]
}
@@ -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() {
}
}
@@ -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() {
}
}
@@ -0,0 +1,7 @@
package com.flowable.local.work.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TestConfiguration {
}
@@ -0,0 +1,49 @@
package com.flowable.local.work.model;
import java.util.List;
public class EmailDto {
private String subject;
private List<String> receiverList;
private String content;
private Object contentRaw;
public EmailDto(String subject, List<String> 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<String> getReceiverList() {
return receiverList;
}
public void setReceiverList(List<String> 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;
}
}
@@ -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<ArrayList<ArrayList<String>>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath);
return bookToJsonNode(book);
}
protected ArrayNode bookToJsonNode(ArrayList<ArrayList<ArrayList<String>>> book) {
ArrayNode bookNode = objectMapper.createArrayNode();
for (ArrayList<ArrayList<String>> sheet : book) {
bookNode.add(sheetToJsonNode(sheet));
}
return bookNode;
}
protected ArrayNode sheetToJsonNode(ArrayList<ArrayList<String>> rows) {
ArrayList<String> paths = new ArrayList<>();
ArrayList<String> 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<String> types, ArrayList<String> paths, ArrayList<String> 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<ArrayList<Object>> excelBookResourceToObj(String resourcePath) {
ArrayList<ArrayList<ArrayList<String>>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath);
return bookToObj(book);
}
protected ArrayList<ArrayList<Object>> bookToObj(ArrayList<ArrayList<ArrayList<String>>> book) {
ArrayList<ArrayList<Object>> bookNode = new ArrayList<>();
for (ArrayList<ArrayList<String>> sheet : book) {
bookNode.add(sheetToObj(sheet));
}
return bookNode;
}
protected ArrayList<Object> sheetToObj(ArrayList<ArrayList<String>> rows) {
ArrayList<String> paths = new ArrayList<>();
ArrayList<String> types = new ArrayList<>();
ArrayList<Object> 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<String> types, ArrayList<String> paths, ArrayList<String> 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<String, Object>();
}
int delimiterPosition = indexOfFirstDelimiter(path, ".*");
if (delimiterPosition <= 0) {
if (parent instanceof LinkedHashMap) {
// Key of a value in an ObjectNode
((Map<String, Object>) parent).put(path, value);
} else if (parent instanceof ArrayList) {
// Index of a value in an ArrayNode
int index = Integer.parseInt(path);
while (((ArrayList<Object>) parent).size() <= index) {
// Create empty entries to parent ArrayNode
((ArrayList<Object>) parent).add(new LinkedHashMap<String, Object> ());
}
((ArrayList<Object>) 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<String, Object>();
} else if (delimiter.equals("*")) {
child = new ArrayList<>();
}
Object node;
if (parent instanceof LinkedHashMap) {
// Key of a JsonNode in an ObjectNode
node = ((Map<String, Object>) parent).get(key);
if (node != null) {
// Take the existing ObjectNode as child
child = node;
}
((Map<String, Object>) 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<Object>) parent).size() > index) {
node = ((ArrayList<Object>) parent).get(index);
} else {
node = null;
}
if (node != null) {
// Take the existing ArrayNode as child
child = node;
}
while (((ArrayList<Object>) parent).size() <= index) {
// Create empty entries to parent ArrayNode
((ArrayList<Object>) parent).add(new LinkedHashMap<String, Object> ());
}
((ArrayList<Object>) parent).set(index, addObj(child, newPath, value));
} else {
throw new RuntimeException("Parent type not supported: " + parent.getClass().getName());
}
return parent;
}
}
@@ -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<ArrayList<ArrayList<String>>> 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<ArrayList<ArrayList<String>>> parseBook(Workbook workBook ) {
ArrayList<ArrayList<ArrayList<String>>> book = new ArrayList<>();
FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator();
for (Sheet workSheet : workBook) {
book.add(parseSheet(workSheet, formulaEvaluator));
}
return book;
}
private ArrayList<ArrayList<String>> parseSheet(Sheet workSheet, FormulaEvaluator formulaEvaluator) {
ArrayList<ArrayList<String>> 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<String> 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<String> parseRow(Row workRow, int maxCols, FormulaEvaluator formulaEvaluator, DataFormatter dataFormatter) {
if (workRow == null) {
// Row is empty
return null;
}
ArrayList<String> 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<LinkedHashMap<String, Object>> parseExcelFromResource(String resourcePath) {
try (FileInputStream fileInputStream = new FileInputStream(getResourcePath(resourcePath))) {
Workbook workBook = new XSSFWorkbook(fileInputStream);
FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator();
ArrayList<ArrayList<String>> sheet = parseSheet(workBook.getSheetAt(0), formulaEvaluator);
ArrayList<String> colHeaders = new ArrayList<>();
ArrayList<LinkedHashMap<String, Object>> rowList = new ArrayList<>();
for (int rowIndex = 0; rowIndex < sheet.size(); rowIndex++) {
ArrayList<String> row = sheet.get(rowIndex);
if (rowIndex == 0) {
// Header row
for (Object cell : row) {
colHeaders.add(String.valueOf(cell));
}
continue;
}
LinkedHashMap<String, Object> 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);
}
}
}
@@ -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<String, Object> parseMap(Object map) throws JsonProcessingException, ClassCastException {
return (Map<String, Object>) parseObject(map);
}
public Object parseObject(Object object) throws JsonProcessingException, ClassCastException {
if (object instanceof Map) {
Map<String, Object> resultMap = new LinkedHashMap<>(Map.of());
for (Map.Entry<String, Object> mapEntry : ((Map<String, Object>) object).entrySet()) {
String key = mapEntry.getKey();
resultMap.put(key, parseMapEntry(key, mapEntry.getValue()));
}
return resultMap;
}
if (object instanceof ArrayList) {
ArrayList<Object> resultList = new ArrayList<>();
for (Object arrayEntry : (ArrayList<Object>) object) {
resultList.add(parseArrayEntry(arrayEntry));
}
return resultList;
}
return null;
}
private Object parseMapEntry(String key, Object value) throws JsonProcessingException {
if (value instanceof Map) {
Map<String, Object> valueMap = (Map<String, Object>) 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;
}
}
}
@@ -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;
*/
}
@@ -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<String, Object> 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<AuditInstance> 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<String, Object> 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<EmailDto> 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<Pair<String, Boolean>> receiverCheckList = new ArrayList<>();
for (String receiver : receivers.split("[,\\s]+")) {
receiverCheckList.add(Pair.of(receiver, false));
}
List<String> 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<String, Boolean> 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<String, Object> getHistoryCasePayload(String caseInstanceId) {
List<HistoricVariableInstance> historicVariableInstanceList = cmmnEngine.getCmmnHistoryService()
.createHistoricVariableInstanceQuery()
.caseInstanceId(caseInstanceId)
.list();
return convertHistVariableListToMap(historicVariableInstanceList);
}
public Map<String, Object> getRuntimeCasePayload(String caseId) {
return cmmnEngine.getCmmnRuntimeService().getVariables(caseId);
}
public CaseInstance startCaseInstance(String key, Map<String, Object> variables) {
return cmmnEngine.getCmmnRuntimeService()
.createCaseInstanceBuilder()
.caseDefinitionKey(key)
.tenantId(TENANT_ID) // Must not be "default"
.variables(variables)
.start();
}
public ProcessInstance startProcessInstance(String key, Map<String, Object> 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<AuditInstance> getAuditTrail() {
return auditService.createAuditInstanceQuery()
//.tenantId(TENANT_ID) TODO: Must not be null, should work with default tenant
.list();
}
public List<EmailDto> 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<String> 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<Arguments> getJsonArgumentsFromExcel(String path) {
ArrayList<Arguments> 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<Arguments> getObjArgumentsFromExcel(String path) {
ArrayList<Arguments> argumentList = new ArrayList<>();
ArrayList<ArrayList<Object>> book = flowableExcelMapper.excelBookResourceToObj(path);
for (ArrayList<Object> 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<Map.Entry<String, JsonNode>> rootVars = rootParam.fields();
while (rootVars.hasNext()) {
Map.Entry<String, JsonNode> 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<String, Object> 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<AuditInstance> auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText());
List<AuditInstance> 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<EmailDto> 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<String, Object> testObjExcelRow(String path, Map<String, Object> row) {
Map<String, Object> vars = new LinkedHashMap<>();
Map<String, Object> rootParam = (Map<String, Object>) row.get("root");
if (rootParam != null) {
for (Map.Entry<String, Object> entry : rootParam.entrySet()) {
vars.put(entry.getKey(), entry.getValue());
}
}
Map<String, Object> inParam = (Map<String, Object>) row.get("in");
if (inParam != null) {
vars.put("__IN", inParam);
}
Map<String, Object> outParam = (Map<String, Object>) row.get("out");
if (outParam != null) {
vars.put("__OUT", outParam);
}
String idParam = (String) row.get("id");
Map<String, Object> test =(Map<String, Object>) 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<String, Object> result = new LinkedHashMap<>();
Map<String, Object> 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<AuditInstance> auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText());
List<AuditInstance> 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<EmailDto> 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<JsonNode> 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<String, Object> 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<String, Object> 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<GrantedAuthority> 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<GrantedAuthority> 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<String, Object> variables, String outcome) {
Map<String, Object> taskVariables = platformTaskService.getTaskVariables(taskId);
Map<String, Object> taskVariablesFlat = jsonUtils.flatten(taskVariables);
taskVariablesFlat.putAll(variables);
Map<String, Object> completionVars = jsonUtils.unflatten(taskVariablesFlat);
completeTask(taskId, completionVars, outcome);
}
public void completeTask(String taskId, Map<String, Object> 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<String, Object> flatVars = jsonUtils.convertJsonNodeToMap(vars);
completeTaskWithFlatVars(task.getId(), flatVars, outcome);
}
public Map<String, Object> getHistProcessPayload(String processInstanceId) {
List<HistoricVariableInstance> historicVariableInstanceList = processEngine.getHistoryService()
.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.list();
return convertHistVariableListToMap(historicVariableInstanceList);
}
private HashMap<String, Object> convertHistVariableListToMap(List<HistoricVariableInstance> historicVariableInstanceList) {
HashMap<String, Object> variableMap = new HashMap<>();
for (HistoricVariableInstance historicVariableInstance : historicVariableInstanceList) {
variableMap.put(historicVariableInstance.getVariableName(), historicVariableInstance.getValue());
}
return variableMap;
}
public ObjectNode createRootTestProcessInstance(String testKey, Map<String, Object> 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<String, Object> inMap = jsonUtils.convertObjectNodeToMap((ObjectNode) inNode);
ArrayList<IOParameter> inParameters = new ArrayList<>();
for (Map.Entry<String, Object> 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<String, String> outMap = (Map) jsonUtils.convertJsonNodeToMap(variables).get("__OUT");
ArrayList<IOParameter> outParameters = new ArrayList<>();
for (Map.Entry<String, String> 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;
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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<String, Object> rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
Assertions.assertThat(rootProcessPayload.get("dataEntry")).isEqualTo("my root text");
}
@NotNull
private Stream<Arguments> 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<String, Object> rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
Assertions.assertThat(rootProcessPayload.get("rootResult")).isEqualTo(null);
}
@NotNull
private Stream<Arguments> 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<String, Object> rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
List<EmailDto> 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<Arguments> 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<Arguments> p002ExcelTestData2() {
return flowableModelTest.getObjArgumentsFromExcel("model/test/P002/p002Test.xlsx");
}
@ParameterizedTest
@MethodSource("p002ExcelTestData2")
public void p002ExcelTest2(String path, Map<String, Object> argument) {
Map<String, Object> 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);
}
}
@@ -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
@@ -0,0 +1,5 @@
{
"root": {
"testText": "my test text"
}
}
@@ -0,0 +1,5 @@
{
"__IN": {
"initiator": "admin"
}
}
@@ -0,0 +1,9 @@
{
"param": true,
"__IN": {
"param": false
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": false,
"__IN": {
"param": true
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "2025-11-12",
"__IN": {
"param": "2025-11-11"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": 123.456,
"__IN": {
"param": 456.789
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": 123,
"__IN": {
"param": 456
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "hello root",
"__IN": {
"param": "hello"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "123",
"__IN": {
"param": "456"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "123.456,",
"__IN": {
"param": "456.789"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,5 @@
{
"root": {
"dataEntry": "my root text"
}
}
Vendored Executable
+295
View File
@@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -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 "$@"
Vendored
+189
View File
@@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
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"
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.flowable.local</groupId>
<artifactId>14</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modules>
<module>flowable-local-work</module>
<module>flowable-local-design</module>
<module>flowable-local-design-angular</module>
<module>flowable-local-control</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</java.version>
<com.flowable.platform.version>3.14.10</com.flowable.platform.version>
<org.postgresql.version>42.7.8</org.postgresql.version>
<com.h2database.version>2.2.224</com.h2database.version>
<liquibase.skip>false</liquibase.skip>
</properties>
<dependencyManagement>
<dependencies>
<!-- Imports the bill-of-materials POM. -->
<dependency>
<groupId>com.flowable</groupId>
<artifactId>flowable-platform-bom</artifactId>
<version>${com.flowable.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>flowable-repo</id>
<username>USER</username> <!-- Replace USER -->
<password>PASSWORD</password> <!-- Replace PASSWORD -->
</server>
</servers>
<profiles>
<profile>
<id>flowable-artifacts</id>
<repositories>
<repository>
<id>flowable-repo</id>
<url>https://artifacts.flowable.com/artifactory/flowable-maven-all</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>flowable-artifacts</activeProfile>
</activeProfiles>
</settings>
Binary file not shown.
+2
View File
@@ -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