diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index 1be49359a52..e34e9f57ee2 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -9,22 +9,16 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean verify -DskipTests=true @@ -36,7 +30,7 @@ jobs: timeout-minutes: 90 services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -67,15 +61,16 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven # - name: Cache SonarCloud packages # uses: actions/cache@v3 @@ -84,13 +79,6 @@ jobs: # key: ${{ runner.os }}-sonar # restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. @@ -113,23 +101,17 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: token: ${{ secrets.PUSH_DOCS }} fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean package install -DskipTests=true @@ -139,8 +121,8 @@ jobs: mvn javadoc:javadoc cp -r ./target/apidocs/* ./docs/javadoc/ - - uses: EndBug/add-and-commit@v8 + - uses: EndBug/add-and-commit@v9 with: add: docs pathspec_error_handling: exitImmediately - message: 'CI - Update documentation' \ No newline at end of file + message: 'CI - Update documentation' diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 318013ca7ea..9b361e4f30e 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -8,22 +8,16 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean verify -DskipTests=true @@ -35,7 +29,7 @@ jobs: timeout-minutes: 200 services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -66,15 +60,16 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven # - name: Cache SonarCloud packages # uses: actions/cache@v3 @@ -83,13 +78,6 @@ jobs: # key: ${{ runner.os }}-sonar # restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. @@ -110,4 +98,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B test \ No newline at end of file + run: mvn -B test diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 85978a4daca..15f7028765f 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -2,41 +2,28 @@ name: Publish package to GitHub Packages on: release: types: [ published ] + workflow_dispatch: jobs: build: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - uses: cardinalby/git-get-release-action@v1 - id: getEnvRelease - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + distribution: 'temurin' + cache: maven - name: Get Project Version from pom.xml uses: entimaniac/read-pom-version-action@1.0.0 id: getVersion - # - name: Check Enviroment release - # if: ${{ !((steps.getEnvRelease.outputs.prerelease && contains(steps.getVersion.outputs.version, '-SNAPSHOT')) || (!steps.getEnvRelease.outputs.prerelease && !contains(steps.getVersion.outputs.version, '-SNAPSHOT'))) }} - # run: exit 1 - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Build run: mvn clean verify -DskipTests=true @@ -47,7 +34,7 @@ jobs: needs: build services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -78,29 +65,16 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - -# - name: Cache SonarCloud packages -# uses: actions/cache@v3 -# with: -# path: ~/.sonar/cache -# key: ${{ runner.os }}-sonar -# restore-keys: ${{ runner.os }}-sonar - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. @@ -108,13 +82,6 @@ jobs: - name: Build run: mvn clean package install -DskipTests=true -# Upgrade Java -# - name: Build, test, and analyze -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -# run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=netgrif_application-engine - - name: Build, test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -129,7 +96,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - id: install-secret-key name: Install gpg secret key @@ -138,10 +105,11 @@ jobs: gpg --list-secret-keys --keyid-format LONG - name: Set up Maven Central Repository - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD @@ -162,26 +130,20 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn -P docker-build clean package install -DskipTests=true - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_AUTH_TOKEN }} @@ -191,14 +153,14 @@ jobs: id: getVersion - name: Push Version ${{ steps.getVersion.outputs.version }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: push: true tags: netgrif/application-engine:${{ steps.getVersion.outputs.version }} - name: Push Latest if: ${{ !contains(steps.getVersion.outputs.version, '-SNAPSHOT') }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: push: true tags: netgrif/application-engine:latest @@ -211,19 +173,13 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: java-version: '11' - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Publish artifact on GitHub Packages run: mvn -B -P github-publish clean deploy -DskipTests @@ -240,7 +196,7 @@ jobs: id-token: write security-events: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Build run: mvn clean package install -DskipTests=true diff --git a/CHANGELOG.md b/CHANGELOG.md index b298051e33e..a978e166bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,33 @@ Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.5.0](h - [NAE-2051] Implement configurable view in menu items - [NAE-2039] Search in workflow view - [NAE-2052] Integrate ticket view with menu items + +Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.2](https://github.com/netgrif/application-engine/commits/v6.4.2) + +## [6.4.2](https://github.com/netgrif/application-engine/releases/tag/v6.4.2) (2025-05-16) + +### Fixed + +- [NAE-2225] Not possible to set empty options using setData +- [NAE-2231] Unable to change behavior of taskRef on finish event without error message +- Refactor ObjectMapper configuration for Elasticsearch +- Remove custom serializers for startDate in ElasticTask +- [NAE-2342] Improve Quartz configuration +- Minor improvements to manage migrations + +### Added + +- [NAE-2100] Case view export button as NAE feature +- [NAE-2136] Speed up Elasticsearch reindex +- [NAE-2303] TaskRef Security Improvements +- [NAE-2310] Elasticsearch fulltext query input sanitation +- [NAE-2246] - Enable Redis TLS & Configure Redis Sentinel +- [NAE-2401] Timestamp of case dataSet change + +## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-03-19) + +### Fixed +- [NAE-2031] Dashboard bug fix ## [6.4.0](https://github.com/netgrif/application-engine/releases/tag/v6.4.0) (2024-12-24) diff --git a/Dockerfile b/Dockerfile index c2fe13e8ea6..2259d3100a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3-jdk-11 AS build +FROM maven:3.9-eclipse-temurin-11 AS build MAINTAINER Netgrif WORKDIR /app COPY src /app/src @@ -6,7 +6,7 @@ COPY pom.xml /app RUN mvn -P docker-build -DskipTests=true -f /app/pom.xml clean package install -FROM openjdk:11-jdk +FROM eclipse-temurin:11-jdk MAINTAINER Netgrif COPY --from=build app/target/app-exec.jar /app.jar COPY --from=build app/src/main/resources /src/main/resources diff --git a/README.md b/README.md index 11e1847f0c5..b3420b67c07 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License](https://img.shields.io/badge/license-NETGRIF%20Community%20License-green)](https://netgrif.com/license) [![Java](https://img.shields.io/badge/Java-11-red)](https://openjdk.java.net/projects/jdk/11/) -[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.com) +[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.org) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/netgrif/application-engine?sort=semver&display_name=tag)](https://github.com/netgrif/application-engine/releases) [![build](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml/badge.svg)](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=netgrif_application-engine&metric=alert_status)](https://sonarcloud.io/dashboard?id=netgrif_application-engine) @@ -17,7 +17,7 @@ is based on Spring framework with fully complaint Petriflow language interpreter Machine. It can be embedded into Java 11 project or used as a standalone process server. On top of the process server, NAE provides additional components to make integration to your project/environment seamless. -* Petriflow low-code language: [http://petriflow.com](https://petriflow.com) +* Petriflow low-code language: [http://petriflow.org](https://petriflow.org) * Documentation: [https://engine.netgrif.com](https://engine.netgrif.com) * Issue Tracker: [GitHub issues](https://github.com/netgrif/application-engine/issues) diff --git a/docker-compose.yml b/docker-compose.yml index b578689d92a..b6b8d2052f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: image: redis:6 ports: - "6379:6379" + minio: image: bawix/minio:2022 ports: @@ -51,13 +52,6 @@ services: - MINIO_ROOT_USER=root - MINIO_ROOT_PASSWORD=password - MINIO_DEFAULT_BUCKETS=default -networks: - minionetwork: - driver: bridge - -volumes: - minio_data: - driver: local # kibana: # image: docker.elastic.co/kibana/kibana:7.17.4 @@ -67,3 +61,12 @@ volumes: # - docker-elastic # ports: # - "5601:5601" + +networks: + minionetwork: + driver: bridge + +volumes: + minio_data: + driver: local + diff --git a/docs/README.md b/docs/README.md index 1643abd37a4..965a7b3d4d6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ [![GitHub](https://img.shields.io/github/license/netgrif/application-engine)](https://netgrif.com/license) [![Java](https://img.shields.io/badge/Java-11-red)](https://openjdk.java.net/projects/jdk/11/) -[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.com) +[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.org) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/netgrif/application-engine?sort=semver&display_name=tag)](https://github.com/netgrif/application-engine/releases) [![build](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml/badge.svg)](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=netgrif_application-engine&metric=alert_status)](https://sonarcloud.io/dashboard?id=netgrif_application-engine) @@ -17,7 +17,7 @@ is based on Spring framework with fully complaint Petriflow language interpreter Machine. It can be embedded into Java 11 project or used as a standalone process server. On top of the process server, NAE provides additional components to make integration to your project/environment seamless. -* Petriflow low-code language: [http://petriflow.com](https://petriflow.com) +* Petriflow low-code language: [http://petriflow.org](https://petriflow.org) * Documentation: [https://engine.netgrif.com](https://engine.netgrif.com) * Getting Started: [https://engine.netgrif.com/get_started](https://engine.netgrif.com/get_started) * Issue Tracker: [GitHub issues](https://github.com/netgrif/application-engine/issues) @@ -157,4 +157,4 @@ our [Contribution guide](https://github.com/netgrif/application-engine/blob/mast ## License The software is licensed under NETGRIF Community license. You may be found this license -at [the LICENSE file](https://github.com/netgrif/application-engine/blob/master/LICENSE) in the repository. \ No newline at end of file +at [the LICENSE file](https://github.com/netgrif/application-engine/blob/master/LICENSE) in the repository. diff --git a/docs/_media/roles/usersRef_net.xml b/docs/_media/roles/usersRef_net.xml index 39db84655a5..29449f4fc3b 100644 --- a/docs/_media/roles/usersRef_net.xml +++ b/docs/_media/roles/usersRef_net.xml @@ -1,5 +1,5 @@ - + testing_model TSM Testing Model diff --git a/docs/_media/views/Layout-Guide-compacting-example.xml b/docs/_media/views/Layout-Guide-compacting-example.xml index 69bb578acc5..cb44471679c 100644 --- a/docs/_media/views/Layout-Guide-compacting-example.xml +++ b/docs/_media/views/Layout-Guide-compacting-example.xml @@ -1,5 +1,5 @@ - + compacting_example EXP Compacting example diff --git a/docs/_media/views/Layout-Guide-flow-example.xml b/docs/_media/views/Layout-Guide-flow-example.xml index 4c7a882aca6..d4f46a67d3c 100644 --- a/docs/_media/views/Layout-Guide-flow-example.xml +++ b/docs/_media/views/Layout-Guide-flow-example.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> flowExample 1.0.0 FEX diff --git a/docs/_media/views/Layout-Guide-grid-example.xml b/docs/_media/views/Layout-Guide-grid-example.xml index 225fe69f4a7..ddd7090e392 100644 --- a/docs/_media/views/Layout-Guide-grid-example.xml +++ b/docs/_media/views/Layout-Guide-grid-example.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> gridExample 1.0.0 GEX diff --git a/docs/_media/views/Layout-Guide-legacy-example.xml b/docs/_media/views/Layout-Guide-legacy-example.xml index db750f8e6d9..dc5e885e0dd 100644 --- a/docs/_media/views/Layout-Guide-legacy-example.xml +++ b/docs/_media/views/Layout-Guide-legacy-example.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> legacyExample 1.0.0 LEX diff --git a/docs/_navbar.md b/docs/_navbar.md index 73170c3718f..68a50824bac 100644 --- a/docs/_navbar.md +++ b/docs/_navbar.md @@ -1,3 +1,3 @@ * [Try Engine](https://demo.netgrif.com/) * [Try Builder](https://builder.netgrif.com) -* [Petriflow](https://petriflow.com) \ No newline at end of file +* [Petriflow](https://petriflow.org) diff --git a/docs/events/case_event.md b/docs/events/case_event.md index 1762cbfbab4..7fbcf4f8d2c 100644 --- a/docs/events/case_event.md +++ b/docs/events/case_event.md @@ -15,7 +15,7 @@ case. ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -53,7 +53,7 @@ User lists cannot be associated with the create event, since they don't exist be ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -88,7 +88,7 @@ execution. ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -126,7 +126,7 @@ User lists can be associated with the delete event ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -144,4 +144,4 @@ User lists can be associated with the delete event ``` - \ No newline at end of file + diff --git a/docs/events/process_event.md b/docs/events/process_event.md index 1428ceed673..85b4f87df97 100644 --- a/docs/events/process_event.md +++ b/docs/events/process_event.md @@ -15,7 +15,7 @@ If you want to change data on another cases, you need to use the `setData` funct ```xml - + constructor_destructor Constructor and Destructor CAD @@ -37,4 +37,4 @@ If you want to change data on another cases, you need to use the `setData` funct ``` - \ No newline at end of file + diff --git a/docs/roles/permissions.md b/docs/roles/permissions.md index 27c408013be..b6976cf9f24 100644 --- a/docs/roles/permissions.md +++ b/docs/roles/permissions.md @@ -23,7 +23,7 @@ Since the developer may want to apply these roles to "all" transitions a shortha The default and the anonymous role can be applied independently of each other. ``` - + ... true true @@ -234,7 +234,7 @@ In the XML model of the process, you can define roles as child elements of the r element. The role is connected to the role reference via the role's ID. ``` - + ... process_role @@ -256,7 +256,7 @@ Permission documentation can be found [here](#Permissions). Roles can be referen - as a child element of the `document` tag for referencing roles on cases: ``` - + ... process_role @@ -272,7 +272,7 @@ Permission documentation can be found [here](#Permissions). Roles can be referen - as a child element of the `transition` tag for referencing roles on tasks: ``` - + ... ... @@ -296,7 +296,7 @@ given user list) to Petriflow objects and their actions. User list can be define defined, as child element of the root **document** element: ``` - + ... user_list_1 @@ -316,7 +316,7 @@ Permission documentation can be found [here](#Permissions). User list can be ref - as a child element of the `document` tag for referencing user list on cases: ``` - + ... user_list_1 @@ -332,7 +332,7 @@ Permission documentation can be found [here](#Permissions). User list can be ref - as a child element of the `transition` tag for referencing user list on tasks: ``` - + ... ... @@ -381,7 +381,7 @@ Each reference element has a child element called **caseLogic**, which can be us permissions for case created from process as follows: ``` - + ... process_role @@ -429,7 +429,7 @@ transition** element. Each reference element has a child element called **logic* permissions for task created from transition as follows: ``` - + ... ... diff --git a/docs/roles/shared_roles.md b/docs/roles/shared_roles.md index 3ef64b026a0..880c98bb7f8 100644 --- a/docs/roles/shared_roles.md +++ b/docs/roles/shared_roles.md @@ -4,7 +4,7 @@ To use a shared role in Petri nets first we must declare it. We can declare it a attribute set to ``true``: ```xml + xsi:noNamespaceSchemaLocation='https://petriflow.org/petriflow.schema.xsd'> nae_1927 ... @@ -33,4 +33,4 @@ Then we can reference it as usual: ... ``` When importing a Petri net, the importer checks, whether the global role has already existed. -If not, the importer creates one. If there has been already one, the importer passes it to a the newly created net. \ No newline at end of file +If not, the importer creates one. If there has been already one, the importer passes it to a the newly created net. diff --git a/pom.xml b/pom.xml index 5b79a2f6768..24550b4ead7 100644 --- a/pom.xml +++ b/pom.xml @@ -64,31 +64,63 @@ 7.70.0.Final netgrif-oss https://sonarcloud.io + 2.15.0-rc1 + + + + + + + + + + + + + + + + + + + + + + + - oss.snapshots - OSSRH SNAPSHOT - https://s01.oss.sonatype.org/content/repositories/snapshots + central + https://repo.maven.apache.org/maven2 - false + true - true + false - mvnrepository1 - https://maven.imagej.net/content/repositories/public/ - - - mvnrepository2 - https://repo.spring.io/plugins-release/ + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + - mulesoft - https://repository.mulesoft.org/nexus/content/repositories/public/ + jitpack.io + https://jitpack.io + + true + + + false + @@ -304,10 +336,15 @@ batik-all 1.17 + + xml-apis + xml-apis-ext + 1.3.04 + commons-io commons-io - 2.7 + 2.14.0 de.rototor.pdfbox @@ -352,12 +389,11 @@ ${querydsl.version} - - com.github.kenglxn.qrgen - javase - 2.6.0 + com.github.kenglxn + QRGen + 3.0.1 @@ -366,6 +402,20 @@ spring-boot-starter-data-elasticsearch + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + + org.glassfish + jakarta.json + 2.0.1 + + + com.google.protobuf @@ -395,13 +445,6 @@ ${drools.version} - - - net.glxn.qrgen - core - 2.0 - - org.apache.commons commons-lang3 @@ -435,7 +478,7 @@ com.netgrif quartz-mongodb-connector - 1.0.0 + 1.0.1 @@ -492,38 +535,38 @@ com.fasterxml.jackson jackson-base - 2.15.0-rc1 + ${jackson.version} pom com.fasterxml.jackson.core jackson-core - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.core jackson-databind - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.core jackson-annotations - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.dataformat jackson-dataformat-xml - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.module jackson-module-jsonSchema - 2.15.0-rc1 + ${jackson.version} io.minio @@ -910,7 +953,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.9.0 true central diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy index 8fc940ac528..7c0c52d9bc1 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy @@ -1,6 +1,7 @@ package com.netgrif.application.engine.migration - +import com.netgrif.application.engine.configuration.ApplicationShutdownProvider +import com.netgrif.application.engine.configuration.properties.MigrationProperties import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.startup.AbstractOrderedCommandLineRunner import org.slf4j.Logger @@ -14,26 +15,55 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL private static final Logger log = LoggerFactory.getLogger(MigrationOrderedCommandLineRunner) private String title = this.class.simpleName + private boolean shutdownAfterFinish = false @Autowired private MigrationRepository repository + @Autowired + private MigrationProperties migrationProperties + @Autowired private IPetriNetService service + @Autowired + private ApplicationShutdownProvider shutdownProvider + @Override void run(String... strings) throws Exception { if (repository.existsByTitle(title)) { log.info("Migration ${title} was already applied") return } + if (migrationProperties.getSkip().contains(title)) { + log.info("Migration ${title} is skipped according to property nae.migration.skip") + return + } log.info("Applying migration ${title}") migrate() repository.save(new Migration(title)) - service.evictAllCaches() + if (migrationProperties.isEvictCaches()) { + log.info("Evicting all caches after migration") + service.evictAllCaches() + } log.info("Migration ${title} applied") + if (shutdownAfterFinish || migrationProperties.isShutdownAfterMigration()) { + // sleep is for elastic executor and other things to flush their work after migration. + // the number was chosen arbitrary by feeling 😅 + sleep(333) + shutdownProvider.shutdown(this.class) + } + } + + protected void enableShutdownAfterFinish() { + this.shutdownAfterFinish = true; + } + + protected void disableShutdownAfterFinish() { + this.shutdownAfterFinish = false; } abstract void migrate() -} \ No newline at end of file + +} diff --git a/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy b/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy index f25f942344d..14c9c83b234 100644 --- a/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy +++ b/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy @@ -45,4 +45,4 @@ class RunnerController { } return runnerOrder } -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java b/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java index 2a3be9d5ed7..d9e68491cf2 100644 --- a/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java +++ b/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java @@ -53,6 +53,11 @@ public void addDefaultAuthorities(IUser user) { } } + @Override + public boolean existsById(String id) { + return repository.existsById(id); + } + @Override public IUser assignAuthority(String userId, String authorityId) { IUser user = resolveById(userId, true); diff --git a/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java b/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java index 01956bc2b8b..989062f7585 100644 --- a/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java +++ b/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java @@ -49,6 +49,8 @@ public interface IUserService { void addDefaultAuthorities(IUser user); + boolean existsById(String id); + IUser assignAuthority(String userId, String authorityId); IUser getLoggedOrSystem(); diff --git a/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java new file mode 100644 index 00000000000..db24485e5d9 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java @@ -0,0 +1,39 @@ +package com.netgrif.application.engine.configuration; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ApplicationShutdownProvider { + + private final ApplicationContext applicationContext; + private final TaskExecutor taskExecutor; + + public void shutdown(Class calledBy, int exitCode) { + String className = calledBy == null ? "unknown" : calledBy.getSimpleName(); + log.info("Application was signalled by {} to shutdown; exit code: {}", className, exitCode); + if (taskExecutor != null && taskExecutor instanceof ThreadPoolTaskExecutor) { + log.info("Shutting down thread pool executor"); + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) taskExecutor; + executor.shutdown(); + } + int ec = SpringApplication.exit(applicationContext, () -> exitCode); + System.exit(ec); + } + + public void shutdown(Class calledBy) { + shutdown(calledBy, 0); + } + + public void shutdown() { + shutdown(null, 0); + } + +} diff --git a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java index dbd923e5a51..3c1e253c8ce 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java @@ -1,9 +1,16 @@ package com.netgrif.application.engine.configuration; +import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; import com.netgrif.application.engine.configuration.properties.UriProperties; import com.netgrif.application.engine.workflow.service.CaseEventHandler; +import lombok.RequiredArgsConstructor; import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -12,14 +19,9 @@ import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; @Configuration +@RequiredArgsConstructor public class ElasticsearchConfiguration { - @Value("${spring.data.elasticsearch.url}") - private String url; - - @Value("${spring.data.elasticsearch.searchport}") - private int port; - @Value("${spring.data.elasticsearch.index.petriNet}") private String petriNetIndex; @@ -32,11 +34,9 @@ public class ElasticsearchConfiguration { @Value("${spring.data.elasticsearch.reindex}") private String cron; - private final UriProperties uriProperties; + private final ElasticsearchProperties elasticsearchProperties; - public ElasticsearchConfiguration(UriProperties uriProperties) { - this.uriProperties = uriProperties; - } + private final UriProperties uriProperties; @Bean public String springElasticsearchReindex() { @@ -65,9 +65,18 @@ public String elasticUriIndex() { @Bean public RestHighLevelClient client() { - - return new RestHighLevelClient( - RestClient.builder(new HttpHost(url, port, "http"))); + RestClientBuilder builder = RestClient.builder(new HttpHost(elasticsearchProperties.getUrl(), elasticsearchProperties.getSearchPort())); + if (hasCredentials()) { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials( + elasticsearchProperties.getUsername(), + elasticsearchProperties.getPassword() + ) + ); + builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + } + return new RestHighLevelClient(builder); } @Bean @@ -79,4 +88,9 @@ public ElasticsearchOperations elasticsearchTemplate() { public CaseEventHandler caseEventHandler() { return new CaseEventHandler(); } + + private boolean hasCredentials() { + return elasticsearchProperties.getUsername() != null && !elasticsearchProperties.getUsername().isBlank() && + elasticsearchProperties.getPassword() != null && !elasticsearchProperties.getPassword().isBlank(); + } } diff --git a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java index afe31ba8f4e..5f06dc63296 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java @@ -1,16 +1,25 @@ package com.netgrif.application.engine.configuration; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.session.web.http.HeaderHttpSessionIdResolver; import org.springframework.session.web.http.HttpSessionIdResolver; +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j @Configuration @EnableRedisHttpSession(redisNamespace = "spring:session:${spring.session.redis.namespace}") @ConditionalOnProperty( @@ -22,8 +31,8 @@ public class SessionConfiguration { @Value("${spring.session.redis.host}") private String hostName; - @Value("${spring.session.redis.port}") - private Integer port; + @Value("${spring.session.redis.port:6379}") + private Integer port = 6379; @Value("${spring.session.redis.username:#{null}}") private String username; @@ -31,17 +40,97 @@ public class SessionConfiguration { @Value("${spring.session.redis.password:#{null}}") private String password; + @Value("${spring.session.redis.ssl:false}") + private Boolean ssl; + + @Value("${spring.session.redis.connection-timeout:2}") + private Long connectionTimeout; // duration in seconds + + @Value("${spring.session.redis.read-timeout:2}") + private Long readTimeout; // duration in seconds + + @Value("${spring.redis.sentinel.master:#{null}}") + private String sentinelMasterName; + + @Value("${spring.redis.sentinel.nodes:#{null}}") + private List sentinelNodes; + + @Value("${spring.redis.sentinel.port:26379}") + private Integer sentinelPort = 26379; + + @Value("${spring.redis.sentinel.username:#{null}}") + private String sentinelUsername; + + @Value("${spring.redis.sentinel.password:#{null}}") + private String sentinelPassword; @Bean public JedisConnectionFactory jedisConnectionFactory() { - hostName = hostName == null ? "localhost" : hostName; - port = port == null || port == 0 ? 6379 : port; + if (sentinelMasterName != null && !sentinelMasterName.isEmpty()) { + return redisSentinelConfiguration(); + } else { + return standaloneRedisConfiguration(); + } + } + + protected JedisConnectionFactory standaloneRedisConfiguration() { + String hostName = this.hostName == null ? "localhost" : this.hostName; + int port = this.port == 0 ? 6379 : this.port; RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(hostName, port); - if(username != null && password !=null && !username.isEmpty() && !password.isEmpty()){ + if (hasCredentials(username, password)) { redisStandaloneConfiguration.setUsername(username); redisStandaloneConfiguration.setPassword(password); } - return new JedisConnectionFactory(redisStandaloneConfiguration); + JedisClientConfiguration clientConfiguration = jedisClientConfiguration(); + log.debug("Redis standalone configuration: host: {}; port: {}; username: {}", hostName, port, username); + return new JedisConnectionFactory(redisStandaloneConfiguration, clientConfiguration); + } + + protected JedisConnectionFactory redisSentinelConfiguration() { + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration(); + sentinelConfiguration.setMaster(sentinelMasterName); + List nodes = sentinelNodes.stream().map(node -> { + try { + return RedisNode.fromString(node); + } catch (Exception e) { + log.warn("Parsing redis sentinel node {} has failed. Trying to use the value as an address without port and adding default sentinel port {}", node, sentinelPort, e); + return new RedisNode(node, sentinelPort); + } + }).collect(Collectors.toList()); + sentinelConfiguration.setSentinels(nodes); + + if (hasCredentials(username, password)) { + sentinelConfiguration.setUsername(username); + sentinelConfiguration.setPassword(password); + } + if (hasCredentials(sentinelUsername, sentinelPassword)) { + sentinelConfiguration.setSentinelUsername(sentinelUsername); + sentinelConfiguration.setSentinelPassword(sentinelPassword); + } + + JedisClientConfiguration clientConfiguration = jedisClientConfiguration(); + log.debug("Redis sentinel configuration: master: {}; nodes: {}; username: {}", sentinelMasterName, sentinelNodes, username); + return new JedisConnectionFactory(sentinelConfiguration, clientConfiguration); + } + + protected JedisClientConfiguration jedisClientConfiguration() { + JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder(); + if (connectionTimeout != null && connectionTimeout != 0) { + builder = builder.connectTimeout(Duration.ofSeconds(connectionTimeout)); + } + if (readTimeout != null && readTimeout != 0) { + builder = builder.readTimeout(Duration.ofSeconds(readTimeout)); + } + if (ssl) { + builder = builder.useSsl().and(); + } + log.debug("Redis client configuration: connection timeout:{}; read timeout:{}; ssl:{}", connectionTimeout, readTimeout, ssl); + return builder.build(); + } + + private boolean hasCredentials(String username, String password) { + return username != null && !username.isBlank() && + password != null && !password.isBlank(); } @Bean @@ -54,4 +143,4 @@ public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java index 4f77f278cb8..f3b1269b094 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java @@ -6,6 +6,8 @@ import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +import javax.validation.Valid; +import javax.validation.constraints.Min; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -33,6 +35,10 @@ public class ElasticsearchProperties { private String url; + private String username; + + private String password; + private Map index; private boolean analyzerEnabled = false; @@ -53,6 +59,9 @@ public class ElasticsearchProperties { private List defaultSearchFilters = new ArrayList<>(); + @Valid + private BatchProperties batch = new BatchProperties(); + @PostConstruct public void init() { indexSettings.putIfAbsent("max_result_window", 10000000); @@ -72,4 +81,13 @@ public void init() { public Map getClassSpecificSettings(String className) { return classSpecificIndexSettings.getOrDefault(className, new HashMap<>()); } + + @Data + public static class BatchProperties { + @Min(1) + private int caseBatchSize = 5000; + + @Min(1) + private int taskBatchSize = 20000; + } } diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java new file mode 100644 index 00000000000..aeba1845e06 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java @@ -0,0 +1,39 @@ +package com.netgrif.application.engine.configuration.properties; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +@Configuration +@ConfigurationProperties(prefix = "nae.migration") +public class MigrationProperties { + + /** + * A list of migration process identifiers or names that should be skipped when applying migration logic. + * This property allows you to configure specific migrations that should be ignored, + * typically useful for excluding unnecessary or problematic migrations. + */ + private Set skip = new LinkedHashSet<>(); + + /** + * Indicates whether caches should be evicted as part of the migration process. + * This property allows enabling or disabling the cache eviction mechanism, which + * is useful in ensuring consistency and up-to-date data during migration operations. + * Default value is {@code true}. + */ + private boolean evictCaches = true; + + /** + * Specifies whether the application should automatically shut down once the migration process is completed. + * This property can be used to terminate the application after the migration, ensuring a clean exit + * if no further operations are intended post-migration. + * Default value is {@code false}. + */ + private boolean shutdownAfterMigration = false; + +} diff --git a/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java index b31b45a1816..294c7915cf6 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java @@ -38,6 +38,9 @@ public class QuartzConfiguration { @Value("${nae.quartz.dbName:nae}") private String db; + @Value("${nae.quartz.mongoOptionWriteConcernW:majority}") + private String mongoOptionWriteConcernW; + @Bean public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); @@ -74,6 +77,7 @@ public SchedulerFactoryBean schedulerFactoryBean() throws Exception { properties.setProperty("org.quartz.jobStore.mongoUri", uri); } properties.setProperty("org.quartz.jobStore.dbName", db); + properties.setProperty("org.quartz.jobStore.mongoOptionWriteConcernW", mongoOptionWriteConcernW); properties.setProperty("org.quartz.jobStore.class", "com.netgrif.quartz.mongodb.MongoDBJobStore"); properties.setProperty("spring.quartz.properties.org.quartz.jobStore.isClustered", "false"); properties.setProperty("org.quartz.jobStore.isClustered", "true"); diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java new file mode 100644 index 00000000000..e661a947d83 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java @@ -0,0 +1,24 @@ +package com.netgrif.application.engine.elastic.domain; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.elasticsearch.annotations.Field; + +import java.util.List; + +import static org.springframework.data.elasticsearch.annotations.FieldType.*; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CaseField extends DataField { + + @Field(type = Text) + private List caseValue; + + public CaseField(List value) { + super(value.toString()); + this.caseValue = value; + } +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java index 1a13ad93484..630ca0c4030 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java @@ -1,9 +1,5 @@ package com.netgrif.application.engine.elastic.domain; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.TaskPair; import lombok.AllArgsConstructor; @@ -18,6 +14,7 @@ import java.sql.Timestamp; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -43,6 +40,8 @@ public class ElasticCase { private Long lastModified; + private Long lastModifiedDataSet; + @Field(type = Keyword) private String stringId; @@ -56,8 +55,6 @@ public class ElasticCase { private String title; - @JsonSerialize(using = LocalDateTimeSerializer.class) - @JsonDeserialize(using = LocalDateTimeDeserializer.class) @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second_millis) private LocalDateTime creationDate; @@ -117,11 +114,14 @@ public ElasticCase(Case useCase) { uriNodeId = useCase.getUriNodeId(); mongoId = useCase.getStringId(); //TODO: Duplication lastModified = Timestamp.valueOf(useCase.getLastModified()).getTime(); + if (useCase.getLastModifiedDataSet() != null) { + lastModifiedDataSet = Timestamp.valueOf(useCase.getLastModifiedDataSet()).getTime(); + } processIdentifier = useCase.getProcessIdentifier(); processId = useCase.getPetriNetId(); visualId = useCase.getVisualId(); title = useCase.getTitle(); - creationDate = useCase.getCreationDate(); + creationDate = useCase.getCreationDate().truncatedTo(ChronoUnit.MILLIS); creationDateSortable = Timestamp.valueOf(useCase.getCreationDate()).getTime(); author = useCase.getAuthor().getId(); authorName = useCase.getAuthor().getFullName(); @@ -142,6 +142,9 @@ public ElasticCase(Case useCase) { public void update(ElasticCase useCase) { version++; lastModified = useCase.getLastModified(); + if (useCase.getLastModifiedDataSet() != null) { + lastModifiedDataSet = useCase.getLastModifiedDataSet(); + } if (useCase.getUriNodeId() != null) { uriNodeId = useCase.getUriNodeId(); } diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java index 9eac5ee86b5..e0618f76e2b 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java @@ -64,8 +64,6 @@ public class ElasticTask { private String userId; - @JsonSerialize(using = LocalDateTimeSerializer.class) - @JsonDeserialize(using = LocalDateTimeDeserializer.class) @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second_millis) private LocalDateTime startDate; diff --git a/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java new file mode 100644 index 00000000000..1d7b2e8c333 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java @@ -0,0 +1,29 @@ +package com.netgrif.application.engine.elastic.serializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; + +public class LocalDateTimeJsonDeserializer extends JsonDeserializer { + private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.MILLI_OF_SECOND, 1, 3, true) + .optionalEnd() + .toFormatter(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String value = p.getValueAsString(); + if (value == null || value.isEmpty()) { + return null; + } + return LocalDateTime.parse(value, FORMATTER); + } +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonSerializer.java b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonSerializer.java new file mode 100644 index 00000000000..a500adb5268 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonSerializer.java @@ -0,0 +1,18 @@ +package com.netgrif.application.engine.elastic.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeJsonSerializer extends JsonSerializer { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(FORMATTER.format(value)); + } +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java index 18666dfb5f0..11286761dae 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java @@ -3,6 +3,7 @@ import com.netgrif.application.engine.elastic.domain.BooleanField; import com.netgrif.application.engine.elastic.domain.ButtonField; +import com.netgrif.application.engine.elastic.domain.CaseField; import com.netgrif.application.engine.elastic.domain.DateField; import com.netgrif.application.engine.elastic.domain.FileField; import com.netgrif.application.engine.elastic.domain.I18nField; @@ -77,6 +78,8 @@ protected Optional transformDataField(String fieldId, Case useCase) { return this.transformFileListField(caseField); } else if (netField instanceof com.netgrif.application.engine.petrinet.domain.dataset.UserListField) { return this.transformUserListField(caseField); + } else if (netField instanceof com.netgrif.application.engine.petrinet.domain.dataset.CaseField) { + return this.transformCaseField(caseField); } else if (netField instanceof com.netgrif.application.engine.petrinet.domain.dataset.I18nField) { return this.transformI18nField(caseField, (com.netgrif.application.engine.petrinet.domain.dataset.I18nField) netField); } else { @@ -227,11 +230,11 @@ private StringBuilder buildFullName(String name, String surname) { protected Optional transformDateField(com.netgrif.application.engine.workflow.domain.DataField dateField, com.netgrif.application.engine.petrinet.domain.dataset.DateField netField) { if (dateField.getValue() instanceof LocalDate) { LocalDate date = (LocalDate) dateField.getValue(); - return formatDateField(LocalDateTime.of(date, LocalTime.NOON)); + return formatDateField(LocalDateTime.of(date, LocalTime.MIDNIGHT)); } else if (dateField.getValue() instanceof Date) { // log.warn(String.format("DateFields should have LocalDate values! DateField (%s) with Date value found! Value will be converted for indexation.", netField.getImportId())); LocalDateTime transformed = this.transformDateValueField(dateField); - return formatDateField(LocalDateTime.of(transformed.toLocalDate(), LocalTime.NOON)); + return formatDateField(LocalDateTime.of(transformed.toLocalDate(), LocalTime.MIDNIGHT)); } else { // TODO throw error? log.error(String.format("Unsupported DateField value type (%s)! Skipping indexation...", dateField.getValue().getClass().getCanonicalName())); @@ -283,6 +286,10 @@ protected Optional transformFileListField(com.netgrif.application.eng return Optional.of(new FileField(((FileListFieldValue) fileListField.getValue()).getNamesPaths().toArray(new FileFieldValue[0]))); } + protected Optional transformCaseField(com.netgrif.application.engine.workflow.domain.DataField caseField) { + return Optional.of(new CaseField((List) caseField.getValue())); + } + protected Optional transformOtherFields(com.netgrif.application.engine.workflow.domain.DataField otherField, Field netField) { log.warn("Field of type " + netField.getClass().getCanonicalName() + " is not supported for indexation by default. Indexing the toString() representation of its value..."); return Optional.of(new TextField(otherField.getValue().toString())); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 4c692096cfd..b15a936daaa 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -173,15 +173,17 @@ public String findUriNodeId(Case aCase) { return elasticCase.getUriNodeId(); } - protected NativeSearchQuery buildQuery(List requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection) { - List singleQueries = requests.stream().map(request -> buildSingleQuery(request, user, locale)).collect(Collectors.toList()); + public NativeSearchQuery buildQuery(List requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection) { + List singleQueries = requests.stream() + .map(request -> buildSingleQuery(request, user, locale)) + .collect(Collectors.toList()); if (isIntersection && !singleQueries.stream().allMatch(Objects::nonNull)) { // one of the queries evaluates to empty set => the entire result is an empty set return null; } else if (!isIntersection) { singleQueries = singleQueries.stream().filter(Objects::nonNull).collect(Collectors.toList()); - if (singleQueries.size() == 0) { + if (singleQueries.isEmpty()) { // all queries result in an empty set => the entire result is an empty set return null; } @@ -215,10 +217,7 @@ protected BoolQueryBuilder buildSingleQuery(CaseSearchRequest request, LoggedUse // TODO: filtered query https://stackoverflow.com/questions/28116404/filtered-query-using-nativesearchquerybuilder-in-spring-data-elasticsearch - if (resultAlwaysEmpty) - return null; - else - return query; + return resultAlwaysEmpty ? null : query; } protected void buildPetriNetQuery(CaseSearchRequest request, LoggedUser user, BoolQueryBuilder query) { @@ -226,17 +225,25 @@ protected void buildPetriNetQuery(CaseSearchRequest request, LoggedUser user, Bo return; } - BoolQueryBuilder petriNetQuery = boolQuery(); + Set identifiers = new HashSet<>(); + Set processIds = new HashSet<>(); - for (CaseSearchRequest.PetriNet process : request.process) { - if (process.identifier != null) { - petriNetQuery.should(termQuery("processIdentifier", process.identifier)); + request.process.forEach(p -> { + if (p.identifier != null) { + identifiers.add(p.identifier); } - if (process.processId != null) { - petriNetQuery.should(termQuery("processId", process.processId)); + if (p.processId != null) { + processIds.add(p.processId); } - } + }); + BoolQueryBuilder petriNetQuery = boolQuery(); + if (!identifiers.isEmpty()) { + petriNetQuery.should(termsQuery("processIdentifier", identifiers)); + } + if (!processIds.isEmpty()) { + petriNetQuery.should(termsQuery("processId", processIds)); + } query.filter(petriNetQuery); } @@ -338,13 +345,7 @@ protected void buildRoleQuery(CaseSearchRequest request, BoolQueryBuilder query) if (request.role == null || request.role.isEmpty()) { return; } - - BoolQueryBuilder roleQuery = boolQuery(); - for (String roleId : request.role) { - roleQuery.should(termQuery("enabledRoles", roleId)); - } - - query.filter(roleQuery); + query.filter(termsQuery("enabledRoles", request.role)); } /** @@ -434,20 +435,14 @@ protected void buildCaseIdQuery(CaseSearchRequest request, BoolQueryBuilder quer if (request.stringId == null || request.stringId.isEmpty()) { return; } - - BoolQueryBuilder caseIdQuery = boolQuery(); - request.stringId.forEach(caseId -> caseIdQuery.should(termQuery("stringId", caseId))); - query.filter(caseIdQuery); + query.filter(termsQuery("stringId", request.stringId)); } protected void buildUriNodeIdQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.uriNodeId == null || request.uriNodeId.isEmpty()) { return; } - - BoolQueryBuilder caseIdQuery = boolQuery(); - caseIdQuery.should(termQuery("uriNodeId", request.uriNodeId)); - query.filter(caseIdQuery); + query.filter(termQuery("uriNodeId", request.uriNodeId)); } /** @@ -476,14 +471,13 @@ protected boolean buildGroupQuery(CaseSearchRequest request, LoggedUser user, Lo PetriNetSearch processQuery = new PetriNetSearch(); processQuery.setGroup(request.group); List groupProcesses = this.petriNetService.search(processQuery, user, new FullPageRequest(), locale).getContent(); - if (groupProcesses.size() == 0) + if (groupProcesses.isEmpty()) { return true; - - BoolQueryBuilder groupQuery = boolQuery(); - groupProcesses.stream().map(PetriNetReference::getIdentifier) - .map(netIdentifier -> termQuery("processIdentifier", netIdentifier)) - .forEach(groupQuery::should); - query.filter(groupQuery); + } + List identifiers = groupProcesses.stream() + .map(PetriNetReference::getIdentifier) + .collect(Collectors.toList()); + query.filter(termsQuery("processIdentifier", identifiers)); return false; } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index e4ff155a89b..0b89b7da00e 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -2,20 +2,36 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; +import com.netgrif.application.engine.elastic.domain.ElasticCase; +import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; +import com.netgrif.application.engine.elastic.domain.ElasticTask; +import com.netgrif.application.engine.elastic.domain.ElasticTaskRepository; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonDeserializer; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonSerializer; import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.petrinet.service.PetriNetService; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.Task; import lombok.extern.slf4j.Slf4j; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; import org.elasticsearch.action.admin.indices.open.OpenIndexResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CloseIndexResponse; import org.elasticsearch.client.indices.PutIndexTemplateRequest; import org.elasticsearch.xcontent.XContentType; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Setting; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; @@ -24,14 +40,17 @@ import org.springframework.data.elasticsearch.core.SearchScrollHits; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.data.elasticsearch.core.query.IndexQuery; import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder; import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.util.CloseableIterator; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.io.InputStream; import java.lang.reflect.Field; +import java.time.LocalDateTime; import java.util.*; @Slf4j @@ -40,17 +59,55 @@ public class ElasticIndexService implements IElasticIndexService { private static final String PLACEHOLDERS = "petriNetIndex, caseIndex, taskIndex"; - @Autowired - private ApplicationContext context; + private final ApplicationContext context; - @Autowired - private ElasticsearchRestTemplate elasticsearchTemplate; + private final ElasticsearchRestTemplate elasticsearchTemplate; - @Autowired - private ElasticsearchOperations operations; + private final RestHighLevelClient elasticsearchClient; - @Autowired - private ElasticsearchProperties elasticsearchProperties; + private final ElasticsearchOperations operations; + + private final ElasticsearchProperties elasticsearchProperties; + + private final ElasticCaseRepository elasticCaseRepository; + + private final ElasticTaskRepository elasticTaskRepository; + + private final PetriNetService petriNetService; + + private final MongoTemplate mongoTemplate; + + private final ElasticCaseMappingService caseMappingService; + + private final ElasticTaskMappingService taskMappingService; + + private final ObjectMapper objectMapper; + + public ElasticIndexService(ApplicationContext context, + ElasticsearchRestTemplate elasticsearchTemplate, + RestHighLevelClient elasticsearchClient, + ElasticsearchOperations operations, + ElasticsearchProperties elasticsearchProperties, + ElasticCaseRepository elasticCaseRepository, + ElasticTaskRepository elasticTaskRepository, + PetriNetService petriNetService, + MongoTemplate mongoTemplate, + ElasticCaseMappingService caseMappingService, + ElasticTaskMappingService taskMappingService) { + this.context = context; + this.elasticsearchTemplate = elasticsearchTemplate; + this.elasticsearchClient = elasticsearchClient; + this.operations = operations; + this.elasticsearchProperties = elasticsearchProperties; + this.elasticCaseRepository = elasticCaseRepository; + this.elasticTaskRepository = elasticTaskRepository; + this.petriNetService = petriNetService; + this.mongoTemplate = mongoTemplate; + this.caseMappingService = caseMappingService; + this.taskMappingService = taskMappingService; + this.objectMapper = new ObjectMapper(); + configureMapper(); + } @Override public boolean indexExists(String indexName) { @@ -69,24 +126,6 @@ public String index(Class clazz, T source, String... placeholders) { .withObject(source).build(), IndexCoordinates.of(indexName)); } - - @Override - public boolean bulkIndex(List list, Class clazz, String... placeholders) { - String indexName = getIndexName(clazz, placeholders); - try { - if (list != null && !list.isEmpty()) { - List indexQueries = new ArrayList<>(); - list.forEach(source -> - indexQueries.add(new IndexQueryBuilder().withId(getIdFromSource(source)).withObject(source).build())); - elasticsearchTemplate.bulkIndex(indexQueries, IndexCoordinates.of(indexName)); - } - } catch (Exception e) { - log.error("bulkIndex:", e); - return false; - } - return true; - } - @Override public boolean createIndex(Class clazz, String... placeholders) { try { @@ -304,6 +343,248 @@ public void clearScrollHits(List scrollIds) { } } + + /** + * Performs bulk indexing of cases and tasks into Elasticsearch. + * + * @param indexAll if true, indexes all cases and tasks, regardless of modification time + * @param after the time after which cases and tasks should be considered for reindexing + * @param caseBatchSize number of cases to process per batch. If null, defaults from Elasticsearch properties + * @param taskBatchSize number of tasks to process per batch. If null, defaults from Elasticsearch properties + */ + @Override + public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSize, Integer taskBatchSize) { + log.info("Reindexing stale cases: started reindexing after {}", after); + LocalDateTime now = LocalDateTime.now(); + + if (caseBatchSize == null) { + caseBatchSize = elasticsearchProperties.getBatch().getCaseBatchSize(); + } + if (taskBatchSize == null) { + taskBatchSize = elasticsearchProperties.getBatch().getTaskBatchSize(); + } + + org.springframework.data.mongodb.core.query.Query query; + if (indexAll || after == null) { + query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now)); + log.info("Reindexing stale cases: force all"); + } else { + query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now).gt(after.minusMinutes(2))); + } + + long count = mongoTemplate.count(query, Case.class); + if (count > 0) { + reindexQueried(query, count, caseBatchSize, taskBatchSize); + } + log.info("Reindexing stale cases: end"); + } + + /** + * Reindexes queried cases and tasks into Elasticsearch in batches. + * + * @param count total number of cases to reindex + * @param caseBatchSize batch size for cases + * @param taskBatchSize batch size for tasks + */ + private void reindexQueried(org.springframework.data.mongodb.core.query.Query query, long count, int caseBatchSize, int taskBatchSize) { + long numOfPages = ((count / caseBatchSize) + 1); + log.info("Reindexing {} pages", numOfPages); + + query.cursorBatchSize(caseBatchSize); + long page = 1, currentBatchSize = 0; + List caseOperations = new ArrayList<>(); + List caseIds = new ArrayList<>(); + + try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { + while (cursor.hasNext()) { + Case aCase = cursor.next(); + prepareCase(aCase); + ElasticCase elasticCase = caseMappingService.transform(aCase); + ElasticCase existingCase = null; + try { + existingCase = elasticCaseRepository.findByStringId(aCase.getStringId()); + } catch (InvalidDataAccessApiUsageException ignored) { + log.debug("[{}]: Case \"{}\" has duplicates, will reindex.", aCase.getStringId(), aCase.getTitle()); + elasticCaseRepository.deleteAllByStringId(aCase.getStringId()); + } + if (existingCase == null) { + existingCase = elasticCase; + } else { + existingCase.update(elasticCase); + } + prepareCaseBulkOperation(existingCase, caseOperations); + caseIds.add(aCase.getStringId()); + + if (++currentBatchSize == caseBatchSize || !cursor.hasNext()) { + log.info("Reindexing case page {} / {}", page, numOfPages); + executeAndValidate(caseOperations); + bulkIndexTasks(caseIds, taskBatchSize); + caseOperations.clear(); + caseIds.clear(); + currentBatchSize = 0; + page++; + } + } + } + } + + /** + * Reindexes tasks into Elasticsearch in batches corresponding to the provided case IDs. + * + * @param caseIds list of case IDs whose tasks need to be reindexed + * @param taskBatchSize size of the batch for tasks + */ + private void bulkIndexTasks(List caseIds, int taskBatchSize) { + if (caseIds == null || caseIds.isEmpty()) { + return; + } + org.springframework.data.mongodb.core.query.Query query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("caseId").in(caseIds)).cursorBatchSize(taskBatchSize); + long totalSize = mongoTemplate.count(query, Task.class); + long numOfPages = ((totalSize / taskBatchSize) + 1); + + long page = 1, currentBatchSize = 0; + List taskOperations = new ArrayList<>(); + + try (CloseableIterator cursor = mongoTemplate.stream(query, Task.class)) { + while (cursor.hasNext()) { + Task task = cursor.next(); + ElasticTask elasticTask = taskMappingService.transform(task); + ElasticTask existingTask = null; + try { + existingTask = elasticTaskRepository.findByStringId(task.getStringId()); + } catch (InvalidDataAccessApiUsageException ignored) { + log.debug("[{}]: Task \"{}\" has duplicates, will reindex.", task.getStringId(), task.getTitle()); + elasticTaskRepository.deleteAllByStringId(task.getStringId()); + } + if (existingTask == null) { + existingTask = elasticTask; + } else { + existingTask.update(elasticTask); + } + prepareTaskBulkOperation(existingTask, taskOperations); + + if (++currentBatchSize == taskBatchSize || !cursor.hasNext()) { + log.info("Reindexing task page {} / {}", page, numOfPages); + executeAndValidate(taskOperations); + taskOperations.clear(); + currentBatchSize = 0; + page++; + } + } + } + } + + /** + * Prepares the case object by ensuring necessary dependencies and last modified timestamp are set. + * + * @param useCase case object to prepare + */ + private void prepareCase(Case useCase) { + if (useCase.getPetriNet() == null) { + useCase.setPetriNet(petriNetService.get(useCase.getPetriNetObjectId())); + } + if (useCase.getLastModified() == null) { + useCase.setLastModified(LocalDateTime.now()); + } + } + + /** + * Prepares a bulk operation for indexing or updating a case in Elasticsearch. + * + * @param doc transformed ElasticCase object + * @param operations collection of BulkOperations to add this operation to + */ + private void prepareCaseBulkOperation(ElasticCase doc, List operations) { + try { + String json = objectMapper.writeValueAsString(doc); + UpdateRequest updateRequest = new UpdateRequest() + .id(doc.getId() == null ? doc.getStringId() : doc.getId()) + .doc(json, XContentType.JSON) + .upsert(json, XContentType.JSON) + .index(elasticsearchProperties.getIndex().get("case")); + operations.add(updateRequest); + } catch (Exception e) { + log.error("Failed to prepare bulk operation for case [{}]: {}", doc.getStringId(), e.getMessage()); + } + } + + /** + * Prepares a bulk operation for indexing or updating a task in Elasticsearch. + * + * @param doc transformed ElasticTask object + * @param operations collection of BulkOperations to add this operation to + */ + private void prepareTaskBulkOperation(ElasticTask doc, List operations) { + try { + String json = objectMapper.writeValueAsString(doc); + UpdateRequest updateRequest = new UpdateRequest() + .id(doc.getId() == null ? doc.getStringId() : doc.getId()) + .doc(json, XContentType.JSON) + .upsert(json, XContentType.JSON) + .index(elasticsearchProperties.getIndex().get("task")); + operations.add(updateRequest); + } catch (Exception e) { + log.error("Failed to prepare bulk operation for task [{}]: {}", doc.getStringId(), e.getMessage()); + } + } + + /** + * Executes the bulk operations and validates the results, retrying on partial failures. + * + * @param operations list of bulk operations to execute + */ + private void executeAndValidate(List operations) { + if (operations.isEmpty()) { + return; + } + + BulkRequest request = new BulkRequest(); + operations.forEach(request::add); + + try { + BulkResponse response = elasticsearchClient.bulk(request, RequestOptions.DEFAULT); + checkForBulkUpdateFailure(response); + log.info("Batch indexed successfully with {} ops", operations.size()); + } catch (ElasticsearchException e) { + log.warn("Failed for {} ops to index bulk {}", operations.size(), e.getMessage(), e); + + if (operations.size() == 1) { + log.error("Single operation failed. Skipping. {}", operations.get(0), e); + return; + } + + log.warn("Dividing the requirement."); + + int mid = operations.size() / 2; + List left = operations.subList(0, mid); + List right = operations.subList(mid, operations.size()); + + executeAndValidate(new ArrayList<>(left)); + executeAndValidate(new ArrayList<>(right)); + } catch (Exception e) { + log.error("Failed to index bulk: {}", e.getMessage(), e); + } + } + + /** + * Checks the results of a bulk indexing operation for failures. + * + * @param response the BulkResponse from Elasticsearch + * @throws ElasticsearchException if there are failures in the bulk response + */ + private void checkForBulkUpdateFailure(BulkResponse response) { + Map failedDocuments = new HashMap<>(); + Arrays.stream(response.getItems()).forEach(item -> { + if (item.getFailure() != null) { + failedDocuments.put(item.getId(), item.getFailure().getMessage()); + } + }); + + if (!failedDocuments.isEmpty()) { + throw new ElasticsearchException("Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for details [{}]", failedDocuments); + } + } + private String getIdFromSource(Object source) { if (source == null) { return null; @@ -377,4 +658,11 @@ private String getIndexName(Class clazz, String... placeholders) { return indexName; } + private void configureMapper() { + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeJsonSerializer()); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeJsonDeserializer()); + objectMapper.registerModule(javaTimeModule); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java index b3c707b6403..cc736dd3480 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java @@ -8,11 +8,13 @@ public abstract class ElasticViewPermissionService { protected void buildViewPermissionQuery(BoolQueryBuilder query, LoggedUser user) { - BoolQueryBuilder viewPermsExists = boolQuery(); - BoolQueryBuilder viewPermNotExists = boolQuery(); - viewPermsExists.should(existsQuery("viewRoles")); - viewPermsExists.should(existsQuery("viewUserRefs")); - viewPermNotExists.mustNot(viewPermsExists); + // Check if viewRoles or viewUserRefs exist + BoolQueryBuilder viewPermsExists = boolQuery() + .should(existsQuery("viewRoles")) + .should(existsQuery("viewUserRefs")); + // Condition where these attributes do NOT exist + BoolQueryBuilder viewPermNotExists = boolQuery() + .mustNot(viewPermsExists); /* Build positive view role query */ BoolQueryBuilder positiveViewRole = buildPositiveViewRoleQuery(viewPermNotExists, user); @@ -38,42 +40,45 @@ protected void buildViewPermissionQuery(BoolQueryBuilder query, LoggedUser user) query.filter(permissionQuery); } + /** + * Build a positive view role query using termsQuery for efficiency. + * This reduces the number of clauses by sending all roles at once. + */ private BoolQueryBuilder buildPositiveViewRoleQuery(BoolQueryBuilder viewPermNotExists, LoggedUser user) { BoolQueryBuilder positiveViewRole = boolQuery(); - BoolQueryBuilder positiveViewRoleQuery = boolQuery(); - for (String roleId : user.getProcessRoles()) { - positiveViewRoleQuery.should(termQuery("viewRoles", roleId)); + if (!user.getProcessRoles().isEmpty()) { + positiveViewRole.should(termsQuery("viewRoles", user.getProcessRoles())); } positiveViewRole.should(viewPermNotExists); - positiveViewRole.should(positiveViewRoleQuery); return positiveViewRole; } + /** + * Build a negative view role query by excluding negative roles. + */ private BoolQueryBuilder buildNegativeViewRoleQuery(LoggedUser user) { BoolQueryBuilder negativeViewRole = boolQuery(); - BoolQueryBuilder negativeViewRoleQuery = boolQuery(); - for (String roleId : user.getProcessRoles()) { - negativeViewRoleQuery.should(termQuery("negativeViewRoles", roleId)); + if (!user.getProcessRoles().isEmpty()) { + negativeViewRole.mustNot(termsQuery("negativeViewRoles", user.getProcessRoles())); } - negativeViewRole.mustNot(negativeViewRoleQuery); return negativeViewRole; } + /** + * Build a positive view user query using filter (as score is not needed). + */ private BoolQueryBuilder buildPositiveViewUser(BoolQueryBuilder viewPermNotExists, LoggedUser user) { - BoolQueryBuilder positiveViewUser = boolQuery(); - BoolQueryBuilder positiveViewUserQuery = boolQuery(); - positiveViewUserQuery.must(termQuery("viewUsers", user.getId())); - positiveViewUser.should(viewPermNotExists); - positiveViewUser.should(positiveViewUserQuery); - return positiveViewUser; + return boolQuery() + .should(viewPermNotExists) + .filter(termQuery("viewUsers", user.getId())); } + /** + * Build a negative view user query to exclude the specified user. + */ private BoolQueryBuilder buildNegativeViewUser(LoggedUser user) { - BoolQueryBuilder negativeViewUser = boolQuery(); - BoolQueryBuilder negativeViewUserQuery = boolQuery(); - negativeViewUserQuery.should(termQuery("negativeViewUsers", user.getId())); - negativeViewUser.mustNot(negativeViewUserQuery); - return negativeViewUser; + return boolQuery() + .mustNot(termQuery("negativeViewUsers", user.getId())); } private BoolQueryBuilder setMinus(BoolQueryBuilder positiveSet, BoolQueryBuilder negativeSet) { @@ -83,10 +88,13 @@ private BoolQueryBuilder setMinus(BoolQueryBuilder positiveSet, BoolQueryBuilder return positiveSetMinusNegativeSet; } + /** + * Unions two queries using OR with a minimum_should_match of 1. + */ private BoolQueryBuilder union(BoolQueryBuilder setA, BoolQueryBuilder setB) { - BoolQueryBuilder unionSet = boolQuery(); - unionSet.should(setA); - unionSet.should(setB); - return unionSet; + return boolQuery() + .should(setA) + .should(setB) + .minimumShouldMatch(1); } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java new file mode 100644 index 00000000000..0a46ed17539 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java @@ -0,0 +1,98 @@ +package com.netgrif.application.engine.elastic.service; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + + +/** + * The ElasticsearchQuerySanitizer class is responsible for sanitizing Elasticsearch queries + * by escaping or removing reserved characters and keywords. This is essential to ensure proper + * handling of Elasticsearch queries and to prevent syntax issues caused by special characters or + * reserved words. + *

+ * This class provides utility methods to sanitize query strings by escaping predefined reserved + * characters, removing certain reserved characters, and excluding specific keywords if provided. + * The reserved characters and keywords are predefined and managed internally. + */ +@Slf4j +public class ElasticsearchQuerySanitizer { + + public static final String[] RESERVED_CHARACTERS_TO_ESCAPE = {"\\", "+", "-", "=", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~", "*", "?", ":", "/", "AND", "OR", "NOT", " "}; + public static final String[] RESERVED_CHARACTERS_TO_REMOVE = {">", "<"}; + public static final Map RESERVED_KEYWORDS = prepareReservedKeywords(); + + /** + * Sanitizes the provided Elasticsearch query string by escaping or removing certain reserved + * characters and excluding specific keywords if applicable. + *

+ * This method applies default sanitization rules and does not consider keyword exclusions. + * + * @param query the Elasticsearch query string to sanitize, such as a search query or filter. + * It must not be null to ensure proper sanitization. + * @return the sanitized query string with reserved characters handled appropriately. + * If the input is empty or null, the behavior depends on the implemented sanitization logic. + */ + public static String sanitize(String query) { + return sanitize(query, null); + } + + /** + * Sanitizes the given query string by replacing reserved keywords with their sanitized equivalents, + * excluding the specified keywords from sanitization. + * + * @param query the query string to sanitize, which may contain reserved characters and keywords. + * This string must not be null. + * @param exclude an array of keywords to exclude from sanitization. If null or empty, all reserved + * keywords will be considered for sanitization. + * @return the sanitized query string with reserved keywords appropriately replaced, and excluded + * keywords untouched. + */ + public static String sanitize(String query, String[] exclude) { + if (query == null || query.isBlank()) { + return query; + } + Map keywordsToEscape = excludeKeywords(exclude); + String sanitized = StringUtils.replaceEach(query, + keywordsToEscape.keySet().toArray(new String[0]), + keywordsToEscape.values().toArray(new String[0])); + log.trace("Sanitized query: {}", sanitized); + return sanitized; + } + + protected static Map prepareReservedKeywords() { + Map result = new HashMap<>(); + for (String reservedString : RESERVED_CHARACTERS_TO_ESCAPE) { + String escaped = Arrays.stream(reservedString.split("")) + .map(c -> "\\" + c) + .collect(Collectors.joining("")); + result.put(reservedString, escaped); + } + for (String reservedString : RESERVED_CHARACTERS_TO_REMOVE) { + result.put(reservedString, "\\ "); + } + + return Collections.unmodifiableMap(result); + } + + protected static Map excludeKeywords(String[] exclude) { + if (exclude == null || exclude.length == 0) { + return RESERVED_KEYWORDS; + } + Map keywordsToEscape = new HashMap<>(RESERVED_KEYWORDS); + for (String toExclude : exclude) { + if (RESERVED_KEYWORDS.containsKey(toExclude)) { + keywordsToEscape.remove(toExclude); + } + } + return Collections.unmodifiableMap(keywordsToEscape); + } + + +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index ae21422527c..3b9426c74fc 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -1,18 +1,13 @@ package com.netgrif.application.engine.elastic.service; import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseMappingService; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskMappingService; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; +import com.netgrif.application.engine.elastic.service.interfaces.*; import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.QCase; import com.netgrif.application.engine.workflow.domain.Task; import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.BooleanExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -36,7 +31,6 @@ public class ReindexingTask { private static final Logger log = LoggerFactory.getLogger(ReindexingTask.class); private int pageSize; - private CaseRepository caseRepository; private TaskRepository taskRepository; private ElasticCaseRepository elasticCaseRepository; private IElasticCaseService elasticCaseService; @@ -44,12 +38,12 @@ public class ReindexingTask { private IElasticCaseMappingService caseMappingService; private IElasticTaskMappingService taskMappingService; private IWorkflowService workflowService; + private IElasticIndexService elasticIndexService; private LocalDateTime lastRun; @Autowired public ReindexingTask( - CaseRepository caseRepository, TaskRepository taskRepository, ElasticCaseRepository elasticCaseRepository, @Qualifier("reindexingTaskElasticCaseService") @@ -60,8 +54,8 @@ public ReindexingTask( IElasticTaskMappingService taskMappingService, IWorkflowService workflowService, @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") int pageSize, - @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from) { - this.caseRepository = caseRepository; + @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from, + IElasticIndexService elasticIndexService) { this.taskRepository = taskRepository; this.elasticCaseRepository = elasticCaseRepository; this.elasticCaseService = elasticCaseService; @@ -69,6 +63,7 @@ public ReindexingTask( this.caseMappingService = caseMappingService; this.taskMappingService = taskMappingService; this.workflowService = workflowService; + this.elasticIndexService = elasticIndexService; this.pageSize = pageSize; lastRun = LocalDateTime.now(); @@ -80,27 +75,11 @@ public ReindexingTask( @Scheduled(cron = "#{springElasticsearchReindex}") public void reindex() { log.info("Reindexing stale cases: started reindexing after " + lastRun); - - BooleanExpression predicate = QCase.case$.lastModified.before(LocalDateTime.now()).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); - + elasticIndexService.bulkIndex(false, lastRun, null, null); lastRun = LocalDateTime.now(); - long count = caseRepository.count(predicate); - if (count > 0) { - reindexAllPages(predicate, count); - } - log.info("Reindexing stale cases: end"); } - private void reindexAllPages(BooleanExpression predicate, long count) { - long numOfPages = ((count / pageSize) + 1); - log.info("Reindexing " + numOfPages + " pages"); - - for (int page = 0; page < numOfPages; page++) { - reindexPage(predicate, page, numOfPages, false); - } - } - public void forceReindexPage(Predicate predicate, int page, long numOfPages) { reindexPage(predicate, page, numOfPages, true); } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java index 024f945aec1..0632acbccc6 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java @@ -4,8 +4,11 @@ import com.netgrif.application.engine.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.workflow.domain.Case; +import org.elasticsearch.index.query.BoolQueryBuilder; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; import org.springframework.scheduling.annotation.Async; import java.util.List; @@ -27,4 +30,6 @@ public interface IElasticCaseService { void removeByPetriNetId(String processId); String findUriNodeId(Case aCase); + + NativeSearchQuery buildQuery(List requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection); } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java index 5660a6db477..f23036457b4 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java @@ -1,10 +1,12 @@ package com.netgrif.application.engine.elastic.service.interfaces; +import com.querydsl.core.types.Predicate; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchScrollHits; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.query.Query; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,8 +33,6 @@ public interface IElasticIndexService { String index(Class clazz, T source, String... placeholders); - boolean bulkIndex(List list, Class clazz, String... placeholders); - SearchScrollHits scrollFirst(Query query, Class clazz, String... placeholders); SearchScrollHits scroll(String scrollId, Class clazz, String... placeholders); @@ -42,4 +42,6 @@ public interface IElasticIndexService { void applySettings(HashMap settingMap, Class clazz); void clearScrollHits(List scrollIds); + + void bulkIndex(boolean indexAll, LocalDateTime lastRun, Integer caseBatchSize, Integer taskBatchSize); } diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java index 9a738c8ab58..c50c37df036 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java @@ -2,6 +2,8 @@ import com.netgrif.application.engine.auth.domain.LoggedUser; import com.netgrif.application.engine.elastic.service.ReindexingTask; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.elastic.web.requestbodies.IndexParams; import com.netgrif.application.engine.workflow.service.CaseSearchService; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.responsebodies.MessageResource; @@ -20,10 +22,7 @@ import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Locale; import java.util.Map; @@ -49,6 +48,9 @@ public class ElasticController { @Autowired private ReindexingTask reindexingTask; + @Autowired + private IElasticIndexService indexService; + @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") private int pageSize; @@ -69,11 +71,11 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth if (count == 0) { log.info("No cases to reindex"); } else { - long numOfPages = (long) ((count / pageSize) + 1); - log.info("Reindexing cases: " + numOfPages + " pages"); + long numOfPages = (count / pageSize) + 1; + log.info("Reindexing cases: {} pages", numOfPages); for (int page = 0; page < numOfPages; page++) { - log.info("Indexing page " + (page + 1)); + log.info("Indexing page {}", (page + 1)); Predicate predicate = searchService.buildQuery(searchBody, user, locale); reindexingTask.forceReindexPage(predicate, page, numOfPages); } @@ -85,4 +87,23 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth return MessageResource.errorMessage(e.getMessage()); } } + + @PreAuthorize("@authorizationService.hasAuthority('ADMIN')") + @Operation(summary = "Reindex all or stale cases with bulk index", + description = "Reindex all or stale cases (specified by IndexParams.indexAll param) with bulk index. Caller must have the ADMIN role", + security = {@SecurityRequirement(name = "BasicAuth")}) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "403", description = "Caller doesn't fulfill the authorisation requirements"), + }) + @PostMapping(value = "/reindex/bulk", produces = MediaType.APPLICATION_JSON_VALUE) + public MessageResource bulkReindex(IndexParams indexParams) { + try { + indexService.bulkIndex(indexParams.isIndexAll(), null, indexParams.getCaseBatchSize(), indexParams.getTaskBatchSize()); + return MessageResource.successMessage("Success"); + } catch (Exception e) { + log.error("Could not index: ", e); + return MessageResource.errorMessage(e.getMessage()); + } + } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java index 61397d61643..735604811b9 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java @@ -1,6 +1,7 @@ package com.netgrif.application.engine.elastic.web.requestbodies; import com.fasterxml.jackson.annotation.JsonFormat; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -60,7 +61,7 @@ public CaseSearchRequest(Map request) { } if (request.containsKey("author") && request.get("author") instanceof List) { List> authors = (List>) request.get("author"); - this.author = authors.stream().map(map -> { + this.author = authors.stream().map(map -> { Author authorRequest = new Author(); if (map.containsKey("id")) authorRequest.id = map.get("id"); @@ -75,7 +76,8 @@ public CaseSearchRequest(Map request) { this.data = (Map) request.get("data"); } if (request.containsKey("fullText") && request.get("fullText") instanceof String) { - this.fullText = (String) request.get("fullText"); + String originalFullText = (String) request.get("fullText"); + this.fullText = ElasticsearchQuerySanitizer.sanitize(originalFullText); } if (request.containsKey("transition") && request.get("transition") instanceof List) { this.transition = (List) request.get("transition"); diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java index a01e639ae7f..39fc80d1504 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.elastic.web.requestbodies; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.PetriNet; import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.TaskSearchCaseRequest; @@ -14,14 +15,14 @@ @AllArgsConstructor public class ElasticTaskSearchRequest extends TaskSearchRequest { public String query; - + public ElasticTaskSearchRequest(Map request) { if (request.containsKey("role") && request.get("role") instanceof List) { this.role = (List) request.get("role"); } if (request.containsKey("useCase") && request.get("useCase") instanceof List) { List> useCases = (List>) request.get("useCase"); - this.useCase = useCases.stream().map(map -> { + this.useCase = useCases.stream().map(map -> { TaskSearchCaseRequest useCase = new TaskSearchCaseRequest(); if (map.containsKey("id")) useCase.id = map.get("id"); @@ -44,7 +45,8 @@ public ElasticTaskSearchRequest(Map request) { this.transitionId = (List) request.get("transitionId"); } if (request.containsKey("fullText") && request.get("fullText") instanceof String) { - this.fullText = (String) request.get("fullText"); + String originalFullText = (String) request.get("fullText"); + this.fullText = ElasticsearchQuerySanitizer.sanitize(originalFullText); } if (request.containsKey("group") && request.get("group") instanceof List) { this.group = (List) request.get("group"); diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java new file mode 100644 index 00000000000..9e3adab68c6 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java @@ -0,0 +1,28 @@ +package com.netgrif.application.engine.elastic.web.requestbodies; + +import lombok.Data; + + +/** + * Represents the parameters to configure the indexing operation. + * This class allows customization of batch sizes for cases and tasks, + * as well as the option to index all data. + */ +@Data +public class IndexParams { + + /** + * Determines whether to index all available data. Default is {@code false}. + */ + private boolean indexAll = false; + + /** + * Specifies the batch size for cases during indexing. Default is {@code 5000}. + */ + private Integer caseBatchSize = 5000; + + /** + * Specifies the batch size for tasks during indexing. Default is {@code 20000}. + */ + private Integer taskBatchSize = 20000; +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java index 3d6a988f670..866aef49048 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.utils.SingleItemAsList; -import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.utils.CaseSearchRequestSingleItemAsListDeserializer; -@JsonDeserialize(using = SingleItemAsListDeserializer.class, contentAs = CaseSearchRequest.class) +@JsonDeserialize(using = CaseSearchRequestSingleItemAsListDeserializer.class, contentAs = CaseSearchRequest.class) public class SingleCaseSearchRequestAsList extends SingleItemAsList { -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java index 7dd274545bf..a2d0e19e12d 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; import com.netgrif.application.engine.utils.SingleItemAsList; -import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.utils.TaskSearchRequestSingleItemAsListDeserializer; -@JsonDeserialize(using = SingleItemAsListDeserializer.class, contentAs = ElasticTaskSearchRequest.class) +@JsonDeserialize(using = TaskSearchRequestSingleItemAsListDeserializer.class, contentAs = ElasticTaskSearchRequest.class) public class SingleElasticTaskSearchRequestAsList extends SingleItemAsList { -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java b/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java new file mode 100644 index 00000000000..5d948bafaf2 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java @@ -0,0 +1,92 @@ +package com.netgrif.application.engine.export.configuration; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Min; + +/** + * Configuration properties for XLS export functionality. + *

+ * Properties are prefixed with nae.xls.export in the configuration files + * (e.g., application.yml or application.properties). + *

+ * These settings control the behavior and formatting of exported Excel files. + */ +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = "nae.xls.export") +public class XlsExportProperties { + + /** + * The default name of the exported XLS file (without extension). + *

+ * Example: export + */ + private String exportFileName = "export"; + + /** + * The name of the sheet inside the exported XLS file. + *

+ * Example: export + */ + private String sheetName = "export"; + + /** + * The maximum number of rows allowed in the export. + *

+ * Must be 0 or greater. If set to 0, row limit is effectively disabled. + * Default is 10,000. + */ + @Min(0) + private long maxRows = 10000L; + + /** + * The number of records per export page. + *

+ * Must be greater than 0. Default is 100. + */ + @Min(1) + private int pageSize = 100; + + /** + * Whether to include metadata (such as case stringId, case author) in the exported document. + *

+ * Default is true. + */ + private boolean addMetaData = true; + + /** + * Whether to export all immediate fields of exported cases. + *

+ * Default is true. + */ + private boolean exportAllImmediateFields = true; + + /** + * Warning message shown when the number of exported records exceeds the configured limit. + *

+ * Default:
+ * Tento dokument obsahuje maximálny povolený počet záznamov pre export. + * Pre zvýšenie limitu exportu záznamov prosím kontaktuje svojho administrátora. + */ + private String trimWarningMessage = "Tento dokument obsahuje maximálny povolený počet záznamov pre export. Pre zvýšenie limitu exportu záznamov prosím kontaktuje svojho administrátora."; + + /** + * Date format used in the exported XLS file. + *

+ * Example: dd.MM.yyyy + */ + private String datePattern = "dd.MM.yyyy"; + + /** + * Date-time format used in the exported XLS file. + *

+ * Example: dd.MM.yyyy HH:mm:ss + */ + private String dateTimePattern = "dd.MM.yyyy HH:mm:ss"; + +} diff --git a/src/main/java/com/netgrif/application/engine/export/domain/CellFactory.java b/src/main/java/com/netgrif/application/engine/export/domain/CellFactory.java new file mode 100644 index 00000000000..02b4abc76d5 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/domain/CellFactory.java @@ -0,0 +1,41 @@ +package com.netgrif.application.engine.export.domain; + +import com.netgrif.application.engine.petrinet.domain.dataset.Field; +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; +import com.netgrif.application.engine.petrinet.domain.dataset.UserFieldValue; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.util.CellUtil; + +import java.util.Map; + +import static com.netgrif.application.engine.export.utils.XlsExportDateUtils.*; + +public class CellFactory { + + public static String DATE_PATTERN = "yyyy-MM-dd"; + public static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + + private static final Map CELL_TYPE_MAP = Map.of( + FieldType.BOOLEAN, new CellType((short) 0x0, (cell, value) -> cell.setCellValue(value.equals(true))), + FieldType.NUMBER, new CellType((short) 0x2, (cell, value) -> cell.setCellValue(Math.round(Double.parseDouble(value.toString()) * 100.0) / 100.0)), + FieldType.DATE, new CellType((short) 0xe, (cell, value) -> cell.setCellValue(dateToString(convertToLocalDateIfNeeded(value), DATE_PATTERN))), + FieldType.DATETIME, new CellType((short) 0x16, (cell, value) -> cell.setCellValue(dateTimeToString(convertToLocalDateTimeIfNeeded(value), DATE_TIME_PATTERN))), + FieldType.USER, new CellType((short) 0x0, (cell, value) -> cell.setCellValue(((UserFieldValue) value).getFullName())) + ); + private static final CellType DEFAULT_CELL_TYPE = new CellType((short) 0x0, (cell, value) -> cell.setCellValue(value.toString())); + + public static Cell create(Row row, int columnIndex, Field field) { + return create(row, columnIndex, field.getType(), field.getValue()); + } + + public static Cell create(Row row, int columnIndex, FieldType fieldType, Object value) { + CellType cellType = CELL_TYPE_MAP.getOrDefault(fieldType, DEFAULT_CELL_TYPE); + Cell cell = row.createCell(columnIndex); + CellUtil.setCellStyleProperty(cell, CellUtil.DATA_FORMAT, cellType.getFormat()); + if (value != null) + cellType.getCellValueSetter().accept(cell, value); + return cell; + } + +} diff --git a/src/main/java/com/netgrif/application/engine/export/domain/CellType.java b/src/main/java/com/netgrif/application/engine/export/domain/CellType.java new file mode 100644 index 00000000000..a169e07d9cc --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/domain/CellType.java @@ -0,0 +1,21 @@ +package com.netgrif.application.engine.export.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.poi.ss.usermodel.Cell; + +import java.util.function.BiConsumer; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CellType { + + /** + * @see org.apache.poi.ss.usermodel.BuiltinFormats + */ + private short format; + private BiConsumer cellValueSetter; + +} diff --git a/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java b/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java new file mode 100644 index 00000000000..3742c86f25d --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java @@ -0,0 +1,62 @@ +package com.netgrif.application.engine.export.domain; + +import com.netgrif.application.engine.petrinet.domain.Imported; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + +@Data +public class ExportedField extends Imported { + + public static final ExportedField STRING_ID = new ExportedField("meta-stringId", "ID Prípadu", true); + public static final ExportedField AUTHOR = new ExportedField("meta-author", "Autor", true); + public static final ExportedField CREATION_DATE = new ExportedField("meta-creationDate", "Dátum vytvorenia", true); + public static final ExportedField TITLE = new ExportedField("meta-title", "Názov", true); + public static final ExportedField VISUAL_ID = new ExportedField("meta-visualId", "Vizuálne ID", true); + + private String name; + private boolean meta; + + public ExportedField(String id, String name) { + this(id, name, false); + } + + public ExportedField(String id, String name, boolean meta) { + super(); + setImportId(id); + this.name = name; + this.meta = meta; + } + + public String getId() { + return getImportId(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExportedField that = (ExportedField) o; + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } + + public static List convert(List fieldIds, List fieldNames) { + if (fieldIds == null || fieldNames == null) return new ArrayList<>(); + if (fieldIds.size() != fieldNames.size()) + throw new IllegalArgumentException("Provided fields IDs does not match to every fields name"); + List list = new ArrayList<>(fieldIds.size()); + for (int i = 0; i < fieldIds.size(); i++) { + list.add(new ExportedField(fieldIds.get(i), fieldNames.get(i))); + } + return list; + } + +} diff --git a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java new file mode 100644 index 00000000000..40b358b0180 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java @@ -0,0 +1,251 @@ +package com.netgrif.application.engine.export.service; + +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.elastic.domain.ElasticCase; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.export.configuration.XlsExportProperties; +import com.netgrif.application.engine.export.service.interfaces.IXlsExportService; +import com.netgrif.application.engine.export.domain.CellFactory; +import com.netgrif.application.engine.export.domain.ExportedField; +import com.netgrif.application.engine.petrinet.domain.PetriNet; +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; +import com.netgrif.application.engine.petrinet.domain.dataset.MapOptionsField; +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; +import org.springframework.data.elasticsearch.core.SearchHitSupport; +import org.springframework.data.elasticsearch.core.SearchScrollHits; +import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileOutputStream; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Slf4j +@Service +@RequiredArgsConstructor +public class XlsExportService implements IXlsExportService { + + private final IWorkflowService workflowService; + private final IElasticIndexService elasticIndexService; + private final ElasticsearchRestTemplate elasticsearchTemplate; + private final IElasticCaseService elasticCaseService; + private final IPetriNetService petriNetService; + private final XlsExportProperties exportProperties; + + @PostConstruct + public void init() { + if (exportProperties.getDatePattern() != null && !exportProperties.getDatePattern().isBlank()) { + CellFactory.DATE_PATTERN = exportProperties.getDatePattern(); + } + if (exportProperties.getDateTimePattern() != null && !exportProperties.getDateTimePattern().isBlank()) { + CellFactory.DATE_TIME_PATTERN = exportProperties.getDateTimePattern(); + } + } + + @Override + public File getExportFilteredCasesFile(FilteredCasesRequest request, LoggedUser user, Locale locale) throws Exception { + List fieldsToExport = ExportedField.convert(request.getSelectedDataFieldIds(), request.getSelectedDataFieldNames()); + fieldsToExport = insertPredefinedFields(fieldsToExport, getProcessIdentifierFromFilteredRequest(request)); + return getCasesToExcel(request.getQuery(), fieldsToExport, user, locale, request.getIsIntersection()); + } + + @Override + public File getExportFilteredCasesFile(List requests, Boolean isIntersection, List selectedField, LoggedUser user, Locale locale) throws Exception { + return getCasesToExcel(requests, insertPredefinedFields(selectedField), user, locale, isIntersection); + } + + protected List insertPredefinedFields(List fieldToExport) { + return insertPredefinedFields(fieldToExport, null); + } + + protected List insertPredefinedFields(List fieldToExport, String processIdentifier) { + if (fieldToExport == null) return new ArrayList<>(); + Set fields = new LinkedHashSet<>(fieldToExport); + if (exportProperties.isAddMetaData()) { + replaceInSet(fields, ExportedField.STRING_ID); + replaceInSet(fields, ExportedField.VISUAL_ID); + replaceInSet(fields, ExportedField.AUTHOR); + replaceInSet(fields, ExportedField.TITLE); + replaceInSet(fields, ExportedField.CREATION_DATE); + } + + if (!exportProperties.isExportAllImmediateFields()) { + return new ArrayList<>(fields); + } + + if (processIdentifier == null || processIdentifier.isBlank()) { + return new ArrayList<>(fields); + } + + PetriNet process = petriNetService.getNewestVersionByIdentifier(processIdentifier); + process.getImmediateFields().stream() + .filter(f -> !f.getName().getDefaultValue().isBlank()) + .map(f -> new ExportedField(f.getImportId(), f.getName().getDefaultValue())) + .forEachOrdered(fields::add); + return new ArrayList<>(fields); + } + + protected void replaceInSet(Set fields, T field) { + boolean added = fields.add(field); + if (!added) { + fields.remove(field); + fields.add(field); + } + } + + private File getCasesToExcel(List requests, List fields, LoggedUser user, Locale locale, Boolean isIntersection) throws Exception { + log.info("Exporting cases to xlsx file. Query: {}", requests.stream().map(request -> request.query).collect(Collectors.joining(", "))); + long caseCount = elasticCaseService.count(requests, user, locale, isIntersection); + boolean isResultTrimmed = false; + if (exportProperties.getMaxRows() > 0) { + if (caseCount > exportProperties.getMaxRows()) { + log.warn("Requested case export could resulted in {} rows. Trimming result to {} row as configured in nae.xls.export.max-rows", caseCount, exportProperties.getMaxRows()); + isResultTrimmed = true; + caseCount = exportProperties.getMaxRows(); + } + } + long numberOfPagesNeeded = caseCount % exportProperties.getPageSize() == 0 ? (caseCount / exportProperties.getPageSize()) : (caseCount / exportProperties.getPageSize()) + 1; + + SXSSFWorkbook workbook = new SXSSFWorkbook(exportProperties.getPageSize()); // https://poi.apache.org/components/spreadsheet/how-to.html#sxssf + Sheet sheet = workbook.createSheet(exportProperties.getSheetName()); + insertHeader(fields, sheet); + + int sizeOfProcessedRows = 0; + int counter = 0; + List scrollIdsToClear = new ArrayList<>(); + NativeSearchQuery query = elasticCaseService.buildQuery(requests, user, PageRequest.of(0, exportProperties.getPageSize()), Locale.ENGLISH, true); + SearchScrollHits scroll = elasticIndexService.scrollFirst(query, ElasticCase.class); + while (scroll.hasSearchHits() && counter < numberOfPagesNeeded) { + Page indexedCases = (Page) SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(scroll, query.getPageable())); + Page page = new PageImpl<>(workflowService.findAllById(indexedCases.get().map(ElasticCase::getStringId).collect(Collectors.toList())), query.getPageable(), scroll.getTotalHits()); + scrollIdsToClear.add(scroll.getScrollId()); + sizeOfProcessedRows = processPage(page, sheet, fields, sizeOfProcessedRows); + counter += 1; + scroll = elasticIndexService.scroll(scroll.getScrollId(), ElasticCase.class); + } + scrollIdsToClear.add(scroll.getScrollId()); + elasticsearchTemplate.searchScrollClear(scrollIdsToClear); + + if (isResultTrimmed) { + int lasRow = sheet.getLastRowNum() < 0 ? 0 : sheet.getLastRowNum() + 1; + sheet.createRow(lasRow).createCell(0); + sheet.createRow(lasRow + 1).createCell(0).setCellValue(exportProperties.getTrimWarningMessage()); + } + + File result = File.createTempFile("case-export-" + LocalDateTime.now().toEpochSecond(ZoneOffset.UTC), ".xlsx"); + try (FileOutputStream fout = new FileOutputStream(result)) { + workbook.write(fout); + } catch (Exception ex) { + log.error("Cannot create export for provided query", ex); + throw ex; + } finally { + workbook.close(); + workbook.dispose(); + } + log.info("Successfully exported {} cases to xlsx file.", caseCount); + return result; + } + + private void insertHeader(List fields, Sheet sheet) { + Row header = sheet.createRow(0); + IntStream.range(0, fields.size()).forEach(idx -> { + Cell cell = header.createCell(idx); + cell.setCellValue(fields.get(idx).getName()); + }); + } + + private int processPage(Page page, Sheet sheet, List fieldsToExport, int numberOfProcessedItems) { + int rowIndex = Math.toIntExact(numberOfProcessedItems == 0 ? ((long) page.getNumber() * exportProperties.getPageSize()) + 1 : numberOfProcessedItems); + for (Case caze : page.getContent()) { + Row row = processCase(caze, sheet, rowIndex, fieldsToExport); + if (row != null) + rowIndex++; + } + return rowIndex; + } + + private Row processCase(Case caze, Sheet sheet, int rowIndex, List fieldsToExport) { + rowIndex = Math.max(rowIndex, 0); + Row row = sheet.createRow(rowIndex); + fieldsToExport.forEach(field -> processField(caze, field, row)); + return row; + } + + private Cell processField(Case caze, ExportedField fieldToExport, Row row) { + Object value = resolveFieldValue(caze, fieldToExport); + FieldType fieldType = caze.getField(fieldToExport.getId()) == null ? FieldType.TEXT : caze.getField(fieldToExport.getId()).getType(); + int cellNum = row.getLastCellNum() < 0 ? 0 : row.getLastCellNum(); + return CellFactory.create(row, cellNum, fieldType, value); + } + + private Object resolveFieldValue(Case caze, ExportedField field) { + try { + if (field.isMeta()) { + return resolveMetaFieldValue(caze, field); + } + if (caze.getField(field.getId()).getType() == FieldType.ENUMERATION_MAP) { + Map options = caze.getDataField(field.getId()).getOptions(); + if (options == null || options.isEmpty()) { + options = ((MapOptionsField) caze.getField(field.getId())).getOptions(); + } + Object value = caze.getFieldValue(field.getId()); + return value == null ? null : options.get(value.toString()).toString(); + } + return caze.getFieldValue(field.getId()); + } catch (Exception ex) { + return "ERROR"; + } + } + + private Object resolveMetaFieldValue(Case caze, ExportedField field) { + if (!field.isMeta()) return null; + if (field.getId().contains("title")) + return caze.getTitle(); + if (field.getId().contains("author")) + return caze.getAuthor().getFullName(); + if (field.getId().contains("creationDate")) + return caze.getCreationDate().format(DateTimeFormatter.ofPattern(exportProperties.getDateTimePattern())); + if (field.getId().contains("visualId")) + return caze.getVisualId(); + if (field.getId().contains("stringId")) + return caze.getStringId(); + return null; + } + + private String getProcessIdentifierFromFilteredRequest(FilteredCasesRequest request) { + if (request.getQuery() == null || + request.getQuery().isEmpty() || + request.getQuery().get(0) == null || + request.getQuery().get(0).query == null || + request.getQuery().get(0).query.isBlank()) { + return ""; + } + return Arrays.stream(request.getQuery().get(0).query.split("\\s+")) + .filter(part -> part.startsWith("processIdentifier:")) + .map(part -> part.split(":", 2)[1]) + .findFirst() + .orElse(""); + } +} diff --git a/src/main/java/com/netgrif/application/engine/export/service/interfaces/IXlsExportService.java b/src/main/java/com/netgrif/application/engine/export/service/interfaces/IXlsExportService.java new file mode 100644 index 00000000000..4aade0845d4 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/service/interfaces/IXlsExportService.java @@ -0,0 +1,18 @@ +package com.netgrif.application.engine.export.service.interfaces; + + +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.export.domain.ExportedField; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; + +import java.io.File; +import java.util.List; +import java.util.Locale; + +public interface IXlsExportService { + + File getExportFilteredCasesFile(FilteredCasesRequest request, LoggedUser user, Locale locale) throws Exception; + + File getExportFilteredCasesFile(List requests, Boolean isIntersection, List selectedField, LoggedUser user, Locale locale) throws Exception; +} diff --git a/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java b/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java new file mode 100644 index 00000000000..aaff27ae27b --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java @@ -0,0 +1,113 @@ +package com.netgrif.application.engine.export.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +@Slf4j +public class XlsExportDateUtils { + + + public static LocalDate convertToLocalDate(Date dateToConvert) { + return Instant.ofEpochMilli(dateToConvert.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + } + + public static LocalDateTime convertToLocalDateTime(Date dateTimeToConvert) { + return Instant.ofEpochMilli(dateTimeToConvert.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } + + public static LocalDate parseLocalDate(String date, List patterns) { + if (date == null) { + throw new IllegalArgumentException("Date is null"); + } + + for (String pattern : patterns) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + try { + return LocalDate.parse(date, formatter); + + } catch (DateTimeParseException ignored) { + } + } + + log.error("Date {} could not be parsed using patterns: {}.", date, patterns); + return null; + } + + public static LocalDateTime parseLocalDateTime(String date, List patterns) { + if (date == null) { + throw new IllegalArgumentException("Date is null"); + } + + for (String pattern : patterns) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + try { + return LocalDateTime.parse(date, formatter); + + } catch (DateTimeParseException e) { + try { + return LocalDate.parse(date, formatter).atStartOfDay(); + + } catch (DateTimeParseException ignored) { + } + } + } + + log.error("Date {} could not be parsed using patterns: {}.", date, patterns); + return null; + } + + public static LocalDate convertToLocalDateIfNeeded(Object date) { + if (date == null) return null; + if (date instanceof String) { + return parseLocalDate((String) date, Arrays.asList("dd.MM.yyyy", "d.M.yyyy")); + } + if (date instanceof LocalDate) { + return (LocalDate) date; + } + if (date instanceof Date) { + return convertToLocalDate((Date) date); + } else { + throw new IllegalArgumentException("Error casting field value: Not java.util.Date, java.lang.String, nor java.time.LocalDateTime"); + } + } + + public static LocalDateTime convertToLocalDateTimeIfNeeded(Object dateTime) { + if (dateTime == null) return null; + if (dateTime instanceof String) { + return parseLocalDateTime((String) dateTime, Arrays.asList("dd.MM.yyyy HH:mm", "d.M.yyyy HH:mm", "dd.MM.yyyy HH:mm:ss", "d.M.yyyy HH:mm:ss", "dd.MM.yyyy", "d.M.yyyy")); + } + if (dateTime instanceof LocalDateTime) { + return (LocalDateTime) dateTime; + } + if (dateTime instanceof Date) { + return convertToLocalDateTime((Date) dateTime); + } else { + throw new IllegalArgumentException("Error casting field value: Not java.util.Date, java.lang.String, nor java.time.LocalDateTime"); + } + } + + public static String dateToString(LocalDate date, String pattern) { + if (date == null || pattern == null) return null; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return date.format(formatter); + } + + public static String dateTimeToString(LocalDateTime dateTime, String pattern) { + if (dateTime == null || pattern == null) return null; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return dateTime.format(formatter); + } +} diff --git a/src/main/java/com/netgrif/application/engine/export/web/ExportController.java b/src/main/java/com/netgrif/application/engine/export/web/ExportController.java new file mode 100644 index 00000000000..27d7a4151ac --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/web/ExportController.java @@ -0,0 +1,66 @@ +package com.netgrif.application.engine.export.web; + +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.export.configuration.XlsExportProperties; +import com.netgrif.application.engine.export.service.interfaces.IXlsExportService; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/export") +public class ExportController { + + private final IXlsExportService exportService; + private final XlsExportProperties exportProperties; + + @PostMapping(value = "/filteredCases", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity getStatisticsFile(@RequestBody FilteredCasesRequest requestBody, Authentication auth, Locale locale) throws Exception { + + LoggedUser user = (LoggedUser) auth.getPrincipal(); + File excel = exportService.getExportFilteredCasesFile(requestBody, user, locale); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=" + + (LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH_mm_ss")) + "-" + exportProperties.getExportFileName() + ".xlsx")); + headers.setContentLength(Files.size(excel.toPath())); + + return ResponseEntity + .ok() + .headers(headers) + .body(new FileSystemResource(excel) { + @Override + public InputStream getInputStream() throws IOException { + return new FileInputStream(excel) { + @Override + public void close() throws IOException { + super.close(); + Files.delete(excel.toPath()); + } + }; + } + }); + } + +} diff --git a/src/main/java/com/netgrif/application/engine/export/web/requestbodies/FilteredCasesRequest.java b/src/main/java/com/netgrif/application/engine/export/web/requestbodies/FilteredCasesRequest.java new file mode 100644 index 00000000000..38d4c58252d --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/web/requestbodies/FilteredCasesRequest.java @@ -0,0 +1,20 @@ +package com.netgrif.application.engine.export.web.requestbodies; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import lombok.Data; + +import java.util.List; + +@Data +public class FilteredCasesRequest { + + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + private List query; + + private List selectedDataFieldNames; + + private List selectedDataFieldIds; + + private Boolean isIntersection; +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java index 2b19385925c..74bc7010ad3 100644 --- a/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java @@ -9,7 +9,10 @@ import org.springframework.web.server.ResponseStatusException; import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; +import java.util.Objects; public class SingleItemAsListDeserializer extends StdDeserializer implements ContextualDeserializer { @@ -27,13 +30,16 @@ protected SingleItemAsListDeserializer(Class vc) { @Override public JsonDeserializer createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { + return new SingleItemAsListDeserializer((Class) getItemClass(deserializationContext, beanProperty)); + } + + protected Class getItemClass(DeserializationContext deserializationContext, BeanProperty beanProperty) { final JavaType type; if (beanProperty != null) type = beanProperty.getType(); else type = deserializationContext.getContextualType(); - - return new SingleItemAsListDeserializer((Class) type.getRawClass()); + return type.getRawClass(); } @Override @@ -64,4 +70,15 @@ public Object deserialize(JsonParser jsonParser, DeserializationContext deserial return wrapper; } + + protected boolean isWrapperClass(Object object, Class wrapperClass, Class wrappedClass) { + try { + Type superClass = object.getClass().getGenericSuperclass(); + return Objects.equals(object.getClass(), wrapperClass) || + (superClass != null && + Objects.equals(((ParameterizedType) superClass).getActualTypeArguments()[0], wrappedClass)); + } catch (Exception e) { + return false; + } + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java index 1559cbe0017..039f6db8b7d 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java @@ -42,6 +42,10 @@ public class Case implements Serializable { @Setter private LocalDateTime lastModified; + @Getter + @Setter + private LocalDateTime lastModifiedDataSet; + @Getter private String visualId; @@ -239,10 +243,10 @@ public void populateDataSet(IInitValueExpressionEvaluator initValueExpressionEva this.dataSet.get(key).setComponent(field.getComponent()); } if (field instanceof UserField) { - this.dataSet.get(key).setChoices(((UserField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet())); + this.dataSet.get(key).setChoices(((UserField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet()), false); } if (field instanceof UserListField) { - this.dataSet.get(key).setChoices(((UserListField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet())); + this.dataSet.get(key).setChoices(((UserListField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet()), false); } if (field instanceof FieldWithAllowedNets) { this.dataSet.get(key).setAllowedNets(((FieldWithAllowedNets) field).getAllowedNets()); @@ -257,9 +261,9 @@ public void populateDataSet(IInitValueExpressionEvaluator initValueExpressionEva dynamicChoicesFields.add((ChoiceField) field); } }); - dynamicInitFields.forEach(field -> this.dataSet.get(field.getImportId()).setValue(initValueExpressionEvaluator.evaluate(this, field, params))); - dynamicChoicesFields.forEach(field -> this.dataSet.get(field.getImportId()).setChoices(initValueExpressionEvaluator.evaluateChoices(this, field, params))); - dynamicOptionsFields.forEach(field -> this.dataSet.get(field.getImportId()).setOptions(initValueExpressionEvaluator.evaluateOptions(this, field, params))); + dynamicInitFields.forEach(field -> this.dataSet.get(field.getImportId()).setValue(initValueExpressionEvaluator.evaluate(this, field, params), false)); + dynamicChoicesFields.forEach(field -> this.dataSet.get(field.getImportId()).setChoices(initValueExpressionEvaluator.evaluateChoices(this, field, params), false)); + dynamicOptionsFields.forEach(field -> this.dataSet.get(field.getImportId()).setOptions(initValueExpressionEvaluator.evaluateOptions(this, field, params), false)); populateDataSetBehaviorAndComponents(); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java index b37ebe82b34..5734a9b3e93 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java @@ -14,6 +14,7 @@ import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; import java.io.Serializable; import java.time.LocalDateTime; @@ -65,6 +66,11 @@ public class DataField implements Referencable, Serializable { @Setter private Component component; + @Transient + @Getter + @Setter + private boolean changed = false; + public DataField() { behavior = new HashMap<>(); dataRefComponents = new HashMap<>(); @@ -81,13 +87,27 @@ public void setBehavior(Map> behavior) { } public void setValue(Object value) { + setValue(value, true); + } + + public void setValue(Object value, boolean trackChange) { this.value = value; update(); + if (trackChange) { + changed(); + } } public void setChoices(Set choices) { + setChoices(choices, true); + } + + public void setChoices(Set choices, boolean trackChange) { this.choices = choices; update(); + if (trackChange) { + changed(); + } } public void setAllowedNets(List allowedNets) { @@ -101,8 +121,15 @@ public void setFilterMetadata(Map filterMetadata) { } public void setOptions(Map options) { + setOptions(options, true); + } + + public void setOptions(Map options, boolean trackChange) { this.options = options; update(); + if (trackChange) { + changed(); + } } public void setValidations(List validations) { @@ -210,6 +237,10 @@ private void update() { version++; } + private void changed() { + changed = true; + } + public boolean isNewerThen(DataField other) { return version > other.getVersion(); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java b/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java index 9fd45065606..2206527eada 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java @@ -6,20 +6,32 @@ import lombok.Data; import java.util.List; +import java.util.Objects; @Data public class FinishTaskEventOutcome extends TaskEventOutcome { + /** + * Outcome flag, which is true if the task is still executable after the finish task event + */ + protected boolean isTaskStillExecutable; + public FinishTaskEventOutcome() { super(); } public FinishTaskEventOutcome(Case useCase, Task task) { super(useCase, task); + this.isTaskStillExecutable = isTaskStillExecutable(useCase, task); } public FinishTaskEventOutcome(Case useCase, Task task, List outcomes) { this(useCase, task); this.setOutcomes(outcomes); } + + protected boolean isTaskStillExecutable(Case useCase, Task task) { + return useCase.getTasks().stream() + .anyMatch(taskPair -> task != null && Objects.equals(taskPair.getTask(), task.getStringId())); + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java b/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java index 841b720f503..9ee7b41ab56 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java @@ -16,6 +16,8 @@ public interface TaskRepository extends MongoRepository, QuerydslP List findAllByCaseId(String id); + List findAllByCaseIdIn(Collection ids); + Page findByCaseIdIn(Pageable pageable, Collection ids); Page findByTransitionIdIn(Pageable pageable, Collection ids); diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index 70a6903f40a..548ee926a2b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -68,6 +68,8 @@ public class DataService implements IDataService { public static final int MONGO_ID_LENGTH = 24; + private static final Set setDataForbiddenFieldTypes = Set.of(FieldType.TASK_REF, FieldType.CASE_REF); + @Autowired protected ApplicationEventPublisher publisher; @@ -219,6 +221,22 @@ public SetDataEventOutcome setData(Task task, ObjectNode values) { @Override public SetDataEventOutcome setData(Task task, ObjectNode values, Map params) { + return setData(task, values, params, false); + } + + /** + * Updates the data field's attributes of the provided task. + * + * @param task the task object of which the data are updated + * @param values information about how to update the data fields + * @param params additional information to be injected to the action delegate context + * @param runStrict if set to true, additional validations are going to be applied when updating the data fields. If + * set to false, minimal restrictions are considered. + * + * @return outcome containing Case, Task and changes that have been made. + * */ + @Override + public SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean runStrict) { Case useCase = workflowService.findOne(task.getCaseId()); IUser user = userService.getLoggedOrSystem(); @@ -230,8 +248,20 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map { String fieldId = entry.getKey(); + if (runStrict) { + Field field = useCase.getField(fieldId); + if (field == null) { + throw new IllegalArgumentException("Such field with id [" + fieldId + "] does not exist in petri net [" + useCase.getPetriNetId() + "]"); + } + if (setDataForbiddenFieldTypes.contains(field.getType())) { + return; + } + } DataField dataField = useCase.getDataSet().get(fieldId); if (dataField != null) { + if (runStrict && !isDataFieldEditable(dataField, task.getTransitionId())) { + throw new IllegalArgumentException("Cannot edit data field [" + fieldId + "], which is not editable on transition [" + task.getTransitionId() + "]."); + } Field field = useCase.getPetriNet().getField(fieldId).get(); outcome.addOutcomes(resolveDataEvents(field, DataEventType.SET, EventPhase.PRE, useCase, task, params)); if (outcome.getMessage() == null) { @@ -303,6 +333,15 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map> behaviorMap = dataField.getBehavior(); + if (behaviorMap == null) { + return false; + } + Set behaviorSet = behaviorMap.get(transId); + return behaviorSet != null && behaviorSet.contains(FieldBehavior.EDITABLE); + } + @Override public GetDataGroupsEventOutcome getDataGroups(String taskId, Locale locale) { return getDataGroups(taskId, locale, new HashSet<>(), 0, null); @@ -653,7 +692,8 @@ public SetDataEventOutcome saveFiles(String taskId, String fieldId, MultipartFil } private List getChangedFieldByFileFieldContainer(String fieldId, Task referencingTask, Case useCase, Map params) { - List outcomes = new ArrayList<>(resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, + List outcomes = new ArrayList<>(); + outcomes.addAll( resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, EventPhase.PRE, useCase, referencingTask, params)); outcomes.addAll(resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, EventPhase.POST, useCase, referencingTask, params)); @@ -1050,16 +1090,13 @@ private Map parseOptions(JsonNode node) { if (optionsNode == null) { return null; } + ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(I18nString.class, new I18nStringDeserializer()); mapper.registerModule(module); - Map optionsMapped = mapper.convertValue(optionsNode, new TypeReference>() { - }); - if (optionsMapped.isEmpty()) { - return null; - } - return optionsMapped; + + return mapper.convertValue(optionsNode, new TypeReference<>() {}); } private void setDataFieldOptions(Map options, DataField dataField, ChangedField changedField, String fieldType) { diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java index 2a045ffad93..910e7590002 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java @@ -36,7 +36,6 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome; import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.*; import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository; -import com.netgrif.application.engine.workflow.domain.triggers.AutoTrigger; import com.netgrif.application.engine.workflow.domain.triggers.TimeTrigger; import com.netgrif.application.engine.workflow.domain.triggers.Trigger; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; @@ -773,12 +772,9 @@ public List save(List tasks) { } @Override - public void resolveUserRef(Case useCase) { - useCase.getTasks().forEach(taskPair -> { - Optional taskOptional = taskRepository.findById(taskPair.getTask()); - taskOptional.ifPresent(task -> resolveUserRef(task, useCase)); - }); - + public List resolveUserRef(Case useCase) { + List tasks = taskRepository.findAllBy_idIn(useCase.getTasks().stream().map(TaskPair::getTask).collect(Collectors.toList())); + return tasks.stream().map(task -> resolveUserRef(task, useCase)).collect(Collectors.toList()); } @Override @@ -787,9 +783,9 @@ public Task resolveUserRef(Task task, Case useCase) { task.getNegativeViewUsers().clear(); task.getUserRefs().forEach((id, permission) -> { List userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(id).getValue()); - if (userIds != null && userIds.size() != 0 && permission.containsKey("view") && !permission.get("view")) { + if (userIds != null && !userIds.isEmpty() && permission.containsKey("view") && !permission.get("view")) { task.getNegativeViewUsers().addAll(userIds); - } else if (userIds != null && userIds.size() != 0) { + } else if (userIds != null && !userIds.isEmpty()) { task.addUsers(new HashSet<>(userIds), permission); } }); @@ -926,6 +922,9 @@ public SetDataEventOutcome getMainOutcome(Map outco } } mainOutcome = outcomes.remove(key); + if (mainOutcome == null) { + return null; + } mainOutcome.addOutcomes(new ArrayList<>(outcomes.values())); return mainOutcome; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 472df01d5ae..87bcb62a025 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -98,6 +98,7 @@ public class WorkflowService implements IWorkflowService { @Autowired protected IInitValueExpressionEvaluator initValueExpressionEvaluator; + @Lazy @Autowired protected IElasticCaseMappingService caseMappingService; @@ -110,6 +111,7 @@ public class WorkflowService implements IWorkflowService { protected IElasticCaseService elasticCaseService; + @Lazy @Autowired public void setElasticCaseService(IElasticCaseService elasticCaseService) { this.elasticCaseService = elasticCaseService; @@ -120,6 +122,7 @@ public Case save(Case useCase) { if (useCase.getPetriNet() == null) { setPetriNet(useCase); } + checkChangedDataSet(useCase); encryptDataSet(useCase); useCase = repository.save(useCase); try { @@ -226,7 +229,7 @@ public Case resolveUserRef(Case useCase) { private void resolveUserRefPermissions(Case useCase, String userListId, Map permission) { List userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(userListId).getValue()); - if (userIds != null && userIds.size() != 0) { + if (userIds != null && !userIds.isEmpty()) { if (permission.containsKey("view") && !permission.get("view")) { useCase.getNegativeViewUsers().addAll(userIds); } else { @@ -239,7 +242,7 @@ private List getExistingUsers(UserListFieldValue userListValue) { if (userListValue == null) return null; return userListValue.getUserValues().stream().map(UserFieldValue::getId) - .filter(id -> userService.resolveById(id, false) != null) + .filter(id -> userService.existsById(id)) .collect(Collectors.toList()); } @@ -474,7 +477,7 @@ private void resolveTaskRefs(Case useCase) { useCase.getPetriNet().getDataSet().values().stream().filter(f -> f instanceof TaskField).map(TaskField.class::cast).forEach(field -> { if (field.getDefaultValue() != null && !field.getDefaultValue().isEmpty() && useCase.getDataField(field.getStringId()).getValue() != null && useCase.getDataField(field.getStringId()).getValue().equals(field.getDefaultValue())) { - useCase.getDataField(field.getStringId()).setValue(new ArrayList<>()); + useCase.getDataField(field.getStringId()).setValue(new ArrayList<>(), false); List taskPairList = useCase.getTasks().stream().filter(t -> (field.getDefaultValue().contains(t.getTransition()))).collect(Collectors.toList()); if (!taskPairList.isEmpty()) { @@ -562,7 +565,7 @@ private void applyCryptoMethodOnDataSet(Case useCase, Function params); + SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean runStrict); + FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview) throws FileNotFoundException; FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview, Map params) throws FileNotFoundException; diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java index f7b867eb79c..691b2de4a9b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java @@ -15,7 +15,6 @@ import com.netgrif.application.engine.workflow.web.responsebodies.TaskReference; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Locale; @@ -106,7 +105,7 @@ public interface ITaskService { DelegateTaskEventOutcome delegateTask(LoggedUser loggedUser, String delegatedId, String taskId, Map params) throws TransitionNotExecutableException; - void resolveUserRef(Case useCase); + List resolveUserRef(Case useCase); Task resolveUserRef(Task task, Case useCase); @@ -127,4 +126,4 @@ public interface ITaskService { List save(List tasks); SetDataEventOutcome getMainOutcome(Map outcomes, String taskId); -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java new file mode 100644 index 00000000000..e66690ac8e3 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java @@ -0,0 +1,69 @@ +package com.netgrif.application.engine.workflow.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleCaseSearchRequestAsList; +import com.netgrif.application.engine.utils.SingleItemAsList; +import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; + +import java.io.IOException; +import java.util.List; + +/** + * Custom deserializer for handling JSON deserialization of objects that extend + * the {@link SingleItemAsList} class, specifically designed for handling + * {@link CaseSearchRequest} and ensuring its fields are properly sanitized. + *

+ * This deserializer extends the functionality of {@link SingleItemAsListDeserializer} + * to additionally process the deserialized objects that represent case search requests. + * It ensures that the `fullText` field in each case search request is sanitized + * using {@link ElasticsearchQuerySanitizer}. + *

+ * It also provides a mechanism to dynamically determine the appropriate type + * using the contextual information during deserialization. + */ +public class CaseSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { + + protected CaseSearchRequestSingleItemAsListDeserializer() { + this(null); + } + + protected CaseSearchRequestSingleItemAsListDeserializer(Class vc) { + super(vc); + } + + @Override + public JsonDeserializer createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { + return new CaseSearchRequestSingleItemAsListDeserializer((Class) getItemClass(deserializationContext, beanProperty)); + } + + /** + * Deserializes a JSON structure into an object, specifically handling instances that + * may extend the {@code SingleCaseSearchRequestAsList}. During deserialization, it + * sanitizes the `fullText` field in each {@code CaseSearchRequest} object for security + * purposes using {@code ElasticsearchQuerySanitizer}. + * + * @param jsonParser the {@code JsonParser} used for reading the JSON input + * @param deserializationContext the {@code DeserializationContext} providing access + * to contextual information during deserialization + * @return the deserialized object, with sanitization applied if it is an instance of + * {@code SingleCaseSearchRequestAsList} + * @throws IOException if any I/O error occurs during deserialization + * @throws IllegalArgumentException if the object could not be properly instantiated or deserialized + */ + @Override + public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { + Object result = super.deserialize(jsonParser, deserializationContext); + if (isWrapperClass(result, SingleCaseSearchRequestAsList.class, CaseSearchRequest.class)) { + List list = ((SingleCaseSearchRequestAsList) result).getList(); + list.forEach(request -> + request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); + } + return result; + } + +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java new file mode 100644 index 00000000000..ddf302b04a5 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java @@ -0,0 +1,74 @@ +package com.netgrif.application.engine.workflow.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; +import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; +import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleElasticTaskSearchRequestAsList; +import com.netgrif.application.engine.utils.SingleItemAsList; +import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; +import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * Custom deserializer for handling cases where single `TaskSearchRequest` items + * are sent as lists or standalone entities during JSON deserialization. + *

+ * This class extends the `SingleItemAsListDeserializer`, enabling support for + * deserialization scenarios where JSON may represent either a single item or a list of items. + * It ensures compatibility with `SingleTaskSearchRequestAsList` by sanitizing the `fullText` field + * in each `TaskSearchRequest` instance using the `ElasticsearchQuerySanitizer`. + */ +public class TaskSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { + + protected TaskSearchRequestSingleItemAsListDeserializer() { + this(null); + } + + protected TaskSearchRequestSingleItemAsListDeserializer(Class vc) { + super(vc); + } + + @Override + public JsonDeserializer createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { + return new TaskSearchRequestSingleItemAsListDeserializer((Class) getItemClass(deserializationContext, beanProperty)); + } + + /** + * Deserializes a JSON input into an object while handling cases where a single + * `TaskSearchRequest` or a list of `TaskSearchRequest` objects is included. If + * the object is a `SingleTaskSearchRequestAsList`, it processes each `TaskSearchRequest` + * in the list by sanitizing the `fullText` field using `ElasticsearchQuerySanitizer`. + * + * @param jsonParser the JSON parser used to parse the incoming JSON content + * @param deserializationContext the context for deserialization, providing shared + * state and configuration + * @return the deserialized object, with sanitization applied to `TaskSearchRequest.fullText` + * if applicable + * @throws IOException if an I/O error occurs during parsing + * @throws IllegalArgumentException if the deserialization process encounters an error + */ + @Override + public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { + Object result = super.deserialize(jsonParser, deserializationContext); + if (isWrapperClass(result, SingleTaskSearchRequestAsList.class, TaskSearchRequest.class) || + isWrapperClass(result, SingleElasticTaskSearchRequestAsList.class, ElasticTaskSearchRequest.class)) { + List list = Collections.emptyList(); + if (result instanceof SingleTaskSearchRequestAsList) { + list = ((SingleTaskSearchRequestAsList) result).getList(); + } else if (result instanceof SingleElasticTaskSearchRequestAsList) { + list = ((SingleElasticTaskSearchRequestAsList) result).getList(); + } + list.forEach(request -> + request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); + } + return result; + } + +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java index 890e053fcde..0e295542652 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java @@ -5,6 +5,7 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleElasticTaskSearchRequestAsList; import com.netgrif.application.engine.eventoutcomes.LocalisedEventOutcomeFactory; +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; import com.netgrif.application.engine.petrinet.domain.throwable.TransitionNotExecutableException; import com.netgrif.application.engine.workflow.domain.IllegalArgumentWithChangedFieldsException; import com.netgrif.application.engine.workflow.domain.MergeFilterOperation; @@ -16,6 +17,7 @@ import com.netgrif.application.engine.workflow.service.FileFieldInputStream; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.*; @@ -38,10 +40,8 @@ import org.springframework.web.multipart.MultipartFile; import java.io.FileNotFoundException; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; public abstract class AbstractTaskController { @@ -51,11 +51,15 @@ public abstract class AbstractTaskController { private final IDataService dataService; + private final IWorkflowService workflowService; + private final IElasticTaskService searchService; - public AbstractTaskController(ITaskService taskService, IDataService dataService, IElasticTaskService searchService) { + public AbstractTaskController(ITaskService taskService, IDataService dataService, IWorkflowService workflowService, + IElasticTaskService searchService) { this.taskService = taskService; this.dataService = dataService; + this.workflowService = workflowService; this.searchService = searchService; } @@ -210,9 +214,31 @@ public EntityModel getData(String taskId, Locale locale public EntityModel setData(String taskId, ObjectNode dataBody, Locale locale) { try { + List dataGroups = dataService.getDataGroups(taskId, locale).getData(); + Set referencedTaskIds = new HashSet<>(); + referencedTaskIds.add(taskId); + for (com.netgrif.application.engine.petrinet.domain.DataGroup dataGroup : dataGroups) { + Set referencedTaskIdsByDataGroup = dataGroup.getFields().getContent().stream() + .filter(localisedField -> localisedField.getType() == FieldType.TASK_REF + && localisedField.getValue() instanceof List + && !((List) localisedField.getValue()).isEmpty()) + .map(localisedField -> (List) localisedField.getValue()) + .flatMap(List::stream) + .collect(Collectors.toSet()); + referencedTaskIds.addAll(referencedTaskIdsByDataGroup); + } Map outcomes = new HashMap<>(); - dataBody.fields().forEachRemaining(it -> outcomes.put(it.getKey(), dataService.setData(it.getKey(), it.getValue().deepCopy()))); + dataBody.fields().forEachRemaining(fieldChangesEntry -> { + String taskIdToChangeWith = fieldChangesEntry.getKey(); + if (!referencedTaskIds.contains(taskIdToChangeWith)) { + return; + } + Task taskToChangeWith = taskService.findOne(taskIdToChangeWith); + outcomes.put(taskIdToChangeWith, dataService.setData(taskToChangeWith, + fieldChangesEntry.getValue().deepCopy(), new HashMap<>(), true)); + }); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } catch (IllegalArgumentWithChangedFieldsException e) { @@ -230,6 +256,7 @@ public EntityModel saveFile(String taskId, MultipartFil Map outcomes = new HashMap<>(); outcomes.put(dataBody.getParentTaskId(), dataService.saveFile(dataBody.getParentTaskId(), dataBody.getFieldId(), multipartFile)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } catch (IllegalArgumentWithChangedFieldsException e) { @@ -262,7 +289,8 @@ public EntityModel deleteFile(String taskId, String fie Map outcomes = new HashMap<>(); outcomes.put(taskId, dataService.deleteFile(taskId, fieldId)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -270,7 +298,8 @@ public EntityModel saveFiles(String taskId, MultipartFi Map outcomes = new HashMap<>(); outcomes.put(requestBody.getParentTaskId(), dataService.saveFiles(requestBody.getParentTaskId(), requestBody.getFieldId(), multipartFiles)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -294,7 +323,8 @@ public EntityModel deleteNamedFile(String taskId, Strin Map outcomes = new HashMap<>(); outcomes.put(taskId, dataService.deleteFileByName(taskId, fieldId, name)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -310,4 +340,13 @@ public ResponseEntity getFilePreview(String taskId, String fieldId) th .headers(headers) .body(fileFieldInputStream != null ? new InputStreamResource(fileFieldInputStream.getInputStream()) : null); } + + protected SetDataEventOutcome handleMainSetDataEventOutcome(SetDataEventOutcome mainOutcome, String taskId) { + if (mainOutcome == null) { + Task task = taskService.findOne(taskId); + return new SetDataEventOutcome(workflowService.findOne(task.getCaseId()), task); + } else { + return mainOutcome; + } + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java index 30294e760c9..02818262191 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java @@ -7,6 +7,7 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.response.EventOutcomeWithMessage; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.LocalisedTaskResource; @@ -49,8 +50,9 @@ public class PublicTaskController extends AbstractTaskController { private final ITaskService taskService; private final IDataService dataService; - public PublicTaskController(ITaskService taskService, IDataService dataService, IUserService userService) { - super(taskService, dataService, null); + public PublicTaskController(ITaskService taskService, IDataService dataService, IUserService userService, + IWorkflowService workflowService) { + super(taskService, dataService, workflowService, null); this.taskService = taskService; this.dataService = dataService; this.userService = userService; diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java index b9690306c3f..593f37ce4ee 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java @@ -9,6 +9,7 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.response.EventOutcomeWithMessage; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.CountResponse; @@ -51,8 +52,9 @@ public class TaskController extends AbstractTaskController { public static final Logger log = LoggerFactory.getLogger(TaskController.class); - public TaskController(ITaskService taskService, IDataService dataService, IElasticTaskService searchService) { - super(taskService, dataService, searchService); + public TaskController(ITaskService taskService, IDataService dataService, IWorkflowService workflowService, + IElasticTaskService searchService) { + super(taskService, dataService, workflowService, searchService); } @Override diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java index 23bfb683f97..d063e4ca274 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java @@ -31,6 +31,7 @@ import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.querydsl.binding.QuerydslPredicate; import org.springframework.data.web.PagedResourcesAssembler; @@ -191,6 +192,37 @@ public MessageResource reloadTasks(@PathVariable("id") String caseId) { } } + @PreAuthorize("@authorizationService.hasAuthority('ADMIN')") + @Operation(summary = "Reload tasks of all cases", + description = "Caller must have the ADMIN role", + security = {@SecurityRequirement(name = "BasicAuth")}) + @GetMapping(value = "/case/reload_all", produces = MediaTypes.HAL_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "403", description = "Caller doesn't fulfill the authorisation requirements"), + }) + public MessageResource reloadTasksOfAllCases(Authentication auth, Locale locale) { + log.info("Starting reload tasks of all cases to repair database integrity."); + try { + long caseCount = workflowService.count(Map.of(), (LoggedUser) auth.getPrincipal(), locale); + log.info("Number of cases: {}", caseCount); + long pageCount = Double.valueOf(Math.ceil((double) caseCount / 1000)).longValue(); + log.info("Calculated number of pages: {}", pageCount); + + for (int i = 0; i < pageCount; i++) { + PageRequest pageRequest = PageRequest.of(i, 1000); + Page cases = workflowService.getAll(pageRequest); + log.info("Processing page {} of {}", i + 1, pageCount); + cases.forEach(aCase -> taskService.reloadTasks(aCase)); + } + + return MessageResource.successMessage("Task reloaded for " + caseCount + " cases"); + } catch (Exception e) { + log.error("Reloading tasks of cases has failed:", e); + return MessageResource.errorMessage("Reloading tasks in cases has failed!"); + } + } + @Deprecated @PreAuthorize("@authorizationService.hasAuthority('ADMIN')") @Operation(summary = "Get all case data", security = {@SecurityRequirement(name = "BasicAuth")}) diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java b/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java index c8cd7b93436..954aff6f2e9 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.netgrif.application.engine.utils.SingleItemAsList; -import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.utils.TaskSearchRequestSingleItemAsListDeserializer; import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; -@JsonDeserialize(using = SingleItemAsListDeserializer.class, contentAs = TaskSearchRequest.class) +@JsonDeserialize(using = TaskSearchRequestSingleItemAsListDeserializer.class, contentAs = TaskSearchRequest.class) public class SingleTaskSearchRequestAsList extends SingleItemAsList { -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java index efc93a61998..708e72ee35d 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java @@ -2,12 +2,19 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.FinishTaskEventOutcome; import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.base.LocalisedTaskEventOutcome; +import lombok.Getter; import java.util.Locale; public class LocalisedFinishTaskEventOutcome extends LocalisedTaskEventOutcome { + @Getter + protected Boolean isTaskStillExecutable; + public LocalisedFinishTaskEventOutcome(FinishTaskEventOutcome outcome, Locale locale) { super(outcome, locale); + if (outcome != null) { + this.isTaskStillExecutable = outcome.isTaskStillExecutable(); + } } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index cfc1901cc4b..5f47f37c6eb 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -45,4 +45,8 @@ nae.cache.petriNetCache=petriNetCache nae.storage.minio.enabled=true nae.storage.minio.hosts.host_1.host=http://127.0.0.1:9000 nae.storage.minio.hosts.host_1.user=root -nae.storage.minio.hosts.host_1.password=password \ No newline at end of file +nae.storage.minio.hosts.host_1.password=password + +# Redis sentinel +#spring.redis.sentinel.master=mymaster +#spring.redis.sentinel.nodes=127.0.0.1:26379 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ea5b8cf5e65..24e03947dae 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,7 +41,8 @@ spring.data.elasticsearch.drop=false spring.data.elasticsearch.executors.size=500 spring.data.elasticsearch.executors.timeout=5 spring.data.elasticsearch.reindex=0 0 * * * * -spring.data.elasticsearch.reindexExecutor.size=20 +spring.data.elasticsearch.reindexExecutor.caseSize=5100 +spring.data.elasticsearch.reindexExecutor.taskSize=20000 spring.data.elasticsearch.reindexExecutor.timeout=60 # Mail Service @@ -70,6 +71,8 @@ spring.session.store-type=redis spring.session.redis.host=${REDIS_HOST:localhost} spring.session.redis.port=${REDIS_PORT:6379} spring.session.redis.namespace=${DATABASE_NAME:nae} +spring.session.redis.connection-timeout=5 +spring.session.redis.read-timeout=5 #Security nae.database.password=${DATABASE_encrypt_password:password} diff --git a/src/main/resources/petriNets/all_data.xml b/src/main/resources/petriNets/all_data.xml index f65ed4ae659..fa77752807f 100644 --- a/src/main/resources/petriNets/all_data.xml +++ b/src/main/resources/petriNets/all_data.xml @@ -1,5 +1,5 @@ - + data/all_data All Data ALL diff --git a/src/main/resources/petriNets/datamap.xml b/src/main/resources/petriNets/datamap.xml index b4f58f4bad6..559235aa565 100644 --- a/src/main/resources/petriNets/datamap.xml +++ b/src/main/resources/petriNets/datamap.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> data/taskref_demo2 1.0.0 TRD @@ -320,4 +320,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/dashboard.xml b/src/main/resources/petriNets/engine-processes/dashboard.xml index 6eea9d570be..832052df500 100644 --- a/src/main/resources/petriNets/engine-processes/dashboard.xml +++ b/src/main/resources/petriNets/engine-processes/dashboard.xml @@ -1,4 +1,4 @@ - + dashboard DSH Dashboard @@ -30,6 +30,7 @@ <component> <name>dashboard</name> + <property key="resolve_data">true</property> </component> </data> <i18n locale="sk"> @@ -289,4 +290,4 @@ <destinationId>t2</destinationId> <multiplicity>1</multiplicity> </arc> -</document> \ No newline at end of file +</document> diff --git a/src/main/resources/petriNets/engine-processes/dashboard_tile.xml b/src/main/resources/petriNets/engine-processes/dashboard_tile.xml index c6928563d38..ad0fe2b6829 100644 --- a/src/main/resources/petriNets/engine-processes/dashboard_tile.xml +++ b/src/main/resources/petriNets/engine-processes/dashboard_tile.xml @@ -1,4 +1,4 @@ -<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> <id>dashboard_tile</id> <initials>DST</initials> <title name="model_title">Dashboard Tile @@ -1544,4 +1544,4 @@ 660 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/export_filters.xml b/src/main/resources/petriNets/engine-processes/export_filters.xml index e057c9cb800..43e332135c5 100644 --- a/src/main/resources/petriNets/engine-processes/export_filters.xml +++ b/src/main/resources/petriNets/engine-processes/export_filters.xml @@ -1,5 +1,5 @@ - + export_filters FTE Export of filters diff --git a/src/main/resources/petriNets/engine-processes/filter.xml b/src/main/resources/petriNets/engine-processes/filter.xml index 0be99e1940f..4563e2d4c5e 100644 --- a/src/main/resources/petriNets/engine-processes/filter.xml +++ b/src/main/resources/petriNets/engine-processes/filter.xml @@ -1,5 +1,5 @@ - + filter FTR Filter @@ -1091,4 +1091,4 @@ p5 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/impersonation_config.xml b/src/main/resources/petriNets/engine-processes/impersonation_config.xml index 1e0778a8c61..25645cab618 100644 --- a/src/main/resources/petriNets/engine-processes/impersonation_config.xml +++ b/src/main/resources/petriNets/engine-processes/impersonation_config.xml @@ -1,4 +1,4 @@ - + impersonation_config 1.0.0 IPC @@ -794,4 +794,4 @@ t3 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml b/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml index aa0d581e141..84cda085a48 100644 --- a/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml +++ b/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml @@ -1,4 +1,4 @@ - + impersonation_users_select 1.0.0 IPU @@ -93,4 +93,4 @@ t2_delegate - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/import_filters.xml b/src/main/resources/petriNets/engine-processes/import_filters.xml index ba4f173f298..af74e2c35f9 100644 --- a/src/main/resources/petriNets/engine-processes/import_filters.xml +++ b/src/main/resources/petriNets/engine-processes/import_filters.xml @@ -1,5 +1,5 @@ - + import_filters FTI Import of filters diff --git a/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml b/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml index 3e1adad7aa7..d1c28db279c 100644 --- a/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml +++ b/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml @@ -393,6 +393,11 @@ </data> + <data type="boolean" immediate="true"> + <id>case_allow_export</id> + <title name="case_allow_export">Allow export? + + Názov tlačidla "Nová inštancia" @@ -428,6 +433,7 @@ Vybrať zobrazenie Nastavenie Asociované zobrazenie + Povoliť export? Schaltflächentitel "Neuer Fall" @@ -463,6 +469,7 @@ Wählen Sie einen Ansichtstyp Einstellungen zugehörige Ansicht + Erlaube Export? @@ -894,6 +901,18 @@ outline + + case_allow_export + + hidden + + + 0 + 999 + 1 + 1 + + diff --git a/src/main/resources/petriNets/engine-processes/org_group.xml b/src/main/resources/petriNets/engine-processes/org_group.xml index f11d3fac83d..b23102b4812 100644 --- a/src/main/resources/petriNets/engine-processes/org_group.xml +++ b/src/main/resources/petriNets/engine-processes/org_group.xml @@ -1,5 +1,5 @@ - + org_group 1.0.0 GRP diff --git a/src/main/resources/petriNets/engine-processes/preference_filter_item.xml b/src/main/resources/petriNets/engine-processes/preference_filter_item.xml index b1cfec77d55..2e12b82a51d 100644 --- a/src/main/resources/petriNets/engine-processes/preference_filter_item.xml +++ b/src/main/resources/petriNets/engine-processes/preference_filter_item.xml @@ -1,5 +1,5 @@ - + preference_filter_item PFI Preference filter item @@ -313,7 +313,7 @@ default_headers Set default headers - + allowed_nets Allowed nets diff --git a/src/main/resources/petriNets/insurance_portal_demo.xml b/src/main/resources/petriNets/insurance_portal_demo.xml index 57740ab2b96..ee5d43f9590 100644 --- a/src/main/resources/petriNets/insurance_portal_demo.xml +++ b/src/main/resources/petriNets/insurance_portal_demo.xml @@ -1,7 +1,7 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> examples/insurance/insurance IPD Insurance Portal Demo @@ -14647,4 +14647,4 @@ 2728 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/insurance_role_test.xml b/src/main/resources/petriNets/insurance_role_test.xml index f9bd098e3fe..aa7732e0fbb 100644 --- a/src/main/resources/petriNets/insurance_role_test.xml +++ b/src/main/resources/petriNets/insurance_role_test.xml @@ -1,7 +1,7 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> false @@ -14651,4 +14651,4 @@ 2728 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/leukemia.xml b/src/main/resources/petriNets/leukemia.xml index 8597ba75d02..cde6b23433b 100644 --- a/src/main/resources/petriNets/leukemia.xml +++ b/src/main/resources/petriNets/leukemia.xml @@ -1,5 +1,5 @@ - + examples/leukemia/leukemia 1.0.0 LEU @@ -269,4 +269,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/leukemia_en.xml b/src/main/resources/petriNets/leukemia_en.xml index 0bee06055c3..ffe7581f337 100644 --- a/src/main/resources/petriNets/leukemia_en.xml +++ b/src/main/resources/petriNets/leukemia_en.xml @@ -1,5 +1,5 @@ - + leukemia/leu 1.0.0 LEU diff --git a/src/main/resources/petriNets/mortgage/address.xml b/src/main/resources/petriNets/mortgage/address.xml index 6cd0a197f23..2b100032882 100644 --- a/src/main/resources/petriNets/mortgage/address.xml +++ b/src/main/resources/petriNets/mortgage/address.xml @@ -1,5 +1,5 @@ - + address ADD Address diff --git a/src/main/resources/petriNets/mortgage/financial_data.xml b/src/main/resources/petriNets/mortgage/financial_data.xml index c7299cc4946..035fd5ab531 100644 --- a/src/main/resources/petriNets/mortgage/financial_data.xml +++ b/src/main/resources/petriNets/mortgage/financial_data.xml @@ -1,5 +1,5 @@ - + financial_data FIN Financial Data @@ -188,4 +188,4 @@ financial_data 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/financial_data_doc.xml b/src/main/resources/petriNets/mortgage/financial_data_doc.xml index f0bc35d2a21..f61bd98057b 100644 --- a/src/main/resources/petriNets/mortgage/financial_data_doc.xml +++ b/src/main/resources/petriNets/mortgage/financial_data_doc.xml @@ -1,5 +1,5 @@ - + financial_data_doc FDC Financial Data Doc @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/financial_data_func.xml b/src/main/resources/petriNets/mortgage/financial_data_func.xml index 52b9795eee5..400b8604216 100644 --- a/src/main/resources/petriNets/mortgage/financial_data_func.xml +++ b/src/main/resources/petriNets/mortgage/financial_data_func.xml @@ -1,5 +1,5 @@ - + financial_data_func FIN Financial Data @@ -192,4 +192,4 @@ financial_data 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/mortgage.xml b/src/main/resources/petriNets/mortgage/mortgage.xml index 1421b036026..ac51fd99e94 100644 --- a/src/main/resources/petriNets/mortgage/mortgage.xml +++ b/src/main/resources/petriNets/mortgage/mortgage.xml @@ -1,5 +1,5 @@ - + mortgage MOR Mortgage @@ -919,4 +919,4 @@ 16 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/mortgage_modeler.xml b/src/main/resources/petriNets/mortgage/mortgage_modeler.xml index 5446579f92d..fadb1042f65 100644 --- a/src/main/resources/petriNets/mortgage/mortgage_modeler.xml +++ b/src/main/resources/petriNets/mortgage/mortgage_modeler.xml @@ -1,5 +1,5 @@ - + mortgage 1.0.0 MOR diff --git a/src/main/resources/petriNets/mortgage/personal_information.xml b/src/main/resources/petriNets/mortgage/personal_information.xml index ccc45ed691b..693af2b11f8 100644 --- a/src/main/resources/petriNets/mortgage/personal_information.xml +++ b/src/main/resources/petriNets/mortgage/personal_information.xml @@ -1,5 +1,5 @@ - + personal_information PER Personal Information @@ -65,4 +65,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/petriflow_schema.xsd b/src/main/resources/petriNets/petriflow_schema.xsd index 28e88a26d6f..0bbd2530d69 100644 --- a/src/main/resources/petriNets/petriflow_schema.xsd +++ b/src/main/resources/petriNets/petriflow_schema.xsd @@ -3,6 +3,6 @@ - + - \ No newline at end of file + diff --git a/src/main/resources/petriNets/posudky.xml b/src/main/resources/petriNets/posudky.xml index b91919f3a4d..3256813e8cd 100644 --- a/src/main/resources/petriNets/posudky.xml +++ b/src/main/resources/petriNets/posudky.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> management/posudky Posudky PSD @@ -348,4 +348,4 @@ 19 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/taskRef-propagation/child.xml b/src/main/resources/petriNets/taskRef-propagation/child.xml index c4143c30485..06830ac7ee1 100644 --- a/src/main/resources/petriNets/taskRef-propagation/child.xml +++ b/src/main/resources/petriNets/taskRef-propagation/child.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> child 1.0.0 CHL diff --git a/src/main/resources/petriNets/taskRef-propagation/parent.xml b/src/main/resources/petriNets/taskRef-propagation/parent.xml index bd50af718cc..84e9ed7e2eb 100644 --- a/src/main/resources/petriNets/taskRef-propagation/parent.xml +++ b/src/main/resources/petriNets/taskRef-propagation/parent.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> parent 1.0.0 PAR diff --git a/src/main/resources/petriNets/test_model_immediate_data.xml b/src/main/resources/petriNets/test_model_immediate_data.xml index 7ef55217802..eefe3b4c0ae 100644 --- a/src/main/resources/petriNets/test_model_immediate_data.xml +++ b/src/main/resources/petriNets/test_model_immediate_data.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> true Nový prípad @@ -326,4 +326,4 @@ 7 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/wizard.xml b/src/main/resources/petriNets/wizard.xml index c4acf2f9dd8..05d3d8cda4d 100644 --- a/src/main/resources/petriNets/wizard.xml +++ b/src/main/resources/petriNets/wizard.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> text text @@ -243,4 +243,4 @@ 3 1 - \ No newline at end of file + diff --git a/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy b/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy index 08017730977..56cd0c04d9b 100644 --- a/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy @@ -158,7 +158,7 @@ class DataSearchRequestTest { new AbstractMap.SimpleEntry("user.emailValue.keyword" as String, "${testUser1.email}" as String), new AbstractMap.SimpleEntry("user.fullNameValue.keyword" as String, "${testUser1.fullName}" as String), new AbstractMap.SimpleEntry("user.userIdValue" as String, "${testUser1.getId()}" as String), - new AbstractMap.SimpleEntry("date.timestampValue" as String, "${Timestamp.valueOf(LocalDateTime.of(date, LocalTime.NOON)).getTime()}" as String), + new AbstractMap.SimpleEntry("date.timestampValue" as String, "${Timestamp.valueOf(LocalDateTime.of(date, LocalTime.MIDNIGHT)).getTime()}" as String), new AbstractMap.SimpleEntry("datetime.timestampValue" as String, "${Timestamp.valueOf(date.atTime(13, 37)).getTime()}" as String), new AbstractMap.SimpleEntry("enumeration" as String, "Alice" as String), new AbstractMap.SimpleEntry("enumeration" as String, "Alica" as String), diff --git a/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java new file mode 100644 index 00000000000..4abac8a4822 --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java @@ -0,0 +1,71 @@ +package com.netgrif.application.engine.export.service; + +import com.netgrif.application.engine.auth.domain.IUser; +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.export.service.interfaces.IXlsExportService; +import com.netgrif.application.engine.startup.FilterRunner; +import com.netgrif.application.engine.startup.SuperCreator; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.io.File; +import java.util.List; +import java.util.Locale; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class XlsExportServiceTest { + + @Autowired + private IXlsExportService xlsExportService; + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private SuperCreator superCreator; + + @Test + void shouldCreateXlsxFile() throws Exception { + LoggedUser superUser = superCreator.getSuperUser().transformToLoggedUser(); + + IntStream.range(0,5).forEach(idx -> workflowService.createCaseByIdentifier(FilterRunner.MENU_NET_IDENTIFIER, "Test case", "", superUser)); + + FilteredCasesRequest request = getTestRequest(); + File excel = xlsExportService.getExportFilteredCasesFile(request, superUser, Locale.ENGLISH); + assertNotNull(excel); + assertTrue(excel.getName().endsWith(".xlsx")); + assertTrue(excel.length() > 0); + + // Clean up + boolean deleted = excel.delete(); + assertTrue(deleted); + } + + FilteredCasesRequest getTestRequest() { + FilteredCasesRequest request = new FilteredCasesRequest(); + request.setQuery(List.of( + CaseSearchRequest.builder() + .query("processIdentifier:" + FilterRunner.MENU_NET_IDENTIFIER) + .build())); + request.setSelectedDataFieldNames(List.of("Menu Item Identifier", "Item URI", "Menu icon identifier", "Name of the item", "Tab icon identifier", "Name of the item")); + request.setSelectedDataFieldIds(List.of("menu_item_identifier", "nodePath", "menu_icon", "menu_name", "tab_icon", "tab_name")); + request.setIsIntersection(true); + return request; + } + + +} diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy index ce970f467d6..e30f8af7f85 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy @@ -1,5 +1,7 @@ package com.netgrif.application.engine.workflow +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.auth.domain.Authority import com.netgrif.application.engine.auth.domain.User @@ -9,7 +11,6 @@ import com.netgrif.application.engine.auth.service.interfaces.IUserService import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.VersionType -import com.netgrif.application.engine.petrinet.domain.dataset.FileFieldValue import com.netgrif.application.engine.petrinet.domain.dataset.FileListFieldValue import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole import com.netgrif.application.engine.petrinet.service.ProcessRoleService @@ -18,12 +19,12 @@ import com.netgrif.application.engine.startup.ImportHelper import com.netgrif.application.engine.startup.SuperCreator import com.netgrif.application.engine.utils.FullPageRequest import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.DataField import com.netgrif.application.engine.workflow.domain.Task import com.netgrif.application.engine.workflow.service.TaskSearchService import com.netgrif.application.engine.workflow.service.TaskService import com.netgrif.application.engine.workflow.service.interfaces.IDataService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService -import com.netgrif.application.engine.workflow.web.PublicTaskController import com.netgrif.application.engine.workflow.web.TaskController import com.netgrif.application.engine.workflow.web.WorkflowController import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest @@ -87,7 +88,9 @@ class TaskControllerTest { @Autowired private TaskController taskController - private PetriNet net + private PetriNet allDataNet + + private PetriNet setDataNet private Case useCase @@ -106,7 +109,7 @@ class TaskControllerTest { state: UserState.ACTIVE, authorities: [authorityService.getOrCreate(Authority.user)] as Set, processRoles: [] as Set)) - importNet() + importNets() } @Test @@ -118,7 +121,7 @@ class TaskControllerTest { @Test void testDeleteFile() { - Case testCase = helper.createCase("My case", net) + Case testCase = helper.createCase("My case", allDataNet) String taskId = testCase.tasks.find {it.transition == "1"}.task dataService.saveFile(taskId, "file", new MockMultipartFile("test", new byte[] {})) @@ -132,7 +135,7 @@ class TaskControllerTest { @Test void testDeleteFileByName() { - Case testCase = helper.createCase("My case", net) + Case testCase = helper.createCase("My case", allDataNet) String taskId = testCase.tasks.find {it.transition == "1"}.task dataService.saveFiles(taskId, "fileList", new MockMultipartFile[] {new MockMultipartFile("test", "test", null, new byte[] {})}) @@ -144,6 +147,79 @@ class TaskControllerTest { assert ((FileListFieldValue) testCase.dataSet["fileList"].value).namesPaths == null || ((FileListFieldValue) testCase.dataSet["fileList"].value).namesPaths.size() == 0 } + @Test + void testSetDataFieldTypeRestriction() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "testSetDataFieldTypeRestriction" }.task + + ObjectNode dataSet = populateNestedDataset([(taskId):["taskRef_0": ["type": "taskRef", "value": [taskId]]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert ((List) workflowService.findOne(testCase.stringId).getDataField("taskRef_0").getValue()).isEmpty() + + dataSet = populateNestedDataset([(taskId):["caseRef_0": ["type": "caseRef", "value": [testCase.stringId]]]]) + response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert ((List) workflowService.findOne(testCase.stringId).getDataField("caseRef_0").getValue()).isEmpty() + } + + @Test + void testSetDataVisibleField() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "data" }.task + + ObjectNode dataSet = populateNestedDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome == null + assert response.content.error != null + + // todo: test visible behavior based on parent taskRef behavior + } + + @Test + void testSetDataNestedTaskRefRestrictions() { + Case testCase1 = helper.createCase("testCase1", setDataNet) + String taskId = testCase1.tasks.find { it.transition == "testSetDataNestedTaskRefRestrictions" }.task + Case testCase2 = helper.createCase("testCase2", setDataNet) + Case testCase3 = helper.createCase("testCase3", setDataNet) + + DataField case1DataField = testCase1.getDataField("taskRef_0") + case1DataField.setValue(List.of(testCase2.tasks.find { it.transition == "testSetDataNestedTaskRefRestrictions" }.task)) + workflowService.save(testCase1) + + DataField case2DataField = testCase2.getDataField("taskRef_0") + case2DataField.setValue(List.of(testCase3.tasks.find { it.transition == "data" }.task)) + workflowService.save(testCase2) + + String nestedOtherTaskId = testCase2.tasks.find { it.transition == "data" }.task + ObjectNode dataSet = populateNestedDataset([(nestedOtherTaskId):["text_0": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert workflowService.findOne(testCase2.stringId).getDataField("text_0").getValue() == null + + String nestedTaskId = testCase3.tasks.find { it.transition == "data" }.task + dataSet = populateNestedDataset([(nestedTaskId):["text_0": ["type": "text", "value": "awd"]]]) + response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert !response.content.outcome.changedFields.changedFields.isEmpty() + assert workflowService.findOne(testCase3.stringId).getDataField("text_0").getValue() == "awd" + } + + @Test + void testSetDataNonReferencedField() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "testSetDataNonReferencedField" }.task + + ObjectNode dataSet = populateNestedDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + + assert response != null && response.content.outcome == null + assert response.content.error != null + } + void testWithRoleAndUserref() { createCase() @@ -167,15 +243,19 @@ class TaskControllerTest { assert !findTasksByMongo().empty } - void importNet() { - PetriNet netOptional = helper.createNet("all_data_refs.xml", VersionType.MAJOR).get() - assert netOptional != null - net = netOptional + void importNets() { + PetriNet allDataNet = helper.createNet("all_data_refs.xml", VersionType.MAJOR).get() + assert allDataNet != null + this.allDataNet = allDataNet + + PetriNet setDataNet = helper.createNet("task_controller_set_data.xml", VersionType.MAJOR).get() + assert setDataNet != null + this.setDataNet = setDataNet } void createCase() { useCase = null - useCase = helper.createCase("My case", net) + useCase = helper.createCase("My case", allDataNet) assert useCase != null } @@ -203,7 +283,7 @@ class TaskControllerTest { } void setUserRole() { - List roles = processRoleService.findAll(net.stringId) + List roles = processRoleService.findAll(allDataNet.stringId) for (ProcessRole role : roles) { if (role.importId == "process_role") { @@ -219,4 +299,10 @@ class TaskControllerTest { Page tasks = taskService.search(taskSearchRequestList, new FullPageRequest(), userService.findByEmail(DUMMY_USER_MAIL, false).transformToLoggedUser(), new Locale("en"), false) return tasks } + + static ObjectNode populateNestedDataset(Map>> data) { + ObjectMapper mapper = new ObjectMapper() + String json = mapper.writeValueAsString(data) + return mapper.readTree(json) as ObjectNode + } } diff --git a/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java b/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java new file mode 100644 index 00000000000..874e29fd24e --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java @@ -0,0 +1,41 @@ +package com.netgrif.application.engine.auth.service; + +import com.netgrif.application.engine.TestHelper; +import com.netgrif.application.engine.auth.domain.IUser; +import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class UserServiceTest { + + @Autowired + private IUserService service; + + @Autowired + private TestHelper testHelper; + + @BeforeEach + void before() { + testHelper.truncateDbs(); + } + + @Test + public void shouldUserExist() { + IUser user = service.findByEmail("super@netgrif.com", true); + assertNotNull(user); + boolean userExists = service.existsById(user.getStringId()); + assertTrue(userExists); + } + +} diff --git a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java new file mode 100644 index 00000000000..884436bebd0 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java @@ -0,0 +1,205 @@ +package com.netgrif.application.engine.elastic; + +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ElasticsearchQuerySanitizerTest { + + @Test + void shouldSanitizeQuery() { + String query = "identifier: some_value AND field.keyword.value <> other_value"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertNotNull(sanitized); + assertEquals("identifier\\:\\ some_value\\ \\A\\N\\D\\ field.keyword.value\\ \\ \\ " + + "\\ other_value", sanitized); + } + + @Test + void shouldEscapeReservedCharacters() { + String query = "test\\query"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("test\\\\query", sanitized); + } + + @Test + void shouldEscapeSpecialOperators() { + String query = "field: value + other - something = test"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field\\:\\ value\\ \\+\\ other\\ \\-\\ something\\ \\=\\ test", sanitized); + } + + @Test + void shouldEscapeLogicalOperators() { + String query = "field1 && field2 || field3"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field1\\ \\&\\&\\ field2\\ \\|\\|\\ field3", sanitized); + } + + @Test + void shouldEscapeBracketsAndParentheses() { + String query = "field: (value) {range} [array]"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field\\:\\ \\(value\\)\\ \\{range\\}\\ \\[array\\]", sanitized); + } + + @Test + void shouldEscapeWildcardsAndSpecialChars() { + String query = "test*query?with^special\"chars~"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("test\\*query\\?with\\^special\\\"chars\\~", sanitized); + } + + @Test + void shouldRemoveAngleBrackets() { + String query = "value<100 AND value>50"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("value\\ 100\\ \\A\\N\\D\\ value\\ 50", sanitized); + } + + @Test + void shouldEscapeSlashes() { + String query = "path/to/resource"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("path\\/to\\/resource", sanitized); + } + + @Test + void shouldEscapeNegationOperator() { + String query = "!important"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("\\!important", sanitized); + } + + @Test + void shouldEscapeKeywordsAndOrNot() { + String query = "field1 AND field2 OR field3 NOT field4"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field1\\ \\A\\N\\D\\ field2\\ \\O\\R\\ field3\\ \\N\\O\\T\\ field4", sanitized); + } + + @Test + void shouldHandleNullQuery() { + String sanitized = ElasticsearchQuerySanitizer.sanitize(null); + assertNull(sanitized); + } + + @Test + void shouldHandleEmptyQuery() { + String sanitized = ElasticsearchQuerySanitizer.sanitize(""); + assertEquals("", sanitized); + } + + @Test + void shouldHandleBlankQuery() { + String sanitized = ElasticsearchQuerySanitizer.sanitize(" "); + assertEquals(" ", sanitized); + } + + @Test + void shouldHandleMultipleReservedCharactersInSequence() { + String query = "field:value&&another||test"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field\\:value\\&\\&another\\|\\|test", sanitized); + } + + @Test + void shouldSanitizeWithExcludedKeywords() { + String query = "field:value AND other:test"; + String[] exclude = {"AND"," "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:value AND other\\:test", sanitized); + } + + @Test + void shouldSanitizeWithMultipleExcludedKeywords() { + String query = "field:value AND other:test OR another:value"; + String[] exclude = {"AND", "OR", " "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:value AND other\\:test OR another\\:value", sanitized); + } + + @Test + void shouldSanitizeWithExcludedSpecialCharacters() { + String query = "field:value + test - other"; + String[] exclude = {"+", "-", " "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:value + test - other", sanitized); + } + + @Test + void shouldHandleNullExcludeArray() { + String query = "field: value AND test"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, null); + assertEquals("field\\:\\ value\\ \\A\\N\\D\\ test", sanitized); + } + + @Test + void shouldHandleEmptyExcludeArray() { + String query = "field: value AND test"; + String[] exclude = {}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:\\ value\\ \\A\\N\\D\\ test", sanitized); + } + + @Test + void shouldIgnoreNonReservedKeywordsInExclude() { + String query = "field: value AND test"; + String[] exclude = {"NONEXISTENT", "INVALID"}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:\\ value\\ \\A\\N\\D\\ test", sanitized); + } + + @Test + void shouldHandleComplexQueryWithMixedCharacters() { + String query = "title:(\"test query\" AND status:active) OR tags:[java, spring] && created_at:[2023 TO 2024]"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertNotNull(sanitized); + assertTrue(sanitized.contains("\\:")); + assertTrue(sanitized.contains("\\(")); + assertTrue(sanitized.contains("\\)")); + assertTrue(sanitized.contains("\\[")); + assertTrue(sanitized.contains("\\]")); + } + + @Test + void shouldEscapeSpaces() { + String query = "hello world"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("hello\\ world", sanitized); + } + + @Test + void shouldHandleQueryWithOnlyReservedCharacters() { + String query = "+-=!(){}[]^\"~*?:/"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertNotNull(sanitized); + assertNotEquals(query, sanitized); + } + + @Test + void shouldNotModifyQueryWithoutReservedCharacters() { + String query = "simple search query"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + // Note: space is a reserved character, so this will be escaped + assertEquals("simple\\ search\\ query", sanitized); + } + + @Test + void shouldNotModifyQueryWithoutReservedCharactersExcludingSpace() { + String query = "simple search query"; + String[] exclude = {" "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + // Note: space is a reserved character, so this will be escaped + assertEquals("simple search query", sanitized); + } + + @Test + void shouldNotModifyQueryWithoutAnyReservedCharacters() { + String query = "simplesearchquery"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("simplesearchquery", sanitized); + } + +} diff --git a/src/test/resources/actionref_test.xml b/src/test/resources/actionref_test.xml index 16ec8c1d9d7..a9693d9cbdc 100644 --- a/src/test/resources/actionref_test.xml +++ b/src/test/resources/actionref_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> actionref_test.xml TST Test @@ -125,4 +125,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/all_data.xml b/src/test/resources/all_data.xml index 076374ca251..b1038511029 100644 --- a/src/test/resources/all_data.xml +++ b/src/test/resources/all_data.xml @@ -1,5 +1,5 @@ - + all_data All Data ALL diff --git a/src/test/resources/all_data_pdf.xml b/src/test/resources/all_data_pdf.xml index e2d8a3f82df..408e03836b3 100644 --- a/src/test/resources/all_data_pdf.xml +++ b/src/test/resources/all_data_pdf.xml @@ -1,5 +1,5 @@ - + all_data All Data ALL diff --git a/src/test/resources/arc_order_test.xml b/src/test/resources/arc_order_test.xml index 27d9be69bb2..8a63dff1e68 100644 --- a/src/test/resources/arc_order_test.xml +++ b/src/test/resources/arc_order_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> arc_order_test.xml TST Test @@ -35,4 +35,4 @@ 1 1 - \ No newline at end of file + diff --git a/src/test/resources/arc_reference_invalid_test.xml b/src/test/resources/arc_reference_invalid_test.xml index 85ade008524..92d83316250 100644 --- a/src/test/resources/arc_reference_invalid_test.xml +++ b/src/test/resources/arc_reference_invalid_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> arc_ref_invalid_test ARi @@ -29,4 +29,4 @@ 1 3 - \ No newline at end of file + diff --git a/src/test/resources/arc_reference_test.xml b/src/test/resources/arc_reference_test.xml index 63a241b6724..8ddfde6422d 100644 --- a/src/test/resources/arc_reference_test.xml +++ b/src/test/resources/arc_reference_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> arc_ref_test ART Arc ref test @@ -29,4 +29,4 @@ 1 2 - \ No newline at end of file + diff --git a/src/test/resources/assignRoleMainNet_test_.xml b/src/test/resources/assignRoleMainNet_test_.xml index 8c5bbd271df..53a21a1703e 100644 --- a/src/test/resources/assignRoleMainNet_test_.xml +++ b/src/test/resources/assignRoleMainNet_test_.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> main ORG main @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/assignRoleSecondaryNet_test.xml b/src/test/resources/assignRoleSecondaryNet_test.xml index f81db9ed386..493dfab39a2 100644 --- a/src/test/resources/assignRoleSecondaryNet_test.xml +++ b/src/test/resources/assignRoleSecondaryNet_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> secondary ORG secondary @@ -14,4 +14,4 @@ admin_secondary admin_secondary - \ No newline at end of file + diff --git a/src/test/resources/assign_cancel_finish_with_Case_test.xml b/src/test/resources/assign_cancel_finish_with_Case_test.xml index fade333fb77..eb87a06e4c7 100644 --- a/src/test/resources/assign_cancel_finish_with_Case_test.xml +++ b/src/test/resources/assign_cancel_finish_with_Case_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> assign_cancel_finish_with_Case_net TST Test @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/autotrigger_taskref.xml b/src/test/resources/autotrigger_taskref.xml index a64af65355e..eaa031db8cb 100644 --- a/src/test/resources/autotrigger_taskref.xml +++ b/src/test/resources/autotrigger_taskref.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> Praca PRC Práca diff --git a/src/test/resources/case_choices_test.xml b/src/test/resources/case_choices_test.xml index 86a3ebc33fd..8c9f355a071 100644 --- a/src/test/resources/case_choices_test.xml +++ b/src/test/resources/case_choices_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -50,4 +50,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/case_search_test.xml b/src/test/resources/case_search_test.xml index 86c2e06ffec..7c8db1daaca 100644 --- a/src/test/resources/case_search_test.xml +++ b/src/test/resources/case_search_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_search_test.xml TST Test @@ -70,4 +70,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/caseref_test.xml b/src/test/resources/caseref_test.xml index cc558f0a17b..3df3f896cf7 100644 --- a/src/test/resources/caseref_test.xml +++ b/src/test/resources/caseref_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> caseref_test.xml TST Test @@ -128,4 +128,4 @@ 8 1 - \ No newline at end of file + diff --git a/src/test/resources/change_allowed_nets_action_test.xml b/src/test/resources/change_allowed_nets_action_test.xml index c47c34aa97e..9c9400a16c6 100644 --- a/src/test/resources/change_allowed_nets_action_test.xml +++ b/src/test/resources/change_allowed_nets_action_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -55,4 +55,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/change_caseref_value_action_test.xml b/src/test/resources/change_caseref_value_action_test.xml index e82f26c7613..10ebcae9a95 100644 --- a/src/test/resources/change_caseref_value_action_test.xml +++ b/src/test/resources/change_caseref_value_action_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> change_value TST Test @@ -81,4 +81,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/constructor_destructor.xml b/src/test/resources/constructor_destructor.xml index 043e2907219..25629c073a7 100644 --- a/src/test/resources/constructor_destructor.xml +++ b/src/test/resources/constructor_destructor.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> constructor_destructor CAD Constructor and Destructor diff --git a/src/test/resources/create_case_locale.xml b/src/test/resources/create_case_locale.xml index 99fe3759b8a..00676b3b6a6 100644 --- a/src/test/resources/create_case_locale.xml +++ b/src/test/resources/create_case_locale.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> currency_test.xml TST Test @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/currency_test.xml b/src/test/resources/currency_test.xml index 1fdf059b642..f806c113078 100644 --- a/src/test/resources/currency_test.xml +++ b/src/test/resources/currency_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> currency_test.xml TST Test @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/data_button_test.xml b/src/test/resources/data_button_test.xml index 8d929164844..95d4c80ca9d 100644 --- a/src/test/resources/data_button_test.xml +++ b/src/test/resources/data_button_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/data_map.xml b/src/test/resources/data_map.xml index cb947645633..b87ee09ec38 100644 --- a/src/test/resources/data_map.xml +++ b/src/test/resources/data_map.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -27,4 +27,4 @@ Zweite Option Dritte Option - \ No newline at end of file + diff --git a/src/test/resources/data_map_2.xml b/src/test/resources/data_map_2.xml index e415c4e3e28..29fdf4f8d4f 100644 --- a/src/test/resources/data_map_2.xml +++ b/src/test/resources/data_map_2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -30,4 +30,4 @@ Zweite Option Dritte Option - \ No newline at end of file + diff --git a/src/test/resources/data_service_referenced.xml b/src/test/resources/data_service_referenced.xml index ce077430b8d..01343e84a8e 100644 --- a/src/test/resources/data_service_referenced.xml +++ b/src/test/resources/data_service_referenced.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> referenced 1.0.0 RFD diff --git a/src/test/resources/data_service_taskref.xml b/src/test/resources/data_service_taskref.xml index 071cef8ced8..d20c3fbe906 100644 --- a/src/test/resources/data_service_taskref.xml +++ b/src/test/resources/data_service_taskref.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> refering 1.0.0 REF diff --git a/src/test/resources/data_test.xml b/src/test/resources/data_test.xml index a9e4feb8786..6fe0ca71154 100644 --- a/src/test/resources/data_test.xml +++ b/src/test/resources/data_test.xml @@ -1,5 +1,5 @@ - + 1 TES Data test @@ -269,4 +269,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/data_text_validation.xml b/src/test/resources/data_text_validation.xml index 2c9f5a9d082..cf90b3a361e 100644 --- a/src/test/resources/data_text_validation.xml +++ b/src/test/resources/data_text_validation.xml @@ -1,6 +1,6 @@ F + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -265,4 +265,4 @@ F - \ No newline at end of file + diff --git a/src/test/resources/datagroup_test_layout.xml b/src/test/resources/datagroup_test_layout.xml index 4e5a0b25667..29202ba0c28 100644 --- a/src/test/resources/datagroup_test_layout.xml +++ b/src/test/resources/datagroup_test_layout.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> datagroup_test DGT Data group test diff --git a/src/test/resources/enum_list.xml b/src/test/resources/enum_list.xml index 808a464f4dd..f9f5893bffa 100644 --- a/src/test/resources/enum_list.xml +++ b/src/test/resources/enum_list.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -143,4 +143,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/enumeration_multichoice_options.xml b/src/test/resources/enumeration_multichoice_options.xml index e5ca5d5b9d7..e0bcfc26be5 100644 --- a/src/test/resources/enumeration_multichoice_options.xml +++ b/src/test/resources/enumeration_multichoice_options.xml @@ -1,5 +1,5 @@ - + enumeration_multichoice_options Enumeration/multichoice options EMO diff --git a/src/test/resources/event_test.xml b/src/test/resources/event_test.xml index 7821605b8b4..49715c88f7b 100644 --- a/src/test/resources/event_test.xml +++ b/src/test/resources/event_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -121,4 +121,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/field_view.xml b/src/test/resources/field_view.xml index 7c4dda54c1b..4a771ee1860 100644 --- a/src/test/resources/field_view.xml +++ b/src/test/resources/field_view.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/file_test.xml b/src/test/resources/file_test.xml index 8bd07b46832..7557663904a 100644 --- a/src/test/resources/file_test.xml +++ b/src/test/resources/file_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -61,4 +61,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/file_test_net.xml b/src/test/resources/file_test_net.xml index 80ea01e33d1..247c8fdf1cb 100644 --- a/src/test/resources/file_test_net.xml +++ b/src/test/resources/file_test_net.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -101,4 +101,4 @@ 6 1 - \ No newline at end of file + diff --git a/src/test/resources/flow.xml b/src/test/resources/flow.xml index 54540fb0ff9..4a21ac59be5 100644 --- a/src/test/resources/flow.xml +++ b/src/test/resources/flow.xml @@ -1,4 +1,4 @@ - + new_model NEW New Model diff --git a/src/test/resources/initial_behavior.xml b/src/test/resources/initial_behavior.xml index e169b58a7b6..26842135573 100644 --- a/src/test/resources/initial_behavior.xml +++ b/src/test/resources/initial_behavior.xml @@ -1,5 +1,5 @@ - + initial_behavior Initial behavior IBH diff --git a/src/test/resources/ipc_bulk.xml b/src/test/resources/ipc_bulk.xml index 11a6eac4416..f6e150eeeba 100644 --- a/src/test/resources/ipc_bulk.xml +++ b/src/test/resources/ipc_bulk.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -35,4 +35,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/ipc_createCase.xml b/src/test/resources/ipc_createCase.xml index b78a5f1deef..09a03611dcd 100644 --- a/src/test/resources/ipc_createCase.xml +++ b/src/test/resources/ipc_createCase.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> create_case_net TST Test @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/ipc_data.xml b/src/test/resources/ipc_data.xml index eea9eca299a..4a99b3de73c 100644 --- a/src/test/resources/ipc_data.xml +++ b/src/test/resources/ipc_data.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -89,4 +89,4 @@ 3 1 - \ No newline at end of file + diff --git a/src/test/resources/ipc_group.xml b/src/test/resources/ipc_group.xml index 356b05e4cd4..85f837c1c53 100644 --- a/src/test/resources/ipc_group.xml +++ b/src/test/resources/ipc_group.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -42,4 +42,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/ipc_set_data.xml b/src/test/resources/ipc_set_data.xml index 97e5fff993b..1e94beee85c 100644 --- a/src/test/resources/ipc_set_data.xml +++ b/src/test/resources/ipc_set_data.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -71,4 +71,4 @@ 2 1 - \ No newline at end of file + diff --git a/src/test/resources/ipc_task_search.xml b/src/test/resources/ipc_task_search.xml index d893e2de581..c9211dbde03 100644 --- a/src/test/resources/ipc_task_search.xml +++ b/src/test/resources/ipc_task_search.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> create_case_net TST Test @@ -56,4 +56,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/ipc_transition_role.xml b/src/test/resources/ipc_transition_role.xml index 44780c237e7..696d51769e1 100644 --- a/src/test/resources/ipc_transition_role.xml +++ b/src/test/resources/ipc_transition_role.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -41,4 +41,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/ipc_where.xml b/src/test/resources/ipc_where.xml index e2202dacad9..c9d6991a51f 100644 --- a/src/test/resources/ipc_where.xml +++ b/src/test/resources/ipc_where.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -76,4 +76,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/mapping_test.xml b/src/test/resources/mapping_test.xml index d7ab7541dd6..960b0f5f94b 100755 --- a/src/test/resources/mapping_test.xml +++ b/src/test/resources/mapping_test.xml @@ -1,7 +1,7 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -898,4 +898,4 @@ 42 1 - \ No newline at end of file + diff --git a/src/test/resources/nae-1272_datagroup_divider_improvement.xml b/src/test/resources/nae-1272_datagroup_divider_improvement.xml index 4acaa3bdea1..82ebc005f2e 100644 --- a/src/test/resources/nae-1272_datagroup_divider_improvement.xml +++ b/src/test/resources/nae-1272_datagroup_divider_improvement.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> nae_1272 NAE NAE-1272 Datagroup divider improvement @@ -154,4 +154,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/net_clone.xml b/src/test/resources/net_clone.xml index 31c6e185104..4f54118ad3e 100644 --- a/src/test/resources/net_clone.xml +++ b/src/test/resources/net_clone.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -73,4 +73,4 @@ 340 - \ No newline at end of file + diff --git a/src/test/resources/net_import_1.xml b/src/test/resources/net_import_1.xml index 1b6eb07a99b..3f0d055e005 100644 --- a/src/test/resources/net_import_1.xml +++ b/src/test/resources/net_import_1.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_model 1.0.0 NEW @@ -55,4 +55,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/net_import_2.xml b/src/test/resources/net_import_2.xml index 520d3c44045..8593fc96c14 100644 --- a/src/test/resources/net_import_2.xml +++ b/src/test/resources/net_import_2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_model 1.0.0 NEW @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/org_group.xml b/src/test/resources/org_group.xml index 0bbc98fe2af..5f03f2ceb61 100644 --- a/src/test/resources/org_group.xml +++ b/src/test/resources/org_group.xml @@ -1,5 +1,5 @@ - + org_group 1.0.0 GRP @@ -489,4 +489,4 @@ 28 1 - \ No newline at end of file + diff --git a/src/test/resources/pdf_run_action.xml b/src/test/resources/pdf_run_action.xml index 2797aa5fda8..cba7da838c1 100644 --- a/src/test/resources/pdf_run_action.xml +++ b/src/test/resources/pdf_run_action.xml @@ -1,4 +1,4 @@ - + 1 TES PDF test @@ -446,4 +446,4 @@ 1_delegate - \ No newline at end of file + diff --git a/src/test/resources/pdf_test_1.xml b/src/test/resources/pdf_test_1.xml index 7ffd56d668f..fd4222adc5e 100644 --- a/src/test/resources/pdf_test_1.xml +++ b/src/test/resources/pdf_test_1.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> 1 TES Data test diff --git a/src/test/resources/pdf_test_2.xml b/src/test/resources/pdf_test_2.xml index 24b53b5c11b..15831035c50 100644 --- a/src/test/resources/pdf_test_2.xml +++ b/src/test/resources/pdf_test_2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -143,4 +143,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/pdf_test_3.xml b/src/test/resources/pdf_test_3.xml index a7ad5d02232..4755336cbac 100644 --- a/src/test/resources/pdf_test_3.xml +++ b/src/test/resources/pdf_test_3.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_model 1.0.0 PER diff --git a/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml b/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml index df867dae6fa..729ea089a57 100644 --- a/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml +++ b/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> NAE_1305_Loading_na_set_data_pre_button ALL NAE_1305_Loading_na_set_data_pre_button @@ -127,4 +127,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml b/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml index 7fcf6574552..ea2f20a7173 100644 --- a/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml +++ b/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml @@ -1,5 +1,5 @@ - + NAE_1382_first_trans_auto ERR New Model diff --git a/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml b/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml index c6e47776721..e2bf2005b9e 100644 --- a/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml +++ b/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml @@ -1,5 +1,5 @@ - + NAE_1382_first_trans_auto ERR New Model diff --git a/src/test/resources/petriNets/action_delegate_concurrency_test.xml b/src/test/resources/petriNets/action_delegate_concurrency_test.xml index 4523fe8d791..935577e75ff 100644 --- a/src/test/resources/petriNets/action_delegate_concurrency_test.xml +++ b/src/test/resources/petriNets/action_delegate_concurrency_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> action_delegate_concurrency_test 1.0.0 TRE @@ -36,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/all_data_refs.xml b/src/test/resources/petriNets/all_data_refs.xml index 69eb782f3b3..c261c14cbd1 100644 --- a/src/test/resources/petriNets/all_data_refs.xml +++ b/src/test/resources/petriNets/all_data_refs.xml @@ -1,5 +1,5 @@ - + all_data All Data ALL diff --git a/src/test/resources/petriNets/change_field_value_init.xml b/src/test/resources/petriNets/change_field_value_init.xml index f34a6c21feb..6bef2b17ba0 100644 --- a/src/test/resources/petriNets/change_field_value_init.xml +++ b/src/test/resources/petriNets/change_field_value_init.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> change_field_value_init change_field_value_init true @@ -121,4 +121,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/changed_fields_allowed_nets.xml b/src/test/resources/petriNets/changed_fields_allowed_nets.xml index d88677a8a3a..1f3c66814f0 100644 --- a/src/test/resources/petriNets/changed_fields_allowed_nets.xml +++ b/src/test/resources/petriNets/changed_fields_allowed_nets.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> changed_fields_allowed_nets NEW New Model diff --git a/src/test/resources/petriNets/data_actions_test.xml b/src/test/resources/petriNets/data_actions_test.xml index e80ffca7ae2..5cc07d079d1 100644 --- a/src/test/resources/petriNets/data_actions_test.xml +++ b/src/test/resources/petriNets/data_actions_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> data_actions_test 1.0.0 tst @@ -118,4 +118,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_case_name_test.xml b/src/test/resources/petriNets/dynamic_case_name_test.xml index cca7a064cfa..21594887fa6 100644 --- a/src/test/resources/petriNets/dynamic_case_name_test.xml +++ b/src/test/resources/petriNets/dynamic_case_name_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_choices dynamic_choices true @@ -30,4 +30,4 @@ auto - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_choices.xml b/src/test/resources/petriNets/dynamic_choices.xml index 89ffc46aedd..655bcccdc15 100644 --- a/src/test/resources/petriNets/dynamic_choices.xml +++ b/src/test/resources/petriNets/dynamic_choices.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_choices dynamic_choices true @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_init.xml b/src/test/resources/petriNets/dynamic_init.xml index d7593bed06a..586c88c14e0 100644 --- a/src/test/resources/petriNets/dynamic_init.xml +++ b/src/test/resources/petriNets/dynamic_init.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_init dynamic_init true @@ -100,4 +100,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_validations.xml b/src/test/resources/petriNets/dynamic_validations.xml index 892960bfa6c..0e6c6da2745 100644 --- a/src/test/resources/petriNets/dynamic_validations.xml +++ b/src/test/resources/petriNets/dynamic_validations.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_validations dynamic_validations true @@ -129,4 +129,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_validations_performance_test.xml b/src/test/resources/petriNets/dynamic_validations_performance_test.xml index e4d349a71dd..d33ba5dc10f 100644 --- a/src/test/resources/petriNets/dynamic_validations_performance_test.xml +++ b/src/test/resources/petriNets/dynamic_validations_performance_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_validations_performance dynamic_validations_performance true @@ -3301,4 +3301,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml b/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml index 9b1684aed87..b9a7973bd9f 100644 --- a/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml +++ b/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_validations_performance_comparison dynamic_validations_performance_comparison true @@ -3275,4 +3275,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading.xml b/src/test/resources/petriNets/function_overloading.xml index 0053e9e7d6a..a3e58babc96 100644 --- a/src/test/resources/petriNets/function_overloading.xml +++ b/src/test/resources/petriNets/function_overloading.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading FOF Test @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading_fail.xml b/src/test/resources/petriNets/function_overloading_fail.xml index e35e537652d..5df7b1cb633 100644 --- a/src/test/resources/petriNets/function_overloading_fail.xml +++ b/src/test/resources/petriNets/function_overloading_fail.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading_fail FOF Test @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading_fail_v2.xml b/src/test/resources/petriNets/function_overloading_fail_v2.xml index edb1a05ff26..88966c403df 100644 --- a/src/test/resources/petriNets/function_overloading_fail_v2.xml +++ b/src/test/resources/petriNets/function_overloading_fail_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading_fail FOF Test @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading_v2.xml b/src/test/resources/petriNets/function_overloading_v2.xml index 2a1f005e7c1..3648f5c1180 100644 --- a/src/test/resources/petriNets/function_overloading_v2.xml +++ b/src/test/resources/petriNets/function_overloading_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading FOF Test @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_res.xml b/src/test/resources/petriNets/function_res.xml index 85c8b4ee109..6faa882219f 100644 --- a/src/test/resources/petriNets/function_res.xml +++ b/src/test/resources/petriNets/function_res.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_res SFR Test @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_res_v2.xml b/src/test/resources/petriNets/function_res_v2.xml index f92e7c9db61..d2c665b384e 100644 --- a/src/test/resources/petriNets/function_res_v2.xml +++ b/src/test/resources/petriNets/function_res_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_res SFR Test @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_test.xml b/src/test/resources/petriNets/function_test.xml index 6a4964e2f32..90d20fe2fcd 100644 --- a/src/test/resources/petriNets/function_test.xml +++ b/src/test/resources/petriNets/function_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_test SFT Test @@ -148,4 +148,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_test_v2.xml b/src/test/resources/petriNets/function_test_v2.xml index 31af0307960..26976d76708 100644 --- a/src/test/resources/petriNets/function_test_v2.xml +++ b/src/test/resources/petriNets/function_test_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_test SFT Test @@ -143,4 +143,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/groovy_shell_test.xml b/src/test/resources/petriNets/groovy_shell_test.xml index ab6db998f8e..cc257a4f00f 100644 --- a/src/test/resources/petriNets/groovy_shell_test.xml +++ b/src/test/resources/petriNets/groovy_shell_test.xml @@ -1,5 +1,5 @@ - + new_model NEW New Model diff --git a/src/test/resources/petriNets/impersonation_test.xml b/src/test/resources/petriNets/impersonation_test.xml index 7a0d1993284..5ed2917f12a 100644 --- a/src/test/resources/petriNets/impersonation_test.xml +++ b/src/test/resources/petriNets/impersonation_test.xml @@ -1,4 +1,4 @@ - + impersonation_test 1.0.0 IMP @@ -115,4 +115,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/importer_upsert.xml b/src/test/resources/petriNets/importer_upsert.xml index 1d3165e3afc..e3db46a1cae 100644 --- a/src/test/resources/petriNets/importer_upsert.xml +++ b/src/test/resources/petriNets/importer_upsert.xml @@ -1,5 +1,5 @@ - + importer_upsert importer_upsert IMP diff --git a/src/test/resources/petriNets/mortgage_net.xml b/src/test/resources/petriNets/mortgage_net.xml index 9e4ddcedd69..98204cb74ad 100644 --- a/src/test/resources/petriNets/mortgage_net.xml +++ b/src/test/resources/petriNets/mortgage_net.xml @@ -1,5 +1,5 @@ - + mortgage MOR Mortgage @@ -916,4 +916,4 @@ 16 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml b/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml index 835b186ab51..743de9c89de 100644 --- a/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml +++ b/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_init_test NIT New init test diff --git a/src/test/resources/petriNets/role_assign_remove_test.xml b/src/test/resources/petriNets/role_assign_remove_test.xml index 82553b484f3..32b2d2cc2f8 100644 --- a/src/test/resources/petriNets/role_assign_remove_test.xml +++ b/src/test/resources/petriNets/role_assign_remove_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> role_assign_remove_test Role Assign And Remove RAR diff --git a/src/test/resources/petriNets/role_test.xml b/src/test/resources/petriNets/role_test.xml index 93f08f0f7ae..8d2ad743285 100644 --- a/src/test/resources/petriNets/role_test.xml +++ b/src/test/resources/petriNets/role_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> role_test IMP role_test diff --git a/src/test/resources/petriNets/task_controller_set_data.xml b/src/test/resources/petriNets/task_controller_set_data.xml new file mode 100644 index 00000000000..60743a813ad --- /dev/null +++ b/src/test/resources/petriNets/task_controller_set_data.xml @@ -0,0 +1,154 @@ + + task_controller_set_data + 1.0.0 + TCS + task_controller_set_data + device_hub + true + true + false + + taskRef_0 + + </data> + <data type="caseRef"> + <id>caseRef_0</id> + <title/> + </data> + <data type="text"> + <id>text_0</id> + <title/> + </data> + <data type="text"> + <id>text_1</id> + <title/> + </data> + <transition> + <id>testSetDataFieldTypeRestriction</id> + <x>720</x> + <y>336</y> + <label>testSetDataFieldTypeRestriction</label> + <dataGroup> + <id>testSetDataFieldTypeRestriction_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>taskRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>caseRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>testSetDataNestedTaskRefRestrictions</id> + <x>720</x> + <y>436</y> + <label>testSetDataNestedTaskRefRestrictions</label> + <dataGroup> + <id>testSetDataNestedTaskRefRestrictions_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>taskRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>data</id> + <x>720</x> + <y>564</y> + <label>data</label> + <dataGroup> + <id>data_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_1</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>1</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>testSetDataNonReferencedField</id> + <x>720</x> + <y>664</y> + <label>testSetDataNonReferencedField</label> + <dataGroup> + <id>data_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> +</document> \ No newline at end of file diff --git a/src/test/resources/petriNets/validation/valid_boolean.xml b/src/test/resources/petriNets/validation/valid_boolean.xml index 77c4c1e5054..bfccf851133 100644 --- a/src/test/resources/petriNets/validation/valid_boolean.xml +++ b/src/test/resources/petriNets/validation/valid_boolean.xml @@ -1,4 +1,4 @@ -<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> <id>valid_boolean</id> <initials>VB1</initials> <title>Validacia BooleanFieldu @@ -81,4 +81,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_date.xml b/src/test/resources/petriNets/validation/valid_date.xml index 008cd3b31bb..4a16b5b7c52 100644 --- a/src/test/resources/petriNets/validation/valid_date.xml +++ b/src/test/resources/petriNets/validation/valid_date.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> valid_date VD1 Validacia DateFieldu @@ -226,4 +226,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_number.xml b/src/test/resources/petriNets/validation/valid_number.xml index bbcb7063438..658d78546a1 100644 --- a/src/test/resources/petriNets/validation/valid_number.xml +++ b/src/test/resources/petriNets/validation/valid_number.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> valid_number VN1 Validacia NumberFieldu @@ -232,4 +232,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_regex.xml b/src/test/resources/petriNets/validation/valid_regex.xml index 2b9dd0239a6..3b9fa45e0d3 100644 --- a/src/test/resources/petriNets/validation/valid_regex.xml +++ b/src/test/resources/petriNets/validation/valid_regex.xml @@ -1,4 +1,4 @@ - + valid_regex VR1 Validacia regexov @@ -201,4 +201,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_text.xml b/src/test/resources/petriNets/validation/valid_text.xml index 6935722ddc2..f3f5477a87e 100644 --- a/src/test/resources/petriNets/validation/valid_text.xml +++ b/src/test/resources/petriNets/validation/valid_text.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> valid_text VT1 Validacia TextFieldu @@ -182,4 +182,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml index 3165ab2c233..e3c19481edf 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_6 PTA Permissions test anonymous 6 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml index d464a3d94ba..8a95b6330c7 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_4 PTA Permissions test anonymous 4 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml index b9451d5e148..429117b420f 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_2 PTA Permissions test anonymous 2 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml index becf2e971a4..865ed55afe4 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_10 PTA Permissions test anonymous 10 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml index ca1bb624926..edae7d805d5 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_7 PTA Permissions test anonymous 7 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml index 00afb2c5939..1f9f219b1a1 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_5 PTA Permissions test anonymous 5 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml index cea6fafa266..a6c04bb3e8b 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_8 PTA Permissions test anonymous 8 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml index 4f9bd8cf4cd..c6c7fc729db 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_3 PTA Permissions test anonymous 3 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml index 1e69657be3f..aebd7cc7e08 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_9 PTA Permissions test anonymous 9 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml index cdc455d6da7..390626b56fc 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_11 PTA Permissions test anonymous 11 diff --git a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml index bc74887e06b..a67f850b7fa 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml @@ -1,5 +1,5 @@ - + permissions_test_combined_2 PTC Permissions test combined 2 diff --git a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml index 446834a9273..854bbe68bed 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml @@ -1,5 +1,5 @@ - + permissions_test_combined_1 PTC Permissions test combined 1 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml index 061a9adcbab..e46e7cf7bf0 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml @@ -1,5 +1,5 @@ - + permissions_test_default_6 PTD Permissions test default 6 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml index a8820f38b4b..9938318bede 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml @@ -1,5 +1,5 @@ - + permissions_test_default_4 PTD Permissions test default 4 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml index 9e877ec7a84..2cede6a9f98 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml @@ -1,5 +1,5 @@ - + permissions_test_default_2 PTD Permissions test default 2 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml index 490145a3fd5..72a3c59eca4 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml @@ -1,5 +1,5 @@ - + permissions_test_default_10 PTD Permissions test default 10 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml index c66effbf3d3..a2ff5e4f30d 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml @@ -1,5 +1,5 @@ - + permissions_test_default_7 PTD Permissions test default 7 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml index c7574cd6417..27615b0c51e 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml @@ -1,5 +1,5 @@ - + permissions_test_default_5 PTD Permissions test default 5 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml index 5756c8d623c..555b6a488dc 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml @@ -1,5 +1,5 @@ - + permissions_test_default_8 PTD Permissions test default 8 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml index 9c3437a0526..930fa096140 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml @@ -1,5 +1,5 @@ - + permissions_test_default_3 PTD Permissions test default 3 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml index 131a28f2806..7e095f8533c 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml @@ -1,5 +1,5 @@ - + permissions_test_default_9 PTD Permissions test default 9 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml index 3b2a7359c9c..25298a9a7eb 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml @@ -1,5 +1,5 @@ - + permissions_test_default_11 PTD Permissions test default 11 diff --git a/src/test/resources/priloha.xml b/src/test/resources/priloha.xml index 1645d2474c7..7e15542391a 100644 --- a/src/test/resources/priloha.xml +++ b/src/test/resources/priloha.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation='https://petriflow.org/petriflow.schema.xsd'> priloha IPR 1.0.0 diff --git a/src/test/resources/process_delete_test.xml b/src/test/resources/process_delete_test.xml index b921eef0bec..3859937941d 100644 --- a/src/test/resources/process_delete_test.xml +++ b/src/test/resources/process_delete_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> processDeleteTest PDT Process Delete Test diff --git a/src/test/resources/process_search_test.xml b/src/test/resources/process_search_test.xml index f92fedb067b..3f53f9069fa 100644 --- a/src/test/resources/process_search_test.xml +++ b/src/test/resources/process_search_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> processSearchTest PST Process Search Test diff --git a/src/test/resources/remoteFileField.xml b/src/test/resources/remoteFileField.xml index 0176593c890..a77c644a0a1 100644 --- a/src/test/resources/remoteFileField.xml +++ b/src/test/resources/remoteFileField.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> remote_file_field_net TST Test @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/remoteFileListField.xml b/src/test/resources/remoteFileListField.xml index bd7ae00ca1a..ac1da8b04ee 100644 --- a/src/test/resources/remoteFileListField.xml +++ b/src/test/resources/remoteFileListField.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> remote_file_list_field_net TST Remote file list field test @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/removeRole_test.xml b/src/test/resources/removeRole_test.xml index f4ad76410b6..3a7c78d7681 100644 --- a/src/test/resources/removeRole_test.xml +++ b/src/test/resources/removeRole_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -32,4 +32,4 @@ manager manager - \ No newline at end of file + diff --git a/src/test/resources/role_cancel_test.xml b/src/test/resources/role_cancel_test.xml index e0700dbb56a..54f10be23ac 100644 --- a/src/test/resources/role_cancel_test.xml +++ b/src/test/resources/role_cancel_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/rolref_view.xml b/src/test/resources/rolref_view.xml index 4988b7d9d38..173725a088c 100644 --- a/src/test/resources/rolref_view.xml +++ b/src/test/resources/rolref_view.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -47,4 +47,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/rule_engine_test.xml b/src/test/resources/rule_engine_test.xml index 787e57a7470..7c1ad639121 100644 --- a/src/test/resources/rule_engine_test.xml +++ b/src/test/resources/rule_engine_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> rule_engine_test 1.0.0 RLN diff --git a/src/test/resources/simple_taskref.xml b/src/test/resources/simple_taskref.xml index 423f9100e8d..4278089966a 100644 --- a/src/test/resources/simple_taskref.xml +++ b/src/test/resources/simple_taskref.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> simple_taskref STR simple taskref diff --git a/src/test/resources/taskRefLayoutTest.xml b/src/test/resources/taskRefLayoutTest.xml index f726cb094bc..f1432c77760 100644 --- a/src/test/resources/taskRefLayoutTest.xml +++ b/src/test/resources/taskRefLayoutTest.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest 1.0.0 NEW diff --git a/src/test/resources/taskRefLayoutTest2.xml b/src/test/resources/taskRefLayoutTest2.xml index 2d73ac95677..d62f9f7e23a 100644 --- a/src/test/resources/taskRefLayoutTest2.xml +++ b/src/test/resources/taskRefLayoutTest2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest2 1.0.0 NEW diff --git a/src/test/resources/taskRefLayoutTest3.xml b/src/test/resources/taskRefLayoutTest3.xml index 8cede4b1de9..72e576ff67d 100644 --- a/src/test/resources/taskRefLayoutTest3.xml +++ b/src/test/resources/taskRefLayoutTest3.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest3 1.0.0 NEW diff --git a/src/test/resources/taskRefLayoutTest4.xml b/src/test/resources/taskRefLayoutTest4.xml index dca0763f449..1052ed6150b 100644 --- a/src/test/resources/taskRefLayoutTest4.xml +++ b/src/test/resources/taskRefLayoutTest4.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest3 1.0.0 NEW diff --git a/src/test/resources/taskRef_propagation_test_child.xml b/src/test/resources/taskRef_propagation_test_child.xml index 2a56be1d167..405bf10a104 100644 --- a/src/test/resources/taskRef_propagation_test_child.xml +++ b/src/test/resources/taskRef_propagation_test_child.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> child 1.0.0 CHL diff --git a/src/test/resources/taskRef_propagation_test_parent.xml b/src/test/resources/taskRef_propagation_test_parent.xml index 04d00788b99..f9373aa5151 100644 --- a/src/test/resources/taskRef_propagation_test_parent.xml +++ b/src/test/resources/taskRef_propagation_test_parent.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> parent 1.0.0 PAR diff --git a/src/test/resources/task_authentication_service_test.xml b/src/test/resources/task_authentication_service_test.xml index 03e79da9421..fb6cb8c6cd3 100644 --- a/src/test/resources/task_authentication_service_test.xml +++ b/src/test/resources/task_authentication_service_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> 1 TST TaskAuthenticationService test diff --git a/src/test/resources/task_authorization_service_test.xml b/src/test/resources/task_authorization_service_test.xml index 6662e550a5b..4ace8c27f14 100644 --- a/src/test/resources/task_authorization_service_test.xml +++ b/src/test/resources/task_authorization_service_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst WST WorkflowAuthorizationService test diff --git a/src/test/resources/task_authorization_service_test_with_userRefs.xml b/src/test/resources/task_authorization_service_test_with_userRefs.xml index 67ca0859531..ef5c5f338d7 100644 --- a/src/test/resources/task_authorization_service_test_with_userRefs.xml +++ b/src/test/resources/task_authorization_service_test_with_userRefs.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst_usersRef WSU WorkflowAuthorizationService test diff --git a/src/test/resources/task_cancel_net.xml b/src/test/resources/task_cancel_net.xml index 7c4673e3a8f..d73f36eae39 100644 --- a/src/test/resources/task_cancel_net.xml +++ b/src/test/resources/task_cancel_net.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -69,4 +69,4 @@ 1 - \ No newline at end of file + diff --git a/src/test/resources/task_events.xml b/src/test/resources/task_events.xml index 41a9af46294..a74caa83bdd 100644 --- a/src/test/resources/task_events.xml +++ b/src/test/resources/task_events.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -34,4 +34,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/task_reindex_test.xml b/src/test/resources/task_reindex_test.xml index 3b924d3d2ef..925de789dd7 100644 --- a/src/test/resources/task_reindex_test.xml +++ b/src/test/resources/task_reindex_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> task_reindex_test TST task_reindex_test @@ -78,4 +78,4 @@ 9 1 - \ No newline at end of file + diff --git a/src/test/resources/taskref_demo.xml b/src/test/resources/taskref_demo.xml index 4e38bcf1359..8b6b22abdfa 100644 --- a/src/test/resources/taskref_demo.xml +++ b/src/test/resources/taskref_demo.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskref_demo 1.0.0 TRD diff --git a/src/test/resources/taskref_init.xml b/src/test/resources/taskref_init.xml index 7adfa558b4d..c91cd8351df 100644 --- a/src/test/resources/taskref_init.xml +++ b/src/test/resources/taskref_init.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskref_init_test TIT Task ref init test net diff --git a/src/test/resources/test_autocomplete_dynamic.xml b/src/test/resources/test_autocomplete_dynamic.xml index 44ca5614426..03d660ad8a8 100644 --- a/src/test/resources/test_autocomplete_dynamic.xml +++ b/src/test/resources/test_autocomplete_dynamic.xml @@ -1,5 +1,5 @@ - + test_autocomplete_dynamic Autocomplete Dynamic Net ADN diff --git a/src/test/resources/test_icon_enum.xml b/src/test/resources/test_icon_enum.xml index 957a2c96408..f9313270fc6 100644 --- a/src/test/resources/test_icon_enum.xml +++ b/src/test/resources/test_icon_enum.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test_icon_enum Icon Enum Net IEN diff --git a/src/test/resources/test_inter_data_actions_dynamic.xml b/src/test/resources/test_inter_data_actions_dynamic.xml index 43b188af49c..26635c21d79 100644 --- a/src/test/resources/test_inter_data_actions_dynamic.xml +++ b/src/test/resources/test_inter_data_actions_dynamic.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -69,4 +69,4 @@ 2 1 - \ No newline at end of file + diff --git a/src/test/resources/test_inter_data_actions_static.xml b/src/test/resources/test_inter_data_actions_static.xml index fa1e6a4873b..f9434af77b3 100644 --- a/src/test/resources/test_inter_data_actions_static.xml +++ b/src/test/resources/test_inter_data_actions_static.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test_inter_data_actions_static.xml TST Test @@ -85,4 +85,4 @@ 1 1 - \ No newline at end of file + diff --git a/src/test/resources/test_setData.xml b/src/test/resources/test_setData.xml index 86fde7e4000..04c0563d5d9 100644 --- a/src/test/resources/test_setData.xml +++ b/src/test/resources/test_setData.xml @@ -1,4 +1,4 @@ - + test_setData TSD TestSetData diff --git a/src/test/resources/this_kw_test.xml b/src/test/resources/this_kw_test.xml index 7f3f6484256..5ef7628e5e9 100644 --- a/src/test/resources/this_kw_test.xml +++ b/src/test/resources/this_kw_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> this_kw_test TKW This keyword test net @@ -71,4 +71,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/user_list.xml b/src/test/resources/user_list.xml index 7f5c2234003..3741fd56d8a 100644 --- a/src/test/resources/user_list.xml +++ b/src/test/resources/user_list.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> user_list User list ULT diff --git a/src/test/resources/userrefs_test.xml b/src/test/resources/userrefs_test.xml index e2b0c67e704..438ff582ea4 100644 --- a/src/test/resources/userrefs_test.xml +++ b/src/test/resources/userrefs_test.xml @@ -1,5 +1,5 @@ - + testing_model TSM Testing Model diff --git a/src/test/resources/variable_arc_test.xml b/src/test/resources/variable_arc_test.xml index 2c2426bf1a4..199883e5db9 100644 --- a/src/test/resources/variable_arc_test.xml +++ b/src/test/resources/variable_arc_test.xml @@ -1,5 +1,5 @@ - + variable_arc_test.xml TST Test diff --git a/src/test/resources/view_permission_test.xml b/src/test/resources/view_permission_test.xml index 48271ca6241..3d4819fe424 100644 --- a/src/test/resources/view_permission_test.xml +++ b/src/test/resources/view_permission_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> vpt VPT ViewPermissionTest test diff --git a/src/test/resources/view_permission_with_userRefs_test.xml b/src/test/resources/view_permission_with_userRefs_test.xml index adeea8ba535..1adbecfb8e0 100644 --- a/src/test/resources/view_permission_with_userRefs_test.xml +++ b/src/test/resources/view_permission_with_userRefs_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> vpt_userRefs VPT ViewPermissionTest test diff --git a/src/test/resources/workflow_authorization_service_test.xml b/src/test/resources/workflow_authorization_service_test.xml index 0d690ed8723..064aa7d689b 100644 --- a/src/test/resources/workflow_authorization_service_test.xml +++ b/src/test/resources/workflow_authorization_service_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst WST WorkflowAuthorizationService test diff --git a/src/test/resources/workflow_authorization_service_test_with_userRefs.xml b/src/test/resources/workflow_authorization_service_test_with_userRefs.xml index f88c05d6381..3801963e3b4 100644 --- a/src/test/resources/workflow_authorization_service_test_with_userRefs.xml +++ b/src/test/resources/workflow_authorization_service_test_with_userRefs.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst_usersRef WSU WorkflowAuthorizationService test