├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── dependency-review.yml │ ├── javadoc-publish.yml │ ├── jib-publish.yml │ ├── publish.yml │ ├── scorecards.yml │ ├── update-gradle-wrapper.yml │ └── validate-gradle-wrapper.yml ├── .gitignore ├── .lgtm.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── camunda-modeler-plugin ├── .gitignore ├── .npmignore ├── client │ ├── dmn-js-extension │ │ ├── DmnCheckJsExtension.js │ │ └── index.js │ └── index.js ├── index.js ├── package-lock.json ├── package.json ├── style │ └── style.css └── webpack.config.js ├── checkerframework └── stubs │ ├── jparsec.astub │ └── jsonobject.astub ├── cli ├── pom.xml └── src │ ├── main │ └── java │ │ └── de │ │ └── redsix │ │ └── dmncheck │ │ └── cli │ │ └── Main.java │ └── test │ └── java │ └── de │ └── redsix │ └── dmncheck │ └── cli │ └── MainTest.java ├── core ├── pom.xml └── src │ ├── main │ └── java │ │ └── de │ │ └── redsix │ │ └── dmncheck │ │ ├── drg │ │ └── RequirementGraph.java │ │ ├── package-info.java │ │ ├── result │ │ ├── Severity.java │ │ └── ValidationResult.java │ │ └── validators │ │ └── core │ │ ├── GenericValidator.java │ │ ├── RequirementGraphValidator.java │ │ ├── SimpleValidator.java │ │ ├── ValidationContext.java │ │ └── Validator.java │ └── test │ └── java │ └── de │ └── redsix │ └── dmncheck │ ├── drg │ └── RequirementGraphTest.java │ └── validators │ └── core │ └── GenericValidatorTest.java ├── docs ├── _config.yml └── index.md ├── gradle-plugin ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── java │ └── de │ │ └── redsix │ │ └── dmncheck │ │ ├── DmnCheckExtension.java │ │ ├── DmnCheckGradlePlugin.java │ │ └── DmnCheckTask.java │ └── resources │ └── META-INF │ └── gradle-plugins │ └── de.redsix.dmncheck.plugin.properties ├── maven-plugin ├── pom.xml └── src │ ├── main │ └── java │ │ └── de │ │ └── redsix │ │ └── dmncheck │ │ └── CheckerMain.java │ └── test │ ├── java │ └── de │ │ └── redsix │ │ └── dmncheck │ │ └── CheckerMainTest.java │ └── resources │ ├── empty-as-well.dmn │ └── empty.dmn ├── plugin-base ├── pom.xml └── src │ ├── main │ └── java │ │ └── de │ │ └── redsix │ │ └── dmncheck │ │ └── plugin │ │ ├── PluginBase.java │ │ └── PrettyPrintValidationResults.java │ └── test │ ├── java │ └── de │ │ └── redsix │ │ └── dmncheck │ │ └── plugin │ │ └── PluginBaseTest.java │ └── resources │ ├── cyclic-diagram.dmn │ ├── decision-requirement-diagram.dmn │ ├── diagram-with-a-loop.dmn │ ├── dish-decision-1-3.dmn │ ├── dish-decision.dmn │ ├── duplicate_collect.dmn │ ├── duplicate_unique.dmn │ ├── empty.dmn │ └── no-decision.dmn ├── pom.xml ├── server ├── pom.xml └── src │ └── main │ └── java │ └── de │ └── redsix │ └── dmncheck │ └── server │ ├── NonValidatingDmnParser.java │ └── ValidationServer.java └── validators ├── checkerframework └── stubs │ ├── jparsec.astub │ └── jsonobject.astub ├── pom.xml └── src ├── main └── java │ └── de │ └── redsix │ └── dmncheck │ ├── feel │ ├── ExpressionType.java │ ├── ExpressionTypeParser.java │ ├── FeelExpression.java │ ├── FeelParser.java │ ├── FeelTypecheck.java │ ├── Operator.java │ └── Subsumption.java │ ├── util │ ├── Either.java │ ├── Expression.java │ ├── ProjectClassLoader.java │ ├── TopLevelExpressionLanguage.java │ ├── TriFunction.java │ ├── Util.java │ └── ValidatorLoader.java │ └── validators │ ├── AggregationOutputTypeValidator.java │ ├── AggregationValidator.java │ ├── ConflictingRuleValidator.java │ ├── ConnectedRequirementGraphValidator.java │ ├── DecisionIdAndNameValidator.java │ ├── DefinitionsIdAndNameValidator.java │ ├── DuplicateColumnLabelValidator.java │ ├── DuplicateRuleValidator.java │ ├── ElementTypeDeclarationValidator.java │ ├── IdAndNameValidator.java │ ├── InputDataIdAndNameValidator.java │ ├── InputEntryTypeValidator.java │ ├── InputTypeDeclarationValidator.java │ ├── InputValuesTypeValidator.java │ ├── ItemDefinitionAllowedValuesTypeValidator.java │ ├── ItemDefinitionIdAndNameValidator.java │ ├── KnowledgeSourceIdAndNameValidator.java │ ├── NoDecisionPresentValidator.java │ ├── OutputEntryTypeValidator.java │ ├── OutputTypeDeclarationValidator.java │ ├── OutputValuesTypeValidator.java │ ├── RequirementGraphLeafValidator.java │ ├── ShadowedRuleValidator.java │ └── TypeValidator.java └── test └── java └── de └── redsix └── dmncheck ├── feel ├── ExpressionTypeParserTest.java ├── FeelExpressionTest.java ├── FeelParserTest.java ├── FeelTypecheckTest.java └── SubsumptionTest.java ├── util └── ExpressionTest.java └── validators ├── AggregationOutputTypeValidatorTest.java ├── AggregationValidatorTest.java ├── ConflictingRuleValidatorTest.java ├── ConnectedRequirementGraphValidatorTest.java ├── DecisionIdAndNameValidatorTest.java ├── DefinitionsIdAndNameValidatorTest.java ├── DuplicateColumnLabelValidatorTest.java ├── DuplicateRuleValidatorTest.java ├── InputDataIdAndNameValidatorTest.java ├── InputEntryTypeValidatorTest.java ├── InputTypeDeclarationValidatorTest.java ├── InputValuesTypeValidatorTest.java ├── ItemDefinitionAllowedValuesTypeValidatorTest.java ├── ItemDefinitionIdAndNameValidatorTest.java ├── KnowledgeSourceIdAndNameValidatorTest.java ├── NoDecisionPresentValidatorTest.java ├── OutputEntryTypeValidatorTest.java ├── OutputTypeDeclarationValidatorTest.java ├── OutputValuesTypeValidatorTest.java ├── RequirementGraphLeafValidatorTest.java ├── ShadowedRuleValidatorTest.java └── util ├── TestEnum.java ├── WithDecisionTable.java ├── WithDefinitions.java ├── WithItemDefinition.java └── WithRequirementGraph.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | - package-ecosystem: "maven" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | 15 | - package-ecosystem: "gradle" 16 | directory: "/gradle-plugin" 17 | schedule: 18 | interval: "monthly" 19 | 20 | - package-ecosystem: "maven" 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | # Setting open-pull-requests-limit to 0 means that dependabot will not 25 | # update regular dependencies on this target branch, but still provide 26 | # security updates for our gomod dependencies 27 | open-pull-requests-limit: 0 28 | target-branch: "release/1.3.x" 29 | 30 | - package-ecosystem: "gradle" 31 | directory: "/gradle-plugin" 32 | schedule: 33 | interval: "monthly" 34 | # Setting open-pull-requests-limit to 0 means that dependabot will not 35 | # update regular dependencies on this target branch, but still provide 36 | # security updates for our gomod dependencies 37 | open-pull-requests-limit: 0 38 | target-branch: "release/1.3.x" 39 | 40 | - package-ecosystem: "maven" 41 | directory: "/" 42 | schedule: 43 | interval: "monthly" 44 | # Setting open-pull-requests-limit to 0 means that dependabot will not 45 | # update regular dependencies on this target branch, but still provide 46 | # security updates for our gomod dependencies 47 | open-pull-requests-limit: 0 48 | target-branch: "release/1.2.x" 49 | 50 | - package-ecosystem: "gradle" 51 | directory: "/gradle-plugin" 52 | schedule: 53 | interval: "monthly" 54 | # Setting open-pull-requests-limit to 0 means that dependabot will not 55 | # update regular dependencies on this target branch, but still provide 56 | # security updates for our gomod dependencies 57 | open-pull-requests-limit: 0 58 | target-branch: "release/1.2.x" 59 | 60 | 61 | - package-ecosystem: npm 62 | directory: /camunda-modeler-plugin 63 | schedule: 64 | interval: daily 65 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 'release/*' 8 | pull_request: 9 | types: [ opened, synchronize, reopened ] 10 | 11 | pull_request_target: 12 | types: [ opened, synchronize, reopened ] 13 | 14 | jobs: 15 | build-dmn-check: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Harden Runner 21 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 22 | with: 23 | egress-policy: audit 24 | 25 | - name: Checkout 26 | if: ${{ github.event_name != 'pull_request_target' }} 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | fetch-depth: 0 # Disabling shallow clone is recommended for improving relevancy of reporting 30 | - name: Checkout PR 31 | if: ${{ github.event_name == 'pull_request_target' }} 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | ref: ${{ github.event.pull_request.head.sha }} 35 | fetch-depth: 0 # Disabling shallow clone is recommended for improving relevancy of reporting 36 | - name: Set up JDK 37 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 38 | with: 39 | java-version: '17' 40 | distribution: 'adopt' 41 | cache: maven 42 | - name: Build with Maven 43 | run: mvn clean install -P checkerframework 44 | 45 | 46 | sonarcloud: 47 | runs-on: ubuntu-latest 48 | # If the PR is coming from a fork (pull_request_target), ensure it's opened by "dependabot[bot]". 49 | # Otherwise, clone it normally. 50 | if: | 51 | (github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]') || 52 | (github.event_name != 'pull_request_target' && github.actor != 'dependabot[bot]') 53 | steps: 54 | - name: Harden Runner 55 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 56 | with: 57 | egress-policy: audit 58 | allowed-endpoints: sonarcloud.io:443 59 | 60 | - name: Checkout 61 | if: ${{ github.event_name != 'pull_request_target' }} 62 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 63 | - name: Checkout PR 64 | if: ${{ github.event_name == 'pull_request_target' }} 65 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 66 | with: 67 | ref: ${{ github.event.pull_request.head.sha }} 68 | - name: Set up JDK 69 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 70 | with: 71 | java-version: '17' 72 | distribution: 'adopt' 73 | - env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 75 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 76 | run: mvn -B verify jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=red6_dmn-check 77 | 78 | 79 | build-gradle-plugin: 80 | runs-on: ubuntu-latest 81 | needs: build-dmn-check 82 | steps: 83 | - name: Harden Runner 84 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 85 | with: 86 | egress-policy: audit 87 | 88 | - name: Checkout 89 | if: ${{ github.event_name != 'pull_request_target' }} 90 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 91 | with: 92 | fetch-depth: 0 # Disabling shallow clone is recommended for improving relevancy of reporting 93 | - name: Checkout PR 94 | if: ${{ github.event_name == 'pull_request_target' }} 95 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 96 | with: 97 | ref: ${{ github.event.pull_request.head.sha }} 98 | fetch-depth: 0 # Disabling shallow clone is recommended for improving relevancy of reporting 99 | - name: Set up JDK 100 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 101 | with: 102 | java-version: '17' 103 | distribution: 'adopt' 104 | cache: maven 105 | - name: Build with Gradle 106 | run: cd gradle-plugin && ./gradlew build 107 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: 18 | - master 19 | - 'release/*' 20 | pull_request: 21 | # The branches below must be a subset of the branches above 22 | branches: 23 | - master 24 | - 'release/*' 25 | schedule: 26 | - cron: '20 19 * * 1' 27 | 28 | permissions: 29 | contents: read 30 | 31 | jobs: 32 | analyze: 33 | permissions: 34 | actions: read # for github/codeql-action/init to get workflow details 35 | contents: read # for actions/checkout to fetch code 36 | security-events: write # for github/codeql-action/autobuild to send a status report 37 | name: Analyze 38 | runs-on: ubuntu-latest 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | language: [ 'java', 'javascript' ] 44 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 45 | # Learn more... 46 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 47 | 48 | steps: 49 | - name: Harden Runner 50 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 51 | with: 52 | egress-policy: audit 53 | 54 | - name: Checkout repository 55 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 56 | 57 | - name: Set up JDK 58 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 59 | with: 60 | java-version: '17' 61 | distribution: 'adopt' 62 | 63 | # Initializes the CodeQL tools for scanning. 64 | - name: Initialize CodeQL 65 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 66 | with: 67 | languages: ${{ matrix.language }} 68 | # If you wish to specify custom queries, you can do so here or in a config file. 69 | # By default, queries listed here will override any specified in a config file. 70 | # Prefix the list here with "+" to use these queries and those in the config file. 71 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 72 | 73 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 74 | # If this step fails, then you should remove it and run the build manually (see below) 75 | - name: Autobuild 76 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 77 | 78 | # ℹ️ Command-line programs to run using the OS shell. 79 | # 📚 https://git.io/JvXDl 80 | 81 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 82 | # and modify them (or add more) to build your code if your project 83 | # uses a compiled language 84 | 85 | #- run: | 86 | # make bootstrap 87 | # make release 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 91 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Harden Runner 18 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 19 | with: 20 | egress-policy: audit 21 | 22 | - name: 'Checkout Repository' 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | - name: 'Dependency Review' 25 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 26 | -------------------------------------------------------------------------------- /.github/workflows/javadoc-publish.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Javadoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | paths: 9 | - "**.java" 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Harden Runner 16 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 17 | with: 18 | egress-policy: audit 19 | 20 | - name: Deploy JavaDoc 🚀 21 | uses: MathieuSoysal/Javadoc-publisher.yml@fda475b197081ba1eca7a1dfadf0c017080a1623 # v3.0.2 22 | with: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | javadoc-branch: javadoc 25 | java-version: 17 26 | target-folder: javadoc 27 | -------------------------------------------------------------------------------- /.github/workflows/jib-publish.yml: -------------------------------------------------------------------------------- 1 | name: JIB container publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | release: 8 | types: [ created ] 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Harden Runner 15 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 16 | with: 17 | egress-policy: audit 18 | 19 | - name: JIB container build and publish 20 | uses: MathieuSoysal/jib-container-publish.yml@8df13913445f036bf93d0450669dbfbac2cac541 # main 21 | with: 22 | PASSWORD: ${{ secrets.GITHUB_TOKEN }} 23 | java-version: 17 24 | module: cli 25 | main-class: de.redsix.dmncheck.cli.Main -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ["master"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | 32 | steps: 33 | - name: Harden Runner 34 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 35 | with: 36 | egress-policy: audit 37 | 38 | - name: "Checkout code" 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | persist-credentials: false 42 | 43 | - name: "Run analysis" 44 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 49 | # - you want to enable the Branch-Protection check on a *public* repository, or 50 | # - you are installing Scorecards on a *private* repository 51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 53 | 54 | # Public repositories: 55 | # - Publish results to OpenSSF REST API for easy access by consumers 56 | # - Allows the repository to include the Scorecard badge. 57 | # - See https://github.com/ossf/scorecard-action#publishing-results. 58 | # For private repositories: 59 | # - `publish_results` will always be set to `false`, regardless 60 | # of the value entered here. 61 | publish_results: true 62 | 63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 64 | # format to the repository Actions tab. 65 | - name: "Upload artifact" 66 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 67 | with: 68 | name: SARIF file 69 | path: results.sarif 70 | retention-days: 5 71 | 72 | # Upload the results to GitHub's code scanning dashboard. 73 | - name: "Upload to code-scanning" 74 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 75 | with: 76 | sarif_file: results.sarif 77 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | on: 3 | schedule: 4 | - cron: '0 0 1 * *' 5 | 6 | jobs: 7 | update-gradle-wrapper: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Harden Runner 11 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 12 | with: 13 | egress-policy: audit 14 | 15 | - name: checkout 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | 18 | - name: update gradle 19 | id: gradleUpdate 20 | uses: EdwarDDay/upgrade-gradle-action@3b3788a65fe8c6d24ee63acbf09a3d59e39b4f0b # v1.5.0 21 | with: 22 | working-directory: gradle-plugin 23 | 24 | - name: create pull request 25 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 26 | with: 27 | commit-message: "Update gradle to ${{ steps.gradleUpdate.outputs.gradle-version }}" 28 | branch: "gradle_update/version_${{ steps.gradleUpdate.outputs.gradle-version }}" 29 | delete-branch: true 30 | title: "Update gradle to ${{ steps.gradleUpdate.outputs.gradle-version }}" 31 | body: | 32 | ${{ steps.gradleUpdate.outputs.version-information }} 33 | 34 | Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action 35 | labels: "dependencies,gradle" -------------------------------------------------------------------------------- /.github/workflows/validate-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: [push] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | validation: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Harden Runner 12 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 13 | with: 14 | egress-policy: audit 15 | 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - uses: gradle/wrapper-validation-action@f9c9c575b8b21b6485636a91ffecd10e558c62f6 # v3.5.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | *.iml 3 | target/** 4 | */target/** 5 | gradle-plugin/.idea -------------------------------------------------------------------------------- /.lgtm.yml: -------------------------------------------------------------------------------- 1 | extraction: 2 | java: 3 | index: 4 | java_version: "17" 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Currently the following versions do get security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.3.x | :white_check_mark: | 10 | | < 1.3 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please report any vulnerability to pascal.wittmann@red6-es.de. 15 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Stores VSCode versions used for testing VSCode extensions 107 | .vscode-test 108 | 109 | # Intellij Idea 110 | *.iml 111 | .idea 112 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | client 3 | webpack.config.js -------------------------------------------------------------------------------- /camunda-modeler-plugin/client/dmn-js-extension/DmnCheckJsExtension.js: -------------------------------------------------------------------------------- 1 | function DmnCheckJsExtension(eventBus, drd, elementRegistry, moddle, overlays, canvas) { 2 | 3 | const messages = document.createElement("div"); 4 | messages.classList.add('message-box'); 5 | messages.id = "message-box"; 6 | messages.style.display = "none"; 7 | canvas.getContainer().parentNode.appendChild(messages); 8 | 9 | eventBus.on('import.done', function() { 10 | validate(); 11 | }); 12 | 13 | 14 | eventBus.on('elements.changed', function() { 15 | validate(); 16 | }); 17 | 18 | function validate() { 19 | const map = {}; 20 | 21 | moddle.toXML(drd._definitions, {}).then((result) => { 22 | log("Start validation."); 23 | 24 | xml = result.xml; 25 | 26 | fetch('http://localhost:42000/validate', { 27 | method: "POST", 28 | body: xml 29 | }).then(res => { 30 | res.text().then(function (results) { 31 | 32 | log("Request complete! response:", results); 33 | 34 | messages.textContent = ""; 35 | 36 | JSON.parse(results).items.forEach(result => { 37 | const shape = elementRegistry.get(result.drgElementId); 38 | 39 | if (typeof shape !== "undefined") { 40 | overlays.add(shape, 'badge', { 41 | position: { 42 | bottom: 0, 43 | left: 21 * map[result.drgElementId] 44 | }, 45 | html: '
' 46 | }); 47 | 48 | map[result.drgElementId] = ~~map[result.drgElementId] + 1; 49 | } else { 50 | messages.textContent = result.message; 51 | } 52 | }); 53 | 54 | if (messages.textContent !== "") { 55 | messages.style.display = "block"; 56 | } 57 | }) 58 | }); 59 | 60 | log("Validation finished."); 61 | }); 62 | } 63 | } 64 | 65 | 66 | 67 | function log(...args) { 68 | console.log('[DmnCheckJsExtension]', ...args); 69 | } 70 | 71 | DmnCheckJsExtension.$inject = ['eventBus', 'drd', 'elementRegistry', 'moddle', 'overlays', 'canvas']; 72 | 73 | module.exports = DmnCheckJsExtension; 74 | 75 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/client/dmn-js-extension/index.js: -------------------------------------------------------------------------------- 1 | import DmnCheckPlugin from './DmnCheckJsExtension'; 2 | 3 | export default { 4 | __init__: [ 'dmnCheckPlugin' ], 5 | dmnCheckPlugin: [ 'type', DmnCheckPlugin ] 6 | }; 7 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/client/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | registerDmnJSPlugin, 3 | } from 'camunda-modeler-plugin-helpers'; 4 | 5 | import DmnExtensionModule from './dmn-js-extension'; 6 | 7 | registerDmnJSPlugin(DmnExtensionModule, 'drd'); 8 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. 6 | * 7 | * Camunda licenses this file to you under the MIT; you may not use this file 8 | * except in compliance with the MIT License. 9 | */ 10 | 11 | 'use strict'; 12 | 13 | module.exports = { 14 | name: 'dmn-check Plugin', 15 | script: './dist/client.js', 16 | style: './style/style.css' 17 | }; 18 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dmn-test-plugin", 3 | "version": "1.0.0", 4 | "description": "The Camunda Modeler dmn-check plug-in", 5 | "main": "index.js", 6 | "scripts": { 7 | "all": "run-s bundle", 8 | "bundle": "webpack", 9 | "dev": "webpack -w", 10 | "prepublishOnly": "run-s bundle" 11 | }, 12 | "keywords": [ 13 | "camunda", 14 | "modeler", 15 | "plugin", 16 | "dmn-check" 17 | ], 18 | "devDependencies": { 19 | "camunda-modeler-plugin-helpers": "^5.1.0", 20 | "npm-run-all": "^4.1.5", 21 | "webpack": "^5.99.9", 22 | "webpack-cli": "^6.0.1" 23 | }, 24 | "dependencies": { 25 | "min-dash": "^4.2.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/style/style.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | position:relative; 3 | } 4 | .badge:after { 5 | content:'🞩'; 6 | position:absolute; 7 | top:-10px; 8 | right:-10px; 9 | font-size:1.2em; 10 | color:white; 11 | width:20px; 12 | height:20px; 13 | text-align:center; 14 | line-height:20px; 15 | border-radius:50%; 16 | box-shadow:0 0 1px #333; 17 | } 18 | 19 | .badge-warning:after { 20 | background: orange; 21 | } 22 | 23 | .badge-error:after { 24 | background: red; 25 | } 26 | 27 | .message-box { 28 | font-size: 1.5em; 29 | position: absolute; 30 | bottom: 20px; 31 | left: 20px; 32 | background: red; 33 | color: white; 34 | } 35 | -------------------------------------------------------------------------------- /camunda-modeler-plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. 6 | * 7 | * Camunda licenses this file to you under the MIT; you may not use this file 8 | * except in compliance with the MIT License. 9 | */ 10 | 11 | const path = require('path'); 12 | 13 | module.exports = { 14 | mode: 'development', 15 | entry: './client/index.js', 16 | output: { 17 | path: path.resolve(__dirname, 'dist'), 18 | filename: 'client.js' 19 | }, 20 | devtool: 'cheap-module-source-map' 21 | }; -------------------------------------------------------------------------------- /checkerframework/stubs/jparsec.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.regex.qual.Regex; 2 | package org.jparsec.pattern; 3 | public final class Patterns { 4 | public static Pattern regex(@Regex String s); 5 | } 6 | -------------------------------------------------------------------------------- /checkerframework/stubs/jsonobject.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.nullness.qual.Nullable; 2 | package org.json; 3 | 4 | public class JSONObject { 5 | public JSONObject put(String key, @Nullable Object value) throws JSONException; 6 | } 7 | -------------------------------------------------------------------------------- /cli/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | dmn-check 7 | de.redsix 8 | 1.4.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | cli 13 | 14 | 15 | 16 | 17 | de.redsix 18 | dmn-check-plugin-base 19 | ${project.parent.version} 20 | compile 21 | 22 | 23 | 24 | de.redsix 25 | dmn-check-validators 26 | ${project.parent.version} 27 | 28 | 29 | 30 | org.slf4j 31 | slf4j-simple 32 | 2.0.17 33 | 34 | 35 | 36 | info.picocli 37 | picocli 38 | 4.7.7 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /cli/src/main/java/de/redsix/dmncheck/cli/Main.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.cli; 2 | 3 | import de.redsix.dmncheck.plugin.PluginBase; 4 | import de.redsix.dmncheck.plugin.PrettyPrintValidationResults; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.concurrent.Callable; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import picocli.CommandLine; 12 | import picocli.CommandLine.Command; 13 | import picocli.CommandLine.Option; 14 | 15 | @Command(mixinStandardHelpOptions = true) 16 | public class Main implements PluginBase, Callable { 17 | private static final Logger logger = LoggerFactory.getLogger(Main.class); 18 | 19 | @Option( 20 | names = {"--excludeList"}, 21 | split = ",") 22 | @SuppressWarnings("nullness") 23 | private List excludeList; 24 | 25 | @Option( 26 | names = {"--searchPath"}, 27 | split = ",") 28 | @SuppressWarnings("nullness") 29 | private List searchPath; 30 | 31 | @Option( 32 | names = {"--validatorPackages"}, 33 | split = ",") 34 | @SuppressWarnings("nullness") 35 | private String[] validatorPackages; 36 | 37 | @Option( 38 | names = {"--validatorClasses"}, 39 | split = ",") 40 | @SuppressWarnings("nullness") 41 | private String[] validatorClasses; 42 | 43 | @Option(names = {"--failOnWarning"}) 44 | @SuppressWarnings("nullness") 45 | private boolean failOnWarning; 46 | 47 | @Option( 48 | names = {"--projectClasspath"}, 49 | split = ":") 50 | @SuppressWarnings("nullness") 51 | private List projectClasspath; 52 | 53 | public static void main(String[] args) { 54 | int exitCode = new CommandLine(new Main()).execute(args); 55 | System.exit(exitCode); 56 | } 57 | 58 | @Override 59 | public PrettyPrintValidationResults.PluginLogger getPluginLogger() { 60 | return new PrettyPrintValidationResults.PluginLogger( 61 | (t) -> logger.info(t.toString()), (t) -> logger.warn(t.toString()), (t) -> logger.error(t.toString())); 62 | } 63 | 64 | @Override 65 | public List getExcludeList() { 66 | if (Objects.isNull(excludeList)) { 67 | return Collections.emptyList(); 68 | } else { 69 | return excludeList; 70 | } 71 | } 72 | 73 | @Override 74 | public List getSearchPathList() { 75 | if (Objects.isNull(searchPath) || searchPath.isEmpty()) { 76 | return Collections.singletonList(""); 77 | } else { 78 | return searchPath; 79 | } 80 | } 81 | 82 | @Override 83 | public String[] getValidatorPackages() { 84 | return validatorPackages; 85 | } 86 | 87 | @Override 88 | public String[] getValidatorClasses() { 89 | return validatorClasses; 90 | } 91 | 92 | @Override 93 | public boolean failOnWarning() { 94 | return failOnWarning; 95 | } 96 | 97 | @Override 98 | public Integer call() { 99 | if (Objects.nonNull(projectClasspath)) { 100 | loadProjectClasspath(projectClasspath); 101 | } 102 | 103 | return validate() ? 1 : 0; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cli/src/test/java/de/redsix/dmncheck/cli/MainTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.cli; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.io.PrintWriter; 6 | import java.io.StringWriter; 7 | import java.util.List; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import picocli.CommandLine; 11 | 12 | class MainTest { 13 | 14 | CommandLine cmd; 15 | 16 | @BeforeEach 17 | void setupCommand() { 18 | Main app = new Main(); 19 | cmd = new CommandLine(app); 20 | } 21 | 22 | @Test 23 | void shouldPrintHelp() { 24 | StringWriter sw = new StringWriter(); 25 | cmd.setOut(new PrintWriter(sw)); 26 | 27 | int exitCode = cmd.execute("--help"); 28 | assertEquals(0, exitCode); 29 | assertTrue(sw.toString().startsWith("Usage:")); 30 | } 31 | 32 | @Test 33 | void shouldSetExcludeList() { 34 | StringWriter sw = new StringWriter(); 35 | cmd.setOut(new PrintWriter(sw)); 36 | 37 | int exitCode = cmd.execute("--excludeList=some-file.dmn"); 38 | Main main = cmd.getCommand(); 39 | assertEquals(0, exitCode); 40 | assertEquals(List.of("some-file.dmn"), main.getExcludeList()); 41 | } 42 | 43 | @Test 44 | void shouldSetSearchPath() { 45 | StringWriter sw = new StringWriter(); 46 | cmd.setOut(new PrintWriter(sw)); 47 | 48 | cmd.execute("--searchPath=model/"); 49 | Main main = cmd.getCommand(); 50 | assertEquals(List.of("model/"), main.getSearchPathList()); 51 | } 52 | 53 | @Test 54 | void shouldSetValidatorPackages() { 55 | StringWriter sw = new StringWriter(); 56 | cmd.setOut(new PrintWriter(sw)); 57 | 58 | cmd.execute("--validatorPackages=com.red6.dmn"); 59 | Main main = cmd.getCommand(); 60 | assertArrayEquals(List.of("com.red6.dmn").toArray(new String[0]), main.getValidatorPackages()); 61 | } 62 | 63 | @Test 64 | void shouldSetValidatorClasses() { 65 | StringWriter sw = new StringWriter(); 66 | cmd.setOut(new PrintWriter(sw)); 67 | 68 | cmd.execute("--validatorClasses=FancyValidator,DreamValidator"); 69 | Main main = cmd.getCommand(); 70 | assertArrayEquals( 71 | List.of("FancyValidator", "DreamValidator").toArray(new String[0]), main.getValidatorClasses()); 72 | } 73 | 74 | @Test 75 | void shouldSetFailOnWarning() { 76 | StringWriter sw = new StringWriter(); 77 | cmd.setOut(new PrintWriter(sw)); 78 | 79 | cmd.execute("--failOnWarning"); 80 | Main main = cmd.getCommand(); 81 | assertTrue(main.failOnWarning()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | dmn-check 7 | de.redsix 8 | 1.4.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | dmn-check-core 13 | 14 | 15 | 16 | 17 | org.camunda.bpm.model 18 | camunda-dmn-model 19 | 7.23.0 20 | 21 | 22 | 23 | org.jgrapht 24 | jgrapht-core 25 | 1.5.2 26 | 27 | 28 | 29 | org.mockito 30 | mockito-core 31 | ${mockito.version} 32 | test 33 | 34 | 35 | 36 | org.mockito 37 | mockito-junit-jupiter 38 | ${mockito.version} 39 | test 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/drg/RequirementGraph.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.drg; 2 | 3 | import java.util.Collection; 4 | import java.util.Objects; 5 | import java.util.stream.Stream; 6 | import org.camunda.bpm.model.dmn.DmnModelInstance; 7 | import org.camunda.bpm.model.dmn.instance.AuthorityRequirement; 8 | import org.camunda.bpm.model.dmn.instance.Decision; 9 | import org.camunda.bpm.model.dmn.instance.Definitions; 10 | import org.camunda.bpm.model.dmn.instance.DrgElement; 11 | import org.camunda.bpm.model.dmn.instance.InformationRequirement; 12 | import org.camunda.bpm.model.dmn.instance.InputData; 13 | import org.camunda.bpm.model.dmn.instance.KnowledgeSource; 14 | import org.jgrapht.graph.DefaultEdge; 15 | import org.jgrapht.graph.DirectedAcyclicGraph; 16 | 17 | public class RequirementGraph extends DirectedAcyclicGraph { 18 | 19 | private final transient Definitions definitions; 20 | 21 | public RequirementGraph(Class edgeClass, Definitions definitions) { 22 | super(edgeClass); 23 | this.definitions = definitions; 24 | } 25 | 26 | public Definitions getDefinitions() { 27 | return definitions; 28 | } 29 | 30 | public static RequirementGraph from(final DmnModelInstance dmnModelInstance) throws IllegalArgumentException { 31 | final Collection decisions = dmnModelInstance.getModelElementsByType(Decision.class); 32 | final Collection knowledgeSources = 33 | dmnModelInstance.getModelElementsByType(KnowledgeSource.class); 34 | final Collection inputData = dmnModelInstance.getModelElementsByType(InputData.class); 35 | 36 | final RequirementGraph drg = new RequirementGraph(DefaultEdge.class, dmnModelInstance.getDefinitions()); 37 | 38 | // checkerframework cannot figure out the types in this case without an explicit declaration 39 | // found : @UnknownRegex Stream 40 | // required: @UnknownRegex Stream 42 | Stream.>of(decisions, knowledgeSources, inputData) 43 | .flatMap(Collection::stream) 44 | .forEach(drg::addVertex); 45 | 46 | for (Decision decision : decisions) { 47 | decision.getInformationRequirements().stream() 48 | .flatMap(RequirementGraph::collectDrgElements) 49 | .forEach(drgElement -> drg.addEdge(drgElement, decision)); 50 | 51 | decision.getAuthorityRequirements().stream() 52 | .flatMap(RequirementGraph::collectDrgElements) 53 | .forEach(drgElement -> drg.addEdge(drgElement, decision)); 54 | } 55 | 56 | for (KnowledgeSource knowledgeSource : knowledgeSources) { 57 | knowledgeSource.getAuthorityRequirement().stream() 58 | .flatMap(RequirementGraph::collectDrgElements) 59 | .forEach(drgElement -> drg.addEdge(drgElement, knowledgeSource)); 60 | } 61 | 62 | return drg; 63 | } 64 | 65 | private static Stream collectDrgElements(InformationRequirement informationRequirement) { 66 | return Stream.of(informationRequirement.getRequiredDecision(), informationRequirement.getRequiredInput()) 67 | .filter(Objects::nonNull); 68 | } 69 | 70 | private static Stream collectDrgElements(AuthorityRequirement authorityRequirement) { 71 | return Stream.of( 72 | authorityRequirement.getRequiredDecision(), 73 | authorityRequirement.getRequiredInput(), 74 | authorityRequirement.getRequiredAuthority()) 75 | .filter(Objects::nonNull); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package de.redsix.dmncheck; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/result/Severity.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.result; 2 | 3 | /** The severity of a validation result. */ 4 | public enum Severity { 5 | WARNING, 6 | ERROR 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/result/ValidationResult.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.result; 2 | 3 | import org.camunda.bpm.model.xml.instance.ModelElementInstance; 4 | 5 | /** 6 | * A validation result consists of the following: 7 | * 8 | *

1) A message that describes the validation result 2) A severity indicating how severe the result is 3) A reference 9 | * to the DMN element that caused result 10 | * 11 | *

In this context a validation result always describes an error or a warning. There is currently no way to express 12 | * positive validation results, except returning no validation results at all. 13 | */ 14 | public class ValidationResult { 15 | 16 | private final Severity severity; 17 | private final String message; 18 | private final ModelElementInstance element; 19 | 20 | private ValidationResult(final String message, final ModelElementInstance element, final Severity severity) { 21 | this.message = message; 22 | this.element = element; 23 | this.severity = severity; 24 | } 25 | 26 | public Severity getSeverity() { 27 | return severity; 28 | } 29 | 30 | public String getMessage() { 31 | return message; 32 | } 33 | 34 | public ModelElementInstance getElement() { 35 | return element; 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return message; 41 | } 42 | 43 | public static final Builder.MessageStep init = message -> (new Builder.SeverityStep() { 44 | 45 | private Severity severity = Severity.ERROR; 46 | 47 | @Override 48 | public Builder.BuildStep element(ModelElementInstance element) { 49 | return new Builder.BuildStep() { 50 | 51 | @Override 52 | public ModelElementInstance getElement() { 53 | return element; 54 | } 55 | 56 | @Override 57 | public Severity getSeverity() { 58 | return severity; 59 | } 60 | 61 | @Override 62 | public String getMessage() { 63 | return message; 64 | } 65 | 66 | @Override 67 | public ValidationResult build() { 68 | return new ValidationResult(message, element, severity); 69 | } 70 | }; 71 | } 72 | 73 | @Override 74 | public Severity getSeverity() { 75 | return severity; 76 | } 77 | 78 | @Override 79 | public Builder.ElementStep severity(Severity severity) { 80 | this.severity = severity; 81 | return this; 82 | } 83 | 84 | @Override 85 | public String getMessage() { 86 | return message; 87 | } 88 | }); 89 | 90 | public static final class Builder { 91 | 92 | @FunctionalInterface 93 | public interface MessageStep { 94 | SeverityStep message(String message); 95 | } 96 | 97 | public interface SeverityStep extends ElementStep { 98 | ElementStep severity(Severity severity); 99 | 100 | String getMessage(); 101 | } 102 | 103 | public interface ElementStep { 104 | BuildStep element(ModelElementInstance element); 105 | 106 | Severity getSeverity(); 107 | 108 | String getMessage(); 109 | } 110 | 111 | public interface BuildStep { 112 | ModelElementInstance getElement(); 113 | 114 | Severity getSeverity(); 115 | 116 | String getMessage(); 117 | 118 | ValidationResult build(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/validators/core/GenericValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.core; 2 | 3 | import de.redsix.dmncheck.result.ValidationResult; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.Stream; 8 | import org.camunda.bpm.model.dmn.DmnModelInstance; 9 | import org.camunda.bpm.model.xml.instance.ModelElementInstance; 10 | 11 | /** 12 | * Generic validator that facilitates writing validations directly on the structure of the DMN model instance. 13 | * 14 | *

Note: We are using the Camunda implementation of the DMN model (...). 16 | * 17 | *

To write a validation using this validator you have to specify: 18 | * 19 | *

1) The element type (T) that is validated 2) The element type (S) that is used to check whether the validation is 20 | * applicable 21 | * 22 | *

The validator collects all elements of type S and checks whether the validation is applicable. It then applies the 23 | * validation on all child elements of type T. 24 | * 25 | * @param Class used to check whether the validation is applicable 26 | * @param Class used for validation 27 | */ 28 | public abstract class GenericValidator 29 | implements Validator { 30 | 31 | /** 32 | * Checks whether the validation is applicable for an element of type S taking account of the validation context. 33 | * 34 | * @param element Element used to check applicability of the validation 35 | * @param validationContext Validation Context 36 | * @return Whether the validation is applicable 37 | */ 38 | protected abstract boolean isApplicable(final S element, ValidationContext validationContext); 39 | 40 | /** 41 | * Validates a given element of type T. 42 | * 43 | *

A validation context can be used to track global assumptions about the DMN model instance. 44 | * 45 | * @param element Element under validation 46 | * @param validationContext Validation Context 47 | * @return A list of validation results 48 | */ 49 | protected abstract List validate(final T element, ValidationContext validationContext); 50 | 51 | /** 52 | * Auxiliary method to determine the class used for the applicability check. 53 | * 54 | * @return Class used to check whether the validation is applicable 55 | */ 56 | protected abstract Class getClassUsedToCheckApplicability(); 57 | 58 | /** 59 | * Auxiliary method to determine the class for the validation. 60 | * 61 | * @return Class used for validation 62 | */ 63 | protected abstract Class getClassUnderValidation(); 64 | 65 | public List apply(final DmnModelInstance dmnModelInstance) { 66 | final ValidationContext validationContext = new ValidationContext(dmnModelInstance); 67 | 68 | final Collection elements = dmnModelInstance.getModelElementsByType(getClassUsedToCheckApplicability()); 69 | return elements.stream() 70 | .filter(element -> isApplicable(element, validationContext)) 71 | .flatMap(this::getElementsUnderValidation) 72 | .flatMap(element -> validate(element, validationContext).stream()) 73 | .collect(Collectors.toList()); 74 | } 75 | 76 | private Stream getElementsUnderValidation(final S element) { 77 | final Stream childElementsUnderValidation = 78 | element.getChildElementsByType(getClassUnderValidation()).stream(); 79 | 80 | if (getClassUnderValidation().isInstance(element)) { 81 | return Stream.concat(childElementsUnderValidation, Stream.of((T) element)); 82 | } else { 83 | return childElementsUnderValidation; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/validators/core/RequirementGraphValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.core; 2 | 3 | import de.redsix.dmncheck.drg.RequirementGraph; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import org.camunda.bpm.model.dmn.DmnModelInstance; 8 | 9 | /** Generic validator that facilitates writing validations on graph-based representation of the DMN model instance. */ 10 | public abstract class RequirementGraphValidator implements Validator { 11 | 12 | /** 13 | * Returns a list of validation results for a requirement graph (see {@link RequirementGraph}). 14 | * 15 | * @param drg Requirement graph used for validation 16 | * @return A possibly empty list of validation results 17 | */ 18 | protected abstract List validate(RequirementGraph drg); 19 | 20 | @Override 21 | public List apply(DmnModelInstance dmnModelInstance) { 22 | try { 23 | final RequirementGraph requirementGraph = RequirementGraph.from(dmnModelInstance); 24 | return validate(requirementGraph); 25 | } catch (IllegalArgumentException exception) { 26 | return Collections.singletonList(ValidationResult.init 27 | .message("Error while construction requirement graph: " + exception.getMessage()) 28 | .element(dmnModelInstance.getDefinitions()) 29 | .build()); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/validators/core/SimpleValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.core; 2 | 3 | import org.camunda.bpm.model.xml.instance.ModelElementInstance; 4 | 5 | /** 6 | * A simple validator is a {@link GenericValidator} where the validation (T) and applicability check (S) type are always 7 | * the same. 8 | * 9 | * @param Class that is used for validation and for the applicability check 10 | */ 11 | public abstract class SimpleValidator extends GenericValidator { 12 | 13 | @Override 14 | public Class getClassUsedToCheckApplicability() { 15 | return getClassUnderValidation(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/validators/core/ValidationContext.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.core; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.function.Function; 7 | import org.camunda.bpm.model.dmn.DmnModelInstance; 8 | import org.camunda.bpm.model.dmn.instance.ItemDefinition; 9 | import org.checkerframework.checker.nullness.qual.NonNull; 10 | 11 | /** 12 | * The validation context is used in validators with a local view of the DMN model instance to provide access to global 13 | * attributes of the DMN model instance. 14 | * 15 | *

Note: Reading from the validation context is cached. 16 | */ 17 | public class ValidationContext { 18 | 19 | private static class Memoizer { 20 | 21 | private final Map, @NonNull U> cache = new ConcurrentHashMap<>(); 22 | 23 | private Function, U> doMemoize(final Function, @NonNull U> function) { 24 | return input -> cache.computeIfAbsent(input, function); 25 | } 26 | } 27 | 28 | private final Function, Collection> itemDefinitions; 29 | 30 | public ValidationContext(final DmnModelInstance dmnModelInstance) { 31 | this.itemDefinitions = new Memoizer>() 32 | .doMemoize(dmnModelInstance::getModelElementsByType); 33 | } 34 | 35 | /** 36 | * Provides access to the item definitions of a DMN model instance. 37 | * 38 | * @return A list of item definitions 39 | */ 40 | public Collection getItemDefinitions() { 41 | return itemDefinitions.apply(ItemDefinition.class); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/java/de/redsix/dmncheck/validators/core/Validator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.core; 2 | 3 | import de.redsix.dmncheck.result.ValidationResult; 4 | import java.util.List; 5 | import org.camunda.bpm.model.dmn.DmnModelInstance; 6 | 7 | /** 8 | * Generic interface for the validation of DMN model instances. 9 | * 10 | *

Every validation for dmn-check has to implement this interface. 11 | */ 12 | public interface Validator { 13 | 14 | /** 15 | * Validates the given DMN model instance and returns the validation results as a list. 16 | * 17 | * @param dmnModelInstance DMN model instance used for validation 18 | * @return A possibly empty list of validation results 19 | */ 20 | List apply(final DmnModelInstance dmnModelInstance); 21 | } 22 | -------------------------------------------------------------------------------- /core/src/test/java/de/redsix/dmncheck/drg/RequirementGraphTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.drg; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import org.camunda.bpm.model.dmn.Dmn; 6 | import org.camunda.bpm.model.dmn.DmnModelInstance; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class RequirementGraphTest { 10 | 11 | @Test 12 | void emptyGraphFromEmptyModel() { 13 | // Arrange 14 | final DmnModelInstance emptyModel = Dmn.createEmptyModel(); 15 | 16 | // Act 17 | final RequirementGraph requirementGraph = RequirementGraph.from(emptyModel); 18 | 19 | // Assert 20 | assertTrue(requirementGraph.vertexSet().isEmpty()); 21 | assertTrue(requirementGraph.edgeSet().isEmpty()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## Welcome to GitHub Pages 2 | 3 | You can use the [editor on GitHub](https://github.com/red6/dmn-check/edit/master/docs/index.md) to maintain and preview the content for your website in Markdown files. 4 | 5 | Whenever you commit to this repository, GitHub Pages will run [Jekyll](https://jekyllrb.com/) to rebuild the pages in your site, from the content in your Markdown files. 6 | 7 | ### Markdown 8 | 9 | Markdown is a lightweight and easy-to-use syntax for styling your writing. It includes conventions for 10 | 11 | ```markdown 12 | Syntax highlighted code block 13 | 14 | # Header 1 15 | ## Header 2 16 | ### Header 3 17 | 18 | - Bulleted 19 | - List 20 | 21 | 1. Numbered 22 | 2. List 23 | 24 | **Bold** and _Italic_ and `Code` text 25 | 26 | [Link](url) and ![Image](src) 27 | ``` 28 | 29 | For more details see [Basic writing and formatting syntax](https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). 30 | 31 | ### Jekyll Themes 32 | 33 | Your Pages site will use the layout and styles from the Jekyll theme you have selected in your [repository settings](https://github.com/red6/dmn-check/settings/pages). The name of this theme is saved in the Jekyll `_config.yml` configuration file. 34 | 35 | ### Support or Contact 36 | 37 | Having trouble with Pages? Check out our [documentation](https://docs.github.com/categories/github-pages-basics/) or [contact support](https://support.github.com/contact) and we’ll help you sort it out. 38 | -------------------------------------------------------------------------------- /gradle-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | 4 | # Ignore Gradle GUI config 5 | gradle-app.setting 6 | 7 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 8 | !gradle-wrapper.jar 9 | 10 | # Cache of project 11 | .gradletasknamecache 12 | 13 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 14 | # gradle/wrapper/gradle-wrapper.properties 15 | -------------------------------------------------------------------------------- /gradle-plugin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red6/dmn-check/a6d4bdd4a327bed920a55bd3ed77884ca3e43c89/gradle-plugin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle-plugin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradle-plugin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /gradle-plugin/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'dmn-check-gradle-plugin' 2 | 3 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/java/de/redsix/dmncheck/DmnCheckExtension.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck; 2 | 3 | import java.util.List; 4 | 5 | public class DmnCheckExtension { 6 | 7 | public List searchPathList; 8 | public List excludeList; 9 | 10 | public List validatorPackages; 11 | public List validatorClasses; 12 | } 13 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/java/de/redsix/dmncheck/DmnCheckGradlePlugin.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck; 2 | 3 | import org.gradle.api.Plugin; 4 | import org.gradle.api.Project; 5 | 6 | public class DmnCheckGradlePlugin implements Plugin { 7 | 8 | public static final String DMN_CHECK_EXTENSION = "dmnCheck"; 9 | public static final String CHECK_DMN_TASK = "checkDmn"; 10 | 11 | @Override 12 | public void apply(Project project) { 13 | final DmnCheckExtension dmnCheckExtension = 14 | project.getExtensions().create(DMN_CHECK_EXTENSION, DmnCheckExtension.class); 15 | final DmnCheckTask dmnCheckTask = project.getTasks().create(CHECK_DMN_TASK, DmnCheckTask.class); 16 | 17 | dmnCheckTask.getExtensions().add(DMN_CHECK_EXTENSION, dmnCheckExtension); 18 | dmnCheckTask.setGroup("verification"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/resources/META-INF/gradle-plugins/de.redsix.dmncheck.plugin.properties: -------------------------------------------------------------------------------- 1 | implementation-class=de.redsix.dmncheck.DmnCheckGradlePlugin 2 | -------------------------------------------------------------------------------- /maven-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | dmn-check 7 | de.redsix 8 | 1.4.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | dmn-check-maven-plugin 13 | maven-plugin 14 | 15 | 16 | 17 | 18 | org.apache.maven.plugins 19 | maven-plugin-plugin 20 | 3.15.1 21 | 22 | true 23 | 24 | 25 | 26 | mojo-descriptor 27 | 28 | descriptor 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | de.redsix 39 | dmn-check-core 40 | ${project.parent.version} 41 | 42 | 43 | 44 | de.redsix 45 | dmn-check-validators 46 | ${project.parent.version} 47 | 48 | 49 | 50 | de.redsix 51 | dmn-check-plugin-base 52 | ${project.parent.version} 53 | 54 | 55 | 56 | org.apache.maven 57 | maven-plugin-api 58 | 3.9.9 59 | provided 60 | 61 | 62 | 63 | org.apache.maven 64 | maven-core 65 | 3.9.9 66 | provided 67 | 68 | 69 | 70 | org.apache.maven.plugin-tools 71 | maven-plugin-annotations 72 | 3.15.1 73 | provided 74 | 75 | 76 | 77 | 78 | org.apache.maven 79 | maven-artifact 80 | 81 | 82 | 83 | 84 | 85 | 86 | org.apache.maven 87 | maven-artifact 88 | 3.9.9 89 | provided 90 | 91 | 92 | org.mockito 93 | mockito-core 94 | ${mockito.version} 95 | test 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/de/redsix/dmncheck/CheckerMain.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck; 2 | 3 | import de.redsix.dmncheck.plugin.PluginBase; 4 | import de.redsix.dmncheck.plugin.PrettyPrintValidationResults; 5 | import java.io.File; 6 | import java.util.*; 7 | import java.util.stream.Collectors; 8 | import org.apache.maven.artifact.Artifact; 9 | import org.apache.maven.plugin.AbstractMojo; 10 | import org.apache.maven.plugin.MojoExecutionException; 11 | import org.apache.maven.plugins.annotations.Mojo; 12 | import org.apache.maven.plugins.annotations.Parameter; 13 | import org.apache.maven.plugins.annotations.ResolutionScope; 14 | import org.apache.maven.project.MavenProject; 15 | 16 | @Mojo(name = "check-dmn", requiresProject = false, requiresDependencyResolution = ResolutionScope.TEST) 17 | public class CheckerMain extends AbstractMojo implements PluginBase { 18 | 19 | @Parameter 20 | @SuppressWarnings("nullness") 21 | String[] excludes; 22 | 23 | @Parameter 24 | @SuppressWarnings("nullness") 25 | String[] searchPaths; 26 | 27 | @Parameter 28 | @SuppressWarnings("nullness") 29 | String[] validatorPackages; 30 | 31 | @Parameter 32 | @SuppressWarnings("nullness") 33 | String[] validatorClasses; 34 | 35 | @Parameter(defaultValue = "${project}", readonly = true) 36 | @SuppressWarnings("nullness") 37 | MavenProject project; 38 | 39 | @Parameter(defaultValue = "false", readonly = true) 40 | @SuppressWarnings("nullness") 41 | Boolean failOnWarning; 42 | 43 | @Parameter(defaultValue = "${classpaths}") 44 | @SuppressWarnings("nullness") 45 | String[] classpath; 46 | 47 | @Override 48 | public void execute() throws MojoExecutionException { 49 | loadClasspath(); 50 | if (validate()) { 51 | throw new MojoExecutionException("Some files are not valid, see previous logs."); 52 | } 53 | } 54 | 55 | @Override 56 | public PrettyPrintValidationResults.PluginLogger getPluginLogger() { 57 | return new PrettyPrintValidationResults.PluginLogger(getLog()::info, getLog()::warn, getLog()::error); 58 | } 59 | 60 | @Override 61 | public List getExcludeList() { 62 | if (excludes != null) { 63 | return Arrays.asList(excludes); 64 | } else { 65 | return new ArrayList<>(); 66 | } 67 | } 68 | 69 | @Override 70 | public List getSearchPathList() { 71 | if (searchPaths != null) { 72 | return Arrays.asList(searchPaths); 73 | } else { 74 | return Collections.singletonList(""); 75 | } 76 | } 77 | 78 | @Override 79 | public String[] getValidatorPackages() { 80 | return validatorPackages; 81 | } 82 | 83 | @Override 84 | public String[] getValidatorClasses() { 85 | return validatorClasses; 86 | } 87 | 88 | @Override 89 | public boolean failOnWarning() { 90 | return this.failOnWarning; 91 | } 92 | 93 | void loadClasspath() { 94 | if (classpath != null && classpath.length != 0) { 95 | loadProjectClasspath(Arrays.asList(classpath)); 96 | } else { 97 | loadProjectClasspath(); 98 | } 99 | } 100 | 101 | void loadProjectClasspath() { 102 | List classpath = project.getArtifacts().stream() 103 | .map(Artifact::getFile) 104 | .map(File::getAbsolutePath) 105 | .collect(Collectors.toList()); 106 | 107 | loadProjectClasspath(classpath); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /maven-plugin/src/test/java/de/redsix/dmncheck/CheckerMainTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck; 2 | 3 | import static org.mockito.Mockito.when; 4 | 5 | import de.redsix.dmncheck.util.ProjectClassLoader; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.net.URLClassLoader; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import org.apache.maven.artifact.Artifact; 15 | import org.apache.maven.plugin.MojoExecutionException; 16 | import org.apache.maven.project.MavenProject; 17 | import org.junit.jupiter.api.Assertions; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Nested; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.function.Executable; 22 | import org.junit.jupiter.api.io.TempDir; 23 | import org.mockito.Mockito; 24 | 25 | class CheckerMainTest { 26 | 27 | private final CheckerMain testee = new CheckerMain(); 28 | 29 | @Nested 30 | class TestsWithNoArtifacts { 31 | 32 | @BeforeEach 33 | void setUp() { 34 | final MavenProject mavenProject = new MavenProject(); 35 | mavenProject.setArtifacts(Collections.emptySet()); 36 | testee.project = mavenProject; 37 | } 38 | 39 | @Test 40 | void shouldSkipFileIfItsExcluded() { 41 | final String ignoredFilename = "empty-as-well.dmn"; 42 | testee.excludes = new String[] {ignoredFilename}; 43 | final List filesToTest = 44 | testee.fetchFilesToTestFromSearchPaths(Collections.singletonList(Paths.get(""))); 45 | 46 | Assertions.assertTrue( 47 | filesToTest.stream().noneMatch(file -> file.getName().equals(ignoredFilename))); 48 | } 49 | 50 | @Test 51 | void shouldSkipFileIfIsNotOnSearchPath() { 52 | final List filesToTest = 53 | testee.fetchFilesToTestFromSearchPaths(Collections.singletonList(Paths.get("src/main/java"))); 54 | Assertions.assertTrue(filesToTest.isEmpty()); 55 | } 56 | 57 | @Test 58 | void shouldDetectIfFileIsOnSearchPath() { 59 | testee.searchPaths = new String[] {"src/"}; 60 | final MojoExecutionException assertionError = 61 | Assertions.assertThrows(MojoExecutionException.class, testee::execute); 62 | Assertions.assertTrue(assertionError.getMessage().contains("Some files are not valid, see previous logs.")); 63 | } 64 | 65 | @Test 66 | void shouldDetectIfFileIsOnSearchPathWithMultiplePaths() { 67 | testee.searchPaths = new String[] {"src/main/java", "src/"}; 68 | final MojoExecutionException assertionError = 69 | Assertions.assertThrows(MojoExecutionException.class, testee::execute); 70 | Assertions.assertTrue(assertionError.getMessage().contains("Some files are not valid, see previous logs.")); 71 | } 72 | } 73 | 74 | @TempDir 75 | Path tempDir; 76 | 77 | @Test 78 | void shouldAddArtifactsFromProjectToProjectClassloader() throws IOException { 79 | final MavenProject mavenProject = new MavenProject(); 80 | 81 | final Artifact artifact = Mockito.mock(Artifact.class); 82 | 83 | final Path artifactFile = Files.createFile(tempDir.resolve("artifact-file")); 84 | 85 | when(artifact.getFile()).thenReturn(artifactFile.toFile()); 86 | 87 | mavenProject.setArtifacts(Collections.singleton(artifact)); 88 | testee.project = mavenProject; 89 | 90 | Assertions.assertDoesNotThrow((Executable) testee::loadProjectClasspath); 91 | 92 | Assertions.assertEquals( 93 | artifactFile.toUri().toURL(), ((URLClassLoader) ProjectClassLoader.INSTANCE.classLoader).getURLs()[0]); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /maven-plugin/src/test/resources/empty-as-well.dmn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red6/dmn-check/a6d4bdd4a327bed920a55bd3ed77884ca3e43c89/maven-plugin/src/test/resources/empty-as-well.dmn -------------------------------------------------------------------------------- /maven-plugin/src/test/resources/empty.dmn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red6/dmn-check/a6d4bdd4a327bed920a55bd3ed77884ca3e43c89/maven-plugin/src/test/resources/empty.dmn -------------------------------------------------------------------------------- /plugin-base/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | dmn-check 7 | de.redsix 8 | 1.4.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | dmn-check-plugin-base 13 | 14 | 15 | de.redsix 16 | dmn-check-core 17 | 1.4.0-SNAPSHOT 18 | compile 19 | 20 | 21 | de.redsix 22 | dmn-check-validators 23 | 1.4.0-SNAPSHOT 24 | compile 25 | 26 | 27 | org.mockito 28 | mockito-core 29 | ${mockito.version} 30 | test 31 | 32 | 33 | org.mockito 34 | mockito-junit-jupiter 35 | ${mockito.version} 36 | test 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /plugin-base/src/main/java/de/redsix/dmncheck/plugin/PrettyPrintValidationResults.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.plugin; 2 | 3 | import de.redsix.dmncheck.result.Severity; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import java.io.File; 6 | import java.util.Comparator; 7 | import java.util.List; 8 | import java.util.function.Consumer; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | import org.camunda.bpm.model.dmn.instance.InputEntry; 12 | import org.camunda.bpm.model.dmn.instance.OutputEntry; 13 | import org.camunda.bpm.model.dmn.instance.Rule; 14 | import org.camunda.bpm.model.xml.instance.ModelElementInstance; 15 | 16 | public final class PrettyPrintValidationResults { 17 | 18 | private PrettyPrintValidationResults() {} 19 | 20 | public static class PluginLogger { 21 | protected final Consumer info; 22 | protected final Consumer warn; 23 | protected final Consumer error; 24 | 25 | public PluginLogger( 26 | final Consumer info, 27 | final Consumer warn, 28 | final Consumer error) { 29 | this.info = info; 30 | this.warn = warn; 31 | this.error = error; 32 | } 33 | } 34 | 35 | public static void logPrettified( 36 | final File file, final List validationResults, final PluginLogger log) { 37 | log.info.accept("Validation results for file " + file.getAbsolutePath()); 38 | 39 | validationResults.sort( 40 | Comparator.comparing(ValidationResult::getSeverity).reversed()); 41 | 42 | for (ValidationResult validationResult : validationResults) { 43 | final String errorMessage = "Element '" + delegate(validationResult.getElement()) + "'" + " of type '" 44 | + validationResult.getElement().getElementType().getTypeName() + "'" 45 | + " has the following validation result: " + validationResult.getMessage(); 46 | getLoggingMethod(validationResult.getSeverity(), log).accept(errorMessage); 47 | } 48 | } 49 | 50 | private static String delegate(final ModelElementInstance element) { 51 | if (element instanceof Rule) { 52 | return prettify((Rule) element); 53 | } else { 54 | return element.getRawTextContent().trim(); 55 | } 56 | } 57 | 58 | private static String prettify(final Rule rule) { 59 | return Stream.concat( 60 | rule.getInputEntries().stream().map(InputEntry::getTextContent), 61 | rule.getOutputEntries().stream().map(OutputEntry::getTextContent)) 62 | .collect(Collectors.joining(",")); 63 | } 64 | 65 | private static Consumer getLoggingMethod(final Severity severity, final PluginLogger logger) { 66 | if (severity == Severity.WARNING) { 67 | return logger.warn; 68 | } 69 | 70 | return logger.error; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugin-base/src/test/resources/cyclic-diagram.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /plugin-base/src/test/resources/diagram-with-a-loop.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /plugin-base/src/test/resources/dish-decision-1-3.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | season 8 | 9 | 10 | 11 | 12 | guestCount 13 | 14 | 15 | 16 | 17 | 18 | "Fall" 19 | 20 | 21 | <= 8 22 | 23 | 24 | "Spareribs" 25 | 26 | 27 | 28 | 29 | "Winter" 30 | 31 | 32 | <= 8 33 | 34 | 35 | "Roastbeef" 36 | 37 | 38 | 39 | 40 | "Spring" 41 | 42 | 43 | <= 7 44 | 45 | 46 | "Dry Aged Gourmet Steak" 47 | 48 | 49 | 50 | Save money 51 | 52 | "Spring" 53 | 54 | 55 | [5..8] 56 | 57 | 58 | "Steak" 59 | 60 | 61 | 62 | Less effort 63 | 64 | "Fall", "Winter", "Spring" 65 | 66 | 67 | > 8 68 | 69 | 70 | "Stew" 71 | 72 | 73 | 74 | Hey, why not!? 75 | 76 | "Summer" 77 | 78 | 79 | 80 | 81 | 82 | "Light Salad and a nice Steak" 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /plugin-base/src/test/resources/dish-decision.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | season 7 | 8 | 9 | 10 | guestCount 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Save money 40 | 41 | 42 | [5..8] 43 | 44 | 45 | 46 | 47 | 48 | Less effort 49 | 50 | 51 | 8]]> 52 | 53 | 54 | 55 | 56 | 57 | Hey, why not!? 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /plugin-base/src/test/resources/duplicate_collect.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | property1 7 | 8 | 9 | 10 | property2 11 | 12 | 13 | 14 | 15 | "INPUT1" 16 | 17 | "INPUT2" 18 | 19 | "OUTPUT1" 20 | 21 | 22 | 23 | "INPUT1" 24 | 25 | "INPUT2" 26 | 27 | "OUTPUT2" 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /plugin-base/src/test/resources/duplicate_unique.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PROPERTY1 7 | 8 | 9 | 10 | PROPERTY2 11 | 12 | 13 | 14 | 15 | "INPUT1" 16 | 17 | "INPUT2" 18 | 19 | "output" 20 | 21 | 22 | 23 | "INPUT1" 24 | 25 | "INPUT2" 26 | 27 | "output" 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /plugin-base/src/test/resources/empty.dmn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red6/dmn-check/a6d4bdd4a327bed920a55bd3ed77884ca3e43c89/plugin-base/src/test/resources/empty.dmn -------------------------------------------------------------------------------- /plugin-base/src/test/resources/no-decision.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | dmn-check 7 | de.redsix 8 | 1.4.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | dmn-check-server 13 | 14 | 15 | 16 | 17 | maven-assembly-plugin 18 | 3.7.1 19 | 20 | 21 | 22 | de.redsix.dmncheck.server.ValidationServer 23 | 24 | 25 | 26 | jar-with-dependencies 27 | 28 | 29 | 30 | 31 | make-assembly 32 | package 33 | 34 | single 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | de.redsix 45 | dmn-check-core 46 | ${project.parent.version} 47 | 48 | 49 | 50 | de.redsix 51 | dmn-check-validators 52 | ${project.parent.version} 53 | 54 | 55 | 56 | com.sparkjava 57 | spark-core 58 | 2.9.4 59 | 60 | 61 | 62 | org.apache.commons 63 | commons-lang3 64 | 3.17.0 65 | 66 | 67 | 68 | org.json 69 | json 70 | 20250517 71 | 72 | 73 | 74 | org.slf4j 75 | slf4j-simple 76 | 2.0.17 77 | 78 | 79 | 80 | com.beust 81 | jcommander 82 | 1.82 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /server/src/main/java/de/redsix/dmncheck/server/NonValidatingDmnParser.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.server; 2 | 3 | import org.camunda.bpm.model.dmn.impl.DmnParser; 4 | import org.camunda.bpm.model.xml.instance.DomDocument; 5 | 6 | public class NonValidatingDmnParser extends DmnParser { 7 | 8 | @Override 9 | public void validateModel(DomDocument document) { 10 | // Do not validate against the xml schema for now. Errors from the xml validation are hard to map back 11 | // into the editor. 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /validators/checkerframework/stubs/jparsec.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.regex.qual.Regex; 2 | package org.jparsec.pattern; 3 | public final class Patterns { 4 | public static Pattern regex(@Regex String s); 5 | } 6 | -------------------------------------------------------------------------------- /validators/checkerframework/stubs/jsonobject.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.nullness.qual.Nullable; 2 | package org.json; 3 | 4 | public class JSONObject { 5 | public JSONObject put(String key, @Nullable Object value) throws JSONException; 6 | } 7 | -------------------------------------------------------------------------------- /validators/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | dmn-check 7 | de.redsix 8 | 1.4.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | dmn-check-validators 13 | 14 | 15 | 16 | de.redsix 17 | dmn-check-core 18 | ${project.parent.version} 19 | 20 | 21 | 22 | io.github.classgraph 23 | classgraph 24 | 4.8.179 25 | 26 | 27 | 28 | org.jparsec 29 | jparsec 30 | 3.1 31 | 32 | 33 | 34 | org.mockito 35 | mockito-core 36 | ${mockito.version} 37 | test 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/feel/ExpressionType.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.feel; 2 | 3 | import static de.redsix.dmncheck.feel.ExpressionTypes.DOUBLE; 4 | import static de.redsix.dmncheck.feel.ExpressionTypes.INTEGER; 5 | import static de.redsix.dmncheck.feel.ExpressionTypes.LONG; 6 | import static de.redsix.dmncheck.feel.ExpressionTypes.TOP; 7 | 8 | import java.util.Arrays; 9 | import org.camunda.bpm.model.dmn.instance.ItemDefinition; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | import org.derive4j.Data; 12 | 13 | @Data 14 | public abstract class ExpressionType { 15 | 16 | public interface Cases { 17 | R TOP(); 18 | 19 | R STRING(); 20 | 21 | R BOOLEAN(); 22 | 23 | R INTEGER(); 24 | 25 | R LONG(); 26 | 27 | R DOUBLE(); 28 | 29 | R DATE(); 30 | 31 | R ENUM(String className); 32 | 33 | R ITEMDEFINITION(ItemDefinition itemDefinition); 34 | } 35 | 36 | public abstract R match(ExpressionType.Cases cases); 37 | 38 | @Override 39 | public abstract int hashCode(); 40 | 41 | @Override 42 | public abstract boolean equals(@Nullable Object obj); 43 | 44 | @Override 45 | public abstract String toString(); 46 | 47 | public static boolean isNumeric(final ExpressionType givenType) { 48 | return !TOP().equals(givenType) 49 | && Arrays.asList(INTEGER(), LONG(), DOUBLE()).contains(givenType); 50 | } 51 | 52 | public boolean isSubtypeOf(final ExpressionType supertype) { 53 | return reflexivity(this, supertype) 54 | || TOPisTopElement(supertype) 55 | || INTEGERsubtypeOfLONG(this, supertype) 56 | || INTEGERsubtypeOfDOUBLE(this, supertype); 57 | } 58 | 59 | private boolean reflexivity(final ExpressionType subtype, final ExpressionType supertype) { 60 | return subtype.equals(supertype); 61 | } 62 | 63 | private boolean TOPisTopElement(final ExpressionType supertype) { 64 | return TOP().equals(supertype); 65 | } 66 | 67 | private boolean INTEGERsubtypeOfLONG(final ExpressionType subtype, final ExpressionType supertype) { 68 | return INTEGER().equals(subtype) && LONG().equals(supertype); 69 | } 70 | 71 | private boolean INTEGERsubtypeOfDOUBLE(final ExpressionType subtype, final ExpressionType supertype) { 72 | return INTEGER().equals(subtype) && DOUBLE().equals(supertype); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/feel/FeelExpression.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.feel; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.Optional; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | import org.derive4j.Data; 7 | import org.derive4j.Derive; 8 | import org.derive4j.Make; 9 | import org.derive4j.Visibility; 10 | 11 | @Data( 12 | value = 13 | @Derive( 14 | withVisibility = Visibility.Package, 15 | make = {Make.constructors, Make.caseOfMatching, Make.getters})) 16 | public abstract class FeelExpression { 17 | 18 | public interface Cases { 19 | R Empty(); 20 | 21 | R Null(); 22 | 23 | R BooleanLiteral(Boolean aBoolean); 24 | 25 | R DateLiteral(LocalDateTime dateTime); 26 | 27 | R DoubleLiteral(Double aDouble); 28 | 29 | R IntegerLiteral(Integer aInteger); 30 | 31 | R StringLiteral(String string); 32 | 33 | R VariableLiteral(String name); 34 | 35 | R RangeExpression( 36 | boolean isLeftInclusive, 37 | FeelExpression lowerBound, 38 | FeelExpression upperBound, 39 | boolean isRightInclusive); 40 | 41 | R UnaryExpression(Operator operator, FeelExpression expression); 42 | 43 | R BinaryExpression(FeelExpression left, Operator operator, FeelExpression right); 44 | 45 | R DisjunctionExpression(FeelExpression head, FeelExpression tail); 46 | } 47 | 48 | public abstract R match(Cases cases); 49 | 50 | public Optional subsumes(final FeelExpression expression) { 51 | return Subsumption.subsumes(this, expression, Subsumption.eq); 52 | } 53 | 54 | public boolean containsVariable(final String name) { 55 | return FeelExpressions.caseOf(this) 56 | .Empty_(false) 57 | .Null_(false) 58 | .BooleanLiteral_(false) 59 | .DateLiteral_(false) 60 | .DoubleLiteral_(false) 61 | .IntegerLiteral_(false) 62 | .StringLiteral_(false) 63 | .VariableLiteral(variableName -> variableName.equals(name)) 64 | .RangeExpression((__, lowerBound, upperBound, ___) -> 65 | lowerBound.containsVariable(name) || upperBound.containsVariable(name)) 66 | .UnaryExpression((__, expression) -> expression.containsVariable(name)) 67 | .BinaryExpression((left, __, right) -> left.containsVariable(name) || right.containsVariable(name)) 68 | .DisjunctionExpression((head, tail) -> head.containsVariable(name) || tail.containsVariable(name)); 69 | } 70 | 71 | public boolean isLiteral() { 72 | return FeelExpressions.caseOf(this) 73 | .BooleanLiteral_(true) 74 | .DateLiteral_(true) 75 | .DoubleLiteral_(true) 76 | .IntegerLiteral_(true) 77 | .StringLiteral_(true) 78 | .VariableLiteral_(true) 79 | .otherwise_(false); 80 | } 81 | 82 | public boolean containsNot() { 83 | return FeelExpressions.caseOf(this) 84 | .Empty_(false) 85 | .Null_(false) 86 | .BooleanLiteral_(false) 87 | .DateLiteral_(false) 88 | .DoubleLiteral_(false) 89 | .IntegerLiteral_(false) 90 | .StringLiteral_(false) 91 | .VariableLiteral_(false) 92 | .RangeExpression( 93 | (__, lowerBound, upperBound, ___) -> lowerBound.containsNot() || upperBound.containsNot()) 94 | .UnaryExpression((operator, expression) -> operator.equals(Operator.NOT)) 95 | .BinaryExpression((left, __, right) -> left.containsNot() || right.containsNot()) 96 | .DisjunctionExpression((head, tail) -> head.containsNot() || tail.containsNot()); 97 | } 98 | 99 | @Override 100 | public abstract int hashCode(); 101 | 102 | @Override 103 | public abstract boolean equals(@Nullable Object obj); 104 | 105 | @Override 106 | public abstract String toString(); 107 | } 108 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/feel/Operator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.feel; 2 | 3 | public enum Operator { 4 | ADD("+"), 5 | SUB("-"), 6 | MUL("*"), 7 | DIV("/"), 8 | EXP("**"), 9 | LT("<"), 10 | GT(">"), 11 | LE("<="), 12 | GE(">="), 13 | NOT("NOT"), 14 | OR("or"), 15 | AND("and"); 16 | 17 | private final String name; 18 | 19 | Operator(final String name) { 20 | this.name = name; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return name; 26 | } 27 | 28 | public boolean isLessThan() { 29 | return this.equals(LT) || this.equals(LE); 30 | } 31 | 32 | public boolean isGreaterThan() { 33 | return this.equals(GT) || this.equals(GE); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/util/Either.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.function.Function; 7 | import java.util.stream.Collector; 8 | import java.util.stream.Collectors; 9 | import org.checkerframework.checker.nullness.qual.Nullable; 10 | import org.derive4j.Data; 11 | import org.derive4j.Derive; 12 | import org.derive4j.Make; 13 | 14 | @Data(value = @Derive(make = {Make.constructors, Make.caseOfMatching, Make.getters})) 15 | public abstract class Either { 16 | public abstract X match(Function left, Function right); 17 | 18 | public Either bind(Function> function) { 19 | return this.match(Eithers::left, function); 20 | } 21 | 22 | public Either map(Function function) { 23 | return this.match(Eithers::left, right -> Eithers.right(function.apply(right))); 24 | } 25 | 26 | public static Collector, ?, Either>> reduce() { 27 | return Collectors.reducing( 28 | Eithers.right(new ArrayList<>()), 29 | either -> either.map(Arrays::asList), 30 | (either, eithers) -> either.bind(a -> eithers.bind(listOfA -> Eithers.right(appendAll(a, listOfA))))); 31 | } 32 | 33 | private static List appendAll(List list1, List list2) { 34 | list1.addAll(list2); 35 | return list1; 36 | } 37 | 38 | @Override 39 | public abstract int hashCode(); 40 | 41 | @Override 42 | public abstract boolean equals(@Nullable Object obj); 43 | 44 | @Override 45 | public abstract String toString(); 46 | } 47 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/util/Expression.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.util; 2 | 3 | import java.util.Objects; 4 | import org.camunda.bpm.model.dmn.impl.DmnModelConstants; 5 | import org.camunda.bpm.model.dmn.instance.LiteralExpression; 6 | import org.camunda.bpm.model.dmn.instance.UnaryTests; 7 | import org.checkerframework.checker.nullness.qual.Nullable; 8 | 9 | public class Expression { 10 | 11 | public final String textContent; 12 | public final String expressionLanguage; 13 | 14 | public Expression(final UnaryTests unaryTests, final @Nullable String toplevelExpressionLanguage) { 15 | this.textContent = unaryTests.getTextContent(); 16 | this.expressionLanguage = 17 | decideExpressionLanguage(unaryTests.getExpressionLanguage(), toplevelExpressionLanguage); 18 | } 19 | 20 | public Expression(final LiteralExpression literalExpression, final @Nullable String toplevelExpressionLanguage) { 21 | this.textContent = literalExpression.getTextContent(); 22 | this.expressionLanguage = 23 | decideExpressionLanguage(literalExpression.getExpressionLanguage(), toplevelExpressionLanguage); 24 | } 25 | 26 | private static String decideExpressionLanguage( 27 | final String localExpressionLanguage, final @Nullable String toplevelExpressionLanguage) { 28 | return Objects.requireNonNullElseGet( 29 | localExpressionLanguage, 30 | () -> Objects.requireNonNullElse(toplevelExpressionLanguage, DmnModelConstants.FEEL_NS)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/util/ProjectClassLoader.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.util; 2 | 3 | import org.checkerframework.checker.nullness.qual.Nullable; 4 | 5 | public enum ProjectClassLoader { 6 | INSTANCE; 7 | 8 | public @Nullable ClassLoader classLoader; 9 | } 10 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/util/TopLevelExpressionLanguage.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.util; 2 | 3 | import org.camunda.bpm.model.dmn.instance.LiteralExpression; 4 | import org.camunda.bpm.model.dmn.instance.UnaryTests; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | 7 | public record TopLevelExpressionLanguage(@Nullable String topLevelExpressionLanguage) { 8 | 9 | public Expression toExpression(final UnaryTests unaryTests) { 10 | return new Expression(unaryTests, topLevelExpressionLanguage); 11 | } 12 | 13 | public Expression toExpression(final LiteralExpression literalExpression) { 14 | return new Expression(literalExpression, topLevelExpressionLanguage); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/util/TriFunction.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.util; 2 | 3 | @FunctionalInterface 4 | public interface TriFunction { 5 | R apply(A a, B b, C c); 6 | } 7 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/util/Util.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.util; 2 | 3 | import java.util.Iterator; 4 | import java.util.Objects; 5 | import java.util.Spliterator; 6 | import java.util.Spliterators; 7 | import java.util.function.BiFunction; 8 | import java.util.stream.Stream; 9 | import java.util.stream.StreamSupport; 10 | 11 | public final class Util { 12 | 13 | private Util() {} 14 | 15 | public static Stream zip( 16 | final Stream a, 17 | final Stream b, 18 | final BiFunction zipper) { 19 | Objects.requireNonNull(zipper); 20 | final Spliterator aSpliterator = Objects.requireNonNull(a).spliterator(); 21 | final Spliterator bSpliterator = Objects.requireNonNull(b).spliterator(); 22 | 23 | // Zipping looses DISTINCT and SORTED characteristics 24 | final int characteristics = aSpliterator.characteristics() 25 | & bSpliterator.characteristics() 26 | & ~(Spliterator.DISTINCT | Spliterator.SORTED); 27 | 28 | final long zipSize = ((characteristics & Spliterator.SIZED) != 0) 29 | ? Math.min(aSpliterator.getExactSizeIfKnown(), bSpliterator.getExactSizeIfKnown()) 30 | : -1; 31 | 32 | final Iterator aIterator = Spliterators.iterator(aSpliterator); 33 | final Iterator bIterator = Spliterators.iterator(bSpliterator); 34 | final Iterator cIterator = new Iterator<>() { 35 | 36 | @Override 37 | public boolean hasNext() { 38 | return aIterator.hasNext() && bIterator.hasNext(); 39 | } 40 | 41 | @Override 42 | public C next() { 43 | return zipper.apply(aIterator.next(), bIterator.next()); 44 | } 45 | }; 46 | 47 | final Spliterator split = Spliterators.spliterator(cIterator, zipSize, characteristics); 48 | return (a.isParallel() || b.isParallel()) 49 | ? StreamSupport.stream(split, true) 50 | : StreamSupport.stream(split, false); 51 | } 52 | 53 | public static Stream zip( 54 | final Stream a, 55 | final Stream b, 56 | final Stream c, 57 | final TriFunction zipper) { 58 | Objects.requireNonNull(zipper); 59 | final Spliterator aSpliterator = Objects.requireNonNull(a).spliterator(); 60 | final Spliterator bSpliterator = Objects.requireNonNull(b).spliterator(); 61 | final Spliterator cSpliterator = Objects.requireNonNull(c).spliterator(); 62 | 63 | // Zipping looses DISTINCT and SORTED characteristics 64 | final int characteristics = aSpliterator.characteristics() 65 | & bSpliterator.characteristics() 66 | & cSpliterator.characteristics() 67 | & ~(Spliterator.DISTINCT | Spliterator.SORTED); 68 | 69 | final long zipSize = ((characteristics & Spliterator.SIZED) != 0) 70 | ? Math.min( 71 | aSpliterator.getExactSizeIfKnown(), 72 | Math.min(bSpliterator.getExactSizeIfKnown(), cSpliterator.getExactSizeIfKnown())) 73 | : -1; 74 | 75 | final Iterator aIterator = Spliterators.iterator(aSpliterator); 76 | final Iterator bIterator = Spliterators.iterator(bSpliterator); 77 | final Iterator cIterator = Spliterators.iterator(cSpliterator); 78 | 79 | final Iterator dIterator = new Iterator<>() { 80 | 81 | @Override 82 | public boolean hasNext() { 83 | return aIterator.hasNext() && bIterator.hasNext() && cIterator.hasNext(); 84 | } 85 | 86 | @Override 87 | public D next() { 88 | return zipper.apply(aIterator.next(), bIterator.next(), cIterator.next()); 89 | } 90 | }; 91 | 92 | final Spliterator split = Spliterators.spliterator(dIterator, zipSize, characteristics); 93 | return (a.isParallel() || b.isParallel() || c.isParallel()) 94 | ? StreamSupport.stream(split, true) 95 | : StreamSupport.stream(split, false); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/util/ValidatorLoader.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.util; 2 | 3 | import de.redsix.dmncheck.validators.core.Validator; 4 | import io.github.classgraph.ClassGraph; 5 | import io.github.classgraph.ClassInfoList; 6 | import io.github.classgraph.ScanResult; 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Modifier; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.Objects; 12 | import java.util.stream.Collectors; 13 | import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 14 | import org.checkerframework.checker.nullness.qual.Nullable; 15 | 16 | public class ValidatorLoader { 17 | 18 | private static final String VALIDATOR_PACKAGE = "de.redsix.dmncheck.validators"; 19 | private static final String VALIDATOR_CORE_PACKAGE = "de.redsix.dmncheck.validators.core"; 20 | 21 | private static int inputParameterHash; 22 | private static @MonotonicNonNull List validators; 23 | 24 | private ValidatorLoader() {} 25 | 26 | public static List getValidators() { 27 | return getValidators(null, null); 28 | } 29 | 30 | public static List getValidators(final String @Nullable [] packages, final String @Nullable [] classes) { 31 | if (inputParameterHash == Objects.hash(Arrays.hashCode(packages), Arrays.hashCode(classes)) 32 | && validators != null) { 33 | return validators; 34 | } 35 | 36 | inputParameterHash = Objects.hash(Arrays.hashCode(packages), Arrays.hashCode(classes)); 37 | 38 | try (ScanResult scanResult = new ClassGraph() 39 | .acceptClasses(Validator.class.getName()) 40 | .acceptPackages(VALIDATOR_CORE_PACKAGE) 41 | .acceptPackagesNonRecursive( 42 | packages == null ? new String[] {VALIDATOR_PACKAGE, VALIDATOR_CORE_PACKAGE} : packages) 43 | .acceptClasses(classes == null ? new String[] {} : classes) 44 | .scan()) { 45 | 46 | final ClassInfoList allValidatorClasses = scanResult.getClassesImplementing(Validator.class.getName()); 47 | 48 | validators = allValidatorClasses.loadClasses(Validator.class).stream() 49 | .filter(validatorClass -> !Modifier.isAbstract(validatorClass.getModifiers())) 50 | .filter(validatorClass -> !Modifier.isInterface(validatorClass.getModifiers())) 51 | .map(ValidatorLoader::instantiateValidator) 52 | .collect(Collectors.toList()); 53 | 54 | return validators; 55 | } 56 | } 57 | 58 | private static Validator instantiateValidator(final Class validator) { 59 | try { 60 | return validator.getDeclaredConstructor().newInstance(); 61 | } catch (IllegalAccessException 62 | | InstantiationException 63 | | NoSuchMethodException 64 | | InvocationTargetException e) { 65 | throw new RuntimeException("Failed to load validator " + validator, e); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/AggregationOutputTypeValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.feel.ExpressionType; 4 | import de.redsix.dmncheck.feel.ExpressionTypeParser; 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.util.Either; 8 | import de.redsix.dmncheck.validators.core.GenericValidator; 9 | import de.redsix.dmncheck.validators.core.ValidationContext; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import org.camunda.bpm.model.dmn.BuiltinAggregator; 15 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 16 | import org.camunda.bpm.model.dmn.instance.Output; 17 | 18 | public class AggregationOutputTypeValidator extends GenericValidator { 19 | 20 | @Override 21 | public boolean isApplicable(DecisionTable decisionTable, ValidationContext validationContext) { 22 | return Objects.nonNull(decisionTable.getAggregation()) 23 | && Arrays.asList(BuiltinAggregator.MAX, BuiltinAggregator.MIN, BuiltinAggregator.SUM) 24 | .contains(decisionTable.getAggregation()); 25 | } 26 | 27 | @Override 28 | public List validate(Output output, ValidationContext validationContext) { 29 | if (output.getTypeRef() == null) { 30 | return Collections.singletonList(ValidationResult.init 31 | .message("An aggregation is used but no output type is defined") 32 | .severity(Severity.WARNING) 33 | .element(output) 34 | .build()); 35 | } else { 36 | final Either eitherType = 37 | ExpressionTypeParser.parse(output.getTypeRef(), validationContext.getItemDefinitions()); 38 | return eitherType.match( 39 | validationResult -> Collections.singletonList( 40 | validationResult.element(output).build()), 41 | type -> { 42 | if (!ExpressionType.isNumeric(type)) { 43 | return Collections.singletonList(ValidationResult.init 44 | .message("Aggregations MAX, MIN, SUM are only valid with numeric output types") 45 | .element(output) 46 | .build()); 47 | } else { 48 | return Collections.emptyList(); 49 | } 50 | }); 51 | } 52 | } 53 | 54 | @Override 55 | public Class getClassUnderValidation() { 56 | return Output.class; 57 | } 58 | 59 | @Override 60 | public Class getClassUsedToCheckApplicability() { 61 | return DecisionTable.class; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/AggregationValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.result.ValidationResult; 4 | import de.redsix.dmncheck.validators.core.SimpleValidator; 5 | import de.redsix.dmncheck.validators.core.ValidationContext; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Objects; 9 | import org.camunda.bpm.model.dmn.BuiltinAggregator; 10 | import org.camunda.bpm.model.dmn.HitPolicy; 11 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 12 | 13 | public class AggregationValidator extends SimpleValidator { 14 | 15 | @Override 16 | public boolean isApplicable(DecisionTable decisionTable, ValidationContext validationContext) { 17 | return !HitPolicy.COLLECT.equals(decisionTable.getHitPolicy()); 18 | } 19 | 20 | @Override 21 | public List validate(DecisionTable decisionTable, ValidationContext validationContext) { 22 | final BuiltinAggregator builtinAggregator = decisionTable.getAggregation(); 23 | if (Objects.nonNull(builtinAggregator)) { 24 | return Collections.singletonList(ValidationResult.init 25 | .message("Aggregations are only valid with HitPolicy " + HitPolicy.COLLECT) 26 | .element(decisionTable) 27 | .build()); 28 | } else { 29 | return Collections.emptyList(); 30 | } 31 | } 32 | 33 | @Override 34 | public Class getClassUnderValidation() { 35 | return DecisionTable.class; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/ConflictingRuleValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.result.Severity; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.SimpleValidator; 6 | import de.redsix.dmncheck.validators.core.ValidationContext; 7 | import java.util.Arrays; 8 | import java.util.Comparator; 9 | import java.util.List; 10 | import java.util.TreeSet; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | import org.camunda.bpm.model.dmn.HitPolicy; 14 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 15 | import org.camunda.bpm.model.dmn.instance.Rule; 16 | import org.camunda.bpm.model.xml.instance.ModelElementInstance; 17 | 18 | public class ConflictingRuleValidator extends SimpleValidator { 19 | 20 | @Override 21 | public Class getClassUnderValidation() { 22 | return DecisionTable.class; 23 | } 24 | 25 | @Override 26 | public boolean isApplicable(DecisionTable decisionTable, ValidationContext validationContext) { 27 | return true; 28 | } 29 | 30 | @Override 31 | public List validate(DecisionTable decisionTable, ValidationContext validationContext) { 32 | return decisionTable.getRules().stream() 33 | .collect(Collectors.groupingBy(ConflictingRuleValidator::extractInputEntriesTextContent)) 34 | .values() 35 | .stream() 36 | .map(rules -> rules.stream() 37 | // Explicit types until https://github.com/typetools/checker-framework/issues/7064 is fixed. 38 | .collect(Collectors.>toCollection(() -> new TreeSet<>(Comparator.comparing( 39 | ConflictingRuleValidator::extractInputAndOutputEntriesTextContent))))) 40 | .filter(rules -> rules.size() > 1) 41 | .map(rules -> ValidationResult.init 42 | .message("Rule is conflicting with rules " 43 | + rules.stream().skip(1).map(Rule::getId).toList()) 44 | .severity( 45 | Arrays.asList(HitPolicy.COLLECT, HitPolicy.RULE_ORDER) 46 | .contains(decisionTable.getHitPolicy()) 47 | ? Severity.WARNING 48 | : Severity.ERROR) 49 | .element(rules.first()) 50 | .build()) 51 | .collect(Collectors.toList()); 52 | } 53 | 54 | private static List extractInputEntriesTextContent(Rule rule) { 55 | return rule.getInputEntries().stream() 56 | .map(ModelElementInstance::getTextContent) 57 | .collect(Collectors.toList()); 58 | } 59 | 60 | private static String extractInputAndOutputEntriesTextContent(Rule rule) { 61 | return Stream.concat(rule.getInputEntries().stream(), rule.getOutputEntries().stream()) 62 | .map(ModelElementInstance::getTextContent) 63 | .collect(Collectors.joining()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/DecisionIdAndNameValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import org.camunda.bpm.model.dmn.instance.Decision; 4 | 5 | public class DecisionIdAndNameValidator extends IdAndNameValidator { 6 | 7 | @Override 8 | public String getName() { 9 | return "decision"; 10 | } 11 | 12 | @Override 13 | public Class getClassUnderValidation() { 14 | return Decision.class; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/DefinitionsIdAndNameValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import org.camunda.bpm.model.dmn.instance.Definitions; 4 | 5 | public class DefinitionsIdAndNameValidator extends IdAndNameValidator { 6 | 7 | @Override 8 | public String getName() { 9 | return "definitions"; 10 | } 11 | 12 | @Override 13 | public Class getClassUnderValidation() { 14 | return Definitions.class; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/DuplicateColumnLabelValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.result.Severity; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.SimpleValidator; 6 | import de.redsix.dmncheck.validators.core.ValidationContext; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.function.Function; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 14 | import org.camunda.bpm.model.dmn.instance.Input; 15 | import org.camunda.bpm.model.dmn.instance.Output; 16 | 17 | public class DuplicateColumnLabelValidator extends SimpleValidator { 18 | 19 | @Override 20 | public boolean isApplicable(DecisionTable decisionTable, ValidationContext validationContext) { 21 | return true; 22 | } 23 | 24 | @Override 25 | public List validate(DecisionTable decisionTable, ValidationContext validationContext) { 26 | return Stream.concat( 27 | validateColumn(decisionTable, decisionTable.getInputs(), Input::getLabel).stream(), 28 | validateColumn(decisionTable, decisionTable.getOutputs(), Output::getLabel).stream()) 29 | .collect(Collectors.toList()); 30 | } 31 | 32 | private List validateColumn( 33 | DecisionTable decisionTable, Collection columns, Function getLabel) { 34 | final List labels = columns.stream().map(getLabel).toList(); 35 | 36 | return labels.stream() 37 | .filter(label -> Collections.frequency(labels, label) > 1) 38 | .distinct() 39 | .map(label -> ValidationResult.init 40 | .message("Column with label '" + label + "' is used more than once") 41 | .severity(Severity.WARNING) 42 | .element(decisionTable) 43 | .build()) 44 | .collect(Collectors.toList()); 45 | } 46 | 47 | @Override 48 | public Class getClassUnderValidation() { 49 | return DecisionTable.class; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/DuplicateRuleValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.result.Severity; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.SimpleValidator; 6 | import de.redsix.dmncheck.validators.core.ValidationContext; 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | import org.camunda.bpm.model.dmn.HitPolicy; 13 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 14 | import org.camunda.bpm.model.dmn.instance.Rule; 15 | import org.camunda.bpm.model.xml.instance.ModelElementInstance; 16 | 17 | public class DuplicateRuleValidator extends SimpleValidator { 18 | 19 | @Override 20 | public Class getClassUnderValidation() { 21 | return DecisionTable.class; 22 | } 23 | 24 | @Override 25 | public boolean isApplicable(DecisionTable decisionTable, ValidationContext validationContext) { 26 | return true; 27 | } 28 | 29 | @Override 30 | public List validate(DecisionTable decisionTable, ValidationContext validationContext) { 31 | final Collection rules = decisionTable.getRules(); 32 | final List> expressions = new ArrayList<>(); 33 | final List result = new ArrayList<>(); 34 | 35 | for (Rule rule : rules) { 36 | final List rowElements = Stream.concat( 37 | rule.getInputEntries().stream(), rule.getOutputEntries().stream()) 38 | .map(ModelElementInstance::getTextContent) 39 | .collect(Collectors.toList()); 40 | if (!expressions.contains(rowElements)) { 41 | expressions.add(rowElements); 42 | } else { 43 | result.add(ValidationResult.init 44 | .message("Rule is defined more than once") 45 | .severity( 46 | HitPolicy.COLLECT.equals(decisionTable.getHitPolicy()) 47 | ? Severity.WARNING 48 | : Severity.ERROR) 49 | .element(rule) 50 | .build()); 51 | } 52 | } 53 | return result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/ElementTypeDeclarationValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.feel.ExpressionType; 4 | import de.redsix.dmncheck.feel.ExpressionTypeParser; 5 | import de.redsix.dmncheck.feel.ExpressionTypes; 6 | import de.redsix.dmncheck.result.Severity; 7 | import de.redsix.dmncheck.result.ValidationResult; 8 | import de.redsix.dmncheck.util.Either; 9 | import de.redsix.dmncheck.validators.core.SimpleValidator; 10 | import de.redsix.dmncheck.validators.core.ValidationContext; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import org.camunda.bpm.model.dmn.instance.DmnElement; 15 | 16 | public abstract class ElementTypeDeclarationValidator extends SimpleValidator { 17 | 18 | abstract String getTypeRef(T expression); 19 | 20 | public boolean isApplicable(T expression, ValidationContext validationContext) { 21 | return true; 22 | } 23 | 24 | public List validate(T expression, ValidationContext validationContext) { 25 | final String expressionType = getTypeRef(expression); 26 | if (Objects.isNull(expressionType)) { 27 | return Collections.singletonList(ValidationResult.init 28 | .message(getClassUnderValidation().getSimpleName() + " has no type") 29 | .severity(Severity.WARNING) 30 | .element(expression) 31 | .build()); 32 | } else { 33 | final Either eitherType = 34 | ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()); 35 | return eitherType.match( 36 | validationResult -> Collections.singletonList( 37 | validationResult.element(expression).build()), 38 | type -> { 39 | if (ExpressionTypes.TOP().equals(type)) { 40 | return Collections.singletonList(ValidationResult.init 41 | .message("TOP is an internal type and cannot be used in declarations.") 42 | .severity(Severity.ERROR) 43 | .element(expression) 44 | .build()); 45 | } else { 46 | return Collections.emptyList(); 47 | } 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/IdAndNameValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.result.Severity; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.SimpleValidator; 6 | import de.redsix.dmncheck.validators.core.ValidationContext; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Objects; 10 | import org.camunda.bpm.model.dmn.instance.DmnElement; 11 | import org.camunda.bpm.model.dmn.instance.NamedElement; 12 | 13 | public abstract class IdAndNameValidator extends SimpleValidator { 14 | 15 | protected abstract String getName(); 16 | 17 | @Override 18 | public boolean isApplicable(T element, ValidationContext validationContext) { 19 | return true; 20 | } 21 | 22 | @Override 23 | public List validate(T element, ValidationContext validationContext) { 24 | final List validationResults = new ArrayList<>(); 25 | 26 | if (Objects.isNull(element.getId())) { 27 | validationResults.add(ValidationResult.init 28 | .message("A " + this.getName() + " has no id.") 29 | .element(element) 30 | .build()); 31 | } 32 | 33 | if (Objects.isNull(element.getName())) { 34 | validationResults.add(ValidationResult.init 35 | .message("A " + this.getName() + " has no name.") 36 | .severity(Severity.WARNING) 37 | .element(element) 38 | .build()); 39 | } 40 | 41 | return validationResults; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/InputDataIdAndNameValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import org.camunda.bpm.model.dmn.instance.InputData; 4 | 5 | public class InputDataIdAndNameValidator extends IdAndNameValidator { 6 | 7 | @Override 8 | public String getName() { 9 | return "input"; 10 | } 11 | 12 | @Override 13 | public Class getClassUnderValidation() { 14 | return InputData.class; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/InputEntryTypeValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.feel.ExpressionType; 4 | import de.redsix.dmncheck.feel.ExpressionTypeParser; 5 | import de.redsix.dmncheck.result.ValidationResult; 6 | import de.redsix.dmncheck.util.Either; 7 | import de.redsix.dmncheck.validators.core.ValidationContext; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 12 | import org.camunda.bpm.model.dmn.instance.Input; 13 | 14 | public class InputEntryTypeValidator extends TypeValidator { 15 | 16 | @Override 17 | public boolean isApplicable(DecisionTable decisionTable, ValidationContext validationContext) { 18 | return decisionTable.getInputs().stream().allMatch(input -> { 19 | final String expressionType = input.getInputExpression().getTypeRef(); 20 | return ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()) 21 | .match(parseError -> false, parseResult -> true); 22 | }); 23 | } 24 | 25 | @Override 26 | public List validate(DecisionTable decisionTable, ValidationContext validationContext) { 27 | final Either> eitherInputTypes = 28 | decisionTable.getInputs().stream() 29 | .map(input -> input.getInputExpression().getTypeRef()) 30 | .map(typeRef -> ExpressionTypeParser.parse(typeRef, validationContext.getItemDefinitions())) 31 | .collect(Either.reduce()); 32 | 33 | return decisionTable.getRules().stream() 34 | .flatMap(rule -> { 35 | final Stream inputVariables = 36 | decisionTable.getInputs().stream().map(Input::getCamundaInputVariable); 37 | 38 | return eitherInputTypes.match( 39 | validationResult -> 40 | Stream.of(validationResult.element(rule).build()), 41 | inputTypes -> typecheck( 42 | rule, 43 | rule.getInputEntries().stream().map(toplevelExpressionLanguage::toExpression), 44 | inputVariables, 45 | inputTypes.stream())); 46 | }) 47 | .collect(Collectors.toList()); 48 | } 49 | 50 | @Override 51 | protected Class getClassUnderValidation() { 52 | return DecisionTable.class; 53 | } 54 | 55 | @Override 56 | public String errorMessage() { 57 | return "Type of input entry does not match type of input expression"; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/InputTypeDeclarationValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import org.camunda.bpm.model.dmn.instance.InputExpression; 4 | 5 | public class InputTypeDeclarationValidator extends ElementTypeDeclarationValidator { 6 | 7 | @Override 8 | public String getTypeRef(InputExpression inputExpression) { 9 | return inputExpression.getTypeRef(); 10 | } 11 | 12 | @Override 13 | public Class getClassUnderValidation() { 14 | return InputExpression.class; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/InputValuesTypeValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.feel.ExpressionTypeParser; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.ValidationContext; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.Stream; 10 | import org.camunda.bpm.model.dmn.instance.Input; 11 | 12 | public class InputValuesTypeValidator extends TypeValidator { 13 | 14 | @Override 15 | public boolean isApplicable(final Input input, ValidationContext validationContext) { 16 | final String expressionType = input.getInputExpression().getTypeRef(); 17 | return input.getInputValues() != null 18 | && ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()) 19 | .match(parseError -> false, parseResult -> true); 20 | } 21 | 22 | @Override 23 | public List validate(final Input input, ValidationContext validationContext) { 24 | final String expressionType = input.getInputExpression().getTypeRef(); 25 | 26 | return ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()) 27 | .match( 28 | validationResult -> Collections.singletonList( 29 | validationResult.element(input).build()), 30 | inputType -> typecheck( 31 | input, 32 | Stream.of(input.getInputValues()).map(toplevelExpressionLanguage::toExpression), 33 | Stream.of(inputType)) 34 | .collect(Collectors.toList())); 35 | } 36 | 37 | @Override 38 | protected Class getClassUnderValidation() { 39 | return Input.class; 40 | } 41 | 42 | @Override 43 | String errorMessage() { 44 | return "Type of predefined input values does not match type of input expression"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/ItemDefinitionAllowedValuesTypeValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.feel.ExpressionTypeParser; 4 | import de.redsix.dmncheck.result.Severity; 5 | import de.redsix.dmncheck.result.ValidationResult; 6 | import de.redsix.dmncheck.validators.core.ValidationContext; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | import org.camunda.bpm.model.dmn.instance.ItemDefinition; 12 | 13 | public class ItemDefinitionAllowedValuesTypeValidator extends TypeValidator { 14 | 15 | @Override 16 | public boolean isApplicable(ItemDefinition itemDefinition, ValidationContext validationContext) { 17 | return itemDefinition.getAllowedValues() != null 18 | || itemDefinition.getItemComponents().stream() 19 | .anyMatch(itemComponent -> itemComponent.getAllowedValues() != null); 20 | } 21 | 22 | @Override 23 | public List validate(ItemDefinition itemDefinition, ValidationContext validationContext) { 24 | 25 | final Collection itemDefinitionsAndComponents = itemDefinition.getItemComponents().stream() 26 | .filter(itemComponent -> itemComponent.getAllowedValues() != null) 27 | .collect(Collectors.toList()); 28 | 29 | if (itemDefinition.getAllowedValues() != null) { 30 | itemDefinitionsAndComponents.add(itemDefinition); 31 | } 32 | 33 | return itemDefinitionsAndComponents.stream() 34 | .flatMap(itemDefinitionOrComponent -> { 35 | if (itemDefinitionOrComponent.getTypeRef() == null) { 36 | return Stream.of(ValidationResult.init 37 | .message("ItemDefintion uses AllowedValues without a type declaration") 38 | .severity(Severity.WARNING) 39 | .element(itemDefinitionOrComponent) 40 | .build()); 41 | } else { 42 | final String expressionType = 43 | itemDefinitionOrComponent.getTypeRef().getTextContent(); 44 | return ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()) 45 | .match( 46 | validationResult -> Stream.of(validationResult 47 | .element(itemDefinitionOrComponent) 48 | .build()), 49 | inputType -> typecheck( 50 | itemDefinitionOrComponent, 51 | Stream.of(itemDefinitionOrComponent.getAllowedValues()) 52 | .map(toplevelExpressionLanguage::toExpression), 53 | Stream.of(inputType))); 54 | } 55 | }) 56 | .collect(Collectors.toList()); 57 | } 58 | 59 | @Override 60 | protected Class getClassUnderValidation() { 61 | return ItemDefinition.class; 62 | } 63 | 64 | @Override 65 | public String errorMessage() { 66 | return "Type of item definition does not match type of allowed values"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/ItemDefinitionIdAndNameValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import org.camunda.bpm.model.dmn.instance.ItemDefinition; 4 | 5 | public class ItemDefinitionIdAndNameValidator extends IdAndNameValidator { 6 | @Override 7 | protected String getName() { 8 | return "item definition"; 9 | } 10 | 11 | @Override 12 | protected Class getClassUnderValidation() { 13 | return ItemDefinition.class; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/KnowledgeSourceIdAndNameValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import org.camunda.bpm.model.dmn.instance.KnowledgeSource; 4 | 5 | public class KnowledgeSourceIdAndNameValidator extends IdAndNameValidator { 6 | 7 | @Override 8 | public String getName() { 9 | return "knowledge source"; 10 | } 11 | 12 | @Override 13 | public Class getClassUnderValidation() { 14 | return KnowledgeSource.class; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/NoDecisionPresentValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.result.Severity; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.SimpleValidator; 6 | import de.redsix.dmncheck.validators.core.ValidationContext; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.instance.Decision; 10 | import org.camunda.bpm.model.dmn.instance.Definitions; 11 | 12 | public class NoDecisionPresentValidator extends SimpleValidator { 13 | 14 | @Override 15 | public boolean isApplicable(Definitions definitions, ValidationContext validationContext) { 16 | return true; 17 | } 18 | 19 | @Override 20 | public List validate(Definitions definitions, ValidationContext validationContext) { 21 | if (definitions.getChildElementsByType(Decision.class).isEmpty()) { 22 | return Collections.singletonList(ValidationResult.init 23 | .message("No decisions found") 24 | .severity(Severity.WARNING) 25 | .element(definitions) 26 | .build()); 27 | } else { 28 | return Collections.emptyList(); 29 | } 30 | } 31 | 32 | @Override 33 | public Class getClassUnderValidation() { 34 | return Definitions.class; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/OutputEntryTypeValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.feel.ExpressionType; 4 | import de.redsix.dmncheck.feel.ExpressionTypeParser; 5 | import de.redsix.dmncheck.result.ValidationResult; 6 | import de.redsix.dmncheck.util.Either; 7 | import de.redsix.dmncheck.validators.core.ValidationContext; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 12 | import org.camunda.bpm.model.dmn.instance.OutputClause; 13 | 14 | public class OutputEntryTypeValidator extends TypeValidator { 15 | 16 | @Override 17 | public boolean isApplicable(DecisionTable decisionTable, ValidationContext validationContext) { 18 | return decisionTable.getOutputs().stream().allMatch(output -> { 19 | final String expressionType = output.getTypeRef(); 20 | return ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()) 21 | .match(parseError -> false, parseResult -> true); 22 | }); 23 | } 24 | 25 | @Override 26 | public List validate(DecisionTable decisionTable, ValidationContext validationContext) { 27 | final Either> eitherOutputTypes = 28 | decisionTable.getOutputs().stream() 29 | .map(OutputClause::getTypeRef) 30 | .map(typeRef -> ExpressionTypeParser.parse(typeRef, validationContext.getItemDefinitions())) 31 | .collect(Either.reduce()); 32 | 33 | return decisionTable.getRules().stream() 34 | .flatMap(rule -> eitherOutputTypes.match( 35 | validationResult -> 36 | Stream.of(validationResult.element(rule).build()), 37 | outputTypes -> typecheck( 38 | rule, 39 | rule.getOutputEntries().stream().map(toplevelExpressionLanguage::toExpression), 40 | outputTypes.stream()))) 41 | .collect(Collectors.toList()); 42 | } 43 | 44 | @Override 45 | protected Class getClassUnderValidation() { 46 | return DecisionTable.class; 47 | } 48 | 49 | @Override 50 | public String errorMessage() { 51 | return "Type of output entry does not match type of output expression"; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/OutputTypeDeclarationValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import org.camunda.bpm.model.dmn.instance.Output; 4 | 5 | public class OutputTypeDeclarationValidator extends ElementTypeDeclarationValidator { 6 | 7 | @Override 8 | public String getTypeRef(Output output) { 9 | return output.getTypeRef(); 10 | } 11 | 12 | @Override 13 | public Class getClassUnderValidation() { 14 | return Output.class; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/OutputValuesTypeValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.feel.ExpressionTypeParser; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.ValidationContext; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.Stream; 10 | import org.camunda.bpm.model.dmn.instance.Output; 11 | 12 | public class OutputValuesTypeValidator extends TypeValidator { 13 | 14 | @Override 15 | public boolean isApplicable(final Output output, ValidationContext validationContext) { 16 | final String expressionType = output.getTypeRef(); 17 | return output.getOutputValues() != null 18 | && ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()) 19 | .match(parseError -> false, parseResult -> true); 20 | } 21 | 22 | @Override 23 | public List validate(final Output output, ValidationContext validationContext) { 24 | final String expressionType = output.getTypeRef(); 25 | 26 | return ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions()) 27 | .match( 28 | validationResult -> Collections.singletonList( 29 | validationResult.element(output).build()), 30 | inputType -> typecheck( 31 | output, 32 | Stream.of(output.getOutputValues()) 33 | .map(toplevelExpressionLanguage::toExpression), 34 | Stream.of(inputType)) 35 | .collect(Collectors.toList())); 36 | } 37 | 38 | @Override 39 | protected Class getClassUnderValidation() { 40 | return Output.class; 41 | } 42 | 43 | @Override 44 | String errorMessage() { 45 | return "Type of predefined output values does not match type of output expression"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /validators/src/main/java/de/redsix/dmncheck/validators/RequirementGraphLeafValidator.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import de.redsix.dmncheck.drg.RequirementGraph; 4 | import de.redsix.dmncheck.result.ValidationResult; 5 | import de.redsix.dmncheck.validators.core.RequirementGraphValidator; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import org.camunda.bpm.model.dmn.instance.DrgElement; 9 | import org.jgrapht.graph.DefaultEdge; 10 | import org.jgrapht.traverse.DepthFirstIterator; 11 | 12 | public class RequirementGraphLeafValidator extends RequirementGraphValidator { 13 | 14 | @Override 15 | public List validate(RequirementGraph drg) { 16 | final DepthFirstIterator iterator = new DepthFirstIterator<>(drg); 17 | 18 | int numberLeafNodes = 0; 19 | while (iterator.hasNext()) { 20 | // Self-loops are not allowed in requirement graphs therefore it is 21 | // sufficient to check if the out degree of the node is zero. 22 | if (drg.outDegreeOf(iterator.next()) == 0) { 23 | numberLeafNodes++; 24 | } 25 | } 26 | 27 | if (numberLeafNodes < 2) { 28 | return Collections.emptyList(); 29 | } else { 30 | return Collections.singletonList(ValidationResult.init 31 | .message("Requirement graphs may only contain one leaf node") 32 | .element(drg.getDefinitions()) 33 | .build()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/feel/ExpressionTypeParserTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.feel; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import de.redsix.dmncheck.validators.util.TestEnum; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import org.camunda.bpm.model.dmn.Dmn; 9 | import org.camunda.bpm.model.dmn.instance.ItemDefinition; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ExpressionTypeParserTest { 13 | 14 | @Test 15 | void shouldParseEmpty() { 16 | final List itemDefinitions = Collections.emptyList(); 17 | 18 | final ExpressionType type = ExpressionTypeParser.PARSER(itemDefinitions).parse(""); 19 | 20 | final ExpressionType expectedType = ExpressionTypes.TOP(); 21 | 22 | assertEquals(expectedType, type); 23 | } 24 | 25 | @Test 26 | void shouldParseBool() { 27 | final List itemDefinitions = Collections.emptyList(); 28 | 29 | final ExpressionType type = ExpressionTypeParser.PARSER(itemDefinitions).parse("boolean"); 30 | 31 | final ExpressionType expectedType = ExpressionTypes.BOOLEAN(); 32 | 33 | assertEquals(expectedType, type); 34 | } 35 | 36 | @Test 37 | void shouldParseInteger() { 38 | final List itemDefinitions = Collections.emptyList(); 39 | 40 | final ExpressionType type = ExpressionTypeParser.PARSER(itemDefinitions).parse("integer"); 41 | 42 | final ExpressionType expectedType = ExpressionTypes.INTEGER(); 43 | 44 | assertEquals(expectedType, type); 45 | } 46 | 47 | @Test 48 | void shouldParseString() { 49 | final List itemDefinitions = Collections.emptyList(); 50 | 51 | final ExpressionType type = ExpressionTypeParser.PARSER(itemDefinitions).parse("string"); 52 | 53 | final ExpressionType expectedType = ExpressionTypes.STRING(); 54 | 55 | assertEquals(expectedType, type); 56 | } 57 | 58 | @Test 59 | void shouldParseDouble() { 60 | final List itemDefinitions = Collections.emptyList(); 61 | 62 | final ExpressionType type = ExpressionTypeParser.PARSER(itemDefinitions).parse("double"); 63 | 64 | final ExpressionType expectedType = ExpressionTypes.DOUBLE(); 65 | 66 | assertEquals(expectedType, type); 67 | } 68 | 69 | @Test 70 | void shouldParseDate() { 71 | final List itemDefinitions = Collections.emptyList(); 72 | 73 | final ExpressionType type = ExpressionTypeParser.PARSER(itemDefinitions).parse("date"); 74 | 75 | final ExpressionType expectedType = ExpressionTypes.DATE(); 76 | 77 | assertEquals(expectedType, type); 78 | } 79 | 80 | @Test 81 | void shouldParseEnum() { 82 | final List itemDefinitions = Collections.emptyList(); 83 | 84 | final ExpressionType type = 85 | ExpressionTypeParser.PARSER(itemDefinitions).parse(TestEnum.class.getCanonicalName()); 86 | 87 | final ExpressionType expectedType = ExpressionTypes.ENUM(TestEnum.class.getCanonicalName()); 88 | 89 | assertEquals(expectedType, type); 90 | } 91 | 92 | @Test 93 | void shouldParseItemDefinition() { 94 | final ItemDefinition itemDefinition = Dmn.createEmptyModel().newInstance(ItemDefinition.class); 95 | itemDefinition.setName("myItemDefinition"); 96 | 97 | final List itemDefinitions = Collections.singletonList(itemDefinition); 98 | 99 | final ExpressionType type = ExpressionTypeParser.PARSER(itemDefinitions).parse("myItemDefinition"); 100 | 101 | final ExpressionType expectedType = ExpressionTypes.ITEMDEFINITION(itemDefinition); 102 | 103 | assertEquals(expectedType, type); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/feel/FeelExpressionTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.feel; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.time.LocalDateTime; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class FeelExpressionTest { 10 | 11 | @Test 12 | void emptyNeverContainsVariable() { 13 | assertFalse(FeelExpressions.Empty().containsVariable("x")); 14 | } 15 | 16 | @Test 17 | void boolNeverContainsVariable() { 18 | assertFalse(FeelExpressions.BooleanLiteral(true).containsVariable("x")); 19 | } 20 | 21 | @Test 22 | void dateNeverContainsVariable() { 23 | assertFalse(FeelExpressions.DateLiteral(LocalDateTime.MIN).containsVariable("x")); 24 | } 25 | 26 | @Test 27 | void doubleNeverContainsVariable() { 28 | assertFalse(FeelExpressions.DoubleLiteral(0.0).containsVariable("x")); 29 | } 30 | 31 | @Test 32 | void intNeverContainsVariable() { 33 | assertFalse(FeelExpressions.IntegerLiteral(1).containsVariable("x")); 34 | } 35 | 36 | @Test 37 | void stringNeverContainsVariable() { 38 | assertFalse(FeelExpressions.StringLiteral("foobar").containsVariable("x")); 39 | } 40 | 41 | @Test 42 | void variableThatContainsVariable() { 43 | assertTrue(FeelExpressions.VariableLiteral("x").containsVariable("x")); 44 | } 45 | 46 | @Test 47 | void variableThatDoesNotContainVariable() { 48 | assertFalse(FeelExpressions.VariableLiteral("y").containsVariable("x")); 49 | } 50 | 51 | @Test 52 | void rangeContainsVariable() { 53 | assertTrue(FeelExpressions.RangeExpression( 54 | true, FeelExpressions.VariableLiteral("x"), FeelExpressions.Empty(), false) 55 | .containsVariable("x")); 56 | } 57 | 58 | @Test 59 | void rangeDoesNotContainVariable() { 60 | assertFalse(FeelExpressions.RangeExpression( 61 | true, FeelExpressions.VariableLiteral("y"), FeelExpressions.Empty(), false) 62 | .containsVariable("x")); 63 | } 64 | 65 | @Test 66 | void unaryContainsVariable() { 67 | assertTrue(FeelExpressions.UnaryExpression(Operator.GT, FeelExpressions.VariableLiteral("x")) 68 | .containsVariable("x")); 69 | } 70 | 71 | @Test 72 | void unaryDoesNotContainVariable() { 73 | assertFalse(FeelExpressions.UnaryExpression(Operator.GT, FeelExpressions.VariableLiteral("y")) 74 | .containsVariable("x")); 75 | } 76 | 77 | @Test 78 | void binaryContainsVariable() { 79 | assertTrue(FeelExpressions.BinaryExpression( 80 | FeelExpressions.Empty(), Operator.GT, FeelExpressions.VariableLiteral("x")) 81 | .containsVariable("x")); 82 | } 83 | 84 | @Test 85 | void binaryDosNotContainVariable() { 86 | assertFalse(FeelExpressions.BinaryExpression( 87 | FeelExpressions.Empty(), Operator.GT, FeelExpressions.VariableLiteral("y")) 88 | .containsVariable("x")); 89 | } 90 | 91 | @Test 92 | void disjunctionContainsVariableInTail() { 93 | assertTrue(FeelExpressions.DisjunctionExpression(FeelExpressions.Empty(), FeelExpressions.VariableLiteral("x")) 94 | .containsVariable("x")); 95 | } 96 | 97 | @Test 98 | void disjunctionContainsVariableInHead() { 99 | assertTrue(FeelExpressions.DisjunctionExpression(FeelExpressions.VariableLiteral("x"), FeelExpressions.Empty()) 100 | .containsVariable("x")); 101 | } 102 | 103 | @Test 104 | void disjunctionDosNotContainVariableInTail() { 105 | assertFalse(FeelExpressions.DisjunctionExpression(FeelExpressions.Empty(), FeelExpressions.VariableLiteral("y")) 106 | .containsVariable("x")); 107 | } 108 | 109 | @Test 110 | void disjunctionDoesNotContainVariableInHead() { 111 | assertFalse(FeelExpressions.DisjunctionExpression(FeelExpressions.VariableLiteral("y"), FeelExpressions.Empty()) 112 | .containsVariable("x")); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/AggregationOutputTypeValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.BuiltinAggregator; 10 | import org.camunda.bpm.model.dmn.HitPolicy; 11 | import org.camunda.bpm.model.dmn.instance.Output; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class AggregationOutputTypeValidatorTest extends WithDecisionTable { 15 | 16 | private final AggregationOutputTypeValidator testee = new AggregationOutputTypeValidator(); 17 | 18 | @Test 19 | void shouldErrorOnStringOutputWithMaxAggregator() { 20 | decisionTable.setHitPolicy(HitPolicy.COLLECT); 21 | decisionTable.setAggregation(BuiltinAggregator.MAX); 22 | final Output output = modelInstance.newInstance(Output.class); 23 | output.setTypeRef("string"); 24 | decisionTable.getOutputs().add(output); 25 | 26 | final List validationResults = testee.apply(modelInstance); 27 | 28 | assertEquals(1, validationResults.size()); 29 | final ValidationResult validationResult = validationResults.get(0); 30 | assertAll( 31 | () -> assertEquals( 32 | "Aggregations MAX, MIN, SUM are only valid with numeric output types", 33 | validationResult.getMessage()), 34 | () -> assertEquals(output, validationResult.getElement()), 35 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 36 | } 37 | 38 | @Test 39 | void shouldWarnOnEmptyOutputtypeWithAggregation() { 40 | decisionTable.setHitPolicy(HitPolicy.COLLECT); 41 | decisionTable.setAggregation(BuiltinAggregator.MAX); 42 | final Output output = modelInstance.newInstance(Output.class); 43 | decisionTable.getOutputs().add(output); 44 | 45 | final List validationResults = testee.apply(modelInstance); 46 | 47 | assertEquals(1, validationResults.size()); 48 | final ValidationResult validationResult = validationResults.get(0); 49 | assertAll( 50 | () -> assertEquals( 51 | "An aggregation is used but no output type is defined", validationResult.getMessage()), 52 | () -> assertEquals(output, validationResult.getElement()), 53 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 54 | } 55 | 56 | @Test 57 | void shouldErrorOnUnsupportedOutputType() { 58 | decisionTable.setHitPolicy(HitPolicy.COLLECT); 59 | decisionTable.setAggregation(BuiltinAggregator.MAX); 60 | final Output output = modelInstance.newInstance(Output.class); 61 | output.setTypeRef("unsupportedType"); 62 | decisionTable.getOutputs().add(output); 63 | 64 | final List validationResults = testee.apply(modelInstance); 65 | 66 | assertEquals(1, validationResults.size()); 67 | final ValidationResult validationResult = validationResults.get(0); 68 | assertAll( 69 | () -> assertEquals( 70 | "Could not parse FEEL expression type 'unsupportedType'", validationResult.getMessage()), 71 | () -> assertEquals(output, validationResult.getElement()), 72 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 73 | } 74 | 75 | @Test 76 | void shouldAllowAggregatorMaxWithIntegerOutputs() { 77 | decisionTable.setHitPolicy(HitPolicy.COLLECT); 78 | decisionTable.setAggregation(BuiltinAggregator.MAX); 79 | final Output output = modelInstance.newInstance(Output.class); 80 | output.setTypeRef("integer"); 81 | decisionTable.getOutputs().add(output); 82 | 83 | final List validationResults = testee.apply(modelInstance); 84 | 85 | assertTrue(validationResults.isEmpty()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/AggregationValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.BuiltinAggregator; 10 | import org.camunda.bpm.model.dmn.HitPolicy; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class AggregationValidatorTest extends WithDecisionTable { 14 | 15 | private final AggregationValidator testee = new AggregationValidator(); 16 | 17 | @Test 18 | void shouldErrorOnHitPolicyUniqueWithAggregatorCount() { 19 | decisionTable.setHitPolicy(HitPolicy.UNIQUE); 20 | decisionTable.setAggregation(BuiltinAggregator.COUNT); 21 | 22 | final List validationResults = testee.apply(modelInstance); 23 | 24 | assertEquals(1, validationResults.size()); 25 | final ValidationResult validationResult = validationResults.get(0); 26 | assertAll( 27 | () -> assertEquals("Aggregations are only valid with HitPolicy COLLECT", validationResult.getMessage()), 28 | () -> assertEquals(decisionTable, validationResult.getElement()), 29 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 30 | } 31 | 32 | @Test 33 | void shouldAllowAggregatorCountWithHitPolicyCollect() { 34 | decisionTable.setHitPolicy(HitPolicy.COLLECT); 35 | decisionTable.setAggregation(BuiltinAggregator.COUNT); 36 | 37 | final List validationResults = testee.apply(modelInstance); 38 | 39 | assertTrue(validationResults.isEmpty()); 40 | } 41 | 42 | @Test 43 | void shouldSkipDecisionTablesWithoutAggregators() { 44 | decisionTable.setHitPolicy(HitPolicy.UNIQUE); 45 | assertNull(decisionTable.getAggregation()); 46 | 47 | final List validationResults = testee.apply(modelInstance); 48 | 49 | assertTrue(validationResults.isEmpty()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/ConflictingRuleValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.HitPolicy; 10 | import org.camunda.bpm.model.dmn.instance.InputEntry; 11 | import org.camunda.bpm.model.dmn.instance.OutputEntry; 12 | import org.camunda.bpm.model.dmn.instance.Rule; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.CsvSource; 16 | 17 | class ConflictingRuleValidatorTest extends WithDecisionTable { 18 | 19 | private final ConflictingRuleValidator testee = new ConflictingRuleValidator(); 20 | 21 | @ParameterizedTest 22 | @CsvSource({ 23 | "COLLECT, WARNING", 24 | "RULE_ORDER, WARNING", 25 | "UNIQUE, ERROR", 26 | "FIRST, ERROR", 27 | "PRIORITY, ERROR", 28 | "ANY, ERROR", 29 | "OUTPUT_ORDER, ERROR" 30 | }) 31 | void shouldDetectConflictingRulesForGivenHitPolicies(final String hitPolicy, final String severity) { 32 | decisionTable.setHitPolicy(HitPolicy.valueOf(hitPolicy)); 33 | 34 | final InputEntry inputEntry = modelInstance.newInstance(InputEntry.class); 35 | inputEntry.setTextContent("foo"); 36 | 37 | final InputEntry inputEntry2 = modelInstance.newInstance(InputEntry.class); 38 | inputEntry2.setTextContent("foo"); 39 | 40 | final OutputEntry outputEntry = modelInstance.newInstance(OutputEntry.class); 41 | outputEntry.setTextContent("bar1"); 42 | 43 | final OutputEntry outputEntry2 = modelInstance.newInstance(OutputEntry.class); 44 | outputEntry2.setTextContent("bar2"); 45 | 46 | final Rule rule1 = modelInstance.newInstance(Rule.class); 47 | final Rule rule2 = modelInstance.newInstance(Rule.class); 48 | 49 | rule1.getInputEntries().add(inputEntry); 50 | rule2.getInputEntries().add(inputEntry2); 51 | 52 | rule1.getOutputEntries().add(outputEntry); 53 | rule2.getOutputEntries().add(outputEntry2); 54 | 55 | decisionTable.getRules().add(rule1); 56 | decisionTable.getRules().add(rule2); 57 | 58 | final List validationResults = testee.apply(modelInstance); 59 | 60 | assertEquals(1, validationResults.size()); 61 | final ValidationResult validationResult = validationResults.get(0); 62 | assertAll( 63 | () -> assertEquals( 64 | "Rule is conflicting with rules [" + rule2.getId() + "]", validationResult.getMessage()), 65 | () -> assertEquals(rule1, validationResult.getElement()), 66 | () -> assertEquals(Severity.valueOf(severity), validationResult.getSeverity())); 67 | } 68 | 69 | @Test 70 | void shouldAllowDuplicateRules() { 71 | final InputEntry inputEntry = modelInstance.newInstance(InputEntry.class); 72 | inputEntry.setTextContent("foo"); 73 | 74 | final InputEntry inputEntry2 = modelInstance.newInstance(InputEntry.class); 75 | inputEntry2.setTextContent("foo"); 76 | 77 | final OutputEntry outputEntry = modelInstance.newInstance(OutputEntry.class); 78 | outputEntry.setTextContent("bar"); 79 | 80 | final OutputEntry outputEntry2 = modelInstance.newInstance(OutputEntry.class); 81 | outputEntry2.setTextContent("bar"); 82 | 83 | final Rule rule1 = modelInstance.newInstance(Rule.class); 84 | final Rule rule2 = modelInstance.newInstance(Rule.class); 85 | 86 | rule1.getInputEntries().add(inputEntry); 87 | rule2.getInputEntries().add(inputEntry2); 88 | 89 | rule1.getOutputEntries().add(outputEntry); 90 | rule2.getOutputEntries().add(outputEntry2); 91 | 92 | decisionTable.getRules().add(rule1); 93 | decisionTable.getRules().add(rule2); 94 | 95 | final List validationResults = testee.apply(modelInstance); 96 | 97 | assertTrue(validationResults.isEmpty()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/DecisionIdAndNameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import de.redsix.dmncheck.result.Severity; 8 | import de.redsix.dmncheck.result.ValidationResult; 9 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 10 | import java.util.List; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class DecisionIdAndNameValidatorTest extends WithDecisionTable { 14 | 15 | private final DecisionIdAndNameValidator testee = new DecisionIdAndNameValidator(); 16 | 17 | @Test 18 | void shouldErrorIfDecisionHasNoId() { 19 | decision.setId(null); 20 | decision.setName("Test"); 21 | 22 | final List validationResults = testee.apply(modelInstance); 23 | 24 | assertEquals(1, validationResults.size()); 25 | final ValidationResult validationResult = validationResults.get(0); 26 | assertAll( 27 | () -> assertEquals("A decision has no id.", validationResult.getMessage()), 28 | () -> assertEquals(decision, validationResult.getElement()), 29 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 30 | } 31 | 32 | @Test 33 | void shouldWarnIfDecisionHasNoName() { 34 | decision.setId("1"); 35 | decision.setName(null); 36 | 37 | final List validationResults = testee.apply(modelInstance); 38 | 39 | assertEquals(1, validationResults.size()); 40 | final ValidationResult validationResult = validationResults.get(0); 41 | assertAll( 42 | () -> assertEquals("A decision has no name.", validationResult.getMessage()), 43 | () -> assertEquals(decision, validationResult.getElement()), 44 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 45 | } 46 | 47 | @Test 48 | void shouldAllowAggregatorCountWithHitPolicyCollect() { 49 | decision.setId("1"); 50 | decision.setName("Test"); 51 | 52 | final List validationResults = testee.apply(modelInstance); 53 | 54 | assertTrue(validationResults.isEmpty()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/DefinitionsIdAndNameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import de.redsix.dmncheck.result.Severity; 8 | import de.redsix.dmncheck.result.ValidationResult; 9 | import de.redsix.dmncheck.validators.util.WithDefinitions; 10 | import java.util.List; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class DefinitionsIdAndNameValidatorTest extends WithDefinitions { 14 | 15 | private final DefinitionsIdAndNameValidator testee = new DefinitionsIdAndNameValidator(); 16 | 17 | @Test 18 | void shouldErrorIfDefinitionsHasNoId() { 19 | definitions.setId(null); 20 | definitions.setName("Test"); 21 | 22 | final List validationResults = testee.apply(modelInstance); 23 | 24 | assertEquals(1, validationResults.size()); 25 | final ValidationResult validationResult = validationResults.get(0); 26 | assertAll( 27 | () -> assertEquals("A definitions has no id.", validationResult.getMessage()), 28 | () -> assertEquals(definitions, validationResult.getElement()), 29 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 30 | } 31 | 32 | @Test 33 | void shouldWarnIfDefinitionsHasNoName() { 34 | definitions.setId("1"); 35 | definitions.setName(null); 36 | 37 | final List validationResults = testee.apply(modelInstance); 38 | 39 | assertEquals(1, validationResults.size()); 40 | final ValidationResult validationResult = validationResults.get(0); 41 | assertAll( 42 | () -> assertEquals("A definitions has no name.", validationResult.getMessage()), 43 | () -> assertEquals(definitions, validationResult.getElement()), 44 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 45 | } 46 | 47 | @Test 48 | void shouldAcceptDefinitionsWithIdAndName() { 49 | definitions.setId("1"); 50 | definitions.setName("Test"); 51 | 52 | final List validationResults = testee.apply(modelInstance); 53 | 54 | assertTrue(validationResults.isEmpty()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/DuplicateColumnLabelValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.instance.Input; 10 | import org.camunda.bpm.model.dmn.instance.Output; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class DuplicateColumnLabelValidatorTest extends WithDecisionTable { 14 | 15 | private final DuplicateColumnLabelValidator testee = new DuplicateColumnLabelValidator(); 16 | 17 | @Test 18 | void outputsWithDistinctLabelsAreAllowed() { 19 | final Output output1 = modelInstance.newInstance(Output.class); 20 | output1.setLabel("Label1"); 21 | decisionTable.getOutputs().add(output1); 22 | 23 | final Output output2 = modelInstance.newInstance(Output.class); 24 | output2.setLabel("Label2"); 25 | decisionTable.getOutputs().add(output2); 26 | 27 | final List validationResults = testee.apply(modelInstance); 28 | 29 | assertEquals(0, validationResults.size()); 30 | } 31 | 32 | @Test 33 | void outputsWithIdenticalLabelsProduceWarnings() { 34 | final Output output1 = modelInstance.newInstance(Output.class); 35 | output1.setLabel("Label"); 36 | decisionTable.getOutputs().add(output1); 37 | 38 | final Output output2 = modelInstance.newInstance(Output.class); 39 | output2.setLabel("Label"); 40 | decisionTable.getOutputs().add(output2); 41 | 42 | final List validationResults = testee.apply(modelInstance); 43 | 44 | assertEquals(1, validationResults.size()); 45 | final ValidationResult validationResult = validationResults.get(0); 46 | assertAll( 47 | () -> assertEquals("Column with label 'Label' is used more than once", validationResult.getMessage()), 48 | () -> assertEquals(decisionTable, validationResult.getElement()), 49 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 50 | } 51 | 52 | @Test 53 | void inputsWithDistinctLabelsAreAllowed() { 54 | final Input input1 = modelInstance.newInstance(Input.class); 55 | input1.setLabel("Label1"); 56 | decisionTable.getInputs().add(input1); 57 | 58 | final Input input2 = modelInstance.newInstance(Input.class); 59 | input2.setLabel("Label2"); 60 | decisionTable.getInputs().add(input2); 61 | 62 | final List validationResults = testee.apply(modelInstance); 63 | 64 | assertEquals(0, validationResults.size()); 65 | } 66 | 67 | @Test 68 | void inputsWithIdenticalLabelsProduceWarnings() { 69 | final Input input1 = modelInstance.newInstance(Input.class); 70 | input1.setLabel("Label"); 71 | decisionTable.getInputs().add(input1); 72 | 73 | final Input input2 = modelInstance.newInstance(Input.class); 74 | input2.setLabel("Label"); 75 | decisionTable.getInputs().add(input2); 76 | 77 | final List validationResults = testee.apply(modelInstance); 78 | 79 | assertEquals(1, validationResults.size()); 80 | final ValidationResult validationResult = validationResults.get(0); 81 | assertAll( 82 | () -> assertEquals("Column with label 'Label' is used more than once", validationResult.getMessage()), 83 | () -> assertEquals(decisionTable, validationResult.getElement()), 84 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/InputDataIdAndNameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import de.redsix.dmncheck.result.Severity; 8 | import de.redsix.dmncheck.result.ValidationResult; 9 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 10 | import java.util.List; 11 | import org.camunda.bpm.model.dmn.instance.InputData; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class InputDataIdAndNameValidatorTest extends WithDecisionTable { 15 | 16 | private final InputDataIdAndNameValidator testee = new InputDataIdAndNameValidator(); 17 | 18 | @Test 19 | void shouldErrorIfInputDataHasNoId() { 20 | final InputData inputData = modelInstance.newInstance(InputData.class); 21 | definitions.addChildElement(inputData); 22 | 23 | // We access the just created InputData node to ensure it is loaded as InputDataImpl instead of 24 | // InputDataReferenceImpl. This issue 25 | // seems to be related to CAM-8888 and CAM-8889. However, this issue only occurs when creating a DMN model 26 | // programmatically using 27 | // the parser of camunda-dmn-model everything is fine. 28 | modelInstance.getModelElementsByType(InputData.class); 29 | 30 | inputData.setId(null); 31 | inputData.setName("Test"); 32 | 33 | final List validationResults = testee.apply(modelInstance); 34 | 35 | assertEquals(1, validationResults.size()); 36 | final ValidationResult validationResult = validationResults.get(0); 37 | assertAll( 38 | () -> assertEquals("A input has no id.", validationResult.getMessage()), 39 | () -> assertEquals(inputData, validationResult.getElement()), 40 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 41 | } 42 | 43 | @Test 44 | void shouldWarnIfInputDataHasNoName() { 45 | final InputData inputData = modelInstance.newInstance(InputData.class); 46 | definitions.addChildElement(inputData); 47 | 48 | // We access the just created InputData node to ensure it is loaded as InputDataImpl instead of 49 | // InputDataReferenceImpl. This issue 50 | // seems to be related to CAM-8888 and CAM-8889. However, this issue only occurs when creating a DMN model 51 | // programmatically using 52 | // the parser of camunda-dmn-model everything is fine. 53 | modelInstance.getModelElementsByType(InputData.class); 54 | 55 | inputData.setId("1"); 56 | inputData.setName(null); 57 | 58 | final List validationResults = testee.apply(modelInstance); 59 | 60 | assertEquals(1, validationResults.size()); 61 | final ValidationResult validationResult = validationResults.get(0); 62 | assertAll( 63 | () -> assertEquals("A input has no name.", validationResult.getMessage()), 64 | () -> assertEquals(inputData, validationResult.getElement()), 65 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 66 | } 67 | 68 | @Test 69 | void shouldAllowInputDataWithIdAndname() { 70 | final InputData inputData = modelInstance.newInstance(InputData.class); 71 | definitions.addChildElement(inputData); 72 | 73 | // We access the just created InputData node to ensure it is loaded as InputDataImpl instead of 74 | // InputDataReferenceImpl. This issue 75 | // seems to be related to CAM-8888 and CAM-8889. However, this issue only occurs when creating a DMN model 76 | // programmatically using 77 | // the parser of camunda-dmn-model everything is fine. 78 | modelInstance.getModelElementsByType(InputData.class); 79 | 80 | inputData.setId("1"); 81 | inputData.setName("Test"); 82 | 83 | final List validationResults = testee.apply(modelInstance); 84 | 85 | assertTrue(validationResults.isEmpty()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/InputTypeDeclarationValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import de.redsix.dmncheck.result.Severity; 8 | import de.redsix.dmncheck.result.ValidationResult; 9 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 10 | import java.util.List; 11 | import org.camunda.bpm.model.dmn.instance.Input; 12 | import org.camunda.bpm.model.dmn.instance.InputExpression; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class InputTypeDeclarationValidatorTest extends WithDecisionTable { 16 | 17 | private final InputTypeDeclarationValidator testee = new InputTypeDeclarationValidator(); 18 | 19 | @Test 20 | void shouldDetectThatOutputHasNoType() { 21 | final Input input = modelInstance.newInstance(Input.class); 22 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 23 | input.setInputExpression(inputExpression); 24 | decisionTable.getInputs().add(input); 25 | 26 | final List validationResults = testee.apply(modelInstance); 27 | 28 | assertEquals(1, validationResults.size()); 29 | final ValidationResult validationResult = validationResults.get(0); 30 | assertAll( 31 | () -> assertEquals("InputExpression has no type", validationResult.getMessage()), 32 | () -> assertEquals(inputExpression, validationResult.getElement()), 33 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 34 | } 35 | 36 | @Test 37 | void shouldDetectThatOutputHasUnsupportedType() { 38 | final Input input = modelInstance.newInstance(Input.class); 39 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 40 | input.setInputExpression(inputExpression); 41 | inputExpression.setTypeRef("unsupportedType"); 42 | decisionTable.getInputs().add(input); 43 | 44 | final List validationResults = testee.apply(modelInstance); 45 | 46 | assertEquals(1, validationResults.size()); 47 | final ValidationResult validationResult = validationResults.get(0); 48 | assertAll( 49 | () -> assertEquals( 50 | "Could not parse FEEL expression type 'unsupportedType'", validationResult.getMessage()), 51 | () -> assertEquals(inputExpression, validationResult.getElement()), 52 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 53 | } 54 | 55 | @Test 56 | void shouldDetectThatOutputUsesInternalTypeTOP() { 57 | final Input input = modelInstance.newInstance(Input.class); 58 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 59 | input.setInputExpression(inputExpression); 60 | inputExpression.setTypeRef(" "); 61 | decisionTable.getInputs().add(input); 62 | 63 | final List validationResults = testee.apply(modelInstance); 64 | 65 | assertEquals(1, validationResults.size()); 66 | final ValidationResult validationResult = validationResults.get(0); 67 | assertAll( 68 | () -> assertEquals( 69 | "TOP is an internal type and cannot be used in declarations.", validationResult.getMessage()), 70 | () -> assertEquals(inputExpression, validationResult.getElement()), 71 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 72 | } 73 | 74 | @Test 75 | void shouldAllowOutputWithSupportedType() { 76 | final Input input = modelInstance.newInstance(Input.class); 77 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 78 | input.setInputExpression(inputExpression); 79 | inputExpression.setTypeRef("integer"); 80 | decisionTable.getInputs().add(input); 81 | 82 | final List validationResults = testee.apply(modelInstance); 83 | 84 | assertTrue(validationResults.isEmpty()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/InputValuesTypeValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.instance.Input; 10 | import org.camunda.bpm.model.dmn.instance.InputExpression; 11 | import org.camunda.bpm.model.dmn.instance.InputValues; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class InputValuesTypeValidatorTest extends WithDecisionTable { 15 | 16 | private final InputValuesTypeValidator testee = new InputValuesTypeValidator(); 17 | 18 | @Test 19 | void shouldAcceptInputWithoutInputValues() { 20 | final Input input = modelInstance.newInstance(Input.class); 21 | 22 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 23 | input.setInputExpression(inputExpression); 24 | inputExpression.setTypeRef("string"); 25 | 26 | decisionTable.getInputs().add(input); 27 | 28 | final List validationResults = testee.apply(modelInstance); 29 | 30 | assertEquals(0, validationResults.size()); 31 | } 32 | 33 | @Test 34 | void shouldAcceptInputValuesWithCorrectType() { 35 | final Input input = modelInstance.newInstance(Input.class); 36 | final InputValues inputValues = modelInstance.newInstance(InputValues.class); 37 | inputValues.setTextContent("\"foo\",\"bar\""); 38 | input.setInputValues(inputValues); 39 | 40 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 41 | input.setInputExpression(inputExpression); 42 | inputExpression.setTypeRef("string"); 43 | 44 | decisionTable.getInputs().add(input); 45 | 46 | final List validationResults = testee.apply(modelInstance); 47 | 48 | assertEquals(0, validationResults.size()); 49 | } 50 | 51 | @Test 52 | void shouldDetectThatInputValuesHaveWrongType() { 53 | final Input input = modelInstance.newInstance(Input.class); 54 | final InputValues inputValues = modelInstance.newInstance(InputValues.class); 55 | inputValues.setTextContent("1,2,3"); 56 | input.setInputValues(inputValues); 57 | 58 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 59 | input.setInputExpression(inputExpression); 60 | inputExpression.setTypeRef("string"); 61 | 62 | decisionTable.getInputs().add(input); 63 | 64 | final List validationResults = testee.apply(modelInstance); 65 | 66 | assertEquals(1, validationResults.size()); 67 | final ValidationResult validationResult = validationResults.get(0); 68 | assertAll( 69 | () -> assertEquals( 70 | "Type of predefined input values does not match type of input expression", 71 | validationResult.getMessage()), 72 | () -> assertEquals(input, validationResult.getElement()), 73 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 74 | } 75 | 76 | @Test 77 | void warnsIfAnOtherExpressionLanguageThanFeelIsUsed() { 78 | final Input input = modelInstance.newInstance(Input.class); 79 | final InputValues inputValues = modelInstance.newInstance(InputValues.class); 80 | inputValues.setTextContent("'foo'.repeat(6)"); 81 | inputValues.setExpressionLanguage("javascript"); 82 | input.setInputValues(inputValues); 83 | 84 | final InputExpression inputExpression = modelInstance.newInstance(InputExpression.class); 85 | input.setInputExpression(inputExpression); 86 | inputExpression.setTypeRef("string"); 87 | 88 | decisionTable.getInputs().add(input); 89 | 90 | final List validationResults = testee.apply(modelInstance); 91 | 92 | assertEquals(1, validationResults.size()); 93 | final ValidationResult validationResult = validationResults.get(0); 94 | assertAll( 95 | () -> assertEquals("Expression language 'javascript' not supported", validationResult.getMessage()), 96 | () -> assertEquals(input, validationResult.getElement()), 97 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/ItemDefinitionIdAndNameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithItemDefinition; 8 | import java.util.List; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class ItemDefinitionIdAndNameValidatorTest extends WithItemDefinition { 12 | 13 | private final ItemDefinitionIdAndNameValidator testee = new ItemDefinitionIdAndNameValidator(); 14 | 15 | @Test 16 | void shouldErrorIfDecisionHasNoId() { 17 | itemDefinition.setId(null); 18 | itemDefinition.setName("Test"); 19 | 20 | final List validationResults = testee.apply(modelInstance); 21 | 22 | assertEquals(1, validationResults.size()); 23 | final ValidationResult validationResult = validationResults.get(0); 24 | assertAll( 25 | () -> assertEquals("A item definition has no id.", validationResult.getMessage()), 26 | () -> assertEquals(itemDefinition, validationResult.getElement()), 27 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 28 | } 29 | 30 | @Test 31 | void shouldWarnIfDecisionHasNoName() { 32 | itemDefinition.setId("1"); 33 | itemDefinition.setName(null); 34 | 35 | final List validationResults = testee.apply(modelInstance); 36 | 37 | assertEquals(1, validationResults.size()); 38 | final ValidationResult validationResult = validationResults.get(0); 39 | assertAll( 40 | () -> assertEquals("A item definition has no name.", validationResult.getMessage()), 41 | () -> assertEquals(itemDefinition, validationResult.getElement()), 42 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 43 | } 44 | 45 | @Test 46 | void shouldAllowItemDefinitionWithIdAndName() { 47 | itemDefinition.setId("1"); 48 | itemDefinition.setName("Test"); 49 | 50 | final List validationResults = testee.apply(modelInstance); 51 | 52 | assertTrue(validationResults.isEmpty()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/KnowledgeSourceIdAndNameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.instance.KnowledgeSource; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class KnowledgeSourceIdAndNameValidatorTest extends WithDecisionTable { 14 | 15 | private final KnowledgeSourceIdAndNameValidator testee = new KnowledgeSourceIdAndNameValidator(); 16 | 17 | private KnowledgeSource knowledgeSource; 18 | 19 | @BeforeEach 20 | void addKnowledgeSource() { 21 | knowledgeSource = modelInstance.newInstance(KnowledgeSource.class); 22 | definitions.addChildElement(knowledgeSource); 23 | } 24 | 25 | @Test 26 | void shouldErrorIfKnowledgeSourceHasNoId() { 27 | knowledgeSource.setId(null); 28 | knowledgeSource.setName("Test"); 29 | 30 | final List validationResults = testee.apply(modelInstance); 31 | 32 | assertEquals(1, validationResults.size()); 33 | final ValidationResult validationResult = validationResults.get(0); 34 | assertAll( 35 | () -> assertEquals("A knowledge source has no id.", validationResult.getMessage()), 36 | () -> assertEquals(knowledgeSource, validationResult.getElement()), 37 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 38 | } 39 | 40 | @Test 41 | void shouldWarnIfKnowledgeSourceHasNoName() { 42 | knowledgeSource.setId("1"); 43 | knowledgeSource.setName(null); 44 | 45 | final List validationResults = testee.apply(modelInstance); 46 | 47 | assertEquals(1, validationResults.size()); 48 | final ValidationResult validationResult = validationResults.get(0); 49 | assertAll( 50 | () -> assertEquals("A knowledge source has no name.", validationResult.getMessage()), 51 | () -> assertEquals(knowledgeSource, validationResult.getElement()), 52 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 53 | } 54 | 55 | @Test 56 | void shouldAllowKnowledgeSourceWithIdAndName() { 57 | knowledgeSource.setId("1"); 58 | knowledgeSource.setName("Test"); 59 | 60 | final List validationResults = testee.apply(modelInstance); 61 | 62 | assertTrue(validationResults.isEmpty()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/NoDecisionPresentValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.Dmn; 10 | import org.camunda.bpm.model.dmn.DmnModelInstance; 11 | import org.camunda.bpm.model.dmn.instance.Definitions; 12 | import org.camunda.bpm.model.dmn.instance.KnowledgeSource; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class NoDecisionPresentValidatorTest extends WithDecisionTable { 16 | 17 | private final NoDecisionPresentValidator testee = new NoDecisionPresentValidator(); 18 | 19 | @Test 20 | void shouldDetectThatDefinitionsContainNoDecisions() { 21 | final DmnModelInstance dmnModelInstance = Dmn.createEmptyModel(); 22 | 23 | final Definitions definitionsWithOnlyOneKnowledgeSource = dmnModelInstance.newInstance(Definitions.class); 24 | dmnModelInstance.setDefinitions(definitionsWithOnlyOneKnowledgeSource); 25 | 26 | final KnowledgeSource knowledgeSource = dmnModelInstance.newInstance(KnowledgeSource.class); 27 | definitionsWithOnlyOneKnowledgeSource.addChildElement(knowledgeSource); 28 | 29 | final List validationResults = testee.apply(dmnModelInstance); 30 | 31 | assertEquals(1, validationResults.size()); 32 | 33 | final ValidationResult validationResult = validationResults.get(0); 34 | assertAll( 35 | () -> assertEquals("No decisions found", validationResult.getMessage()), 36 | () -> assertEquals(definitionsWithOnlyOneKnowledgeSource, validationResult.getElement()), 37 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 38 | } 39 | 40 | @Test 41 | void shouldAcceptModelWithOneDecision() { 42 | // Decision is defined in super class WithDecisionTable 43 | 44 | final List validationResults = testee.apply(modelInstance); 45 | 46 | assertEquals(0, validationResults.size()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/OutputTypeDeclarationValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import de.redsix.dmncheck.result.Severity; 8 | import de.redsix.dmncheck.result.ValidationResult; 9 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 10 | import java.util.List; 11 | import org.camunda.bpm.model.dmn.instance.Output; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class OutputTypeDeclarationValidatorTest extends WithDecisionTable { 15 | 16 | private final OutputTypeDeclarationValidator testee = new OutputTypeDeclarationValidator(); 17 | 18 | @Test 19 | void shouldDetectThatOutputHasNoType() { 20 | final Output output = modelInstance.newInstance(Output.class); 21 | decisionTable.getOutputs().add(output); 22 | 23 | final List validationResults = testee.apply(modelInstance); 24 | 25 | assertEquals(1, validationResults.size()); 26 | final ValidationResult validationResult = validationResults.get(0); 27 | assertAll( 28 | () -> assertEquals("Output has no type", validationResult.getMessage()), 29 | () -> assertEquals(output, validationResult.getElement()), 30 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 31 | } 32 | 33 | @Test 34 | void shouldDetectThatOutputHasUnsupportedType() { 35 | final Output output = modelInstance.newInstance(Output.class); 36 | output.setTypeRef("unsupportedType"); 37 | decisionTable.getOutputs().add(output); 38 | 39 | final List validationResults = testee.apply(modelInstance); 40 | 41 | assertEquals(1, validationResults.size()); 42 | final ValidationResult validationResult = validationResults.get(0); 43 | assertAll( 44 | () -> assertEquals( 45 | "Could not parse FEEL expression type 'unsupportedType'", validationResult.getMessage()), 46 | () -> assertEquals(output, validationResult.getElement()), 47 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 48 | } 49 | 50 | @Test 51 | void shouldAllowOutputWithSupportedType() { 52 | final Output output = modelInstance.newInstance(Output.class); 53 | output.setTypeRef("integer"); 54 | decisionTable.getOutputs().add(output); 55 | 56 | final List validationResults = testee.apply(modelInstance); 57 | 58 | assertTrue(validationResults.isEmpty()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/OutputValuesTypeValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import de.redsix.dmncheck.result.Severity; 6 | import de.redsix.dmncheck.result.ValidationResult; 7 | import de.redsix.dmncheck.validators.util.WithDecisionTable; 8 | import java.util.List; 9 | import org.camunda.bpm.model.dmn.instance.Output; 10 | import org.camunda.bpm.model.dmn.instance.OutputValues; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class OutputValuesTypeValidatorTest extends WithDecisionTable { 14 | 15 | private final OutputValuesTypeValidator testee = new OutputValuesTypeValidator(); 16 | 17 | @Test 18 | void shouldAcceptOutputWithoutOutputValues() { 19 | final Output output = modelInstance.newInstance(Output.class); 20 | output.setTypeRef("string"); 21 | 22 | decisionTable.getOutputs().add(output); 23 | 24 | final List validationResults = testee.apply(modelInstance); 25 | 26 | assertEquals(0, validationResults.size()); 27 | } 28 | 29 | @Test 30 | void shouldAcceptOutputValuesWithCorrectType() { 31 | final Output output = modelInstance.newInstance(Output.class); 32 | final OutputValues OutputValues = modelInstance.newInstance(OutputValues.class); 33 | OutputValues.setTextContent("\"foo\",\"bar\""); 34 | output.setOutputValues(OutputValues); 35 | output.setTypeRef("string"); 36 | 37 | decisionTable.getOutputs().add(output); 38 | 39 | final List validationResults = testee.apply(modelInstance); 40 | 41 | assertEquals(0, validationResults.size()); 42 | } 43 | 44 | @Test 45 | void shouldDetectThatOutputValuesHaveWrongType() { 46 | final Output output = modelInstance.newInstance(Output.class); 47 | final OutputValues OutputValues = modelInstance.newInstance(OutputValues.class); 48 | OutputValues.setTextContent("1,2,3"); 49 | output.setOutputValues(OutputValues); 50 | output.setTypeRef("string"); 51 | 52 | decisionTable.getOutputs().add(output); 53 | 54 | final List validationResults = testee.apply(modelInstance); 55 | 56 | assertEquals(1, validationResults.size()); 57 | final ValidationResult validationResult = validationResults.get(0); 58 | assertAll( 59 | () -> assertEquals( 60 | "Type of predefined output values does not match type of output expression", 61 | validationResult.getMessage()), 62 | () -> assertEquals(output, validationResult.getElement()), 63 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 64 | } 65 | 66 | @Test 67 | void warnsIfAnOtherExpressionLanguageThanFeelIsUsed() { 68 | final Output output = modelInstance.newInstance(Output.class); 69 | final OutputValues outputValues = modelInstance.newInstance(OutputValues.class); 70 | outputValues.setTextContent("'foo'.repeat(6)"); 71 | outputValues.setExpressionLanguage("javascript"); 72 | output.setOutputValues(outputValues); 73 | output.setTypeRef("string"); 74 | 75 | decisionTable.getOutputs().add(output); 76 | 77 | final List validationResults = testee.apply(modelInstance); 78 | 79 | assertEquals(1, validationResults.size()); 80 | final ValidationResult validationResult = validationResults.get(0); 81 | assertAll( 82 | () -> assertEquals("Expression language 'javascript' not supported", validationResult.getMessage()), 83 | () -> assertEquals(output, validationResult.getElement()), 84 | () -> assertEquals(Severity.WARNING, validationResult.getSeverity())); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/RequirementGraphLeafValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import de.redsix.dmncheck.result.Severity; 7 | import de.redsix.dmncheck.result.ValidationResult; 8 | import de.redsix.dmncheck.validators.util.WithRequirementGraph; 9 | import java.util.List; 10 | import org.camunda.bpm.model.dmn.instance.Decision; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class RequirementGraphLeafValidatorTest extends WithRequirementGraph { 14 | 15 | private final RequirementGraphLeafValidator testee = new RequirementGraphLeafValidator(); 16 | 17 | @Test 18 | void shouldAcceptEmptyGraph() { 19 | final List validationResults = testee.apply(modelInstance); 20 | 21 | assertEquals(0, validationResults.size()); 22 | } 23 | 24 | @Test 25 | void shouldAcceptGraphWithSingleDecision() { 26 | final Decision decision = modelInstance.newInstance(Decision.class); 27 | definitions.addChildElement(decision); 28 | 29 | final List validationResults = testee.apply(modelInstance); 30 | 31 | assertEquals(0, validationResults.size()); 32 | } 33 | 34 | @Test 35 | void shouldAcceptGraphWithTwoConnectedDecisions() { 36 | final Decision decision1 = modelInstance.newInstance(Decision.class); 37 | definitions.addChildElement(decision1); 38 | 39 | final Decision decision2 = modelInstance.newInstance(Decision.class); 40 | definitions.addChildElement(decision2); 41 | 42 | connect(decision1, decision2); 43 | 44 | final List validationResults = testee.apply(modelInstance); 45 | 46 | assertEquals(0, validationResults.size()); 47 | } 48 | 49 | @Test 50 | void shouldAcceptGraphWithThreeConnectedDecisionsWithOneLeaf() { 51 | final Decision decision1 = modelInstance.newInstance(Decision.class); 52 | definitions.addChildElement(decision1); 53 | 54 | final Decision decision2 = modelInstance.newInstance(Decision.class); 55 | definitions.addChildElement(decision2); 56 | 57 | final Decision decision3 = modelInstance.newInstance(Decision.class); 58 | definitions.addChildElement(decision3); 59 | 60 | connect(decision1, decision3); 61 | connect(decision2, decision3); 62 | 63 | final List validationResults = testee.apply(modelInstance); 64 | 65 | assertEquals(0, validationResults.size()); 66 | } 67 | 68 | @Test 69 | void shouldRejectGraphWithThreeConnectedDecisionsWithTwoLeafs() { 70 | final Decision decision1 = modelInstance.newInstance(Decision.class); 71 | definitions.addChildElement(decision1); 72 | 73 | final Decision decision2 = modelInstance.newInstance(Decision.class); 74 | definitions.addChildElement(decision2); 75 | 76 | final Decision decision3 = modelInstance.newInstance(Decision.class); 77 | definitions.addChildElement(decision3); 78 | 79 | connect(decision1, decision2); 80 | connect(decision1, decision3); 81 | 82 | final List validationResults = testee.apply(modelInstance); 83 | 84 | assertEquals(1, validationResults.size()); 85 | final ValidationResult validationResult = validationResults.get(0); 86 | assertAll( 87 | () -> assertEquals("Requirement graphs may only contain one leaf node", validationResult.getMessage()), 88 | () -> assertEquals(definitions, validationResult.getElement()), 89 | () -> assertEquals(Severity.ERROR, validationResult.getSeverity())); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/util/TestEnum.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.util; 2 | 3 | public enum TestEnum { 4 | some 5 | } 6 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/util/WithDecisionTable.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.util; 2 | 3 | import org.camunda.bpm.model.dmn.instance.Decision; 4 | import org.camunda.bpm.model.dmn.instance.DecisionTable; 5 | import org.junit.jupiter.api.BeforeEach; 6 | 7 | public class WithDecisionTable extends WithDefinitions { 8 | protected Decision decision; 9 | protected DecisionTable decisionTable; 10 | 11 | @BeforeEach 12 | public void prepareDecisionTable() { 13 | decision = modelInstance.newInstance(Decision.class); 14 | definitions.addChildElement(decision); 15 | 16 | decisionTable = modelInstance.newInstance(DecisionTable.class); 17 | decision.addChildElement(decisionTable); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/util/WithDefinitions.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.util; 2 | 3 | import org.camunda.bpm.model.dmn.Dmn; 4 | import org.camunda.bpm.model.dmn.DmnModelInstance; 5 | import org.camunda.bpm.model.dmn.instance.Definitions; 6 | import org.junit.jupiter.api.BeforeEach; 7 | 8 | public class WithDefinitions { 9 | 10 | protected DmnModelInstance modelInstance; 11 | protected Definitions definitions; 12 | 13 | @BeforeEach 14 | public void prepareDefinitions() { 15 | modelInstance = Dmn.createEmptyModel(); 16 | 17 | definitions = modelInstance.newInstance(Definitions.class); 18 | modelInstance.setDefinitions(definitions); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/util/WithItemDefinition.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.util; 2 | 3 | import org.camunda.bpm.model.dmn.instance.ItemDefinition; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | public class WithItemDefinition extends WithDefinitions { 7 | 8 | protected ItemDefinition itemDefinition; 9 | 10 | @BeforeEach 11 | public void prepareItemDefinitions() { 12 | itemDefinition = modelInstance.newInstance(ItemDefinition.class); 13 | definitions.addChildElement(itemDefinition); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /validators/src/test/java/de/redsix/dmncheck/validators/util/WithRequirementGraph.java: -------------------------------------------------------------------------------- 1 | package de.redsix.dmncheck.validators.util; 2 | 3 | import org.camunda.bpm.model.dmn.Dmn; 4 | import org.camunda.bpm.model.dmn.DmnModelInstance; 5 | import org.camunda.bpm.model.dmn.instance.Decision; 6 | import org.camunda.bpm.model.dmn.instance.Definitions; 7 | import org.camunda.bpm.model.dmn.instance.InformationRequirement; 8 | import org.junit.jupiter.api.BeforeEach; 9 | 10 | public class WithRequirementGraph { 11 | protected DmnModelInstance modelInstance; 12 | protected Definitions definitions; 13 | 14 | @BeforeEach 15 | void prepareRequirementGraph() { 16 | modelInstance = Dmn.createEmptyModel(); 17 | 18 | definitions = modelInstance.newInstance(Definitions.class); 19 | modelInstance.setDefinitions(definitions); 20 | } 21 | 22 | protected void connect(Decision decision1, Decision decision2) { 23 | final InformationRequirement informationRequirement = modelInstance.newInstance(InformationRequirement.class); 24 | informationRequirement.setRequiredDecision(decision1); 25 | decision2.addChildElement(informationRequirement); 26 | } 27 | } 28 | --------------------------------------------------------------------------------