diff --git a/.circleci/bump_version.sh b/.circleci/bump_version.sh deleted file mode 100644 index 54f1b3799..000000000 --- a/.circleci/bump_version.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -if [[ "$1" == "" ]]; then - echo "Error: no version given" - exit 1 -else - DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - cd "$DIR/.." - mvn org.codehaus.mojo:versions-maven-plugin:2.7:set -DnewVersion=$1 -DgenerateBackupPoms=false -fi - diff --git a/.circleci/commit-msg b/.circleci/commit-msg deleted file mode 100755 index 0ddb2f7fa..000000000 --- a/.circleci/commit-msg +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash - -# THIS IS A GIT (PRE COMMIT) HOOK - -# What is it good for? -# It will prefix a commit messages w/ [skip ci] string if changes -# to be commited happend in blacklisted files only to avoid triggering -# a CI build on CircleCI. Blacklisted files are defined in the file -# .circleciignore in project's root folder. Folder name or patterns like -# logs/* can be used for blacklisting as well. - -# How to use it? -# Copy this file into .git/hooks folder -# And make it executable (chmod +x commit-msg) -# That's it. - -# More details -# https://circleci.com/blog/circleci-hacks-automate-the-decision-to-skip-builds-using-a-git-hook/ -# https://gist.github.com/felicianotech/12a4b38c594fcf3d3999de2c01f7d05e - - -if [[ ! -a .circleciignore ]]; then - exit # If .circleciignore doesn't exists, just quit this Git hook -fi - -# Load in every file that will be changed via this commit into an array -changes=( `git diff --name-only --cached` ) - -# Load the patterns we want to skip into an array -mapfile -t blacklist < .circleciignore - -for i in "${blacklist[@]}" -do - # Remove the current pattern from the list of changes - changes=( ${changes[@]/$i/} ) - if [[ ${#changes[@]} -eq 0 ]]; then - # If we've exhausted the list of changes before we've finished going - # through patterns, that's okay, just quit the loop - break - fi -done - -if [[ ${#changes[@]} -gt 0 ]]; then - # If there's still changes left, then we have stuff to build, leave the commit alone. - exit -fi -# Prefix the commit message with "[skip ci]" -commitContent=$(<$1) -echo "[skip ci] ${commitContent}" > $1 \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 379e294cb..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,3010 +0,0 @@ -version: 2.1 - -# Copyright (c) 2019 Wladislaw Wagner (Vitasystems GmbH). -# This file is part of Project EHRbase -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on a, Pablo Pazosn Vitasystems GmbHS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# 88888888888 88 88 88888888ba 88888888ba db ad88888ba 88888888888 -# 88 88 88 88 "8b 88 "8b d88b d8" "8b 88 -# 88 88 88 88 ,8P 88 ,8P d8'`8b Y8, 88 -# 88aaaaa 88aaaaaaaa88 88aaaaaa8P' 88aaaaaa8P' d8' `8b `Y8aaaaa, 88aaaaa -# 88""""" 88""""""""88 88""""88' 88""""""8b, d8YaaaaY8b `"""""8b, 88""""" -# 88 88 88 88 `8b 88 `8b d8""""""""8b `8b 88 -# 88 88 88 88 `8b 88 a8P d8' `8b Y8a a8P 88 -# 88888888888 88 88 88 `8b 88888888P" d8' `8b "Y88888P" 88888888888 - - - -workflows: - version: 2 - - # WORKFLOW 1/3 - build-and-test: - jobs: - - check-codestyle: - context: org-global - filters: - branches: - ignore: - - /release\/.*/ - - master - - /sync\/.*/ - - /feature/sync\/.*/ - - build-ehrbase: - context: org-global - filters: - branches: - ignore: - - /release\/.*/ - - master - - /sync\/.*/ - - /feature/sync\/.*/ - - - run-SDK-integration-tests: - context: org-global - requires: - - build-ehrbase - - - SANITY-tests: - context: org-global - requires: - - build-ehrbase - - - COMPOSITION-tests-1: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - # post-steps: - # - provide-test-status-report-via-slack # - - - COMPOSITION-tests-2: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-3: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-4: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-5: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-6: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-7: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-8: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-9: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-10: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-11: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-12: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-13: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-14: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - COMPOSITION-tests-15: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - TEMPLATE-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - HEADER-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - EHRScape-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - TERMINOLOGY-MOCK-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - CONTRIBUTION-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - DIRECTORY-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - EHRSERVICE-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - EHRSTATUS-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - KNOWLEDGE-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - STOREDQUERY-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - QUERYSERVICE-test-1: - context: org-global - requires: - - build-ehrbase - - # - QUERYSERVICE-smoke: - # context: org-global - # # requires: - # # - build-ehrbase - - - QUERYSERVICE-test-2: - context: org-global - requires: - - build-ehrbase - - - NEWQUERY-test-set: - context: org-global - requires: - - build-ehrbase - - - SECURITY-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - CORS-test: - context: org-global - requires: - - build-ehrbase - - - ADMIN-test: - context: org-global - requires: - - build-ehrbase - - - ROBOT-TEST-REPORT: - context: org-global - requires: - - SANITY-tests - - COMPOSITION-tests-1 - - COMPOSITION-tests-2 - - COMPOSITION-tests-3 - - COMPOSITION-tests-4 - - COMPOSITION-tests-5 - - COMPOSITION-tests-6 - - COMPOSITION-tests-7 - - COMPOSITION-tests-8 - - COMPOSITION-tests-9 - - COMPOSITION-tests-10 - - COMPOSITION-tests-11 - - COMPOSITION-tests-12 - - COMPOSITION-tests-13 - - COMPOSITION-tests-14 - - COMPOSITION-tests-15 - - TEMPLATE-tests - - HEADER-tests - - EHRScape-tests - - TERMINOLOGY-MOCK-tests - - CONTRIBUTION-test - - DIRECTORY-test - - EHRSERVICE-test - - EHRSTATUS-test - - KNOWLEDGE-test - - STOREDQUERY-test - - QUERYSERVICE-test-1 - - QUERYSERVICE-test-2 - - NEWQUERY-test-set - - SECURITY-test - - CORS-test - - ADMIN-test - - - sonar-analysis: - context: org-global - requires: - - run-SDK-integration-tests - # TODO: reactivate this after https://github.com/ehrbase/ehrbase/issues/330 - # resolved - # - COMPOSITION-tests-1 - # - COMPOSITION-tests-2 - # - COMPOSITION-tests-3 - # - COMPOSITION-tests-4 - # - CONTRIBUTION-test - # - DIRECTORY-test - # - EHRSERVICE-test - # - EHRSTATUS-test - # - KNOWLEDGE-test - # - QUERYSERVICE-test-1 - # - QUERYSERVICE-test-2 - # - SECURITY-test - - - - - - # WORKFLOW 2/3 - release: - jobs: - - build-ehrbase: - context: org-global - filters: - branches: - only: - - /^(release\/v\d+\.\d+\.\d+|master)$/ - - - run-SDK-integration-tests: - context: org-global - requires: - - build-ehrbase - - - SANITY-tests: - context: org-global - requires: - - build-ehrbase - - - COMPOSITION-tests-1: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - # post-steps: - # - provide-test-status-report-via-slack # - - - COMPOSITION-tests-2: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-3: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-4: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-5: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-6: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-7: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-8: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-9: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-10: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-11: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-12: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-13: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-14: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - COMPOSITION-tests-15: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - TEMPLATE-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - HEADER-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - EHRScape-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - TERMINOLOGY-MOCK-tests: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - CONTRIBUTION-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - DIRECTORY-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - EHRSERVICE-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - EHRSTATUS-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - KNOWLEDGE-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - STOREDQUERY-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-2 - - - QUERYSERVICE-test-1: - context: org-global - requires: - - build-ehrbase - - - QUERYSERVICE-test-2: - context: org-global - requires: - - build-ehrbase - - - NEWQUERY-test-set: - context: org-global - requires: - - build-ehrbase - - - SECURITY-test: - context: org-global - requires: - - build-ehrbase - - - CORS-test: - context: org-global - requires: - - build-ehrbase - - - ADMIN-test: - context: org-global - requires: - - build-ehrbase - - - ROBOT-TEST-REPORT: - context: org-global - requires: - - SANITY-tests - - COMPOSITION-tests-1 - - COMPOSITION-tests-2 - - COMPOSITION-tests-3 - - COMPOSITION-tests-4 - - COMPOSITION-tests-5 - - COMPOSITION-tests-6 - - COMPOSITION-tests-7 - - COMPOSITION-tests-8 - - COMPOSITION-tests-9 - - COMPOSITION-tests-10 - - COMPOSITION-tests-11 - - COMPOSITION-tests-12 - - COMPOSITION-tests-13 - - COMPOSITION-tests-14 - - COMPOSITION-tests-15 - - TEMPLATE-tests - - HEADER-tests - - EHRScape-tests - - TERMINOLOGY-MOCK-tests - - CONTRIBUTION-test - - DIRECTORY-test - - EHRSERVICE-test - - EHRSTATUS-test - - KNOWLEDGE-test - - STOREDQUERY-test - - QUERYSERVICE-test-1 - - QUERYSERVICE-test-2 - - NEWQUERY-test-set - - SECURITY-test - - CORS-test - - ADMIN-test - - - sonar-analysis: - context: org-global - requires: - - run-SDK-integration-tests - # TODO: reactivate this after https://github.com/ehrbase/ehrbase/issues/330 - # resolved - # - COMPOSITION-tests-1 - # - COMPOSITION-tests-2 - # - COMPOSITION-tests-3 - # - COMPOSITION-tests-4 - # - CONTRIBUTION-test - # - DIRECTORY-test - # - EHRSERVICE-test - # - EHRSTATUS-test - # - KNOWLEDGE-test - # - QUERYSERVICE-test-1 - # - QUERYSERVICE-test-2 - # - SECURITY-test - - - - - - # WORKFLOW 3/3 - synced-feature-check: - description: | - WHAT THIS WORKFLOW DOES - ======================= - - Build EHRbase and run all openEHR_SDK Java tests (unit and integration tests) - with SDK being checked out from a branch named sync/* or sync/feature/* - - Consider the following scenarios - -------------------------------- - - code change in repo | - EHRBASE SDK | BRANCH | CI ACTION | COMMENT - --------------------|-------------------|-------------------|--------------------------------------------------------------------- - - YES NO feature/* default build ehrbase uses SDK referenced in it's parent pom.xml (commit hash) - NO YES feature/* default build sdk uses EHRBASE (built) from develop branch - - YES YES feature/* SHOULD FAIL default builds triggered on both CIs do not take into account - respective changes in the featue branch of the other repository - NOTE: if the build does NOT fail on ehrbase's and/or sdk's CI - then proper Java unit/integration tests are missing! - - YES NO sync/feature/* SHOULD FAIL ehrbase's CI fails to checkout sync/feature/* branch from sdk repo - NO YES sync/feature/* SHOULD FAIL sdk's CI fails to checkout sync/feature/* branch from ehrbase repo - - YES YES sync/feature/* synced build - ehrbase's CI uses SDK from sync/feature/* branch - - sdk's CI uses EHRBASE from sync/feature/* branch - - explanations - -------------------- - default build (ehrbase) EHRbase is build using SDK version given in it's parent pom.xml - default build (sdk) SDK is build and tested using EHRbase build from develop branch - - synced build both CIs take into account respective changes in sync/feature/* - branch of each repository - - Find detailed steps description for this workflow in following jobs: - - "build package ehrbase with locally built sdk from sync-branch" - - "run java integration tests - sdk" - - HOW TO USE THIS WORKFLOW? - ========================= - - 1. create TWO branches "sync/[issue-id]_name branches" respectively in - - - ehrbase repo --> i.e. sync/123_example-issue - - openehr_sdk repo --> i.e. sync/123_example-issue - - 2. apply and commit your code changes (!!! in both repositories) - 3. push to openehr_sdk repo (sdk's CI will trigger a similar workflow) - 4. push to ehrbase repo (ehrbase's CI will trigger this workflow) - - NOTE: at this point 'synced build' should be running on both CIs - - 5. create TWO PRs (one in ehrbase, one in openehr_sdk) - 6. merge both PRs (WARNING): - - /////////////////////////////////////////////////////////////////////// - /// /// - /// - make sure that both PRs are reviewed and ready to be merged /// - /// at the same time! /// - /// - make sure to sync both PRs w/ develop before merging! /// - /// - open each PR in it's own browser window /// - /// - MERGE BOTH PRs AT THE SAME TIME! /// - /// /// - ////////////////////////////////////////////////////////////////////// - - # when: - # or: - # - equal: [ sync/*, << pipeline.git.branch >> ] - # - equal: [ feature/sync/*, << pipeline.git.branch >> ] - - jobs: - - check-codestyle: - filters: - branches: - only: - - /^sync\/.*/ - - /^feature\/sync\/.*/ - - build package ehrbase with locally built sdk from sync-branch: - filters: - branches: - only: - - /^sync\/.*/ - - /^feature\/sync\/.*/ - - run java integration tests - sdk: - requires: - - build package ehrbase with locally built sdk from sync-branch - - SANITY-tests: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-1: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-2: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-3: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-4: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-5: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-6: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-7: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-8: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-9: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-10: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-11: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-12: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-13: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-14: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - COMPOSITION-tests-15: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - TEMPLATE-tests: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - HEADER-tests: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - EHRScape-tests: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - TERMINOLOGY-MOCK-tests: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - CONTRIBUTION-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - DIRECTORY-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - EHRSERVICE-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - EHRSTATUS-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - KNOWLEDGE-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - STOREDQUERY-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - QUERYSERVICE-test-1: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - QUERYSERVICE-test-2: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - NEWQUERY-test-set: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - SECURITY-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - CORS-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - ADMIN-test: - context: org-global - requires: - - build package ehrbase with locally built sdk from sync-branch - - ROBOT-TEST-REPORT: - context: org-global - requires: - - SANITY-tests - - COMPOSITION-tests-1 - - COMPOSITION-tests-2 - - COMPOSITION-tests-3 - - COMPOSITION-tests-4 - - COMPOSITION-tests-5 - - COMPOSITION-tests-6 - - COMPOSITION-tests-7 - - COMPOSITION-tests-8 - - COMPOSITION-tests-9 - - COMPOSITION-tests-10 - - COMPOSITION-tests-11 - - COMPOSITION-tests-12 - - COMPOSITION-tests-13 - - COMPOSITION-tests-14 - - COMPOSITION-tests-15 - - TEMPLATE-tests - - HEADER-tests - - EHRScape-tests - - TERMINOLOGY-MOCK-tests - - CONTRIBUTION-test - - DIRECTORY-test - - EHRSERVICE-test - - EHRSTATUS-test - - KNOWLEDGE-test - - STOREDQUERY-test - - QUERYSERVICE-test-1 - - QUERYSERVICE-test-2 - - NEWQUERY-test-set - - SECURITY-test - - CORS-test - - ADMIN-test - - - - - -jobs: - # 88 ,ad8888ba, 88888888ba ad88888ba - # 88 d8"' `"8b 88 "8b d8" "8b - # 88 d8' `8b 88 ,8P Y8, - # 88 88 88 88aaaaaa8P' `Y8aaaaa, - # 88 88 88 88""""""8b, `"""""8b, - # 88 Y8, ,8P 88 `8b `8b - # 88, ,d88 Y8a. .a8P 88 a8P Y8a a8P - # "Y8888P" `"Y8888Y"' 88888888P" "Y88888P" - - build package ehrbase with locally built sdk from sync-branch: - executor: docker-py3-postgres - steps: - - checkout - - install-java17 - - install-maven - - git-clone-sdk-repo - - git-checkout-sdk-sync-branch - - cache-out-sdk-m2-dependencies-sync-branch - - maven-install-sdk - - cache-in-sdk-m2-dependencies-sync-branch - - collect-sdk-unittest-results - - save-sdk-test-results - - save-openehr-sdk-repo - - cache-out-ehrbase-m2-dependencies-syncbranch - - build-and-test-ehrbase - - cache-in-ehrbase-m2-dependencies-syncbranch - - save-packaged-ehrbase-jar - - collect-ehrbase-unittest-results - - save-ehrbase-test-results - - - run java integration tests - sdk: - # executor: machine-ubuntu-2004 - executor: docker-py3-postgres - steps: - - checkout - - install-java17 - - install-maven - - restore-workspace - - cache-out-sdk-m2-dependencies-sync-branch - - cache-out-ehrbase-m2-dependencies-syncbranch - - start-ehrbase-and-run-java-integration-sdk-tests - - collect-sdk-integrationtest-results - - save-sdk-test-results - - - check-codestyle: - executor: docker-python3 - # executor: machine-ubuntu-2004 - steps: - - checkout - - install-java17 - - install-maven - - maven-check-codestyle - - - build-ehrbase: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - install-java17 - - install-maven - - restore-build-ehrbase-job-caches - - maven-package - - save-packaged-ehrbase-jar - - save-build-ehrbase-job-caches - - collect-ehrbase-unittest-results - - save-ehrbase-test-results - #- git-clone-robot-integration-tests-repo - # - run: echo MOCKED JOB 1 # USE FOR PIPELINE DEBUGGING ONLY - # - cache-out-ehrbase-workspace # USE FOR PIPELINE DEBUGGING ONLY - # - cache-in-ehrbase-workspace # USE FOR PIPELINE DEBUGGING ONLY - - - run-SDK-integration-tests: - description: Run openEHR_SDK's Java integration tests (w/ EHRbase + DB running). - executor: docker-py3-postgres - steps: - - checkout - - attach-target-folder - - setup-jacoco-distribution - - git-clone-sdk-repo - - cache-out-sdk-m2-dependencies - - start-ehrbase-and-run-all-sdk-tests - - cache-in-sdk-m2-dependencies - - collect-sdk-it-coverage - - generate-sdk-it-coverage-report - - collect-sdk-unittest-results - - collect-sdk-integrationtest-results - - save-sdk-test-results - - SANITY-tests: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "Sanity" - test-suite-path: "SANITY_TESTS" - test-suite-name: "SANITY" - - persist-robot-requirements-file - - COMPOSITION-tests-1: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_create" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_1" - - COMPOSITION-tests-2: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - run: echo MOCKED JOB 3 - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_get" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_2" - - COMPOSITION-tests-3: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_get_versioned" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_3" - - COMPOSITION-tests-4: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_update" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_4" - - COMPOSITION-tests-5: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_delete" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_5" - - COMPOSITION-tests-6: - executor: docker-py3-postgres-ci-timezone-berlin-postgres-timezone-berlin - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_BERLIN_POSTGRES_TIMEZONE_BERLIN" - - COMPOSITION-tests-7: - executor: docker-py3-postgres-ci-timezone-berlin-postgres-timezone-utc - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_BERLIN_POSTGRES_TIMEZONE_UTC" - - COMPOSITION-tests-8: - executor: docker-py3-postgres-ci-timezone-berlin-postgres-timezone-shanghai - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_BERLIN_POSTGRES_TIMEZONE_SHANGHAI" - - COMPOSITION-tests-9: - executor: docker-py3-postgres-ci-timezone-utc-postgres-timezone-berlin - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_UTC_POSTGRES_TIMEZONE_BERLIN" - - COMPOSITION-tests-10: - executor: docker-py3-postgres-ci-timezone-utc-postgres-timezone-utc - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_UTC_POSTGRES_TIMEZONE_UTC" - - COMPOSITION-tests-11: - executor: docker-py3-postgres-ci-timezone-utc-postgres-timezone-shanghai - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_UTC_POSTGRES_TIMEZONE_SHANGHAI" - - COMPOSITION-tests-12: - executor: docker-py3-postgres-ci-timezone-shanghai-postgres-timezone-berlin - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_SHANGHAI_POSTGRES_TIMEZONE_BERLIN" - - COMPOSITION-tests-13: - executor: docker-py3-postgres-ci-timezone-shanghai-postgres-timezone-utc - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_SHANGHAI_POSTGRES_TIMEZONE_UTC" - - COMPOSITION-tests-14: - executor: docker-py3-postgres-ci-timezone-shanghai-postgres-timezone-shanghai - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_SHANGHAI_POSTGRES_TIMEZONE_SHANGHAI" - - COMPOSITION-tests-15: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "compositionANDcomposition_ism_transitions" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_15" - - TEMPLATE-tests: - executor: docker-py3-postgres-ci-timezone-shanghai-postgres-timezone-shanghai - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "Template" - test-suite-path: "TEMPLATE_TESTS" - test-suite-name: "TEMPLATE_TESTS" - - HEADER-tests: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "HeadersChecks" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "HEADER-tests" - - EHRScape-tests: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "EhrScapeTag" - test-suite-path: "EHRSCAPE_TESTS" - test-suite-name: "EHRSCAPE" - - TERMINOLOGY-MOCK-tests: - executor: docker-mock-server - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "MOCKSUITE" - test-suite-path: "MOCK_FHIR_TESTS" - test-suite-name: "TERMINOLOGY_MOCK" - - CONTRIBUTION-test: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "CONTRIBUTION" - test-suite-path: "CONTRIBUTION_TESTS" - test-suite-name: "CONTRIBUTION" - - DIRECTORY-test: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "directory" - test-suite-path: "DIRECTORY_TESTS" - test-suite-name: "DIRECTORY" - - EHRSERVICE-test: - executor: docker-py3-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "EHR_SERVICE" - test-suite-path: "EHR_SERVICE_TESTS" - test-suite-name: "EHR_SERVICE" - - EHRSTATUS-test: - executor: docker-py3-postgres - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "EHR_STATUS" - test-suite-path: "EHR_STATUS_TESTS" - test-suite-name: "EHR_STATUS" - - KNOWLEDGE-test: - executor: docker-py3-postgres - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "OPT" - test-suite-path: "KNOWLEDGE_TESTS" - test-suite-name: "KNOWLEDGE" - allow-template-overwrite: false - cache-enabled: false - - STOREDQUERY-test: - executor: docker-py3-postgres - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "stored_query" - test-suite-path: "STORED_QUERY_TESTS" - test-suite-name: "STORED_QUERY" - allow-template-overwrite: true - - QUERYSERVICE-test-1: - executor: docker-py3-postgres - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "aql_adhoc-queryANDaql_empty_db" - test-suite-path: "QUERY_SERVICE_TESTS" - test-suite-name: "ADHOC-QUERY-1" - - NEWQUERY-test-set: - executor: docker-py3-postgres - steps: - - checkout - - restore-workspace - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - include-tags: "NEW_query_test_set" - test-suite-path: "QUERY_TESTS" - test-suite-name: "NEW_QUERY_TESTS" - - QUERYSERVICE-test-2: - # executor: docker-py3-postgres - executor: machine-ubuntu-2004 - environment: - SUT: TEST - steps: - - checkout - - restore-workspace - - install-java17 - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - # - cache-out-ehrbase-workspace # USE FOR PIPELINE DEBUGGING ONLY - - restore_cache: - keys: - - expected-results-loaded-db-v9 - - run: - name: list files after restore of cache - command: | - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/A/ - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/B/ - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/C/ - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/D/ - - restore_cache: - keys: - - ehrbasedb-dump-v9 - # COMMENT: USE THE NEXT LINE ONLY TO FORCE TEST-DATA REGENERATION! Otherwise comment it out! - - run: echo "FORCE GENERATION OF TEST-DATA AND EXPECTED RESULTS!" > /tmp/DATA_CHANGED_NOTICE - - run: - name: CHECK IF EXPECTED-RESULT TEMPLATES HAVE CHANGED AND REGENERATE TEST-DATA IF NEEDED - command: | - FILE=/tmp/DATA_CHANGED_NOTICE - if [ -f "$FILE" ]; then - echo "REGENERATION OF TEST-DATA AND EXPECTED RESULT SETS IS EITHER REQUIRED OR WAS FORCED." - else - find tests/robot/_resources/test_data_sets/query/expected_results/loaded_db/ -type f ! -name *.tmp.json | sort | xargs cat > /tmp/expected-results-loaded_db-seed - sha256sum /tmp/expected-results-loaded_db-seed - ACTUAL_HASH="$(sha256sum /tmp/expected-results-loaded_db-seed | cat)" - EXPECTED_HASH="f5ee5a9a55c50687dafc3c3acff66089759f1577d7a5dd71aff6e60793ce91c2 /tmp/expected-results-loaded_db-seed" - [ "$ACTUAL_HASH" = "$EXPECTED_HASH" ] && echo "Expected results unchanged! Don't regenerate test-data!" || echo "Expected result data-sets changed. Regenerate!" > /tmp/DATA_CHANGED_NOTICE - fi - - run-robot-tests: - sut: "TEST" - # include-tags: "SMOKE" - # test-suite-path: "QUERY_SERVICE_TESTS" - # test-suite-name: "ADHOC-QUERY-SMOKE" - include-tags: "aql_adhoc-queryANDaql_loaded_db" - test-suite-path: "QUERY_SERVICE_TESTS" - test-suite-name: "ADHOC-QUERY-2" - - save_cache: - key: expected-results-loaded-db-v9-{{ checksum "/tmp/expected-results-loaded_db-seed" }} - paths: - - tests/robot/_resources/test_data_sets/query/expected_results/loaded_db/ - - tests/robot/_resources/test_data_sets/query/aql_queries_valid/ - - run: - name: list files after save of cache - command: | - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/A/ - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/B/ - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/C/ - ls -la tests/robot/_resources/test_data_sets/query/aql_queries_valid/D/ - - save_cache: - key: ehrbasedb-dump-v8-{{ checksum "/tmp/ehrbasedb_dump.sql" }} - paths: - - /tmp/ehrbasedb_dump.sql - - - SECURITY-test: - executor: machine-ubuntu-2004 #docker-mock-server #machine-ubuntu-2004 - environment: - SUT: TEST - SECURITY_AUTHTYPE: OAUTH - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI: "http://localhost:8081/auth/realms/ehrbase" - steps: - - checkout - - restore-workspace - - install-java17 - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - #- download-and-install-docker-compose - - download-and-install-mockserver - - install-and-configure-keycloak - - run-robot-tests: - sut: "TEST -v AUTH_TYPE:OAUTH" - include-tags: "SECURITY_oauth" - # test-suite-path: "" - test-suite-name: "SECURITY" - - CORS-test: - # executor: docker-py3-postgres - executor: machine-ubuntu-2004 - environment: - SUT: TEST - ADMINAPI_ACTIVE: "TRUE" - SYSTEM_ALLOWTEMPLATEOVERWRITE: "TRUE" - steps: - - checkout - - restore-workspace - - install-java17 - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - sut: "TEST" - include-tags: "HTTPACCESSCONTROL" - test-suite-path: "CORS_TESTS" - test-suite-name: "CORS" - - ADMIN-test: - # executor: docker-py3-postgres - executor: machine-ubuntu-2004 - environment: - SUT: TEST - ADMINAPI_ACTIVE: "TRUE" - SYSTEM_ALLOWTEMPLATEOVERWRITE: "TRUE" - steps: - - checkout - - restore-workspace - - install-java17 - - git-clone-robot-integration-tests-repo - - move-integration-tests-content-to-local-tests-folder - - run-robot-tests: - sut: "ADMIN-TEST" - include-tags: "ADMIN" - test-suite-path: "ADMIN_TESTS" - test-suite-name: "ADMIN" - - - ROBOT-TEST-REPORT: - executor: docker-python3 - steps: - - restore-test-results-folder - - merge-robot-outputs - - sonar-analysis: - executor: docker-python3 - steps: - - checkout - - install-java17 - - attach_workspace: - at: /home/circleci - - run: - name: Merge Jacoco .exec files - command: | - java -jar ~/jacoco/lib/jacococli.jar merge ./*/target/jacoco*.exec \ - --destfile test-coverage/jacoco-all-tests-coverage.exec - - run: - name: Generate coverage report from jacoco-all-tests-coverage.exec - command: | - mkdir -p test-coverage/overall-coverage-report - java -jar ~/jacoco/lib/jacococli.jar report test-coverage/jacoco-all-tests-coverage.exec \ - --classfiles api/target/classes/ \ - --classfiles application/target/classes/ \ - --classfiles base/target/classes/ \ - --classfiles jooq-pq/target/classes/ \ - --classfiles rest-ehr-scape/target/classes/ \ - --classfiles rest-openehr/target/classes/ \ - --classfiles service/target/classes/ \ - --sourcefiles api/src/main/java/ \ - --sourcefiles application/src/main/java/ \ - --sourcefiles base/src/main/java/ \ - --sourcefiles jooq-pq/src/main/java/ \ - --sourcefiles rest-ehr-scape/src/main/java/ \ - --sourcefiles rest-openehr/src/main/java/ \ - --sourcefiles service/src/main/java/ \ - --html test-coverage/overall-coverage-report \ - --xml test-coverage/overall-coverage-report/jacoco.xml \ - --name "EHRbase Code Coverage w/ All Tests (Unit, SDK, Robot)" - - store_artifacts: - path: ~/projects/test-coverage/overall-coverage-report - - - sonarcloud/scan: - cache_version: 1 # NOTE: increment this value to force cache rebuild - - - - - - - - - - -commands: - # ,ad8888ba, ,ad8888ba, 88b d88 88b d88 db 888b 88 88888888ba, ad88888ba - # d8"' `"8b d8"' `"8b 888b d888 888b d888 d88b 8888b 88 88 `"8b d8" "8b - # d8' d8' `8b 88`8b d8'88 88`8b d8'88 d8'`8b 88 `8b 88 88 `8b Y8, - # 88 88 88 88 `8b d8' 88 88 `8b d8' 88 d8' `8b 88 `8b 88 88 88 `Y8aaaaa, - # 88 88 88 88 `8b d8' 88 88 `8b d8' 88 d8YaaaaY8b 88 `8b 88 88 88 `"""""8b, - # Y8, Y8, ,8P 88 `8b d8' 88 88 `8b d8' 88 d8""""""""8b 88 `8b 88 88 8P `8b - # Y8a. .a8P Y8a. .a8P 88 `888' 88 88 `888' 88 d8' `8b 88 `8888 88 .a8P Y8a a8P - # `"Y8888Y"' `"Y8888Y"' 88 `8' 88 88 `8' 88 d8' `8b 88 `888 88888888Y"' "Y88888P" - # 88 - # 88 ,d ,d ,d - # 88 88 88 88 - # 8b,dPPYba, ,adPPYba, 88,dPPYba, ,adPPYba, MM88MMM MM88MMM ,adPPYba, ,adPPYba, MM88MMM ,adPPYba, - # 88P' "Y8 a8" "8a 88P' "8a a8" "8a 88 88 a8P_____88 I8[ "" 88 I8[ "" - # 88 8b d8 88 d8 8b d8 88 88 8PP""""""" `"Y8ba, 88 `"Y8ba, - # 88 "8a, ,a8" 88b, ,a8" "8a, ,a8" 88, 88, "8b, ,aa aa ]8I 88, aa ]8I - # 88 `"YbbdP"' 8Y"Ybbd8"' `"YbbdP"' "Y888 "Y888 `"Ybbd8"' `"YbbdP"' "Y888 `"YbbdP"' - # - - - run-robot-tests: - description: Run integration tests written in Robot Framework - parameters: - sut: - description: SUT - System Under Test Config - enum: ["DEV", "DEV -v AUTH_TYPE:OAUTH", "ADMIN-DEV", "TEST", "TEST -v AUTH_TYPE:OAUTH", "ADMIN-TEST"] - default: "DEV" - type: enum - - nodename: - description: | - EHRbase's "CREATING_SYSTEM_ID". It can be set from cli when starting server .jar, i.e.: - `java -jar application.jar --server.nodename=local.ehrbase.org` - default: "circleci.ehrbase.org" - type: string - - allow-template-overwrite: - description: Sets EHRbase's cli option `--system.allow-template-overwrite=true` - default: true - type: boolean - - cache-enabled: - description: Sets EHRbase's cli option `--cache.enabled=true` - default: true - type: boolean - - include-tags: - description: Which tests to include by TAGs (Robot syntax applies!) - type: string - - test-suite-path: - description: Target test-suite given by it's folder name e.g. COMPOSITION_TESTS - default: "" - type: string - - test-suite-name: - description: Titel of generated Robot Log/Report.html - type: string - - steps: - # - cache-out-python-requirements - - install-java17 - - install-maven - - install-python3-requirements - - install-xml-cli-tool - # - run: jps - # - run: - # name: Wait until EHRbase server is ready - # command: | - # grep -m 1 "Started EhrBase in" <(tail -f log) - - run: - name: START EHRBASE SERVER AND EXECUTE ROBOT TESTS - no_output_timeout: 30m - command: | - echo "SUT: $SUT" - echo "ADMINAPI_ACTIVE: $ADMINAPI_ACTIVE" - echo "SECURITY_AUTHTYPE: $SECURITY_AUTHTYPE" - echo "OAUTH_RESRCSERVER_URL: $SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI" - ls -la - if [ "${SUT}" != "TEST" ]; then - - EHRbase_VERSION=$(xmlstarlet sel -t -m "/_:project/_:version" -v . pom.xml ) - echo ${EHRbase_VERSION} - java -jar "application/target/application-${EHRbase_VERSION}.jar" \ - --system.allow-template-overwrite=<< parameters.allow-template-overwrite >> \ - --server.nodename=<< parameters.nodename >> \ - --plugin-manager.enable=true \ - --plugin-manager.plugin-dir='tests/test_plugin' \ - --plugin-manager.plugin-config-dir='tests/test_plugin_config_dir' \ - --cache.enabled=<< parameters.cache-enabled >> > log & app_pid=$! - timeout=360 - while [ ! -f ./log ]; - do - echo "Waiting for file ./log ..." - if [ "$timeout" == 0 ]; then - echo "ERROR: timed out while waiting for file ./log" - exit 1 - fi - sleep 1 - ((timeout--)) - done - while ! (cat ./log | grep -m 1 "Started EhrBase in"); - do - echo "waiting for EHRbase to be ready ..."; - tail -n 5 ./log - if [ "$timeout" == 0 ]; then - echo "WARNING: Did not see a startup message even after waiting 60s" - exit 1 - fi - sleep 1; - ((timeout--)) - done - echo "REMAINING TIMEOUT: $timeout" - fi - jps - cd ~/projects/tests - robot --include "<< parameters.include-tags >>" \ - --skip TODO --skip future -e obsolete -e libtest \ - --console dotted \ - --loglevel TRACE \ - --skiponfailure not-ready \ - --flattenkeywords for \ - --flattenkeywords foritem \ - --flattenkeywords name:_resources.* \ - --outputdir results/<< parameters.test-suite-name >> \ - --timestampoutputs \ - --name "<< parameters.test-suite-name >>" \ - -v SUT:<< parameters.sut >> \ - -v NODENAME:<< parameters.nodename >> \ - -v ALLOW-TEMPLATE-OVERWRITE:<< parameters.allow-template-overwrite >> \ - robot/<< parameters.test-suite-path >> - # - cache-in-python-requirements - - save-test-results-folder: - suite-results-folder-name: << parameters.test-suite-name >> - - store_test_results: - path: ~/projects/tests/results/ - - store_artifacts: - path: ~/projects/tests/results/ - - save-test-results-folder: - description: Persist Robot tests folder to workspace - parameters: - suite-results-folder-name: - description: Title of generated Robot Outputs - type: string - steps: - - run: - name: PERSIST ROBOT TEST RESULTS - when: always - command: echo "persist test results & requirements.txt file" - - persist_to_workspace: - root: /home/circleci - paths: - - projects/tests/results/<< parameters.suite-results-folder-name >> - - - restore-test-results-folder: - description: Attach Robot tests folder back to workspace - steps: - - attach_workspace: - at: /home/circleci/ - - - merge-robot-outputs: - description: Merge Robot Results from Parallel Tests - steps: - - run: - command: | - pip install -r ~/projects/tests/requirements.txt - - run: - name: POST PROCESS & MERGE TEST RESULTS - when: always - command: | - cd tests - - # Create Log/Report with ALL DETAILS - rebot --outputdir results/0 \ - --name EHRbase \ - -e obsolete -e libtest \ - --removekeywords for \ - --removekeywords wuks \ - --loglevel TRACE \ - --output EHRbase-output.xml \ - --log EHRbase-log.html \ - --report EHRbase-report.html \ - results/*/*.xml - - run: - name: GENERATE TEST SUMMARY - when: always - command: | - cd tests - - # Create JUNIT report from merged results - rebot --outputdir results/robot-tests \ - -e obsolete -e libtest \ - --xunit junit-output.xml --xunitskipnoncritical \ - --log NONE \ - --report NONE \ - results/0/EHRbase-output.xml - - save-test-results-folder: - suite-results-folder-name: "0" - - store_test_results: - path: ~/projects/tests/results/ - - store_artifacts: - path: ~/projects/tests/results/ - - - persist-robot-requirements-file: - steps: - - persist_to_workspace: - root: /home/circleci - paths: projects/tests/requirements.txt - - - # /////////////////////////////////////////////////////////////////////////// - # /// SDK COMMANDS (openEHR_SDK) /// - # /////////////////////////////////////////////////////////////////////////// - - git-clone-sdk-repo: - steps: - - run: - name: CLONE SDK REPO - command: | - git clone git@github.com:ehrbase/openEHR_SDK.git - ls -la - - git-clone-robot-integration-tests-repo: - steps: - - run: - name: CLONE ROBOT INTEGRATION TESTS REPO - command: | - pwd - ls -la - integration_folder=/home/circleci/projects/integration-tests - if [ -d "$integration_folder" ]; then - echo "'$integration_folder' found! Skipping clone repo to path '$integration_folder'" - else - echo "'$integration_folder' not found! Cloning external repo in progress ..." - git clone git@github.com:ehrbase/integration-tests.git - ls -la - fi - - move-integration-tests-content-to-local-tests-folder: - steps: - - run: - name: MOVE INTEGRATION TESTS CONTENT TO LOCAL PROJECT TESTS FOLDER - command: | - pwd - robot_folder_path=/home/circleci/projects/tests/robot/ - if [ -d "robot_folder_path" ]; then - echo "Tests folder from root project contains robot folder" - ls -la /home/circleci/projects/tests/robot/ - else - echo "Moving files and folders to root project, tests folder ..." - mv -v /home/circleci/projects/integration-tests/tests/* /home/circleci/projects/tests/ - ls -la /home/circleci/projects/tests/ - echo "-------------------" - ls -la /home/circleci/projects/tests/robot/ - fi - - git-checkout-sdk-sync-branch: - steps: - - run: - name: CHECKOUT SDK SYNC/BRANCH - command: | - echo BRANCH NAME TO CHECKOUT: $CIRCLE_BRANCH - cd ~/projects/openEHR_SDK - git checkout $CIRCLE_BRANCH - - - maven-install-sdk: - steps: - - install-xml-cli-tool - - run: - name: Save the version number of locally installed SDK into a file - command: | - cd ~/projects/openEHR_SDK - mvn build-helper:parse-version versions:set -DnewVersion=\${project.version}-LOCAL$(cat /proc/sys/kernel/random/uuid) -DprocessAllModules=true versions:commit - - SDK_VERSION=$(xmlstarlet sel -t -m "/_:project/_:version" -v . pom.xml ) - echo $SDK_VERSION > SDK_VERSION - cat SDK_VERSION - - run: - name: INSTALL SDK - command: | - cd ~/projects/openEHR_SDK - mvn install -Dmaven.javadoc.skip=true -Djacoco.skip=true - - - start-ehrbase-and-run-java-integration-sdk-tests: - description: | - Starts ehrbase server and runs maven test phase using 'slow' profile defined in parent pom.xml - This way only the Java integration tests are executed. - steps: - - install-xml-cli-tool - - run: - name: Start EHRbase server and run SDK's java integration tests - command: | - ls -la - cd ~/projects - EHRbase_VERSION=$(xmlstarlet sel -t -m "/_:project/_:version" -v . pom.xml ) - echo ${EHRbase_VERSION} - java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true --plugin-manager.enable=true --plugin-manager.plugin-dir='tests/test_plugin' --plugin-manager.plugin-config-dir='tests/test_plugin_config_dir'> log & - grep -m 1 "Started EhrBase in" <(tail -f log) - cd ~/projects/openEHR_SDK - jps - # mvn verify -DskipIntegrationTests=false -Dmaven.javadoc.skip=true - mvn test -Pslow -Dmaven.javadoc.skip=true - - - start-ehrbase-and-run-all-sdk-tests: - description: | - Executes all SDK java tests (unit and integration) - This requires EHRbase + DB to be running during test execution. - steps: - - install-java17 - - install-maven - - install-xml-cli-tool - - run: - name: Start EHRbase server and run all test of SDK - command: | - ls -la - java --version - EHRbase_VERSION=$(xmlstarlet sel -t -m "/_:project/_:version" -v . pom.xml ) - echo ${EHRbase_VERSION} - cd ~/projects # NOTE: This is where the target folder w/ artifacts were persisted to in previous step. - echo "" > log - java -javaagent:/home/circleci/jacoco/lib/jacocoagent.jar=output=tcpserver,address=127.0.0.1 \ - -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true --plugin-manager.enable=true --plugin-manager.plugin-dir='tests/test_plugin' --plugin-manager.plugin-config-dir='tests/test_plugin_config_dir'>> log & - grep -m 1 "Started EhrBase in" <(tail -f log) - cd ~/projects/openEHR_SDK - jps - mvn verify -DskipIntegrationTests=false -Dmaven.javadoc.skip=true - java -jar ~/jacoco/lib/jacococli.jar dump \ - --destfile=/home/circleci/projects/test-coverage/target/jacoco-sdk-it-coverage.exec - while [ ! -f /home/circleci/projects/test-coverage/target/jacoco-sdk-it-coverage.exec ]; - do echo "Waiting for jacoco execution data file"; - sleep 1 - done; - echo "Jacoco execution data found!" - - - collect-sdk-it-coverage: - description: Persists coverage execution data from SDK integration tests - steps: - - run: echo Collect Code Coverage From SDK Integration Tests - - persist_to_workspace: - name: Persist coverage data (projects/test-coverage/target/jacoco-sdk-it-coverage.exec) - root: /home/circleci - paths: - - projects/test-coverage/target/jacoco-sdk-it-coverage.exec - - - generate-sdk-it-coverage-report: - description: Generates human readable reports from jacoco-sdk-it-coverage.exec - steps: - - run: - name: Generate SDK IT Coverage Report - command: | - mkdir -p test-coverage/sdk-it-coverage-report - java -jar ~/jacoco/lib/jacococli.jar report test-coverage/target/jacoco-sdk-it-coverage.exec \ - --classfiles api/target/classes/ \ - --classfiles application/target/classes/ \ - --classfiles base/target/classes/ \ - --classfiles jooq-pq/target/classes/ \ - --classfiles rest-ehr-scape/target/classes/ \ - --classfiles rest-openehr/target/classes/ \ - --classfiles service/target/classes/ \ - --sourcefiles api/src/main/java/ \ - --sourcefiles application/src/main/java/ \ - --sourcefiles base/src/main/java/ \ - --sourcefiles jooq-pq/src/main/java/ \ - --sourcefiles rest-ehr-scape/src/main/java/ \ - --sourcefiles rest-openehr/src/main/java/ \ - --sourcefiles service/src/main/java/ \ - --html test-coverage/sdk-it-coverage-report \ - --xml test-coverage/sdk-it-coverage-report/jacoco.xml \ - --name "EHRbase Code Coverage w/ SDK Integration Tests" - - store_artifacts: - path: ~/projects/test-coverage/sdk-it-coverage-report - - - save-local-sdk-installation: - steps: - - persist_to_workspace: - root: /home/circleci - paths: - - .m2/repository/com/github/ehrbase/openEHR_SDK - - projects/openEHR_SDK - - projects/SDK_VERSION - - - restore-local-sdk-installation: - steps: - - attach_workspace: - at: /home/circleci/ - - - cache-out-sdk-m2-dependencies: - steps: - - run: - name: Generate Cache Checksum for openEHR_SDK Dependencies - command: find openEHR_SDK/ -type f -name *.java | sort | xargs cat > /tmp/openEHR_SDK_maven_cache_seed - - restore_cache: - key: openEHR_SDK- - - - cache-in-sdk-m2-dependencies: - steps: - - save_cache: - key: openEHR_SDK-{{ checksum "/tmp/openEHR_SDK_maven_cache_seed" }} - paths: - - ~/.m2 - - - cache-out-sdk-m2-dependencies-sync-branch: - steps: - - run: - name: Generate Cache Checksum for openEHR_SDK Dependencies - command: find openEHR_SDK/ -type f -name *.java | sort | xargs cat > /tmp/openEHR_SDK_syncbranch_maven_cache_seed - - restore_cache: - key: openEHR_SDK-syncbranch-v1- - - - cache-in-sdk-m2-dependencies-sync-branch: - steps: - - save_cache: - key: openEHR_SDK-syncbranch-v1-{{ checksum "/tmp/openEHR_SDK_syncbranch_maven_cache_seed" }} - paths: - - ~/.m2 - - - collect-sdk-unittest-results: - steps: - - run: - name: Save unit test results - command: | - mkdir -p ~/sdk-test-results/unit-tests/ - find ~/projects/openEHR_SDK/ -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/sdk-test-results/unit-tests/ \; - find ~/projects/openEHR_SDK/ -type f -regex ".*/target/surefire-reports/.*txt" -exec cp {} ~/sdk-test-results/unit-tests/ \; - when: always - - - collect-sdk-integrationtest-results: - steps: - - run: - name: Save integration test results - command: | - mkdir -p ~/sdk-test-results/integration-tests/ - find ~/projects/openEHR_SDK/ -type f -regex ".*/target/failsafe-reports/.*xml" -exec cp {} ~/sdk-test-results/integration-tests/ \; - find ~/projects/openEHR_SDK/ -type f -regex ".*/target/failsafe-reports/.*txt" -exec cp {} ~/sdk-test-results/integration-tests/ \; - when: always - - - save-sdk-test-results: - steps: - - store_test_results: - path: ~/sdk-test-results - - store_artifacts: - path: ~/sdk-test-results - - - - - - # /////////////////////////////////////////////////////////////////////////// - # /// EHRBASE COMMANDS /// - # /////////////////////////////////////////////////////////////////////////// - - force-ehrbase-build-to-use-local-sdk-version: - steps: - - install-xml-cli-tool - - run: - name: Adjust SDK version number in EHRbase's pom - command: | - SDK_VERSION=$(cat ~/projects/openEHR_SDK/SDK_VERSION) - echo $SDK_VERSION - # cd ~/projects - xmlstarlet edit --inplace -u /_:project/_:properties/_:ehrbase.sdk.version -v $SDK_VERSION bom/pom.xml - - run: - name: Show EHRbase's pom - command: cat bom/pom.xml - - - build-and-test-ehrbase: - steps: - - force-ehrbase-build-to-use-local-sdk-version - - run: - name: Maven build EHRbase - command: | - mvn package -Dmaven.javadoc.skip=true # -Dmaven.test.skip - - - start-ehrbase-server: - steps: - - install-java17 - - install-maven - - install-xml-cli-tool - - run: - name: Start EHRbase server and wait for it to be ready - background: true - command: | - ls -la - - EHRbase_VERSION=$(xmlstarlet sel -t -m "/_:project/_:version" -v . pom.xml ) - echo ${EHRbase_VERSION} - java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true --plugin-manager.enable=true --plugin-manager.plugin-dir='tests/test_plugin' --plugin-manager.plugin-config-dir='tests/test_plugin_config_dir' > log & - grep -m 1 "Started EhrBase in" <(tail -f log) - jps - - - cache-in-ehrbase-m2-dependencies-syncbranch: - steps: - - save_cache: - key: EHRbase-sychbranch-v2-{{ checksum "/tmp/EHRbase_syncbranch_maven_cache_seed" }} - paths: - - ~/.m2/repository/org/ehrbase/openehr/ - - - cache-out-ehrbase-m2-dependencies-syncbranch: - steps: - - run: - name: Generate Cache Checksum for EHRbase Dependencies - command: find ~/projects -name 'pom.xml' | sort | xargs cat > /tmp/EHRbase_syncbranch_maven_cache_seed - - restore_cache: - key: EHRbase-sychbranch-v2 - - - save-packaged-ehrbase-jar: - steps: - - persist_to_workspace: - root: /home/circleci - paths: - - projects/api/target - - projects/application/target - - projects/base/target - - projects/jooq-pq/target - - projects/rest-ehr-scape/target - - projects/rest-openehr/target - - projects/service/target - - projects/test-coverage/target - - projects/tests/requirements.txt - - - restore-workspace: - steps: - - attach_workspace: - at: /home/circleci/ - - save-openehr-sdk-repo: - steps: - - persist_to_workspace: - root: /home/circleci - paths: - - projects/openEHR_SDK - - - collect-ehrbase-unittest-results: - steps: - - run: - name: Save unit test results - command: | - mkdir -p ~/ehrbase-test-results/unit-tests/ - find ~/projects -type f -regex ".*/target/surefire-reports/.*xml" -not -path "~/projects/openEHR_SDK/*" -exec cp {} ~/ehrbase-test-results/unit-tests/ \; - find ~/projects -type f -regex ".*/target/surefire-reports/.*txt" -not -path "~/projects/openEHR_SDK/*" -exec cp {} ~/ehrbase-test-results/unit-tests/ \; - when: always - - - save-ehrbase-test-results: - steps: - - store_test_results: - path: ~/ehrbase-test-results - - store_artifacts: - path: ~/ehrbase-test-results - - - # WARNING: don't use these two steps in production - # use them for pipeline debugging only! - cache-in-ehrbase-workspace: - steps: - - save_cache: - key: ehrbase-workspace-cache-v1 - paths: - - application/target - cache-out-ehrbase-workspace: - steps: - - restore_cache: - key: ehrbase-workspace-cache-v1 - - - - - - # 88 ad88 - # ,d ,d "" d8" - # 88 88 88 - # MM88MMM ,adPPYba, ,adPPYba, MM88MMM 88 8b,dPPYba, MM88MMM 8b,dPPYba, ,adPPYYba, - # 88 a8P_____88 I8[ "" 88 88 88P' `"8a 88 88P' "Y8 "" `Y8 - # 88 8PP""""""" `"Y8ba, 88 88 88 88 88 88 ,adPPPPP88 - # 88, "8b, ,aa aa ]8I 88, 88 88 88 88 88 88, ,88 - # "Y888 `"Ybbd8"' `"YbbdP"' "Y888 88 88 88 88 88 `"8bbdP"Y8 - # - - configure-git-for-ci-bot: - steps: - - add_ssh_keys: - fingerprints: - - 3e:42:46:e1:9e:40:4d:ae:33:ab:db:0a:95:24:d2:99 - - run: - name: Configure GIT - command: | - git config --global user.email "ci-bot@ehrbase.org" - git config --global user.name "ci-bot" - # git config --global push.followTags true - git remote -v - - install-java17: - description: Install Java 17 (if not already installed) - steps: - - run: - name: Install Java 17 - command: | - if [ ! -d "/usr/lib/jvm/jdk-17.0.5+8" ]; then - wget https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.5%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.5_8.tar.gz -O /tmp/openjdk-17.tar.gz - sudo mkdir -p /usr/lib/jvm - sudo tar xfvz /tmp/openjdk-17.tar.gz --directory /usr/lib/jvm - rm -f /tmp/openjdk-17.tar.gz - export JAVA_HOME=/usr/lib/jvm/jdk-17.0.5+8 - export PATH=$PATH:$JAVA_HOME/bin - sudo sh -c 'for bin in /usr/lib/jvm/jdk-17.0.5+8/bin/*; do update-alternatives --install /usr/bin/$(basename $bin) $(basename $bin) $bin 100; done' - sudo sh -c 'for bin in /usr/lib/jvm/jdk-17.0.5+8/bin/*; do update-alternatives --set $(basename $bin) $bin; done' - fi - java --version - - - install-maven: - description: Install Maven 3.8.7 (if not already installed) - steps: - - run: - name: Install Maven tool - command: | - sudo killall -9 apt-get || true - if [ ! -d "/usr/lib/maven/apache-maven-3.8.7" ]; then - wget https://archive.apache.org/dist/maven/maven-3/3.8.7/binaries/apache-maven-3.8.7-bin.tar.gz -O /tmp/maven.tar.gz - sudo mkdir -p /usr/lib/maven - sudo tar xfvz /tmp/maven.tar.gz --directory /usr/lib/maven - rm -f /tmp/maven.tar.gz - export MAVEN_HOME=/usr/lib/maven/apache-maven-3.8.7 - export PATH=$MAVEN_HOME/bin:$PATH - sudo update-alternatives --install /usr/bin/mvn mvn /usr/lib/maven/apache-maven-3.8.7/bin/mvn 100 - sudo update-alternatives --set mvn /usr/lib/maven/apache-maven-3.8.7/bin/mvn - fi - mvn --version - - download-and-install-docker-compose: - description: Download and install docker compose image - steps: - - setup_remote_docker: - docker_layer_caching: true - version: 20.10.12 - - run: - name: Download and Install Docker instance - command: | - sudo apt -y update && sudo apt upgrade && sudo apt install curl - curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-x86_64" -o ~/docker-compose - chmod +x ~/docker-compose - sudo mv ~/docker-compose /usr/local/bin/docker-compose - - run: - name: Spin up database - command: | - docker-compose down -v - docker-compose up -d - - download-and-install-mockserver: - description: Download and install latest version of Mockserver Docker image - steps: - - run: - name: Download mockserver - command: | - docker pull mockserver/mockserver - - run: - name: - command: | - docker run -d --rm -p 1080:1080 --env MOCKSERVER_SERVER_PORT=1080 mockserver/mockserver - - install-and-configure-keycloak: - description: Setups a Keycloak Docker instance and restores a previously exportd configuration. - steps: - - run: - name: Start Keycloak in a Docker container - command: | - cd tests/robot/SECURITY_TESTS/I_OAuth2_Keycloak - docker run -d --name keycloak \ - -p 8081:8080 \ - -v $(pwd)/exported-keycloak-config:/restore-keycloak-config \ - -e KEYCLOAK_USER=admin \ - -e KEYCLOAK_PASSWORD=admin \ - jboss/keycloak:10.0.2 - - run: - name: Restore Keycloak configuration (realm, clients, roles, users) - background: true - command: | - docker exec -it keycloak /opt/jboss/keycloak/bin/standalone.sh \ - -Djboss.socket.binding.port-offset=100 \ - -Dkeycloak.migration.action=import \ - -Dkeycloak.migration.provider=dir \ - -Dkeycloak.profile.feature.upload_scripts=enabled \ - -Dkeycloak.migration.dir=/restore-keycloak-config \ - -Dkeycloak.migration.strategy=OVERWRITE_EXISTING - - run: - name: Wait until Keycloak configuration import is complete - command: | - echo - echo "Wait for Keycloak to be ready" - echo "=============================" - echo - while ! (docker container logs keycloak | fgrep -q "Keycloak 10.0.2 (WildFly Core 11.1.1.Final) started in"); - do sleep 1; - # uncomment next line to see progress in terminal - #docker container logs --tail 3 --raw keycloak; - echo "... waiting for keycloak ..."; - done - echo "KEYCLOAK READY" - - - install-xml-cli-tool: - steps: - - run: - name: Install xmlstarlet to handle XML file from CLI - command: | - sudo killall -9 apt-get || true - sudo apt -y update && sudo apt -y install xmlstarlet - - - configure-python-version: - description: Configure Python version to 3.7.0 - steps: - - run: - name: Configure Python version to 3.7.0 - command: | - pyenv global 3.7.0 - - install-python-requirements: - description: Install Python requirements - steps: - - run: - name: Install Python requirements - command: | - pwd - ls -la - python -c "import site; print(site.getsitepackages())" - pip install -r ~/projects/tests/requirements.txt - - install-python3-requirements: - description: Install Python requirements - steps: - - run: - name: Install Python requirements - command: | - python3 -c "import site; print(site.getsitepackages())" - pip3 install -r ~/projects/tests/requirements.txt - - setup-database: - description: Setup ehrbase database - steps: - - run: - name: Setup database - command: | - docker run -d --name ehrdb \ - -e POSTGRES_USER=$POSTGRES_USER \ - -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD -d \ - -e DISABLE_SECURITY=true \ - -p 5432:5432 ehrbase/ehrbase-postgres:13.4.v2 - - - setup-file-repo: - description: Setup file repo - steps: - - run: - name: Unzip provided file repo - command: | - unzip ~/projects/.circleci/file_repo_content.zip -d ~/projects - - setup-jacoco-distribution: - description: Download and unzip Jacoco Code Coverage Tool - steps: - - run: - name: Download and unzip Jacoco - command: | - mkdir -p ~/download - cd ~/download - [ -e jacoco-0.8.6.zip ] || wget https://repo1.maven.org/maven2/org/jacoco/jacoco/0.8.8/jacoco-0.8.8.zip - mkdir -p ~/jacoco - unzip -uo jacoco-0.8.8.zip -d ~/jacoco - - persist_to_workspace: - name: Persist Jacoco Download (~/jacoco) - root: /home/circleci - paths: - - jacoco - - - - - - # 88 - # "" ,d ,d ,d - # 88 88 88 - # 88 88 8b,dPPYba, 88 MM88MMM MM88MMM ,adPPYba, ,adPPYba, MM88MMM ,adPPYba, - # 88 88 88P' `"8a 88 88 88 a8P_____88 I8[ "" 88 I8[ "" - # 88 88 88 88 88 88 88 8PP""""""" `"Y8ba, 88 `"Y8ba, - # "8a, ,a88 88 88 88 88, 88, "8b, ,aa aa ]8I 88, aa ]8I - # `"YbbdP'Y8 88 88 88 "Y888 "Y888 `"Ybbd8"' `"YbbdP"' "Y888 `"YbbdP"' - # - - maven-test: - description: Test Maven app - steps: - - run: - name: Test Maven app - command: | - cd ~/projects - mvn org.jacoco:jacoco-maven-plugin:0.8.2:prepare-agent test \ - org.jacoco:jacoco-maven-plugin:0.8.2:report - - persist-unit-test-coverage: - description: Persist unit test coverage report to workspace - steps: - - persist_to_workspace: - root: /home/circleci - paths: - - projects/rest-ehr-scape/target/site/jacoco/jacoco.xml - - projects/rest-openehr/target/site/jacoco/jacoco.xml - - projects/serialisation/target/site/jacoco/jacoco.xml - - projects/service/target/site/jacoco/jacoco.xml - - projects/terminology/target/site/jacoco/jacoco.xml - - projects/validation/target/site/jacoco/jacoco.xml - - save-unit-tests-job-caches: - description: Save all caches in unit tests job - steps: - - save_cache: - key: job-unit-tests-v1-mvn-dependencies-{{ checksum "~/projects/pom.xml" }} - paths: - - ~/.m2/repository - - restore-unit-tests-job-caches: - description: Restore all caches in unit tests job - steps: - - restore_cache: - keys: - - job-unit-tests-v1-mvn-dependencies-{{ checksum "~/projects/pom.xml" }} - - job-unit-tests-v1-mvn-dependencies - - - - - - # - # - # - # 88,dPYba,,adPYba, ,adPPYYba, 8b d8 ,adPPYba, 8b,dPPYba, - # 88P' "88" "8a "" `Y8 `8b d8' a8P_____88 88P' `"8a - # 88 88 88 ,adPPPPP88 `8b d8' 8PP""""""" 88 88 - # 88 88 88 88, ,88 `8b,d8' "8b, ,aa 88 88 - # 88 88 88 `"8bbdP"Y8 "8" `"Ybbd8"' 88 88 - # - - maven-package: - description: Package Maven app - steps: - - run: - name: Package Maven app - command: | - cd ~/projects - mvn package -U # -DskipTests - maven-check-codestyle: - description: Run spotless plugin check goal - steps: - - run: - name: Run spotless plugin check goal - command: | - cd ~/projects - mvn com.diffplug.spotless:spotless-maven-plugin:check - - - - - - - - # 88 - # 88 - # 88 - # 8b db d8 ,adPPYba, 8b,dPPYba, 88 ,d8 ,adPPYba, 8b,dPPYba, ,adPPYYba, ,adPPYba, ,adPPYba, - # `8b d88b d8' a8" "8a 88P' "Y8 88 ,a8" I8[ "" 88P' "8a "" `Y8 a8" "" a8P_____88 - # `8b d8'`8b d8' 8b d8 88 8888[ `"Y8ba, 88 d8 ,adPPPPP88 8b 8PP""""""" - # `8bd8' `8bd8' "8a, ,a8" 88 88`"Yba, aa ]8I 88b, ,a8" 88, ,88 "8a, ,aa "8b, ,aa - # YP YP `"YbbdP"' 88 88 `Y8a `"YbbdP"' 88`YbbdP"' `"8bbdP"Y8 `"Ybbd8"' `"Ybbd8"' - # 88 - # 88 - - persist-tests-folder: - description: Persist Robot tests folder to workspace - steps: - - run: - when: always - command: | - echo "persist test results" - - persist_to_workspace: - root: /home/circleci - paths: - - projects/tests/results - - attach-tests-folder: - description: Attach Robot tests folder back to workspace - steps: - - attach_workspace: - at: /home/circleci - - persist-integration-test-coverage: - description: Persist integration test coverage report to workspace - steps: - - persist_to_workspace: - root: /home/circleci - paths: - - projects/application/target/jacoco-it.exec - - persist-target-folder: - description: Persist target folder to workspace - steps: - - persist_to_workspace: - root: /home/circleci - paths: - - projects/application/target - - attach-target-folder: - description: Attach target folder back to workspace - steps: - - attach_workspace: - at: /home/circleci - - # persist-dependency-check-results: - # description: Persist dependency check results - # steps: - # - persist_to_workspace: - # root: /home/circleci - # paths: - # - projects/sonar_issues.json - - - - - - # 88 - # 88 - # 88 - # ,adPPYba, ,adPPYYba, ,adPPYba, 88,dPPYba, ,adPPYba, - # a8" "" "" `Y8 a8" "" 88P' "8a a8P_____88 - # 8b ,adPPPPP88 8b 88 88 8PP""""""" - # "8a, ,aa 88, ,88 "8a, ,aa 88 88 "8b, ,aa - # `"Ybbd8"' `"8bbdP"Y8 `"Ybbd8"' 88 88 `"Ybbd8"' - # - - - save-integration-tests-job-caches: - description: Save all caches in interation tests job - steps: - - run: - when: always - command: echo "save integration test cache" - - save_cache: - key: job-integration-tests-v1-download-0.8.2 - paths: - - ~/downloads - - save_cache: - key: job-integration-tests-v1-installation-0.8.2 - paths: - - ~/jacoco-0.8.2 - - save_cache: - key: job-integration-tests-v2-pip-{{ checksum "~/projects/tests/requirements.txt" }} - paths: - - ~/.cache/pip - # - /opt/circleci/.pyenv/versions/3.7.0/lib/python3.7/site-packages - - save_cache: - key: google-chrome-incl-webdriver-75 - paths: - - ~/downloads/chrome - - restore-integration-tests-job-caches: - description: Restore all caches in interation tests job - steps: - - restore_cache: - keys: - - job-integration-tests-v1-download-0.8.2 - - restore_cache: - keys: - - job-integration-tests-v1-installation-0.8.2 - - restore_cache: - keys: - - job-integration-tests-v2-pip-{{ checksum "~/projects/tests/requirements.txt" }} - - restore_cache: - keys: - - google-chrome-incl-webdriver- - - save-build-ehrbase-job-caches: - description: Save all caches in building artifacts job - steps: - - save_cache: - key: job-build-ehrbase-v2-mvn-dependencies-{{ checksum "~/projects/pom.xml" }} - paths: - - ~/.m2/repository - - restore-build-ehrbase-job-caches: - description: Restore all caches in building artifacts job - steps: - - restore_cache: - keys: - - job-build-ehrbase-v2-mvn-dependencies-{{ checksum "~/projects/pom.xml" }} - - job-build-ehrbase-v2-mvn-dependencies - - save-sonar-analysis-job-caches: - description: Save all caches in dependency check job - steps: - - save_cache: - key: job-sonar-analysis-v1-download-4.2.0.1873 - paths: - - ~/downloads - - save_cache: - key: job-sonar-analysis-v1-installation-4.2.0.1873 - paths: - - ~/sonar-scanner-4.2.0.1873-linux - - save_cache: - key: job-sonar-analysis-v1-scannerwork-{{ epoch }} - paths: - - ~/projects/.scannerwork - - save_cache: - key: job-sonar-analysis-v1-user-cache-{{ epoch }} - paths: - - ~/.sonar/cache - - restore-sonar-analysis-job-caches: - description: Restore all caches in dependency check job - steps: - - restore_cache: - keys: - - job-sonar-analysis-v1-download-4.2.0.1873 - - restore_cache: - keys: - - job-sonar-analysis-v1-installation-4.2.0.1873 - - restore_cache: - keys: - - job-sonar-analysis-v1-scannerwork - - restore_cache: - keys: - - job-sonar-analysis-v1-user-cache - - save-caches: - description: Save all caches - steps: - - save_cache: - paths: - - ~/.m2/repository - key: v1-mvn-dependencies-{{ checksum "pom.xml" }} - - restore-caches: - description: Restore all caches - steps: - - restore_cache: - keys: - - v1-mvn-dependencies-{{ checksum "pom.xml" }} - - v1-mvn-dependencies- - - - - - -# /////////////////////////////////////////////////////////////////////////// -# /// CIRCLECI META /// -# /////////////////////////////////////////////////////////////////////////// - - -orbs: - maven: circleci/maven@1.0.1 - openjdk-install: cloudesire/openjdk-install@1.2.3 - sonarcloud: sonarsource/sonarcloud@2.0.0 - -executors: - # https://hub.docker.com/u/cimg (circleci next-gen docker images) - # https://hub.docker.com/u/circleci (they call them legacy but they are still maintained) - - docker-base: - working_directory: ~/projects - docker: - - image: cimg/base:stable - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - - docker-python3: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - - docker-py3-postgres: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - - docker-mock-server: - working_directory: ~/projects - docker: - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - - image: mockserver/mockserver:5.13.2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - MOCKSERVER_SERVER_PORT: 1080 - MOCKSERVER_LOG_LEVEL: DEBUG - - image: docker:17.05.0-ce-git - - docker-py3-postgres-ci-timezone-berlin-postgres-timezone-berlin: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Europe/Berlin" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Europe/Berlin" - - docker-py3-postgres-ci-timezone-berlin-postgres-timezone-utc: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Europe/Berlin" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "UTC" - - docker-py3-postgres-ci-timezone-berlin-postgres-timezone-shanghai: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Europe/Berlin" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Asia/Shanghai" - - docker-py3-postgres-ci-timezone-utc-postgres-timezone-berlin: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "UTC" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Europe/Berlin" - - docker-py3-postgres-ci-timezone-utc-postgres-timezone-utc: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "UTC" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "UTC" - - docker-py3-postgres-ci-timezone-utc-postgres-timezone-shanghai: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "UTC" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Asia/Shanghai" - - docker-py3-postgres-ci-timezone-shanghai-postgres-timezone-berlin: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Asia/Shanghai" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Europe/Berlin" - - docker-py3-postgres-ci-timezone-shanghai-postgres-timezone-utc: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Asia/Shanghai" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "UTC" - - docker-py3-postgres-ci-timezone-shanghai-postgres-timezone-shanghai: - working_directory: ~/projects - docker: - # - image: cimg/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: cimg/python:3.8.5-buster-node-browsers - - image: cimg/python:3.8.14-node - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Asia/Shanghai" - - image: ehrbase/ehrbase-postgres:13.4.v2 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Asia/Shanghai" - - - machine-ubuntu-2004: - description: | - Ubuntu 20.04 VM (machine executor) - - openjdk 1.8 - - openjdk 11.0.8 (default) - - maven 3.6.3 - - gradle 6.6 - - python 2.7.17 - - python 3.8.5 - - pip/pip3 - - docker 19.03.12 - - docker-compose 1.26.2 - - aws-cli 2.0.43 - - google cloud sdk 307.0.0 - - heroku 7.42.12 - - chrome 85.0.4183 - - chromedriver 85.0.4183 - - firefox 80.0.0 - - go 1.15 - - leiningen 2.9.4 - - node 12.18.3 (default) - - node 14.8.0 - - ruby 2.7.1 - - sbt 1.3.13 - - yarn 1.22.4 - working_directory: ~/projects - environment: - PIPELINE_ID: << pipeline.id >> - BRANCH_NAME: << pipeline.git.branch >> - machine: - image: ubuntu-2004:202008-01 - - -# oooooooooo. .o. .oooooo. oooo oooo ooooo ooo ooooooooo. -# `888' `Y8b .888. d8P' `Y8b `888 .8P' `888' `8' `888 `Y88. -# 888 888 .8"888. 888 888 d8' 888 8 888 .d88' -# 888oooo888' .8' `888. 888 88888[ 888 8 888ooo88P' -# 888 `88b .88ooo8888. 888 888`88b. 888 8 888 -# 888 .88P .8' `888. `88b ooo 888 `88b. `88. .8' 888 -# o888bood8P' o88o o8888o `Y8bood8P' o888o o888o `YbodP' o888o -# -# [ BACKUP ] -# -# machine-ubuntu-1604: -# description: | -# Ubuntu 1604 VM -# - Python/Python3 -# - Pip/Pip3 -# - Java 8 and Maven -# - Docker 18.06 -# - Docker-Compose 1.22.0 -# working_directory: ~/projects -# environment: -# PIPELINE_ID: << pipeline.id >> -# BRANCH_NAME: << pipeline.git.branch >> -# machine: -# # image: circleci/classic:201808-01 -# # image: ubuntu-1604:201903-01 -# image: ubuntu-1604:202007-01 -# -# upload-test-status-report-to-slack: -# description: Uploads status report to Slack -# steps: -# - run: -# name: Upload test status report to Slack -# command: | -# curl -F file=@/home/circleci/projects/tests/results/test-status-report.png \ -# -F channels=playground \ -# -F title="${CIRCLE_PROJECT_REPONAME} TEST STATUS | ${CIRCLE_BRANCH}" \ -# -H "Authorization: Bearer xoxp-701547379457-696494594291-710681511959-9c9a861be3770efdd4f8637a076bf8c8" \ -# https://slack.com/api/files.upload - -# save-chrome-and-chromedirver-download-cache: -# description: Save Google Chrome and chromedriver download to cache -# steps: -# - save_cache: -# key: google-chrome-incl-webdriver-{{ $CHROME_VERSION }} -# paths: -# - ~/downloads/chrome -# -# -# restore-chrome-and-chromedirver-download-cache: -# description: Restore Google Chrome and chromedriver download from cache -# steps: -# - restore_cache: -# key: google-chrome-incl-webdriver- - -# COMPOSITION-tests-1: -# machine: -# image: ubuntu-1604:201903-01 -# environment: -# POSTGRES_USER: postgres -# POSTGRES_PASSWORD: postgres -# steps: -# - configure-python-version -# - checkout -# - restore-integration-tests-job-caches -# - setup-jacoco-distribution -# - attach-target-folder -# - install-python-requirements -# - run-integration-tests: -# include: "compositionANDjson1" -# - save-integration-tests-job-caches - -# run-integration-tests: -# description: Run integration tests -# parameters: -# include: -# type: string -# default: xxx -# steps: -# - run: -# name: Run integration tests with coverage -# no_output_timeout: 45m -# command: | -# cd tests -# robot -d results --console dotted --noncritical not-ready -L TRACE \ -# -i << parameters.include >> \ -# -e libtest \ -# -e obsolete \ -# -e future \ -# -e TODO \ -# -e circleci \ -# -e EHRSCAPE \ -# --xunit junit-output.xml --xunitskipnoncritical \ -# -v CODE_COVERAGE:True \ -# -v JACOCO_LIB_PATH:/home/circleci/jacoco-0.8.2/lib \ -# -v COVERAGE_DIR:/home/circleci/projects/application/target robot/ - -# set-slack-build-status: -# description: Set status env at the end of a job based on success or failure. -# steps: -# - run: -# name: Slack - Setting Failure Condition -# when: on_fail -# command: | -# echo 'export SLACK_BUILD_STATUS="FAIL"' >> $BASH_ENV -# - run: -# when: on_success -# name: Slack - Setting Success Condition -# command: | -# echo 'export SLACK_BUILD_STATUS="PASS"' >> $BASH_ENV - -# provide-test-status-report-via-slack: -# description: Generates an integration test status report and sends it to our Slack channel -# steps: -# - set-slack-build-status -# - run: -# name: Download and install Chrome and Chromedriver -# when: always -# command: | -# mkdir -p ~/downloads/chrome -# cd ~/downloads/chrome -# sudo killall -9 apt-get || true && \ -# sudo apt-get update && \ -# sudo apt-get install -f lsb-release libappindicator3-1 -# [ -e google-chrome.deb ] || curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -# sudo dpkg --configure -a -# sudo dpkg -i google-chrome.deb -# sudo sed -i 's|HERE/chrome"|HERE/chrome" --no-sandbox|g' /opt/google/chrome/google-chrome -# rm google-chrome.deb -# CHROME_VERSION=$(google-chrome --version | sed -r 's/[^0-9]+([0-9]+\.[0-9]+\.[0-9]+).*/\1/g') -# CHROMEDRIVER_VERSION=$(curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROME_VERSION) -# [ -e chromedriver_linux64.zip ] || wget https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip -# unzip chromedriver_linux64.zip -# sudo mv chromedriver /usr/local/bin/chromedriver -# sudo chown root:root /usr/local/bin/chromedriver -# sudo chmod +x /usr/local/bin/chromedriver -# - run: -# name: Check Browser Versions -# when: always -# command: | -# which chromedriver -# chromedriver --version -# google-chrome --version -# - run: -# name: Generate and Send Test Report To Slack Channel -# when: always -# command: | -# cd tests -# cp robot/_resources/status_report.robot results/status_report.robot -# cp robot/_resources/slack-message.json results/slack-message.json -# cp robot/_resources/logo.jpg results/logo.jpg -# cd results -# robot -d trash --output NONE --log NONE --noncritical chill status_report.robot -# - store_test_results: -# path: ~/projects/tests/results/ -# - store_artifacts: -# path: ~/projects/tests/results/ - -# ## Nightly builds example -# workflows: -# version: 2 -# nightly: -# triggers: -# - schedule: -# cron: "0 21 * * *" -# filters: -# branches: -# only: -# - feature/NUM-985-nightly-builds -# jobs: -# - build-ehrbase -# - run-SDK-integration-tests diff --git a/.circleci/fail_if_not_snapshot.sh b/.circleci/fail_if_not_snapshot.sh deleted file mode 100644 index 032a51945..000000000 --- a/.circleci/fail_if_not_snapshot.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd "$DIR" -VERSION=$(./verify_and_return_version.sh) -echo "Version is $VERSION" -if [[ $VERSION =~ -SNAPSHOT$ ]]; then - echo "Snapshot version confirmed" - exit 0 -else - echo "Error: not a snapshot version" - exit 1 -fi \ No newline at end of file diff --git a/.circleci/fail_if_snapshot.sh b/.circleci/fail_if_snapshot.sh deleted file mode 100644 index 2b31a5df0..000000000 --- a/.circleci/fail_if_snapshot.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd "$DIR" -VERSION=$(./verify_and_return_version.sh) -echo "Version is $VERSION" -if [[ $VERSION =~ -SNAPSHOT$ ]]; then - echo "Error: snapshot version" - exit 1 -else - echo "release version confirmed" - exit 0 -fi \ No newline at end of file diff --git a/.circleci/file_repo_content.zip b/.circleci/file_repo_content.zip deleted file mode 100644 index 975edfb41..000000000 Binary files a/.circleci/file_repo_content.zip and /dev/null differ diff --git a/.circleci/get_target_branch.sh b/.circleci/get_target_branch.sh deleted file mode 100644 index 8f2716e0e..000000000 --- a/.circleci/get_target_branch.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -case $CIRCLE_BRANCH in - feature*) - echo "develop" - exit 0 - ;; - release* | hotfix*) - echo "master" - exit 0 - ;; - *) - exit 0 - ;; -esac \ No newline at end of file diff --git a/.circleci/settings.xml b/.circleci/settings.xml deleted file mode 100644 index 23089527f..000000000 --- a/.circleci/settings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - ossrh - ${env.OSSRH_LOGIN} - ${env.OSSRH_PASSWORD} - - - diff --git a/.circleci/verify_and_return_version.sh b/.circleci/verify_and_return_version.sh deleted file mode 100644 index b5848a782..000000000 --- a/.circleci/verify_and_return_version.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd "$DIR/.." -VERSION=$(mvn org.apache.maven.plugins:maven-help-plugin:3.1.0:evaluate -Dexpression=project.version -q -DforceStdout) -if [[ $VERSION != "" ]]; then - echo $VERSION - exit 0 -else - exit 1 -fi \ No newline at end of file diff --git a/.circleciignore b/.circleciignore deleted file mode 100644 index bdb485457..000000000 --- a/.circleciignore +++ /dev/null @@ -1,5 +0,0 @@ -CHANGELOG.md -LICENSE.md -README.md -Notice.md -logs/* \ No newline at end of file diff --git a/.docker_scripts/docker-entrypoint.sh b/.docker_scripts/docker-entrypoint.sh deleted file mode 100644 index a4e35bbaf..000000000 --- a/.docker_scripts/docker-entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo -echo "EHRBASE_VERSION: $(cat ehrbase_version)" -java -Dspring.profiles.active=docker -jar ehrbase.jar diff --git a/.dockerignore b/.dockerignore index 8c0138049..583851320 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,9 @@ -.git -.gitattributes -.gitignore -.circleci -.circleciignore -.dockerignore -.pgdata -CHANGELOG.md -LICENSE.md -Notice.md -README.md -sonar-project.properties -tests +# Ignore everything +* + +# Allow files and directories +!application/target/ehrbase.jar +!docker-entrypoint.sh +!tests/docker-int-test-entrypoint.sh +!pom.xml +!**/pom.xml diff --git a/.env.ehrbase b/.env.ehrbase index ad6f8fa19..5b2891a42 100644 --- a/.env.ehrbase +++ b/.env.ehrbase @@ -1,12 +1,10 @@ SERVER_NODENAME=local.ehrbase.org -SECURITY_AUTHTYPE=BASIC SECURITY_AUTHUSER=ehrbase-user SECURITY_AUTHPASSWORD=SuperSecretPassword SECURITY_AUTHADMINUSER=ehrbase-admin SECURITY_AUTHADMINPASSWORD=EvenMoreSecretPassword SECURITY_OAUTH2USERROLE=USER SECURITY_OAUTH2ADMINROLE=ADMIN -SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI= MANAGEMENT_ENDPOINTS_WEB_EXPOSURE=env,health,info,metrics,prometheus MANAGEMENT_ENDPOINTS_WEB_BASEPATH=/management MANAGEMENT_ENDPOINT_ENV_ENABLED=false diff --git a/.github/workflows/build-multiarch-image-latest.yml b/.github/workflows/build-multiarch-image-latest.yml deleted file mode 100644 index 81b0a7f6a..000000000 --- a/.github/workflows/build-multiarch-image-latest.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Build & Deploy Docker Image (latest) - -on: - push: - branches: - - 'master' - paths-ignore: - - '**/*.md' - - 'doc/**' - - 'tests/**' - -jobs: - build-docker: - runs-on: ubuntu-20.04 - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push (AMD64) - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64 - push: true - tags: ehrbase/ehrbase:latest-amd64 - - - name: Build and push (ARM64) - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: ehrbase/ehrbase:latest-arm64 - - - - name: Create and push MultiArch Manifest - run: | - docker buildx imagetools create \ - ehrbase/ehrbase:latest-arm64 \ - ehrbase/ehrbase:latest-amd64 \ - -t ehrbase/ehrbase:latest - docker pull ehrbase/ehrbase:latest - #docker manifest inspect currently fails (the multiarch manifest is pushed successfully though) - #- - # name: Inspect MultiArch Manifest - # run: docker manifest inspect ehrbase/ehrbase:latest - - - - - -# STEPS FOR LOCAL REPRODUCTION -# ============================ -# provides build runtimes for addition platforms -# > docker run --privileged --rm tonistiigi/binfmt --install all -# -# creates a 'docker-container' driver -# which allows building for multiple platforms -# > docker buildx create --use --name mybuild -# -# shows build Driver and available target platforms -# > docker buildx inspect mybuild -# -# builds image for specific platform -# and pushes it to docker-hub -# > docker buildx build --push --platform=linux/arm64 -t ehrbase/ehrbase:next-arm . -# > docker buildx build --push --platform=linux/amd64 -t ehrbase/ehrbase:next-amd . -# -# creates multiarch manifest from given images -# and pushes it to docker-hub -# > docker buildx imagetools create ehrbase/ehrbase:next-arm ehrbase/ehrbase:next-amd -t ehrbase/ehrbase:next -# -# inspects created mulitarch image -# > docker manifest inspect ehrbase/ehrbase:next diff --git a/.github/workflows/build-multiarch-image-next.yml b/.github/workflows/build-multiarch-image-next.yml deleted file mode 100644 index aa174566b..000000000 --- a/.github/workflows/build-multiarch-image-next.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Build & Deploy Docker Image (next) - -on: - push: - branches: - - 'develop' - paths-ignore: - - '**/*.md' - - 'doc/**' - - 'tests/**' - repository_dispatch: - types: [ build-ehrbase-next ] - workflow_dispatch: - inputs: - tag: - description: tag for the image - required: true -jobs: - build-docker: - runs-on: ubuntu-20.04 - steps: - - name: Calculate tag - run: | - if [ -z "${{ github.event.inputs.tag }}" ] - then - v='next' - else - v=${{ github.event.inputs.tag }} - fi - echo "build tag ${v}" - # Set as Environment for all further steps - echo "TAG=${v}" >> $GITHUB_ENV - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push (AMD64) - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64 - push: true - tags: ehrbase/ehrbase:${{env.TAG}}-amd64 - - - name: Build and push (ARM64) - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: ehrbase/ehrbase:${{env.TAG}}-arm64 - - - name: Create and push MultiArch Manifest - run: | - - docker buildx imagetools create \ - ehrbase/ehrbase:${{env.TAG}}-arm64 \ - ehrbase/ehrbase:${{env.TAG}}-amd64 \ - -t ehrbase/ehrbase:${{env.TAG}} - docker pull ehrbase/ehrbase:${{env.TAG}} -#docker manifest inspect currently fails (the multiarch manifest is pushed successfully though) -# - name: Inspect MultiArch Manifest -# run: docker manifest inspect ehrbase/ehrbase:${{env.TAG}} - - - - - -# STEPS FOR LOCAL REPRODUCTION -# ============================ -# provides build runtimes for addition platforms -# > docker run --privileged --rm tonistiigi/binfmt --install all -# -# creates a 'docker-container' driver -# which allows building for multiple platforms -# > docker buildx create --use --name mybuild -# -# shows build Driver and available target platforms -# > docker buildx inspect mybuild -# -# builds image for specific platform -# and pushes it to docker-hub -# > docker buildx build --push --platform=linux/arm64 -t ehrbase/ehrbase:next-arm . -# > docker buildx build --push --platform=linux/amd64 -t ehrbase/ehrbase:next-amd . -# -# creates multiarch manifest from given images -# and pushes it to docker-hub -# > docker buildx imagetools create ehrbase/ehrbase:next-arm ehrbase/ehrbase:next-amd -t ehrbase/ehrbase:next -# -# inspects created mulitarch image -# > docker manifest inspect ehrbase/ehrbase:next diff --git a/.github/workflows/build-multiarch-image-tag.yml b/.github/workflows/build-multiarch-image-tag.yml deleted file mode 100644 index 97af6f8b8..000000000 --- a/.github/workflows/build-multiarch-image-tag.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Build & Deploy Docker Image (version-tag) - -on: - push: - branches: - - 'release/**' - paths-ignore: - - '**/*.md' - - 'doc/**' - - 'tests/**' - -jobs: - build-docker: - runs-on: ubuntu-20.04 - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Create TAG ENV from version of release Branch - run: | - echo $GITHUB_REF - echo "${GITHUB_REF#refs/heads/}" - BRANCH=$(echo "${GITHUB_REF#refs/heads/}") - TAG="$(echo $BRANCH | awk -F'/v' '{print $2;}')" - echo $TAG - echo "TAG=$TAG" >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push (AMD64) - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64 - push: true - tags: ehrbase/ehrbase:tag-amd64 - - - name: Build and push (ARM64) - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: ehrbase/ehrbase:tag-arm64 - - - name: Create and push MultiArch Manifest - run: | - #BRANCH=$(echo "${GITHUB_REF#refs/heads/}") - #TAG="$(echo $BRANCH | awk -F'/v' '{print $2;}')" - docker buildx imagetools create \ - ehrbase/ehrbase:tag-arm64 \ - ehrbase/ehrbase:tag-amd64 \ - -t ehrbase/ehrbase:${{ env.TAG }} - docker pull ehrbase/ehrbase:${{ env.TAG }} - #docker manifest inspect currently fails (the multiarch manifest is pushed successfully though) - #- - # name: Inspect MultiArch Manifest - # run: docker manifest inspect ehrbase/ehrbase:${{ env.TAG }} - - - - - -# STEPS FOR LOCAL REPRODUCTION -# ============================ -# provides build runtimes for addition platforms -# > docker run --privileged --rm tonistiigi/binfmt --install all -# -# creates a 'docker-container' driver -# which allows building for multiple platforms -# > docker buildx create --use --name mybuild -# -# shows build Driver and available target platforms -# > docker buildx inspect mybuild -# -# builds image for specific platform -# and pushes it to docker-hub -# > docker buildx build --push --platform=linux/arm64 -t ehrbase/ehrbase:next-arm . -# > docker buildx build --push --platform=linux/amd64 -t ehrbase/ehrbase:next-amd . -# -# creates multiarch manifest from given images -# and pushes it to docker-hub -# > docker buildx imagetools create ehrbase/ehrbase:next-arm ehrbase/ehrbase:next-amd -t ehrbase/ehrbase:next -# -# inspects created mulitarch image -# > docker manifest inspect ehrbase/ehrbase:next diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 256b09de6..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: publish maven - -on: - push: - branches: - - develop - - master - paths-ignore: - - '.circleci/**' - - '.docker_scripts/**' - - '.github/**' - - 'doc/**' - - 'tests/**' - - '**/*.md' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - services: - ehrbase-db: - image: ehrbase/ehrbase-postgres:13.4.v2 - ports: - - 5432:5432 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - EHRBASE_USER_ADMIN: ehrbase - EHRBASE_PASSWORD_ADMIN: ehrbase - EHRBASE_USER: ehrbase_restricted - EHRBASE_PASSWORD: ehrbase_restricted - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' - - - name: Build with Maven - run: mvn -B verify - - - name: Setup Maven Central - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' - server-id: ossrh - server-username: OSSRH_USERNAME - server-password: OSSRH_TOKEN - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-passphrase: GPG_PASSPHRASE - - - name: Publish to Maven Central - run: mvn -B deploy -P release -DskipTests - env: - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 000000000..53bdb9b4f --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,648 @@ +name: "Build & Test" + +# we have multiple workflows - this helps to distinguish for them +run-name: "${{ github.event.pull_request.title && github.event.pull_request.title || github.ref_name }} - Build & Test" + +on: + push: + branches: [ master, develop, release/* ] + pull_request: + branches: [ develop ] + workflow_dispatch: + +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: 'temurin' + UPLOAD_PERF_HTML_REPORTS: true + +jobs: + + # + # Performs maven build and check as well as junit test result collection. Finally, creates the ehrbase docker image + # and saves it, as an archive, for later usage. + # + build-maven: + name: Build-Maven + runs-on: ubuntu-latest + outputs: + # Map the step outputs to job outputs + ehrbase-version: ${{ steps.get_version.outputs.ehrbase-version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup - Java 21 + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + cache: 'maven' + + - name: Setup - Dependency Cache + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: deps-${{ runner.os }}-m2-${{ github.head_ref }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + deps-${{ runner.os }}-m2-${{ github.head_ref }}- + deps-${{ runner.os }}-m2- + deps-${{ runner.os }}- + deps- + + - name: Maven - Verify and Package + run: mvn --batch-mode --update-snapshots --activate-profiles full -Dmaven.test.failure.ignore=true verify package + + - name: Maven - Get Version + id: "get_version" + run: | + # evaluate project version + version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "EHRbase version [$version]" + echo "ehrbase-version=${version}" >> $GITHUB_OUTPUT + + - name: Upload - Jar + uses: actions/upload-artifact@v4 + with: + name: ehrbase-jar + path: ./application/target/ehrbase.jar + if-no-files-found: error + retention-days: 1 + + # Upload created class files that are needed for the merged jacoco coverage in a later step + - name: Upload - Class Files + if: always() + uses: actions/upload-artifact@v4 + with: + name: java-class-files + path: "**/target/classes/**/*.class" + if-no-files-found: error + + - name: Upload - Jacoco Coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-coverage-partial-build-maven + path: "**/target/jacoco*.exec" + if-no-files-found: error + + - name: Upload - Junit reports + uses: actions/upload-artifact@v4 # upload test results + if: success() || failure() # run this step even if previous step failed + with: + name: junit-test-results + path: '**/target/surefire-reports/*.xml' + + # + # Performs docker build and test images build. + # + docker-build-test-image: + name: Docker Build And Test-Images + runs-on: ubuntu-latest + needs: [ + build-maven + ] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download - Jar + uses: actions/download-artifact@v4 + with: + name: ehrbase-jar + path: ./application/target/ + + - name: Docker - Build Base Image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + load: true + tags: ehrbase/ehrbase:build + + - name: Docker - Build Test Image + run: | + docker build \ + --tag ehrbase/ehrbase:test \ + --build-arg EHRBASE_IMAGE=ehrbase/ehrbase:build \ + --file tests/DockerfileTest . + + - name: Docker - Save Images + run: | + docker save --output ${{ runner.temp }}/ehrbase-test.tar ehrbase/ehrbase:test + docker save --output ${{ runner.temp }}/ehrbase-build.tar ehrbase/ehrbase:build + + - name: Upload - Test Image + uses: actions/upload-artifact@v4 + with: + name: ehrbase-image-test + path: ${{ runner.temp }}/ehrbase-test.tar + if-no-files-found: error + retention-days: 1 + + - name: Upload - Build Image + uses: actions/upload-artifact@v4 + with: + name: ehrbase-image-build + path: ${{ runner.temp }}/ehrbase-build.tar + if-no-files-found: error + retention-days: 1 + + # + # Uses the ehrbase docker image from [build] to run the robot integrations against it. + # + integration-test-server: + runs-on: ubuntu-latest + needs: [ + docker-build-test-image + ] + strategy: + fail-fast: false # ensure all tests run + matrix: + test-suite: [ + # Swagger EHRBase API endpoints checks + { path: 'SWAGGER_TESTS', name: 'SWAGGER_TESTS', tags: 'SWAGGER_EHRBASE', env: { 'AUTH_TYPE': 'NONE' } }, + # Basic and OUATH authorization type checks + { path: 'AUTH_TYPE_TESTS', name: 'BASIC_AUTH_TYPE_TESTS', tags: 'AUTH_TYPE_TESTS_BASIC', env: { 'AUTH_TYPE': 'BASIC' } }, + { path: 'AUTH_TYPE_TESTS', name: 'OAUTH_AUTH_TYPE_TESTS', tags: 'AUTH_TYPE_TESTS_OAUTH', env: { 'AUTH_TYPE': 'OAUTH' } }, + # sanity checks + { path: 'SANITY_TESTS', name: 'SANITY', tags: 'Sanity', env: { 'AUTH_TYPE': 'NONE' } }, + # rest/openehr/v1/definition + { path: 'TEMPLATE_TESTS', name: 'TEMPLATE', tags: 'Template', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'STORED_QUERY_TESTS', name: 'STORED_QUERY', tags: 'stored_query', suite: 'TEST', env: { 'AUTH_TYPE': 'NONE' } }, + ## rest/openehr/v1/ehr + { path: 'EHR_SERVICE_TESTS', name: 'EHR_SERVICE', tags: 'EHR_SERVICE', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'EHR_STATUS_TESTS', name: 'EHR_STATUS', tags: 'EHR_STATUS', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'DIRECTORY_TESTS', name: 'DIRECTORY', tags: 'directory', env: { 'AUTH_TYPE': 'NONE' } }, + ## rest/openehr/v1/ehr/{ehr_id}/contribution + { path: 'CONTRIBUTION_TESTS', name: 'CONTRIBUTION', tags: 'CONTRIBUTION', env: { 'AUTH_TYPE': 'NONE' } }, + ## rest/openehr/v1/ehr/{ehr_id}/composition + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_CREATE_1', tags: 'compositionANDcomposition_create_1', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_CREATE_2', tags: 'compositionANDcomposition_create_2', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_CREATE_3', tags: 'compositionANDcomposition_create_3', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_CREATE_4', tags: 'compositionANDcomposition_create_4', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_CREATE_5', tags: 'compositionANDcomposition_create_5', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_GET', tags: 'compositionANDcomposition_get', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_UPDATE', tags: 'compositionANDcomposition_update', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_DELETE', tags: 'compositionANDcomposition_delete', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_GET_VERSIONED', tags: 'compositionANDcomposition_get_versioned', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_VALIDATION', tags: 'COMPOSITION_validation', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_HEADERS_CHECKS', tags: 'HeadersChecks', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_ISM_TRANSITIONS', tags: 'compositionANDcomposition_ism_transitions', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'COMPOSITION_TESTS', name: 'COMPOSITION_WITH_DIFFERENT_TIME_ZONES', tags: 'COMPOSITION_dtz', env: { 'AUTH_TYPE': 'NONE' } }, + ## rest/openehr/v1/query/aql - could be split into individual sub-suite + { path: 'AQL_TESTS', name: 'AQL', tags: 'AQL_TESTS_PACKAGE', env: { 'AUTH_TYPE': 'NONE' } }, + ## rest/rest/ecis + { path: 'EHRSCAPE_TESTS', name: 'EHRSCAPE', tags: 'EhrScapeTag', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'ADMIN_TESTS', name: 'ADMIN', tags: 'ADMIN', env: { 'AUTH_TYPE': 'NONE' } }, + { path: 'TAGS_TESTS', name: 'TAGS_TESTS', tags: 'TAGS_SUITES', env: { 'AUTH_TYPE': 'NONE' } }, + # TODO Still missing + # FHIR_TERMINOLOGY + # SECURITY_TESTS + ] + name: Robot (${{ matrix.test-suite.name }}) + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: | + tests + .env.ehrbase + docker-compose.yml + + - name: Download - Test Image + uses: actions/download-artifact@v4 + with: + name: ehrbase-image-test + path: ${{ runner.temp }} + + - name: Docker - Load Image + run: docker load --input ${{ runner.temp }}/ehrbase-test.tar + + # image used by the docker-compose-int-test.yml + - name: Docker Compose - Setup env + run: | + echo "EHRBASE_IMAGE=ehrbase/ehrbase:test" >> $GITHUB_ENV + echo "JACOCO_RESULT_PATH=/app/coverage/jacoco-${{ matrix.test-suite.path }}-${{ matrix.test-suite.name }}.exec" >> $GITHUB_ENV + + - name: Modify .env.ehrbase file + run: | + echo "SECURITY_AUTHTYPE=${{ matrix.test-suite.env['AUTH_TYPE'] }}" >> .env.ehrbase + if [ "${{ matrix.test-suite.env['AUTH_TYPE'] }}" == "OAUTH" ]; then + echo "SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI=http://keycloak:8080/auth/realms/ehrbase" >> .env.ehrbase + fi + echo "EHRBASE_REST_EXPERIMENTAL_TAGS_ENABLED=true" >> .env.ehrbase + cat .env.ehrbase + + - name: Docker Compose - Starting + run: | + docker compose -f docker-compose.yml -f tests/docker-compose-int-test.yml up -d + + - name: Wait for Keycloak to be ready + run: | + if [ "${{ matrix.test-suite.env['AUTH_TYPE'] }}" == "OAUTH" ]; then + until curl -s http://localhost:8081/auth/realms/ehrbase; do + echo "Waiting for Keycloak..." + sleep 30 + done + #curl -s http://localhost:8081/auth/realms/ehrbase/.well-known/openid-configuration + fi + docker compose -f docker-compose.yml -f tests/docker-compose-int-test.yml ps + + - name: Run - Robot Test-Suite + run: | + docker compose -f docker-compose.yml -f tests/docker-compose-int-test.yml run --remove-orphans --rm ehrbase-integration-tests runRobotTest \ + --name ${{ matrix.test-suite.name }} \ + --path ${{ matrix.test-suite.path }} \ + --tags ${{ matrix.test-suite.tags }} \ + --env ${{ matrix.test-suite.env['AUTH_TYPE'] }} + + - name: Docker Compose - Logs ehrbase + if: always() + run: docker compose -f docker-compose.yml -f tests/docker-compose-int-test.yml logs ehrbase + + - name: Docker Compose - Logs keycloak + if: always() + run: docker compose -f docker-compose.yml -f tests/docker-compose-int-test.yml logs keycloak + + - name: Docker Compose - Stopping + if: always() + run: | + docker compose -f docker-compose.yml -f tests/docker-compose-int-test.yml down --remove-orphans + docker compose -f docker-compose.yml -f tests/docker-compose-int-test.yml rm --force --volumes + + - name: Upload - Jacoco Coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-coverage-partial-robot-${{ matrix.test-suite.path }}-${{ matrix.test-suite.name }} + path: ./tests/coverage/jacoco*.exec + if-no-files-found: error + + - name: Upload - Robot results + if: always() + uses: actions/upload-artifact@v4 + with: + name: robot-result-${{ matrix.test-suite.name }} + path: ./tests/results/${{ matrix.test-suite.name }}/output.xml + if-no-files-found: error + + # + # Uses the ehrbase docker image from [build] to run the performance tests against it. + # + performance-test-run: + runs-on: ubuntu-latest + needs: [ + docker-build-test-image + ] + name: Perf (${{ matrix.test-plan.name }}) + strategy: + fail-fast: false # ensure all tests run + matrix: + test-plan: [ + { name: 'ehrbase_no_auth_scenario', env: { 'AUTH_TYPE': 'NONE' } }, + { name: 'ehrbase_basic_auth_scenario', env: { 'AUTH_TYPE': 'BASIC' } }, + { name: 'ehrbase_oauth_auth_scenario', env: { 'AUTH_TYPE': 'OAUTH' } }, + ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: | + tests + .env.ehrbase + docker-compose.yml + + - name: Download - Build Image + uses: actions/download-artifact@v4 + with: + name: ehrbase-image-build + path: ${{ runner.temp }} + + - name: Docker - Load Image + run: docker load --input ${{ runner.temp }}/ehrbase-build.tar + + - name: Docker Compose - Setup env + run: | + echo "EHRBASE_IMAGE=ehrbase/ehrbase:build" >> $GITHUB_ENV + + - name: Modify .env.ehrbase file + run: | + echo "SECURITY_AUTHTYPE=${{ matrix.test-plan.env['AUTH_TYPE'] }}" >> .env.ehrbase + if [ "${{ matrix.test-plan.env['AUTH_TYPE'] }}" == "OAUTH" ]; then + echo "SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI=http://keycloak:8080/auth/realms/ehrbase" >> .env.ehrbase + fi + cat .env.ehrbase + + - name: Docker Compose - Starting + run: | + docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml up -d + + - name: Wait for Keycloak to be ready + run: | + if [ "${{ matrix.test-plan.env['AUTH_TYPE'] }}" == "OAUTH" ]; then + until curl -s http://localhost:8081/auth/realms/ehrbase; do + echo "Waiting for Keycloak..." + sleep 20 + done + fi + docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml ps + + - name: Wait for JMeter to be ready + run: | + until docker compose exec jmeter ls / > /dev/null 2>&1; do + echo "Waiting for JMeter service to be ready..." + sleep 10 + done + + - name: Show JMeter Test Plans + run: | + docker compose exec jmeter ls -al /tests + + - name: Run JMeter Tests + env: + TEST_PLAN_PATH: /tests/${{ matrix.test-plan.name }}.jmx + REPORTS_DIR: /reports/${{ matrix.test-plan.name }} + HOST: ehrbase + PORT: 8080 + KEYCLOAK_HOST: keycloak + KEYCLOAK_PORT: 8080 + THREADS: 10 + RAMP_UP: 1 + LOOP_COUNT: 50 + DURATION: 180 + run: | + docker compose exec jmeter mkdir -p /reports/${{ matrix.test-plan.name }} + + # Run the JMeter test + docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml run --rm \ + -v ${{ github.workspace }}/tests/perf:/tests \ + -v ${{ github.workspace }}/reports:/reports \ + jmeter -n -t $TEST_PLAN_PATH \ + -Jhost=$HOST -Jport=$PORT \ + -Jkeycloak_host=$KEYCLOAK_HOST -Jkeycloak_port=$KEYCLOAK_PORT \ + -Jthreads=$THREADS -JrampUp=$RAMP_UP -JloopCount=$LOOP_COUNT -Jduration=$DURATION \ + -l /reports/${{ matrix.test-plan.name }}/result.jtl \ + -e -o /reports/${{ matrix.test-plan.name }}/html/ \ + -Djava.awt.headless=true + + - name: Upload Results + uses: actions/upload-artifact@v4 + with: + name: jmeter-result-${{ matrix.test-plan.name }} + path: ./reports/${{ matrix.test-plan.name }}/result.jtl + if-no-files-found: error + + - name: Upload HTML Reports + if: env.UPLOAD_PERF_HTML_REPORTS == 'true' + uses: actions/upload-artifact@v4 + with: + name: jmeter-report-${{ matrix.test-plan.name }} + path: reports/${{ matrix.test-plan.name }}/html + if-no-files-found: error + + - name: Docker Compose - Logs ehrbase + if: always() + run: docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml logs ehrbase + + - name: Docker Compose - Logs keycloak + if: always() + run: docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml logs keycloak + + - name: Docker Compose - jmeter + if: always() + run: docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml logs jmeter + + - name: Docker Compose - Stopping + if: always() + run: | + docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml down --remove-orphans + docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml rm --force --volumes + + # + # Collect all JMeter results from [performance-test-run], store them in single folder and upload it as artifact. + # + performance-tests-collect: + name: JMeter-Collect + if: ${{ always() }} + needs: [ + performance-test-run + ] + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Download - JMeter results + uses: actions/download-artifact@v4 + with: + pattern: jmeter-result-* + path: ./tests/jmeter-results/ + + - name: Download - JMeter reports + if: env.UPLOAD_PERF_HTML_REPORTS == 'true' + uses: actions/download-artifact@v4 + with: + pattern: jmeter-report-* + path: ./tests/jmeter-reports/ + + - name: Archive and Upload - JMeter results + if: always() + uses: actions/upload-artifact@v4 + with: + name: jmeter-results-final + path: ./tests/jmeter-results + + - name: Archive and Upload - JMeter reports + if: env.UPLOAD_PERF_HTML_REPORTS == 'true' + uses: actions/upload-artifact@v4 + with: + name: jmeter-reports-final + path: ./tests/jmeter-reports + + - name: Cleanup - Test Folder + if: always() + run: | + rm -rf ./tests/jmeter-results | true + rm -rf ./tests/jmeter-reports | true + + # + # Collect all Robot result from [integration-test-server] and generated the final report. + # + robot-collect: + name: Robot-Collect + if: ${{ always() }} + needs: [ + integration-test-server + ] + runs-on: ubuntu-latest + # allow to write comments to the issue + permissions: + issues: write + pull-requests: write + steps: + - name: Download - Robot results + uses: actions/download-artifact@v4 + with: + pattern: robot-result-* + path: ./tests/results/ + + - name: Generate - Robot Tests-Report + run: | + docker run \ + -v ./tests/results:/integration-tests/results \ + -v ./tests/report:/integration-tests/report \ + ehrbase/integration-tests:latest collectRebotResults + + - name: Archive - Robot Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: robot-report-final + path: ./tests/report + + - name: Cleanup - Test Folder + if: always() + run: | + rm -rf ./tests/result | true + rm -rf ./tests/report | true + + # + # Collect all Robot result from [integration-test-server] and generated the final report. + # + coverage-collect: + name: Jacoco-Collect + if: ${{ always() }} + needs: [ + integration-test-server + ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download - Class files + uses: actions/download-artifact@v4 + with: + name: java-class-files + path: ./ + + - name: Download - Robot results + uses: actions/download-artifact@v4 + with: + pattern: jacoco-coverage-partial-* + path: ./tests/coverage + + - name: Docker - Build Jacoco-CLI + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_NO_SUMMARY: true + with: + context: . + file: tests/DockerfileJacocoCLI + load: true + tags: jacoco-cli:local + + # create merged.exec + - name: Jacoco - Merge + run: | + cd ./tests/coverage + docker run --rm -v ./:/workspace -w /workspace --pull never jacoco-cli:local merge $(find . -type f -name '*.exec' | tr '\n' ' ') --destfile jacoco-merged.exec + + # it is easier to copy over .java and .class and pass them later as a bundle to the jacoco report generation + - name: Collect - Sources & Classes + run: | + mkdir -p ./tests/coverage/ehrbase + mkdir -p ./tests/coverage/ehrbase/src + find . -type d -path '*/src/main/java' | xargs -0 sh -c 'cp -prnv $0 ./tests/coverage/ehrbase/src' | true + find . -type d -path '*/generated-sources' | xargs -0 sh -c 'cp -prnv $0 ./tests/coverage/ehrbase/src' | true + find . -type d -path '*/target/classes' | xargs -0 sh -c 'cp -prnv $0 ./tests/coverage/ehrbase' | true + + # create final jacoco report + - name: Jacoco - Report + run: | + cd ./tests/coverage + mkdir -p jacoco-report-final + docker run --rm -v $(pwd)/:/workspace -w /workspace --pull never jacoco-cli:local report jacoco-merged.exec --classfiles ehrbase/classes/ --sourcefiles ehrbase/src/java --sourcefiles ehrbase/src/generated-sources --encoding utf-8 --name Merged --html ./jacoco-report-final --xml ./jacoco-report-final/jacoco.xml + + - name: Archive - Jacoco Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-report-final + path: ./tests/coverage/jacoco-report-final + + # + # Uses the ehrbase docker image from [build] to run the robot integrations against it. + # + integration-test-cli: + name: IT + uses: ./.github/workflows/job-integration-test-cli.yml + secrets: inherit + needs: [ + docker-build-test-image + ] + with: + ehrbase-image-tag: ehrbase/ehrbase:test + ehrbase-image-artifact: ehrbase-image-test + + # + # Build and push docker image + # + docker-build-push: + name: Docker + uses: ./.github/workflows/job-docker-build-push.yml + secrets: inherit + # ignore dependabot here. + if: ${{ github.actor != 'dependabot[bot]' }} + needs: [ + build-maven, # needed to obtain ehrbase-version from + integration-test-server, + integration-test-cli + ] + with: + ehrbase-version: ${{ needs.build-maven.outputs.ehrbase-version }} + ehrbase-jar-artifact: ehrbase-jar + + # + # Maven publish + # + maven-publish: + name: Maven + uses: ./.github/workflows/job-maven-publish.yml + secrets: inherit + # ignore dependabot here. + if: ${{ github.actor != 'dependabot[bot]' }} + needs: [ + build-maven, # only to have them in the same UI group as docker-build-push + integration-test-server + ] + + # + # Cleanup serialized oci image as well as intermediate robot results + # + cleanup: + name: Cleanup + if: ${{ always() }} + needs: [ + coverage-collect, + performance-tests-collect, + robot-collect, + docker-build-push, + maven-publish + ] + runs-on: ubuntu-latest + steps: + - name: Delete - Temp Artifacts + uses: geekyeggo/delete-artifact@v5 + with: + name: | + java-class-files + ehrbase-jar + ehrbase-image-* + robot-result-* + jmeter-result-* + jmeter-report-* + jacoco-coverage-* + failOnError: false diff --git a/.github/workflows/check-codeql.yml b/.github/workflows/check-codeql.yml new file mode 100644 index 000000000..32a90c73c --- /dev/null +++ b/.github/workflows/check-codeql.yml @@ -0,0 +1,87 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +# we have multiple workflows - this helps to distinguish for them +run-name: "${{ github.event.pull_request.title && github.event.pull_request.title || github.ref_name }} - CodeQL" + +on: + push: + branches: [ "develop" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "develop" ] + schedule: + - cron: '39 12 * * 6' + +jobs: + codeql: + name: CodeQL + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + java: [ '21' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/check-style.yml b/.github/workflows/check-style.yml new file mode 100644 index 000000000..d3528b85a --- /dev/null +++ b/.github/workflows/check-style.yml @@ -0,0 +1,33 @@ +name: "Codestyle" + +# we have multiple workflows - this helps to distinguish for them +run-name: "${{ github.event.pull_request.title && github.event.pull_request.title || github.ref_name }} - Codestyle" + +on: + push: + branches: [ develop, release/* ] + workflow_dispatch: + pull_request: + branches: [ develop ] + +# +# Style-check it a dedicated workflow. This allows us to open a PR, run all tests and fix styling issue later ;). +# +jobs: + spotless: + name: Spotless-Check + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 21 + cache: 'maven' + + - name: Spotless + run: mvn spotless:check diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 1b277311d..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,96 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "develop" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "develop" ] - schedule: - - cron: '39 12 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - services: - ehrbase-db: - image: ehrbase/ehrbase-postgres:13.4.v2 - ports: - - 5432:5432 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - EHRBASE_USER_ADMIN: ehrbase - EHRBASE_PASSWORD_ADMIN: ehrbase - EHRBASE_USER: ehrbase_restricted - EHRBASE_PASSWORD: ehrbase_restricted - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/collect-junit-results.yml b/.github/workflows/collect-junit-results.yml new file mode 100644 index 000000000..bca25c0f5 --- /dev/null +++ b/.github/workflows/collect-junit-results.yml @@ -0,0 +1,55 @@ +name: "Collect JUnit Results" + +# we have multiple workflows - this helps to distinguish for them +run-name: "${{ github.event.workflow_run.head_branch }} - Collect JUnit Results" + +on: + workflow_run: + workflows: ["Build & Test"] # runs after build and test workflow + types: + - completed + +permissions: + contents: read + actions: read + checks: write + +# +# see https://github.com/dorny/test-reporter?tab=readme-ov-file#recommended-setup-for-public-repositories +# +jobs: + # + # Collect Junit reports generated by build_and_test + # + collect-junit-reports: + runs-on: ubuntu-latest + steps: + + # checkout because dorny/test-reporter needs to read tracked files + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} + ref: ${{ github.event.workflow_run.head_branch }} + + # Download report separately because the dorny/test-reporter is not compatible with + # actions/[upload/download]-artifact@v4 https://github.com/dorny/test-reporter/issues/363 + - name: Download - Unit Reports + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.BOT_ACCESS_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} # download artifacts from build and test workflow + pattern: junit-test-results # uses artifact of build and test workflow + merge-multiple: true + path: ./ + + - name: Collect - JUnit Reports + uses: dorny/test-reporter@v1 + # Dependabot has not enough rights to add the report to the run. + if: ${{ github.actor != 'dependabot[bot]' }} + with: + name: Unit Tests + path: '**/target/surefire-reports/*.xml' + reporter: java-junit + fail-on-error: 'true' + fail-on-empty: 'true' diff --git a/.github/workflows/docker-ehrbase-postgres.yml b/.github/workflows/docker-ehrbase-postgres.yml new file mode 100644 index 000000000..24fe388d3 --- /dev/null +++ b/.github/workflows/docker-ehrbase-postgres.yml @@ -0,0 +1,77 @@ +name: "Create Docker ehrbase-postgres" + +on: + # + # Manual dispatched with postgres version and publish options. + # + workflow_dispatch: + inputs: + postgres-version: + description: 'Version of Postgres to build (like: 16.2)' + required: true + default: '16.2' + type: string + push-image: + description: 'Push the resulting image to dockerhub' + required: true + default: false + type: boolean + +jobs: + build-docker-image: + runs-on: ubuntu-latest + + env: + REGISTRY: docker.io + POSTGRES_VERSION: unspecified # assign from workflow input + IMAGE_NAME: ehrbase/ehrbase-v2-postgres + + steps: + - name: Assign Env vars + run: | + echo "POSTGRES_VERSION=${{ inputs.postgres-version }}" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v4 + + # Docker registry login + - name: Login into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + if: ${{ !env.ACT }} # skip for local tests + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Docker metadata extraction - obtain version and labels from here + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + if: ${{ !env.ACT }} # skip for local tests + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # setup qemu for multi arch + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # setup buildx + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Build & Publish image + - name: Build and push Versioned Docker Image + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile_postgres + platforms: linux/amd64,linux/arm64 + push: ${{ inputs.push-image }} + tags: ${{ env.IMAGE_NAME }}:${{ env.POSTGRES_VERSION }} + labels: ${{ steps.meta.outputs.labels }} + build-args: POSTGRES_VERSION=${{ env.POSTGRES_VERSION }} + + - name: Build and push Versioned Docker Image (Summary) + if: ${{ github.ref != 'refs/heads/main' }} + run: | + echo "Image \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.POSTGRES_VERSION }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/jdk-compat-robot.yml b/.github/workflows/jdk-compat-robot.yml deleted file mode 100644 index a42608639..000000000 --- a/.github/workflows/jdk-compat-robot.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Test other DB user credentials - -on: - push: - branches: [ develop ] -# pull_request: -# branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - - env: - DB_URL: jdbc:postgresql://localhost:5432/ehrbase - DB_USER_ADMIN: ehrbase_admin_username - DB_PASS_ADMIN: ehrbase_admin_password - DB_USER: ehrbase_restricted_username - DB_PASS: ehrbase_restricted_password - - strategy: - matrix: - java: [ '17' ] - - services: - ehrbase-db: - image: ehrbase/ehrbase-postgres:13.4.v2 - ports: - - 5432:5432 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - EHRBASE_USER_ADMIN: ${{ env.DB_USER_ADMIN }} - EHRBASE_PASSWORD_ADMIN: ${{ env.DB_PASS_ADMIN }} - EHRBASE_USER: ${{ env.DB_USER }} - EHRBASE_PASSWORD: ${{ env.DB_PASS }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: ${{ matrix.java }} - cache: 'maven' - - - name: Build with Maven - run: mvn -B verify -Ddatabase.user=${{ env.DB_USER_ADMIN }} -Ddatabase.pass=${{ env.DB_PASS_ADMIN }} - - - name: Run ehrbase - run: | - xml_content=$(cat application/pom.xml) - app_version=$(echo "$xml_content" | grep -oP '(?<=).*?(?=)' | awk 'NR==1{print}') - echo "Value from XML: $app_version" - java -jar application/target/application-$app_version.jar \ - --spring.profiles.active=docker \ - --admin-api.active=true \ - --DB_URL=${{ env.DB_URL }} \ - --DB_USER=${{ env.DB_USER }} \ - --DB_PASS=${{ env.DB_PASS }} \ - --DB_USER_ADMIN=${{ env.DB_USER_ADMIN }} \ - --DB_PASS_ADMIN=${{ env.DB_PASS_ADMIN }} \ - --server.nodename=local.ehrbase.org \ - >/var/tmp/log.txt & - sleep 30 - cat /var/tmp/log.txt - - - name: Checkout robot - uses: actions/checkout@v4 - with: - fetch-depth: 0 - repository: ehrbase/integration-tests - ref: main - - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Install Dependencies - run: pip install -r tests/requirements.txt - - - name: Run robot - run: | - cd tests - robot \ - -v NODENAME:local.ehrbase.org \ - --console dotted \ - --flattenkeywords for \ - --flattenkeywords foritem \ - --flattenkeywords name:_resources.* \ - -d results \ - --noncritical not-ready -L TRACE \ - robot/SANITY_TESTS - - - name: Cleanup Robot results folder - if: always() - run: | - rm -vf /home/runner/work/ehrbase/ehrbase/tests/results/*.xml - - - name: Archive code coverage results - if: always() - uses: actions/upload-artifact@v4 - with: - name: robot-result - path: /home/runner/work/ehrbase/ehrbase/tests/results/ - - - name: Archive logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: log - path: /var/tmp/log.txt \ No newline at end of file diff --git a/.github/workflows/jdk-compat.yml b/.github/workflows/jdk-compat.yml deleted file mode 100644 index efe8db011..000000000 --- a/.github/workflows/jdk-compat.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: build - -on: - push: - branches: [ develop ] -# pull_request: -# branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - java: [ '17' ] - - services: - ehrbase-db: - image: ehrbase/ehrbase-postgres:13.4.v2 - ports: - - 5432:5432 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - EHRBASE_USER_ADMIN: ehrbase - EHRBASE_PASSWORD_ADMIN: ehrbase - EHRBASE_USER: ehrbase_restricted - EHRBASE_PASSWORD: ehrbase_restricted - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: ${{ matrix.java }} - cache: 'maven' - - - name: Build with Maven - run: mvn -B verify diff --git a/.github/workflows/job-docker-build-push.yml b/.github/workflows/job-docker-build-push.yml new file mode 100644 index 000000000..2dca8df79 --- /dev/null +++ b/.github/workflows/job-docker-build-push.yml @@ -0,0 +1,73 @@ +name: "Docker Build & Push" + +on: + workflow_call: + inputs: + ehrbase-version: + type: string + description: 'EHRbase version used for tagging' + ehrbase-jar-artifact: + type: string + description: 'Archived ehrbase-jar artifact name' + required: false + +jobs: + + # + # Build and pushes the EHRbase docker image for the given input jar + # + build-and-push: + name: Build-And-Push + runs-on: ubuntu-latest + # Sanity check to ensure docker push only happen on dev/main/tag[v] refs + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }} # || startsWith(github.ref, 'refs/heads/release/') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: | + Dockerfile + + - name: Download - Jar + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.ehrbase-jar-artifact }} + path: ./application/target/ + + # Docker registry login + - name: Login into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # setup qemu for multi arch + - name: Docker - Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Docker - Set up Buildx + uses: docker/setup-buildx-action@v3 + + # Docker metadata extraction - obtain version and labels from here + - name: Docker - Metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ehrbase/ehrbase + tags: | + # refs/heads/develop -> tags: ehrbase/ehrbase:next + type=raw,value=next,enable=${{ github.ref == 'refs/heads/develop' }} + # refs/heads/master -> tags: ehrbase/ehrbase:${version}, ehrbase/ehrbase:latest + type=raw,priority=200,value=${{ inputs.ehrbase-version }},enable=${{ github.ref == 'refs/heads/master' }} + type=raw,priority=100,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + + # build the release multi arch image + - name: Docker - Build & Push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64,linux/arm64 # possible we could add linux/arm/v6,linux/arm/v7 as well? + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=registry diff --git a/.github/workflows/job-integration-test-cli.yml b/.github/workflows/job-integration-test-cli.yml new file mode 100644 index 000000000..d6194f1f8 --- /dev/null +++ b/.github/workflows/job-integration-test-cli.yml @@ -0,0 +1,40 @@ +name: "Integration Test - EHRbase CLI" + +on: + workflow_call: + inputs: + ehrbase-image-tag: + type: string + description: 'Docker image tag name' + required: true + ehrbase-image-artifact: + type: string + description: 'Archived ehrbase docker image artifact name' + required: true + +jobs: + # + # Runs simple CLI integration tests against the EHRbase image + # + integration-test-cli: + name: EHRbase CLI + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Download - Image + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.ehrbase-image-artifact }} + path: ${{ runner.temp }} + + # Docker load login + - name: Docker - Load Image + run: docker load --input ${{ runner.temp }}/ehrbase-test.tar + + # Docker run test + - name: Docker - Test cli help + run: docker run -i --rm ${{ inputs.ehrbase-image-tag }} cli help + + # Docker run test + - name: Docker - Test cli database help + run: docker run -i --rm ${{ inputs.ehrbase-image-tag }} cli database help diff --git a/.github/workflows/job-maven-publish.yml b/.github/workflows/job-maven-publish.yml new file mode 100644 index 000000000..8ac288970 --- /dev/null +++ b/.github/workflows/job-maven-publish.yml @@ -0,0 +1,59 @@ +name: "Maven Publish" + +on: + workflow_call: + # workflow_dispatch: <- is this needed? + +jobs: + + # + # Build and publish jars to maven central + # + maven-publish: + name: Publish + runs-on: ubuntu-latest + # Sanity check to ensure docker push only happen on dev/main/tag[v] refs + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }} # || startsWith(github.ref, 'refs/heads/release/') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup - Java 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: 'temurin' + cache: 'maven' + + - name: Setup - Maven Central + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: 'temurin' + cache: 'maven' + server-id: ossrh + server-username: OSSRH_USERNAME + server-password: OSSRH_TOKEN + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Restore - Dependency Cache + uses: actions/cache/restore@v4 + with: + path: ~/.m2/repository + key: deps-${{ runner.os }}-m2-${{ github.head_ref }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + deps-${{ runner.os }}-m2-${{ github.head_ref }}- + deps-${{ runner.os }}-m2- + deps-${{ runner.os }}- + deps- + fail-on-cache-miss: true # we run only with cached dependencies + + - name: Publish - Maven Central + run: mvn -B deploy -P release -DskipTests + env: + OSSRH_USERNAME: ${{ secrets.S01_OSSRH_USERNAME }} + OSSRH_TOKEN: ${{ secrets.S01_OSSRH_TOKEN }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c490ba2ca..4143f7232 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,49 +1,22 @@ # Create a Release -name: release +name: "Release" on: workflow_dispatch: inputs: version: - description: "optional: version to release" + description: "Version to release, defaults to project.version" required: false jobs: build: runs-on: ubuntu-latest steps: - # Install the sdk main in the local repo so maven version plugin can find them - - name: Checkout SDK main - uses: actions/checkout@v4 - with: - fetch-depth: 0 - repository: ehrbase/openEHR_SDK - ref: master - # This will be used by git in all further steps - # We need a PERSONAL ACCESS TOKEN so pushes trigger other github actions - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - - - name: Setup Java + - name: Setup - Java 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' - - - name: install sdk main - run: mvn install -Dmaven.test.skip=true - # Install the sdk dev in the local repo so maven version plugin can find them - - name: Checkout SDK dev - uses: actions/checkout@v4 - with: - fetch-depth: 0 - repository: ehrbase/openEHR_SDK - ref: develop - # This will be used by git in all further steps - # We need a PERSONAL ACCESS TOKEN so pushes trigger other github actions - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - - - name: install sdk dev - run: mvn install + java-version: '21' - name: Checkout uses: actions/checkout@v4 @@ -51,47 +24,63 @@ jobs: fetch-depth: 0 # This will be used by git in all further steps # We need a PERSONAL ACCESS TOKEN so pushes trigger other github actions - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + token: ${{ secrets.BOT_ACCESS_TOKEN }} + + - name: Configure Git user + run: | + # Config git robot user + git config --global user.email "bot@ehrbase.org" + git config --global user.name "bot" + # + # Uses the input version or read the version from the project pom + # - name: Calculate Release Version run: | if [ -z "${{ github.event.inputs.version }}" ] then - v=$(grep -oPm1 "(?<=)[^<]+" "pom.xml" | sed 's/-SNAPSHOT//') - echo ${{ github.repository }} + version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout | sed 's/-SNAPSHOT//') else - v=${{ github.event.inputs.version }} + version=${{ github.event.inputs.version }} fi - echo "realise version ${v}" + echo "Release version: ${version}" # Set as Environment for all further steps - echo "VERSION=${v}" >> $GITHUB_ENV + echo "VERSION=${version}" >> $GITHUB_ENV + # + # Uses the enforcer plugin to ensure no -SNAPSHOT version are used + # + - name: Enforce no SNAPSHOT used + run: | + mvn -P no-snapshots enforcer:enforce + + # + # Create a new release branch and adjust the release version and changelog. + # - name: Create Release Branch run: | - # Config git - git config --global user.email "bot@ehrbase.org" - git config --global user.name "bot" # create branch git checkout -b release/v${VERSION} # Update version mvn versions:set -DnewVersion=${VERSION} -DprocessAllModules=true - #Update sdk version latest release - mvn versions:update-properties -Dincludes=org.ehrbase.openehr.sdk:* - sdk_version=$( grep -oPm1 "(?<=)[^<]+" "bom/pom.xml") - echo "sdk_version: ${sdk_version}" - #edit changelog - replace="0,/Changed/{s/Changed/Changed \n - Upgrade openEHR_SDK to version ${sdk_version} see https:\/\/github.com\/ehrbase\/openEHR_SDK\/blob\/develop\/CHANGELOG.md/}" - sed -i "${replace}" CHANGELOG.md - replace="s/\[unreleased\]/\[${VERSION}\]/" - sed -i ${replace} CHANGELOG.md - replace="s/...HEAD/\...v${VERSION}/" - sed -i ${replace} CHANGELOG.md + # Update Changelog + sed -i "s/\[unreleased\]/\[${VERSION}\]/" CHANGELOG.md + sed -i "s/...HEAD/\...v${VERSION}/" CHANGELOG.md + + # + # Publish release branch + # + - name: Publish Release Branch + run: | # commit & push git add -A git commit -m "release ${VERSION}: updated version to ${VERSION}" git push -u origin release/v${VERSION} - # wait for status of commit to change from pending - - name: Wait for ci pipeline + + # + # Wait for status of commit to change from pending + # + - name: Wait for CI pipeline run: | STATUS="pending" # Get commit last commit of release branch @@ -99,37 +88,43 @@ jobs: echo "Listen for commit $COMMIT" WAITED="0" # Time between calls - SLEEP_TIME="60" + SLEEP_TIME="15" while [ "$STATUS" == "pending" ] && [ "$WAITED" -le 1800 ] do - sleep ${SLEEP_TIME} - WAITED=$((WAITED+SLEEP_TIME)) - STATUS=$(gh api /repos/${{ github.repository }}/commits/"${COMMIT}"/status -q .state) - echo "status : $STATUS" - echo "waited $WAITED s" + sleep ${SLEEP_TIME} + WAITED=$((WAITED+SLEEP_TIME)) + STATUS=$(gh api /repos/${{ github.repository }}/commits/"${COMMIT}"/status -q .state) + echo "status : $STATUS" + echo "waited $WAITED s" done echo "status : $STATUS" if [ "$STATUS" != "success" ] - then exit 1 + then exit 1 fi env: - GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }} - - name: Merge into Main + # + # In case the CI build was successful - we can merge everything back into the master branch + # + - name: Merge into Master run: | git checkout master git pull git merge --no-ff release/v${VERSION} - git tag -a -m "v${VERSION}" "v${VERSION}" + git tag -a -m "v${VERSION}" "v${VERSION}" git push --follow-tags - - name: Create Release + # + # Create the actual github release for the version using the actual changelog + # + - name: Create Github Release run: | - gh release create "v${VERSION}" -t "v${VERSION}" -F CHANGELOG.md -R ${{ github.repository }} --target master + gh release create "v${VERSION}" -t "v${VERSION}" -F CHANGELOG.md -R ${{ github.repository }} --target master env: - GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }} - - name: Merge into dev + - name: Prepare next dev version run: | # increment minor version and add SNAPSHOT ARRAY_VERSION=( ${VERSION//./ } ) @@ -138,17 +133,16 @@ jobs: echo "next version: $NEXT_VERSION" # update version mvn versions:set -DnewVersion=${NEXT_VERSION} -DprocessAllModules=true - #update sdk to latest snapshot - mvn versions:update-properties -Dincludes=org.ehrbase.openehr.sdk:* -DallowSnapshots=true #edit changelog sed -i '8i ## [unreleased]\n ### Added\n ### Changed \n ### Fixed \n' CHANGELOG.md replace="$ a \[unreleased\]: https:\/\/github.com\/ehrbase\/ehrbase\/compare\/v$VERSION...HEAD" sed -i "${replace}" CHANGELOG.md + + - name: Merge into dev + run: | git add -A - git commit -m " updated version to ${NEXT_VERSION}" + git commit -m " Updated version to ${NEXT_VERSION}" git checkout develop git pull git merge --no-ff release/v${VERSION} git push - - diff --git a/.github/workflows/report-robot-results.yml b/.github/workflows/report-robot-results.yml new file mode 100644 index 000000000..abac719ed --- /dev/null +++ b/.github/workflows/report-robot-results.yml @@ -0,0 +1,47 @@ +name: "Collect Robot Results" + +# we have multiple workflows - this helps to distinguish for them +run-name: "${{ github.event.workflow_run.head_branch }} - Report Robot Results" + +on: + workflow_run: + workflows: ["Build & Test"] # runs after build and test workflow + types: + - completed + +permissions: + contents: write + +# +# see https://github.com/dorny/test-reporter?tab=readme-ov-file#recommended-setup-for-public-repositories +# +jobs: + # + # Collect and upload sonar report with coverage generated by the Build & Test workflow + # + robot-report: + name: Robot-Report + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + steps: + # Download jacoco overall coverage from build & test output + - name: Download - Robot Results + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.BOT_ACCESS_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} # download artifacts from build and test workflow + pattern: robot-report-final # uses artifact of build and test workflow + merge-multiple: true + path: ${{ github.workspace }}/tests/report + + - name: Github - Send Robot Report to PR + # Dependabot has not enough rights to add the report to the PR. + uses: joonvena/robotframework-reporter-action@v2.4 + with: + gh_access_token: ${{ secrets.BOT_ACCESS_TOKEN }} + # using pull_request_id is misleading, under the hood it is the PR number + pull_request_id: ${{ github.event.workflow_run.pull_requests[0].number }} + report_path: ./tests/report + summary: true + only_summary: false + show_passed_tests: false diff --git a/.github/workflows/report-sonar-results.yml b/.github/workflows/report-sonar-results.yml new file mode 100644 index 000000000..d05582962 --- /dev/null +++ b/.github/workflows/report-sonar-results.yml @@ -0,0 +1,78 @@ +name: "Report Sonar Results" + +# we have multiple workflows - this helps to distinguish for them +run-name: "${{ github.event.workflow_run.head_branch }} - Report Sonar Results" + +on: + workflow_run: + workflows: ["Build & Test"] # runs after build and test workflow + types: + - completed + +jobs: + # + # Collect and upload sonar report with coverage generated by the Build & Test workflow + # + sonar-report: + name: Sonar-Report + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + steps: + + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} + ref: ${{ github.event.workflow_run.head_branch }} + fetch-depth: 0 + + - name: Setup - Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'maven' + + - name: Restore - Dependency Cache + uses: actions/cache/restore@v4 + with: + path: ~/.m2/repository + key: deps-${{ runner.os }}-m2-${{ github.event.workflow_run.pull_requests[0].head.ref }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + deps-${{ runner.os }}-m2-${{ github.head_ref }}- + deps-${{ runner.os }}-m2- + deps-${{ runner.os }}- + deps- + + - name: Setup - SonarCloud Cache + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: sonar-${{ runner.os }} + restore-keys: sonar-${{ runner.os }} + + # Download jacoco overall coverage from build & test output + - name: Download - Jacoco Overall Coverage + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.BOT_ACCESS_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} # download artifacts from build and test workflow + pattern: jacoco-report-final # uses artifact of build and test workflow + merge-multiple: true + path: ${{ github.workspace }}/tests/coverage/jacoco-report-final/ + + - name: Sonar - Analyze + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + mvn --batch-mode -DskipTests compile sonar:sonar \ + -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }} \ + -Dsonar.pullrequest.key=${{ github.event.workflow_run.pull_requests[0].number }} \ + -Dsonar.pullrequest.branch=${{ github.event.workflow_run.pull_requests[0].head.ref }} \ + -Dsonar.pullrequest.base=${{ github.event.workflow_run.pull_requests[0].base.ref }} \ + -Dsonar.host.url=https://sonarcloud.io \ + -Dsonar.organization=ehrbase \ + -Dsonar.projectKey=ehrbase_ehrbase \ + -Dsonar.exclusions=test/** \ + -Dsonar.coverage.exclusions=test/** \ + -Dsonar.coverage.jacoco.xmlReportPaths=${{ github.workspace }}/tests/coverage/jacoco-report-final/jacoco.xml diff --git a/.github/workflows/status.yml b/.github/workflows/status.yml index 8c8eeea86..7e9801812 100644 --- a/.github/workflows/status.yml +++ b/.github/workflows/status.yml @@ -1,9 +1,12 @@ # Adds the results of a workflow as a commit status -name: Set test status +name: "Set Github Status" + +# we have multiple workflows - this helps to distinguish for them +run-name: "${{ github.event.pull_request.title && github.event.pull_request.title || github.ref_name }} - Set Github Status" on: workflow_run: - workflows: ["Build & Deploy Docker Image (version-tag)"] + workflows: ["Build & Test"] types: - completed diff --git a/.gitignore b/.gitignore index de53e042e..ceb398528 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ Temporary Items # Compiled class files *.class +### jacoco ### +*.exec + # Mobile Tools for Java (J2ME) .mtj.tmp/ @@ -133,26 +136,6 @@ local.properties ## Directory-based project format: .idea/ -# if you remove the above rule, at least ignore the following: - -# User-specific stuff: -# .idea/workspace.xml -# .idea/tasks.xml -# .idea/dictionaries - -# Sensitive or high-churn files: -# .idea/dataSources.ids -# .idea/dataSources.xml -# .idea/sqlDataSources.xml -# .idea/dynamic.xml -# .idea/uiDesigner.xml - -# Gradle: -# .idea/gradle.xml -# .idea/libraries - -# Mongo Explorer plugin: -# .idea/mongoSettings.xml ## File-based project format: *.ipr @@ -183,7 +166,11 @@ log.html output.xml report.html std* +*.robot *.tmp.json +tests/coverage +tests/results +tests/report tests/DBDUMP_STDERR tests/DBRESTORE_STDERR tests/DBRESTORE_STDOUT @@ -219,7 +206,3 @@ vulnerability_analysis.json application/.pgdata /plugin_dir/ /plugin_config_dir/ - -# Robot tests, as they are stored in external repository -tests/ -*.robot \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e15a5fa7a..fbae99101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,705 +5,83 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.32.0] - ### Added - ### Changed - - Upgrade openEHR_SDK to version 2.6.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - ### Fixed - - Fix the folder audit status event type ([#1193](https://github.com/ehrbase/ehrbase/pull/1193)) - - Fix OAuth2 Security Configuration for admin role and management endpoints ([#1196](https://github.com/ehrbase/ehrbase/pull/1196)) - - Fix party identified NP ([#1191](https://github.com/ehrbase/ehrbase/pull/1191)) - - -## [0.31.0] - ### Added - ### Changed - - Upgrade openEHR_SDK to version 2.3.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - - Migrated to spring boot 3 ([#1174](https://github.com/ehrbase/ehrbase/pull/1174)) - - Removed authorization scopes from endpoints and added support for overwriting controllers ([#1157](https://github.com/ehrbase/ehrbase/pull/1157)) - ### Fixed - - Fix audit logs location ([#1160](https://github.com/ehrbase/ehrbase/pull/1160)) - - Address AQL query security vulnerabilities ([#1190](https://github.com/ehrbase/ehrbase/pull/1190)) - -## [0.30.0] - ### Added - ### Changed - - Upgrade openEHR_SDK to version 2.2.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - ### Fixed - - Fix storing attributes of Locatable.name ([#1161](https://github.com/ehrbase/ehrbase/pull/1161)) - -## [0.29.0] - ### Added - ### Changed - - Upgrade openEHR_SDK to version 2.1.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - ### Fixed -- cleanup security context ([#1159](https://github.com/ehrbase/ehrbase/pull/1159)) -- enforce unique template id ([#1158](https://github.com/ehrbase/ehrbase/pull/1158)) - -## [0.28.0] +## [2.7.0] ### Added - - - Added support tenant deletion ([#1146](https://github.com/ehrbase/ehrbase/pull/1146)) - +* Experimental ItemTag REST endpoints for EHR_STATUS and COMPOSITION (configs: `ehrbase.rest.experimental.tags.*`) ([1343](https://github.com/ehrbase/ehrbase/pull/1343)) +* CLI runner with support for flyway pre-migrations ([1387](https://github.com/ehrbase/ehrbase/pull/1387)) ### Changed - - Upgrade openEHR_SDK to version 2.0.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - - Audit log functionality has been integrated as a plugin ([#1131](https://github.com/ehrbase/ehrbase/pull/1131)) - - update to SDK 2.0.0-SNAPSHOT ([#1141](https://github.com/ehrbase/ehrbase/pull/1141)) - - move dbencoding from SDK to ehrbase ([#1141](https://github.com/ehrbase/ehrbase/pull/1141)) - - adjust to new SDK package paths ([#1141](https://github.com/ehrbase/ehrbase/pull/1141)) - ### Fixed +* Require EHR_STATUS `is_queryable` and `is_modifiable` to be present ([#1377](https://github.com/ehrbase/ehrbase/pull/1377)) -## [0.27.4] +## [2.6.0] ### Added ### Changed - - Upgrade openEHR_SDK to version 1.29.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md +* Improved data structure for hierarchy of versioned objects ([#1359](https://github.com/ehrbase/ehrbase/pull/1359)) +* Upgrade openEHR_SDK to version 2.15.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md ### Fixed - - Run directory operation in one transaction ([#1133](https://github.com/ehrbase/ehrbase/pull/1133)) -## [0.27.3] +## [2.5.0] ### Added - ### Changed - - Upgrade openEHR_SDK to version 1.28.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - ### Fixed +* Create a `ehrbase` user to run the Docker container ([#1336](https://github.com/ehrbase/ehrbase/pull/1336)) + ### Changed +* Deprecate plugin aspects ([#1344](https://github.com/ehrbase/ehrbase/pull/1344)) +* Add simplified JSON-based “web template” format support for GET Template ADL 1.4 using header `Accept: application/openehr.wt+json` ([1334](https://github.com/ehrbase/ehrbase/pull/1334)) +* Improved AQL performance ([#1358](https://github.com/ehrbase/ehrbase/pull/1358)) +* Upgrade openEHR_SDK to version 2.14.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md -## [0.27.2] - ### Added - ### Changed - - Upgrade openEHR_SDK to version 1.28.0-SNAPSHOT see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - - fix flyway_checksum for script 83 ([#1130](https://github.com/ehrbase/ehrbase/pull/1130)) - ### Fixed - -## [0.27.1] - ### Added - ### Changed - - Upgrade openEHR_SDK to version 1.28.0-SNAPSHOT see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - ### Fixed - - Fix DB Migration scripts. Remove the tenant RLS policy during script execution ([#1127](https://github.com/ehrbase/ehrbase/pull/1127)). - -## [0.27.0] - ### Added - - Sub-folders are retained in the response upon updating Folders. ([#1108](https://github.com/ehrbase/ehrbase/pull/1108)) - ### Changed - - Upgrade openEHR_SDK to version 1.27.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - - Changed namespace UUID to number-based ID and added to the primary key in each entity. ([#1100](https://github.com/ehrbase/ehrbase/pull/1100)) - ### Fixed - - Fixes NPE if during start up with many templates in the system. ([#1101](https://github.com/ehrbase/ehrbase/pull/1101)) + ### Fixed +* Return `201` instead of `204` for EHR creation ([1371](https://github.com/ehrbase/ehrbase/pull/1371)) +* Fixed AQL predicate reduction logic ([#1358](https://github.com/ehrbase/ehrbase/pull/1358)) +* Respect AQL root predicates ([#1358](https://github.com/ehrbase/ehrbase/pull/1358)) -## [0.26.0] +## [2.4.0] ### Added - - use from sdk archie version 3.0.0 and antlr4 version 4.11.1 ([#1078](https://github.com/ehrbase/ehrbase/pull/1078)) +- Configurable flyway migration strategy +- Configurable fetch limit checks + default limit for AQL queries +- Configurable fetch limit precedence strategy for AQL queries ### Changed - - Upgrade openEHR_SDK to version 1.26.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - - Respect the predefined value of the composition UID when creating new composition ([#1090](https://github.com/ehrbase/ehrbase/pull/1090)) + - Upgrade openEHR_SDK to version 2.13.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md ### Fixed -## [0.25.0] +## [2.3.0] ### Added ### Changed - - Upgrade openEHR_SDK to version 1.25.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - - new Directory implementation ([#1059](https://github.com/ehrbase/ehrbase/pull/1059)) - ### Fixed - - error on concurrent user creation ([#1067](https://github.com/ehrbase/ehrbase/pull/1067)) - -## [0.24.0] + - Upgrade openEHR_SDK to version 2.12.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md +* AQL-Performance: paths containing non-locatable structure attributes (EVENT_CONTEXT, FEEDER_AUDIT) ([#1341](https://github.com/ehrbase/ehrbase/pull/1341)) +* Removed `@Schema(MediaType.class)` Header declaration from swagger UI ([#1333](https://github.com/ehrbase/ehrbase/pull/1333)) + ### Fixed +## [2.2.0] ### Added - -- add caches for ehr.concept, ehr.territory, ehr.language([#1007](https://github.com/ehrbase/ehrbase/pull/1007)) -- add stored query text plain request support ([#1021](https://github.com/ehrbase/ehrbase/pull/1021)) -- unwrap if match additional double quotes ([#1022](https://github.com/ehrbase/ehrbase/pull/1022)) -- general support for tenants and authorization for plugins -- Expose management and swagger endpoints publicly([#1030](https://github.com/ehrbase/ehrbase/pull/1030)) -- Include tenant information in ATNA logs([#1055](https://github.com/ehrbase/ehrbase/pull/1055)) - +* Added AQL debug support ([#1296](https://github.com/ehrbase/ehrbase/pull/1296)) ### Changed - - Upgrade openEHR_SDK to version 1.24.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - -- use caffeine cache instead of ehcache as ehcache has unnecessary - blocking([#1007](https://github.com/ehrbase/ehrbase/pull/1007)) -- remove unnecessary DB queries([#1007](https://github.com/ehrbase/ehrbase/pull/1007)) -- Performance improvements of the composition audit.([#1042](https://github.com/ehrbase/ehrbase/pull/1042)) -- improved versioning of stored queries ([#1050](https://github.com/ehrbase/ehrbase/pull/1050)) -- switch to non-privileged user for DB Access ([#11064](https://github.com/ehrbase/ehrbase/pull/1064)) - -### Fixed - -- Update spring-boot(spring-security), postgresql, pf4j-spring dependency version ([#1060](https://github.com/ehrbase/ehrbase/pull/1060)) -- Update jackson dependency version ([#1063](https://github.com/ehrbase/ehrbase/pull/1063)) -- maintain a correct history of participations([#1016](https://github.com/ehrbase/ehrbase/pull/1016)) -- Fixed NullPointerException when language is missing ([#1023](https://github.com/ehrbase/ehrbase/pull/1023)) -- Endpoint for storing queries gives a detailed error regarding incorrect - version([#1032](https://github.com/ehrbase/ehrbase/pull/1032)) -- Fixes NullPointerException while sending ehr status request(GET/PUT) when abac is - enabled ([#1031](https://github.com/ehrbase/ehrbase/pull/1031)) -- Fixed status update if party already exist for ehr([#1024](https://github.com/ehrbase/ehrbase/pull/1024)) -- Change response code from 400 to 406 Not Acceptable on querying POST template endpoint with unsupported `Accept` - header application/json([#1029](https://github.com/ehrbase/ehrbase/pull/1029)) -- Fixed abac default tenant claims validation([#1041](https://github.com/ehrbase/ehrbase/pull/1041)) -- Fixed incorrect path in Location http header([#1044](https://github.com/ehrbase/ehrbase/pull/1044)) -- Fixed get deleted composition returns 500([#1048](https://github.com/ehrbase/ehrbase/pull/1048)) - -## [0.23.0] - -### Added - -- Authorization enhancements [#1002](https://github.com/ehrbase/ehrbase/pull/1002) - - Impl. API extensions needed for authorization - - Added annotations on all REST endpoints to support authorization decissions - -### Changed - -- Upgrade openEHR_SDK to version 1.24.0-SNAPSHOT see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md -- JAVA 17 baseline -- Spring Boot 2.7.4 -- Upgrade openEHR_SDK to version 1.24.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - -### Fixed - -## [0.22.0] - -### Added - -- Add spotless plugin, Add codestyle check to workflows ([#864](https://github.com/ehrbase/ehrbase/pull/864)) - -### Changed - -- Upgrade openEHR_SDK to version 1.23.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md -- Change DB-model to save Data in a namespace ([#994](https://github.com/ehrbase/ehrbase/pull/994)) - -### Fixed - -- Error causing a 500 Response when requesting a deleted composition via ECIS GET Composition - Endpoint ([#875](https://github.com/ehrbase/ehrbase/pull/875)) -- Update folder was not always corectly updating it items ([#974](https://github.com/ehrbase/ehrbase/pull/974)) -- AuditDetails had timezone missing ([#998](https://github.com/ehrbase/ehrbase/pull/998)) -- numerus AQL - fixes ([#1001](https://github.com/ehrbase/ehrbase/pull/1001), [#1006](https://github.com/ehrbase/ehrbase/pull/1006)) - -## [0.21.1] - -### Fixed - -- Fixed update script for user consolidation ([#865](https://github.com/ehrbase/ehrbase/pull/865)) - -## [0.21.0] - -### Added - -- Implement template example endpoints ([#801](https://github.com/ehrbase/openEHR_SDK/pull/801)) -- Implement EHR_STATUS.is_modifiable semantics on service - level ([#791](https://github.com/ehrbase/openEHR_SDK/pull/791)) -- use bom for dependence management ([#820](https://github.com/ehrbase/ehrbase/pull/820)) -- add Release action ([#831](https://github.com/ehrbase/ehrbase/pull/831) -- Added hooks for the plugin system ([#816](https://github.com/ehrbase/ehrbase/pull/816)) -- Added index to `party_identified` to improve performance of find EHR by - subject-id ([857](https://github.com/ehrbase/ehrbase/pull/857))) - -### Changed - -- Upgrade to Spring boot 2.5.12 - see [spring-framework-rce](https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement) ([#800](https://github.com/ehrbase/ehrbase/pull/800)) - . -- Add unique constraints on `status` and `entry` ([#821](https://github.com/ehrbase/ehrbase/pull/821)). -- Removed Postgres with extensions setup ([#840](https://github.com/ehrbase/ehrbase/pull/840)) -- Upgrade openEHR_SDK to version 1.19.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - -### Fixed - -- Handle 4xx status code related Spring MVC Exceptions, instead of making them all a 500, and handle - ResponseStatusException ([#803](https://github.com/ehrbase/openEHR_SDK/pull/803)) -- Fix duplicate users issue ([#826](https://github.com/ehrbase/ehrbase/pull/826)). -- Fix validation errors in ECIS EHR endpoint ([#828](https://github.com/ehrbase/ehrbase/pull/828)) -- Fix 400 error in ECIS EHR update ([#834](https://github.com/ehrbase/ehrbase/pull/834)) - -## [0.20.0] (beta) - -### Added - -- Add Plugins system ([#772](https://github.com/ehrbase/ehrbase/pull/772), - [#779](https://github.com/ehrbase/ehrbase/pull/779)). -- AQL: support `ORDER BY` and `LIMIT [OFFSET]` clauses in any - order ([#782](https://github.com/ehrbase/openEHR_SDK/pull/782)). - -### Changed - -- Update Archie to version 2.0.1 [#784](https://github.com/ehrbase/ehrbase/pull/784) -- Add missing database indexes [#788](https://github.com/ehrbase/ehrbase/pull/788) - and [#796](https://github.com/ehrbase/ehrbase/pull/796) -- Upgrade openEHR_SDK to version 1.18.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - -### Fixed - -- Remove unused Operational Template cache ([#759](https://github.com/ehrbase/ehrbase/pull/759)). -- Allow update/adding/removal of feeder_audit/links on Composition ([#773](https://github.com/ehrbase/ehrbase/pull/773)) -- Add default ASC direction to ORDER BY clause in AQL ([#780](https://github.com/ehrbase/ehrbase/pull/780)). -- Fix DB Migration scripts. Allow user different then ehrbase ([#795](https://github.com/ehrbase/ehrbase/pull/795)). - -## [0.19.0] (beta) - -### Added - -- Add Flyway callback to check `IntervalStyle` configuration - parameter ([#720](https://github.com/ehrbase/ehrbase/pull/720)). -- Validate RM types used in OPT template ([#739](https://github.com/ehrbase/ehrbase/issues/739)). - -### Changed - -- Upgrade to Archie 1.0.4 ([#719](https://github.com/ehrbase/ehrbase/pull/719)). -- Improve errors and exceptions logging ([#745](https://github.com/ehrbase/ehrbase/pull/745)). -- Upgrade openEHR_SDK to version 1.17.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md - -### Fixed - -- Fixed SQL encoding whenever template is - unresolved ([#723](https://github.com/ehrbase/ehrbase/issues/723)) -- Modified handling of conflicting identified - parties ([#710](https://github.com/ehrbase/ehrbase/issues/710)) -- Fixes wrong status code returned by EHRbase while creating FLAT - composition ([#726](https://github.com/ehrbase/ehrbase/pull/726)) -- Fix NullPointerException while deleting unknown (or already deleted) composition - parameter ([#722](https://github.com/ehrbase/ehrbase/pull/722)). -- Fix querying other_participations ([#707](https://github.com/ehrbase/ehrbase/issues/707)) - -## [0.18.3] (beta) - -### Added - -### Changed - -- removed log4j (see https://github.com/ehrbase/ehrbase/pull/711) - -### Fixed - -## [0.18.2] (beta) - -### Fixed - -- updated log4j from 1.15.0 to 1.60.0 - -## [0.18.1] (beta) - -### Fixed - -- Fix deployment issue with Flyway migration V62__add_entry_history_missing_columns.sql - -## [0.18.0] (beta) - -### Added - -- Migrated to Archie openEHR library version > 1.0.0, incl. its new strict invariant checks ( - see: https://github.com/ehrbase/ehrbase/pull/570) -- Support Structured format on ecis composition endpoints ( - see https://github.com/ehrbase/ehrbase/pull/648) -- Add new configuration options to customise user/admin role names when using OAuth authentication - (see https://github.com/ehrbase/ehrbase/pull/667) -- Add configuration properties to customize CORS configuration ( - see https://github.com/ehrbase/ehrbase/pull/697) - -### Changed - -### Fixed - -- Missing details in response returned by Directory REST API ( - see: https://github.com/ehrbase/ehrbase/pull/605) -- Add foreign key between `folder` and `ehr` tables ( - see: https://github.com/ehrbase/ehrbase/pull/616) -- Improves 'Admin Delete EHR' performance (see https://github.com/ehrbase/ehrbase/pull/626) -- many fixes to the flat support (see https://github.com/ehrbase/ehrbase/pull/627) -- Fix conversion between `DvDateTime` and `Timestamp` ( - see https://github.com/ehrbase/ehrbase/pull/634) -- Fix FLAT format does not return the archetype data if the archetype_id contains the letters "and" -- Datetime inconsistent handling (see https://github.com/ehrbase/ehrbase/pull/649) -- Fix issue using DV_DATE_TIME without time-zone (see https://github.com/ehrbase/ehrbase/pull/658) -- update lg4j version (see https://github.com/ehrbase/ehrbase/pull/702) - -## [0.17.2] (beta) - -### Added - -- Github Action worklows to deploy multiarch images (`latest`, `next`, `version-tag`) to Docker - Hub ( - see: https://github.com/ehrbase/ehrbase/pull/578) - -### Changed - -- Removes SELECT statement when PartyProxy object is empty ( - see: https://github.com/ehrbase/ehrbase/pull/581) -- Provide configuration properties for configuring context paths of openEHR REST API and Admin API ( - see: https://github.com/ehrbase/ehrbase/pull/585) - -### Fixed - -- `Accept` header with multiple MIME types causes an IllegalArgumentException ( - see: https://github.com/ehrbase/ehrbase/pull/583) -- Composition version Uid schema in EhrScape API (see: https://github.com/ehrbase/ehrbase/pull/520) -- Terminology Service calls from within AQL queries does not work ( - see: https://github.com/ehrbase/ehrbase/pull/572) - -## [0.17.1] (beta) - -### Added - -- Default handling for audit metadata (see: https://github.com/ehrbase/ehrbase/pull/552) - -### Changed - -- Updated the SDK dependency to the latest version ( - see: https://github.com/ehrbase/ehrbase/pull/565) -- Refactored versioned object (interfaces) on service and access layer ( - see: https://github.com/ehrbase/ehrbase/pull/552) - -### Fixed - -- Assigner in DV_IDENTIFIER not selected in aql (see: https://github.com/ehrbase/ehrbase/pull/561) -- ehr_status.uuid not selects via aql (see: https://github.com/ehrbase/ehrbase/pull/561) -- DB migration file conflict (see: https://github.com/ehrbase/ehrbase/pull/564) -- Ddmin delete of multiple status versions (see: https://github.com/ehrbase/ehrbase/pull/552) - -## [0.17.0] (beta) - -### Added - -- Implement validation of compositions using external FHIR TS ( - see: https://github.com/ehrbase/ehrbase/pull/493) -- Support for Attribute-based Access Control (see: https://github.com/ehrbase/ehrbase/pull/499) -- Support AQL array resolution in EHR_STATUS::other_details - -### Changed - -- Update paths for Admin API, Management API and `/status` endpoint ( - see: https://github.com/ehrbase/ehrbase/pull/541) - -### Fixed - -- Folder handling (update, delete and missing audits) ( - see: https://github.com/ehrbase/ehrbase/pull/529) -- Fixed and refactored handling of audits and versioned objects ( - see: https://github.com/ehrbase/ehrbase/pull/552/) - -## [0.16.0] (beta) - -### Added - -- Endpoints and integration tests for VERSIONED_COMPOSITION ( - see: https://github.com/ehrbase/ehrbase/pull/448) -- ATNA Logging for composition endpoints, querying and operations on the EHR object ( - see: https://github.com/ehrbase/ehrbase/pull/452) -- EHRbase Release Checklist (see: https://github.com/ehrbase/ehrbase/pull/451) -- CACHE_ENABLED ENV to Dockerfile (see: https://github.com/ehrbase/ehrbase/pull/467) - -### Changed - -- Updated the SDK dependency to the latest version ( - see: https://github.com/ehrbase/ehrbase/pull/463) -- Force retrieval of operational template from DB (see: https://github.com/ehrbase/ehrbase/pull/468) - + - Upgrade openEHR_SDK to version 2.11.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md +* The field `q` of AQL query responses now contain the requested, and not the executed, query string ([#1296](https://github.com/ehrbase/ehrbase/pull/1296)) +* The field `meta._schema_version` of AQL query responses has been changed to `1.0.3` ([#1296](https://github.com/ehrbase/ehrbase/pull/1296)) +* Return HTTP 422 Unprocessable Content in case fetch or offset is defined inside the aql query and as parameter ([#1325](https://github.com/ehrbase/ehrbase/pull/1325)). ### Fixed -- WHERE field construct (see: https://github.com/ehrbase/ehrbase/pull/439) -- Inconsistent behavior in SMICS Virology Query (see: https://github.com/ehrbase/ehrbase/pull/456) -- Bunch of AQL issues (see: https://github.com/ehrbase/ehrbase/pull/461) -- AQL: Error in processing OR in Contains clause (see: https://github.com/ehrbase/ehrbase/pull/462) -- Cache issue on Startup (see: https://github.com/ehrbase/ehrbase/pull/465) - -## [0.15.0] (beta) - -### Added - -- Adds Admin API endpoints: Del EHR, Del Composition and Del Contribution ( - see: https://github.com/ehrbase/ehrbase/pull/344) -- Add ATNA logging configuration capabilities (see https://github.com/ehrbase/ehrbase/pull/355) -- Support for EHR_STATUS and (partial) FOLDER version objects in contributions ( - see: https://github.com/ehrbase/ehrbase/pull/372) -- Add status endpoint to retrieve version information on running EHRbase instance and for heartbeat - checks. ( - see: https://github.com/ehrbase/ehrbase/pull/393) -- Add /status/info endpoint using actuator for basic info on running app ( - see: https://github.com/ehrbase/ehrbase/pull/400) -- Add /status/health endpoint for kubernetes liveness and readiness probes ( - see: https://github.com/ehrbase/ehrbase/pull/400) -- Add /status/env endpoint for environment information ( - see: https://github.com/ehrbase/ehrbase/pull/400) -- Add /status/metrics endpoint for detailed metrics on specific topics (db connection, http - requests, etc.) ( - see: https://github.com/ehrbase/ehrbase/pull/400) -- Add /status/prometheus endpoint for prometheus metrics ( - see: https://github.com/ehrbase/ehrbase/pull/400) -- Endpoints and integration tests for VERISONED_EHR_STATUS ( - see: https://github.com/ehrbase/ehrbase/pull/415) - -### Changed - -- support AQL querying on full EHR (f.e. SELECT e) (see ) -- Update Dockerfile for usage with metrics and status ( - see https://github.com/ehrbase/ehrbase/pull/408) -- Refactored DB handling of contributions, removed misleading `CONTIRUBITON_HISTORY` table ( - see https://github.com/ehrbase/ehrbase/pull/416) - -## [0.14.0] (beta) - -### Added - -- Add admin API endpoint stubs (see: https://github.com/ehrbase/ehrbase/pull/280) -- Add support for FeederAudit in Locatable. Refactored Composition Serializer for DB encoding ( - see https://github.com/ehrbase/ehrbase/tree/feature/311_feeder_audit - , https://github.com/ehrbase/openEHR_SDK/tree/feature/311_feeder_audit) -- Change the strategy to resolve CONTAINS in AQL (https://github.com/ehrbase/ehrbase/pull/276) -- Add admin template API functionality (see: https://github.com/ehrbase/ehrbase/pull/301) -- Persist caches to java.io.tmpdir (see: https://github.com/ehrbase/ehrbase/pull/308) -- Precalculate containment tree from OPT template (see https://github.com/ehrbase/ehrbase/pull/312) - -### Changed - -- Detection of duplicate directories on EHR on POST -- Using ObjectVersionId for DIRECTORY Controller and Service Layers ( - see: https://github.com/ehrbase/ehrbase/pull/297) -- Added Junit5 support via spring-boot-starter-test (https://github.com/ehrbase/ehrbase/pull/298) -- Enable cartesian products on embedded arrays in JSONB ( - see https://github.com/ehrbase/ehrbase/pull/309) -- Use new OPT-Parser from sdk (see https://github.com/ehrbase/ehrbase/pull/314) -- Add CORS config to enable clients to detect auth method ( - see https://github.com/ehrbase/ehrbase/pull/354). - -### Fixed - -- Detect duplicates on POST Directory (see: https://github.com/ehrbase/ehrbase/pull/281) -- Support context-less composition (see: https://github.com/ehrbase/ehrbase/pull/288) -- Fixed missing AQL level of parenthesis when using NOT in WHERE clause ( - see https://github.com/ehrbase/ehrbase/pull/293) -- Allow duplicated paths in AQL resultsets (see: https://github.com/ehrbase/ehrbase/issues/263) -- Transaction timestamps are now truncated to ms (see: https://github.com/ehrbase/ehrbase/pull/299) -- Change response code on not found directory to 412 if not found ( - see: https://github.com/ehrbase/ehrbase/pull/304) - -## [0.13.0] (beta) - -### Added - -- Added support for various functions in AQL (aggregation, statistical, string etc.) ( - see: https://github.com/ehrbase/ehrbase/pull/223/) - -### Changed - -#### DIRECTORY - -- PreconditionFailed error response contains proper ETag and Location headers ( - see: https://github.com/ehrbase/ehrbase/pull/183) - -#### Robot Tests - -- Update of AQL-Query test suite (see: https://github.com/ehrbase/ehrbase/pull/179) - -### Fixed - -- force a default timezone if not present for context/start_time and context/end_time if - specified (https://github.com/ehrbase/ehrbase/pull/215) -- Representation of version uid of EHR_STATUS (see: https://github.com/ehrbase/ehrbase/pull/180) -- Refactored support of PartyProxy and ObjectId in both CRUD and AQL operations ( - see https://github.com/ehrbase/ehrbase/pull/248) -- fix support of mandatory attributes in ENTRY specialization including rm_version ( - see https://github.com/ehrbase/ehrbase/pull/247) - -#### DIRECTORY - -- Directory IDs from input path or If-Match header must now be in version_uid format ( - see https://github.com/ehrbase/ehrbase/pull/183) -- Folder IDs inside body are now parsed correctly (see: https://github.com/ehrbase/ehrbase/pull/183) -- PreconditionFailed error response contains proper ETag and Location headers ( - see: https://github.com/ehrbase/ehrbase/pull/183) - -#### Robot Tests - -- Added validation checking for other_details and ehr_status. ( - see: https://github.com/ehrbase/ehrbase/pull/207) -- Supports archetype_node_id and name for EHR_STATUS ( - see: https://github.com/ehrbase/ehrbase/pull/207) -- fixes bad canonical encoding for observation/data/origin ( - see: https://github.com/ehrbase/ehrbase/pull/213) -- POST without accept header for ehr, composition and contribution endpoints ( - see: https://github.com/ehrbase/ehrbase/pull/199) - -## [0.12.0] (alpha) - -### Added - -- Basic Authentication as opt-in (see: https://github.com/ehrbase/ehrbase/pull/200) -- Allow Templates can now be overwritten via spring configuration ( - see: https://github.com/ehrbase/ehrbase/pull/194) - -### Fixed - -- Contribution endpoint checks for some invalid input combinations ( - see: https://github.com/ehrbase/ehrbase/pull/202) -- Fixes response code on /ehr PUT with invalid ID ( - see: https://github.com/ehrbase/project_management/issues/163) -- Fixes STATUS w/ empty subject bug (see: https://github.com/ehrbase/ehrbase/pull/196) -- Now querying on composition category returns the correct result (composition/category...) -- Fixes storage of party self inside compositions (see: https://github.com/ehrbase/ehrbase/pull/195) -- Added support of AQL query in the form of c/composer ( - see: https://github.com/ehrbase/ehrbase/pull/184) -- Java error with UTF-8 encoding resolved (see: https://github.com/ehrbase/ehrbase/pull/173) -- AQL refactoring and fixes to support correct canonical json representation ( - see: https://github.com/ehrbase/ehrbase/pull/201) -- fix terminal value test for non DataValue 'value' attribute ( - see: https://github.com/ehrbase/ehrbase/pull/189) - -## [0.11.0] (alpha) - -**Note:** Due to the transition to this changelog the following list is not complete. Starting with -the next release this file will provide a proper overview. - -### Added - -- Docker and docker-compose support for both application and database -- Get folder with version_at_time parameter -- Get Folder with path parameter - -### Changed - -- FasterXML Jackson version raised to 2.10.2 -- Java version raised from 8 to 11 -- Jooq version raised to 3.12.3 -- Spring Boot raised to version 2 - -### Fixed - -- Response code when composition is logically deleted ( - see: https://github.com/ehrbase/ehrbase/pull/144) -- Response and `PREFER` header handling of `/ehr` endpoints ( - see: https://github.com/ehrbase/ehrbase/pull/165) -- Deserialization of EhrStatus attributes is_modifiable and is_queryable are defaulting to `true` - now ( - see: https://github.com/ehrbase/ehrbase/pull/158) -- Updating of composition with invalid template (e.g. completely different template than the - previous version) ( - see: https://github.com/ehrbase/ehrbase/pull/166) -- Folder names are checked for duplicates (see: https://github.com/ehrbase/ehrbase/pull/168) -- AQL parser threw an unspecific exception when an alias was used in a WHERE - clause (https://github.com/ehrbase/ehrbase/pull/149) -- Improved exception handling in composition validation ( - see: https://github.com/ehrbase/ehrbase/pull/147) -- Improved Reference Model validation (see: https://github.com/ehrbase/ehrbase/pull/147) -- Error when reading a composition that has a provider name set( - see: https://github.com/ehrbase/ehrbase/pull/143) -- Allow content to be null inside a composition (see: https://github.com/ehrbase/ehrbase/pull/129) -- Fixed deletion of compositions through a contribution ( - see: https://github.com/ehrbase/ehrbase/pull/128) -- Start time of a composition was not properly updated ( - see: https://github.com/ehrbase/ehrbase/pull/137) -- Fixed validation of null values on participations ( - see: https://github.com/ehrbase/ehrbase/pull/132) -- Order by in AQL did not work properly (see: https://github.com/ehrbase/ehrbase/pull/112) -- Order of variables in AQL result was not preserved ( - see: https://github.com/ehrbase/ehrbase/pull/103) -- Validation of compositions for unsupported language( - see: https://github.com/ehrbase/ehrbase/pull/107) -- Duplicated ehr attributes in query due to cartesian product ( - see: https://github.com/ehrbase/ehrbase/pull/106) -- Retrieve of EHR_STATUS gave Null Pointer Exception for non-existing EHRs ( - see: https://github.com/ehrbase/ehrbase/pull/136) -- Correct resolution of ehr/system_id in AQL (see: https://github.com/ehrbase/ehrbase/pull/102) -- Detection of duplicate aliases in aql select (see: https://github.com/ehrbase/ehrbase/pull/98) - -## [0.10.0] (alpha) - -### Added - -- openEHR REST API DIRECTORY Endpoints -- openEHR REST API EHR_STATUS Endpoints (including other_details) -- Spring Transactions: EHRbase now ensures complete rollback if part of a transaction fails. -- Improved Template storage: openEHR Templates are stored inside the postgres database instead of - the file system ( - including handling of duplicates) -- AQL queries with partial paths return data in canonical json format (including full compositions) -- Multimedia data can be correctly stored and retrieved -- Spring configuration allows setting the System ID -- Validation of openEHR Terminology (openEHR terminology codes are tested against an internal - terminology service) - -### Fixed - -- Order of columns in AQL result sets are now reliably - preserved (https://github.com/ehrbase/ehrbase/issues/37) -- Some projection issues for EHR attributes have been resolved in AQL -- Fixed error regarding DISTINCT operator in AQL (https://github.com/ehrbase/ehrbase/issues/50) -- Fixed null pointer exceptions that could occur in persistent compositions - -## [0.9.0] (pre-alpha) - -### Added - -- openEHR REST API DIRECTORY Endpoints -- openEHR REST API EHR_STATUS Endpoints (including other_details) -- Spring Transactions: EHRbase now ensures complete rollback if part of a transaction fails. -- Improved Template storage: openEHR Templates are stored inside the postgres database instead of - the file system ( - including handling of duplicates) -- AQL queries with partial paths return data in canonical json format (including full compositions) -- Multimedia data can be correctly stored and retrieved -- Spring configuration allows setting the System ID -- Validation of openEHR Terminology (openEHR terminology codes are tested against an internal terminology service) - -### Fixed - -- Order of columns in AQL result sets are now reliably preserved (https://github.com/ehrbase/ehrbase/issues/37) -- Some projection issues for EHR attributes have been resolved in AQL -- Fixed error regarding DISTINCT operator in AQL (https://github.com/ehrbase/ehrbase/issues/50) -- Fixed null pointer exceptions that could occur in persistent compositions - -[0.21.1]: https://github.com/ehrbase/ehrbase/compare/v0.21.0...v0.21.1 - -[0.21.0]: https://github.com/ehrbase/ehrbase/compare/v0.20.0...v0.21.0 - -[0.20.0]: https://github.com/ehrbase/ehrbase/compare/v0.19.0...v0.20.0 - -[0.19.0]: https://github.com/ehrbase/ehrbase/compare/v0.18.3...v0.19.0 - -[0.18.3]: https://github.com/ehrbase/ehrbase/compare/v0.18.2...v0.18.3 - -[0.18.2]: https://github.com/ehrbase/ehrbase/compare/v0.18.1...v0.18.2 - -[0.18.1]: https://github.com/ehrbase/ehrbase/compare/v0.18.0...v0.18.1 - -[0.18.0]: https://github.com/ehrbase/ehrbase/compare/v0.17.2...v0.18.0 - -[0.17.2]: https://github.com/ehrbase/ehrbase/compare/v0.17.1...v0.17.2 - -[0.17.1]: https://github.com/ehrbase/ehrbase/compare/v0.17.0...v0.17.1 - -[0.17.0]: https://github.com/ehrbase/ehrbase/compare/v0.16.0...v0.17.0 - -[0.16.0]: https://github.com/ehrbase/ehrbase/compare/v0.15.0...v0.16.0 - -[0.15.0]: https://github.com/ehrbase/ehrbase/compare/v0.14.0...v0.15.0 - -[0.14.0]: https://github.com/ehrbase/ehrbase/compare/v0.13.0...v0.14.0 - -[0.13.0]: https://github.com/ehrbase/ehrbase/compare/v0.12.0...v0.13.0 - -[0.12.0]: https://github.com/ehrbase/ehrbase/compare/v0.11.0...v0.12.0 - -[0.11.0]: https://github.com/ehrbase/ehrbase/compare/v0.10.0...v0.11.0 - -[0.10.0]: https://github.com/ehrbase/ehrbase/compare/v0.9.0...v0.10.0 - -[0.9.0]: https://github.com/ehrbase/ehrbase/releases/tag/v0.9.0 - -[0.22.0]: https://github.com/ehrbase/ehrbase/compare/v0.21.1...v0.22.0 - -[0.23.0]: https://github.com/ehrbase/ehrbase/compare/v0.22.0...v0.23.0 - -[0.24.0]: https://github.com/ehrbase/ehrbase/compare/v0.23.0...v0.24.0 -[0.25.0]: https://github.com/ehrbase/ehrbase/compare/v0.24.0...v0.25.0 -[0.26.0]: https://github.com/ehrbase/ehrbase/compare/v0.25.0...v0.26.0 -[0.27.0]: https://github.com/ehrbase/ehrbase/compare/v0.26.0...v0.27.0 -[0.27.1]: https://github.com/ehrbase/ehrbase/compare/v0.27.0...v0.27.1 -[0.27.2]: https://github.com/ehrbase/ehrbase/compare/v0.27.1...v0.27.2 -[0.27.3]: https://github.com/ehrbase/ehrbase/compare/v0.27.2...v0.27.3 -[0.27.4]: https://github.com/ehrbase/ehrbase/compare/v0.27.3...v0.27.4 -[0.28.0]: https://github.com/ehrbase/ehrbase/compare/v0.27.4...v0.28.0 -[0.29.0]: https://github.com/ehrbase/ehrbase/compare/v0.28.0...v0.29.0 -[0.30.0]: https://github.com/ehrbase/ehrbase/compare/v0.29.0...v0.30.0 -[0.31.0]: https://github.com/ehrbase/ehrbase/compare/v0.30.0...v0.31.0 -[0.32.0]: https://github.com/ehrbase/ehrbase/compare/v0.31.0...v0.32.0 +## [2.1.0] + ### Added +* Added `STORED_QUERY_CACHE` ([#1258](https://github.com/ehrbase/ehrbase/pull/1258)) +* Added new config option `ehrbase.security.management.endpoints.web.csrf-validation-enabled` ([#1294](https://github.com/ehrbase/ehrbase/pull/1294),[#1297](https://github.com/ehrbase/ehrbase/pull/1297)) + ### Changed + - Upgrade openEHR_SDK to version 2.10.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md +* Changed `StoredQueryRepository` methods to only accept `StoredQueryQualifiedName` as arguments ([#1258](https://github.com/ehrbase/ehrbase/pull/1258)) + ### Fixed +* Fixed an issue with AQL, which caused NPEs when the query required adding filtering subqueries on a DV_ORDERED path ([#1293](https://github.com/ehrbase/ehrbase/pull/1293)) +* Delete Contribution now returns a 501 Not Implemented instead of 500 as it's not supported since 2.0.0 ([#1278](https://github.com/ehrbase/ehrbase/pull/1278)) + +## [2.0.0] + Welcome to EHRbase 2.0.0. This major release contains a complete overhaul of the data structure and + the Archetype Query Language (AQL) engine. + + See [UPDATING.md](./UPDATING.md) for details on how to update to the new release. + +[2.1.0]: https://github.com/ehrbase/ehrbase/compare/v2.0.0...v2.1.0 +[2.2.0]: https://github.com/ehrbase/ehrbase/compare/v2.1.0...v2.2.0 +[2.3.0]: https://github.com/ehrbase/ehrbase/compare/v2.2.0...v2.3.0 +[2.4.0]: https://github.com/ehrbase/ehrbase/compare/v2.3.0...v2.4.0 +[2.5.0]: https://github.com/ehrbase/ehrbase/compare/v2.4.0...v2.5.0 +[2.6.0]: https://github.com/ehrbase/ehrbase/compare/v2.5.0...v2.6.0 +[2.7.0]: https://github.com/ehrbase/ehrbase/compare/v2.6.0...v2.7.0 diff --git a/Dockerfile b/Dockerfile index 3039546ac..d8de0de04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,137 +1,19 @@ # syntax=docker/dockerfile:1 -FROM --platform=$BUILDPLATFORM postgres:13.3-alpine AS builder -ARG TARGETPLATFORM -ARG BUILDPLATFORM -RUN echo "Running on $BUILDPLATFORM, building EHRbase for $TARGETPLATFORM" > /log +FROM eclipse-temurin:21-jre-alpine -# SHOW POSTGRES SERVER AND CLIENT VERSION -RUN postgres -V && \ - psql -V +RUN addgroup -S ehrbase && adduser -S ehrbase -G ehrbase -# SET POSTGRES DATA DIRECTORY TO CUSTOM FOLDER -# CREATE CUSTOM DATA DIRECTORY AND CHANGE OWNERSHIP TO POSTGRES USER -# INITIALIZE DB IN CUSTOM DATA DIRECTORY -# NOTE: default data directory is /var/lib/postgresql/data and the -# approach of this multi stage dockerfile build does not work with it! -ENV PGDATA="/var/lib/postgresql/pgdata" -RUN mkdir -p ${PGDATA}; \ - chown postgres: ${PGDATA}; \ - chmod 0700 ${PGDATA}; \ - su - postgres -c "initdb -D ${PGDATA}" +USER ehrbase -# COPY DB SETUP SCRIPT -# START DB AND LET THE SCRIPT DO ALL REQUIRED CONFIGURATION -COPY base/db-setup/createdb.sql /postgres/createdb.sql -RUN su - postgres -c "pg_ctl -D ${PGDATA} -w start" && \ - su - postgres -c "psql < /postgres/createdb.sql" && \ - su - postgres -c "pg_ctl -D ${PGDATA} -w stop" +WORKDIR /app -# INSTALL JAVA 17 JDK -#FIXME version is fixed to 17.0.6_p10-r0 because 17.0.7 is currently broken (https://packages.adoptium.net/ui/native/apk/alpine/main/x86_64/) -ENV JAVA_HOME /usr/lib/jvm/java-17-temurin -ENV PATH $JAVA_HOME/bin:$PATH -RUN apk update && apk upgrade --no-cache -RUN wget -O /etc/apk/keys/adoptium.rsa.pub https://packages.adoptium.net/artifactory/api/security/keypair/public/repositories/apk && \ - echo 'https://packages.adoptium.net/artifactory/apk/alpine/main' >> /etc/apk/repositories && \ - su -c "apk add temurin-17=17.0.6_p10-r0" && \ - java --version +COPY /application/target/ehrbase.jar /app/ehrbase.jar +COPY --chown=ehrbase:ehrbase /docker-entrypoint.sh /app/docker-entrypoint -# INSTALL MAVEN -ENV MAVEN_VERSION 3.8.6 -ENV MAVEN_HOME /usr/lib/mvn -ENV PATH $MAVEN_HOME/bin:$PATH -RUN wget http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz && \ - tar -zxvf apache-maven-$MAVEN_VERSION-bin.tar.gz && \ - rm apache-maven-$MAVEN_VERSION-bin.tar.gz && \ - mv apache-maven-$MAVEN_VERSION /usr/lib/mvn && \ - mvn --version - -# COPY POMs -RUN ls -la -COPY ./pom.xml ./pom.xml -COPY ./api/pom.xml ./api/pom.xml -COPY ./application/pom.xml ./application/pom.xml -COPY ./base/pom.xml ./base/pom.xml -COPY ./jooq-pq/pom.xml ./jooq-pq/pom.xml -COPY ./rest-ehr-scape/pom.xml ./rest-ehr-scape/pom.xml -COPY ./rest-openehr/pom.xml ./rest-openehr/pom.xml -COPY ./service/pom.xml ./service/pom.xml -COPY ./test-coverage/pom.xml ./test-coverage/pom.xml -COPY ./plugin/pom.xml ./plugin/pom.xml -COPY ./bom/pom.xml ./bom/pom.xml - -# COPY SOURCEFILES -COPY ./api/src ./api/src -COPY ./application/src ./application/src -COPY ./base/src ./base/src -COPY ./jooq-pq/src ./jooq-pq/src -COPY ./rest-ehr-scape/src ./rest-ehr-scape/src -COPY ./rest-openehr/src ./rest-openehr/src -COPY ./service/src ./service/src -COPY ./plugin/src ./plugin/src - -# START DB AND PACKAGE EHRBASE .JAR -RUN ls -la; \ - su - postgres -c "pg_ctl -D ${PGDATA} -w start" && \ - mvn package -Dmaven.javadoc.skip=true -Djacoco.skip=true -Dmaven.test.skip && \ - su - postgres -c "pg_ctl -D ${PGDATA} -w stop" - -# WRITE EHRBASE VERSION TO A FILE -# MOVE EHRBASE.jar TO /tmp FOLDER -RUN ls -la; \ - EHRBASE_VERSION=$(mvn -q -Dexec.executable="echo" \ - -Dexec.args='${project.version}' \ - --non-recursive exec:exec) && \ - echo ${EHRBASE_VERSION} > /tmp/ehrbase_version && \ - cp application/target/application-${EHRBASE_VERSION}.jar /tmp/ehrbase.jar - - - - - -# FINAL EHRBASE IMAGE WITH JRE AND JAR ONLY -FROM --platform=$BUILDPLATFORM eclipse-temurin:17-jre-ubi9-minimal AS final -COPY --from=builder /tmp/ehrbase.jar . -COPY --from=builder /tmp/ehrbase_version . -COPY .docker_scripts/docker-entrypoint.sh . -RUN chmod +x ./docker-entrypoint.sh; \ - echo "EHRBASE_VERSION: $(cat ehrbase_version)" - -# SET DEFAULT ENVS (CAN BE OVERRITEN FROM CLI VIA --build-arg FLAG) -ARG DB_URL=jdbc:postgresql://ehrdb:5432/ehrbase -ARG DB_USER="ehrbase" -ARG DB_PASS="ehrbase" -ARG SERVER_NODENAME=local.ehrbase.org - -# THESE ENVIRONMENT VARIABLES ARE ALSO APPLIED TO STARTUP OF THE CONTAINER -# AND CAN BE OVERWRITTEN WITH THE '-e' FLAG ON 'docker run' COMMAND -ENV EHRBASE_VERSION=${EHRBASE_VERSION} -ENV DB_USER=${DB_USER} -ENV DB_PASS=${DB_PASS} -ENV DB_URL=${DB_URL} -ENV SERVER_NODENAME=${SERVER_NODENAME} - -# SECURITY ENVs -ENV SECURITY_AUTHTYPE="NONE" -ENV SECURITY_AUTHUSER="ehrbase-user" -ENV SECURITY_AUTHPASSWORD="SuperSecretPassword" -ENV SECURITY_AUTHADMINUSER="ehrbase-admin" -ENV SECURITY_AUTHADMINPASSWORD="EvenMoreSecretPassword" -ENV SECURITY_OAUTH2USERROLE="USER" -ENV SECURITY_OAUTH2ADMINROLE="ADMIN" -ENV SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI="" - -# STATUS METRIC ENDPOINT ENVs -ENV MANAGEMENT_ENDPOINTS_WEB_EXPOSURE="env,health,info,metrics,prometheus" -ENV MANAGEMENT_ENDPOINTS_WEB_BASEPATH="/management" -ENV MANAGEMENT_ENDPOINT_ENV_ENABLED="false" -ENV MANAGEMENT_ENDPOINT_HEALTH_ENABLED="false" -ENV MANAGEMENT_ENDPOINT_HEALTH_DATASOURCE_ENABLED="false" -ENV MANAGEMENT_ENDPOINT_INFO_ENABLED="false" -ENV MANAGEMENT_ENDPOINT_METRICS_ENABLED="false" -ENV MANAGEMENT_ENDPOINT_PROMETHEUS_ENABLED="false" -ENV MANAGEMENT_ENDPOINT_HEALTH_PROBES_ENABLED="true" -ENV CACHE_ENABLED="true" +RUN chown -R ehrbase:ehrbase /app &\ + chmod +x /app/docker-entrypoint EXPOSE 8080 -CMD ./docker-entrypoint.sh + +# wrapped in entrypoint to be able to accept cli args and use jacoco cli env var +ENTRYPOINT ["/app/docker-entrypoint"] diff --git a/Dockerfile_postgres b/Dockerfile_postgres new file mode 100644 index 000000000..ddae8bd24 --- /dev/null +++ b/Dockerfile_postgres @@ -0,0 +1,27 @@ +ARG POSTGRES_VERSION + +# syntax=docker/dockerfile:1 +FROM postgres:${POSTGRES_VERSION}-alpine + +RUN apk --no-cache add musl-locales + +# SHOW POSTGRES SERVER AND CLIENT VERSION +RUN postgres -V; \ + psql -V + +# SET DEFAULT VALUES FOR DATABASE USER AND PASSWORDS +ARG EHRBASE_USER="ehrbase_restricted" +ARG EHRBASE_PASSWORD="ehrbase_restricted" +ARG EHRBASE_USER_ADMIN="ehrbase" +ARG EHRBASE_PASSWORD_ADMIN="ehrbase" +ARG POSTGRES_PASSWORD="postgres" +ENV EHRBASE_USER_ADMIN=${EHRBASE_USER_ADMIN} +ENV EHRBASE_PASSWORD_ADMIN=${EHRBASE_PASSWORD_ADMIN} +ENV EHRBASE_USER=${EHRBASE_USER} +ENV EHRBASE_PASSWORD=${EHRBASE_PASSWORD} +ENV POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + +# COPY DB SETUP SCRIPT TO POSTGRES's DEFAULT DOCKER ENTRYPOINT FOLDER +# NOTE: check postgres's docker docs for details +# https://hub.docker.com/_/postgres/ +COPY createdb-docker.sql /docker-entrypoint-initdb.d/ diff --git a/LICENSE b/LICENSE index 0e3db4f5d..7a4a3ea24 100755 --- a/LICENSE +++ b/LICENSE @@ -1,601 +1,202 @@ -Copyright (c) 2018-2019 Vitasystems GmbH and Hannover Medical School. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ------------------------------------------------------------------------------------- - -EHRbase contains code and derived code from EtherCIS (ethercis.org) which has been developed by Christian Chevalley (ADOC Software Development Co.,Ltd). -EtherCIS is also licensed under the Apache License, Version 2.0. Copy of the license: - -Copyright (c) Ripple Foundation CIC Ltd, UK, 2017 - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ------------------------------------------------------------------------------------- - -EHRbase bundles various third-party components under diverse open source licenses. -This section summarizes those components and their licenses. If required, find full license -texts at the bottom of this file. - -Apache License, Version 2.0 (see https://www.apache.org/licenses/LICENSE-2.0): -- Archie Library (https://github.com/nedap/archie) -- Spring Boot (http://projects.spring.io) -- Keycloak (http://keycloak.org) -- JOOQ (http://www.jooq.org) -- Maven (http://maven.apache.org) -- Tomcat (Embedded) (http://tomcat.apache.org/) -- Flyway-Core (https://flywaydb.org/flyway-core) -- Spring Framework (https://github.com/spring-projects) -- Spring Security (http://spring.io/spring-security) -- Jackson (http://github.com/FasterXML/jackson) -- Springfox (https://github.com/springfox) -- Swagger (https://github.com/swagger-api) -- Apache Commons Collections (http://commons.apache.org) -- Apache Http (http://hc.apache.org/) -- Apache Log4j (http://logging.apache.org) -- Apache XML Beans (http://xmlbeans.apache.org) -- Joda-Time (http://www.joda.org/joda-time/) -- Guava: Google Core Libraries for Java (https://github.com/google/guava/guava) -- Gson (https://github.com/google/gson) -- FindBugs-jsr305 (http://findbugs.sourceforge.net/) -- Everit JSON Schema(https://github.com/everit-org/json-schema) -- Apache Maven Surefire Report Plugin (https://maven.apache.org/components/surefire/) -- JSR107 API and SPI (https://github.com/jsr107/jsr107spec) -- AssertJ (https://github.com/assertj/) -- json-io (https://github.com/jdereg/json-io) -- JUnit Toolbox (https://github.com/MichaelTamm/junit-toolbox) -- Spring Plugin - Metadata Extension, (https://repo.spring.io/release/org/springframework/plugin/spring-plugin-metadata/) -- Jayway JsonPath (https://github.com/json-path/JsonPath) -- JCTree (https://github.com/gauravsaxena81/jctree) -- Fast-Serialization (FST) (https://github.com/RuedigerMoeller/fast-serialization) -- XMLUnit (https://github.com/xmlunit/xmlunit) -- Plexus Interpolation API, (http://plexus.codehaus.org/plexus-components/plexus-interpolation) -- Plexus Common Utilities, (http://plexus.codehaus.org/plexus-utils) -- Default Plexus Container,(https://codehaus-plexus.github.io/plexus-containers/plexus-container-default/) -- SnakeYAML (http://www.snakeyaml.org) -- StAX API (http://stax.codehaus.org/) -- Objenesis (http://objenesis.org) -- Bean Validation API (http://beanvalidation.org) -- Woodstox (https://github.com/FasterXML/woodstox) -- ClassMate, (http://github.com/FasterXML/java-classmate) -- MapStruct Core (http://mapstruct.org/mapstruct/) -- Apache FreeMarker (http://freemarker.org/) -- Handy URI Templates (https://github.com/damnhandy/Handy-URI-Templates) -- Dockerfile Maven (https://github.com/spotify/dockerfile-maven) -- RobotFramework (https://robotframework.org) -- Selenium (https://www.seleniumhq.org/) -- RestInstance (https://github.com/asyrjasalo/RESTinstance) -- Robotframework-Database-Library (http://franz-see.github.io/Robotframework-Database-Library/) - -BSD License: -- ANTLR 4 (http://www.antlr.org), The BSD License (see below) -- PostgreSQL JDBC Driver - JDBC 4.2 (https://github.com/pgjdbc/pgjdbc), BSD-2-Clause (see below) -- JScience (http://jscience.org/), JScience BSD License (see below) -- Temporal Tables Extension, BSD 2-Clause "Simplified" License (see below) - -MIT License: -- Mockito (https://site.mockito.org/) (see below) -- SLF4J (http://www.slf4j.org) (see below) -- DeepDiff (https://pypi.org/project/deepdiff/) (see below) -- Robotframework-Requests (https://github.com/bulkan/robotframework-requests) (see below) -- Robot-Framework Metrics (https://github.com/adiralashiva8/robotframework-metrics) - -Others: -- JAXB (http://jaxb.java.net), Common Development and Distribution License 1.0 (see https://opensource.org/licenses/CDDL-1.0) -- Jdom (https://github.com/hunterhacker/jdom/), Jdom license (see below) -- JUnit (http://junit.org), Eclipse Public License - v 1.0 (see below) -- JSON Java (https://github.com/douglascrockford/JSON-java), The JSON License (see below) -- Backport of JSR 166 (http://backport-jsr166.sourceforge.net/), Creative Commons Public Domain (see https://creativecommons.org/licenses/publicdomain/) -- Bouncy Castle (http://www.bouncycastle.org), Bouncy Castle Licence (see below) -- Reflections (http://github.com/ronmamo/reflections), Do What the Fuck You Want to Public License (see http://www.wtfpl.net/) -- JsQuery – json query language with GIN indexing support (https://github.com/postgrespro/jsquery), PostgreSQL License (see below) -- psycopg2 - Python-PostgreSQL Database Adapter (https://github.com/psycopg/psycopg2), GNU Lesser General Public License - - ----- -self4j license: - - Copyright (c) 2004-2017 QOS.ch - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---- - - JSON Java license: - - ============================================================================ - -Copyright (c) 2002 JSON.org - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -The Software shall be used for Good, not Evil. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ----- - -ANTLR 4 License: - -The BSD License - -Copyright (c) 2012 Terence Parr and Sam Harwell -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - - -PostgreSQL JDBC Driver: - -Copyright (c) 1997, PostgreSQL Global Development Group -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - ----- - -Mockito License: - -The MIT License - -Copyright (c) 2007 Mockito contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ----- - -JScience License: - -JScience - Java(TM) Tools and Libraries for the Advancement of Sciences. -Copyright (C) 2006 - JScience (http://jscience.org/) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice - and include this license agreemeent. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - -Jdom license: - -Copyright (C) 2000-2012 Jason Hunter & Brett McLaughlin. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions, and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions, and the disclaimer that follows - these conditions in the documentation and/or other materials - provided with the distribution. - - 3. The name "JDOM" must not be used to endorse or promote products - derived from this software without prior written permission. For - written permission, please contact . - - 4. Products derived from this software may not be called "JDOM", nor - may "JDOM" appear in their name, without prior written permission - from the JDOM Project Management . - - In addition, we request (but do not require) that you include in the - end-user documentation provided with the redistribution and/or in the - software itself an acknowledgement equivalent to the following: - "This product includes software developed by the - JDOM Project (http://www.jdom.org/)." - Alternatively, the acknowledgment may be graphical using the logos - available at http://www.jdom.org/images/logos. - - THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED - WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE JDOM AUTHORS OR THE PROJECT - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF - USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - SUCH DAMAGE. - - This software consists of voluntary contributions made by many - individuals on behalf of the JDOM Project and was originally - created by Jason Hunter and - Brett McLaughlin . For more information - on the JDOM Project, please see . - ----- - -Bouncy Castle License - -LICENSE -Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software -and associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -JUnit License (Eclipse Public License - v 1.0) - -Eclipse Public License - v 1.0 -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, -REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and -b) in the case of each subsequent Contributor: -i) changes to the Program, and -ii) additions to the Program; - -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. -A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. -Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own -license agreement, and (ii) are not derivative works of the Program. - -"Contributor" means any person or entity that distributes the Program. -"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. -"Program" means the Contributions distributed in accordance with this Agreement. -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. - -3. REQUIREMENTS -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: -a) it complies with the terms and conditions of this Agreement; and - -b) its license agreement: -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. - -When the Program is made available in source code form: -a) it must be made available under this Agreement; and -b) a copy of this Agreement must be included with each copy of the Program. - -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. -While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product -offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program -in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor -("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions -brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor -in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any -claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: -a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and -cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may -participate in any such claim at its own expense. -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a -Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, -those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor -would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other -Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A -PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks -associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable -laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS -GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or -enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision -shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) -alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), -then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this - Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights - under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, - Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is -copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) -of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the -initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. -Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be -distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, - Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) - and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, - by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. -No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party -waives its rights to a jury trial in any resulting litigation. - ----- - -Copyright (c) 2012-2017, Vladislav Arkhipov -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -© 2019 GitHub, Inc. - ----- - -JSQuery License: - -JsQuery is released under the PostgreSQL License, a liberal Open Source license, similar to the BSD or MIT licenses. - -Copyright (c) 2014-2018, Postgres Professional -Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group -Portions Copyright (c) 1994, The Regents of the University of California - -Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. - -IN NO EVENT SHALL POSTGRES PROFESSIONAL BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF POSTGRES PROFESSIONAL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -POSTGRES PROFESSIONAL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND POSTGRES PROFESSIONAL HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - ----- - -Robot-Framework Metrics License: - -MIT License - -Copyright (c) 2019 Shiva Prasad Adirala - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.) - ------ - -Robotframework-Requests License - -Copyright (c) 2016 Bulkan Evcimen - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -DeepDiff License: - -The MIT License (MIT) - -Copyright (c) 2014 - 2016 Sep Ehr (Seperman) and contributors -www.zepworks.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ----- - -psycopg2 License: - -GNU Lesser General Public License - -Copyright (c) 2010—2019 — Daniele Varrazzo - -psycopg2 is free software: you can redistribute it and/or modify it -under the terms of the GNU Lesser General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -psycopg2 is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -License for more details. - -In addition, as a special exception, the copyright holders give -permission to link this program with the OpenSSL library (or with -modified versions of OpenSSL that use the same license as OpenSSL), -and distribute linked combinations including the two. - -You must obey the GNU Lesser General Public License in all respects for -all of the code used other than OpenSSL. If you modify file(s) with this -exception, you may extend this exception to your version of the file(s), -but you are not obligated to do so. If you do not wish to do so, delete -this exception statement from your version. If you delete this exception -statement from all source files in the program, then also delete it here. - -You should have received a copy of the GNU Lesser General Public License -along with psycopg2 (see the doc/ directory.) -If not, see . - ----- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..7489c7b09 --- /dev/null +++ b/NOTICE @@ -0,0 +1,589 @@ +EHRbase was developed in a collaborative effort between Vitasystems GmbH and Hannover Medical School. + +------------------------------------------------------------------------------------ + +EHRbase contains code and derived code from EtherCIS (ethercis.org) which has been developed by Christian Chevalley (ADOC Software Development Co.,Ltd). +EtherCIS is also licensed under the Apache License, Version 2.0. Copy of the license: + +Copyright (c) Ripple Foundation CIC Ltd, UK, 2017 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +------------------------------------------------------------------------------------ + +EHRbase bundles various third-party components under diverse open source licenses. +This section summarizes those components and their licenses. If required, find full license +texts at the bottom of this file. + +Apache License, Version 2.0 (see https://www.apache.org/licenses/LICENSE-2.0): +- Archie Library (https://github.com/nedap/archie) +- Spring Boot (http://projects.spring.io) +- Keycloak (http://keycloak.org) +- JOOQ (http://www.jooq.org) +- Maven (http://maven.apache.org) +- Tomcat (Embedded) (http://tomcat.apache.org/) +- Flyway-Core (https://flywaydb.org/flyway-core) +- Spring Framework (https://github.com/spring-projects) +- Spring Security (http://spring.io/spring-security) +- Jackson (http://github.com/FasterXML/jackson) +- Springfox (https://github.com/springfox) +- Swagger (https://github.com/swagger-api) +- Apache Commons Collections (http://commons.apache.org) +- Apache Http (http://hc.apache.org/) +- Apache Log4j (http://logging.apache.org) +- Apache XML Beans (http://xmlbeans.apache.org) +- Joda-Time (http://www.joda.org/joda-time/) +- Guava: Google Core Libraries for Java (https://github.com/google/guava/guava) +- Gson (https://github.com/google/gson) +- FindBugs-jsr305 (http://findbugs.sourceforge.net/) +- Everit JSON Schema(https://github.com/everit-org/json-schema) +- Apache Maven Surefire Report Plugin (https://maven.apache.org/components/surefire/) +- JSR107 API and SPI (https://github.com/jsr107/jsr107spec) +- AssertJ (https://github.com/assertj/) +- json-io (https://github.com/jdereg/json-io) +- JUnit Toolbox (https://github.com/MichaelTamm/junit-toolbox) +- Spring Plugin - Metadata Extension, (https://repo.spring.io/release/org/springframework/plugin/spring-plugin-metadata/) +- Jayway JsonPath (https://github.com/json-path/JsonPath) +- JCTree (https://github.com/gauravsaxena81/jctree) +- Fast-Serialization (FST) (https://github.com/RuedigerMoeller/fast-serialization) +- XMLUnit (https://github.com/xmlunit/xmlunit) +- Plexus Interpolation API, (http://plexus.codehaus.org/plexus-components/plexus-interpolation) +- Plexus Common Utilities, (http://plexus.codehaus.org/plexus-utils) +- Default Plexus Container,(https://codehaus-plexus.github.io/plexus-containers/plexus-container-default/) +- SnakeYAML (http://www.snakeyaml.org) +- StAX API (http://stax.codehaus.org/) +- Objenesis (http://objenesis.org) +- Bean Validation API (http://beanvalidation.org) +- Woodstox (https://github.com/FasterXML/woodstox) +- ClassMate, (http://github.com/FasterXML/java-classmate) +- MapStruct Core (http://mapstruct.org/mapstruct/) +- Apache FreeMarker (http://freemarker.org/) +- Handy URI Templates (https://github.com/damnhandy/Handy-URI-Templates) +- Dockerfile Maven (https://github.com/spotify/dockerfile-maven) +- RobotFramework (https://robotframework.org) +- Selenium (https://www.seleniumhq.org/) +- RestInstance (https://github.com/asyrjasalo/RESTinstance) +- Robotframework-Database-Library (http://franz-see.github.io/Robotframework-Database-Library/) + +BSD License: +- ANTLR 4 (http://www.antlr.org), The BSD License (see below) +- PostgreSQL JDBC Driver - JDBC 4.2 (https://github.com/pgjdbc/pgjdbc), BSD-2-Clause (see below) +- JScience (http://jscience.org/), JScience BSD License (see below) +- Temporal Tables Extension, BSD 2-Clause "Simplified" License (see below) + +MIT License: +- Mockito (https://site.mockito.org/) (see below) +- SLF4J (http://www.slf4j.org) (see below) +- DeepDiff (https://pypi.org/project/deepdiff/) (see below) +- Robotframework-Requests (https://github.com/bulkan/robotframework-requests) (see below) +- Robot-Framework Metrics (https://github.com/adiralashiva8/robotframework-metrics) + +Others: +- JAXB (http://jaxb.java.net), Common Development and Distribution License 1.0 (see https://opensource.org/licenses/CDDL-1.0) +- Jdom (https://github.com/hunterhacker/jdom/), Jdom license (see below) +- JUnit (http://junit.org), Eclipse Public License - v 1.0 (see below) +- JSON Java (https://github.com/douglascrockford/JSON-java), The JSON License (see below) +- Backport of JSR 166 (http://backport-jsr166.sourceforge.net/), Creative Commons Public Domain (see https://creativecommons.org/licenses/publicdomain/) +- Bouncy Castle (http://www.bouncycastle.org), Bouncy Castle Licence (see below) +- Reflections (http://github.com/ronmamo/reflections), Do What the Fuck You Want to Public License (see http://www.wtfpl.net/) +- JsQuery – json query language with GIN indexing support (https://github.com/postgrespro/jsquery), PostgreSQL License (see below) +- psycopg2 - Python-PostgreSQL Database Adapter (https://github.com/psycopg/psycopg2), GNU Lesser General Public License + + +---- +self4j license: + + Copyright (c) 2004-2017 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---- + + JSON Java license: + + ============================================================================ + +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---- + +ANTLR 4 License: + +The BSD License + +Copyright (c) 2012 Terence Parr and Sam Harwell +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + + +PostgreSQL JDBC Driver: + +Copyright (c) 1997, PostgreSQL Global Development Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +---- + +Mockito License: + +The MIT License + +Copyright (c) 2007 Mockito contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---- + +JScience License: + +JScience - Java(TM) Tools and Libraries for the Advancement of Sciences. +Copyright (C) 2006 - JScience (http://jscience.org/) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice + and include this license agreemeent. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Jdom license: + +Copyright (C) 2000-2012 Jason Hunter & Brett McLaughlin. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions, and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer that follows + these conditions in the documentation and/or other materials + provided with the distribution. + + 3. The name "JDOM" must not be used to endorse or promote products + derived from this software without prior written permission. For + written permission, please contact . + + 4. Products derived from this software may not be called "JDOM", nor + may "JDOM" appear in their name, without prior written permission + from the JDOM Project Management . + + In addition, we request (but do not require) that you include in the + end-user documentation provided with the redistribution and/or in the + software itself an acknowledgement equivalent to the following: + "This product includes software developed by the + JDOM Project (http://www.jdom.org/)." + Alternatively, the acknowledgment may be graphical using the logos + available at http://www.jdom.org/images/logos. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE JDOM AUTHORS OR THE PROJECT + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + This software consists of voluntary contributions made by many + individuals on behalf of the JDOM Project and was originally + created by Jason Hunter and + Brett McLaughlin . For more information + on the JDOM Project, please see . + +---- + +Bouncy Castle License + +LICENSE +Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +JUnit License (Eclipse Public License - v 1.0) + +Eclipse Public License - v 1.0 +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, +REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and +b) in the case of each subsequent Contributor: +i) changes to the Program, and +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. +A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. +Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own +license agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. +"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. +"Program" means the Contributions distributed in accordance with this Agreement. +"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. + +2. GRANT OF RIGHTS +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. + +3. REQUIREMENTS +A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: +i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; +ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; +iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and +iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: +a) it must be made available under this Agreement; and +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the Program. + +Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. +While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program +in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor +("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions +brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor +in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any +claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: +a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and +cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a +Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, +those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor +would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks +associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable +laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS +GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or +enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision +shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), +then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights + under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, + Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is +copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) +of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the +initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. +Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be +distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, + Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) + and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, + by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. +No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party +waives its rights to a jury trial in any resulting litigation. + +---- + +Copyright (c) 2012-2017, Vladislav Arkhipov +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +© 2019 GitHub, Inc. + +---- + +JSQuery License: + +JsQuery is released under the PostgreSQL License, a liberal Open Source license, similar to the BSD or MIT licenses. + +Copyright (c) 2014-2018, Postgres Professional +Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group +Portions Copyright (c) 1994, The Regents of the University of California + +Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. + +IN NO EVENT SHALL POSTGRES PROFESSIONAL BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF POSTGRES PROFESSIONAL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +POSTGRES PROFESSIONAL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND POSTGRES PROFESSIONAL HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +---- + +Robot-Framework Metrics License: + +MIT License + +Copyright (c) 2019 Shiva Prasad Adirala + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.) + +----- + +Robotframework-Requests License + +Copyright (c) 2016 Bulkan Evcimen + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----- + +DeepDiff License: + +The MIT License (MIT) + +Copyright (c) 2014 - 2016 Sep Ehr (Seperman) and contributors +www.zepworks.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---- + +psycopg2 License: + +GNU Lesser General Public License + +Copyright (c) 2010—2019 — Daniele Varrazzo + +psycopg2 is free software: you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +psycopg2 is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +License for more details. + +In addition, as a special exception, the copyright holders give +permission to link this program with the OpenSSL library (or with +modified versions of OpenSSL that use the same license as OpenSSL), +and distribute linked combinations including the two. + +You must obey the GNU Lesser General Public License in all respects for +all of the code used other than OpenSSL. If you modify file(s) with this +exception, you may extend this exception to your version of the file(s), +but you are not obligated to do so. If you do not wish to do so, delete +this exception statement from your version. If you delete this exception +statement from all source files in the program, then also delete it here. + +You should have received a copy of the GNU Lesser General Public License +along with psycopg2 (see the doc/ directory.) +If not, see . + +---- diff --git a/README.md b/README.md index 7695461fa..3520a35c0 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,72 @@ -# EHRbase -![Maven Central](https://img.shields.io/maven-central/v/org.ehrbase.openehr/server) ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/ehrbase/ehrbase?sort=semver) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ehrbase_ehrbase&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ehrbase_ehrbase) [![Documentation Status](https://readthedocs.org/projects/ehrbase/badge/?version=latest)](https://ehrbase.readthedocs.io/en/latest/?badge=latest) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) +# EHRbase -[![EHRbase Logo](ehrbase.png)](ehrbase.png) +![Maven Central](https://img.shields.io/maven-central/v/org.ehrbase.openehr/server) ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/ehrbase/ehrbase?sort=semver) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ehrbase_ehrbase&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ehrbase_ehrbase) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) -EHRbase is an [openEHR](https://www.openehr.org/) Clinical Data Repository, providing a standard-based backend for interoperable clinical applications. It implements the latest version of the openEHR Reference Model (RM 1.1.0) and version 1.4 of the Archetype Definition Language (ADL). Applications can use the capabilities of EHRbase through the latest version of the [openEHR REST API](https://specifications.openehr.org/releases/ITS-REST/latest/) and model-based queries using the [Archetype Query Language](https://specifications.openehr.org/releases/QUERY/latest/AQL.html). +[![EHRbase Logo](ehrbase.png)](ehrbase.png) + +EHRbase is an [openEHR](https://www.openehr.org/) Clinical Data Repository, providing a standard-based backend for +interoperable clinical applications. It implements the latest version of the openEHR Reference Model (RM 1.1.0) and +version 1.4 of the Archetype Definition Language (ADL). Applications can use the capabilities of EHRbase through the +latest version of the [openEHR REST API](https://specifications.openehr.org/releases/ITS-REST/latest/) and model-based +queries using the [Archetype Query Language](https://specifications.openehr.org/releases/QUERY/latest/AQL.html). ---- ## Release notes -Please check the [CHANGELOG](https://github.com/ehrbase/ehrbase/blob/develop/CHANGELOG.md) and / or [EHRbase Documentation](https://ehrbase.readthedocs.io/en/latest/) for more details. +Please check the [CHANGELOG](https://github.com/ehrbase/ehrbase/blob/develop/CHANGELOG.md) ## Documentation -[EHRbase Documentation](https://ehrbase.readthedocs.io/en/latest/) is build with Sphinx and hosted on [Read the Docs](https://readthedocs.org/). - -## Quick Start: Run EHRbase with Docker -See our [Run EHRbase + DB with Docker-Compose](https://ehrbase.readthedocs.io/en/latest/03_development/04_docker_images/01_ehrbase/02_use_image/index.html#run-ehrbase-db-with-docker-compose) documentation page for a quick start. - -## Acknowledgments - -EHRbase contains code and derived code from EtherCIS (ethercis.org) which has been developed by Christian Chevalley (ADOC Software Development Co.,Ltd). -Dr. Tony Shannon and Phil Berret of the [Ripple Foundation CIC Ltd, UK](https://ripple.foundation/) and Dr. Ian McNicoll (FreshEHR Ltd.) greatly contributed to EtherCIS. -EHRbase heavily relies on the openEHR Reference Model implementation ([Archie](https://github.com/openEHR/archie)) made by Nedap. Many thanks to Pieter Bos and his team for their work! +Check out the documentation at https://docs.ehrbase.org -EHRbase is jointly developed by [Vitasystems GmbH](https://www.vitagroup.ag/de_DE/Ueber-uns/vitasystems) and [Peter L. Reichertz Institute for Medical Informatics of TU Braunschweig and Hannover Medical School](https://www.plri.de/) +## Quick Start: Run EHRbase with Docker ----- +Check out the Installation guide at https://docs.ehrbase.org/docs/EHRbase/installation ## Building and Installing EHRbase -These instructions will get you a copy of the project up and running on your local machine **for development and testing purposes**. Please read these instructions carefully. See [deployment](#deployment) for notes on how to deploy the project on a live system. -### Prerequisites +These instructions will get you a copy of the project up and running on your local machine **for development and testing +purposes**. Please read these instructions carefully. See [deployment](#deployment) for notes on how to deploy the +project on a live system. -You will need Java JDK/JRE 17 (preferably openJDK: e.g. from https://adoptopenjdk.net/) +### Prerequisites -You will need a Postgres Database (at least Version 10.4, Version 13 recommended) (Docker image or local installation). -We recommend the Docker -image to get started quickly. +You will need Java JDK/JRE 21 (preferably openJDK: e.g. from https://adoptopenjdk.net/) +You will need a Postgres Database (at least Version 15 or higher, Version 16 recommended) (Docker image or local installation). +We recommend the Docker image to get started quickly. ### Installing #### 1. Setup database -> NOTE: Building EHRbase requires a properly set up and running DB for the following steps. +> NOTE: Building EHRbase requires a properly set-up and running DB for the following steps. -Run `./base/db-setup/createdb.sql` as `postgres` User. +Run `./createdb.sql` as `postgres` User. You can also use this Docker image which is a preconfigured Postgres database: + ```shell docker network create ehrbase-net - docker run --name ehrdb --network ehrbase-net -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 ehrbase/ehrbase-postgres:13.4.v2 + docker run --name ehrdb --network ehrbase-net -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 ehrbase/ehrbase-v2-postgres:16.2 ``` -(For a preconfigured EHRbase application Docker image and its usage see the [documentation](https://ehrbase.readthedocs.io/en/latest/03_development/04_docker_images/index.html)) +(For a preconfigured EHRbase application Docker image and its usage see the [Installation](https://docs.ehrbase.org/docs/EHRbase/installation) guide. + #### 2. Setup Maven environment Edit the database properties in `./pom.xml` if necessary #### 3. Build EHRbase + Run `mvn package` #### 4. Run EHRbase -Replace the * with the current version, e.g. `application/target/application-0.9.0.jar` +Replace the * with the current version, e.g. `application/target/ehrbase-2.0.0.jar` -`java -jar application/target/application-*.jar` +`java -jar application/target/ehrbase-*.jar` ### Authentication Types @@ -85,7 +85,7 @@ Currently we have support one user with password which can be set via environmen and can be overridden by environment values. Alternatively you can set them inside the corresponding application.yml file. -The same applies to the *admin* user, via `SECURITY_AUTHADMINUSER`, `SECURITY_AUTHADMINPASSWORD` +The same applies to the *admin* user, via `SECURITY_AUTHADMINUSER`, `SECURITY_AUTHADMINPASSWORD` and their default values of `ehrbase-admin` and `EvenMoreSecretPassword`. #### 2. OAuth2 @@ -95,7 +95,6 @@ Environment variable `SECURITY_AUTHTYPE=OAUTH` is enabling OAuth2 authentication Additionally, setting the following variable to point to the existing OAuth2 server and realm is necessary: `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI=http://localhost:8081/auth/realms/ehrbase` - Two roles are available: a user role, and admin role. By default, these roles are expected to be named `USER` and `ADMIN`. The names of these roles can be customised through the `SECURITY_OAUTH2USERROLE` and `SECURITY_OAUTH2ADMINROLE` environment variables. Users should have their roles assigned accordingly, either in the `realm_access.roles` or `scope` @@ -104,20 +103,28 @@ claim of the JWT used for authentication. ## Contributing ### Codestyle/Formatting -EHRbase java sourcecode is using [palantir-java-format](https://github.com/palantir/palantir-java-format) codestyle. -The formatting is checked and applied using the [spotless-maven-plugin](https://github.com/diffplug/spotless/tree/main/plugin-maven). -To apply the codestyle run the `com.diffplug.spotless:spotless-maven-plugin:apply` maven goal in the root directory of the project. -To check if the code conforms to the codestyle run the `com.diffplug.spotless:spotless-maven-plugin:check` maven goal in the root directory of the project. + +EHRbase java sourcecode is using [palantir-java-format](https://github.com/palantir/palantir-java-format) codestyle. +The formatting is checked and applied using +the [spotless-maven-plugin](https://github.com/diffplug/spotless/tree/main/plugin-maven). +To apply the codestyle run the `com.diffplug.spotless:spotless-maven-plugin:apply` maven goal in the root directory of +the project. +To check if the code conforms to the codestyle run the `com.diffplug.spotless:spotless-maven-plugin:check` maven goal in +the root directory of the project. These maven goals can also be run for a single module by running them in the modules' subdirectory. -To make sure all code conforms to the codestyle, the "check-codestyle" check is run on all pull requests. +To make sure all code conforms to the codestyle, the "check-codestyle" check is run on all pull requests. Pull requests not passing this check shall not be merged. -If you wish to automatically apply the formatting on commit for *.java files, a simple pre-commit hook script "pre-commit.sh" is available in the root directory of this repository. -To enable the hook you can either copy the script to or create a symlink for it at `.git/hooks/pre-commit`. -The git hook will run the "apply" goal for the whole project, but formatting changes will only be staged for already staged files, to avoid including unrelated changes. +If you wish to automatically apply the formatting on commit for *.java files, a simple pre-commit hook script " +pre-commit.sh" is available in the root directory of this repository. +To enable the hook you can either copy the script to or create a symlink for it at `.git/hooks/pre-commit`. +The git hook will run the "apply" goal for the whole project, but formatting changes will only be staged for already +staged files, to avoid including unrelated changes. + +In case there is a section of code that you carefully formatted in a special way the formatting can be turned off for +that section like this: -In case there is a section of code that you carefully formatted in a special way the formatting can be turned off for that section like this: ``` everything here will be reformatted.. @@ -130,6 +137,7 @@ everything here will be reformatted.. everything here will be reformatted.. ``` + Please be aware that `@formatter:off/on` should only be used on rare occasions to increase readability of complex code and shall be looked at critically when reviewing merge requests. ## Running the tests @@ -138,7 +146,7 @@ For integration tests please refer to the [integration-test](https://github.com/ ## Deployment - 1. `java -jar application/target/application-*.jar` You can override the application properties (like database settings) using the normal spring boot mechanism: [Command-Line Arguments in Spring Boot](https://www.baeldung.com/spring-boot-command-line-arguments) + 1. `java -jar application/target/ehrbase-*.jar` You can override the application properties (like database settings) using the normal spring boot mechanism: [Command-Line Arguments in Spring Boot](https://www.baeldung.com/spring-boot-command-line-arguments) 2. Browse to Swagger UI --> http://localhost:8080/ehrbase/swagger-ui.html ## Updating @@ -151,14 +159,25 @@ updating. * [Maven](https://maven.apache.org/) - Dependency Management - ---- -## License +## Acknowledgments + +EHRbase contains code and derived code from EtherCIS (ethercis.org) which has been developed by Christian Chevalley ( +ADOC Software Development Co.,Ltd). +Dr. Tony Shannon and Phil Berret of the [Ripple Foundation CIC Ltd, UK](https://ripple.foundation/) and Dr. Ian +McNicoll (FreshEHR Ltd.) greatly contributed to EtherCIS. -EHRbase uses the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) +EHRbase heavily relies on the openEHR Reference Model implementation ([Archie](https://github.com/openEHR/archie)) made +by Nedap. Many thanks to Pieter Bos and his team for their work! +EHRbase is jointly developed by [Vitasystems GmbH](https://www.vitagroup.ag/de_DE/Ueber-uns/vitasystems) +and [Peter L. Reichertz Institute for Medical Informatics of TU Braunschweig and Hannover Medical School](https://www.plri.de/) + + +## License +EHRbase uses the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) ## Stargazers over time diff --git a/UPDATING.md b/UPDATING.md index 11e1a51f3..5e187a9bd 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,121 +1,13 @@ # Updating EHRbase -This file documents any backwards-incompatible changes in EHRBase and -assists users migrating to a new version. +This file documents any backwards-incompatible changes in EHRBase and assists users migrating to a new version. -## EHRbase 0.19.0 +## EHRbase 2.0.0 -### Database Configuration +### Migrating data -The creation of the DB must ensure that SQL interval type is ISO-8601 compliant. This is required to ensure proper -formatting of the resultset. -Scripts provided ensure this encoding is done properly (see `base/db-setup`) with the following statement: +EHRbase 2.0.0 comes with a completely overhauled data structure that is not automatically migrated when deploying this +new version over an older data structure. -``` --- ensure INTERVAL is ISO8601 encoded -alter database ehrbase SET intervalstyle = 'iso_8601'; -``` - -If an old version of the scripts was used this statement needs to be run manually. - -## EHRbase 0.21.0 - -### Switch to native Postgres - -Before 0.21.0 EHRbase offered two setups, one using the -extensions [temporal tables](https://github.com/arkhipov/temporal_tables), -[jsquery](https://github.com/postgrespro/jsquery) and one without. With 0.21.0 EHRbase now always runs against a plain -postgres. -To migrate a postgres without those extensions run `base/db-setup/migrate_to_cloud_db_setup.sql`. This is not needed if -you used the old `base/db-setup/cloud_db_setup.sql` -or run the EHRbase Postgres docker image. - -### Fix Duplicate User issue -Prior to release 0.21.0, EHRbase contained a bug that creates a new internal user for each request. - -The execution of the Flyway migration script `V71__merge_duplicate_users.sql` may take its time as the duplicates are -being consolidated. - -## EHRbase 0.24.0 - -### Switch to non-privileged user for DB Access - -Prior to 0.24.0 used one user for DDL Statements and to run the application's logic. With 0.24.0 these are run with different Users with different DB Privileges. -To migrate run adjust the password in `base/db-setup/add_restricted_user.sql` and run it as DB-Admin in the ehrbase DB. -After that adjust the ehrbase Properties: - -Set the migration to use the user with DDL Privilege: -``` -spring: - flyway: - user: ehrbase - password: ehrbase -``` -And set the application to use the restricted user -``` -spring: - datasource: - username: ehrbase_restricted - password: ehrbase_restricted -``` - -If you use the official Docker image you can also set this via - -``` - environment: - DB_URL: jdbc:postgresql://ehrdb:5432/ehrbase - DB_USER_ADMIN: ehrbase - DB_PASS_ADMIN: ehrbase - DB_USER: ehrbase_restricted - DB_PASS: ehrbase_restricted -``` - -see `\docker-compose.yml ` - -## EHRbase 0.25.0 - -### Switch to new directory structure - -With release 0.25.0 a new Structure to store EHR directory was introduced. There is no automatic migration of old EHR -directory data into the new structure. -If you used EHR directory and are fine with losing this data you can run in postgres as admin - -``` --- remove Ehr directory!!! - -begin; -alter table ehr.ehr drop column if exists directory; -TRUNCATE ehr.folder, ehr.folder_hierarchy, ehr.folder_items,ehr.folder_history,ehr.folder_items_history,ehr.folder_hierarchy_history; -alter table ehr.ehr add column directory uuid references ehr.folder(id); -commit ; -``` - -If you need to migrate old EHR directory data please contact us. - -## EHRbase 0.27.0 - -With release 0.27.0 the multi-tenancy implementation has been updated to allow two EHRs, compositions, -or directories with the same ID to exist in different tenants. This was achieved by replacing the tenant UUID -with an internal number-based ID and adding it to the primary key. - -Please note that executing the Flyway migration script `V83__change_sys_tenant_to_short.sql` may take some time. - -## EHRbase 0.29.0 - -### Fix Duplicated UUIDs and template IDs -Prior to release 0.29.0, EHRbase contained a bug that may have resulted in duplicated UUIDs and template IDs within the tenant -If exception occurs during the migration script execution (`V85__enforce_unique_template_id.sql`) manual interventions are required. - -Action Required: -Ensure that the **ehr.template_store.id** and **ehr.template_store.template_id** columns have unique values within the tenant. - -## EHRbase 0.30.0 - -### Fix storage of Locatable.name -An error in the encoding for Locatable.name was fixed. -With the fixed encoding Locatable.name.mappings and Locatable.name.defining_code are now stored correctly in the DB and Locatable.name.defining_code can also be queried using AQL. -There is a manual migration script available at `base/db-setup/fix-dv_coded_text-locatable-names.sql` which will fix the existing compositions. -This migration is only needed if Locatable.name.defining_code is used in any composition. -Be aware that this migration might take a while, as it will affect every composition in the database. -It will replace the Strings `"codeString":` and `"terminologyId":` with `"code_string":` and `"terminology_id":`. -This replacement is done over the whole JSON stored in the database, so there is a small chance that not only the broken JSON keys are affected. +To support the migrating of data from systems `pre-2.0.0` to `2.0.0`, a migration tool and instructions are provided +at https://github.com/ehrbase/migration-tool. \ No newline at end of file diff --git a/api/pom.xml b/api/pom.xml index 45ccc7152..1dc5cbc55 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -1,7 +1,7 @@ - + - 4.0.0 + 4.0.0 - - org.ehrbase.openehr - server - 0.32.0 - + + org.ehrbase.openehr + server + 2.7.0 + - application - jar + application + jar - - vitasystems/hip-openehr - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-cache - - - org.springframework.boot - spring-boot-starter-data-redis - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-jdbc - - - org.springframework.boot - spring-boot-starter-validation - - - io.micrometer - micrometer-registry-prometheus - - - org.flywaydb - flyway-core - - - org.ehrbase.openehr - service - - - org.ehrbase.openehr - rest-ehr-scape - - - org.ehrbase.openehr - api - - - org.ehrbase.openehr.sdk - serialisation - - - org.ehrbase.openehr - rest-openehr - - - org.ehrbase.openehr - base - - - net.bull.javamelody - javamelody-spring-boot-starter - - - org.springframework.boot - spring-boot-starter-test - test - - - org.junit.vintage - junit-vintage-engine - test - + + + org.ehrbase.openehr + configuration + + + org.ehrbase.openehr + cli + + - - org.springframework.security - spring-security-test - test - - - org.ehrbase.openehr.sdk - test-data - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - repackage - - - org.ehrbase.application.EhrBase - - - - - - com.spotify - dockerfile-maven-plugin - - - - - - - \ No newline at end of file + + ehrbase + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + org.ehrbase.application.EhrBase + false + + + + + diff --git a/application/src/main/java/org/ehrbase/application/EhrBase.java b/application/src/main/java/org/ehrbase/application/EhrBase.java index 9810a4ec4..dd7292e34 100644 --- a/application/src/main/java/org/ehrbase/application/EhrBase.java +++ b/application/src/main/java/org/ehrbase/application/EhrBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -17,30 +17,18 @@ */ package org.ehrbase.application; -import org.ehrbase.ServiceModuleConfiguration; -import org.ehrbase.rest.RestModuleConfiguration; -import org.ehrbase.rest.ehrscape.RestEHRScapeModuleConfiguration; +import java.util.Arrays; +import org.ehrbase.application.cli.EhrBaseCli; +import org.ehrbase.application.server.EhrBaseServer; +import org.ehrbase.cli.CliRunner; import org.springframework.boot.SpringApplication; -import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.context.annotation.Import; -/** - * @author Stefan Spiska - * @since 1.0 - */ -@SpringBootApplication( - exclude = { - ManagementWebSecurityAutoConfiguration.class, - R2dbcAutoConfiguration.class, - SecurityAutoConfiguration.class - }) -@Import({ServiceModuleConfiguration.class, RestEHRScapeModuleConfiguration.class, RestModuleConfiguration.class}) public class EhrBase { public static void main(String[] args) { - SpringApplication.run(EhrBase.class, args); + + SpringApplication app = + Arrays.asList(args).contains(CliRunner.CLI) ? EhrBaseCli.build(args) : EhrBaseServer.build(args); + app.run(args); } } diff --git a/application/src/main/java/org/ehrbase/application/abac/AbacConfig.java b/application/src/main/java/org/ehrbase/application/abac/AbacConfig.java deleted file mode 100644 index 0b0866949..000000000 --- a/application/src/main/java/org/ehrbase/application/abac/AbacConfig.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.abac; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.net.URI; -import java.util.Map; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.ehrbase.api.exception.InternalServerException; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@ConditionalOnProperty(name = "abac.enabled") -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "abac") -@SuppressWarnings("java:S6212") -public class AbacConfig { - - public enum AbacType { - EHR, - EHR_STATUS, - COMPOSITION, - CONTRIBUTION, - QUERY - } - - public enum PolicyParameter { - ORGANIZATION, - PATIENT, - TEMPLATE - } - - static class Policy { - private String name; - private PolicyParameter[] parameters; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public PolicyParameter[] getParameters() { - return parameters; - } - - public void setParameters(PolicyParameter[] parameters) { - this.parameters = parameters; - } - } - - private URI server; - private String organizationClaim; - private String patientClaim; - private Map policy; - - @Bean - public AbacCheck abacCheck(HttpClient httpClient) { - return new AbacCheck(httpClient); - } - - public URI getServer() { - return server; - } - - public void setServer(URI server) { - this.server = server; - } - - public String getOrganizationClaim() { - return organizationClaim; - } - - public void setOrganizationClaim(String organizationClaim) { - this.organizationClaim = organizationClaim; - } - - public String getPatientClaim() { - return patientClaim; - } - - public void setPatientClaim(String patientClaim) { - this.patientClaim = patientClaim; - } - - public Map getPolicy() { - return policy; - } - - public void setPolicy(Map policy) { - this.policy = policy; - } - - /* - This class has only some extracted methods to handle ABAC server connection and requests. - It is mainly a separate class so it can be overwritten by a MockBean in the context of tests. - */ - public static class AbacCheck { - - private final HttpClient httpClient; - - public AbacCheck(HttpClient httpClient) { - this.httpClient = httpClient; - } - - /** - * Helper to build and send the actual HTTP request to the ABAC server. - * - * @param url URL for ABAC server request - * @param bodyMap Map of attributes for the request - * @return HTTP response - * @throws IOException On error during attribute or HTTP handling - */ - public boolean execute(String url, Map bodyMap) throws IOException { - return evaluateResponse(send(url, bodyMap)); - } - - private HttpResponse send(String url, Map bodyMap) throws IOException { - // convert bodyMap to JSON - ObjectMapper objectMapper = new ObjectMapper(); - String requestBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(bodyMap); - - HttpPost request = new HttpPost(url); - request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); - - try { - return httpClient.execute(request); - } catch (Exception e) { - throw new InternalServerException( - "ABAC: Connection with ABAC server failed. Check configuration. Error: " + e.getMessage()); - } - } - - private boolean evaluateResponse(HttpResponse response) { - return response.getStatusLine().getStatusCode() == 200; - } - } -} diff --git a/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionHandler.java b/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionHandler.java deleted file mode 100644 index 43d7796c7..000000000 --- a/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.abac; - -import org.aopalliance.intercept.MethodInvocation; -import org.ehrbase.api.service.CompositionService; -import org.ehrbase.api.service.ContributionService; -import org.ehrbase.api.service.EhrService; -import org.ehrbase.application.abac.AbacConfig.AbacCheck; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Lazy; -import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; -import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; -import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.authentication.AuthenticationTrustResolverImpl; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Component; - -@ConditionalOnProperty(name = "abac.enabled") -@Component -public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler { - - private final AbacConfig abacConfig; - private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - private final CompositionService compositionService; - private final ContributionService contributionService; - private final EhrService ehrService; - private final AbacCheck abacCheck; - - @Lazy - public CustomMethodSecurityExpressionHandler( - AbacConfig abacConfig, - CompositionService compositionService, - ContributionService contributionService, - EhrService ehrService, - AbacCheck abacCheck) { - this.abacConfig = abacConfig; - this.compositionService = compositionService; - this.contributionService = contributionService; - this.ehrService = ehrService; - this.abacCheck = abacCheck; - } - - @Override - protected MethodSecurityExpressionOperations createSecurityExpressionRoot( - Authentication authentication, MethodInvocation invocation) { - CustomMethodSecurityExpressionRoot root = - new CustomMethodSecurityExpressionRoot(authentication, abacConfig, abacCheck); - root.setCompositionService(this.compositionService); - root.setContributionService(this.contributionService); - root.setEhrService(this.ehrService); - root.setPermissionEvaluator(getPermissionEvaluator()); - root.setTrustResolver(this.trustResolver); - root.setRoleHierarchy(getRoleHierarchy()); - return root; - } -} diff --git a/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionRoot.java b/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionRoot.java deleted file mode 100644 index b9d679bee..000000000 --- a/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionRoot.java +++ /dev/null @@ -1,552 +0,0 @@ -/* - * Copyright (c) 2021-2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.abac; - -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.ehrbase.rest.util.AuthHelper.getRequestedJwtClaim; -import static org.springframework.http.HttpStatus.NO_CONTENT; - -import com.nedap.archie.rm.composition.Composition; -import com.nedap.archie.rm.support.identification.ObjectVersionId; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; -import org.ehrbase.api.exception.InternalServerException; -import org.ehrbase.api.service.CompositionService; -import org.ehrbase.api.service.ContributionService; -import org.ehrbase.api.service.EhrService; -import org.ehrbase.application.abac.AbacConfig.AbacCheck; -import org.ehrbase.application.abac.AbacConfig.AbacType; -import org.ehrbase.application.abac.AbacConfig.Policy; -import org.ehrbase.application.abac.AbacConfig.PolicyParameter; -import org.ehrbase.aql.compiler.AuditVariables; -import org.ehrbase.openehr.sdk.response.dto.OriginalVersionResponseData; -import org.ehrbase.openehr.sdk.response.dto.ehrscape.CompositionFormat; -import org.ehrbase.rest.BaseController; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.expression.SecurityExpressionRoot; -import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.Authentication; - -/** - * Implementation of custom security expression, to be used in e.g. @PreAuthorize(..) to allow ABAC - * requests. - * - * @author Jake Smolka - * @since 1.0 - */ -@SuppressWarnings("unused") -public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot - implements MethodSecurityExpressionOperations { - - static final String ORGANIZATION = "organization"; - static final String PATIENT = "patient"; - static final String TEMPLATE = "template"; - static final String PRE = "pre"; - static final String POST = "post"; - - private final AbacConfig abacConfig; - private final AbacCheck abacCheck; - private CompositionService compositionService; - private ContributionService contributionService; - private EhrService ehrService; - private Object filterObject; - private Object returnObject; - - public CustomMethodSecurityExpressionRoot( - Authentication authentication, AbacConfig abacConfig, AbacCheck abacCheck) { - super(authentication); - this.abacConfig = abacConfig; - this.abacCheck = abacCheck; - } - - public void setCompositionService(CompositionService compositionService) { - this.compositionService = compositionService; - } - - public void setContributionService(ContributionService contributionService) { - this.contributionService = contributionService; - } - - public void setEhrService(EhrService ehrService) { - this.ehrService = ehrService; - } - - /** - * Custom SpEL expression to be used to check if the remote ABAC allows the operation by given - * data. For @PostAuthorize cases. - * - * @param type Type of scope's resource - * @param subject Subject ID from the current EHR context - * @param payload Payload object, either request's input or response's output - * @param contentType Content type from the scope - * @return True if ABAC authorizes given attributes - * @throws IOException On parsing error - */ - public boolean checkAbacPost(String type, String subject, Object payload, String contentType) throws IOException { - return checkAbac(type, subject, payload, contentType, POST); - } - - public boolean checkAbacPostQuery(Object payload) throws IOException { - return checkAbac(BaseController.QUERY, null, payload, null, POST); - } - - /** - * Custom SpEL expression to be used to check if the remote ABAC allows the operation by given - * data. For @PreAuthorize cases. - * - * @param type Type of scope's resource - * @param subject Subject ID from the current EHR context - * @param payload Payload object, either request's input or response's output - * @param contentType Content type from the scope - * @return True if ABAC authorizes given attributes - * @throws IOException On parsing error - */ - public boolean checkAbacPre(String type, String subject, Object payload, String contentType) throws IOException { - // @PreAuthorize will give different types, e.g. String (for composition), EhrStatus,... - // so just pipe it through to templateHandling and make by-type handling there - return checkAbac(type, subject, payload, contentType, PRE); - } - - /* - Short call with less parameters. - */ - public boolean checkAbacPre(String type, String subject) throws IOException { - return checkAbac(type, subject, null, null, PRE); - } - - /** - * Builds the ABAC request with given data and evaluates the ABAC's response. - * @param type Object type of scope - * @param subject Subject ID from the current EHR context - * @param payload Payload object, either request's input or response's output - * @param contentType Content type from the scope - * @param authType Pre- or PostAuthorize, determines payload style (string or object) - * @return True if ABAC returns a positive feedback, False if not - * @throws IOException On parsing error - */ - private boolean checkAbac(String type, String subject, Object payload, String contentType, String authType) - throws IOException { - // Set type specific settings: - // Extract and set parameters according to which parameters are configured - List policyParameters; - // Build abac server request, depending on type - var requestUrl = abacConfig.getServer().toString(); - - Map policy = abacConfig.getPolicy(); - - switch (type) { - case BaseController.EHR: - policyParameters = Arrays.asList(policy.get(AbacType.EHR).getParameters()); - requestUrl = requestUrl.concat(policy.get(AbacType.EHR).getName()); - break; - case BaseController.EHR_STATUS: - policyParameters = Arrays.asList(policy.get(AbacType.EHR_STATUS).getParameters()); - requestUrl = requestUrl.concat(policy.get(AbacType.EHR_STATUS).getName()); - break; - case BaseController.COMPOSITION: - policyParameters = - Arrays.asList(policy.get(AbacType.COMPOSITION).getParameters()); - requestUrl = requestUrl.concat(policy.get(AbacType.COMPOSITION).getName()); - break; - case BaseController.CONTRIBUTION: - policyParameters = - Arrays.asList(policy.get(AbacType.CONTRIBUTION).getParameters()); - requestUrl = requestUrl.concat(policy.get(AbacType.CONTRIBUTION).getName()); - break; - case BaseController.QUERY: - policyParameters = Arrays.asList(policy.get(AbacType.QUERY).getParameters()); - requestUrl = requestUrl.concat(policy.get(AbacType.QUERY).getName()); - break; - default: - throw new InternalServerException("ABAC: Invalid type given from Pre- or PostAuthorize"); - } - - // Check and extract JWT - var jwt = getJwtAuthenticationToken(this.getAuthentication()); - - // Request body map. will result in simple JSON like {"patient_id":"...", ...} - // but requires "Object" for template handling, which can have a Set for multiple IDs - Map requestMap = new HashMap<>(); - - // Organization attribute handling - if (policyParameters.contains(PolicyParameter.ORGANIZATION)) { - organizationHandling(jwt, requestMap); - } - - // Patient attribute handling - if (policyParameters.contains(PolicyParameter.PATIENT)) { - // populate requestMap, but also already check if subject from token and request matches - boolean patientMatch = patientHandling(jwt, subject, requestMap, type, payload); - if (!patientMatch) { - // doesn't match -> requesting data for patient X with token for patient Y - return false; - } - } - - // Extract template ID from object of type "type" - if (policyParameters.contains(PolicyParameter.TEMPLATE)) { - templateHandling(type, payload, contentType, requestMap, authType); - } - - // Final check, if request would be empty even though params were configured to be used - if ((policyParameters.contains(PolicyParameter.ORGANIZATION) - || policyParameters.contains(PolicyParameter.PATIENT) - || policyParameters.contains(PolicyParameter.TEMPLATE)) - && requestMap.size() == 0) { - throw new InternalServerException( - "ABAC: Parameters were configured, but request parameters " + "are empty."); - } - - return abacCheckRequest(requestUrl, requestMap); - } - - /** - * Handles organization ID extraction. Uses token's claim. - * @param token Token - * @param requestMap ABAC request attribute map to add the result - */ - private void organizationHandling(AbstractAuthenticationToken token, Map requestMap) { - String orgId = getRequestedJwtClaim(token, abacConfig.getOrganizationClaim()); - - if (isBlank(orgId)) { - // organization configured but claim not available or empty - throw new IllegalArgumentException( - "ABAC use of an organization claim is configured but can't be retrieved from the given JWT."); - } - - requestMap.put(ORGANIZATION, orgId); - } - - /** - * Extracts the patient claim from the token's claims. - * @param token Token - * @return The patient claim - */ - private String getPatient(AbstractAuthenticationToken token) { - String tokenPatient = getRequestedJwtClaim(token, abacConfig.getPatientClaim()); - - if (isBlank(tokenPatient)) { - throw new IllegalArgumentException("ABAC: Patient parameter configured, but no claim attribute available."); - } - - return tokenPatient; - } - - /** - * Handles patient ID extraction. Either uses token's claim or EHR's subject. - * @param token Token - * @param subject Subject from EHR - * @param requestMap ABAC request attribute map to add the result - */ - @SuppressWarnings("unchecked") - boolean patientHandling( - AbstractAuthenticationToken token, - String subject, - Map requestMap, - String type, - Object payload) { - - String tokenPatient = getPatient(token); - - boolean isQuery = type.equals(BaseController.QUERY); - - if (!isQuery && (tokenPatient.equals(subject) || subject == null)) { - requestMap.put(PATIENT, tokenPatient); - return true; - } else if (!isQuery) return false; - else if (!(payload instanceof Map)) - throw new InternalServerException("ABAC: AQL audit patient data malformed."); - else { - if (((Map) payload).containsKey(AuditVariables.EHR_PATH)) { - Set ehrs = (Set) ((Map) payload).get(AuditVariables.EHR_PATH); - List allSubjectExtRefs = ehrService.getSubjectExtRefs( - ehrs.stream().map(UUID::toString).collect(Collectors.toList())); - boolean isValidRefs = - allSubjectExtRefs.stream().map(tokenPatient::equals).reduce(true, (b1, b2) -> b1 && b2); - - if (!isValidRefs) return false; - - Set patientSet = new HashSet<>(); - patientSet.add(tokenPatient); - requestMap.put(PATIENT, patientSet); - return true; - } else throw new InternalServerException("ABAC: AQL audit patient data unavailable."); - } - } - - /** - * Handles template ID extraction of specific payload. - *

- * Payload will be a response body string, in case of @PostAuthorize. - *

- * Payload will be request body string, or already deserialized object (e.g. EhrStatus), in case of @PreAuthorize. - * @param type Object type of scope - * @param payload Payload object, either request's input or response's output - * @param contentType Content type from the scope - * @param requestMap ABAC request attribute map to add the result - * @param authType Pre- or PostAuthorize, determines payload style (string or object) - */ - @SuppressWarnings("unchecked") - private void templateHandling( - String type, Object payload, String contentType, Map requestMap, String authType) { - switch (type) { - case BaseController.EHR: - throw new IllegalArgumentException( - "ABAC: Unsupported configuration: Can't set template ID for EHR type."); - case BaseController.EHR_STATUS: - throw new IllegalArgumentException( - "ABAC: Unsupported configuration: Can't set template ID for EHR_STATUS type."); - case BaseController.COMPOSITION: - String content = ""; - if (authType.equals(POST)) { - // @PostAuthorize gives a ResponseEntity type for "returnObject", so payload is of that type - if (!(payload instanceof ResponseEntity responseEntity)) { - throw new InternalServerException("ABAC: unexpected payload type"); - } - if (!responseEntity.hasBody()) { - if (NO_CONTENT.equals(responseEntity.getStatusCode())) { - return; - } - throw new InternalServerException("ABAC: unexpected empty response body"); - } - Object body = responseEntity.getBody(); - if (NO_CONTENT.equals(responseEntity.getStatusCode())) { - if (body instanceof Map) { - Object error = ((Map) body).get("error"); - if (error != null && ((String) error).contains("delet")) { - // composition was deleted, so nothing to check here, skip - break; - } - } - throw new InternalServerException("ABAC: Unexpected empty response from composition request"); - } - if (body instanceof OriginalVersionResponseData) { - // case of versioned_composition --> fast path, because template is easy to get - Object data = ((OriginalVersionResponseData) body).getData(); - if (data instanceof Composition composition) { - String template = Objects.requireNonNull( - composition.getArchetypeDetails().getTemplateId()) - .getValue(); - requestMap.put(TEMPLATE, template); - break; // special case, so done here, exit - } - } else if (body instanceof String) { - content = (String) body; - } else { - throw new InternalServerException("ABAC: unexpected composition payload object"); - } - } else if (authType.equals(PRE)) { - try { - // try if this is the Delete composition case. Payload would contain the UUID of the compo. - ObjectVersionId versionId = new ObjectVersionId((String) payload); - UUID compositionUid = - UUID.fromString(versionId.getRoot().getValue()); - Optional compoDto = compositionService.retrieve( - compositionService.getEhrId(compositionUid), compositionUid, null); - if (compoDto.isPresent()) { - Composition c = compoDto.get(); - if (c.getArchetypeDetails() != null - && c.getArchetypeDetails().getTemplateId() != null) { - requestMap.put( - TEMPLATE, - c.getArchetypeDetails().getTemplateId().getValue()); - } - break; // special case, so done here, exit - } else { - throw new InternalServerException( - "ABAC: unexpected empty response from composition delete"); - } - } catch (IllegalArgumentException e) { - // if not an UUID, the payload is a composition itself so continue - content = (String) payload; - } - } else { - throw new InternalServerException("ABAC: invalid auth type given."); - } - String templateId; - if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_JSON)) { - templateId = compositionService.getTemplateIdFromInputComposition(content, CompositionFormat.JSON); - } else if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_XML)) { - templateId = compositionService.getTemplateIdFromInputComposition(content, CompositionFormat.XML); - } else { - throw new IllegalArgumentException("ABAC: Only JSON and XML composition are supported."); - } - requestMap.put(TEMPLATE, templateId); - break; - case BaseController.CONTRIBUTION: - CompositionFormat format; - if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_JSON)) { - format = CompositionFormat.JSON; - } else if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_XML)) { - format = CompositionFormat.XML; - } else { - throw new IllegalArgumentException("ABAC: Only JSON and XML composition are supported."); - } - if (payload instanceof String) { - Set templates = contributionService.getListOfTemplates((String) payload, format); - requestMap.put(TEMPLATE, templates); - break; - } else { - throw new InternalServerException("ABAC: invalid POST contribution payload."); - } - case BaseController.QUERY: - // special case of type QUERY, where multiple subjects are possible - if (payload instanceof Map) { - if (((Map) payload).containsKey(AuditVariables.TEMPLATE_PATH)) { - Set templates = (Set) ((Map) payload).get(AuditVariables.TEMPLATE_PATH); - Set templateSet = new HashSet<>(templates); - // put result set into the requestMap and exit - requestMap.put(TEMPLATE, templateSet); - break; - } else { - throw new InternalServerException("ABAC: AQL audit template data unavailable."); - } - } else { - throw new InternalServerException("ABAC: AQL audit template data malformed."); - } - default: - throw new InternalServerException("ABAC: Invalid type given from Pre- or PostAuthorize"); - } - } - - @SuppressWarnings("unchecked") - private boolean abacCheckRequest(String url, Map bodyMap) throws IOException { - // prepare request attributes and convert from to - Map request = new HashMap<>(); - if (bodyMap.containsKey(ORGANIZATION)) { - request.put(ORGANIZATION, (String) bodyMap.get(ORGANIZATION)); - } - // check if patient attribues are available and see if it contains a Set or simple String - if (bodyMap.containsKey(PATIENT)) { - if (bodyMap.get(PATIENT) instanceof Set) { - // check if templates are also configured - if (bodyMap.containsKey(TEMPLATE)) { - if (bodyMap.get(TEMPLATE) instanceof Set) { - // multiple templates possible: need cartesian product of n patients and m templates - // so: for each patient, go through templates and do a request each - Set setP = (Set) bodyMap.get(PATIENT); - for (String p : setP) { - request.put(PATIENT, p); - boolean success = sendRequestForEach(TEMPLATE, url, bodyMap, request); - if (!success) { - return false; - } - } - // in case all combinations were validated successfully - return true; - } - } else { - // only patients (or + orga) set. So run request for each patient, without template. - return sendRequestForEach(PATIENT, url, bodyMap, request); - } - } else if (bodyMap.get(PATIENT) instanceof String) { - request.put(PATIENT, (String) bodyMap.get(PATIENT)); - } else { - // if it is just a String, set it and continue normal - throw new InternalServerException("ABAC: Invalid patient attribute content."); - } - } - // check if template attributes are available and see if it contains a Set or simple String - if (bodyMap.containsKey(TEMPLATE)) { - if (bodyMap.get(TEMPLATE) instanceof Set) { - // set each template and send separate ABAC requests - return sendRequestForEach(TEMPLATE, url, bodyMap, request); - } else if (bodyMap.get(TEMPLATE) instanceof String) { - // if it is just a String, set it and continue normal - request.put(TEMPLATE, (String) bodyMap.get(TEMPLATE)); - } else { - throw new InternalServerException("ABAC: Invalid template attribute content."); - } - } - return abacCheck.execute(url, request); - } - - /** - * Goes through all template IDs and sends an ABAC request for each. - * @param type Type, either ORGANIZATION, TEMPLATE, PATIENT - * @param url ABAC server request URL - * @param bodyMap Unprocessed attributes for the request - * @param request Processed attributes for the request - * @return True on success, False if one combinations is rejected by the ABAC server - * @throws IOException On error during attribute or HTTP handling - */ - @SuppressWarnings("unchecked") - private boolean sendRequestForEach( - String type, String url, Map bodyMap, Map request) throws IOException { - Set set = (Set) bodyMap.get(type); - for (String s : set) { - request.put(type, s); - boolean allowed = abacCheck.execute(url, request); - if (!allowed) { - // if only one combination of attributes is rejected by ABAC return false for all - return false; - } - } - // in case all combinations were validated successfully - return true; - } - - /** - * Extracts the JWT auth token. - * @param auth Auth object. - * @return JWT Auth Token - */ - private AbstractAuthenticationToken getJwtAuthenticationToken(Authentication auth) { - if (auth instanceof AbstractAuthenticationToken jwt) { - return jwt; - } else { - throw new IllegalArgumentException("ABAC: Invalid authentication, no JWT available."); - } - } - - @Override - public Object getFilterObject() { - return this.filterObject; - } - - @Override - public void setFilterObject(Object filterObject) { - this.filterObject = filterObject; - } - - @Override - public Object getReturnObject() { - return this.returnObject; - } - - @Override - public void setReturnObject(Object returnObject) { - this.returnObject = returnObject; - } - - @Override - public Object getThis() { - return this; - } -} diff --git a/application/src/main/java/org/ehrbase/application/abac/MethodSecurityConfig.java b/application/src/main/java/org/ehrbase/application/abac/MethodSecurityConfig.java deleted file mode 100644 index f6fc4d874..000000000 --- a/application/src/main/java/org/ehrbase/application/abac/MethodSecurityConfig.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.abac; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; - -@ConditionalOnProperty(name = "abac.enabled") -@Configuration -@EnableGlobalMethodSecurity(prePostEnabled = true) -public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { - - private final AbacConfig abacConfig; - - public MethodSecurityConfig(AbacConfig abacConfig) { - this.abacConfig = abacConfig; - } - - /** - * Registration of custom SpEL expressions, here to include ABAC checks. - */ - @Override - protected MethodSecurityExpressionHandler createExpressionHandler() { - // "null" for beans here, but autowiring will make the beans available on runtime - return new CustomMethodSecurityExpressionHandler(abacConfig, null, null, null, null); - } -} diff --git a/application/src/main/java/org/ehrbase/application/cli/EhrBaseCli.java b/application/src/main/java/org/ehrbase/application/cli/EhrBaseCli.java new file mode 100644 index 000000000..8bf902a44 --- /dev/null +++ b/application/src/main/java/org/ehrbase/application/cli/EhrBaseCli.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.application.cli; + +import java.util.Map; +import org.ehrbase.cli.CliConfiguration; +import org.ehrbase.cli.CliRunner; +import org.ehrbase.configuration.EhrBaseCliConfiguration; +import org.springframework.boot.Banner; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Import; + +@SpringBootApplication(exclude = {WebMvcAutoConfiguration.class, RedisAutoConfiguration.class}) +@Import({EhrBaseCliConfiguration.class, CliConfiguration.class}) +public class EhrBaseCli implements CommandLineRunner { + + public static SpringApplication build(String[] args) { + return new SpringApplicationBuilder(EhrBaseCli.class) + .web(WebApplicationType.NONE) + .headless(true) + .properties(Map.of( + "spring.main.allow-bean-definition-overriding", "true", + "spring.banner.location", "classpath:banner-cli.txt")) + .bannerMode(Banner.Mode.CONSOLE) + .logStartupInfo(false) + .build(args); + } + + private final CliRunner cliRunner; + + public EhrBaseCli(CliRunner cliRunner) { + this.cliRunner = cliRunner; + } + + @Override + public void run(String... args) { + cliRunner.run(args); + } +} diff --git a/application/src/main/java/org/ehrbase/application/config/ServerConfigImp.java b/application/src/main/java/org/ehrbase/application/config/ServerConfigImp.java deleted file mode 100644 index e09d2a8c0..000000000 --- a/application/src/main/java/org/ehrbase/application/config/ServerConfigImp.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2020 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ConfigurationProperties(prefix = "server") -public class ServerConfigImp implements org.ehrbase.api.definitions.ServerConfig { - - @Min(1025) - @Max(65536) - private int port; - - private String nodename = "local.ehrbase.org"; - private AqlConfig aqlConfig; - private boolean disableStrictValidation = false; - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public String getNodename() { - return nodename; - } - - public void setNodename(String nodename) { - this.nodename = nodename; - } - - @Override - public String getAqlIterationSkipList() { - return aqlConfig.getIgnoreIterativeNodeList(); - } - - @Override - public Integer getAqlDepth() { - return aqlConfig.getIterationScanDepth(); - } - - public AqlConfig getAqlConfig() { - return aqlConfig; - } - - public void setAqlConfig(AqlConfig aqlConfig) { - this.aqlConfig = aqlConfig; - } - - public static class AqlConfig { - - private String ignoreIterativeNodeList; - private Integer iterationScanDepth = 1; - - public String getIgnoreIterativeNodeList() { - return ignoreIterativeNodeList; - } - - public Integer getIterationScanDepth() { - return iterationScanDepth; - } - - public void setIgnoreIterativeNodeList(String ignoreIterativeNodeList) { - this.ignoreIterativeNodeList = ignoreIterativeNodeList; - } - - public void setIterationScanDepth(Integer iterationScanDepth) { - this.iterationScanDepth = iterationScanDepth; - } - } - - @Override - public boolean isDisableStrictValidation() { - return disableStrictValidation; - } - - public void setDisableStrictValidation(boolean disableStrictValidation) { - this.disableStrictValidation = disableStrictValidation; - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/cache/CacheConfiguration.java b/application/src/main/java/org/ehrbase/application/config/cache/CacheConfiguration.java deleted file mode 100644 index 4a97ba476..000000000 --- a/application/src/main/java/org/ehrbase/application/config/cache/CacheConfiguration.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.cache; - -import com.github.benmanes.caffeine.cache.Caffeine; -import java.util.concurrent.TimeUnit; -import org.ehrbase.cache.CacheOptions; -import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.caffeine.CaffeineCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link Configuration} for EhCache using JCache. - * - * @author Renaud Subiger - * @since 1.0.0 - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(CacheProperties.class) -@EnableCaching -public class CacheConfiguration { - - @Bean - public CacheOptions cacheOptions(CacheProperties properties) { - var options = new CacheOptions(); - options.setPreBuildQueries(properties.isPreBuildQueries()); - options.setPreBuildQueriesDepth(properties.getPreBuildQueriesDepth()); - options.setPreInitialize(properties.isInitOnStartup()); - return options; - } - - @Bean - @ConditionalOnExpression( - "T(org.springframework.boot.autoconfigure.cache.CacheType).CAFFEINE.name().equalsIgnoreCase(\"${spring.cache.type}\")") - public CacheManagerCustomizer cacheManagerCustomizer() { - return cm -> { - cm.registerCustomCache( - CacheOptions.INTROSPECT_CACHE, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.QUERY_CACHE, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.FIELDS_CACHE, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.MULTI_VALUE_CACHE, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.CONCEPT_CACHE_ID, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.CONCEPT_CACHE_CONCEPT_ID, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.CONCEPT_CACHE_DESCRIPTION, - Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.TERRITORY_CACHE, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.LANGUAGE_CACHE, Caffeine.newBuilder().build()); - cm.registerCustomCache( - CacheOptions.USER_ID_CACHE, - Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build()); - cm.registerCustomCache( - CacheOptions.SYS_TENANT, Caffeine.newBuilder().build()); - }; - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/cache/CacheProperties.java b/application/src/main/java/org/ehrbase/application/config/cache/CacheProperties.java deleted file mode 100644 index 5bc9e3667..000000000 --- a/application/src/main/java/org/ehrbase/application/config/cache/CacheProperties.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.cache; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * {@link ConfigurationProperties} for EHRbase cache configuration. - * - * @author Renaud Subiger - * @since 1.0.0 - */ -@ConfigurationProperties(prefix = "cache") -public class CacheProperties { - - /** - * Whether to initialize the caches during application startup. - */ - private boolean initOnStartup = true; - - /** - * Whether to pre-build queries when a new template is added. - */ - private boolean preBuildQueries = true; - - /** - * The default node depth for pre-built queries. - */ - private Integer preBuildQueriesDepth = 4; - - public boolean isInitOnStartup() { - return initOnStartup; - } - - public void setInitOnStartup(boolean initOnStartup) { - this.initOnStartup = initOnStartup; - } - - public boolean isPreBuildQueries() { - return preBuildQueries; - } - - public void setPreBuildQueries(boolean preBuildQueries) { - this.preBuildQueries = preBuildQueries; - } - - public Integer getPreBuildQueriesDepth() { - return preBuildQueriesDepth; - } - - public void setPreBuildQueriesDepth(Integer preBuildQueriesDepth) { - this.preBuildQueriesDepth = preBuildQueriesDepth; - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/security/BasicAuthSecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/BasicAuthSecurityConfiguration.java deleted file mode 100644 index 23cf4bfb6..000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/BasicAuthSecurityConfiguration.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2021-2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.security; - -import static org.ehrbase.application.config.security.SecurityProperties.ADMIN; -import static org.springframework.security.config.Customizer.withDefaults; - -import javax.annotation.PostConstruct; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -/** - * {@link Configuration} for Basic authentication. - * - * @author Jake Smolka - * @author Renaud Subiger - * @since 1.0.0 - */ -@Configuration -@ConditionalOnProperty(prefix = "security", name = "authType", havingValue = "basic") -@EnableWebSecurity -public class BasicAuthSecurityConfiguration { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @PostConstruct - public void initialize() { - logger.info("Using basic authentication"); - } - - @Bean - public InMemoryUserDetailsManager inMemoryUserDetailsManager( - SecurityProperties properties, ObjectProvider passwordEncoder) { - - return new InMemoryUserDetailsManager( - User.withUsername(properties.getAuthUser()) - .password("{noop}" + properties.getAuthPassword()) - .roles(SecurityProperties.USER) - .build(), - User.withUsername(properties.getAuthAdminUser()) - .password("{noop}" + properties.getAuthAdminPassword()) - .roles(SecurityProperties.ADMIN) - .build()); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.addFilterBefore(new SecurityFilter(), BasicAuthenticationFilter.class); - - http.cors(withDefaults()) - .csrf(c -> c.ignoringRequestMatchers(AntPathRequestMatcher.antMatcher("/rest/**"))) - .authorizeHttpRequests(auth -> auth.requestMatchers( - AntPathRequestMatcher.antMatcher("/rest/admin/**"), - AntPathRequestMatcher.antMatcher("/management/**")) - .hasRole(ADMIN) - .anyRequest() - .hasAnyRole(ADMIN, SecurityProperties.USER)) - .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .httpBasic(withDefaults()); - - return http.build(); - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/security/NoOpSecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/NoOpSecurityConfiguration.java deleted file mode 100644 index 3852476d4..000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/NoOpSecurityConfiguration.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2021-2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.security; - -import javax.annotation.PostConstruct; -import org.ehrbase.service.IAuthenticationFacade; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; - -/** - * {@link Configuration} used when security is disabled. - * - * @author Renaud Subiger - * @since 1.0.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnProperty(prefix = "security", name = "auth-type", havingValue = "none") -public class NoOpSecurityConfiguration { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @PostConstruct - public void initialize() { - logger.warn("Security is disabled. Configure 'security.auth-type' to disable this warning."); - } - - @Bean - @Primary - public IAuthenticationFacade anonymousAuthentication() { - var filter = new AnonymousAuthenticationFilter("key"); - return () -> new AnonymousAuthenticationToken("key", filter.getPrincipal(), filter.getAuthorities()); - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/security/OAuth2SecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/OAuth2SecurityConfiguration.java deleted file mode 100644 index 89d4f5f99..000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/OAuth2SecurityConfiguration.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2021-2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.security; - -import static org.springframework.security.config.Customizer.withDefaults; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import javax.annotation.PostConstruct; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; -import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -/** - * {@link Configuration} for OAuth2 authentication. - * - * @author Jake Smolka - * @since 1.0.0 - */ -@Configuration -@EnableWebSecurity -@ConditionalOnProperty(prefix = "security", name = "auth-type", havingValue = "oauth") -public class OAuth2SecurityConfiguration { - - private static final String PUBLIC = "PUBLIC"; - private static final String PRIVATE = "PRIVATE"; - public static final String ADMIN_ONLY = "ADMIN_ONLY"; - public static final String PROFILE_SCOPE = "PROFILE"; - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Value("${management.endpoints.web.access:ADMIN_ONLY}") - private String managementEndpointsAccessType; - - private final SecurityProperties securityProperties; - - private final OAuth2ResourceServerProperties oAuth2Properties; - - private final WebEndpointProperties managementWebEndpointProperties; - - public OAuth2SecurityConfiguration( - SecurityProperties securityProperties, - OAuth2ResourceServerProperties oAuth2Properties, - WebEndpointProperties managementWebEndpointProperties) { - this.securityProperties = securityProperties; - this.oAuth2Properties = oAuth2Properties; - this.managementWebEndpointProperties = managementWebEndpointProperties; - } - - @PostConstruct - public void initialize() { - logger.info("Using OAuth2 authentication"); - logger.debug("Using issuer URI: {}", oAuth2Properties.getJwt().getIssuerUri()); - logger.debug("Using user role: {}", securityProperties.getOauth2UserRole()); - logger.debug("Using admin role: {}", securityProperties.getOauth2AdminRole()); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - String userRole = securityProperties.getOauth2UserRole(); - String adminRole = securityProperties.getOauth2AdminRole(); - - http.addFilterBefore(new SecurityFilter(), BearerTokenAuthenticationFilter.class); - - http.cors(withDefaults()) - .authorizeHttpRequests(auth -> { - AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry = - auth.requestMatchers(AntPathRequestMatcher.antMatcher("/rest/admin/**")) - .hasRole(adminRole); - - var managementAuthorizedUrl = registry.requestMatchers(AntPathRequestMatcher.antMatcher( - this.managementWebEndpointProperties.getBasePath() + "/**")); - - switch (managementEndpointsAccessType) { - case ADMIN_ONLY -> - // management endpoints are locked behind an authorization - // and are only available for users with the admin role - managementAuthorizedUrl.hasRole(adminRole); - case PRIVATE -> - // management endpoints are locked behind an authorization, but are available to any role - managementAuthorizedUrl.hasAnyRole(adminRole, userRole, PROFILE_SCOPE); - case PUBLIC -> - // management endpoints can be accessed without an authorization - managementAuthorizedUrl.permitAll(); - default -> throw new IllegalStateException(String.format( - "Unexpected management endpoints access control type %s", - managementEndpointsAccessType)); - } - - registry.anyRequest().hasAnyRole(adminRole, userRole, PROFILE_SCOPE); - }) - .oauth2ResourceServer(o -> o.jwt(j -> j.jwtAuthenticationConverter(getJwtAuthenticationConverter()))); - - return http.build(); - } - - // Converter creates list of "ROLE_*" (upper case) authorities for each "realm access" role - // and "roles" role from JWT - @SuppressWarnings("unchecked") - private Converter getJwtAuthenticationConverter() { - var converter = new JwtAuthenticationConverter(); - converter.setJwtGrantedAuthoritiesConverter(jwt -> { - Map realmAccess; - realmAccess = (Map) jwt.getClaims().get("realm_access"); - - Collection authority = new HashSet<>(); - if (realmAccess != null && realmAccess.containsKey("roles")) { - authority.addAll(((List) realmAccess.get("roles")) - .stream() - .map(roleName -> "ROLE_" + roleName.toUpperCase()) - .map(SimpleGrantedAuthority::new) - .toList()); - } - - if (jwt.getClaims().containsKey("scope")) { - authority.addAll( - Arrays.stream(jwt.getClaims().get("scope").toString().split(" ")) - .map(roleName -> "ROLE_" + roleName.toUpperCase()) - .map(SimpleGrantedAuthority::new) - .toList()); - } - return authority; - }); - return converter; - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/security/SecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/SecurityConfiguration.java deleted file mode 100644 index 9f3aebf7e..000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2020 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.security; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(SecurityProperties.class) -@Import({NoOpSecurityConfiguration.class, BasicAuthSecurityConfiguration.class}) -public class SecurityConfiguration {} diff --git a/application/src/main/java/org/ehrbase/application/config/validation/ValidationConfiguration.java b/application/src/main/java/org/ehrbase/application/config/validation/ValidationConfiguration.java deleted file mode 100644 index 4a1161faa..000000000 --- a/application/src/main/java/org/ehrbase/application/config/validation/ValidationConfiguration.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2021-2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.validation; - -import com.nedap.archie.rm.datavalues.DvCodedText; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.apache.http.client.HttpClient; -import org.ehrbase.openehr.sdk.util.functional.Try; -import org.ehrbase.openehr.sdk.validation.ConstraintViolation; -import org.ehrbase.openehr.sdk.validation.ConstraintViolationException; -import org.ehrbase.openehr.sdk.validation.terminology.ExternalTerminologyValidation; -import org.ehrbase.openehr.sdk.validation.terminology.ExternalTerminologyValidationChain; -import org.ehrbase.openehr.sdk.validation.terminology.FhirTerminologyValidation; -import org.ehrbase.openehr.sdk.validation.terminology.TerminologyParam; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link Configuration} for external terminology validation. - * - * @author Renaud Subiger - * @since 1.0.0 - */ -@Configuration -@EnableConfigurationProperties(ValidationProperties.class) -@SuppressWarnings("java:S6212") -public class ValidationConfiguration { - private static final String ERR_MSG = "External terminology validation is disabled, consider to enable it"; - private final Logger logger = LoggerFactory.getLogger(getClass()); - private final ValidationProperties properties; - private final HttpClient httpClient; - - // @Autowired - @Value("${validation.external-terminology.enabled}") - private Boolean enableExternalValidation; - - public ValidationConfiguration(ValidationProperties properties, HttpClient httpClient) { - this.properties = properties; - this.httpClient = httpClient; - } - - @Bean - public ExternalTerminologyValidation externalTerminologyValidator() { - if (!enableExternalValidation) { - logger.warn(ERR_MSG); - return new ExternalTerminologyValidation() { - private final ConstraintViolation err = new ConstraintViolation(ERR_MSG); - - public Try validate(TerminologyParam param) { - return Try.failure(new ConstraintViolationException(List.of(err))); - } - - public boolean supports(TerminologyParam param) { - return false; - } - - public List expand(TerminologyParam param) { - return Collections.emptyList(); - } - }; - } - - Map providers = properties.getProvider(); - - if (providers.isEmpty()) { - throw new IllegalStateException("At least one external terminology provider must be defined " - + "if 'validation.external-validation.enabled' is set to 'true'"); - } else if (providers.size() == 1) { - Map.Entry provider = - providers.entrySet().iterator().next(); - return buildExternalTerminologyValidation(provider); - } else { - ExternalTerminologyValidationChain chain = new ExternalTerminologyValidationChain(); - for (Map.Entry provider : providers.entrySet()) { - chain.addExternalTerminologyValidationSupport(buildExternalTerminologyValidation(provider)); - } - return chain; - } - } - - private ExternalTerminologyValidation buildExternalTerminologyValidation( - Map.Entry provider) { - logger.info( - "Initializing '{}' external terminology provider (type: {})", - provider.getKey(), - provider.getValue().getType()); - if (provider.getValue().getType() == ValidationProperties.ProviderType.FHIR) { - return fhirTerminologyValidation(provider.getValue().getUrl()); - } - throw new IllegalArgumentException( - "Invalid provider type: " + provider.getValue().getType()); - } - - private FhirTerminologyValidation fhirTerminologyValidation(String url) { - return new FhirTerminologyValidation(url, properties.isFailOnError(), httpClient); - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/web/AuditHandlerInterceptorDelegator.java b/application/src/main/java/org/ehrbase/application/config/web/AuditHandlerInterceptorDelegator.java deleted file mode 100644 index bae34fc20..000000000 --- a/application/src/main/java/org/ehrbase/application/config/web/AuditHandlerInterceptorDelegator.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.web; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.ehrbase.api.audit.interceptor.AuditInterceptor; -import org.springframework.lang.Nullable; -import org.springframework.web.servlet.HandlerInterceptor; - -public class AuditHandlerInterceptorDelegator implements HandlerInterceptor { - private final AuditInterceptor interceptor; - - public AuditHandlerInterceptorDelegator(AuditInterceptor interceptor) { - this.interceptor = interceptor; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - return interceptor.preHandle(request, response, handler); - } - - @Override - public void afterCompletion( - HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) - throws Exception { - interceptor.afterCompletion(request, response, handler, ex); - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/web/AuditInterceptorHandler.java b/application/src/main/java/org/ehrbase/application/config/web/AuditInterceptorHandler.java deleted file mode 100644 index e76c275b0..000000000 --- a/application/src/main/java/org/ehrbase/application/config/web/AuditInterceptorHandler.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.web; - -import static org.ehrbase.rest.BaseController.*; - -import java.util.Arrays; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.ehrbase.api.audit.interceptor.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; - -@Component -public class AuditInterceptorHandler { - - private final Logger log = LoggerFactory.getLogger(getClass()); - - private static final String ANY_SEGMENT = "*"; - private static final String ROLLBACK = "rollback"; - private static final String ANY_TRAILING_SEGMENTS = "**"; - - @Value(API_CONTEXT_PATH_WITH_VERSION) - protected String apiContextPath; - - @Value(ADMIN_API_CONTEXT_PATH) - protected String adminApiContextPath; - - @Autowired(required = false) - AuditEhrInterceptor ehrInterceptor; - - @Autowired(required = false) - AuditQueryInterceptor queryInterceptor; - - @Autowired(required = false) - AuditEhrAdminInterceptor ehrAdminInterceptor; - - @Autowired(required = false) - AuditEhrStatusInterceptor ehrStatusInterceptor; - - @Autowired(required = false) - AuditCompositionInterceptor compositionInterceptor; - - @Autowired(required = false) - AuditContributionInterceptor contributionInterceptor; - - @Autowired(required = false) - AuditCompensationInterceptor compensationInterceptor; - - @Autowired(required = false) - AuditDirectoryInterceptor directoryInterceptor; - - public void registerAuditInterceptors(InterceptorRegistry registry) { - if (shouldRegisterInterceptors()) { - registerEhrInterceptor(registry); - registerQueryInterceptor(registry); - registerEhrAdminInterceptor(registry); - registerEhrStatusInterceptor(registry); - registerCompositionInterceptor(registry); - registerContributionInterceptor(registry); - registerCompensationInterceptor(registry); - registerDirectoryInterceptor(registry); - } - } - - private void registerEhrStatusInterceptor(InterceptorRegistry registry) { - if (ehrStatusInterceptor != null) { - // Ehr Status and Versioned Ehr Status endpoints - registry.addInterceptor(new AuditHandlerInterceptorDelegator(ehrStatusInterceptor)) - .addPathPatterns( - contextPathPattern(EHR, ANY_SEGMENT, EHR_STATUS), - contextPathPattern(EHR, ANY_SEGMENT, EHR_STATUS, ANY_TRAILING_SEGMENTS)) - .addPathPatterns( - contextPathPattern(EHR, ANY_SEGMENT, EHR_STATUS), - contextPathPattern(EHR, ANY_SEGMENT, VERSIONED_EHR_STATUS, ANY_TRAILING_SEGMENTS)); - } else { - log.info("Ehr Status interceptor bean is not available."); - } - } - - private void registerQueryInterceptor(InterceptorRegistry registry) { - if (queryInterceptor != null) { - // Query endpoint - registry.addInterceptor(new AuditHandlerInterceptorDelegator(queryInterceptor)) - .addPathPatterns(contextPathPattern(QUERY, ANY_TRAILING_SEGMENTS)) - .addPathPatterns(contextPathPattern(DEFINITION, QUERY, ANY_TRAILING_SEGMENTS)); - } else { - log.info("Query interceptor bean is not available."); - } - } - - private void registerEhrAdminInterceptor(InterceptorRegistry registry) { - if (ehrAdminInterceptor != null) { - // Ehr admin endpoint - registry.addInterceptor(new AuditHandlerInterceptorDelegator(ehrAdminInterceptor)) - .addPathPatterns(contextAdminPathPattern(EHR), contextAdminPathPattern(EHR, ANY_SEGMENT)); - } else { - log.info("Ehr admin interceptor bean is not available."); - } - } - - private void registerEhrInterceptor(InterceptorRegistry registry) { - if (ehrInterceptor != null) { - // Ehr endpoint - registry.addInterceptor(new AuditHandlerInterceptorDelegator(ehrInterceptor)) - .addPathPatterns(contextPathPattern(EHR), contextPathPattern(EHR, ANY_SEGMENT)); - } else { - log.info("Ehr interceptor bean is not available."); - } - } - - private void registerContributionInterceptor(InterceptorRegistry registry) { - if (contributionInterceptor != null) { - // Contribution endpoint - registry.addInterceptor(new AuditHandlerInterceptorDelegator(contributionInterceptor)) - .addPathPatterns(contextPathPattern(EHR, ANY_SEGMENT, CONTRIBUTION, ANY_TRAILING_SEGMENTS)) - .addPathPatterns(contextAdminPathPattern(EHR, ANY_SEGMENT, CONTRIBUTION, ANY_TRAILING_SEGMENTS)); - } else { - log.info("Contribution interceptor bean is not available."); - } - } - - private void registerCompositionInterceptor(InterceptorRegistry registry) { - if (compositionInterceptor != null) { - // Composition endpoint - registry.addInterceptor(new AuditHandlerInterceptorDelegator(compositionInterceptor)) - .addPathPatterns(contextPathPattern(EHR, ANY_SEGMENT, COMPOSITION, ANY_TRAILING_SEGMENTS)) - .addPathPatterns(contextPathPattern(EHR, ANY_SEGMENT, VERSIONED_COMPOSITION, ANY_TRAILING_SEGMENTS)) - .addPathPatterns(contextAdminPathPattern(EHR, ANY_SEGMENT, COMPOSITION, ANY_SEGMENT)); - } else { - log.info("Composition interceptor bean is not available."); - } - } - - private void registerCompensationInterceptor(InterceptorRegistry registry) { - if (compensationInterceptor != null) { - // Compensation plugin endpoint - registry.addInterceptor(new AuditHandlerInterceptorDelegator(compensationInterceptor)) - .addPathPatterns(getPathPattern("", EHR, ANY_SEGMENT, CONTRIBUTION, ANY_SEGMENT, ROLLBACK)); - } else { - log.info("Compensation interceptor bean is not available."); - } - } - - private void registerDirectoryInterceptor(InterceptorRegistry registry) { - if (directoryInterceptor != null) { - // Directory plugin endpoint - registry.addInterceptor(new AuditHandlerInterceptorDelegator(directoryInterceptor)) - .addPathPatterns(contextPathPattern(EHR, ANY_SEGMENT, DIRECTORY)) - .addPathPatterns(contextPathPattern(EHR, ANY_SEGMENT, DIRECTORY, ANY_SEGMENT)) - .addPathPatterns(contextAdminPathPattern(EHR, ANY_SEGMENT, DIRECTORY, ANY_SEGMENT)); - } else { - log.info("Directory interceptor bean is not available."); - } - } - - private boolean shouldRegisterInterceptors() { - return compositionInterceptor != null - || ehrInterceptor != null - || ehrAdminInterceptor != null - || queryInterceptor != null - || contributionInterceptor != null - || ehrStatusInterceptor != null - || directoryInterceptor != null - || compensationInterceptor != null; - } - - private String contextPathPattern(String... segments) { - return getPathPattern(apiContextPath, segments); - } - - private String contextAdminPathPattern(String... segments) { - return getPathPattern(adminApiContextPath, segments); - } - - private String getPathPattern(String apiContextPath, String... segments) { - return Stream.concat(Stream.of(apiContextPath), Arrays.stream(segments)).collect(Collectors.joining("/")); - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/web/WebConfiguration.java b/application/src/main/java/org/ehrbase/application/config/web/WebConfiguration.java deleted file mode 100644 index 35d78bb48..000000000 --- a/application/src/main/java/org/ehrbase/application/config/web/WebConfiguration.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.config.web; - -import org.ehrbase.application.util.IsoDateTimeConverter; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.format.FormatterRegistry; -import org.springframework.lang.NonNull; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -/** - * {@link Configuration} from Spring Web MVC. - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(CorsProperties.class) -public class WebConfiguration implements WebMvcConfigurer { - - private final CorsProperties properties; - - AuditInterceptorHandler auditInterceptorHandler; - - public WebConfiguration(CorsProperties properties, AuditInterceptorHandler auditInterceptorHandler) { - this.properties = properties; - this.auditInterceptorHandler = auditInterceptorHandler; - } - - @Override - public void addFormatters(FormatterRegistry registry) { - registry.addConverter(new IsoDateTimeConverter()); // Converter for version_at_time and other ISO date params - } - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**").combine(properties.toCorsConfiguration()); - } - - @Override - public void addInterceptors(@NonNull InterceptorRegistry registry) { - auditInterceptorHandler.registerAuditInterceptors(registry); - } - - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - configurer.setUseTrailingSlashMatch(true); - } -} diff --git a/application/src/main/java/org/ehrbase/application/server/EhrBaseServer.java b/application/src/main/java/org/ehrbase/application/server/EhrBaseServer.java new file mode 100644 index 000000000..e5559057a --- /dev/null +++ b/application/src/main/java/org/ehrbase/application/server/EhrBaseServer.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.application.server; + +import org.ehrbase.configuration.EhrBaseServerConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Import; + +@SpringBootApplication( + exclude = { + ManagementWebSecurityAutoConfiguration.class, + R2dbcAutoConfiguration.class, + SecurityAutoConfiguration.class + }) +@Import({EhrBaseServerConfiguration.class}) +@SuppressWarnings("java:S1118") +public class EhrBaseServer { + + public static SpringApplication build(String[] args) { + return new SpringApplicationBuilder(EhrBaseServer.class) + .web(WebApplicationType.SERVLET) + .build(args); + } +} diff --git a/application/src/main/resources/application-cloud.yml b/application/src/main/resources/application-cloud.yml deleted file mode 100644 index e547842f9..000000000 --- a/application/src/main/resources/application-cloud.yml +++ /dev/null @@ -1,33 +0,0 @@ -spring: - datasource: - url: jdbc:postgresql://localhost:5432/ehrbase - username: ehrbase_restricted - password: ehrbase_restricted - hikari: - maximum-pool-size: 50 - max-lifetime: 1800000 - minimum-idle: 10 - flyway: - schemas: ehr - user: ehrbase - password: ehrbase - -security: - authType: NONE - - -server: - port: 8080 - # Optional custom server nodename - # nodename: 'local.test.org' - - aqlConfig: - # if true, WHERE clause is using jsquery, false uses SQL only - useJsQuery: false - # ignore unbounded item in path starting with one of - ignoreIterativeNodeList: 'events,activities,content' - # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 1 - iterationScanDepth: 1 - - servlet: - context-path: /ehrbase diff --git a/application/src/main/resources/application-docker.yml b/application/src/main/resources/application-docker.yml deleted file mode 100644 index fa2c63db4..000000000 --- a/application/src/main/resources/application-docker.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2019 Vitasystems GmbH and Hannover Medical School. -# -# This file is part of Project EHRbase -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -spring: - datasource: - url: ${DB_URL} - username: ${DB_USER} - password: ${DB_PASS} - hikari: - maximum-pool-size: 50 - max-lifetime: 1800000 - minimum-idle: 10 - flyway: - schemas: ehr - user: ${DB_USER_ADMIN} - password: ${DB_PASS_ADMIN} -security: - authType: NONE - -server: - port: 8080 - # Optional custom server nodename - # nodename: 'local.test.org' - servlet: - context-path: /ehrbase - - aqlConfig: - # if true, WHERE clause is using jsquery, false uses SQL only - useJsQuery: false - # ignore unbounded item in path starting with one of - ignoreIterativeNodeList: 'events,activities,content' - # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 1 - iterationScanDepth: 1 - -admin-api: - active: false - allowDeleteAll: false - context-path: /rest/admin diff --git a/application/src/main/resources/application-local.yml b/application/src/main/resources/application-local.yml deleted file mode 100644 index 3711ff92a..000000000 --- a/application/src/main/resources/application-local.yml +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2019 Vitasystems GmbH and Hannover Medical School. -# -# This file is part of Project EHRbase -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -spring: - datasource: - url: jdbc:postgresql://localhost:5432/ehrbase - username: ehrbase_restricted - password: ehrbase_restricted - flyway: - schemas: ehr - user: ehrbase - password: ehrbase - - tomcat: - maxIdle: 10 - max-active: 50 - max-wait: 10000 - - jpa: - open-in-view: false - generate-ddl: false - properties: - hibernate: - format_sql: false - show_sql: false - dialect: org.hibernate.dialect.PostgreSQL10Dialect - default_schema: ehr - hbm2ddl: - auto: " " -server: - port: 8080 - # Optional custom server nodename - # nodename: 'local.test.org' - servlet: - context-path: /ehrbase - - aqlConfig: - # if true, WHERE clause is using jsquery, false uses SQL only - useJsQuery: false - # ignore unbounded item in path starting with one of - ignoreIterativeNodeList: 'dummy' - # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 2 - iterationScanDepth: 20 - -security: - authType: NONE - -#use admin for cleaning up the db during tests -admin-api: - active: true - allowDeleteAll: true - -terminology_server: - tsUrl: 'https://r4.ontoserver.csiro.au/fhir/' - codePath: '$["expansion"]["contains"][*]["code"]' - systemPath: '$["expansion"]["contains"][*]["system"]' - displayPath: '$["expansion"]["contains"][*]["display"]' \ No newline at end of file diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml deleted file mode 100644 index d482723ff..000000000 --- a/application/src/main/resources/application.yml +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (Hannover Medical School). -# -# This file is part of Project EHRbase -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ -# General How-to: -# -# You can set all config values here or via an corresponding environment variable which is named as the property you -# want to set. Replace camel case (aB) as all upper case (AB), dashes (-) and low dashes (_) just get ignored adn words -# will be in one word. Each nesting step of properties will be separated by low dash in environment variable name. -# E.g. if you want to allow the delete all endpoints in the admin api set an environment variable like this: -# ADMINAPI_ALLOWDELETEALL=true -# -# See https://docs.spring.io/spring-boot/docs/2.5.0/reference/html/features.html#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables -# for official documentation on this feature. -# -# Also see the documentation on externalized configuration in general: -# https://docs.spring.io/spring-boot/docs/2.5.0/reference/html/features.html#features.external-config - -spring: - application: - name: ehrbase - - cache: - type: CAFFEINE # change to type redis if usage of redis distributed cache is intended - - # the following redis properties are only used if the cache.type=redis - data: - redis: - host: localhost - port: 6379 - - security: - oauth2: - resourceserver: - jwt: - issuer-uri: # http://localhost:8081/auth/realms/ehrbase # Example issuer URI - or set via env var - profiles: - active: local - datasource: - driver-class-name: org.postgresql.Driver - - flyway: - schemas: ehr - user: ehrbase - password: ehrbase - - jackson: - default-property-inclusion: NON_NULL - -security: - authType: BASIC - authUser: ehrbase-user - authPassword: SuperSecretPassword - authAdminUser: ehrbase-admin - authAdminPassword: EvenMoreSecretPassword - oauth2UserRole: USER - oauth2AdminRole: ADMIN - -# Attribute Based Access Control -abac: - enabled: false - # Server URL incl. trailing "/"! - server: http://localhost:3001/rest/v1/policy/execute/name/ - # Definition of the JWT claim which contains the organization ID. - organizationClaim: 'organization_id' - # Definition of the JWT claim which contains the patient ID. Falls back to the EHR's subject. - patientClaim: 'patient_id' - # Policies need to be named and configured for each resource. Available parameters are - # - organization - # - patient - # - template - policy: - ehr: - name: 'has_consent_patient' - parameters: 'organization, patient' - ehrstatus: - name: 'has_consent_patient' - parameters: 'organization, patient' - composition: - name: 'has_consent_template' - parameters: 'organization, patient, template' - #parameters: 'template' # for manual testing, doesn't depend on real claims in JWT - contribution: - name: 'has_consent_template' - parameters: 'organization, patient, template' - query: - name: 'has_consent_template' - parameters: 'organization, patient, template' - -httpclient: -#proxy: 'localhost' -#proxyPort: 1234 - -cache: - init-on-startup: true - pre-build-queries: true - pre-build-queries-depth: 4 - -system: - allow-template-overwrite: false - -openehr-api: - context-path: /rest/openehr -admin-api: - active: false - allowDeleteAll: false - context-path: /rest/admin - -# Logging Properties -logging: - level: - org.ehcache: info - org.jooq: info - org.jooq.Constants: warn - org.springframework: info - pattern: - console: '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr([%X]){faint} %clr(${PID}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx' - -server: - # Optional custom server nodename - # nodename: 'local.test.org' - - aqlConfig: - # if true, WHERE clause is using jsquery, false uses SQL only - useJsQuery: false - # ignore unbounded item in path starting with one of - ignoreIterativeNodeList: 'activities,content' - # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 2 - iterationScanDepth: 2 - - tomcat: - threads: - min-spare: 200 - max: 200 - - # Option to disable strict invariant validation. - # disable-strict-validation: true - - -# Configuration of actuator for reporting and health endpoints -management: - endpoints: - # Disable all endpoint by default to opt-in enabled endpoints - enabled-by-default: false - web: - base-path: '/management' - exposure: - include: 'env, health, info, metrics, prometheus' - # The access to management endpoints can be controlled - # ADMIN_ONLY - (default) endpoints are locked behind an authorization and are only available for users with the admin role - # PRIVATE - endpoints are locked behind an authorization, but are available to any role - # PUBLIC - endpoints can be accessed without an authorization - access: ADMIN_ONLY - # Per endpoint settings - endpoint: - # Env endpoint - Shows information on environment of EHRbase - env: - # Enable / disable env endpoint - enabled: false - # Health endpoint - Shows information on system status - health: - # Enable / disable health endpoint - enabled: false - # Show components in health endpoint. Can be "never", "when-authorized" or "always" - show-components: 'when-authorized' - # Show details in health endpoint. Can be "never", "when-authorized" or "always" - show-details: 'when-authorized' - # Show additional information on used systems. See https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-health-indicators for available keys - datasource: - # Enable / disable report if datasource connection could be established - enabled: true - # Info endpoint - Shows information on the application as build infor, etc. - info: - # Enable / disable info endpoint - enabled: false - # Metrics endpoint - Shows several metrics on running EHRbase - metrics: - # Enable / disable metrics endpoint - enabled: false - # Prometheus metric endpoint - Special metrics format to display in microservice observer solutions - prometheus: - metrics: - export: - enabled: true -# External Terminology Validation Properties -validation: - external-terminology: - enabled: false - provider: - fhir: - type: FHIR - url: https://r4.ontoserver.csiro.au/fhir/ - -# SSL Properties (used by Spring WebClient and Apache HTTP Client) -client: - ssl: - enabled: false - -# JavaMelody -javamelody: - enabled: false - -# plugin configuration -plugin-manager: - plugin-dir: ./plugin_dir - plugin-config-dir: ./plugin_config_dir - enable: true - plugin-context-path: /plugin diff --git a/application/src/main/resources/banner-cli.txt b/application/src/main/resources/banner-cli.txt new file mode 100644 index 000000000..6a8b00f51 --- /dev/null +++ b/application/src/main/resources/banner-cli.txt @@ -0,0 +1 @@ +${AnsiColor.BLUE}EHRbase CLI (Spring Boot ${spring-boot.version} EHRbase ${application.formatted-version} https://ehrbase.org/)${AnsiBackground.DEFAULT}${AnsiColor.DEFAULT} \ No newline at end of file diff --git a/application/src/main/resources/static/img/ehrbase.png b/application/src/main/resources/static/img/ehrbase.png new file mode 100644 index 000000000..684e11c78 Binary files /dev/null and b/application/src/main/resources/static/img/ehrbase.png differ diff --git a/application/src/main/resources/static/img/favicons/apple-icon-120x120.png b/application/src/main/resources/static/img/favicons/apple-icon-120x120.png new file mode 100644 index 000000000..d84a25a29 Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-120x120.png differ diff --git a/application/src/main/resources/static/img/favicons/apple-icon-152x152.png b/application/src/main/resources/static/img/favicons/apple-icon-152x152.png new file mode 100644 index 000000000..7232b8bf5 Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-152x152.png differ diff --git a/application/src/main/resources/static/img/favicons/apple-icon-60x60.png b/application/src/main/resources/static/img/favicons/apple-icon-60x60.png new file mode 100644 index 000000000..416ad41cc Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-60x60.png differ diff --git a/application/src/main/resources/static/img/favicons/apple-icon-76x76.png b/application/src/main/resources/static/img/favicons/apple-icon-76x76.png new file mode 100644 index 000000000..e8ba74a88 Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-76x76.png differ diff --git a/application/src/main/resources/static/img/favicons/favicon-16x16.png b/application/src/main/resources/static/img/favicons/favicon-16x16.png new file mode 100644 index 000000000..6c5d99153 Binary files /dev/null and b/application/src/main/resources/static/img/favicons/favicon-16x16.png differ diff --git a/application/src/main/resources/static/img/favicons/favicon-32x32.png b/application/src/main/resources/static/img/favicons/favicon-32x32.png new file mode 100644 index 000000000..0a49b0e9a Binary files /dev/null and b/application/src/main/resources/static/img/favicons/favicon-32x32.png differ diff --git a/application/src/main/resources/static/index.html b/application/src/main/resources/static/index.html new file mode 100644 index 000000000..2c11459ef --- /dev/null +++ b/application/src/main/resources/static/index.html @@ -0,0 +1,31 @@ + + + + + + + EHRbase Open Source + + + + + + + + + +EHRbase Open Source + + \ No newline at end of file diff --git a/application/src/test/java/org/ehrbase/application/abac/AbacIntegrationTest.java b/application/src/test/java/org/ehrbase/application/abac/AbacIntegrationTest.java deleted file mode 100644 index 443c28fb4..000000000 --- a/application/src/test/java/org/ehrbase/application/abac/AbacIntegrationTest.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.abac; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.jayway.jsonpath.JsonPath; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import org.apache.commons.io.IOUtils; -import org.ehrbase.openehr.sdk.test_data.composition.CompositionTestDataCanonicalJson; -import org.ehrbase.openehr.sdk.test_data.operationaltemplate.OperationalTemplateTestData; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.info.BuildProperties; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.ResourceUtils; - -@SpringBootTest -@ActiveProfiles({"local", "test"}) -// @EnabledIfEnvironmentVariable(named = "EHRBASE_ABAC_IT_TEST", matches = "true") -@AutoConfigureMockMvc -@Disabled("see https://jira.vitagroup.ag/browse/CDR-368") -class AbacIntegrationTest { - - private static final String ORGA_ID = "f47bfc11-ec8d-412e-aebf-c6953cc23e7d"; - - @MockBean - private AbacConfig.AbacCheck abacCheck; - - @MockBean - private BuildProperties buildProperties; - - @Autowired - private MockMvc mockMvc; - - @Autowired - private AbacConfig abacConfig; - - @Test - /* - * This test requires a new and clean DB state to run successfully. - */ - - void testAbacIntegrationTest() throws Exception { - /* - ----------------- TEST CONTEXT SETUP ----------------- - */ - // Configure the mock bean of the ABAC server, so we can test with this external service. - given(this.abacCheck.execute(anyString(), anyMap())).willReturn(true); - - Map attributes = new HashMap<>(); - attributes.put("sub", "my-id"); - attributes.put("email", "test@test.org"); - - // Counters to keep track of number of requests to mock ABAC server bean - int hasConsentPatientCount = 0; - int hasConsentTemplateCount = 0; - - String externalSubjectRef = UUID.randomUUID().toString(); - - String ehrStatus = String.format( - IOUtils.toString(ResourceUtils.getURL("classpath:ehr_status.json"), StandardCharsets.UTF_8), - externalSubjectRef); - - MvcResult result = mockMvc.perform(post("/rest/openehr/v1/ehr") - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE) - .contentType(MediaType.APPLICATION_JSON) - .content(ehrStatus)) - .andExpectAll(status().isCreated(), jsonPath("$.ehr_id.value").exists()) - .andReturn(); - - String ehrId = JsonPath.read(result.getResponse().getContentAsString(), "$.ehr_id.value"); - Assertions.assertNotNull(ehrId); - assertNotEquals("", ehrId); - - InputStream stream = OperationalTemplateTestData.CORONA_ANAMNESE.getStream(); - Assertions.assertNotNull(stream); - String streamString = IOUtils.toString(stream, StandardCharsets.UTF_8); - - mockMvc.perform(post("/rest/openehr/v1/definition/template/adl1.4/") - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .content(streamString) - .contentType(MediaType.APPLICATION_XML) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_XML)) - .andExpect(r -> assertTrue( - // created 201 or conflict 409 are okay - r.getResponse().getStatus() == HttpStatus.CREATED.value() - || r.getResponse().getStatus() == HttpStatus.CONFLICT.value())); - - stream = CompositionTestDataCanonicalJson.CORONA.getStream(); - Assertions.assertNotNull(stream); - streamString = IOUtils.toString(stream, StandardCharsets.UTF_8); - - /* - ----------------- TEST CASES ----------------- - */ - - /* - GET EHR - */ - mockMvc.perform(get(String.format("/rest/openehr/v1/ehr/%s", ehrId)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()); - - verify(abacCheck, times(++hasConsentPatientCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - } - }); - /* - GET EHR_STATUS - */ - result = mockMvc.perform(get(String.format("/rest/openehr/v1/ehr/%s/ehr_status", ehrId)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()) - .andReturn(); - - verify(abacCheck, times(++hasConsentPatientCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - } - }); - - String ehrStatusVersionUid = JsonPath.read(result.getResponse().getContentAsString(), "$.uid.value"); - Assertions.assertNotNull(ehrStatusVersionUid); - assertNotEquals("", ehrStatusVersionUid); - - /* - PUT EHR_STATUS - */ - mockMvc.perform(put(String.format("/rest/openehr/v1/ehr/%s/ehr_status", ehrId)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("If-Match", ehrStatusVersionUid) - .header("PREFER", "return=representation") - .content(ehrStatus) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()); - - verify(abacCheck, times(++hasConsentPatientCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - } - }); - /* - GET VERSIONED_EHR_STATUS - */ - mockMvc.perform(get(String.format("/rest/openehr/v1/ehr/%s/versioned_ehr_status/version", ehrId)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()); - - verify(abacCheck, times(++hasConsentPatientCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - } - }); - /* - POST COMPOSITION - */ - result = mockMvc.perform(post(String.format("/rest/openehr/v1/ehr/%s/composition", ehrId)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .content(streamString) - .contentType(MediaType.APPLICATION_JSON) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()) - .andReturn(); - - String compositionVersionUid = JsonPath.read(result.getResponse().getContentAsString(), "$.uid.value"); - Assertions.assertNotNull(compositionVersionUid); - assertNotEquals("", compositionVersionUid); - assertTrue(compositionVersionUid.contains("::")); - - verify(abacCheck, times(++hasConsentTemplateCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "Corona_Anamnese"); - } - }); - /* - GET VERSIONED_COMPOSITION - */ - mockMvc.perform(get(String.format( - "/rest/openehr/v1/ehr/%s/versioned_composition/%s/version/%s", - ehrId, compositionVersionUid.split("::")[0], compositionVersionUid)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()); - - verify(abacCheck, times(++hasConsentTemplateCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "Corona_Anamnese"); - } - }); - - /* - GET COMPOSITION (here of deleted composition) - */ - mockMvc.perform(get(String.format("/rest/openehr/v1/ehr/%s/composition/%s", ehrId, compositionVersionUid)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()); - - // Failing: Does not call ABAC Server. Deleted composition does not need to call ABAC server? - verify(abacCheck, times(++hasConsentTemplateCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "Corona_Anamnese"); - } - }); - - /* - DELETE COMPOSITION - */ - mockMvc.perform(delete(String.format("/rest/openehr/v1/ehr/%s/composition/%s", ehrId, compositionVersionUid)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isNoContent()); - - verify(abacCheck, times(++hasConsentTemplateCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "Corona_Anamnese"); - } - }); - - String contribution = String.format( - IOUtils.toString(ResourceUtils.getURL("classpath:contribution.json"), StandardCharsets.UTF_8), - streamString); - /* - POST CONTRIBUTION - */ - mockMvc.perform(post(String.format("/rest/openehr/v1/ehr/%s/contribution", ehrId)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .content(contribution) - .contentType(MediaType.APPLICATION_JSON) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()) - .andReturn(); - - verify(abacCheck, times(++hasConsentTemplateCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "Corona_Anamnese"); - } - }); - - /* - POST QUERY - */ - mockMvc.perform(post("/rest/openehr/v1/query/aql") - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .content("{\n" - + " \"q\": \"select e/ehr_id/value, c/uid/value, c/archetype_details/template_id/value, c/feeder_audit from EHR e CONTAINS composition c\"\n" - + "}") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()); - - verify(abacCheck, times(++hasConsentTemplateCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "Corona_Anamnese"); - } - }); - - /* - GET QUERY - */ - String pathQuery = - "select e/ehr_id/value, c/uid/value, c/archetype_details/template_id/value, c/feeder_audit from EHR e CONTAINS composition c"; - - mockMvc.perform(get(String.format("/rest/openehr/v1/query/aql?q=%s", pathQuery)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))) - .andExpect(status().isOk()); - - verify(abacCheck, times(++hasConsentTemplateCount)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "Corona_Anamnese"); - } - }); - - /* - GET QUERY WITH MULTIPLE EHRs AND TEMPLATES (incl. posting those) - */ - // post another template - stream = OperationalTemplateTestData.MINIMAL_EVALUATION.getStream(); - Assertions.assertNotNull(stream); - streamString = IOUtils.toString(stream, StandardCharsets.UTF_8); - - mockMvc.perform(post("/rest/openehr/v1/definition/template/adl1.4/") - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .content(streamString) - .contentType(MediaType.APPLICATION_XML) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_XML)) - .andExpect(r -> assertTrue( - // created 201 or conflict 409 are okay - r.getResponse().getStatus() == HttpStatus.CREATED.value() - || r.getResponse().getStatus() == HttpStatus.CONFLICT.value())); - - streamString = IOUtils.toString(ResourceUtils.getURL("classpath:composition.json"), StandardCharsets.UTF_8); - - mockMvc.perform(post(String.format("/rest/openehr/v1/ehr/%s/composition", ehrId)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID))) - .content(streamString) - .contentType(MediaType.APPLICATION_JSON) - .header("PREFER", "return=representation") - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()) - .andReturn(); - - verify(abacCheck) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "minimal_evaluation.en.v1"); - } - }); - - mockMvc.perform(get(String.format("/rest/openehr/v1/query/aql?q=%s", pathQuery)) - .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)) - .jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef) - .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))) - .andExpect(status().isOk()); - - // verify(abacCheck, times(++hasConsentTemplateCount)).execute( - // "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{ - // put("patient", externalSubjectRef); - // put("organization", ORGA_ID); - // put("template", "Corona_Anamnese"); - // }}); - - verify(abacCheck, times(3)) - .execute("http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() { - { - put("patient", externalSubjectRef); - put("organization", ORGA_ID); - put("template", "minimal_evaluation.en.v1"); - } - }); - } -} diff --git a/application/src/test/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionRootTest.java b/application/src/test/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionRootTest.java deleted file mode 100644 index df0d11bd7..000000000 --- a/application/src/test/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionRootTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.abac; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.stream.Stream; -import org.ehrbase.api.service.EhrService; -import org.ehrbase.aql.compiler.AuditVariables; -import org.ehrbase.rest.BaseController; -import org.junit.Assert; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; - -class CustomMethodSecurityExpressionRootTest { - - @Test - void patientHandling() { - String PAT_TOKEN = "myToken"; - Map reqMap = new HashMap<>(); - Assert.assertTrue(theTest(PAT_TOKEN, List.of(PAT_TOKEN, PAT_TOKEN), reqMap)); - Assert.assertTrue(reqMap.containsKey(CustomMethodSecurityExpressionRoot.PATIENT)); - } - - @Test - void patientHandlingFail() { - String PAT_TOKEN = "myToken"; - Assert.assertFalse(theTest("No_PAT_TOKEN", List.of(PAT_TOKEN, PAT_TOKEN), null)); - } - - @SuppressWarnings({"unchecked", "serial"}) - private boolean theTest(String theSubject, List subjectExtRef, Map reqMap) { - final String CLAIM = "myClaim"; - - AbacConfig cfg = anyAbacConfig(c -> c.setPatientClaim(CLAIM)); - - var token = Mockito.mock(JwtAuthenticationToken.class); - Jwt jwt = Jwt.withTokenValue("token") - .header("alg", "none") - .claim(CLAIM, theSubject) - .build(); - Mockito.when(((Jwt) token.getCredentials())).thenReturn(jwt); - - EhrService service = Mockito.mock(EhrService.class); - Mockito.when(service.getSubjectExtRefs(Mockito.isA(Collection.class))).thenReturn(subjectExtRef); - - CustomMethodSecurityExpressionRoot expr = - new CustomMethodSecurityExpressionRoot(Mockito.mock(Authentication.class), cfg, null); - expr.setEhrService(service); - - Map payload = new HashMap<>() { - { - put(AuditVariables.EHR_PATH, Set.of(UUID.randomUUID(), UUID.randomUUID())); - } - }; - - return expr.patientHandling(token, theSubject, reqMap, BaseController.QUERY, payload); - } - - @SuppressWarnings({"unchecked"}) - private AbacConfig anyAbacConfig(Consumer... constraints) { - AbacConfig cfg = new AbacConfig(); - Stream.of(constraints).forEach(c -> c.accept(cfg)); - return cfg; - } -} diff --git a/application/src/test/java/org/ehrbase/application/abac/FlywayTestConfiguration.java b/application/src/test/java/org/ehrbase/application/abac/FlywayTestConfiguration.java deleted file mode 100644 index f41bc0c62..000000000 --- a/application/src/test/java/org/ehrbase/application/abac/FlywayTestConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.abac; - -import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class FlywayTestConfiguration { - - @Bean - public FlywayMigrationStrategy clean() { - return flyway -> { - flyway.clean(); - flyway.migrate(); - }; - } -} diff --git a/application/src/test/java/org/ehrbase/application/cors/CorsBasicAuthIT.java b/application/src/test/java/org/ehrbase/application/cors/CorsBasicAuthIT.java deleted file mode 100644 index ccfbf5e61..000000000 --- a/application/src/test/java/org/ehrbase/application/cors/CorsBasicAuthIT.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.cors; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; - -@SpringBootTest(properties = {"security.authType=basic", "spring.cache.type=simple"}) -@AutoConfigureMockMvc -@Disabled -class CorsBasicAuthIT { - - @Autowired - private MockMvc mockMvc; - - @Test - void testCors() throws Exception { - mockMvc.perform(options("/rest/openehr/v1/definition/template/adl1.4") - .header("Access-Control-Request-Method", "GET") - .header("Origin", "https://client.ehrbase.org")) - .andDo(print()) - .andExpect(status().isOk()); - } -} diff --git a/application/src/test/java/org/ehrbase/application/cors/CorsNoAuthIT.java b/application/src/test/java/org/ehrbase/application/cors/CorsNoAuthIT.java deleted file mode 100644 index 6db2cb050..000000000 --- a/application/src/test/java/org/ehrbase/application/cors/CorsNoAuthIT.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ehrbase.application.cors; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; - -@SpringBootTest(properties = {"spring.cache.type=simple"}) -@AutoConfigureMockMvc -@Disabled -class CorsNoAuthIT { - - @Autowired - private MockMvc mockMvc; - - @Test - void testCors() throws Exception { - mockMvc.perform(options("/rest/openehr/v1/definition/template/adl1.4") - .header("Access-Control-Request-Method", "GET") - .header("Origin", "https://client.ehrbase.org")) - .andDo(print()) - .andExpect(status().isOk()); - } -} diff --git a/aql-engine/pom.xml b/aql-engine/pom.xml new file mode 100644 index 000000000..12dd73c7a --- /dev/null +++ b/aql-engine/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + org.ehrbase.openehr + server + 2.7.0 + + + aql-engine + + + 21 + 21 + UTF-8 + + + + + + org.ehrbase.openehr + rm-db-format + + + org.ehrbase.openehr + api + + + org.ehrbase.openehr + jooq-pg + + + org.ehrbase.openehr.sdk + validation + + + + + org.springframework + spring-context + + + org.springframework + spring-core + + + org.springframework + spring-tx + + + org.springframework + spring-web + + + + + org.apache.commons + commons-lang3 + + + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlEngineModuleConfiguration.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlEngineModuleConfiguration.java new file mode 100644 index 000000000..e71dd30f4 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlEngineModuleConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@Configuration +@ComponentScan +@EnableAspectJAutoProxy +public class AqlEngineModuleConfiguration {} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacement.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacement.java new file mode 100644 index 000000000..bfaee4181 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacement.java @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.collections4.MapUtils; +import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.condition.ComparisonOperatorCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.ExistsCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.LikeCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.LogicalOperatorCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.MatchesCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.NotCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.WhereCondition; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.Containment; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentNotOperator; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperator; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentVersionExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.BooleanPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.ComparisonLeftOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.DoublePrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.LikeOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.LongPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.MatchesOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.Operand; +import org.ehrbase.openehr.sdk.aql.dto.operand.PathPredicateOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.QueryParameter; +import org.ehrbase.openehr.sdk.aql.dto.operand.SingleRowFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.TemporalPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.TerminologyFunction; +import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPathUtil; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.select.SelectClause; +import org.ehrbase.openehr.sdk.aql.dto.select.SelectExpression; +import org.ehrbase.openehr.sdk.aql.parser.AqlParseException; + +/** + * Replaces parameters in an AQL query + */ +public final class AqlParameterReplacement { + + private AqlParameterReplacement() { + // NOOP + } + + /** + * Replaces all parameters in the aqlQuery with values from the parameterMap. + * The replacement is performed in-place, modifying the source object. + * Missing parameter values are set to NULL. + * + * @param aqlQuery the query to me modified + * @param parameterMap a map of parameter values + */ + public static void replaceParameters(AqlQuery aqlQuery, Map parameterMap) { + if (MapUtils.isNotEmpty(parameterMap)) { + // SELECT + SelectParams.replaceParameters(aqlQuery.getSelect(), parameterMap); + // FROM + ContainmentParams.replaceParameters(aqlQuery.getFrom(), parameterMap); + // WHERE + WhereParams.replaceParameters(aqlQuery.getWhere(), parameterMap); + // ORDER BY + OrderByParams.replaceParameters(parameterMap, aqlQuery.getOrderBy()); + } + } + + public static void replaceIdentifiedPathParameters( + IdentifiedPath identifiedPath, Map parameterMap) { + // revise root predicates in-place + Optional.of(identifiedPath).map(IdentifiedPath::getRootPredicate).stream() + .flatMap(List::stream) + .map(AndOperatorPredicate::getOperands) + .flatMap(List::stream) + .forEach(co -> ObjectPathParams.replaceComparisonOperatorParameters(co, true, parameterMap)); + ObjectPathParams.replaceParameters(identifiedPath.getPath(), parameterMap) + .ifPresent(identifiedPath::setPath); + } + + /** + * @param operand + * @param parameterMap + * @return the new primitive, if the operand needs to be replaced + */ + private static Optional replaceOperandParameters(Operand operand, Map parameterMap) { + + return switch (operand) { + case QueryParameter qp -> resolveParameter(qp, parameterMap); + case IdentifiedPath path -> { + replaceIdentifiedPathParameters(path, parameterMap); + yield Optional.empty(); + } + case SingleRowFunction func -> { + replaceFunctionParameters(func, parameterMap); + yield Optional.empty(); + } + case TerminologyFunction __ -> Optional.empty(); + case Primitive __ -> Optional.empty(); + }; + } + + private static void replaceFunctionParameters(SingleRowFunction func, Map parameterMap) { + Utils.reviseList(func.getOperandList(), o -> replaceOperandParameters(o, parameterMap)); + } + + private static Optional resolveParameter(QueryParameter param, Map parameterMap) { + String paramName = param.getName(); + Object paramValue = parameterMap.get(paramName); + + return Optional.of( + switch (paramValue) { + case null -> throw new AqlParseException("Missing parameter '%s'".formatted(paramName)); + case Integer i -> new LongPrimitive(i.longValue()); + case Long i -> new LongPrimitive(i); + case Number nr -> new DoublePrimitive(nr.doubleValue()); + case String str -> Utils.stringToPrimitive(str); + case Boolean b -> new BooleanPrimitive(b); + default -> { + throw new IllegalArgumentException( + "Type of parameter '%s' is not supported".formatted(paramName)); + } + }); + } + + private record ModifiedElement(int index, T node) {} + + private static final class OrderByParams { + + public static void replaceParameters(Map parameterMap, List orderBy) { + if (orderBy != null) { + orderBy.stream() + .map(OrderByExpression::getStatement) + .forEach(path -> replaceIdentifiedPathParameters(path, parameterMap)); + } + } + } + + private static final class SelectParams { + + public static void replaceParameters(SelectClause selectClause, Map parameterMap) { + selectClause.getStatement().stream() + .map(SelectExpression::getColumnExpression) + .forEach(ce -> { + switch (ce) { + case SingleRowFunction func -> replaceFunctionParameters(func, parameterMap); + case AggregateFunction aFunc -> replaceIdentifiedPathParameters( + aFunc.getIdentifiedPath(), parameterMap); + case IdentifiedPath identifiedPath -> replaceIdentifiedPathParameters( + identifiedPath, parameterMap); + case Primitive __ -> { + /* No parameters */ + } + case TerminologyFunction __ -> { + /* No parameters */ + } + } + }); + } + } + + private static final class WhereParams { + public static void replaceParameters(WhereCondition condition, Map parameterMap) { + switch (condition) { + case null -> { + /* NOOP */ + } + case ComparisonOperatorCondition c -> { + replaceComparisonLeftOperandParameters(c.getStatement(), parameterMap); + replaceOperandParameters(c.getValue(), parameterMap).ifPresent(c::setValue); + } + case NotCondition c -> replaceParameters(c.getConditionDto(), parameterMap); + case MatchesCondition c -> Utils.reviseList( + c.getValues(), o -> replaceMatchesParameters(o, parameterMap)); + case LikeCondition c -> replaceLikeOperandParameters(c.getValue(), parameterMap) + .ifPresent(c::setValue); + case LogicalOperatorCondition c -> c.getValues().forEach(v -> replaceParameters(v, parameterMap)); + case ExistsCondition __ -> { + /* NOOP */ + } + default -> throw new IllegalStateException("Unexpected value: " + condition); + } + } + + private static Optional replaceLikeOperandParameters( + LikeOperand value, Map parameterMap) { + if (value instanceof QueryParameter qp) { + return Optional.of(qp.getName()) + .map(parameterMap::get) + .map(Object::toString) + .map(Utils::stringToPrimitive) + .or(() -> Optional.of(new StringPrimitive(null))); + } else { + return Optional.empty(); + } + } + + private static Optional replaceMatchesParameters( + MatchesOperand operand, Map parameterMap) { + if (operand instanceof QueryParameter qp) { + return resolveParameter(qp, parameterMap); + } else { + return Optional.empty(); + } + } + + private static void replaceComparisonLeftOperandParameters( + ComparisonLeftOperand statement, Map parameterMap) { + switch (statement) { + case SingleRowFunction func -> replaceFunctionParameters(func, parameterMap); + case IdentifiedPath path -> replaceIdentifiedPathParameters(path, parameterMap); + case TerminologyFunction __ -> { + /* cannot contain parameters */ + } + } + } + } + + private static final class ContainmentParams { + public static void replaceParameters(Containment containment, Map parameterMap) { + switch (containment) { + case null -> { + /*NOOP*/ + } + case ContainmentSetOperator cso -> cso.getValues().forEach(c -> replaceParameters(c, parameterMap)); + case ContainmentNotOperator cno -> replaceParameters(cno.getContainmentExpression(), parameterMap); + case ContainmentClassExpression cce -> replaceContainmentClassExpressionParameters(cce, parameterMap); + case ContainmentVersionExpression cve -> replaceContainmentVersionExpressionParameters( + cve, parameterMap); + } + } + + private static void replaceContainmentClassExpressionParameters( + ContainmentClassExpression cce, Map parameterMap) { + streamComparisonOperatorPredicates(cce) + .forEach(op -> ObjectPathParams.replaceComparisonOperatorParameters(op, true, parameterMap)); + replaceParameters(cce.getContains(), parameterMap); + } + + private static void replaceContainmentVersionExpressionParameters( + ContainmentVersionExpression cve, Map parameterMap) { + Optional.of(cve) + .map(ContainmentVersionExpression::getPredicate) + .ifPresent(op -> ObjectPathParams.replaceComparisonOperatorParameters(op, true, parameterMap)); + replaceParameters(cve.getContains(), parameterMap); + } + + private static Stream streamComparisonOperatorPredicates( + ContainmentClassExpression cce) { + return Optional.of(cce) + .filter(AbstractContainmentExpression::hasPredicates) + .map(AbstractContainmentExpression::getPredicates) + .stream() + .flatMap(List::stream) + .map(AndOperatorPredicate::getOperands) + .flatMap(Collection::stream); + } + } + + private static final class ObjectPathParams { + + /** + * Replaces all parameters. + * If parameters were replaced, the modified AqlObjectPath is returned. + * The provided path remains unchanged. + * + * @param path + * @param parameterMap + * @return + */ + public static Optional replaceParameters(AqlObjectPath path, Map parameterMap) { + if (path == null) { + return Optional.empty(); + } + return Utils.replaceChildParameters( + path.getPathNodes(), ObjectPathParams::replacePathNodeParameters, parameterMap) + .map(AqlObjectPath::new); + } + + private static Optional replaceComparisonOperatorPredicateParameters( + ComparisonOperatorPredicate n, Map parameterMap) { + Optional replacedPath = replaceParameters(n.getPath(), parameterMap); + + Optional replacedValue = + switch (n.getValue()) { + case QueryParameter qp -> { + Optional newPrimitive = resolveParameter(qp, parameterMap); + newPrimitive.ifPresent(p -> validateParameterSyntax(n.getPath(), p)); + yield newPrimitive.map(PathPredicateOperand.class::cast); + } + case Primitive __ -> Optional.empty(); + case AqlObjectPath p -> replaceParameters(p, parameterMap) + .map(PathPredicateOperand.class::cast); + default -> throw new IllegalStateException("Unexpected value: " + n.getValue()); + }; + + if (replacedPath.isEmpty() && replacedValue.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(new ComparisonOperatorPredicate( + replacedPath.orElse(n.getPath()), n.getOperator(), replacedValue.orElse(n.getValue()))); + } + + /** + * if ARCHETYPE_NODE_ID: check syntax + * @param path + * @param p + */ + private static void validateParameterSyntax(AqlObjectPath path, Primitive p) { + if (AqlObjectPathUtil.ARCHETYPE_NODE_ID.equals(path)) { + if (p instanceof StringPrimitive sp) { + try { + AslRmTypeAndConcept.fromArchetypeNodeId(sp.getValue()); + } catch (IllegalArgumentException e) { + throw new AqlParseException( + "Invalid parameter for %s".formatted(AqlObjectPathUtil.ARCHETYPE_NODE_ID)); + } + } else { + throw new AqlParseException( + "Invalid parameter type for %s".formatted(AqlObjectPathUtil.ARCHETYPE_NODE_ID)); + } + } + } + + private static Optional replaceAndOperatorPredicateParameters( + AndOperatorPredicate and, Map parameterMap) { + return Utils.replaceChildParameters( + and.getOperands(), + ObjectPathParams::replaceComparisonOperatorPredicateParameters, + parameterMap) + .map(AndOperatorPredicate::new); + } + + private static Optional replacePathNodeParameters( + AqlObjectPath.PathNode node, Map parameterMap) { + return Utils.replaceChildParameters( + node.getPredicateOrOperands(), + ObjectPathParams::replaceAndOperatorPredicateParameters, + parameterMap) + .map(l -> new AqlObjectPath.PathNode(node.getAttribute(), l)); + } + + /** + * {@link ComparisonOperatorPredicate}s are used in + *

    + *
  • ContainmentClassExpression.predicates.predicateOrOperands.operands/li> + *
  • ContainmentVersionExpression.predicate
  • + *
  • IdentifiedPath.rootPredicate.predicateOrOperands.operands
  • + *
  • IdentifiedPath.path (via AqlObjectPath)
  • + *
  • AqlObjectPath.pathNodes.predicateOrOperands.operands
  • + *
  • code>ComparisonOperatorPredicate.path (recursively via AqlObjectPath)
  • + *
  • code>ComparisonOperatorPredicate.value (recursively via PathPredicateOperand implementation AqlObjectPath)
  • + *
+ * + * XXX In case of ComparisonOperatorPredicate, AqlObjectPath and ContainmentVersionExpression + * the replacement may not be performed in-place. Here the data structure has to be replaced. + * + * @param op + * @param inPlace + * @param parameterMap + */ + public static Optional replaceComparisonOperatorParameters( + ComparisonOperatorPredicate op, boolean inPlace, Map parameterMap) { + + Optional newPath = replaceParameters(op.getPath(), parameterMap); + + Optional newValue = + switch (op.getValue()) { + case null -> throw new NullPointerException( + "Missing value for path " + op.getPath().render()); + case QueryParameter qp -> { + Optional primitive = resolveParameter(qp, parameterMap); + primitive.ifPresent(p -> validateParameterSyntax(op.getPath(), p)); + yield primitive.map(PathPredicateOperand.class::cast); + } + case Primitive __ -> Optional.empty(); + case AqlObjectPath path -> replaceParameters(path, parameterMap) + .map(PathPredicateOperand.class::cast); + default -> throw new IllegalArgumentException("Unexpected type of value: " + + op.getValue().getClass().getSimpleName()); + }; + + if (inPlace) { + newPath.ifPresent(op::setPath); + newValue.ifPresent(op::setValue); + return Optional.empty(); + + } else if (newPath.isPresent() || newValue.isPresent()) { + return Optional.of(new ComparisonOperatorPredicate( + newPath.orElseGet(op::getPath), op.getOperator(), newValue.orElseGet(op::getValue))); + + } else { + return Optional.empty(); + } + } + } + + static final class TemporalPrimitivePattern { + + static final Pattern TEMPORAL_PATTERN; + + static { + // see AqlLexer.g4 + String DIGIT = "[0-9]"; + // Year in ISO8601:2004 is 4 digits with 0-filling as needed + String YEAR = DIGIT + DIGIT + DIGIT + DIGIT; + // month in year + String MONTH = nonCapturing(or("[0][1-9]", "[1][0-2]")); + // day in month + String DAY = nonCapturing(or("[0][1-9]", "[12][0-9]", "[3][0-1]")); + // hour in 24 hour clock + String HOUR = nonCapturing(or("[01][0-9]", "[2][0-3]")); + // minutes + String MINUTE = "[0-5][0-9]"; + String SECOND = MINUTE; + + String DATE_SHORT = YEAR + MONTH + DAY; + String DATE_LONG = YEAR + '-' + MONTH + '-' + DAY; + String TIME_SHORT = HOUR + MINUTE + SECOND; + String TIME_LONG = HOUR + ':' + MINUTE + ':' + SECOND; + String FRACTIONAL_SECONDS = "\\." + nonCapturing(DIGIT) + "{1,9}"; + // hour offset, e.g. `+09:30`, or else literal `Z` indicating +0000. + String TIMEZONE = or("Z", nonCapturing("[-+]", HOUR, optional("[:]?" + MINUTE))); + + TEMPORAL_PATTERN = Pattern.compile(or( + // extended datetime + DATE_LONG + optional("T", TIME_LONG, optional(FRACTIONAL_SECONDS), optional(TIMEZONE)), + // compact datetime + DATE_SHORT + optional("T", TIME_SHORT, optional(FRACTIONAL_SECONDS), optional(TIMEZONE)), + // compact & extended time + nonCapturing(or(TIME_SHORT, TIME_LONG)) + optional(FRACTIONAL_SECONDS) + optional(TIMEZONE))); + } + + private TemporalPrimitivePattern() {} + + private static String nonCapturing(String... content) { + return Arrays.stream(content).collect(Collectors.joining("", "(?:", ")")); + } + + private static String or(String... content) { + return String.join("|", content); + } + + private static String optional(String... content) { + return nonCapturing(content) + "?"; + } + + public static boolean matches(String input) { + return TEMPORAL_PATTERN.matcher(input).matches(); + } + } + + private static final class Utils { + + public static StringPrimitive stringToPrimitive(String str) { + return TemporalPrimitivePattern.matches(str) ? new TemporalPrimitive(str) : new StringPrimitive(str); + } + + /** + * For each entry of the list the replacementFunc is called. + * It if returns a new entry, the old one is replaced. + * + * @param list + * @param replacementFunc + * @param + */ + public static void reviseList(List list, Function> replacementFunc) { + final ListIterator li = list.listIterator(); + while (li.hasNext()) { + replacementFunc.apply(li.next()).ifPresent(li::set); + } + } + + /** + * Generic function to hierarchically replace all parameters of an object. + * If parameters were replaced, a new list with modified objects is returned. + * The provided children remain unchanged. + * + * @param children + * @param childReplacementFunc returns an Optional with a modified child, if applicable + * @param parameterMap + * @return + */ + public static Optional> replaceChildParameters( + List children, + BiFunction, Optional> childReplacementFunc, + Map parameterMap) { + + ModifiedElement[] modifiedElements = IntStream.range(0, children.size()) + .mapToObj(i -> childReplacementFunc + .apply(children.get(i), parameterMap) + .map(m -> new ModifiedElement(i, m))) + .flatMap(Optional::stream) + .toArray(ModifiedElement[]::new); + + if (modifiedElements.length == 0) { + return Optional.empty(); + } + + C[] newChildren = (C[]) children.toArray(); + for (ModifiedElement modifiedNode : modifiedElements) { + newChildren[modifiedNode.index()] = modifiedNode.node(); + } + return Optional.of(List.of(newChildren)); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlQueryUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlQueryUtils.java new file mode 100644 index 000000000..65f91af01 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlQueryUtils.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine; + +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Stream; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.condition.ComparisonOperatorCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.ExistsCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.LikeCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.LogicalOperatorCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.MatchesCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.NotCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.WhereCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.ColumnExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.ComparisonLeftOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.Operand; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.QueryParameter; +import org.ehrbase.openehr.sdk.aql.dto.operand.SingleRowFunction; +import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression; +import org.ehrbase.openehr.sdk.aql.dto.select.SelectExpression; + +public final class AqlQueryUtils { + private AqlQueryUtils() {} + + public static Stream allIdentifiedPaths(AqlQuery query) { + + return Stream.of( + query.getSelect().getStatement().stream().flatMap(AqlQueryUtils::allIdentifiedPaths), + streamWhereConditions(query.getWhere()).flatMap(AqlQueryUtils::allIdentifiedPaths), + Optional.of(query).map(AqlQuery::getOrderBy).stream() + .flatMap(Collection::stream) + .map(OrderByExpression::getStatement)) + .flatMap(s -> s); + } + + public static Stream allIdentifiedPaths(WhereCondition w) { + if (w instanceof ComparisonOperatorCondition c) { + return Stream.concat(allIdentifiedPaths(c.getStatement()), allIdentifiedPaths(c.getValue())); + } else if (w instanceof MatchesCondition c) { + return Stream.of(c.getStatement()); + } else if (w instanceof LikeCondition c) { + return Stream.of(c.getStatement()); + } else if (w instanceof ExistsCondition c) { + // XXX Should this be included in the analysis? + return Stream.of(c.getValue()); + } else { + throw new IllegalArgumentException("Unsupported type of " + w); + } + } + + public static Stream allIdentifiedPaths(SelectExpression selectExpression) { + ColumnExpression columnExpression = selectExpression.getColumnExpression(); + if (columnExpression instanceof Primitive) { + return Stream.empty(); + } else if (columnExpression instanceof AggregateFunction f) { + return Optional.of(f).map(AggregateFunction::getIdentifiedPath).stream(); + } else if (columnExpression instanceof IdentifiedPath ip) { + return Stream.of(ip); + } else if (columnExpression instanceof SingleRowFunction f) { + return f.getOperandList().stream().flatMap(AqlQueryUtils::allIdentifiedPaths); + } else { + throw new IllegalArgumentException("Unsupported type of " + columnExpression); + } + } + + public static Stream allIdentifiedPaths(Operand operand) { + if (operand instanceof Primitive) { + return Stream.empty(); + } else if (operand instanceof QueryParameter) { + return Stream.empty(); + } else if (operand instanceof IdentifiedPath ip) { + return Stream.of(ip); + } else if (operand instanceof SingleRowFunction f) { + return f.getOperandList().stream().flatMap(AqlQueryUtils::allIdentifiedPaths); + } else { + throw new IllegalArgumentException("Unsupported type of " + operand); + } + } + + public static Stream allIdentifiedPaths(ComparisonLeftOperand operand) { + if (operand instanceof IdentifiedPath ip) { + return Stream.of(ip); + } else if (operand instanceof SingleRowFunction f) { + return f.getOperandList().stream().flatMap(AqlQueryUtils::allIdentifiedPaths); + } else { + throw new IllegalArgumentException("Unsupported type of " + operand); + } + } + + public static Stream streamWhereConditions(WhereCondition condition) { + if (condition == null) { + return Stream.empty(); + } + return Stream.of(condition).flatMap(c -> { + if (c instanceof ComparisonOperatorCondition + || c instanceof MatchesCondition + || c instanceof LikeCondition + || c instanceof ExistsCondition) { + return Stream.of(c); + } else if (c instanceof LogicalOperatorCondition logical) { + return logical.getValues().stream().flatMap(AqlQueryUtils::streamWhereConditions); + } else if (c instanceof NotCondition not) { + return streamWhereConditions(not.getConditionDto()); + } else { + throw new IllegalStateException("Unsupported condition type %s".formatted(c.getClass())); + } + }); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtils.java new file mode 100644 index 000000000..bf71de5f0 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine; + +import java.util.Map; +import java.util.Optional; +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; +import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap; +import org.ehrbase.jooq.pg.enums.ContributionChangeType; + +public final class ChangeTypeUtils { + public static final BidiMap JOOQ_CHANGE_TYPE_TO_CODE = + UnmodifiableBidiMap.unmodifiableBidiMap(new DualHashBidiMap<>(Map.of( + ContributionChangeType.creation, "249", + ContributionChangeType.amendment, "250", + ContributionChangeType.modification, "251", + ContributionChangeType.synthesis, "252", + ContributionChangeType.Unknown, "253", + ContributionChangeType.deleted, "523"))); + + private ChangeTypeUtils() {} + + public static ContributionChangeType getJooqChangeTypeByCode(String code) { + return Optional.ofNullable(code).map(JOOQ_CHANGE_TYPE_TO_CODE::getKey).orElse(null); + } + + public static String getCodeByJooqChangeType(ContributionChangeType cct) { + return Optional.ofNullable(cct).map(JOOQ_CHANGE_TYPE_TO_CODE::get).orElse(null); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayer.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayer.java new file mode 100644 index 000000000..1b4a2ae5d --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayer.java @@ -0,0 +1,438 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_DATE; +import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_DATE_TIME; +import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_DURATION; +import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_TIME; + +import com.nedap.archie.rm.datavalues.quantity.datetime.DvDate; +import com.nedap.archie.rm.datavalues.quantity.datetime.DvDateTime; +import com.nedap.archie.rm.datavalues.quantity.datetime.DvDuration; +import com.nedap.archie.rm.datavalues.quantity.datetime.DvTime; +import java.time.LocalDate; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.api.service.SystemService; +import org.ehrbase.jooq.pg.enums.ContributionChangeType; +import org.ehrbase.openehr.aqlengine.ChangeTypeUtils; +import org.ehrbase.openehr.aqlengine.asl.AslUtils.AliasProvider; +import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept; +import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslDvOrderedValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFalseQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotNullQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslTrueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslAggregatingField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslDvOrderedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper.SelectType; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ComparisonOperatorConditionWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.ComparisonConditionOperator; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.LogicalConditionOperator; +import org.ehrbase.openehr.aqlengine.querywrapper.where.LogicalOperatorConditionWrapper; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction.AggregateFunctionName; +import org.ehrbase.openehr.sdk.aql.dto.operand.DoublePrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.LongPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.TemporalPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression.OrderByDirection; +import org.ehrbase.openehr.sdk.util.OpenEHRDateTimeSerializationUtils; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.jooq.SortOrder; +import org.springframework.stereotype.Component; + +@Component +public class AqlSqlLayer { + + private static final Set NUMERIC_DV_ORDERED_TYPES = Set.of( + RmConstants.DV_ORDINAL, + RmConstants.DV_SCALE, + RmConstants.DV_PROPORTION, + RmConstants.DV_COUNT, + RmConstants.DV_QUANTITY); + + private final KnowledgeCacheService knowledgeCache; + private final SystemService systemService; + + public AqlSqlLayer(KnowledgeCacheService knowledgeCache, SystemService systemService) { + this.knowledgeCache = knowledgeCache; + this.systemService = systemService; + } + + public AslRootQuery buildAslRootQuery(AqlQueryWrapper query) { + + AliasProvider aliasProvider = new AliasProvider(); + AslRootQuery aslQuery = new AslRootQuery(); + + // FROM + AslFromCreator.ContainsToOwnerProvider containsToStructureSubquery = + new AslFromCreator(aliasProvider, knowledgeCache).addFromClause(aslQuery, query); + + // Paths + final AslPathCreator.PathToField pathToField = new AslPathCreator( + aliasProvider, knowledgeCache, systemService.getSystemId()) + .addPathQueries(query, containsToStructureSubquery, aslQuery); + + // SELECT + if (query.nonPrimitiveSelects().findAny().isEmpty()) { + addSyntheticSelect(query, containsToStructureSubquery, aslQuery); + } else { + boolean usesAggregateFunction = addSelect(query, pathToField, aslQuery); + addOrderBy(query, pathToField, aslQuery, usesAggregateFunction); + } + + // WHERE + Optional.of(query) + .map(AqlQueryWrapper::where) + .flatMap(w -> buildWhereCondition(w, pathToField)) + .ifPresent(aslQuery::addConditionAnd); + + // LIMIT + aslQuery.setLimit(query.limit()); + aslQuery.setOffset(query.offset()); + + return aslQuery; + } + + private static void addOrderBy( + AqlQueryWrapper query, + AslPathCreator.PathToField pathToField, + AslRootQuery rootQuery, + boolean usesAggregateFunction) { + CollectionUtils.emptyIfNull(query.orderBy()) + .forEach(o -> rootQuery.addOrderBy( + pathToField.getField(o.identifiedPath()), + o.direction() == OrderByDirection.DESC ? SortOrder.DESC : SortOrder.ASC, + query.distinct() || usesAggregateFunction)); + } + + /** + * + * @param query + * @param pathToField + * @param rootQuery + * @return if the select contains aggregate functions + */ + private static boolean addSelect( + AqlQueryWrapper query, AslPathCreator.PathToField pathToField, AslRootQuery rootQuery) { + // SELECT + query.nonPrimitiveSelects() + .map(select -> switch (select.type()) { + case PATH -> pathToField.getField(select.getIdentifiedPath().orElseThrow()); + + case AGGREGATE_FUNCTION -> new AslAggregatingField( + select.getAggregateFunctionName(), + // identified path is null for COUNT(*) + pathToField.getField(select.getIdentifiedPath().orElse(null)), + select.isCountDistinct()); + case PRIMITIVE, FUNCTION -> throw new IllegalArgumentException(); + }) + .forEach(rootQuery.getSelect()::add); + + // GROUP BY is determined by the aggregate functions in the select + boolean usesAggregateFunction = + query.nonPrimitiveSelects().anyMatch(s -> s.type() == SelectType.AGGREGATE_FUNCTION); + if (usesAggregateFunction) { + rootQuery + .getGroupByFields() + .addAll(query.nonPrimitiveSelects() + .filter(s -> s.type() != SelectType.AGGREGATE_FUNCTION) + .map(SelectWrapper::getIdentifiedPath) + .flatMap(Optional::stream) + .map(pathToField::getField) + .flatMap(aslField -> aslField.fieldsForAggregation(rootQuery)) + .distinct() + .toList()); + + } else if (query.distinct()) { + // DISTINCT: group by all selects + rootQuery + .getGroupByFields() + .addAll(rootQuery.getSelect().stream() + .flatMap(aslField -> aslField.fieldsForAggregation(rootQuery)) + .distinct() + .toList()); + } + return usesAggregateFunction; + } + + /** + * If a query only selects constants, the number of results is only counted. + * Later, when creating the result set, this determines the number of identical rows + * that are returned. + * + * @param query + * @param containsToStructureSubQuery + * @param rootQuery + */ + private static void addSyntheticSelect( + AqlQueryWrapper query, + AslFromCreator.ContainsToOwnerProvider containsToStructureSubQuery, + AslRootQuery rootQuery) { + AslQuery ownerForSyntheticSelect = containsToStructureSubQuery + // We can get the first since the first chain always must have at least one entry + .get(query.containsChain().chain().getFirst()) + .owner(); + AslColumnField field = rootQuery.getAvailableFields().stream() + .filter(AslColumnField.class::isInstance) + .map(AslColumnField.class::cast) + .filter(f -> f.getOwner() == ownerForSyntheticSelect) + .filter(f -> StringUtils.equalsAny(f.getColumnName(), "id", AslStructureColumn.VO_ID.getFieldName())) + .findFirst() + .orElseThrow(); + rootQuery.getSelect().add(new AslAggregatingField(AggregateFunctionName.COUNT, field, false)); + } + + private Optional buildWhereCondition( + ConditionWrapper condition, AslPathCreator.PathToField pathToField) { + + return switch (condition) { + case LogicalOperatorConditionWrapper lcd -> logicalOperatorCondition( + lcd, c -> buildWhereCondition(c, pathToField)); + case ComparisonOperatorConditionWrapper comparison -> { + AslField aslField = + pathToField.getField(comparison.leftComparisonOperand().path()); + if (aslField == null) { + throw new IllegalArgumentException("unknown field: %s" + .formatted(comparison + .leftComparisonOperand() + .path() + .getPath() + .render())); + } + + if (aslField instanceof AslDvOrderedColumnField dvOrderedField) { + yield buildDvOrderedCondition( + dvOrderedField, comparison.operator(), comparison.rightComparisonOperands()); + } else { + yield fieldValueQueryCondition(aslField, comparison); + } + } + }; + } + + @Nonnull + private static Optional logicalOperatorCondition( + LogicalOperatorConditionWrapper condition, + Function> conditionBuilder) { + + Stream operands = + condition.logicalOperands().stream().map(conditionBuilder).flatMap(Optional::stream); + + if (condition.operator() == LogicalConditionOperator.NOT) { + return Optional.of(LogicalConditionOperator.NOT.build(operands.toList())); + } else { + return AslUtils.reduceConditions(condition.operator(), operands); + } + } + + @Nonnull + private Optional fieldValueQueryCondition( + AslField aslField, ComparisonOperatorConditionWrapper comparison) { + ComparisonConditionOperator operator = comparison.operator(); + return Optional.of( + switch (operator) { + case EXISTS -> aslField.getExtractedColumn() != null + ? new AslTrueQueryCondition() + : new AslNotNullQueryCondition(aslField); + case LIKE, MATCHES, EQ, GT_EQ, GT, LT_EQ, LT, NEQ -> { + List values = whereConditionValues(aslField, comparison, operator); + if (values.isEmpty()) { + yield switch (operator.getAslOperator()) { + case AslConditionOperator.IN, + AslConditionOperator.EQ, + AslConditionOperator.LIKE -> new AslFalseQueryCondition(); + case AslConditionOperator.NEQ -> new AslTrueQueryCondition(); + default -> throw new IllegalArgumentException( + "Unexpected operator %s".formatted(operator.getAslOperator())); + }; + } else { + yield new AslFieldValueQueryCondition<>(aslField, operator.getAslOperator(), values); + } + } + }); + } + + private List whereConditionValues( + AslField aslField, ComparisonOperatorConditionWrapper comparison, ComparisonConditionOperator operator) { + return switch (aslField.getExtractedColumn()) { + case TEMPLATE_ID -> AslUtils.templateIdConditionValues( + comparison.rightComparisonOperands(), operator, knowledgeCache::findUuidByTemplateId); + case ARCHETYPE_NODE_ID -> AslUtils.archetypeNodeIdConditionValues( + comparison.rightComparisonOperands(), operator); + case ROOT_CONCEPT -> AslUtils.archetypeNodeIdConditionValues(comparison.rightComparisonOperands(), operator) + .stream() + // archetype must be for COMPOSITION + .filter(tc -> StructureRmType.COMPOSITION.getAlias().equals(tc.aliasedRmType())) + .map(AslRmTypeAndConcept::concept) + .toList(); + case OV_TIME_COMMITTED_DV, EHR_TIME_CREATED_DV -> AslUtils.streamStringPrimitives(comparison) + .map(AslUtils::toOffsetDateTime) + .filter(Objects::nonNull) + .toList(); + case AD_CHANGE_TYPE_CODE_STRING -> AslUtils.streamStringPrimitives(comparison) + .map(StringPrimitive::getValue) + .map(ChangeTypeUtils::getJooqChangeTypeByCode) + .filter(Objects::nonNull) + .toList(); + case AD_CHANGE_TYPE_PREFERRED_TERM, AD_CHANGE_TYPE_VALUE -> AslUtils.streamStringPrimitives(comparison) + .map(StringPrimitive::getValue) + .map(v -> "unknown".equals(v) + ? ContributionChangeType.Unknown + : ContributionChangeType.lookupLiteral(v)) + .filter(Objects::nonNull) + .toList(); + case null -> AslUtils.conditionValue(comparison.rightComparisonOperands(), operator, aslField.getType()); + default -> AslUtils.conditionValue(comparison.rightComparisonOperands(), operator, aslField.getType()); + }; + } + + private static Optional buildDvOrderedCondition( + AslDvOrderedColumnField field, ComparisonConditionOperator operator, List values) { + if (operator == ComparisonConditionOperator.EXISTS || operator == ComparisonConditionOperator.LIKE) { + throw new IllegalArgumentException("LIKE/EXISTS on DV_ORDERED is not supported"); + } + List, Set>> typeToValues = + determinePossibleDvOrderedTypesAndValues(field.getDvOrderedTypes(), operator, values); + if (typeToValues.isEmpty()) { + return Optional.of(new AslFalseQueryCondition()); + } + return AslUtils.reduceConditions( + LogicalConditionOperator.OR, + typeToValues.stream() + .map(e -> new AslDvOrderedValueQueryCondition<>( + e.getKey(), field, operator.getAslOperator(), List.copyOf(e.getValue())))); + } + + /** + * + * @param allowedTypes + * @param values + * @return <Set<DvOrdered type>, Set<magnitude value>> + */ + private static List, Set>> determinePossibleDvOrderedTypesAndValues( + Set allowedTypes, ComparisonConditionOperator operator, Collection values) { + // non-numeric DvOrdered cannot be handled together + HashMap> nonNumericDvOrderedTypeToValues = new HashMap<>(); + boolean hasNumericDvOrdered = CollectionUtils.containsAny(allowedTypes, NUMERIC_DV_ORDERED_TYPES); + Set numericValues = new HashSet<>(); + boolean isEqualsOp = + operator == ComparisonConditionOperator.EQ || operator == ComparisonConditionOperator.MATCHES; + for (Primitive value : values) { + if (value instanceof TemporalPrimitive p) { + handleTemporalPrimitiveForDvOrdered( + allowedTypes, p.getTemporal(), isEqualsOp, nonNumericDvOrderedTypeToValues); + } else if (value instanceof StringPrimitive p) { + handleStringPrimitiveForDvOrdered(allowedTypes, p, isEqualsOp, nonNumericDvOrderedTypeToValues); + } else if (value instanceof DoublePrimitive || value instanceof LongPrimitive) { + if (hasNumericDvOrdered) numericValues.add(value.getValue()); + } + } + + List, Set>> result = new ArrayList<>(); + if (!numericValues.isEmpty()) { + Set numericDvOrderedTypes = SetUtils.intersection(allowedTypes, NUMERIC_DV_ORDERED_TYPES); + result.add(Pair.of(numericDvOrderedTypes, numericValues)); + } + nonNumericDvOrderedTypeToValues.entrySet().stream() + .filter(e -> !e.getValue().isEmpty()) + .map(e -> Pair.of(Set.of(e.getKey()), e.getValue())) + .forEach(result::add); + return result; + } + + private static void handleStringPrimitiveForDvOrdered( + Set allowedTypes, StringPrimitive p, boolean isEqualsOp, HashMap> result) { + /* + DATE_TIME/TIME strings with fractional seconds, where the precision is not 10^-3, + or DURATION strings will not be parsed as TemporalPrimitive by the AQL parser. + To avoid confusion we also support those by checking for the possibility and manually parsing them. + */ + String val = p.getValue(); + if (CollectionUtils.containsAny(allowedTypes, DV_DATE, DV_DATE_TIME, DV_TIME)) { + AslUtils.parseDateTimeOrTimeWithHigherPrecision(val) + .ifPresent(t -> handleTemporalPrimitiveForDvOrdered(allowedTypes, t, isEqualsOp, result)); + } + if (allowedTypes.contains(DV_DURATION)) { + Optional.of(val) + .map(v -> { + try { + return new DvDuration(val); + } catch (IllegalArgumentException e) { + // not a duration value -> skip it + return null; + } + }) + .map(DvDuration::getMagnitude) + .ifPresent(m -> addToMultiValuedMap(result, DV_DURATION, m)); + } + } + + private static void handleTemporalPrimitiveForDvOrdered( + Set allowedTypes, TemporalAccessor p, boolean isEqualsOp, HashMap> result) { + boolean hasDate = p.isSupported(ChronoField.YEAR); + boolean hasTime = p.isSupported(ChronoField.HOUR_OF_DAY); + if (hasDate) { + if ((!hasTime || !isEqualsOp) && allowedTypes.contains(DV_DATE)) { + addToMultiValuedMap( + result, DV_DATE, OpenEHRDateTimeSerializationUtils.toMagnitude(new DvDate(LocalDate.from(p)))); + } + if (allowedTypes.contains(DV_DATE_TIME)) { + addToMultiValuedMap( + result, DV_DATE_TIME, OpenEHRDateTimeSerializationUtils.toMagnitude(new DvDateTime(p))); + } + } else if (hasTime && allowedTypes.contains(DV_TIME)) { + addToMultiValuedMap(result, DV_TIME, OpenEHRDateTimeSerializationUtils.toMagnitude(new DvTime(p))); + } + } + + private static void addToMultiValuedMap(Map> map, K key, V value) { + map.computeIfAbsent(key, k -> new LinkedHashSet<>()).add(value); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslFromCreator.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslFromCreator.java new file mode 100644 index 000000000..cf5f36202 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslFromCreator.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.jooq.pg.Tables; +import org.ehrbase.openehr.aqlengine.asl.AslUtils.AliasProvider; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslDescendantCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotNullQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoin; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslEncapsulatingQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsChain; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsSetOperationWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.RmContainsWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.VersionContainsWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.LogicalConditionOperator; +import org.ehrbase.openehr.dbformat.AncestorStructureRmType; +import org.ehrbase.openehr.dbformat.RmTypeAlias; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperatorSymbol; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.jooq.JoinType; + +final class AslFromCreator { + private final AliasProvider aliasProvider; + private final KnowledgeCacheService knowledgeCacheService; + + AslFromCreator(AliasProvider aliasProvider, KnowledgeCacheService knowledgeCacheService) { + this.aliasProvider = aliasProvider; + this.knowledgeCacheService = knowledgeCacheService; + } + + @FunctionalInterface + interface ContainsToOwnerProvider { + OwnerProviderTuple get(ContainsWrapper contains); + } + + public ContainsToOwnerProvider addFromClause(AslRootQuery rootQuery, AqlQueryWrapper queryWrapper) { + + final Map containsToStructureSubQuery = new HashMap<>(); + ContainsChain fromChain = queryWrapper.containsChain(); + addContainsChain(rootQuery, null, fromChain, false, containsToStructureSubQuery); + + // add contains condition to rootQuery + buildContainsCondition(fromChain, false, containsToStructureSubQuery).ifPresent(rootQuery::addConditionAnd); + + return containsToStructureSubQuery::get; + } + + /** + * Determines the AslSourceRelation. + * If it cannot be determined from desc, the parent is consulted. + * This is the case when the structure rm type is not "distinguishing", e.g. for CLUSTER. + * + * @param desc + * @param parent + * @return + */ + private static AslSourceRelation getSourceRelation(RmContainsWrapper desc, AslStructureQuery parent) { + if (RmConstants.EHR.equals(desc.getRmType())) { + return AslSourceRelation.EHR; + } + return Optional.of(desc) + .map(RmContainsWrapper::getStructureRmType) + .map(StructureRmType::getStructureRoot) + .or(() -> AncestorStructureRmType.byTypeName(desc.getRmType()) + .map(AncestorStructureRmType::getStructureRoot)) + .map(AslSourceRelation::get) + .or(() -> Optional.ofNullable(parent).map(AslStructureQuery::getType)) + .orElse(null); + } + + private void addContainsChain( + AslEncapsulatingQuery encapsulatingQuery, + AslStructureQuery lastParent, + ContainsChain containsChain, + boolean useLeftJoin, + Map containsToStructureSubQuery) { + + AslStructureQuery currentParent = lastParent; + for (ContainsWrapper descriptor : containsChain.chain()) { + currentParent = addContainsSubquery( + encapsulatingQuery, useLeftJoin, containsToStructureSubQuery, descriptor, currentParent); + } + + if (containsChain.hasTrailingSetOperation()) { + addContainsChainSetOperator( + encapsulatingQuery, containsChain, useLeftJoin, containsToStructureSubQuery, currentParent); + } + } + + private AslStructureQuery addContainsSubquery( + AslEncapsulatingQuery encapsulatingQuery, + boolean useLeftJoin, + Map containsToStructureSubQuery, + ContainsWrapper descriptor, + AslStructureQuery currentParent) { + + final RmContainsWrapper usedWrapper; + final boolean isOriginalVersion; + switch (descriptor) { + case VersionContainsWrapper vcw -> { + usedWrapper = vcw.child(); + isOriginalVersion = true; + } + case RmContainsWrapper rcw -> { + usedWrapper = rcw; + isOriginalVersion = false; + } + } + + boolean hasEhrParent = currentParent != null && currentParent.getType() == AslSourceRelation.EHR; + + AslSourceRelation sourceRelation = getSourceRelation(usedWrapper, currentParent); + boolean requiresVersionJoin; + if (hasEhrParent || isOriginalVersion) { + requiresVersionJoin = true; + } else if (sourceRelation == AslSourceRelation.EHR || currentParent != null) { + requiresVersionJoin = false; + } else { + // Some paths for structure roots require access to the version table + // (If we knew about the paths, the version join might sometimes be omitted) + requiresVersionJoin = Optional.of(usedWrapper) + .map(RmContainsWrapper::getStructureRmType) + .filter(StructureRmType::isStructureRoot) + .isPresent(); + } + + final AslStructureQuery structureQuery = containsSubquery(usedWrapper, requiresVersionJoin, sourceRelation); + structureQuery.setRepresentsOriginalVersionExpression(isOriginalVersion); + + addContainsSubqueryToContainer(encapsulatingQuery, structureQuery, currentParent, useLeftJoin); + + OwnerProviderTuple ownerProviderTuple = new OwnerProviderTuple(structureQuery, structureQuery); + containsToStructureSubQuery.put(usedWrapper, ownerProviderTuple); + if (isOriginalVersion) { + containsToStructureSubQuery.put(descriptor, ownerProviderTuple); + } + return structureQuery; + } + + private static void addContainsSubqueryToContainer( + AslEncapsulatingQuery container, + AslStructureQuery toAdd, + AslStructureQuery joinParent, + boolean asLeftJoin) { + AslJoin join; + if (joinParent == null || container.getChildren().isEmpty()) { + join = null; + } else { + join = new AslJoin( + joinParent, + asLeftJoin ? JoinType.LEFT_OUTER_JOIN : JoinType.JOIN, + toAdd, + new AslDescendantCondition( + joinParent.getType(), joinParent, joinParent, toAdd.getType(), toAdd, toAdd) + .provideJoinCondition()); + } + container.addChild(toAdd, join); + } + + private void addContainsChainSetOperator( + AslEncapsulatingQuery currentQuery, + ContainsChain containsChain, + boolean asLeftJoin, + Map containsToStructureSubQuery, + AslStructureQuery currentParent) { + ContainsSetOperationWrapper setOperator = containsChain.trailingSetOperation(); + for (ContainsChain operand : setOperator.operands()) { + boolean requiresOrOperandSubQuery = + setOperator.operator() == ContainmentSetOperatorSymbol.OR && operand.size() > 1; + + if (requiresOrOperandSubQuery) { + // OR operands with chaining inside need to be mapped to their own subquery. + // Else the nested contains chain would not be isolated from the parent + // and the outer left join would bleed into it. + AslEncapsulatingQuery orSq = + buildOrOperandAsEncapsulatingQuery(containsToStructureSubQuery, currentParent, operand); + AslStructureQuery child = + (AslStructureQuery) orSq.getChildren().getFirst().getLeft(); + currentQuery.addChild( + orSq, + new AslJoin( + currentParent, + JoinType.LEFT_OUTER_JOIN, + orSq, + new AslDescendantCondition( + currentParent.getType(), + currentParent, + currentParent, + child.getType(), + orSq, + child) + .provideJoinCondition())); + } else { + // AND operands and simple (no chaining inside) OR operands can be joined directly + addContainsChain( + currentQuery, + currentParent, + operand, + asLeftJoin || setOperator.operator() == ContainmentSetOperatorSymbol.OR, + containsToStructureSubQuery); + } + } + } + + private AslEncapsulatingQuery buildOrOperandAsEncapsulatingQuery( + Map containsToStructureSubQuery, + AslStructureQuery currentParent, + ContainsChain operand) { + AslEncapsulatingQuery orSq = new AslEncapsulatingQuery(aliasProvider.uniqueAlias("or_sq")); + HashMap subQueryMap = new HashMap<>(); + + addContainsChain(orSq, currentParent, operand, false, subQueryMap); + + // add contains condition to orSq + buildContainsCondition(operand, false, subQueryMap).ifPresent(orSq::addStructureCondition); + + // provider must be orSq + subQueryMap.forEach((k, v) -> containsToStructureSubQuery.put(k, new OwnerProviderTuple(v.owner(), orSq))); + + return orSq; + } + + private AslStructureQuery containsSubquery( + RmContainsWrapper containsWrapper, boolean requiresVersionJoin, AslSourceRelation sourceRelation) { + // e.g. "sCO_c_1" + String rmType = containsWrapper.getRmType(); + final String sAlias = aliasProvider.uniqueAlias("s" + + RmTypeAlias.optionalAlias(rmType).orElse(rmType) + + Optional.of(containsWrapper) + .map(ContainsWrapper::alias) + .map(a -> "_" + a) + .orElse("")); + + final List rmTypes; + boolean isRoot; + if (RmConstants.EHR.equals(rmType)) { + rmTypes = List.of(RmConstants.EHR); + isRoot = false; + } else { + // We only support structure types therefore we can ignore all non-structure descendants + rmTypes = AncestorStructureRmType.byTypeName(rmType) + .map(AncestorStructureRmType::getDescendants) + .map(s -> s.stream().distinct().map(StructureRmType::name).toList()) + .orElseGet( + () -> List.of(containsWrapper.getStructureRmType().name())); + + // Folder may be root, but is recursive + isRoot = RmConstants.COMPOSITION.equals(rmType) || RmConstants.EHR_STATUS.equals(rmType); + } + final List fields = fieldsForContainsSubquery(containsWrapper, requiresVersionJoin, sourceRelation); + + AslStructureQuery aslStructureQuery = new AslStructureQuery( + sAlias, sourceRelation, fields, rmTypes, isRoot ? List.of() : rmTypes, null, requiresVersionJoin); + AslUtils.predicates( + containsWrapper.getPredicate(), + c -> AslUtils.structurePredicateCondition( + c, aslStructureQuery, knowledgeCacheService::findUuidByTemplateId)) + .ifPresent(aslStructureQuery::addConditionAnd); + if (isRoot) { + aslStructureQuery.addConditionAnd(new AslFieldValueQueryCondition<>( + AslUtils.findFieldForOwner( + AslStructureColumn.NUM, aslStructureQuery.getSelect(), aslStructureQuery), + AslConditionOperator.EQ, + List.of(0))); + } + + return aslStructureQuery; + } + + @Nonnull + private static List fieldsForContainsSubquery( + RmContainsWrapper nextDesc, boolean requiresVersionJoin, AslSourceRelation sourceRelation) { + final List fields = new ArrayList<>(); + if (RmConstants.EHR.equals(nextDesc.getRmType())) { + fields.add(new AslColumnField(UUID.class, "id", null, false, AslExtractedColumn.EHR_ID)); + fields.add(new AslColumnField(OffsetDateTime.class, "creation_date", null, false, null)); + } else { + Arrays.stream(AslStructureColumn.values()) + .filter(c -> requiresVersionJoin + || c.isFromDataTable() + // Support for non-vo_id PKs + || sourceRelation.getPkeyFields().stream() + .anyMatch(f -> f.getName().equals(c.getFieldName()))) + // remove fields not supported by the relation + .filter(c -> Optional.of(c) + .map(f -> (requiresVersionJoin && c.isFromVersionTable()) + ? sourceRelation.getVersionTable() + : sourceRelation.getDataTable()) + .map(t -> t.field(c.getFieldName())) + .isPresent()) + .map(AslStructureColumn::field) + .forEach(fields::add); + + // (Only) for Compositions version.root_concept mirrors the data.entity_concept of the COMPOSITION row + if (requiresVersionJoin && RmConstants.COMPOSITION.equals(nextDesc.getRmType())) { + fields.add(new AslColumnField( + String.class, + Tables.COMP_VERSION.ROOT_CONCEPT.getName(), + null, + true, + AslExtractedColumn.ROOT_CONCEPT)); + } + } + return fields; + } + + private static Optional buildContainsCondition( + ContainsChain chainDescriptor, + final boolean chainIsBelowOr, + Map containsToStructureSubQuery) { + if (!chainIsBelowOr && !chainDescriptor.hasTrailingSetOperation()) { + return Optional.empty(); + } + + List conditions = new ArrayList<>(); + if (chainIsBelowOr) { + chainDescriptor.chain().stream() + .map(containsToStructureSubQuery::get) + .map(OwnerProviderTuple::provider) + // The first field in structure sub-queries should always be the id + .map(t -> new AslNotNullQueryCondition(t.getSelect().getFirst())) + .forEach(conditions::add); + } + + if (chainDescriptor.hasTrailingSetOperation()) { + containsConditionForSetOperator(chainDescriptor, chainIsBelowOr, containsToStructureSubQuery) + .forEach(conditions::add); + } + // merge as AND + return Optional.of(conditions).map(List::stream).map(AslUtils::and); + } + + private static Stream containsConditionForSetOperator( + ContainsChain chainDescriptor, + boolean chainIsBelowOr, + Map containsToStructureSubQuery) { + ContainsSetOperationWrapper setOperator = chainDescriptor.trailingSetOperation(); + boolean isOrOperator = setOperator.operator() == ContainmentSetOperatorSymbol.OR; + + Stream operatorConditions = setOperator.operands().stream() + .map(operand -> { + if (isOrOperator && operand.size() > 1) { + OwnerProviderTuple subQuery = + containsToStructureSubQuery.get(operand.chain().getFirst()); + return new AslNotNullQueryCondition(AslUtils.findFieldForOwner( + AslStructureColumn.VO_ID, subQuery.provider().getSelect(), subQuery.owner())); + } else { + return buildContainsCondition( + operand, chainIsBelowOr || isOrOperator, containsToStructureSubQuery) + .orElse(null); + } + }) + .filter(Objects::nonNull); + + return isOrOperator + ? AslUtils.reduceConditions(LogicalConditionOperator.OR, operatorConditions).stream() + : operatorConditions; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslPathCreator.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslPathCreator.java new file mode 100644 index 000000000..0dd323660 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslPathCreator.java @@ -0,0 +1,737 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import static org.ehrbase.jooq.pg.Tables.AUDIT_DETAILS; +import static org.ehrbase.openehr.aqlengine.asl.AslUtils.streamConditionDescriptors; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.openehr.aqlengine.asl.AslUtils.AliasProvider; +import org.ehrbase.openehr.aqlengine.asl.DataNodeInfo.ExtractedColumnDataNodeInfo; +import org.ehrbase.openehr.aqlengine.asl.DataNodeInfo.JsonRmDataNodeInfo; +import org.ehrbase.openehr.aqlengine.asl.DataNodeInfo.StructureRmDataNodeInfo; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept; +import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotNullQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslPathChildCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslTrueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslComplexExtractedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslConstantField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField.FieldSource; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslSubqueryField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslAuditDetailsJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoin; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslEncapsulatingQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslFilteringQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslPathDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRmObjectDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; +import org.ehrbase.openehr.aqlengine.pathanalysis.ANode.NodeCategory; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathCohesionAnalysis.PathCohesionTreeNode; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathInfo; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathInfo.JoinMode; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper.SelectType; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ComparisonOperatorConditionWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.LogicalConditionOperator; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPathUtil; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.util.AqlUtil; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.jooq.JSONB; +import org.jooq.JoinType; +import org.springframework.util.function.SingletonSupplier; + +final class AslPathCreator { + + private final AliasProvider aliasProvider; + private final KnowledgeCacheService knowledgeCacheService; + private final String systemId; + + @FunctionalInterface + interface PathToField { + AslField getField(IdentifiedPath path); + } + + AslPathCreator(AliasProvider aliasProvider, KnowledgeCacheService knowledgeCacheService, String systemId) { + this.aliasProvider = aliasProvider; + this.knowledgeCacheService = knowledgeCacheService; + this.systemId = systemId; + } + + @Nonnull + public PathToField addPathQueries( + AqlQueryWrapper query, + AslFromCreator.ContainsToOwnerProvider containsToStructureSubQuery, + AslRootQuery rootQuery) { + Map pathToField = new LinkedHashMap<>(); + + addEhrFields(query, containsToStructureSubQuery, pathToField); + + List dataNodeInfos = new ArrayList<>(); + + query.pathInfos().forEach((contains, pathInfo) -> { + if (RmConstants.EHR.equals(contains.getRmType())) { + throw new IllegalArgumentException("Only paths within [EHR_STATUS,COMPOSITION,CLUSTER] are supported"); + } + + OwnerProviderTuple parent = containsToStructureSubQuery.get(contains); + AslSourceRelation sourceRelation = ((AslStructureQuery) parent.owner()).getType(); + + joinPathStructureNode( + rootQuery, + parent, + null, + sourceRelation, + pathInfo.getCohesionTreeRoot(), + pathInfo, + parent.provider(), + -1) + .forEach(dataNodeInfos::add); + }); + + addQueriesForDataNode(dataNodeInfos.stream(), rootQuery, null, pathToField); + + return pathToField::get; + } + + private void addEhrFields( + AqlQueryWrapper query, + AslFromCreator.ContainsToOwnerProvider containsToStructureSubQuery, + Map pathToField) { + Stream.of( + // select + query.nonPrimitiveSelects() + // We want to skip COUNT(*) since it does not have a path + .filter(sd -> sd.type() != SelectType.AGGREGATE_FUNCTION + || sd.getIdentifiedPath().isPresent()) + .map(s -> + Pair.of(s.root(), s.getIdentifiedPath().orElse(null))), + // where + streamConditionDescriptors(query.where()) + .map(ComparisonOperatorConditionWrapper::leftComparisonOperand) + .map(s -> Pair.of(s.root(), s.path())), + // order by + query.orderBy().stream().map(s -> Pair.of(s.root(), s.identifiedPath()))) + .flatMap(s -> s) + .filter(p -> RmConstants.EHR.equals(p.getLeft().getRmType())) + .distinct() + .forEach(p -> { + ContainsWrapper contains = p.getLeft(); + AslExtractedColumn ec = AslExtractedColumn.find( + contains, p.getRight().getPath()) + .orElseThrow(); + AslQuery ehrSubquery = + containsToStructureSubQuery.get(contains).owner(); + AslField field; + if (ec == AslExtractedColumn.EHR_SYSTEM_ID_DV || ec == AslExtractedColumn.EHR_SYSTEM_ID) { + field = new AslConstantField<>( + String.class, systemId, new FieldSource(ehrSubquery, ehrSubquery, ehrSubquery), ec); + } else { + field = findExtractedColumnField(ec, new FieldSource(ehrSubquery, ehrSubquery, ehrSubquery)); + } + pathToField.put(p.getRight(), field); + }); + } + + private void addQueriesForDataNode( + Stream dataNodeInfos, + AslRootQuery rootQuery, + AslPathDataQuery parentPathDataQuery, + Map pathToField) { + dataNodeInfos.forEach(dni -> { + switch (dni) { + case ExtractedColumnDataNodeInfo ecDni -> addExtractedColumns(rootQuery, ecDni, pathToField); + case JsonRmDataNodeInfo jrdDni -> addPathDataQuery(jrdDni, rootQuery, parentPathDataQuery, pathToField); + case StructureRmDataNodeInfo srdDni -> addRmObjectData(srdDni, rootQuery, pathToField); + } + dni.node().getPathsEndingAtNode().forEach(ip -> addFilterQueryIfRequired(dni, ip, rootQuery, pathToField)); + }); + } + + private void addPathDataQuery( + JsonRmDataNodeInfo dni, + AslRootQuery rootQuery, + AslPathDataQuery parentPathDataQuery, + Map pathToField) { + boolean hasPathQueryParent = parentPathDataQuery != null; + boolean splitMultipleValued = dni.multipleValued() && !hasPathQueryParent; + final AslQuery base = hasPathQueryParent + ? parentPathDataQuery + : (AslStructureQuery) dni.parent().owner(); + final AslQuery provider = hasPathQueryParent ? parentPathDataQuery : dni.providerSubQuery(); + + final AslPathDataQuery dataQuery; + String alias = aliasProvider.uniqueAlias("pd"); + if (splitMultipleValued) { + AslPathDataQuery arrayQuery = new AslPathDataQuery( + alias + "_array", base, provider, dni.pathInJson(), false, dni.dvOrderedTypes(), JSONB.class); + rootQuery.addChild(arrayQuery, new AslJoin(provider, JoinType.LEFT_OUTER_JOIN, arrayQuery)); + + dataQuery = new AslPathDataQuery( + alias, arrayQuery, arrayQuery, List.of(), true, dni.dvOrderedTypes(), dni.type()); + rootQuery.addChild(dataQuery, new AslJoin(arrayQuery, JoinType.LEFT_OUTER_JOIN, dataQuery)); + } else { + dataQuery = new AslPathDataQuery( + alias, base, provider, dni.pathInJson(), dni.multipleValued(), dni.dvOrderedTypes(), dni.type()); + rootQuery.addChild(dataQuery, new AslJoin(provider, JoinType.LEFT_OUTER_JOIN, dataQuery)); + } + dni.node() + .getPathsEndingAtNode() + .forEach(path -> pathToField.put(path, dataQuery.getSelect().getFirst())); + + addQueriesForDataNode(dni.dependentPathDataNodes(), rootQuery, dataQuery, pathToField); + } + + private void addFilterQueryIfRequired( + DataNodeInfo dni, + IdentifiedPath identifiedPath, + AslRootQuery rootQuery, + Map pathToField) { + List filterConditions = Stream.concat( + rootQuery.getChildren().stream() + .filter(jp -> jp.getLeft() == dni.providerSubQuery()) + .map(Pair::getRight) + .filter(Objects::nonNull) + .map(AslJoin::getLeft), + Stream.of(dni.providerSubQuery())) + .map(AslQuery::joinConditionsForFiltering) + .map(m -> m.getOrDefault(identifiedPath, Collections.emptyList())) + .flatMap(List::stream) + .filter(jc -> !(jc.getCondition() instanceof AslTrueQueryCondition)) + .map(jc -> jc.withLeftProvider(rootQuery)) + .map(AslJoinCondition.class::cast) + .toList(); + if (!filterConditions.isEmpty()) { + AslField sourceField = pathToField.get(identifiedPath); + + if (sourceField instanceof AslSubqueryField sf) { + AslSubqueryField filtered = sf.withFilterConditions(filterConditions); + pathToField.replace(identifiedPath, filtered); + + } else { + AslFilteringQuery filteringQuery = new AslFilteringQuery( + aliasProvider.uniqueAlias(sourceField.getOwner().getAlias() + "_f"), sourceField); + rootQuery.addChild( + filteringQuery, + new AslJoin( + sourceField.getInternalProvider(), + JoinType.LEFT_OUTER_JOIN, + filteringQuery, + filterConditions)); + pathToField.replace(identifiedPath, filteringQuery.getSelect().getFirst()); + } + } + } + + private void addRmObjectData( + StructureRmDataNodeInfo dni, AslRootQuery rootQuery, Map pathToField) { + + AslStructureQuery base = (AslStructureQuery) dni.parent().owner(); + AslQuery provider = dni.providerSubQuery(); + AslRmObjectDataQuery dataQuery = new AslRmObjectDataQuery(aliasProvider.uniqueAlias("pd"), base, provider); + + AslSubqueryField field = AslSubqueryField.createAslSubqueryField(JSONB.class, dataQuery); + + dni.node().getPathsEndingAtNode().forEach(path -> pathToField.put(path, field)); + } + + private void addExtractedColumns( + AslRootQuery root, ExtractedColumnDataNodeInfo dni, Map pathToField) { + final FieldSource fieldSource = new FieldSource(dni.parent().owner(), dni.providerSubQuery(), root); + AslField field = createExtractedColumnField(dni.extractedColumn(), fieldSource); + dni.node().getPathsEndingAtNode().forEach(path -> pathToField.put(path, field)); + } + + private AslField createExtractedColumnField(AslExtractedColumn ec, FieldSource fieldSource) { + return switch (ec) { + case NAME_VALUE, + TEMPLATE_ID, + EHR_ID, + ROOT_CONCEPT, + OV_CONTRIBUTION_ID, + OV_TIME_COMMITTED, + OV_TIME_COMMITTED_DV, + AD_CHANGE_TYPE_PREFERRED_TERM, + AD_CHANGE_TYPE_CODE_STRING, + AD_CHANGE_TYPE_VALUE, + AD_CHANGE_TYPE_DV, + AD_DESCRIPTION_VALUE, + AD_DESCRIPTION_DV, + EHR_TIME_CREATED, + EHR_TIME_CREATED_DV -> findExtractedColumnField(ec, fieldSource); + case AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE -> new AslConstantField<>( + String.class, "openehr", fieldSource, ec); + case AD_SYSTEM_ID, EHR_SYSTEM_ID, EHR_SYSTEM_ID_DV -> new AslConstantField<>( + String.class, systemId, fieldSource, ec); + case VO_ID, ARCHETYPE_NODE_ID -> new AslComplexExtractedColumnField(ec, fieldSource); + }; + } + + @Nonnull + private static AslColumnField findExtractedColumnField(AslExtractedColumn ec, FieldSource fieldSource) { + AslColumnField field = AslUtils.findFieldForOwner( + ec.getColumns().getFirst(), + fieldSource.internalProvider().getSelect(), + fieldSource.owner()) + .withProvider(fieldSource.provider()); + if (field.getExtractedColumn() == null) { + /* + Some extracted columns refer to fields representing multiple extracted columns. + The field is copied, so the field represents exactly one extracted column. + */ + field = new AslColumnField( + field.getType(), + field.getColumnName(), + new FieldSource(field.getOwner(), field.getInternalProvider(), field.getProvider()), + field.isVersionTableField(), + ec); + } + return field; + } + + private Stream joinPathStructureNode( + AslEncapsulatingQuery query, + OwnerProviderTuple parent, + JoinMode parentJoinMode, + AslSourceRelation sourceRelation, + PathCohesionTreeNode currentNode, + PathInfo pathInfo, + AslQuery rootProviderQuery, + final int structureLevel) { + + final OwnerProviderTuple subQuery; + final AslEncapsulatingQuery currentQuery; + final JoinMode joinMode = pathInfo.joinMode(currentNode); + if (joinMode == JoinMode.ROOT) { + subQuery = parent; + currentQuery = query; + } else { + + AslStructureQuery sq = pathStructureSubQuery( + currentNode.getAttribute().getAttribute(), + currentNode.getAttribute().getPredicateOrOperands(), + sourceRelation, + pathInfo.getTargetTypes(currentNode)); + subQuery = new OwnerProviderTuple(sq, sq); + + if (parentJoinMode == JoinMode.INTERNAL_SINGLE_CHILD) { + currentQuery = addInternalPathNode(query, parent, sourceRelation, sq, currentNode); + } else { + currentQuery = addEncapsulatingQueryWithPathNode( + query, parent, parentJoinMode, sourceRelation, sq, currentNode); + if (parentJoinMode == JoinMode.ROOT) { + rootProviderQuery = currentQuery; + } + } + } + + if (subQuery.owner() instanceof AslStructureQuery sq) { + addFiltersToPathNodeSubquery(currentNode, structureLevel, sq); + } + + final AslQuery finalRootProviderSubQuery = rootProviderQuery; + Stream dataNodeInfoStream = currentNode.getChildren().stream() + .flatMap(child -> handlePathStructureNodeChild( + sourceRelation, + pathInfo, + structureLevel, + child, + subQuery, + currentQuery, + finalRootProviderSubQuery, + joinMode)); + + if ((joinMode == JoinMode.ROOT || joinMode == JoinMode.DATA) + // this node only returns an RM object, if there is actually a path ending here + && !currentNode.getPathsEndingAtNode().isEmpty()) { + return Stream.of( + dataNodeInfoStream, + Stream.of(new StructureRmDataNodeInfo( + currentNode, subQuery, currentQuery, rootProviderQuery))) + .flatMap(s -> s); + } else { + return dataNodeInfoStream; + } + } + + private Stream handlePathStructureNodeChild( + AslSourceRelation sourceRelation, + PathInfo pathInfo, + int structureLevel, + PathCohesionTreeNode child, + OwnerProviderTuple subQuery, + AslEncapsulatingQuery currentQuery, + AslQuery rootProvider, + JoinMode joinMode) { + if (subQuery.owner() instanceof AslStructureQuery sq + && sq.isRepresentsOriginalVersionExpression() + && pathInfo.getTargetTypes(child).stream().anyMatch(RmConstants.AUDIT_DETAILS::equals)) { + // VERSION.commit_audit + return joinAuditDetailsPaths(currentQuery, subQuery, child, rootProvider); + } + + NodeCategory nodeCategory = pathInfo.getNodeCategory(child); + return switch (nodeCategory) { + case STRUCTURE -> joinPathStructureNode( + currentQuery, + subQuery, + joinMode, + sourceRelation, + child, + pathInfo, + rootProvider, + structureLevel + 1); + case STRUCTURE_INTERMEDIATE, FOUNDATION_EXTENDED -> throw new IllegalArgumentException(); + case RM_TYPE -> joinRmTypeNode(child, currentQuery, subQuery, rootProvider, pathInfo, 1); + case FOUNDATION -> joinFoundationNode(child, currentQuery, subQuery, rootProvider, pathInfo, 1); + }; + } + + @Nonnull + private AslEncapsulatingQuery addEncapsulatingQueryWithPathNode( + AslEncapsulatingQuery query, + OwnerProviderTuple parent, + JoinMode parentJoinMode, + AslSourceRelation sourceRelation, + AslStructureQuery sq, + PathCohesionTreeNode currentNode) { + final AslEncapsulatingQuery currentQuery = new AslEncapsulatingQuery(aliasProvider.uniqueAlias("p_eq")); + currentQuery.addChild(sq, null); + + AslQuery parentProvider = parentJoinMode == JoinMode.ROOT ? parent.provider() : parent.owner(); + AslJoinCondition[] joinConditions = Stream.concat( + Stream.of(new AslPathChildCondition( + sourceRelation, + parentProvider, + parent.owner(), + sourceRelation, + currentQuery, + sq) + .provideJoinCondition()), + parentFiltersAsJoinCondition(parent, currentNode).stream()) + .toArray(AslJoinCondition[]::new); + query.addChild( + currentQuery, new AslJoin(parent.provider(), JoinType.LEFT_OUTER_JOIN, currentQuery, joinConditions)); + + if (parentJoinMode == JoinMode.INTERNAL_FORK) { + query.addConditionOr(new AslNotNullQueryCondition( + AslUtils.findFieldForOwner(AslStructureColumn.VO_ID, currentQuery.getSelect(), sq))); + } + return currentQuery; + } + + @Nonnull + private static AslEncapsulatingQuery addInternalPathNode( + AslEncapsulatingQuery query, + OwnerProviderTuple parent, + AslSourceRelation sourceRelation, + AslStructureQuery nodeSubquery, + PathCohesionTreeNode currentNode) { + List childNodeJoinConditions = new ArrayList<>(); + parentFiltersAsJoinCondition(parent, currentNode).ifPresent(childNodeJoinConditions::add); + childNodeJoinConditions.add(new AslPathChildCondition( + sourceRelation, parent.provider(), parent.owner(), sourceRelation, nodeSubquery, nodeSubquery) + .provideJoinCondition()); + query.addChild( + nodeSubquery, new AslJoin(parent.provider(), JoinType.JOIN, nodeSubquery, childNodeJoinConditions)); + return query; + } + + private void addFiltersToPathNodeSubquery( + PathCohesionTreeNode currentNode, int structureLevel, AslStructureQuery sq) { + List condition1 = currentNode.getAttribute().getPredicateOrOperands(); + long attributePredicateCount = AqlUtil.streamPredicates(condition1).count(); + List>> allPathPredicates = currentNode.getPaths().stream() + .map(ip -> Pair.of( + ip, + structureLevel < 0 + ? ListUtils.emptyIfNull(ip.getRootPredicate()) + : ip.getPath() + .getPathNodes() + .get(structureLevel) + .getPredicateOrOperands())) + .toList(); + + if (allPathPredicates.stream() + .map(Pair::getRight) + .map(AqlUtil::streamPredicates) + .map(Stream::count) + .anyMatch(c -> attributePredicateCount != c)) { + allPathPredicates.forEach(p -> sq.addJoinConditionForFiltering( + p.getKey(), + AslUtils.predicates( + p.getRight(), + cp -> AslUtils.structurePredicateCondition( + cp, sq, knowledgeCacheService::findUuidByTemplateId)) + .orElse(new AslTrueQueryCondition()))); + } + } + + private static Optional parentFiltersAsJoinCondition( + OwnerProviderTuple parent, PathCohesionTreeNode currentNode) { + Map> filterConditions = + parent.owner().joinConditionsForFiltering(); + if (filterConditions.isEmpty()) { + return Optional.empty(); + } + + return AslUtils.reduceConditions( + LogicalConditionOperator.OR, + filterConditions.entrySet().stream() + .filter(e -> currentNode.getPaths().contains(e.getKey())) + .map(Entry::getValue) + .map(Collection::stream) + .map(jc -> jc.map(AslPathFilterJoinCondition::getCondition)) + .map(AslUtils::and) + .filter(Objects::nonNull)) + .filter(condition -> !(condition instanceof AslTrueQueryCondition)) + .map(condition -> new AslPathFilterJoinCondition(parent.owner(), condition)); + } + + private Stream joinAuditDetailsPaths( + AslEncapsulatingQuery currentQuery, + OwnerProviderTuple parent, + PathCohesionTreeNode currentNode, + AslQuery rootProviderSubQuery) { + Supplier auditDetailsParent = + SingletonSupplier.of(() -> addAuditDetailsSubQuery(currentQuery, parent)); + + Map pathToNode = streamCohesionTreeNodes(currentNode) + .flatMap(n -> n.getPathsEndingAtNode().stream().map(p -> Pair.of(p, n))) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + return currentNode.getPaths().stream() + .map(ip -> Pair.of( + ip, + AslExtractedColumn.find(RmConstants.ORIGINAL_VERSION, ip.getPath()) + // VERSION.commit_audit + .or(() -> AslExtractedColumn.find(RmConstants.AUDIT_DETAILS, ip.getPath(), 1)) + .orElseThrow())) + .map(p -> { + boolean isAuditDetailsColumn = + p.getRight().getAllowedRmTypes().contains(RmConstants.AUDIT_DETAILS); + return new ExtractedColumnDataNodeInfo( + pathToNode.get(p.getLeft()), + isAuditDetailsColumn ? auditDetailsParent.get() : parent, + isAuditDetailsColumn ? auditDetailsParent.get().owner() : rootProviderSubQuery, + p.getRight()); + }); + } + + private OwnerProviderTuple addAuditDetailsSubQuery(AslEncapsulatingQuery currentQuery, OwnerProviderTuple parent) { + List fields = Stream.of(AUDIT_DETAILS.ID, AUDIT_DETAILS.DESCRIPTION, AUDIT_DETAILS.CHANGE_TYPE) + .map(f -> (AslField) new AslColumnField(f.getType(), f.getName(), null, false, null)) + .toList(); + AslStructureQuery auditDetailsQuery = new AslStructureQuery( + aliasProvider.uniqueAlias("p_ca"), + AslSourceRelation.AUDIT_DETAILS, + fields, + Set.of(RmConstants.AUDIT_DETAILS), + Set.of(RmConstants.AUDIT_DETAILS), + null, + false); + + currentQuery.addChild( + auditDetailsQuery, + new AslJoin( + parent.provider(), + JoinType.JOIN, + auditDetailsQuery, + new AslAuditDetailsJoinCondition(parent.owner(), auditDetailsQuery))); + return new OwnerProviderTuple(auditDetailsQuery, auditDetailsQuery); + } + + private static Stream streamCohesionTreeNodes(PathCohesionTreeNode node) { + return Stream.of(Stream.of(node), node.getChildren().stream().flatMap(AslPathCreator::streamCohesionTreeNodes)) + .flatMap(Function.identity()); + } + + private static Stream streamJsonRmDataNodes( + PathCohesionTreeNode currentNode, + OwnerProviderTuple subQuery, + AslEncapsulatingQuery query, + AslQuery rootProviderSubQuery, + PathInfo pathInfo, + Stream dependentNodes, + int levelInJson) { + + boolean multipleValued = pathInfo.isMultipleValued(currentNode); + boolean pathsEndingAtNode = !currentNode.getPathsEndingAtNode().isEmpty(); + + if (!pathsEndingAtNode && !multipleValued) { + return Stream.empty(); + } + + List pathToNode = pathInfo.getPathToNode(currentNode); + Class fieldType = Set.of("STRING").equals(pathInfo.getTargetTypes(currentNode)) ? String.class : JSONB.class; + + return Stream.of(new JsonRmDataNodeInfo( + currentNode, + subQuery, + query, + rootProviderSubQuery, + pathToNode.subList(pathToNode.size() - levelInJson, pathToNode.size()), + pathInfo.isMultipleValued(currentNode), + dependentNodes, + pathInfo.getDvOrderedTypes(currentNode), + fieldType)); + } + + private static Stream joinRmTypeNode( + PathCohesionTreeNode currentNode, + AslEncapsulatingQuery query, + OwnerProviderTuple parentStructureQuery, + AslQuery rootProviderQuery, + PathInfo pathInfo, + int levelInJson) { + + boolean multipleValued = pathInfo.isMultipleValued(currentNode); + int nextLevelInJson = multipleValued ? 1 : (levelInJson + 1); + OwnerProviderTuple parent = multipleValued ? null : parentStructureQuery; + Stream childNodes = currentNode.getChildren().stream().flatMap(child -> { + NodeCategory nodeCategory = pathInfo.getNodeCategory(child); + return switch (nodeCategory) { + case STRUCTURE, STRUCTURE_INTERMEDIATE -> throw new IllegalArgumentException(); + case RM_TYPE, FOUNDATION_EXTENDED -> joinRmTypeNode( + child, query, parent, rootProviderQuery, pathInfo, nextLevelInJson); + case FOUNDATION -> joinFoundationNode( + child, query, parent, rootProviderQuery, pathInfo, nextLevelInJson); + }; + }); + + return Stream.of( + streamJsonRmDataNodes( + currentNode, + parentStructureQuery, + query, + rootProviderQuery, + pathInfo, + multipleValued ? childNodes : Stream.empty(), + levelInJson), + multipleValued ? Stream.empty() : childNodes) + .flatMap(s -> s); + } + + private static Stream joinFoundationNode( + PathCohesionTreeNode currentNode, + AslEncapsulatingQuery query, + OwnerProviderTuple parentStructureQuery, + AslQuery rootProviderQuery, + PathInfo pathInfo, + int levelInJson) { + AslQuery parent = Optional.ofNullable(parentStructureQuery) + .map(OwnerProviderTuple::owner) + .orElse(null); + Optional extractedColumnInfo = (parent instanceof AslStructureQuery sq) + ? Stream.of(AslExtractedColumn.values()) + .filter(ec -> ec.getAllowedRmTypes().stream() + .anyMatch(t -> sq.getRmTypes().contains(t) + || (sq.isRepresentsOriginalVersionExpression() + && RmConstants.ORIGINAL_VERSION.equals(t)))) + .filter(ec -> levelInJson == ec.getPath().getPathNodes().size()) + .filter(ec -> currentNode.getPaths().stream() + .allMatch(p -> p.getPath().endsWith(ec.getPath()))) + .findFirst() + .map(ec -> new ExtractedColumnDataNodeInfo( + currentNode, parentStructureQuery, rootProviderQuery, ec)) + : Optional.empty(); + + if (extractedColumnInfo.isPresent()) { + return extractedColumnInfo.stream(); + } else { + return streamJsonRmDataNodes( + currentNode, parentStructureQuery, query, rootProviderQuery, pathInfo, Stream.empty(), levelInJson); + } + } + + private AslStructureQuery pathStructureSubQuery( + String attribute, + List attributePredicates, + AslSourceRelation sourceRelation, + Collection rmTypes) { + + final List fields = Arrays.stream(AslStructureColumn.values()) + // remove fields not supported by the relation + .filter(c -> sourceRelation.getDataTable().field(c.getFieldName()) != null) + .map(AslStructureColumn::field) + .collect(Collectors.toList()); + fields.add(new AslColumnField(String.class, AslStructureQuery.ENTITY_ATTRIBUTE, false)); + + final String sqAlias = aliasProvider.uniqueAlias("p_" + attribute + "_"); + AslStructureQuery aslStructureQuery = + new AslStructureQuery(sqAlias, sourceRelation, fields, rmTypes, List.of(), attribute, false); + + AslUtils.predicates(attributePredicates, cp -> pathStructurePredicateCondition(cp, aslStructureQuery)) + .ifPresent(aslStructureQuery::addConditionAnd); + + return aslStructureQuery; + } + + @Nonnull + private static AslFieldValueQueryCondition pathStructurePredicateCondition( + ComparisonOperatorPredicate cp, AslStructureQuery aslStructureQuery) { + String value = ((StringPrimitive) cp.getValue()).getValue(); + if (AqlObjectPathUtil.ARCHETYPE_NODE_ID.equals(cp.getPath())) { + return new AslFieldValueQueryCondition<>( + AslComplexExtractedColumnField.archetypeNodeIdField(FieldSource.withOwner(aslStructureQuery)), + AslConditionOperator.EQ, + List.of(AslRmTypeAndConcept.fromArchetypeNodeId(value))); + } else if (AqlObjectPathUtil.NAME_VALUE.equals(cp.getPath())) { + return new AslFieldValueQueryCondition<>( + AslUtils.findFieldForOwner( + AslStructureColumn.ENTITY_NAME, aslStructureQuery.getSelect(), aslStructureQuery), + AslConditionOperator.EQ, + List.of(value)); + } else { + throw new IllegalArgumentException("Unexpected attribute predicate path: %s".formatted(cp.getPath())); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslUtils.java new file mode 100644 index 000000000..1e8ec5397 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslUtils.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.apache.commons.collections4.CollectionUtils; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept; +import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslAndQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFalseQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotNullQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslOrQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslProvidesJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslTrueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslComplexExtractedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField.FieldSource; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ComparisonOperatorConditionWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.ComparisonConditionOperator; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.LogicalConditionOperator; +import org.ehrbase.openehr.aqlengine.querywrapper.where.LogicalOperatorConditionWrapper; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.TemporalPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; +import org.ehrbase.openehr.sdk.util.OpenEHRDateTimeParseUtils; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.jooq.JSONB; + +public final class AslUtils { + + static final class AliasProvider { + private final Map aliasCounters = new HashMap<>(); + + public String uniqueAlias(String alias) { + return alias + "_" + aliasCounters.compute(alias, (k, v) -> v == null ? 0 : v + 1); + } + } + + private AslUtils() {} + + public static Stream streamConditionFields(AslQueryCondition condition) { + return switch (condition) { + case AslAndQueryCondition c -> c.getOperands().stream().flatMap(AslUtils::streamConditionFields); + case AslOrQueryCondition c -> c.getOperands().stream().flatMap(AslUtils::streamConditionFields); + case AslNotQueryCondition c -> streamConditionFields(c.getCondition()); + case AslNotNullQueryCondition c -> Stream.of(c.getField()); + case AslFieldValueQueryCondition c -> Stream.of(c.getField()); + case AslFalseQueryCondition __ -> Stream.empty(); + case AslTrueQueryCondition __ -> Stream.empty(); + case AslProvidesJoinCondition __ -> throw new IllegalArgumentException(); + }; + } + + public static Stream streamConditionDescriptors(ConditionWrapper condition) { + if (condition == null) { + return Stream.empty(); + } else if (condition instanceof ComparisonOperatorConditionWrapper cd) { + return Stream.of(cd); + } else if (condition instanceof LogicalOperatorConditionWrapper ld) { + return ld.logicalOperands().stream().flatMap(AslUtils::streamConditionDescriptors); + } else { + throw new IllegalArgumentException("Unsupported type: " + condition); + } + } + + public static String translateAqlLikePatternToSql(String aqlLike) { + StringBuilder sb = new StringBuilder(aqlLike.length()); + + for (int pos = 0, l = aqlLike.length(); pos < l; pos++) { + char c = aqlLike.charAt(pos); + switch (c) { + // sql reserved + case '%', '_' -> sb.append('\\').append(c); + // escape char + case '\\' -> { + pos++; + if (pos >= l) { + throw new IllegalArgumentException("Invalid LIKE pattern: %s".formatted(aqlLike)); + } + + char next = aqlLike.charAt(pos); + switch (next) { + case '*', '?' -> sb.append(next); + case '\\' -> sb.append("\\\\"); + default -> throw new IllegalArgumentException("Invalid LIKE pattern: %s".formatted(aqlLike)); + } + } + // replace by sql + case '?' -> sb.append('_'); + case '*' -> sb.append('%'); + default -> sb.append(c); + } + } + return sb.toString(); + } + + public static OffsetDateTime toOffsetDateTime(StringPrimitive sp) { + final TemporalAccessor temporal; + if (sp instanceof TemporalPrimitive tp) { + temporal = tp.getTemporal(); + } else { + temporal = parseDateTimeOrTimeWithHigherPrecision(sp.getValue()).orElse(null); + } + if (temporal == null) { + return null; + } else if (temporal instanceof OffsetDateTime odt) { + return odt; + } else if (!temporal.isSupported(ChronoField.YEAR)) { + return null; + } + + boolean hasTime = temporal.isSupported(ChronoField.HOUR_OF_DAY); + boolean hasOffset = hasTime && temporal.isSupported(ChronoField.OFFSET_SECONDS); + + if (hasOffset) { + return OffsetDateTime.from(temporal); + } else if (hasTime) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return LocalDate.from(temporal).atStartOfDay().atOffset(ZoneOffset.UTC); + } + } + + public static Optional parseDateTimeOrTimeWithHigherPrecision(String val) { + int dotIdx = val.indexOf('.'); + int tIdx = val.indexOf('T'); + int length = val.length(); + try { + if (dotIdx == 19 && tIdx == 10 && length > 20 || dotIdx == 15 && tIdx == 8 && length > 16) { + // extended or compact DATE_TIME format + return Optional.of(OpenEHRDateTimeParseUtils.parseDateTime(val)); + } else if (tIdx == -1 && (dotIdx == 8 && length > 9 || dotIdx == 10 && length > 11)) { + // extended or compact TIME format + return Optional.of(OpenEHRDateTimeParseUtils.parseTime(val)); + } + } catch (IllegalArgumentException e) { + if (!(e.getCause() instanceof DateTimeException)) { + throw e; + } + } + + return Optional.empty(); + } + + public static AslColumnField findFieldForOwner( + AslStructureColumn structureField, List fields, AslQuery owner) { + return findFieldForOwner(structureField.getFieldName(), fields, owner); + } + + // TODO convert to AslQuery member + public static AslColumnField findFieldForOwner(String fieldName, List fields, AslQuery owner) { + return fields.stream() + .filter(f -> f.getOwner() == owner) + .filter(AslColumnField.class::isInstance) + .map(AslColumnField.class::cast) + .filter(f -> fieldName.equals(f.getColumnName())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Field '%s' does not exist for owner '%s'".formatted(fieldName, owner.getAlias()))); + } + + static AslQueryCondition structurePredicateCondition( + ComparisonOperatorPredicate predicate, + AslStructureQuery query, + Function> templateUuidLookupFunc) { + + Set candidateTypes = new HashSet<>(query.getRmTypes()); + if (candidateTypes.isEmpty() && query.getType() == AslSourceRelation.EHR) { + candidateTypes.add(RmConstants.EHR); + } + AslExtractedColumn extractedColumn = AslExtractedColumn.find( + candidateTypes.iterator().next(), predicate.getPath()) + .filter(ec -> ec.getAllowedRmTypes().containsAll(candidateTypes)) + .orElseThrow(); + ComparisonConditionOperator operator = + ComparisonConditionOperator.valueOf(predicate.getOperator().name()); + final AslConditionOperator aslOperator = operator.getAslOperator(); + FieldSource ownerSource = FieldSource.withOwner(query); + List value = List.of(((Primitive) predicate.getValue())); + AslFieldValueQueryCondition condition = + switch (extractedColumn) { + case NAME_VALUE -> new AslFieldValueQueryCondition<>( + findFieldForOwner(AslStructureColumn.ENTITY_NAME, query.getSelect(), query), + aslOperator, + conditionValue(value, operator, String.class)); + case VO_ID -> new AslFieldValueQueryCondition<>( + AslComplexExtractedColumnField.voIdField(ownerSource), + aslOperator, + conditionValue(value, operator, String.class)); + case EHR_ID -> new AslFieldValueQueryCondition<>( + findFieldForOwner("id", query.getSelect(), query), + aslOperator, + conditionValue(value, operator, String.class)); + case ARCHETYPE_NODE_ID -> new AslFieldValueQueryCondition<>( + AslComplexExtractedColumnField.archetypeNodeIdField(ownerSource), + aslOperator, + archetypeNodeIdConditionValues(value, operator)); + case ROOT_CONCEPT -> new AslFieldValueQueryCondition<>( + findFieldForOwner("root_concept", query.getSelect(), query), + aslOperator, + archetypeNodeIdConditionValues(value, operator).stream() + // archetype must be for COMPOSITION + .filter(tc -> StructureRmType.COMPOSITION + .getAlias() + .equals(tc.aliasedRmType())) + .map(AslRmTypeAndConcept::concept) + .toList()); + case TEMPLATE_ID -> { + // Template id is handled separately since the extracted column stores the internal uuid + List templateUuids = templateIdConditionValues(value, operator, templateUuidLookupFunc); + yield new AslFieldValueQueryCondition<>( + findFieldForOwner(AslStructureColumn.TEMPLATE_ID, query.getSelect(), query), + aslOperator, + templateUuids); + } + case OV_CONTRIBUTION_ID, + OV_TIME_COMMITTED, + OV_TIME_COMMITTED_DV, + AD_SYSTEM_ID, + AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE, + AD_CHANGE_TYPE_PREFERRED_TERM, + AD_CHANGE_TYPE_CODE_STRING, + AD_CHANGE_TYPE_VALUE, + AD_CHANGE_TYPE_DV, + AD_DESCRIPTION_VALUE, + AD_DESCRIPTION_DV, + EHR_TIME_CREATED, + EHR_TIME_CREATED_DV, + EHR_SYSTEM_ID, + EHR_SYSTEM_ID_DV -> throw new IllegalArgumentException( + "Unexpected structure predicate on %s".formatted(extractedColumn)); + }; + if (condition.getValues().isEmpty()) { + return switch (condition.getOperator()) { + case IN, EQ, LIKE -> new AslFalseQueryCondition(); + case NEQ -> new AslTrueQueryCondition(); + default -> throw new IllegalArgumentException( + "Unexpected operator %s".formatted(condition.getOperator())); + }; + } + + return condition; + } + + @Nonnull + static List archetypeNodeIdConditionValues( + List comparison, ComparisonConditionOperator operator) { + return conditionValue(comparison, operator, String.class).stream() + .map(String.class::cast) + .map(AslRmTypeAndConcept::fromArchetypeNodeId) + .toList(); + } + + @Nonnull + static List templateIdConditionValues( + List operands, + ComparisonConditionOperator operator, + Function> templateUuidLookupFunc) { + if (EnumSet.of( + ComparisonConditionOperator.LIKE, + ComparisonConditionOperator.GT_EQ, + ComparisonConditionOperator.GT, + ComparisonConditionOperator.LT_EQ, + ComparisonConditionOperator.LT) + .contains(operator)) { + // These operators will require special implementation for template_id + throw new IllegalArgumentException("unexpected operator for template_id: %s".formatted(operator)); + } + return conditionValue(operands, operator, String.class).stream() + .filter(Objects::nonNull) + .map(String.class::cast) + .map(templateUuidLookupFunc) + .flatMap(Optional::stream) + .toList(); + } + + static Stream streamStringPrimitives(ComparisonOperatorConditionWrapper c) { + return c.rightComparisonOperands().stream() + .filter(StringPrimitive.class::isInstance) + .map(StringPrimitive.class::cast); + } + + static Optional reduceConditions( + LogicalConditionOperator setOp, Stream conditions) { + + List unfiltered = conditions.toList(); + + if (unfiltered.isEmpty()) { + return Optional.empty(); + } + + List filtered = + unfiltered.stream().filter(setOp::filterNotNoop).toList(); + + if (filtered.isEmpty()) { + // if all conditions are noop conditions, return one of them + return Optional.of(unfiltered.getFirst()); + } + + if (filtered.size() == 1) { + return Optional.of(filtered.getFirst()); + } + + return filtered.stream() + .filter(setOp::filterShortCircuit) + .findFirst() + .or(() -> Optional.of(setOp.build(filtered))); + } + + static Optional predicates( + List orPredicates, + Function comparisonOperatorHandler) { + return reduceConditions( + LogicalConditionOperator.OR, + CollectionUtils.emptyIfNull(orPredicates).stream() + .map(p -> reduceConditions( + LogicalConditionOperator.AND, + p.getOperands().stream().map(comparisonOperatorHandler))) + .flatMap(Optional::stream)); + } + + static List conditionValue(List values, ComparisonConditionOperator operator, Class type) { + boolean isJsonbField = JSONB.class.isAssignableFrom(type); + return switch (operator) { + case EXISTS -> Collections.emptyList(); + case MATCHES, EQ, NEQ -> values.stream() + .map(Primitive::getValue) + .filter(p -> isJsonbField + || type.isInstance(p) + || UUID.class.isAssignableFrom(type) && p instanceof String) + .toList(); + case LT, GT_EQ, GT, LT_EQ -> values.stream() + .map(Primitive::getValue) + .toList(); + case LIKE -> values.stream() + .map(Primitive::getValue) + .map(String.class::cast) + .map(AslUtils::translateAqlLikePatternToSql) + .filter(p -> isJsonbField || type.isInstance(p) || UUID.class.isAssignableFrom(type)) + .toList(); + }; + } + + static AslQueryCondition and(Stream conditionStream) { + List conditions = conditionStream.toList(); + return switch (conditions.size()) { + case 0 -> null; + case 1 -> conditions.getFirst(); + default -> new AslAndQueryCondition(conditions); + }; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/DataNodeInfo.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/DataNodeInfo.java new file mode 100644 index 000000000..1483891be --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/DataNodeInfo.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import org.ehrbase.openehr.aqlengine.asl.DataNodeInfo.ExtractedColumnDataNodeInfo; +import org.ehrbase.openehr.aqlengine.asl.DataNodeInfo.JsonRmDataNodeInfo; +import org.ehrbase.openehr.aqlengine.asl.DataNodeInfo.StructureRmDataNodeInfo; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslEncapsulatingQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathCohesionAnalysis.PathCohesionTreeNode; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; + +sealed interface DataNodeInfo permits ExtractedColumnDataNodeInfo, JsonRmDataNodeInfo, StructureRmDataNodeInfo { + PathCohesionTreeNode node(); + + OwnerProviderTuple parent(); + + AslQuery providerSubQuery(); + + record JsonRmDataNodeInfo( + @Override PathCohesionTreeNode node, + @Override OwnerProviderTuple parent, + AslEncapsulatingQuery parentJoin, + @Override AslQuery providerSubQuery, + List pathInJson, + boolean multipleValued, + Stream dependentPathDataNodes, + Set dvOrderedTypes, + Class type) + implements DataNodeInfo {} + + record ExtractedColumnDataNodeInfo( + @Override PathCohesionTreeNode node, + @Override OwnerProviderTuple parent, + @Override AslQuery providerSubQuery, + AslExtractedColumn extractedColumn) + implements DataNodeInfo {} + + record StructureRmDataNodeInfo( + @Override PathCohesionTreeNode node, + @Override OwnerProviderTuple parent, + AslEncapsulatingQuery parentJoin, + @Override AslQuery providerSubQuery) + implements DataNodeInfo {} +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/OwnerProviderTuple.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/OwnerProviderTuple.java new file mode 100644 index 000000000..8cd9778e2 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/OwnerProviderTuple.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +record OwnerProviderTuple(AslQuery owner, AslQuery provider) {} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslExtractedColumn.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslExtractedColumn.java new file mode 100644 index 000000000..23ac2b5c0 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslExtractedColumn.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model; + +import static org.ehrbase.jooq.pg.Tables.AUDIT_DETAILS; +import static org.ehrbase.jooq.pg.Tables.COMP_VERSION; +import static org.ehrbase.jooq.pg.Tables.EHR_; +import static org.ehrbase.jooq.pg.tables.CompData.COMP_DATA; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; +import org.ehrbase.jooq.pg.tables.Ehr; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.dbformat.AncestorStructureRmType; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPathUtil; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.jooq.Field; + +public enum AslExtractedColumn { + NAME_VALUE( + AqlObjectPathUtil.NAME_VALUE, + COMP_DATA.ENTITY_NAME, + String.class, + false, + Stream.concat(Arrays.stream(StructureRmType.values()), Arrays.stream(AncestorStructureRmType.values())) + .map(Enum::name) + .toArray(String[]::new)), + VO_ID( + AqlObjectPath.parse("uid/value"), + List.of(COMP_DATA.VO_ID, COMP_VERSION.SYS_VERSION), + String.class, + true, + StructureRmType.COMPOSITION.name(), + StructureRmType.EHR_STATUS.name(), + RmConstants.ORIGINAL_VERSION), + ROOT_CONCEPT( + // same path as ARCHETYPE_NODE_ID (alternative for Compositions) + AqlObjectPathUtil.ARCHETYPE_NODE_ID, + COMP_VERSION.ROOT_CONCEPT, + String.class, + true, + StructureRmType.COMPOSITION.name()), + ARCHETYPE_NODE_ID( + AqlObjectPathUtil.ARCHETYPE_NODE_ID, + List.of(COMP_DATA.RM_ENTITY, COMP_DATA.ENTITY_CONCEPT), + String.class, + false, + Stream.concat(Arrays.stream(StructureRmType.values()), Arrays.stream(AncestorStructureRmType.values())) + // for Compositions ROOT_CONCEPT is used + .filter(v -> !v.equals(StructureRmType.COMPOSITION)) + .map(Enum::name) + .toArray(String[]::new)), + TEMPLATE_ID( + AqlObjectPath.parse("archetype_details/template_id/value"), + COMP_VERSION.TEMPLATE_ID, + String.class, + true, + StructureRmType.COMPOSITION.name()), + + // EHR + EHR_ID(AqlObjectPath.parse("ehr_id/value"), Ehr.EHR_.ID, UUID.class, false, RmConstants.EHR), + EHR_SYSTEM_ID( + AqlObjectPath.parse("system_id/value"), Collections.emptyList(), String.class, false, RmConstants.EHR), + EHR_SYSTEM_ID_DV(AqlObjectPath.parse("system_id"), Collections.emptyList(), String.class, false, RmConstants.EHR), + EHR_TIME_CREATED_DV(AqlObjectPath.parse("time_created"), EHR_.CREATION_DATE, String.class, false, RmConstants.EHR), + EHR_TIME_CREATED( + AqlObjectPath.parse("time_created/value"), EHR_.CREATION_DATE, String.class, false, RmConstants.EHR), + + // ORIGINAL_VERSION + OV_CONTRIBUTION_ID( + AqlObjectPath.parse("contribution/id/value"), + COMP_VERSION.CONTRIBUTION_ID, + String.class, + true, + RmConstants.ORIGINAL_VERSION), + OV_TIME_COMMITTED_DV( + AqlObjectPath.parse("commit_audit/time_committed"), + COMP_VERSION.SYS_PERIOD_LOWER, + String.class, + true, + RmConstants.ORIGINAL_VERSION), + OV_TIME_COMMITTED( + AqlObjectPath.parse("commit_audit/time_committed/value"), + COMP_VERSION.SYS_PERIOD_LOWER, + String.class, + true, + RmConstants.ORIGINAL_VERSION), + + // AUDIT_DETAILS + AD_SYSTEM_ID( + AqlObjectPath.parse("system_id"), Collections.emptyList(), String.class, true, RmConstants.AUDIT_DETAILS), + AD_DESCRIPTION_DV( + AqlObjectPath.parse("description"), + AUDIT_DETAILS.DESCRIPTION, + String.class, + true, + RmConstants.AUDIT_DETAILS), + AD_DESCRIPTION_VALUE( + AqlObjectPath.parse("description/value"), + AUDIT_DETAILS.DESCRIPTION, + String.class, + true, + RmConstants.AUDIT_DETAILS), + AD_CHANGE_TYPE_DV( + AqlObjectPath.parse("change_type"), + AUDIT_DETAILS.CHANGE_TYPE, + String.class, + true, + RmConstants.AUDIT_DETAILS), + AD_CHANGE_TYPE_VALUE( + AqlObjectPath.parse("change_type/value"), + AUDIT_DETAILS.CHANGE_TYPE, + String.class, + true, + RmConstants.AUDIT_DETAILS), + AD_CHANGE_TYPE_CODE_STRING( + AqlObjectPath.parse("change_type/defining_code/code_string"), + AUDIT_DETAILS.CHANGE_TYPE, + String.class, + true, + RmConstants.AUDIT_DETAILS), + AD_CHANGE_TYPE_PREFERRED_TERM( + AqlObjectPath.parse("change_type/defining_code/preferred_term"), + AUDIT_DETAILS.CHANGE_TYPE, + String.class, + true, + RmConstants.AUDIT_DETAILS), + AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE( + AqlObjectPath.parse("change_type/defining_code/terminology_id/value"), + Collections.emptyList(), + String.class, + true, + RmConstants.AUDIT_DETAILS); + + private final AqlObjectPath path; + private final List columns; + private final Class columnType; + private final Set allowedRmTypes; + private final boolean requiresVersionTable; + + AslExtractedColumn( + AqlObjectPath path, + Field column, + Class columnType, + boolean requiresVersionTable, + String... allowedRmTypes) { + this(path, List.of(column), columnType, requiresVersionTable, allowedRmTypes); + } + + AslExtractedColumn( + AqlObjectPath path, + List columns, + Class columnType, + boolean requiresVersionTable, + String... allowedRmTypes) { + this.path = Objects.requireNonNull(path).frozen(); + this.columnType = Objects.requireNonNull(columnType); + this.columns = Optional.ofNullable(columns) + .map(l -> l.stream().map(Field::getName).toList()) + .orElse(null); + this.requiresVersionTable = requiresVersionTable; + this.allowedRmTypes = Set.of(allowedRmTypes); + } + + public AqlObjectPath getPath() { + return path; + } + + public Set getAllowedRmTypes() { + return allowedRmTypes; + } + + public boolean requiresVersionTable() { + return requiresVersionTable; + } + + public static Optional find(ContainsWrapper contains, AqlObjectPath toMatch) { + return find(contains.getRmType(), toMatch); + } + + public static Optional find(String containmentType, AqlObjectPath toMatch) { + return Arrays.stream(AslExtractedColumn.values()) + .filter(ep -> ep.matches(containmentType, toMatch)) + .findFirst(); + } + + public static Optional find(String containmentType, AqlObjectPath toMatch, int skip) { + List pathNodes = Optional.ofNullable(toMatch).map(AqlObjectPath::getPathNodes).stream() + .flatMap(List::stream) + .skip(skip) + .toList(); + return find(containmentType, new AqlObjectPath(pathNodes)); + } + + public boolean matches(String containmentType, AqlObjectPath toMatch) { + return allowedRmTypes.contains(containmentType) && Objects.equals(toMatch, path); + } + + public Class getColumnType() { + return columnType; + } + + public List getColumns() { + return columns; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslRmTypeAndConcept.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslRmTypeAndConcept.java new file mode 100644 index 000000000..a638e2aa0 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslRmTypeAndConcept.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model; + +import org.ehrbase.openehr.dbformat.RmTypeAlias; + +/** + * archetypeNodeId maps to rm entity and entity concept columns + * + * @param aliasedRmType + * @param concept + */ +public record AslRmTypeAndConcept(String aliasedRmType, String concept) { + + public static final String ARCHETYPE_PREFIX = "openEHR-EHR-"; + + public static AslRmTypeAndConcept fromArchetypeNodeId(String archetypeNodeId) { + if (archetypeNodeId == null) { + return null; + } + + if (archetypeNodeId.startsWith(ARCHETYPE_PREFIX)) { + int pos = archetypeNodeId.indexOf('.', ARCHETYPE_PREFIX.length()); + if (pos < 0) { + throw new IllegalArgumentException("Archetype id is not valid: " + archetypeNodeId); + } + String alias = RmTypeAlias.optionalAlias(archetypeNodeId.substring(ARCHETYPE_PREFIX.length(), pos)) + .orElseThrow(() -> new IllegalArgumentException( + "Archetype id for unsupported/unknown RM type: " + archetypeNodeId)); + String concept = archetypeNodeId.substring(pos); + return new AslRmTypeAndConcept(alias, concept); + + } else if (archetypeNodeId.startsWith("at") || archetypeNodeId.startsWith("id")) { + // at or id code + return new AslRmTypeAndConcept(null, archetypeNodeId); + } else { + throw new IllegalArgumentException("Invalid archetype_node_id: %s".formatted(archetypeNodeId)); + } + } + + /** + * Removes the fixed prefix from archetype ids (openEHR-EHR-{RM-type}), + * but leaves the '.', which hints the missing prefix + * @param archetypeNodeId + * @return + */ + public static String toEntityConcept(String archetypeNodeId) { + if (archetypeNodeId == null) { + return null; + } + return fromArchetypeNodeId(archetypeNodeId).concept; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslStructureColumn.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslStructureColumn.java new file mode 100644 index 000000000..99fa69c49 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslStructureColumn.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model; + +import static org.ehrbase.jooq.pg.Tables.COMP_VERSION; + +import java.time.OffsetDateTime; +import java.util.UUID; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.dbformat.jooq.prototypes.ObjectDataTablePrototype; +import org.ehrbase.openehr.dbformat.jooq.prototypes.ObjectVersionTablePrototype; +import org.jooq.Field; + +public enum AslStructureColumn { + VO_ID(ObjectDataTablePrototype.INSTANCE.VO_ID, UUID.class, null), + NUM(ObjectDataTablePrototype.INSTANCE.NUM, Integer.class, false), + NUM_CAP(ObjectDataTablePrototype.INSTANCE.NUM_CAP, Integer.class, false), + PARENT_NUM(ObjectDataTablePrototype.INSTANCE.PARENT_NUM, Integer.class, false), + EHR_ID(ObjectVersionTablePrototype.INSTANCE.EHR_ID, UUID.class, true), + ENTITY_IDX(ObjectDataTablePrototype.INSTANCE.ENTITY_IDX, String.class, false), + ENTITY_IDX_LEN(ObjectDataTablePrototype.INSTANCE.ENTITY_IDX_LEN, Integer.class, false), + ENTITY_CONCEPT(ObjectDataTablePrototype.INSTANCE.ENTITY_CONCEPT, String.class, false), + ENTITY_NAME(ObjectDataTablePrototype.INSTANCE.ENTITY_NAME, String.class, AslExtractedColumn.NAME_VALUE, false), + RM_ENTITY(ObjectDataTablePrototype.INSTANCE.RM_ENTITY, String.class, false), + TEMPLATE_ID(COMP_VERSION.TEMPLATE_ID, UUID.class, AslExtractedColumn.TEMPLATE_ID, true), + SYS_VERSION(ObjectVersionTablePrototype.INSTANCE.SYS_VERSION, Integer.class, true), + + // Columns for VERSION querying + AUDIT_ID(ObjectVersionTablePrototype.INSTANCE.AUDIT_ID, UUID.class, true), + CONTRIBUTION_ID(ObjectVersionTablePrototype.INSTANCE.CONTRIBUTION_ID, UUID.class, null, true), + SYS_PERIOD_LOWER(ObjectVersionTablePrototype.INSTANCE.SYS_PERIOD_LOWER, OffsetDateTime.class, null, true); + + private final String fieldName; + private final Class clazz; + private final AslExtractedColumn extractedColumn; + private final Boolean fromVersionTable; + + AslStructureColumn(Field field, Class clazz, Boolean inVersionTable) { + this(field, clazz, null, inVersionTable); + } + + AslStructureColumn(Field field, Class clazz, AslExtractedColumn extractedColumn, Boolean inVersionTable) { + this.fieldName = field.getName(); + this.clazz = clazz; + this.extractedColumn = extractedColumn; + this.fromVersionTable = inVersionTable; + } + + public AslField field() { + return new AslColumnField(clazz, fieldName, null, fromVersionTable, extractedColumn); + } + + public String getFieldName() { + return fieldName; + } + + public Class getClazz() { + return clazz; + } + + public boolean isFromVersionTable() { + return !Boolean.FALSE.equals(fromVersionTable); + } + + public boolean isFromDataTable() { + return !Boolean.TRUE.equals(fromVersionTable); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslAndQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslAndQueryCondition.java new file mode 100644 index 000000000..d69fcc1e8 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslAndQueryCondition.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslAndQueryCondition implements AslQueryCondition { + private final List operands; + + public AslAndQueryCondition(AslQueryCondition... conditions) { + this.operands = Arrays.stream(conditions).collect(Collectors.toList()); + } + + public AslAndQueryCondition(List operands) { + this.operands = new ArrayList<>(operands); + } + + public List getOperands() { + return operands; + } + + @Override + public AslAndQueryCondition withProvider(AslQuery provider) { + return new AslAndQueryCondition(operands.stream() + .map(condition -> condition.withProvider(provider)) + .collect(Collectors.toList())); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslDescendantCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslDescendantCondition.java new file mode 100644 index 000000000..b53aacf59 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslDescendantCondition.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; + +/** + * For contains and path joins + */ +public final class AslDescendantCondition implements AslProvidesJoinCondition { + private final AslSourceRelation parentRelation; + private final AslSourceRelation descendantRelation; + private final AslQuery leftProvider; + private final AslQuery leftOwner; + private final AslQuery rightProvider; + private final AslQuery rightOwner; + + public AslDescendantCondition( + AslSourceRelation parentRelation, + AslQuery leftProvider, + AslQuery leftOwner, + AslSourceRelation descendantRelation, + AslQuery rightProvider, + AslQuery rightOwner) { + this.parentRelation = parentRelation; + this.leftProvider = leftProvider; + this.leftOwner = leftOwner; + this.descendantRelation = descendantRelation; + this.rightProvider = rightProvider; + this.rightOwner = rightOwner; + } + + public AslSourceRelation getParentRelation() { + return parentRelation; + } + + public AslSourceRelation getDescendantRelation() { + return descendantRelation; + } + + @Override + public AslQuery getLeftOwner() { + return leftOwner; + } + + @Override + public AslQuery getRightOwner() { + return rightOwner; + } + + public AslQuery getLeftProvider() { + return leftProvider; + } + + public AslQuery getRightProvider() { + return rightProvider; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslDvOrderedValueQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslDvOrderedValueQueryCondition.java new file mode 100644 index 000000000..173ad24d6 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslDvOrderedValueQueryCondition.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.apache.commons.collections4.CollectionUtils; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslDvOrderedColumnField; + +public final class AslDvOrderedValueQueryCondition extends AslFieldValueQueryCondition { + private final Set typesToCompare; + + public AslDvOrderedValueQueryCondition( + Set typesToCompare, AslDvOrderedColumnField field, AslConditionOperator operator, List values) { + super(field, operator, values); + if (CollectionUtils.isEmpty(typesToCompare)) { + throw new IllegalArgumentException("Affected DV_ORDERED types not specified"); + } + this.typesToCompare = Collections.unmodifiableSet(typesToCompare); + } + + public Set getTypesToCompare() { + return typesToCompare; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslEntityIdxOffsetCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslEntityIdxOffsetCondition.java new file mode 100644 index 000000000..b382d109f --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslEntityIdxOffsetCondition.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslEntityIdxOffsetCondition implements AslProvidesJoinCondition { + private final int offset; + private final AslQuery leftProvider; + private final AslQuery leftOwner; + private final AslQuery rightProvider; + private final AslQuery rightOwner; + + public AslEntityIdxOffsetCondition( + AslQuery leftProvider, AslQuery leftOwner, AslQuery rightProvider, AslQuery rightOwner, int offset) { + this.leftProvider = leftProvider; + this.leftOwner = leftOwner; + this.rightProvider = rightProvider; + this.rightOwner = rightOwner; + this.offset = offset; + } + + public int getOffset() { + return offset; + } + + @Override + public AslQuery getLeftOwner() { + return leftOwner; + } + + @Override + public AslQuery getRightOwner() { + return rightOwner; + } + + public AslQuery getLeftProvider() { + return leftProvider; + } + + public AslQuery getRightProvider() { + return rightProvider; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslFalseQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslFalseQueryCondition.java new file mode 100644 index 000000000..2c5957cbc --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslFalseQueryCondition.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslFalseQueryCondition implements AslQueryCondition { + @Override + public AslFalseQueryCondition withProvider(AslQuery provider) { + return new AslFalseQueryCondition(); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslFieldValueQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslFieldValueQueryCondition.java new file mode 100644 index 000000000..a30ba5e10 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslFieldValueQueryCondition.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.collections4.ListUtils; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public sealed class AslFieldValueQueryCondition implements AslQueryCondition + permits AslDvOrderedValueQueryCondition { + + private final AslField field; + private final AslConditionOperator operator; + private final List values; + + public AslFieldValueQueryCondition(AslField field, AslConditionOperator operator, List values) { + this.field = field; + this.operator = operator; + this.values = ListUtils.emptyIfNull(values); + } + + public AslField getField() { + return field; + } + + public AslConditionOperator getOperator() { + return operator; + } + + public List getValues() { + return values; + } + + @Override + public AslFieldValueQueryCondition withProvider(AslQuery provider) { + return new AslFieldValueQueryCondition<>(field.withProvider(provider), operator, new ArrayList<>(values)); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslNotNullQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslNotNullQueryCondition.java new file mode 100644 index 000000000..b2935c80d --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslNotNullQueryCondition.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +/** + * This condition is used to make sure a left-joined subquery is not empty, + * by checking that a field based on a column with a NOT NULL constraint (i.e. COMP.VO_ID) is not null. + */ +public final class AslNotNullQueryCondition implements AslQueryCondition { + private final AslField field; + + public AslNotNullQueryCondition(AslField field) { + this.field = field; + } + + public AslField getField() { + return field; + } + + @Override + public AslNotNullQueryCondition withProvider(AslQuery provider) { + return new AslNotNullQueryCondition(field.withProvider(provider)); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslNotQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslNotQueryCondition.java new file mode 100644 index 000000000..5fab0d0e6 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslNotQueryCondition.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslNotQueryCondition implements AslQueryCondition { + private final AslQueryCondition condition; + + public AslNotQueryCondition(AslQueryCondition condition) { + this.condition = condition; + } + + public AslQueryCondition getCondition() { + return condition; + } + + @Override + public AslNotQueryCondition withProvider(AslQuery provider) { + return new AslNotQueryCondition(condition.withProvider(provider)); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslOrQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslOrQueryCondition.java new file mode 100644 index 000000000..cdb5a6794 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslOrQueryCondition.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslOrQueryCondition implements AslQueryCondition { + private final List operands; + + public AslOrQueryCondition(AslQueryCondition... conditions) { + this.operands = Arrays.stream(conditions).collect(Collectors.toList()); + } + + public AslOrQueryCondition(List operands) { + this.operands = operands; + } + + public List getOperands() { + return operands; + } + + @Override + public AslOrQueryCondition withProvider(AslQuery provider) { + return new AslOrQueryCondition(operands.stream() + .map(condition -> condition.withProvider(provider)) + .collect(Collectors.toList())); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslPathChildCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslPathChildCondition.java new file mode 100644 index 000000000..85ff78cbd --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslPathChildCondition.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; + +/** + * For contains and path joins + */ +public final class AslPathChildCondition implements AslProvidesJoinCondition { + private final AslSourceRelation parentRelation; + private final AslSourceRelation childRelation; + private final AslQuery leftProvider; + private final AslQuery leftOwner; + private final AslQuery rightProvider; + private final AslQuery rightOwner; + + public AslPathChildCondition( + AslSourceRelation parentRelation, + AslQuery leftProvider, + AslQuery leftOwner, + AslSourceRelation childRelation, + AslQuery rightProvider, + AslQuery rightOwner) { + this.parentRelation = parentRelation; + this.leftProvider = leftProvider; + this.leftOwner = leftOwner; + this.childRelation = childRelation; + this.rightProvider = rightProvider; + this.rightOwner = rightOwner; + } + + public AslSourceRelation getParentRelation() { + return parentRelation; + } + + public AslSourceRelation getChildRelation() { + return childRelation; + } + + @Override + public AslQuery getLeftOwner() { + return leftOwner; + } + + @Override + public AslQuery getRightOwner() { + return rightOwner; + } + + public AslQuery getLeftProvider() { + return leftProvider; + } + + public AslQuery getRightProvider() { + return rightProvider; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslProvidesJoinCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslProvidesJoinCondition.java new file mode 100644 index 000000000..a7808b912 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslProvidesJoinCondition.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.join.AslDelegatingJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public sealed interface AslProvidesJoinCondition extends AslQueryCondition + permits AslDescendantCondition, AslEntityIdxOffsetCondition, AslPathChildCondition { + + AslQuery getLeftOwner(); + + AslQuery getRightOwner(); + + default AslDelegatingJoinCondition provideJoinCondition() { + return new AslDelegatingJoinCondition(this); + } + + @Override + default AslQueryCondition withProvider(AslQuery provider) { + throw new UnsupportedOperationException(); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslQueryCondition.java new file mode 100644 index 000000000..00d64a199 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslQueryCondition.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public sealed interface AslQueryCondition + permits AslAndQueryCondition, + AslFalseQueryCondition, + AslFieldValueQueryCondition, + AslNotNullQueryCondition, + AslNotQueryCondition, + AslOrQueryCondition, + AslTrueQueryCondition, + AslProvidesJoinCondition { + enum AslConditionOperator { + LIKE, + IN, + EQ, + NEQ, + GT_EQ, + GT, + LT_EQ, + LT, + IS_NULL, + IS_NOT_NULL + } + + AslQueryCondition withProvider(AslQuery provider); +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslTrueQueryCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslTrueQueryCondition.java new file mode 100644 index 000000000..d6e6d69b4 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslTrueQueryCondition.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.condition; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslTrueQueryCondition implements AslQueryCondition { + @Override + public AslTrueQueryCondition withProvider(AslQuery provider) { + return new AslTrueQueryCondition(); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslAggregatingField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslAggregatingField.java new file mode 100644 index 000000000..9cd091924 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslAggregatingField.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction.AggregateFunctionName; + +public final class AslAggregatingField extends AslVirtualField { + + private final AggregateFunctionName function; + private final AslField baseField; + private final boolean distinct; + + public AslAggregatingField(AggregateFunctionName function, AslField baseField, boolean distinct) { + super(Number.class, null, null); + this.function = function; + this.baseField = baseField; + this.distinct = distinct; + } + + public AggregateFunctionName getFunction() { + return function; + } + + public AslField getBaseField() { + return baseField; + } + + @Override + public AslQuery getOwner() { + return baseField == null ? null : baseField.getOwner(); + } + + @Override + public AslQuery getInternalProvider() { + return baseField == null ? null : baseField.getInternalProvider(); + } + + @Override + public AslQuery getProvider() { + return baseField == null ? null : baseField.getProvider(); + } + + @Override + public String aliasedName(String name) { + return "agg_" + baseField.aliasedName(name); + } + + @Override + public AslField withProvider(AslQuery provider) { + throw new UnsupportedOperationException(); + } + + @Override + public AslField copyWithOwner(AslQuery aslFilteringQuery) { + throw new UnsupportedOperationException(); + } + + public boolean isDistinct() { + return distinct; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslColumnField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslColumnField.java new file mode 100644 index 000000000..cc381e986 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslColumnField.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public sealed class AslColumnField extends AslField permits AslDvOrderedColumnField { + private final String columnName; + private final Boolean versionTableField; + + public AslColumnField(Class type, String columnName, boolean versionTableField) { + this(type, columnName, null, versionTableField); + } + + public AslColumnField(Class type, String columnName, FieldSource fieldSource, boolean versionTableField) { + this(type, columnName, fieldSource, versionTableField, null); + } + + public AslColumnField( + Class type, + String columnName, + FieldSource fieldSource, + Boolean versionTableField, + AslExtractedColumn extractedColumn) { + super(type, fieldSource, extractedColumn); + this.columnName = columnName; + this.versionTableField = versionTableField; + } + + public String getName(boolean aliased) { + return aliased ? getAliasedName() : getColumnName(); + } + + public String getAliasedName() { + return aliasedName(columnName); + } + + public String getColumnName() { + return columnName; + } + + public boolean isVersionTableField() { + return !Boolean.FALSE.equals(versionTableField); + } + + public boolean isDataTableField() { + return !Boolean.TRUE.equals(versionTableField); + } + + @Override + public AslColumnField withProvider(AslQuery provider) { + return new AslColumnField( + type, columnName, fieldSource.withProvider(provider), versionTableField, getExtractedColumn()); + } + + @Override + public AslColumnField copyWithOwner(AslQuery owner) { + return new AslColumnField( + type, columnName, FieldSource.withOwner(owner), versionTableField, getExtractedColumn()); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslComplexExtractedColumnField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslComplexExtractedColumnField.java new file mode 100644 index 000000000..21c7f34d7 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslComplexExtractedColumnField.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslComplexExtractedColumnField extends AslVirtualField { + + public AslComplexExtractedColumnField(AslExtractedColumn extractedColumn, FieldSource fieldSource) { + super(extractedColumn.getColumnType(), fieldSource, extractedColumn); + this.extractedColumn = extractedColumn; + } + + @Override + public AslComplexExtractedColumnField withProvider(AslQuery provider) { + return new AslComplexExtractedColumnField(extractedColumn, fieldSource.withProvider(provider)); + } + + @Override + public AslComplexExtractedColumnField copyWithOwner(AslQuery owner) { + return new AslComplexExtractedColumnField(extractedColumn, FieldSource.withOwner(owner)); + } + + public static AslComplexExtractedColumnField archetypeNodeIdField(FieldSource fieldSource) { + return new AslComplexExtractedColumnField(AslExtractedColumn.ARCHETYPE_NODE_ID, fieldSource); + } + + public static AslComplexExtractedColumnField voIdField(FieldSource fieldSource) { + return new AslComplexExtractedColumnField(AslExtractedColumn.VO_ID, fieldSource); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslConstantField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslConstantField.java new file mode 100644 index 000000000..b6d881e98 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslConstantField.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import java.lang.constant.Constable; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslConstantField extends AslField { + private final T value; + + public AslConstantField(Class type, T value, FieldSource fieldSource, AslExtractedColumn extractedColumn) { + super(type, fieldSource, extractedColumn); + this.value = value; + } + + public T getValue() { + return value; + } + + @Override + public AslConstantField withProvider(AslQuery provider) { + return new AslConstantField<>((Class) type, value, fieldSource.withProvider(provider), getExtractedColumn()); + } + + @Override + public AslConstantField copyWithOwner(AslQuery owner) { + return new AslConstantField<>((Class) type, value, FieldSource.withOwner(owner), getExtractedColumn()); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslDvOrderedColumnField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslDvOrderedColumnField.java new file mode 100644 index 000000000..48492754f --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslDvOrderedColumnField.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import java.util.Collections; +import java.util.Set; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.jooq.JSONB; + +public final class AslDvOrderedColumnField extends AslColumnField { + + private final Set dvOrderedTypes; + + public AslDvOrderedColumnField(String columnName, FieldSource fieldSource, Set dvOrderedTypes) { + super(JSONB.class, columnName, fieldSource, false); + this.dvOrderedTypes = Collections.unmodifiableSet(dvOrderedTypes); + } + + public Set getDvOrderedTypes() { + return dvOrderedTypes; + } + + @Override + public AslDvOrderedColumnField withProvider(AslQuery provider) { + return new AslDvOrderedColumnField(getColumnName(), fieldSource.withProvider(provider), dvOrderedTypes); + } + + @Override + public AslDvOrderedColumnField copyWithOwner(AslQuery owner) { + return new AslDvOrderedColumnField(getColumnName(), FieldSource.withOwner(owner), dvOrderedTypes); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslField.java new file mode 100644 index 000000000..0c52cea69 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslField.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import java.util.stream.Stream; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; + +public abstract sealed class AslField permits AslColumnField, AslConstantField, AslSubqueryField, AslVirtualField { + public record FieldSource( + /** + * The table that the fields originates from + */ + AslQuery owner, + /** + * The table that provides the field to "provider" + */ + AslQuery internalProvider, + /** + * The table that provides the field + */ + AslQuery provider) { + + public static FieldSource withOwner(AslQuery owner) { + return new FieldSource(owner, owner, owner); + } + + public FieldSource withProvider(AslQuery newProvider) { + return new FieldSource(owner, provider, newProvider); + } + } + + protected Class type; + protected FieldSource fieldSource; + protected AslExtractedColumn extractedColumn; + + protected AslField(Class type, FieldSource fieldSource, AslExtractedColumn extractedColumn) { + this.type = type; + this.fieldSource = fieldSource; + this.extractedColumn = extractedColumn; + } + + public Class getType() { + return type; + } + + public AslQuery getOwner() { + return fieldSource.owner(); + } + + public AslQuery getInternalProvider() { + return fieldSource.internalProvider(); + } + + public AslQuery getProvider() { + return fieldSource.provider(); + } + + public abstract AslField withProvider(AslQuery provider); + + public AslField withOwner(AslQuery owner) { + if (fieldSource != null) { + throw new IllegalArgumentException("fieldSource is already set"); + } + return copyWithOwner(owner); + } + + public AslExtractedColumn getExtractedColumn() { + return extractedColumn; + } + + protected String aliasedName(String name) { + return fieldSource.owner().getAlias() + "_" + name; + } + + public abstract AslField copyWithOwner(AslQuery aslFilteringQuery); + + public Stream fieldsForAggregation(AslRootQuery rootQuery) { + if (this.getProvider() == rootQuery) { + return Stream.of(this); + } else { + return Stream.of(this.withProvider(rootQuery)); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslOrderByField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslOrderByField.java new file mode 100644 index 000000000..363e18a9f --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslOrderByField.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import org.jooq.SortOrder; + +public record AslOrderByField(AslField field, SortOrder direction) {} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslSubqueryField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslSubqueryField.java new file mode 100644 index 000000000..96c39149d --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslSubqueryField.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import java.util.List; +import java.util.stream.Stream; +import org.ehrbase.openehr.aqlengine.asl.AslUtils; +import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRmObjectDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; + +public final class AslSubqueryField extends AslField { + + private final AslQuery baseQuery; + private final List filterConditions; + + private AslSubqueryField(Class type, AslQuery baseQuery, List filterConditions) { + super(type, null, null); + this.baseQuery = baseQuery; + this.filterConditions = filterConditions; + } + + public static AslSubqueryField createAslSubqueryField(Class type, AslQuery baseQuery) { + return new AslSubqueryField(type, baseQuery, List.of()); + } + + public AslQuery getBaseQuery() { + return baseQuery; + } + + public List getFilterConditions() { + return filterConditions; + } + + @Override + public AslQuery getOwner() { + return null; + } + + @Override + public AslQuery getInternalProvider() { + return null; + } + + @Override + public AslQuery getProvider() { + return null; + } + + @Override + protected String aliasedName(String name) { + throw new UnsupportedOperationException(); + } + + public String getAliasedName() { + return baseQuery.getAlias(); + } + + @Override + public AslField withProvider(AslQuery provider) { + throw new UnsupportedOperationException(); + } + + @Override + public AslField copyWithOwner(AslQuery aslFilteringQuery) { + throw new UnsupportedOperationException(); + } + + public AslSubqueryField withFilterConditions(List filterConditions) { + List conditions = filterConditions.stream() + .map(c -> switch (c) { + case AslPathFilterJoinCondition pfc -> pfc.getCondition(); + default -> throw new IllegalArgumentException("Unsupported condition type: " + c.getClass()); + }) + .toList(); + + return new AslSubqueryField(getType(), baseQuery, conditions); + } + + @Override + public Stream fieldsForAggregation(AslRootQuery rootQuery) { + if (getBaseQuery() instanceof AslRmObjectDataQuery odq) { + List baseProviderFields = odq.getBaseProvider().getSelect(); + AslQuery base = odq.getBase(); + return Stream.concat( + Stream.of( + AslUtils.findFieldForOwner(AslStructureColumn.VO_ID, baseProviderFields, base), + AslUtils.findFieldForOwner(AslStructureColumn.NUM, baseProviderFields, base), + AslUtils.findFieldForOwner(AslStructureColumn.NUM_CAP, baseProviderFields, base), + AslUtils.findFieldForOwner( + AslStructureColumn.ENTITY_IDX, baseProviderFields, base)), + filterConditions.stream() + .flatMap(AslUtils::streamConditionFields) + .distinct()) + .map(f -> f.getProvider() == rootQuery ? f : f.withProvider(rootQuery)); + } + + return super.fieldsForAggregation(rootQuery); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslVirtualField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslVirtualField.java new file mode 100644 index 000000000..9145b9cd0 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslVirtualField.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.field; + +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; + +public abstract sealed class AslVirtualField extends AslField + permits AslAggregatingField, AslComplexExtractedColumnField { + public AslVirtualField(Class type, FieldSource fieldSource, AslExtractedColumn extractedColumn) { + super(type, fieldSource, extractedColumn); + } + + @Override + public String aliasedName(String name) { + return super.aliasedName(name); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslAbstractJoinCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslAbstractJoinCondition.java new file mode 100644 index 000000000..8bf36e357 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslAbstractJoinCondition.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.join; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public abstract sealed class AslAbstractJoinCondition implements AslJoinCondition + permits AslDelegatingJoinCondition, AslPathFilterJoinCondition { + protected AslQuery leftOwner; + protected AslQuery rightOwner; + + public AslAbstractJoinCondition(AslQuery leftOwner, AslQuery rightOwner) { + this.leftOwner = leftOwner; + this.rightOwner = rightOwner; + } + + @Override + public AslQuery getLeftOwner() { + return leftOwner; + } + + @Override + public AslQuery getRightOwner() { + return rightOwner; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslAuditDetailsJoinCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslAuditDetailsJoinCondition.java new file mode 100644 index 000000000..18b0e3eaa --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslAuditDetailsJoinCondition.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.join; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; + +public final class AslAuditDetailsJoinCondition implements AslJoinCondition { + + private final AslQuery leftOwner; + private final AslStructureQuery rightOwner; + + public AslAuditDetailsJoinCondition(AslQuery leftOwner, AslStructureQuery rightOwner) { + this.leftOwner = leftOwner; + this.rightOwner = rightOwner; + } + + @Override + public AslQuery getLeftOwner() { + return leftOwner; + } + + @Override + public AslStructureQuery getRightOwner() { + return rightOwner; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslDelegatingJoinCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslDelegatingJoinCondition.java new file mode 100644 index 000000000..48272d088 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslDelegatingJoinCondition.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.join; + +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslProvidesJoinCondition; + +/** + * For contains and path joins + */ +public final class AslDelegatingJoinCondition extends AslAbstractJoinCondition { + + private final AslProvidesJoinCondition delegate; + + public AslDelegatingJoinCondition(AslProvidesJoinCondition delegate) { + super(delegate.getLeftOwner(), delegate.getRightOwner()); + this.delegate = delegate; + } + + public AslProvidesJoinCondition getDelegate() { + return delegate; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslJoin.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslJoin.java new file mode 100644 index 000000000..9acb34761 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslJoin.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.join; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.jooq.JoinType; + +public class AslJoin { + private final AslQuery left; + private final JoinType joinType; + private final AslQuery right; + private final List on; + + public AslJoin(AslQuery left, JoinType joinType, AslQuery right, List on) { + this.left = left; + this.joinType = joinType; + this.right = right; + this.on = new ArrayList<>(on); + } + + public AslJoin(AslQuery left, JoinType joinType, AslQuery right, AslJoinCondition... on) { + this(left, joinType, right, Arrays.asList(on)); + } + + public AslQuery getLeft() { + return left; + } + + public JoinType getJoinType() { + return joinType; + } + + public AslQuery getRight() { + return right; + } + + public List getOn() { + return on; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslJoinCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslJoinCondition.java new file mode 100644 index 000000000..b2edd306a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslJoinCondition.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.join; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public sealed interface AslJoinCondition permits AslAbstractJoinCondition, AslAuditDetailsJoinCondition { + AslQuery getLeftOwner(); + + AslQuery getRightOwner(); +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslPathFilterJoinCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslPathFilterJoinCondition.java new file mode 100644 index 000000000..436501bdd --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslPathFilterJoinCondition.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.join; + +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +public final class AslPathFilterJoinCondition extends AslAbstractJoinCondition { + + private AslQueryCondition condition; + + public AslPathFilterJoinCondition(AslQuery leftOwner, AslQueryCondition condition) { + super(leftOwner, null); + this.condition = condition; + } + + public AslQueryCondition getCondition() { + return condition; + } + + public void setCondition(AslQueryCondition condition) { + this.condition = condition; + } + + public AslPathFilterJoinCondition withLeftProvider(AslQuery provider) { + return new AslPathFilterJoinCondition(leftOwner, condition.withProvider(provider)); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslDataQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslDataQuery.java new file mode 100644 index 000000000..46df743aa --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslDataQuery.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import java.util.List; + +public abstract sealed class AslDataQuery extends AslQuery permits AslRmObjectDataQuery, AslPathDataQuery { + + private AslQuery base; + private final AslQuery baseProvider; + + protected AslDataQuery(String alias, AslQuery base, AslQuery baseProvider) { + super(alias, List.of()); + this.base = base; + this.baseProvider = baseProvider; + } + + public AslQuery getBase() { + return base; + } + + public void setBase(AslStructureQuery base) { + this.base = base; + } + + public AslQuery getBaseProvider() { + return baseProvider; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslEncapsulatingQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslEncapsulatingQuery.java new file mode 100644 index 000000000..c0d72972a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslEncapsulatingQuery.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoin; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; + +public sealed class AslEncapsulatingQuery extends AslQuery permits AslRootQuery { + private final List> children = new ArrayList<>(); + + public AslEncapsulatingQuery(String alias) { + super(alias, new ArrayList<>()); + } + + public List> getChildren() { + return children; + } + + public Pair getLastChild() { + if (this.children.isEmpty()) { + return null; + } + return this.children.get(this.children.size() - 1); + } + + public void addChild(AslQuery child, AslJoin join) { + this.children.add(Pair.of(child, join)); + } + + @Override + public Map> joinConditionsForFiltering() { + return children.stream() + .map(Pair::getLeft) + .map(AslQuery::joinConditionsForFiltering) + .map(Map::entrySet) + .flatMap(Set::stream) + .map(e -> Pair.of( + e.getKey(), + e.getValue().stream() + .map(jc -> jc.withLeftProvider(this)) + .toList())) + .collect(Collectors.groupingBy( + Pair::getKey, + LinkedHashMap::new, + Collectors.flatMapping(e -> e.getValue().stream(), Collectors.toList()))); + } + + @Override + public List getSelect() { + return children.stream() + .map(Pair::getLeft) + .map(AslQuery::getSelect) + .flatMap(List::stream) + .map(f -> f.withProvider(this)) + .toList(); + } + + public void addStructureCondition(AslQueryCondition condition) { + this.structureConditions.add(condition); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslFilteringQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslFilteringQuery.java new file mode 100644 index 000000000..d09171605 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslFilteringQuery.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; + +public final class AslFilteringQuery extends AslQuery { + + private final AslField sourceField; + private final AslField select; + + public AslFilteringQuery(String alias, AslField sourceField) { + super(alias, Collections.emptyList()); + this.sourceField = sourceField; + this.select = sourceField.copyWithOwner(this); + } + + @Override + public Map> joinConditionsForFiltering() { + return Collections.emptyMap(); + } + + @Override + public List getSelect() { + return List.of(select); + } + + public AslField getSourceField() { + return sourceField; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslPathDataQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslPathDataQuery.java new file mode 100644 index 000000000..65c6e6312 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslPathDataQuery.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.collections4.CollectionUtils; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslDvOrderedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField.FieldSource; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; + +public final class AslPathDataQuery extends AslDataQuery { + public static final String DATA_COLUMN_NAME = "data"; + + private final List dataPath; + private final AslColumnField dataField; + private final boolean multipleValued; + private final Set dvOrderedTypes; + + public AslPathDataQuery( + String alias, + AslQuery base, + AslQuery baseProvider, + List dataPath, + boolean multipleValued, + Set dvOrderedTypes, + Class fieldType) { + super(alias, base, baseProvider); + this.dvOrderedTypes = Collections.unmodifiableSet(dvOrderedTypes); + if (!(base instanceof AslStructureQuery || base instanceof AslPathDataQuery)) { + throw new IllegalArgumentException( + "%s is not a valid base for AslPathDataQuery".formatted(base.getClass())); + } + this.dataPath = dataPath; + FieldSource fieldSource = FieldSource.withOwner(this); + this.dataField = CollectionUtils.isEmpty(dvOrderedTypes) + ? new AslColumnField(fieldType, DATA_COLUMN_NAME, fieldSource, false) + : new AslDvOrderedColumnField(DATA_COLUMN_NAME, fieldSource, dvOrderedTypes); + this.multipleValued = multipleValued; + } + + public AslColumnField getDataField() { + return dataField; + } + + @Override + public Map> joinConditionsForFiltering() { + return Collections.emptyMap(); + } + + @Override + public List getSelect() { + return List.of(dataField); + } + + public List getPathNodes(AslColumnField field) { + if (field != dataField) { + throw new IllegalArgumentException("field is not part of this AslPathDataQuery"); + } + return dataPath; + } + + public boolean isMultipleValued() { + return multipleValued; + } + + public Set getDvOrderedTypes() { + return dvOrderedTypes; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslQuery.java new file mode 100644 index 000000000..54eb1075b --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslQuery.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslAndQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslOrQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; + +public abstract sealed class AslQuery + permits AslDataQuery, AslEncapsulatingQuery, AslFilteringQuery, AslStructureQuery { + protected List structureConditions; + private final String alias; + private AslQueryCondition condition; + + protected AslQuery(String alias, List structureConditions) { + this.alias = alias; + this.structureConditions = structureConditions; + } + + public abstract Map> joinConditionsForFiltering(); + + public abstract List getSelect(); + + public String getAlias() { + return alias; + } + + public AslQueryCondition getCondition() { + return condition; + } + + public void setCondition(AslQueryCondition condition) { + this.condition = condition; + } + + public AslQuery addConditionAnd(AslQueryCondition toAdd) { + if (this.condition == null) { + this.condition = toAdd; + } else if (this.condition instanceof AslAndQueryCondition and) { + and.getOperands().add(toAdd); + } else { + this.condition = new AslAndQueryCondition(condition, toAdd); + } + return this; + } + + public AslQuery addConditionOr(AslQueryCondition toAdd) { + if (this.condition == null) { + this.condition = toAdd; + } else if (this.condition instanceof AslOrQueryCondition or) { + or.getOperands().add(toAdd); + } else { + this.condition = new AslOrQueryCondition(condition, toAdd); + } + return this; + } + + public List getStructureConditions() { + return Collections.unmodifiableList(structureConditions); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslRmObjectDataQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslRmObjectDataQuery.java new file mode 100644 index 000000000..d0f3e5911 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslRmObjectDataQuery.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslDescendantCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField.FieldSource; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.jooq.JSONB; + +/** + *
+ *   select
+ * 	  jsonb_object_agg(
+ * 	( sub_string(d2."entity_idx" FROM char_length(c2."entity_idx") + 1)
+ * 	), "data"
+ * 	) as "data"
+ *     from "ehr"."comp_one" d2
+ * 	  where
+ *       c2."ehr_id" = "d2"."ehr_id"
+ *       and c2."VO_ID" = "d2"."VO_ID"
+ *       and c2."num" <= "d2"."num"
+ *       and c2."num_cap" >= "d2"."num"
+ * 	  group by "d2"."VO_ID"
+ * 	 
+ * + * @see AslDescendantCondition + */ +public final class AslRmObjectDataQuery extends AslDataQuery { + private final AslField field; + + public AslRmObjectDataQuery(String alias, AslStructureQuery base, AslQuery baseProvider) { + super(alias, base, baseProvider); + this.field = new AslColumnField(JSONB.class, "data", FieldSource.withOwner(this), false); + } + + @Override + public Map> joinConditionsForFiltering() { + return Collections.emptyMap(); + } + + @Override + public List getSelect() { + return List.of(field); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslRootQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslRootQuery.java new file mode 100644 index 000000000..9a8c3e1b6 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslRootQuery.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslDvOrderedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslOrderByField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.jooq.SortOrder; + +public final class AslRootQuery extends AslEncapsulatingQuery { + + private final List fields = new ArrayList<>(); + + private final List orderByFields = new ArrayList<>(); + private final List groupByFields = new ArrayList<>(); + private final List groupByDvOrderedMagnitudeFields = new ArrayList<>(); + private Long limit; + private Long offset; + + public AslRootQuery() { + super(null); + } + + public List getSelect() { + return fields; + } + + /** + * @return all field known to the subqueries + */ + public List getAvailableFields() { + return super.getSelect(); + } + + public Long getLimit() { + return limit; + } + + public void setLimit(Long limit) { + this.limit = limit; + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } + + public List getOrderByFields() { + return orderByFields; + } + + @Override + public Map> joinConditionsForFiltering() { + throw new UnsupportedOperationException(); + } + + public List getGroupByFields() { + return groupByFields; + } + + public List getGroupByDvOrderedMagnitudeFields() { + return groupByDvOrderedMagnitudeFields; + } + + public void addOrderBy(AslField field, SortOrder sortOrder, boolean usesAggregateFunctionOrDistinct) { + getOrderByFields().add(new AslOrderByField(field, sortOrder)); + + field.fieldsForAggregation(this).forEach(f -> { + if (usesAggregateFunctionOrDistinct && !getGroupByFields().contains(f)) { + if (field instanceof AslDvOrderedColumnField df) { + getGroupByDvOrderedMagnitudeFields().add(df); + } else { + getGroupByFields().add(f); + } + } + }); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslStructureQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslStructureQuery.java new file mode 100644 index 000000000..2a45418e5 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/query/AslStructureQuery.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model.query; + +import static org.ehrbase.jooq.pg.Tables.COMP_DATA; +import static org.ehrbase.jooq.pg.Tables.COMP_VERSION; +import static org.ehrbase.jooq.pg.Tables.EHR_; +import static org.ehrbase.jooq.pg.Tables.EHR_FOLDER_DATA; +import static org.ehrbase.jooq.pg.Tables.EHR_FOLDER_VERSION; +import static org.ehrbase.jooq.pg.Tables.EHR_STATUS_DATA; +import static org.ehrbase.jooq.pg.Tables.EHR_STATUS_VERSION; + +import com.nedap.archie.rm.archetyped.Locatable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.ehrbase.jooq.pg.Tables; +import org.ehrbase.openehr.aqlengine.asl.AslUtils; +import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField.FieldSource; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.dbformat.RmAttributeAlias; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.dbformat.StructureRoot; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.jooq.Table; +import org.jooq.TableField; + +/** + *
+ * select
+ *       "sCOMPOSITIONsq"."vo_id" as "sCOMPOSITIONc0_vo_id",
+ *       "sCOMPOSITIONsq"."ehr_id" as "sCOMPOSITIONc0_ehr_id",
+ *       "sCOMPOSITIONsq"."parent_num" as "sCOMPOSITIONc0_parent_num",
+ *       "sCOMPOSITIONsq"."num" as "sCOMPOSITIONc0_num",
+ *       "sCOMPOSITIONsq"."num_cap" as "sCOMPOSITIONc0_num_cap",
+ *       "sCOMPOSITIONsq"."entity_idx" as "sCOMPOSITIONc0_entity_idx",
+ *       "sCOMPOSITIONsq"."entity_idx_len" as "sCOMPOSITIONc0_entity_idx_len",
+ *       "sCOMPOSITIONsq"."entity_concept" as "sCOMPOSITIONc0_entity_concept",
+ *       "sCOMPOSITIONsq"."entity_name" as "sCOMPOSITIONc0_entity_name",
+ *       "sCOMPOSITIONsq"."rm_entity" as "sCOMPOSITIONc0_rm_entity"
+ *     from "ehr"."comp" as "sCOMPOSITIONsq"
+ *     where (
+ *       (and other-predicates)
+ *     )
+ *     
+ */ +public final class AslStructureQuery extends AslQuery { + + public static final String ENTITY_ATTRIBUTE = "entity_attribute"; + + public boolean isRequiresVersionTableJoin() { + return requiresVersionTableJoin; + } + + public boolean isRepresentsOriginalVersionExpression() { + return representsOriginalVersionExpression; + } + + public void setRepresentsOriginalVersionExpression(boolean representsOriginalVersionExpression) { + this.representsOriginalVersionExpression = representsOriginalVersionExpression; + } + + public enum AslSourceRelation { + EHR(StructureRoot.EHR, null, EHR_), + EHR_STATUS(StructureRoot.EHR_STATUS, EHR_STATUS_VERSION, EHR_STATUS_DATA), + COMPOSITION(StructureRoot.COMPOSITION, COMP_VERSION, COMP_DATA), + FOLDER(StructureRoot.FOLDER, EHR_FOLDER_VERSION, EHR_FOLDER_DATA), + AUDIT_DETAILS(null, null, Tables.AUDIT_DETAILS); + + private static final Map BY_STRUCTURE_ROOT = + new EnumMap<>(StructureRoot.class); + + private final StructureRoot structureRoot; + private final Table versionTable; + private final Table dataTable; + + private final List> pkeyFields; + + AslSourceRelation(StructureRoot structureRoot, Table versionTable, Table dataTable) { + this.structureRoot = structureRoot; + this.versionTable = versionTable; + this.dataTable = dataTable; + this.pkeyFields = List.of(ObjectUtils.firstNonNull(versionTable, dataTable) + .getPrimaryKey() + .getFieldsArray()); + } + + public StructureRoot getStructureRoot() { + return structureRoot; + } + + public Table getVersionTable() { + return versionTable; + } + + public Table getDataTable() { + return dataTable; + } + + public List> getPkeyFields() { + return pkeyFields; + } + + static { + for (AslSourceRelation value : values()) { + if (value.structureRoot != null) { + BY_STRUCTURE_ROOT.put(value.structureRoot, value); + } + } + } + + public static AslSourceRelation get(StructureRoot structureRoot) { + return BY_STRUCTURE_ROOT.get(structureRoot); + } + } + + private static final Set NON_LOCATABLE_STRUCTURE_RM_TYPES = Arrays.stream(StructureRmType.values()) + .filter(StructureRmType::isStructureEntry) + .filter(s -> !Locatable.class.isAssignableFrom(s.type)) + .map(StructureRmType::getAlias) + .collect(Collectors.toSet()); + + private final Map joinConditionsForFiltering = new HashMap<>(); + private final AslSourceRelation type; + private final Collection rmTypes; + private final List fields = new ArrayList<>(); + private final String alias; + private final boolean requiresVersionTableJoin; + private boolean representsOriginalVersionExpression = false; + + public AslStructureQuery( + String alias, + AslSourceRelation type, + List fields, + Collection rmTypes, + Collection rmTypesConstraint, + String attribute, + boolean requiresVersionTableJoin) { + super(alias, new ArrayList<>()); + this.type = type; + this.rmTypes = List.copyOf(rmTypes); + this.requiresVersionTableJoin = requiresVersionTableJoin; + fields.forEach(this::addField); + this.alias = alias; + if (type != AslSourceRelation.EHR && type != AslSourceRelation.AUDIT_DETAILS) { + if (!rmTypes.isEmpty()) { + List aliasedRmTypes = rmTypes.stream() + .map(StructureRmType::getAliasOrTypeName) + .toList(); + if (NON_LOCATABLE_STRUCTURE_RM_TYPES.containsAll(aliasedRmTypes)) { + this.structureConditions.add(new AslFieldValueQueryCondition( + AslUtils.findFieldForOwner(AslStructureColumn.ENTITY_CONCEPT, this.getSelect(), this), + AslConditionOperator.IS_NULL, + List.of())); + } + } + if (!rmTypesConstraint.isEmpty()) { + List aliasedRmTypes = rmTypesConstraint.stream() + .map(StructureRmType::getAliasOrTypeName) + .toList(); + this.structureConditions.add(new AslFieldValueQueryCondition( + AslUtils.findFieldForOwner(AslStructureColumn.RM_ENTITY, this.getSelect(), this), + AslConditionOperator.IN, + aliasedRmTypes)); + } + if (StringUtils.isNotBlank(attribute)) { + this.structureConditions.add(new AslFieldValueQueryCondition( + new AslColumnField(String.class, ENTITY_ATTRIBUTE, FieldSource.withOwner(this), false), + AslConditionOperator.EQ, + List.of(RmAttributeAlias.getAlias(attribute)))); + } + } + } + + public Collection getRmTypes() { + return rmTypes; + } + + private void addField(AslField aslField) { + fields.add(aslField.withOwner(this)); + } + + @Override + public Map> joinConditionsForFiltering() { + return joinConditionsForFiltering.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> List.of(e.getValue()))); + } + + public void addJoinConditionForFiltering(IdentifiedPath ip, AslQueryCondition condition) { + this.joinConditionsForFiltering.put(ip, new AslPathFilterJoinCondition(this, condition)); + } + + @Override + public List getSelect() { + return fields; + } + + @Override + public String getAlias() { + return alias; + } + + public AslSourceRelation getType() { + return type; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/package-info.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/package-info.java new file mode 100644 index 000000000..d9cd2af97 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/package-info.java @@ -0,0 +1,4 @@ +/** + *

AQL to SQL Layer + */ +package org.ehrbase.openehr.aqlengine.asl; diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheck.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheck.java new file mode 100644 index 000000000..1a8e1af4b --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheck.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import org.ehrbase.api.service.SystemService; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.springframework.stereotype.Component; + +@Component +public final class AqlQueryFeatureCheck { + + private final FeatureCheck[] featureChecks; + + public AqlQueryFeatureCheck(SystemService systemService) { + this.featureChecks = new FeatureCheck[] { + new FromCheck(systemService), + new SelectCheck(systemService), + new WhereCheck(systemService), + new OrderByCheck(systemService) + }; + } + + public void ensureQuerySupported(AqlQuery aqlQuery) { + for (FeatureCheck featureCheck : featureChecks) { + featureCheck.ensureSupported(aqlQuery); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/ClauseType.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/ClauseType.java new file mode 100644 index 000000000..d3834b9e9 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/ClauseType.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +enum ClauseType { + SELECT, + WHERE, + FROM_PREDICATE, + ORDER_BY +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FeatureCheck.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FeatureCheck.java new file mode 100644 index 000000000..293a4d537 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FeatureCheck.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; + +interface FeatureCheck { + void ensureSupported(AqlQuery aqlQuery); +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FeatureCheckUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FeatureCheckUtils.java new file mode 100644 index 000000000..dc0842cee --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FeatureCheckUtils.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import com.nedap.archie.rm.datavalues.quantity.DvOrdered; +import com.nedap.archie.rminfo.ArchieRMInfoLookup; +import com.nedap.archie.rminfo.RMTypeInfo; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept; +import org.ehrbase.openehr.aqlengine.pathanalysis.ANode; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathAnalysis; +import org.ehrbase.openehr.dbformat.AncestorStructureRmType; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentVersionExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.NullPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.render.AqlRenderer; +import org.ehrbase.openehr.sdk.aql.util.AqlUtil; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; + +final class FeatureCheckUtils { + public static final ArchieRMInfoLookup RM_INFO_LOOKUP = ArchieRMInfoLookup.getInstance(); + private static final Set DV_ORDERED_TYPES = + RM_INFO_LOOKUP.getTypeInfo(DvOrdered.class).getAllDescendantClasses().stream() + .filter(t -> !Modifier.isAbstract(t.getJavaClass().getModifiers())) + .map(RMTypeInfo::getRmName) + .collect(Collectors.toSet()); + private static final Pattern OBJECT_VERSION_ID_REGEX = + Pattern.compile("([a-fA-F0-9-]{36})(::([^:]*)::([1-9]\\d*))?"); + + // TODO performance: change data structure EnumMap> ; Set> ; Index ClauseType, + // path... + private static final List, Set>> SUPPORTED_VERSION_PATHS = Stream.of( + Pair.of("uid/value", Set.of(ClauseType.SELECT, ClauseType.WHERE, ClauseType.ORDER_BY)), + Pair.of( + "commit_audit/time_committed", + Set.of(ClauseType.SELECT, ClauseType.WHERE, ClauseType.ORDER_BY)), + Pair.of("commit_audit/time_committed/value", Set.of(ClauseType.SELECT)), + Pair.of("commit_audit/system_id", Set.of(ClauseType.SELECT, ClauseType.WHERE)), + Pair.of("commit_audit/description", Set.of(ClauseType.SELECT)), + Pair.of( + "commit_audit/description/value", + Set.of(ClauseType.SELECT, ClauseType.WHERE, ClauseType.ORDER_BY)), + Pair.of("commit_audit/change_type", Set.of(ClauseType.SELECT)), + Pair.of( + "commit_audit/change_type/value", + Set.of(ClauseType.SELECT, ClauseType.WHERE, ClauseType.ORDER_BY)), + Pair.of( + "commit_audit/change_type/defining_code/code_string", + Set.of(ClauseType.SELECT, ClauseType.WHERE, ClauseType.ORDER_BY)), + Pair.of( + "commit_audit/change_type/defining_code/preferred_term", + Set.of(ClauseType.SELECT, ClauseType.WHERE, ClauseType.ORDER_BY)), + Pair.of( + "commit_audit/change_type/defining_code/terminology_id/value", + Set.of(ClauseType.SELECT, ClauseType.WHERE)), + Pair.of("contribution/id/value", Set.of(ClauseType.SELECT, ClauseType.WHERE, ClauseType.ORDER_BY))) + .map(p -> Pair.of(Arrays.asList(p.getLeft().split("/")), p.getRight())) + .toList(); + + record PathDetails(AslExtractedColumn extractedColumn, Set targetTypes) { + public boolean targetsDvOrdered() { + return targetTypes.stream().anyMatch(DV_ORDERED_TYPES::contains); + } + + public boolean targetsPrimitive() { + return targetTypes.stream().map(RM_INFO_LOOKUP::getTypeInfo).anyMatch(Objects::isNull); + } + } + + private FeatureCheckUtils() {} + + public static boolean startsWith(IdentifiedPath successor, IdentifiedPath predecessor) { + if (successor == predecessor) { + return true; + } + if (successor == null || predecessor == null) { + return false; + } + if (!Objects.equals(successor.getRoot(), predecessor.getRoot())) { + return false; + } + if (!Objects.equals(successor.getRootPredicate(), predecessor.getRootPredicate())) { + return false; + } + + List successorPathNodes = Optional.of(successor) + .map(IdentifiedPath::getPath) + .map(AqlObjectPath::getPathNodes) + .orElse(List.of()); + List predecessorPathNodes = Optional.of(predecessor) + .map(IdentifiedPath::getPath) + .map(AqlObjectPath::getPathNodes) + .orElse(List.of()); + int predecessorSize = predecessorPathNodes.size(); + if (successorPathNodes.size() < predecessorSize) { + return false; + } + return predecessorPathNodes.equals(successorPathNodes.subList(0, predecessorSize)); + } + + private static void ensurePathPredicateSupported( + AqlObjectPath path, String nodeType, List predicate, String systemId) { + AqlUtil.streamPredicates(predicate).forEach(p -> { + Optional extractedColumn = AslExtractedColumn.find(nodeType, p.getPath()); + if (extractedColumn.isEmpty()) { + throw new AqlFeatureNotImplementedException("Path predicate %s in path %s contains unsupported path %s" + .formatted(AqlRenderer.renderPredicate(predicate), path, p.getPath())); + } + if (extractedColumn.get() == AslExtractedColumn.ARCHETYPE_NODE_ID + && !EnumSet.of( + ComparisonOperatorPredicate.PredicateComparisonOperator.EQ, + ComparisonOperatorPredicate.PredicateComparisonOperator.NEQ) + .contains(p.getOperator())) { + throw new AqlFeatureNotImplementedException("Predicates on 'archetype_node_id' only support = and !="); + } + ensureOperandSupported(new PathDetails(extractedColumn.get(), Set.of(nodeType)), p.getValue(), systemId); + }); + } + + public static PathDetails findSupportedIdentifiedPath( + IdentifiedPath ip, boolean allowEmpty, ClauseType clauseType, String systemId) { + AqlObjectPath path = ip.getPath(); + AbstractContainmentExpression root = ip.getRoot(); + String containmentType = + switch (root) { + case ContainmentClassExpression cce -> cce.getType(); + case ContainmentVersionExpression __ -> RmConstants.ORIGINAL_VERSION; + }; + boolean isVersionPath = RmConstants.ORIGINAL_VERSION.equals(containmentType); + if (path == null) { + if (allowEmpty) { + if (isVersionPath) { + throw new AqlFeatureNotImplementedException( + "selecting the full VERSION object (%s)".formatted(root.getIdentifier())); + } + if (RmConstants.EHR.equals(containmentType)) { + throw new AqlFeatureNotImplementedException( + "selecting the full EHR object (%s)".formatted(root.getIdentifier())); + } + return new PathDetails( + null, + AncestorStructureRmType.byTypeName(containmentType) + .map(AncestorStructureRmType::getDescendants) + .map(s -> s.stream().map(StructureRmType::name).collect(Collectors.toSet())) + .orElse(Set.of(containmentType))); + } else { + throw new AqlFeatureNotImplementedException( + "%s: identified path for type %s is missing".formatted(clauseType, containmentType)); + } + } + + if (RmConstants.EHR.equals(containmentType)) { + return AslExtractedColumn.find(containmentType, path) + .filter(ec -> !EnumSet.of(AslExtractedColumn.EHR_TIME_CREATED, AslExtractedColumn.EHR_SYSTEM_ID_DV) + .contains(ec) + || clauseType == ClauseType.SELECT) + .map(ec -> new PathDetails(ec, Set.of("String"))) + .orElseThrow(() -> + new AqlFeatureNotImplementedException("%s: identified path '%s' for type %s not supported" + .formatted(clauseType, path.render(), containmentType))); + } + + List pathAttributes = path.getPathNodes().stream() + .map(AqlObjectPath.PathNode::getAttribute) + .toList(); + // if VERSION check supported paths list first + if (isVersionPath + && SUPPORTED_VERSION_PATHS.stream() + .filter(p -> p.getRight().contains(clauseType)) + .map(Pair::getLeft) + .noneMatch(p -> p.equals(pathAttributes))) { + throw new AqlFeatureNotImplementedException("%s: VERSION path %s/%s is not supported" + .formatted(clauseType, root.getIdentifier(), path.render())); + } + + int level = -1; + ANode analyzed = PathAnalysis.analyzeAqlPathTypes( + containmentType, ip.getRootPredicate(), root.getPredicates(), path, null); + if (analyzed.getCandidateTypes().isEmpty()) { + throw new IllegalAqlException("%s is not a valid RM path".formatted(ip.render())); + } + Map> attributeInfos = PathAnalysis.createAttributeInfos(analyzed); + + Set targetTypes = new HashSet<>(); + Set parentTargetTypes = AncestorStructureRmType.byTypeName(containmentType) + .map(AncestorStructureRmType::getDescendants) + .map(s -> s.stream().map(StructureRmType::name).collect(Collectors.toSet())) + .orElse(Set.of(containmentType)); + final List pathNodes = path.getPathNodes(); + for (int i = 0; i < pathNodes.size(); i++) { + AqlObjectPath.PathNode pathNode = pathNodes.get(i); + String attribute = pathAttributes.get(i); + ANode analyzedParent = analyzed; + analyzed = analyzed.getAttribute(attribute); + level++; + targetTypes = attributeInfos.get(analyzedParent).get(attribute).targetTypes().stream() + .filter(t -> + !isVersionPath || !attribute.equals("commit_audit") || RmConstants.AUDIT_DETAILS.equals(t)) + .collect(Collectors.toSet()); + Set categories = analyzed.getCategories(); + if (categories.contains(ANode.NodeCategory.STRUCTURE_INTERMEDIATE)) { + throw new AqlFeatureNotImplementedException("%s: path %s contains STRUCTURE_INTERMEDIATE attribute %s" + .formatted(clauseType, path.render(), attribute)); + } + + if (clauseType == ClauseType.WHERE + && i == pathNodes.size() - 1 + && targetTypes.stream() + .map(RM_INFO_LOOKUP::getTypeInfo) + .noneMatch(t -> t == null || DV_ORDERED_TYPES.contains(t.getRmName()))) { + throw new AqlFeatureNotImplementedException( + "%s: path %s only targets types that are not derived from DV_ORDERED and not primitive" + .formatted(clauseType, path.render())); + } + if (categories.size() != 1 || Collections.disjoint(categories, Set.of(ANode.NodeCategory.STRUCTURE))) { + // (path ends with) extracted column? + AqlObjectPath subPath = new AqlObjectPath( + path.getPathNodes().stream().skip(level).toList()); + + final Set currentParentTargetTypes = parentTargetTypes; + Optional extractedColumn = AslExtractedColumn.find( + currentParentTargetTypes.iterator().next(), subPath) + .filter(ec -> ec.getAllowedRmTypes().containsAll(currentParentTargetTypes)); + + if (extractedColumn.isEmpty()) { + List condition = pathNode.getPredicateOrOperands(); + if (AqlUtil.streamPredicates(condition).findAny().isPresent()) { + throw new AqlFeatureNotImplementedException( + "%s: path %s contains a non-structure attribute (%s) with at least one predicate" + .formatted(clauseType, path.render(), attribute)); + } + } else { + List nodes = subPath.getPathNodes(); + for (int j = 1; j < nodes.size(); j++) { + AqlObjectPath.PathNode node = nodes.get(j); + analyzedParent = analyzed; + analyzed = analyzed.getAttribute(node.getAttribute()); + targetTypes = attributeInfos + .get(analyzedParent) + .get(node.getAttribute()) + .targetTypes(); + } + return new PathDetails(extractedColumn.get(), targetTypes); + } + } + targetTypes.forEach( + t -> ensurePathPredicateSupported(path, t, pathNode.getPredicateOrOperands(), systemId)); + parentTargetTypes = targetTypes; + } + + return new PathDetails(null, targetTypes); + } + + public static void ensureOperandSupported(PathDetails pathWithType, Object operand, String systemId) { + if (!(operand instanceof Primitive)) { + throw new AqlFeatureNotImplementedException("Only primitive operands are supported"); + } + if (operand instanceof NullPrimitive) { + throw new AqlFeatureNotImplementedException("NULL is not supported"); + } + if (pathWithType.extractedColumn() == AslExtractedColumn.VO_ID) { + if (!(operand instanceof StringPrimitive sp)) { + throw new IllegalAqlException("/uid/value comparisons require a string operand"); + } + String value = sp.getValue(); + Matcher matcher = OBJECT_VERSION_ID_REGEX.matcher(value); + if (!matcher.matches()) { + throw new IllegalAqlException("%s is not a valid OBJECT_VERSION_ID/UID".formatted(value)); + } + try { + // Check syntax + UUID.fromString(matcher.group(1)); + } catch (IllegalArgumentException e) { + throw new IllegalAqlException("%s does not start with a valid UID".formatted(value)); + } + if (matcher.group(2) != null) { + String system = matcher.group(3); + if (StringUtils.isNotEmpty(system) && !system.equals(systemId)) { + throw new IllegalAqlException( + "CREATING_SYSTEM_ID of %s does not match this server (%s)".formatted(value, systemId)); + } + } + } else if (pathWithType.extractedColumn() == AslExtractedColumn.ARCHETYPE_NODE_ID) { + if (!(operand instanceof StringPrimitive sp)) { + throw new IllegalAqlException("%s comparisons require a string operand" + .formatted( + AslExtractedColumn.ARCHETYPE_NODE_ID.getPath().render())); + } + try { + // Check syntax & type support + AslRmTypeAndConcept.fromArchetypeNodeId(sp.getValue()); + } catch (IllegalArgumentException e) { + throw new IllegalAqlException(e); + } + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FromCheck.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FromCheck.java new file mode 100644 index 000000000..98c2f1247 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FromCheck.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.api.service.SystemService; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.dbformat.AncestorStructureRmType; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.dbformat.StructureRoot; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.Containment; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentNotOperator; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperator; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentVersionExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.util.AqlUtil; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; + +final class FromCheck implements FeatureCheck { + + private final SystemService systemService; + + public FromCheck(SystemService systemService) { + this.systemService = systemService; + } + + @Override + public void ensureSupported(AqlQuery aqlQuery) { + Containment currentContainment = aqlQuery.getFrom(); + if (currentContainment == null) { + throw new AqlFeatureNotImplementedException("FROM must be specified"); + } + if (currentContainment instanceof ContainmentClassExpression fc && RmConstants.EHR.equals(fc.getType())) { + currentContainment = fc.getContains(); + } else if (!(currentContainment instanceof AbstractContainmentExpression)) { + throw new AqlFeatureNotImplementedException("AND/OR/NOT only allowed after CONTAINS"); + } + + // remaining CONTAINS + ensureContainmentSupported(currentContainment, null); + + // predicates in FROM + AqlUtil.streamContainments(aqlQuery.getFrom()).forEach(this::ensureContainmentPredicateSupported); + } + + private static Pair ensureStructureContainsSupported( + ContainmentClassExpression nextContainment, StructureRoot structure) { + + Set structureRmTypes = StructureRmType.byTypeName(nextContainment.getType()) + .map(Set::of) + .or(() -> ensureAbstractStructureContainsSupported(nextContainment, structure) + .map(AncestorStructureRmType::getDescendants)) + .orElseThrow(() -> cremateUnsupportedType(nextContainment)); + + if (CollectionUtils.containsAny(structureRmTypes, EnumSet.of(StructureRmType.FOLDER))) { + throw new AqlFeatureNotImplementedException( + "CONTAINS %s is not supported".formatted(nextContainment.getType())); + } + + if (!structureRmTypes.stream().allMatch(StructureRmType::isStructureEntry)) { + throw new AqlFeatureNotImplementedException( + "CONTAINS %s is currently not supported".formatted(nextContainment.getType())); + } + + if (structure == null + && structureRmTypes.stream() + .map(StructureRmType::getStructureRoot) + .anyMatch(Objects::isNull)) { + throw new IllegalAqlException( + "It is unclear if %s targets a COMPOSITION or EHR_STATUS".formatted(nextContainment.getType())); + } + + StructureRoot structureRoot = structureRmTypes.stream() + .map(StructureRmType::getStructureRoot) + .collect(Collectors.reducing((a, b) -> a == b ? a : null)) + .orElse(null); + + return Pair.of(nextContainment.getContains(), structureRoot); + } + + private static IllegalAqlException cremateUnsupportedType(ContainmentClassExpression nextContainment) { + return new IllegalAqlException("Type %s is not supported in FROM, only: EHR, %s" + .formatted( + nextContainment.getType(), + Stream.of( + Arrays.stream(AncestorStructureRmType.values()) + .filter(at -> at.getNonStructureDescendants() + .isEmpty()) + .filter(at -> at.getDescendants().stream() + .allMatch(StructureRmType::isStructureEntry)), + Arrays.stream(StructureRmType.values()) + .filter(StructureRmType::isStructureEntry)) + .flatMap(s -> s) + .map(Enum::name) + .collect(Collectors.joining(", ")))); + } + + private static void ensureContainmentSupported(Containment c, final StructureRoot parentStructure) { + switch (c) { + case null -> { + /*NOOP*/ + } + case ContainmentClassExpression cce -> { + var next = ensureStructureContainsSupported(cce, parentStructure); + StructureRoot structureRoot = + Optional.of(next).map(Pair::getRight).orElse(parentStructure); + ensureContainmentSupported(next.getLeft(), structureRoot); + + ensureContainmentStructureSupported(parentStructure, cce, structureRoot); + } + case ContainmentVersionExpression cve -> ensureVersionContaimentSupported(cve); + case ContainmentSetOperator cso -> cso.getValues() + .forEach(nc -> ensureContainmentSupported(nc, parentStructure)); + case ContainmentNotOperator __ -> throw new AqlFeatureNotImplementedException("NOT CONTAINS"); + default -> throw new IllegalAqlException( + "Unknown containment type: %s".formatted(c.getClass().getSimpleName())); + } + } + + private static void ensureVersionContaimentSupported(ContainmentVersionExpression cve) { + Containment nextContainment = cve.getContains(); + if (nextContainment == null) { + throw new IllegalAqlException("VERSION containment must be followed by another CONTAINS expression"); + } + if (nextContainment instanceof ContainmentVersionExpression) { + throw new IllegalAqlException("VERSION cannot contain another VERSION"); + } + if (nextContainment instanceof ContainmentSetOperator || nextContainment instanceof ContainmentNotOperator) { + throw new AqlFeatureNotImplementedException("AND/OR/NOT operator as next containment after VERSION"); + } + ensureContainmentSupported(nextContainment, null); + } + + private static void ensureContainmentStructureSupported( + StructureRoot parentStructure, ContainmentClassExpression cce, StructureRoot structure) { + boolean containmentStructureSupported = + switch (parentStructure) { + case null -> structure != null; + case FOLDER -> structure == StructureRoot.FOLDER || structure == StructureRoot.COMPOSITION; + case COMPOSITION, EHR_STATUS -> parentStructure == structure; + default -> throw new RuntimeException("%s is not root structure".formatted(parentStructure)); + }; + if (!containmentStructureSupported) { + throw new IllegalAqlException("Structure %s cannot CONTAIN %s (of structure %s)" + .formatted( + Optional.ofNullable(parentStructure) + .map(Object::toString) + .orElse(RmConstants.EHR), + cce.getType(), + structure)); + } + } + + private void ensureContainmentPredicateSupported(AbstractContainmentExpression containment) { + if (containment instanceof ContainmentVersionExpression cve) { + ContainmentVersionExpression.VersionPredicateType pType = cve.getVersionPredicateType(); + if (pType != ContainmentVersionExpression.VersionPredicateType.LATEST_VERSION + && pType != ContainmentVersionExpression.VersionPredicateType.NONE) { + throw new AqlFeatureNotImplementedException( + "Only VERSION queries without predicate or on LATEST_VERSION supported"); + } + } + if (containment.hasPredicates()) { + List condition = containment.getPredicates(); + AqlUtil.streamPredicates(condition).forEach(predicate -> { + IdentifiedPath identifiedPath = new IdentifiedPath(); + identifiedPath.setRoot(containment); + identifiedPath.setPath(predicate.getPath()); + FeatureCheckUtils.PathDetails pathWithType = FeatureCheckUtils.findSupportedIdentifiedPath( + identifiedPath, false, ClauseType.FROM_PREDICATE, systemService.getSystemId()); + if (identifiedPath.getPath().equals(AslExtractedColumn.ARCHETYPE_NODE_ID.getPath()) + && !EnumSet.of( + ComparisonOperatorPredicate.PredicateComparisonOperator.EQ, + ComparisonOperatorPredicate.PredicateComparisonOperator.NEQ) + .contains(predicate.getOperator())) { + throw new AqlFeatureNotImplementedException( + "Predicates on 'archetype_node_id' only support = and !="); + } + FeatureCheckUtils.ensureOperandSupported( + pathWithType, predicate.getValue(), systemService.getSystemId()); + }); + } + } + + private static Optional ensureAbstractStructureContainsSupported( + ContainmentClassExpression nextContainment, final StructureRoot structure) { + Optional abstractType = AncestorStructureRmType.byTypeName(nextContainment.getType()); + + abstractType.ifPresent(at -> { + if (structure == null && at.getStructureRoot() == null) { + throw new IllegalAqlException( + "It is unclear if %s targets a COMPOSITION or EHR_STATUS".formatted(nextContainment.getType())); + } else if (!at.getNonStructureDescendants().isEmpty()) { + throw new AqlFeatureNotImplementedException( + "CONTAINS %s: abstract type with non structure descendants (%s) not yet supported" + .formatted( + nextContainment.getType(), + at.getNonStructureDescendants().stream() + .map(Class::getSimpleName) + .toList())); + } + }); + + return abstractType; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/OrderByCheck.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/OrderByCheck.java new file mode 100644 index 000000000..9f6c5b80a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/OrderByCheck.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.service.SystemService; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression; +import org.ehrbase.openehr.sdk.aql.dto.select.SelectExpression; +import org.ehrbase.openehr.sdk.aql.render.AqlRenderer; + +final class OrderByCheck implements FeatureCheck { + private final SystemService systemService; + + public OrderByCheck(SystemService systemService) { + this.systemService = systemService; + } + + @Override + public void ensureSupported(AqlQuery aqlQuery) { + Optional.of(aqlQuery).map(AqlQuery::getOrderBy).stream() + .flatMap(List::stream) + .map(OrderByExpression::getStatement) + .forEach(ip -> ensureOrderByStatementSupported(aqlQuery, ip)); + } + + private void ensureOrderByStatementSupported(AqlQuery aqlQuery, IdentifiedPath ip) { + + // find fields not present in SELECT + if (aqlQuery.getSelect().getStatement().stream() + .map(SelectExpression::getColumnExpression) + .filter(IdentifiedPath.class::isInstance) + .map(IdentifiedPath.class::cast) + .noneMatch(selected -> FeatureCheckUtils.startsWith(selected, ip))) { + throw new AqlFeatureNotImplementedException("ORDER BY: Path: %s%s/%s is not present in SELECT statement" + .formatted( + ip.getRoot().getIdentifier(), + ip.getRootPredicate() == null ? "" : AqlRenderer.renderPredicate(ip.getRootPredicate()), + ip.getPath().render())); + } + FeatureCheckUtils.PathDetails pathWithType = FeatureCheckUtils.findSupportedIdentifiedPath( + ip, false, ClauseType.ORDER_BY, systemService.getSystemId()); + if (EnumSet.of( + AslExtractedColumn.OV_TIME_COMMITTED, + AslExtractedColumn.AD_SYSTEM_ID, + AslExtractedColumn.AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE) + .contains(pathWithType.extractedColumn())) { + throw new AqlFeatureNotImplementedException( + "ORDER BY: Path: %s on VERSION".formatted(ip.getPath().render())); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/SelectCheck.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/SelectCheck.java new file mode 100644 index 000000000..946e88898 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/SelectCheck.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import java.util.EnumSet; +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.api.service.SystemService; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.CountDistinctAggregateFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; + +final class SelectCheck implements FeatureCheck { + private final SystemService systemService; + + public SelectCheck(SystemService systemService) { + this.systemService = systemService; + } + + @Override + public void ensureSupported(AqlQuery aqlQuery) { + // SELECT + var select = aqlQuery.getSelect(); + + select.getStatement().forEach(selectExp -> { + switch (selectExp.getColumnExpression()) { + case IdentifiedPath ip -> ensureSelectPathSupported(ip); + case AggregateFunction af -> ensureAggregateFunctionSupported(af); + case Primitive __ -> { + // Primitives are allowed + } + default -> throw new AqlFeatureNotImplementedException("%s is not supported in SELECT" + .formatted(selectExp.getClass().getSimpleName())); + } + }); + } + + private void ensureAggregateFunctionSupported(AggregateFunction af) { + AggregateFunction.AggregateFunctionName func = af.getFunctionName(); + IdentifiedPath ip = af.getIdentifiedPath(); + if (ip == null) { + // These check for invalid AQL -> IllegalAqlException + if (func != AggregateFunction.AggregateFunctionName.COUNT) { + throw new IllegalAqlException( + "Aggregate function %s requires an identified path argument.".formatted(func)); + } else if (af instanceof CountDistinctAggregateFunction) { + throw new IllegalAqlException("COUNT(DISTINCT) requires an identified path argument"); + } + } else { + AbstractContainmentExpression containment = ip.getRoot(); + FeatureCheckUtils.PathDetails pathWithType = FeatureCheckUtils.findSupportedIdentifiedPath( + ip, true, ClauseType.SELECT, systemService.getSystemId()); + if (func != AggregateFunction.AggregateFunctionName.COUNT) { + if (pathWithType.extractedColumn() != null + && !EnumSet.of( + AslExtractedColumn.OV_TIME_COMMITTED, + AslExtractedColumn.OV_TIME_COMMITTED_DV, + AslExtractedColumn.EHR_TIME_CREATED, + AslExtractedColumn.EHR_TIME_CREATED_DV) + .contains(pathWithType.extractedColumn())) { + throw new AqlFeatureNotImplementedException( + "SELECT: Aggregate function %s is not supported for path %s/%s (COUNT only)" + .formatted( + func, + containment.getIdentifier(), + ip.getPath().render())); + } + if (EnumSet.of(AggregateFunction.AggregateFunctionName.AVG, AggregateFunction.AggregateFunctionName.SUM) + .contains(func)) { + if (EnumSet.of( + AslExtractedColumn.OV_TIME_COMMITTED, + AslExtractedColumn.OV_TIME_COMMITTED_DV, + AslExtractedColumn.EHR_TIME_CREATED, + AslExtractedColumn.EHR_TIME_CREATED_DV) + .contains(pathWithType.extractedColumn())) { + throw new AqlFeatureNotImplementedException( + "SELECT: Aggregate function %s(%s/%s) not applicable to the given path" + .formatted( + func, + containment.getIdentifier(), + ip.getPath().render())); + } + if (pathWithType.targetsDvOrdered()) { + throw new AqlFeatureNotImplementedException( + "SELECT: Aggregate function %s(%s/%s) not applicable to paths targeting subtypes of DV_ORDERED" + .formatted( + func, + containment.getIdentifier(), + ip.getPath().render())); + } + if (!pathWithType.targetsPrimitive()) { + throw new AqlFeatureNotImplementedException( + "SELECT: Aggregate function %s(%s/%s) only applicable to paths targeting primitive types" + .formatted( + func, + containment.getIdentifier(), + ip.getPath().render())); + } + } else if (EnumSet.of( + AggregateFunction.AggregateFunctionName.MAX, + AggregateFunction.AggregateFunctionName.MIN) + .contains(func) + && !(pathWithType.targetsPrimitive() || pathWithType.targetsDvOrdered())) { + throw new AqlFeatureNotImplementedException( + "SELECT: Aggregate function %s(%s/%s) only applicable to paths targeting primitive types or subtypes of DV_ORDERED" + .formatted( + func, + containment.getIdentifier(), + ip.getPath().render())); + } + } + } + } + + private void ensureSelectPathSupported(IdentifiedPath ip) { + FeatureCheckUtils.findSupportedIdentifiedPath(ip, true, ClauseType.SELECT, systemService.getSystemId()); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/WhereCheck.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/WhereCheck.java new file mode 100644 index 000000000..3b9206e01 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/WhereCheck.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import java.util.EnumSet; +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.api.service.SystemService; +import org.ehrbase.openehr.aqlengine.AqlQueryUtils; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.condition.ComparisonOperatorCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.ComparisonOperatorSymbol; +import org.ehrbase.openehr.sdk.aql.dto.condition.ExistsCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.LikeCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.MatchesCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.WhereCondition; +import org.ehrbase.openehr.sdk.aql.dto.operand.ComparisonLeftOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.LikeOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; + +final class WhereCheck implements FeatureCheck { + private final SystemService systemService; + + public WhereCheck(SystemService systemService) { + this.systemService = systemService; + } + + @Override + public void ensureSupported(AqlQuery aqlQuery) { + WhereCondition where = aqlQuery.getWhere(); + + AqlQueryUtils.streamWhereConditions(where).forEach(c -> { + switch (c) { + case ComparisonOperatorCondition comp -> ensureWhereComparisonConditionSupported(comp); + case LikeCondition like -> ensureLikeConditionSupported(like); + case MatchesCondition matches -> ensureMatchesConditionSupported(matches); + case ExistsCondition exists -> ensureExistsConditionSupported(exists); + default -> throw new IllegalAqlException("Unexpected condition type %s".formatted(c)); + } + }); + } + + private void ensureWhereComparisonConditionSupported(ComparisonOperatorCondition condition) { + ComparisonLeftOperand conditionStatement = condition.getStatement(); + + if (conditionStatement instanceof IdentifiedPath conditionField) { + FeatureCheckUtils.PathDetails pathWithType = FeatureCheckUtils.findSupportedIdentifiedPath( + conditionField, false, ClauseType.WHERE, systemService.getSystemId()); + if (conditionField.getPath().equals(AslExtractedColumn.ARCHETYPE_NODE_ID.getPath()) + && !EnumSet.of(ComparisonOperatorSymbol.EQ, ComparisonOperatorSymbol.NEQ) + .contains(condition.getSymbol())) { + throw new AqlFeatureNotImplementedException( + "Conditions on 'archetype_node_id' only support =,!=, LIKE and MATCHES"); + } + if (conditionField.getPath().equals(AslExtractedColumn.TEMPLATE_ID.getPath()) + && !EnumSet.of(ComparisonOperatorSymbol.EQ, ComparisonOperatorSymbol.NEQ) + .contains(condition.getSymbol())) { + throw new AqlFeatureNotImplementedException( + "Conditions on 'archetype_details/template_id/value' only support =,!= and MATCHES"); + } + if (pathWithType.extractedColumn() == AslExtractedColumn.OV_TIME_COMMITTED) { + throw new AqlFeatureNotImplementedException("Conditions on %s of VERSION" + .formatted(conditionField.getPath().render())); + } + if (EnumSet.of( + AslExtractedColumn.AD_CHANGE_TYPE_VALUE, + AslExtractedColumn.AD_CHANGE_TYPE_CODE_STRING, + AslExtractedColumn.AD_CHANGE_TYPE_PREFERRED_TERM) + .contains(pathWithType.extractedColumn()) + && !EnumSet.of(ComparisonOperatorSymbol.EQ, ComparisonOperatorSymbol.NEQ) + .contains(condition.getSymbol())) { + throw new AqlFeatureNotImplementedException("Conditions on %s of VERSION only support =,!= and MATCHES" + .formatted(conditionField.getPath().render())); + } + FeatureCheckUtils.ensureOperandSupported(pathWithType, condition.getValue(), systemService.getSystemId()); + } else { + throw new AqlFeatureNotImplementedException("Functions are not supported in WHERE"); + } + } + + private static void ensureExistsConditionSupported(ExistsCondition exists) { + throw new AqlFeatureNotImplementedException("WHERE: EXISTS operator is not supported"); + // ensureIdentifiedPathSupported(exists.getValue(), false, "WHERE"); + } + + private void ensureMatchesConditionSupported(MatchesCondition matches) { + FeatureCheckUtils.PathDetails pathWithType = FeatureCheckUtils.findSupportedIdentifiedPath( + matches.getStatement(), false, ClauseType.WHERE, systemService.getSystemId()); + matches.getValues() + .forEach(operand -> + FeatureCheckUtils.ensureOperandSupported(pathWithType, operand, systemService.getSystemId())); + } + + private void ensureLikeConditionSupported(LikeCondition like) { + AqlObjectPath path = like.getStatement().getPath(); + FeatureCheckUtils.findSupportedIdentifiedPath( + like.getStatement(), false, ClauseType.WHERE, systemService.getSystemId()); + LikeOperand operand = like.getValue(); + if (AslExtractedColumn.VO_ID.getPath().equals(path)) { + throw new AqlFeatureNotImplementedException("LIKE on /uid/value is not supported"); + } + if (!(operand instanceof Primitive primitive)) { + throw new AqlFeatureNotImplementedException("Only primitive operands are supported"); + } + Object value = primitive.getValue(); + if (!(value instanceof String s)) { + throw new AqlFeatureNotImplementedException("LIKE must use String values"); + } + if (AslExtractedColumn.ARCHETYPE_NODE_ID.getPath().equals(path) && !s.matches("openEHR-EHR-[A-Z]+\\..*")) { + throw new AqlFeatureNotImplementedException( + "LIKE on archetype_node_id has to start with 'openEHR-EHR-{RM-TYPE}.'"); + } + if (AslExtractedColumn.TEMPLATE_ID.getPath().equals(path)) { + throw new AqlFeatureNotImplementedException( + "Conditions on 'archetype_details/template_id/value' only support =,!= and MATCHES"); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/ANode.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/ANode.java new file mode 100644 index 000000000..82b8ddd71 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/ANode.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPathUtil; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; + +public class ANode { + /** + * null means that the types are not constrained. + * An empty set means there constrains cannot be satisfied. + */ + Set candidateTypes; + + public enum NodeCategory { + /** + * {@link StructureRmType} + * with structureEntry == true: + * LOCATABLEs + EVENT_CONTEXT + */ + STRUCTURE, + /** + * An RM element that may contain structure entries, but is none itself: + * {@link StructureRmType}.structureEntry == false: + * FEEDER_AUDIT_DETAILS, INSTRUCTION_DETAILS + * Candidates are typically PATHABLEs that are not LOCATABLE. + * EVENT_CONTEXT is mapped as STRUCTURE; + * ISM_TRANSITION does not contain LOCATABLEs + */ + STRUCTURE_INTERMEDIATE, + /** + * An RM type + */ + RM_TYPE, + /** + * {@link FoundationType} + */ + FOUNDATION, + /** + * FOUNDATION + DV_CODED_TEXT + DV_PARSABLE in ELEMENT/value/value + *

A common operation is retrieving the value of a DATA_VALUE contained in an ELEMENT.

+ *

This does not, however, hold true for DV_TIME_SPECIFICATION subtypes (DV_PERIODIC_TIME_SPECIFICATION and DV_GENERAL_TIME_SPECIFICATION), where value is a DV_PARSABLE + * and DV_STATE where it is a DV_CODED_TEXT.

+ *

This may have to be considered for e.g. comparisons and post-processing of results may be required.

+ *

Furthermore, contrary to DV_STATE, DV_CODED_TEXT.mappings is multiple-valued. This means that the data may be spread over several rows. + * In order to omit having to query for all sub-rows, in this case the JSONB object should be supplemented with a copy of the full RM hierarchy of mappings could be stored additionally. + * The first entry may be omitted if the TERM_MAPPING.purpose field is not split into several rows.

+ */ + FOUNDATION_EXTENDED + } + + public Set getCategories() { + if (candidateTypes == null) { + throw new IllegalStateException("The candidate types have not been calculated"); + } + + Set result = EnumSet.noneOf(NodeCategory.class); + candidateTypes.stream().map(ANode::getCategory).forEach(result::add); + return result; + } + + private static NodeCategory getCategory(String typeName) { + + return StructureRmType.byTypeName(typeName) + .map(t -> t.isStructureEntry() ? NodeCategory.STRUCTURE : NodeCategory.STRUCTURE_INTERMEDIATE) + .orElseGet(() -> FoundationType.byTypeName(typeName) + .map(t -> NodeCategory.FOUNDATION) + .orElse(NodeCategory.RM_TYPE)); + } + + final Map attributes = new LinkedHashMap<>(); + + public ANode(String rmType, List parentPredicates, List predicates) { + this(rmType == null ? null : Set.of(rmType), parentPredicates, predicates); + } + + public ANode( + Set rmTypes, List parentPredicates, List predicates) { + // candidate types by specified RM type + if (rmTypes == null) { + candidateTypes = null; + } else { + candidateTypes = rmTypes.stream() + .flatMap(PathAnalysis::resolveConcreteTypeNames) + .collect(Collectors.toSet()); + } + + constrainByArchetype(parentPredicates); + constrainByArchetype(predicates); + + addPredicateConstraints(parentPredicates); + addPredicateConstraints(predicates); + } + + public ANode getAttribute(String attribute) { + return attributes.get(attribute); + } + + public Set getCandidateTypes() { + return new HashSet<>(candidateTypes); + } + + public void addPredicateConstraints(List predicates) { + Iterator it = Optional.ofNullable(predicates) + .filter(p -> p.size() == 1) + .map(List::getFirst) + .map(AndOperatorPredicate::getOperands) + .stream() + .flatMap(List::stream) + .filter(p -> !EnumSet.of( + ComparisonOperatorPredicate.PredicateComparisonOperator.NEQ, + ComparisonOperatorPredicate.PredicateComparisonOperator.MATCHES) + .contains(p.getOperator())) + .iterator(); + + while (it.hasNext()) { + ComparisonOperatorPredicate p = it.next(); + PathAnalysis.appendPath(this, p.getPath(), PathAnalysis.getCandidateTypes(p.getValue())); + } + } + + public void constrainByArchetype(List predicates) { + candidateTypes = constrainByArchetype(candidateTypes, predicates); + } + + public static Set constrainByArchetype( + final Set candidateTypes, List predicates) { + + if (predicates == null || (candidateTypes != null && candidateTypes.isEmpty())) { + return candidateTypes; + } + + boolean singleAnd = predicates.size() == 1; + if (singleAnd) { + // candidateTypes only changes when it has been null before + return constrainByArchetype(candidateTypes, predicates.getFirst()); + + } else { + // for OR: constrain by union of all AND constraints + Set constraintUnion = null; + + Iterator it = predicates.iterator(); + while (it.hasNext() || (constraintUnion != null && constraintUnion.isEmpty())) { + Set candidateSet = + Optional.ofNullable(candidateTypes).map(HashSet::new).orElse(null); + candidateSet = constrainByArchetype(candidateSet, it.next()); + + if (candidateSet != null) { + if (constraintUnion == null) { + constraintUnion = candidateSet; + } else { + constraintUnion.addAll(candidateSet); + } + } + } + + if (constraintUnion == null) { + return candidateTypes; + } else if (candidateTypes == null) { + return constraintUnion; + } else { + candidateTypes.retainAll(constraintUnion); + return candidateTypes; + } + } + } + + public static Set constrainByArchetype(Set candidateTypes, AndOperatorPredicate predicates) { + Set constrained = candidateTypes; + + Iterator it = predicates.getOperands().iterator(); + while (it.hasNext() && !(constrained != null && constrained.isEmpty())) { + String archetypeNodeId = getArchetypeNodeId(it.next()).orElse(null); + if (archetypeNodeId != null) { + constrained = constrainByArchetype(constrained, archetypeNodeId); + } + } + return candidateTypes; + } + + private static Optional getArchetypeNodeId(ComparisonOperatorPredicate cmpOp) { + return Optional.of(cmpOp) + .filter(p -> p.getOperator() == ComparisonOperatorPredicate.PredicateComparisonOperator.EQ) + .filter(p -> AqlObjectPathUtil.ARCHETYPE_NODE_ID.equals((p.getPath()))) + .map(ComparisonOperatorPredicate::getValue) + .filter(StringPrimitive.class::isInstance) + .map(StringPrimitive.class::cast) + .map(StringPrimitive::getValue); + } + + /** + * Remove types not matching archetype + * + * @param archetypeNodeId + */ + static Set constrainByArchetype(Set candidateTypes, String archetypeNodeId) { + return PathAnalysis.rmTypeFromArchetype(archetypeNodeId) + .map(PathAnalysis::resolveConcreteTypeNames) + .map(s -> s.collect(Collectors.toSet())) + .map(s -> { + if (candidateTypes == null) { + return s; + } else { + candidateTypes.retainAll(s); + return candidateTypes; + } + }) + .orElse(candidateTypes); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationType.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationType.java new file mode 100644 index 000000000..4329de46d --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationType.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * https://specifications.openehr.org/releases/BASE/latest/foundation_types.html + *

+ * Note that the types names are based on the Archie type model and are not fully aligned with the specification. + */ +public enum FoundationType { + BOOLEAN(FoundationTypeCategory.BOOLEAN), + /** + * Java-Alias for "Octet". + * Only as multiple-valued for byte arrays + */ + BYTE(FoundationTypeCategory.BYTEA), + DOUBLE(FoundationTypeCategory.NUMERIC), + INTEGER(FoundationTypeCategory.NUMERIC), + /** + * Java-Alias for "Integer64" + */ + LONG(FoundationTypeCategory.NUMERIC), + STRING(FoundationTypeCategory.TEXT), + URI(FoundationTypeCategory.TEXT), + + /* + *

+     * Openehr:
+     * Temporal
+     * - Iso8601_type
+     * -- Iso8601_date
+     * -- Iso8601_time
+     * -- Iso8601_date_time
+     * -- Iso8601_duration
+     *
+     * DV_DATE_TIME: inherit DV_TEMPORAL, Iso8601_date_time
+     * DV_DATE: inherit DV_TEMPORAL, Iso8601_date
+     * DV_TIME: inherit DV_TEMPORAL, Iso8601_time
+     * DV_DURATION: inherit DV_AMOUNT, Iso8601_duration
+     *
+     * As opposed to the specification, the Archie temporal classes do not extend/implement a subtype of Temporal.
+     *
+     * Also the value property of these classes is not of type String:
+     * - DvDateTime.value: TemporalAccessor
+     * - DvDate.value: Temporal
+     * - DvTime.value: TemporalAccessor
+     * - DvDuration.value: TemporalAmount
+     *
+     * It may be feasible to treat those temporal types as subtypes of STRING.
+     *
+     * 
+ */ + TEMPORAL(FoundationTypeCategory.TEXT), + TEMPORAL_ACCESSOR(FoundationTypeCategory.TEXT), + TEMPORAL_AMOUNT(FoundationTypeCategory.TEXT), + /** + * Java-Alias for "Character", + * Only used for TERM_MAPPING.match + */ + CHAR(FoundationTypeCategory.TEXT), + /** + * Java-Alias for "Any": not a foundation type, used as generic placeholder in INTERVAL + */ + OBJECT(FoundationTypeCategory.ANY); + + private static final Map BY_TYPE_NAME = new HashMap<>(); + + static { + for (FoundationType value : values()) { + BY_TYPE_NAME.put(value.name(), value); + } + } + + public final FoundationTypeCategory category; + + FoundationType(FoundationTypeCategory category) { + this.category = category; + } + + public static Optional byTypeName(String typeName) { + return Optional.ofNullable(BY_TYPE_NAME.get(typeName)); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationTypeCategory.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationTypeCategory.java new file mode 100644 index 000000000..d40ea642a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationTypeCategory.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +public enum FoundationTypeCategory { + ANY, + BOOLEAN, + BYTEA, + NUMERIC, + TEXT, +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathAnalysis.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathAnalysis.java new file mode 100644 index 000000000..bafdfcd4e --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathAnalysis.java @@ -0,0 +1,598 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import com.nedap.archie.aom.ArchetypeHRID; +import com.nedap.archie.rminfo.ArchieRMInfoLookup; +import com.nedap.archie.rminfo.RMAttributeInfo; +import com.nedap.archie.rminfo.RMTypeInfo; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.ehrbase.openehr.sdk.aql.dto.operand.BooleanPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.DoublePrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.LongPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.PathPredicateOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.TemporalPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; + +/** + *

AQL paths occur in select expressions, where conditions, + * but also within the predicates of AQL paths.

+ * + *

+ * Paths originate from structure RM types from the FROM clause, + * or from a path node featuring a predicate. + * FROM roots can be constrained via predicates and additional CONTAINS clauses. + * + *

+ * select
+ * o[openEHR-EHR-OBSERVATION.blood_pressure.v2]/data[archetype_node_id="at0001" and name/value="History"]/events
+ * FROM ENTRY o;
+ * 
+ * + * A path originates from a structure RM type from the FROM clause, + * which is constrained via predicates and additional CONTAINS clauses. + * + * or from the node featuring a predicate. + * It consists of a list of attributes, which can be constrained by additional predicates. + * The RM model specifies the RM types with their attributes and data types of those attributes.

+ * + *

This information can be used to infer + *

    + *
  • if a path is valid
  • + *
  • if a path, or a node, is single-valued
  • + *
  • the possible data types of a path
  • + *
+ *

+ * + *

Since many WhereConditions only operate on certain data types, these can also constrain the paths. + * Note that these constraints may, however, not inherently apply to the path if the constraint is part of a OR or NOT condition.

+ * + *

The information can be used to determine how the field needs to be accessed in the database structure, e.g. + *

    + *
  • What structure nodes need to be joined
  • + *
  • If joins can/must be shared between different paths + * (For performance reasons, but also in order to prevent cartesian products due to multiple-valued paths sharing common base objects)
  • + *
  • If an RM object needs to be reconstructed
  • + *
+ *

+ * + *

Rules that constrain the base type of a node

+ *
    + *
  • The candidate base types do not contain abstract classes ( derived classes, instead)
  • + *
  • The candidate base types only contain classes that feature the given attribute
  • + *
  • The candidate base types only contain classes where the attribute types match
  • + *
  • If a base type does not possess the TODO
  • + *
+ * + * + */ +public class PathAnalysis { + static final ArchieRMInfoLookup RM_INFOS = ArchieRMInfoLookup.getInstance(); + + public record AttInfo(boolean multipleValued, boolean nullable, Set targetTypes) {} + + public static class AttributeInfos { + + /** + * All RM types that are relevant in the context of this type + */ + static final Set rmTypes; + + static final Map> baseTypesByAttribute; + + /** + * Map<attribute_name, Map<parent_type, Set<child_type>>> + */ + static final Map>> typedAttributes; + + /** + * Map<attribute_name, Map<parent_type, AttInfo>> + */ + static final Map> attributeInfos; + + static { + LinkedHashSet typesModifiable = new LinkedHashSet<>(); + Stream.of(RmConstants.COMPOSITION, RmConstants.EHR_STATUS, RmConstants.ORIGINAL_VERSION) + .map(AttributeInfos::calculateContainedTypes) + .forEach(typesModifiable::addAll); + + Map> baseTypesByAttributeModifiable = calculateBaseTypesByAttribute(typesModifiable); + Map>> typedAttributesModifiable = calculateTypedAttributes(typesModifiable); + Map> attributeInfosModifiable = + calculateAttributeInfos(typedAttributesModifiable); + + // manually add EHR + addEhrAttributes( + typesModifiable, + baseTypesByAttributeModifiable, + typedAttributesModifiable, + attributeInfosModifiable); + + rmTypes = Collections.unmodifiableSet(typesModifiable); + baseTypesByAttribute = unmodifiableCopy(baseTypesByAttributeModifiable); + typedAttributes = unmodifiableCopy(typedAttributesModifiable); + attributeInfos = unmodifiableCopy(attributeInfosModifiable); + } + + private static void addEhrAttributes( + Set rmTypes, + Map> baseTypesByAttribute, + Map>> typedAttributes, + Map> attributeInfos) { + String baseType = "EHR"; + rmTypes.add(baseType); + + // ehrId: HIER_OBJECT_ID + addAttribute( + "ehrId", baseType, Set.of("HIER_OBJECT_ID"), baseTypesByAttribute, typedAttributes, attributeInfos); + + // timeCreated: DV_DATE_TIME, + addAttribute( + "timeCreated", + baseType, + Set.of("DV_DATE_TIME"), + baseTypesByAttribute, + typedAttributes, + attributeInfos); + + // ehrStatus: OBJECT_REF -> EHR_STATUS + addAttribute( + "ehrStatus", baseType, Set.of("EHR_STATUS"), baseTypesByAttribute, typedAttributes, attributeInfos); + + // compositions: OBJECT_REF -> COMPOSITION + addAttribute( + "compositions", + baseType, + Set.of("COMPOSITION"), + baseTypesByAttribute, + typedAttributes, + attributeInfos); + + // Not supported: + // systemId: HIER_OBJECT_ID + // ehrAccess: OBJECT_REF -> EHR_ACCESS + // directory: OBJECT_REF -> FOLDER + // contributions: OBJECT_REF -> CONTRIBUTION + // folders: OBJECT_REF -> FOLDER + } + + private static void addAttribute( + String attribute, + String baseType, + Set targetTypes, + Map> baseTypesByAttribute, + Map>> typedAttributes, + Map> attributeInfos) { + baseTypesByAttribute + .computeIfAbsent(attribute, k -> new HashSet<>()) + .add(baseType); + typedAttributes + .computeIfAbsent(attribute, k -> new HashMap<>()) + .computeIfAbsent(baseType, k -> new HashSet<>()) + .addAll(targetTypes); + attributeInfos + .computeIfAbsent(attribute, k -> new HashMap<>()) + .put(baseType, new AttInfo(false, false, targetTypes)); + } + + private static Map unmodifiableCopy(Map map) { + if (map == null) { + return null; + } + Map ret = new HashMap(); + map.forEach((k, v) -> { + ret.put( + k, + switch (v) { + case Map m -> unmodifiableCopy(m); + case Set s -> unmodifiableCopy(s); + default -> v; + }); + }); + return Map.copyOf(ret); + } + + private static Set unmodifiableCopy(Set set) { + if (set == null) { + return null; + } + Set ret = new HashSet(); + set.forEach(v -> { + ret.add( + switch (v) { + case Map m -> unmodifiableCopy(m); + case Set s -> unmodifiableCopy(s); + default -> v; + }); + }); + return Set.copyOf(ret); + } + + private static List calculateContainedTypes(String rootType) { + Queue remainingTypes = new LinkedList<>(); + remainingTypes.add(RM_INFOS.getTypeInfo(rootType)); + + Set seen = new HashSet<>(); + seen.add(remainingTypes.peek()); + + List typeNames = new ArrayList<>(); + + while (!remainingTypes.isEmpty()) { + RMTypeInfo typeInfo = remainingTypes.poll(); + + typeNames.add(typeInfo.getRmName()); + + typeInfo.getDirectDescendantClasses().stream().filter(seen::add).forEach(remainingTypes::add); + + typeInfo.getAttributes().values().stream() + .filter(ti -> !ti.isComputed()) + .map(RMAttributeInfo::getTypeNameInCollection) + .map(RM_INFOS::getTypeInfo) + .filter(Objects::nonNull) + .filter(seen::add) + .forEach(remainingTypes::add); + } + return typeNames; + } + + private static Map> calculateBaseTypesByAttribute(Set rmTypes) { + return rmTypes.stream() + .map(RM_INFOS::getTypeInfo) + .filter(Objects::nonNull) + .flatMap(t -> t.getAttributes().values().stream() + .filter(a -> !a.isComputed()) + .map(a -> Pair.of(t.getRmName(), a))) + .collect(Collectors.groupingBy( + p -> p.getRight().getRmName(), Collectors.mapping(p -> p.getLeft(), Collectors.toSet()))); + } + + static Map>> calculateTypedAttributes(Set rmTypes) { + return rmTypes.stream() + .map(RM_INFOS::getTypeInfo) + .filter(Objects::nonNull) + .flatMap(t -> t.getAttributes().values().stream() + .filter(a -> !a.isComputed()) + .flatMap(a -> resolveConcreteTypeNames(a.getTypeNameInCollection()) + .map(vt -> Triple.of(t.getRmName(), a.getRmName(), vt)))) + .collect(Collectors.groupingBy( + Triple::getMiddle, + Collectors.groupingBy( + Triple::getLeft, Collectors.mapping(Triple::getRight, Collectors.toSet())))); + } + + private static Map> calculateAttributeInfos( + Map>> typedAttributes) { + return typedAttributes.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, f -> { + RMAttributeInfo attributeInfo = RM_INFOS.getAttributeInfo(f.getKey(), e.getKey()); + return new AttInfo( + attributeInfo.isMultipleValued(), attributeInfo.isNullable(), f.getValue()); + })))); + } + } + + static void validateAttributeNamesExist(ANode rootNode) { + Iterator nodeIt = iterateNodes(rootNode); + while (nodeIt.hasNext()) { + ANode node = nodeIt.next(); + node.attributes.keySet().forEach(att -> { + if (!AttributeInfos.attributeInfos.containsKey(att)) { + throw new IllegalArgumentException("Unknown attribute: %s".formatted(att)); + } + }); + } + } + + private PathAnalysis() { + // NOOP + } + + /** + * For abstract RM-Types all implementations are returned + * + * @param abstractType + * @return + */ + static Stream resolveConcreteTypeNames(String abstractType) { + + RMTypeInfo typeInfo = RM_INFOS.getTypeInfo(abstractType); + if (typeInfo == null) { + // no RM object + return Stream.of(abstractType); + } + + Set concreteTypes = typeInfo.getAllDescendantClasses(); + concreteTypes.add(typeInfo); + concreteTypes.removeIf(i -> Modifier.isAbstract(i.getJavaClass().getModifiers())); + return concreteTypes.stream().map(RMTypeInfo::getRmName); + } + + static Optional rmTypeFromArchetype(String archetypeNodeId) { + return Optional.ofNullable(archetypeNodeId) + .filter(s -> s.startsWith("openEHR-EHR-")) + .map(s -> { + try { + return new ArchetypeHRID(archetypeNodeId); + } catch (IllegalArgumentException e) { + return null; + } + }) + .map(ArchetypeHRID::getRmClass); + } + + /** + * Determine which types the value is compatible with + * + * @param value + * @return + */ + static Set getCandidateTypes(PathPredicateOperand value) { + + // FoundationType.BOOLEAN, + // FoundationType.BYTE, + // FoundationType.DOUBLE, + // FoundationType.INTEGER, + // FoundationType.LONG, + // FoundationType.STRING, + // FoundationType.URI, + // FoundationType.TEMPORAL, + // FoundationType.TEMPORAL_ACCESSOR, + // FoundationType.TEMPORAL_AMOUNT, + // FoundationType.CHAR, + // FoundationType.OBJECT, + + if (value instanceof Primitive p) { + if (p.getValue() == null) { + return null; + } + + if (value instanceof DoublePrimitive || value instanceof LongPrimitive) { + return Stream.of(FoundationType.DOUBLE, FoundationType.INTEGER, FoundationType.LONG) + .map(Enum::name) + .collect(Collectors.toSet()); + } else if (value instanceof BooleanPrimitive) { + return Stream.of(FoundationType.BOOLEAN).map(Enum::name).collect(Collectors.toSet()); + } else if (value instanceof StringPrimitive) { + + if (value instanceof TemporalPrimitive) { + // XXX really all? Or check data? + return Stream.of( + FoundationType.STRING, + FoundationType.TEMPORAL, + FoundationType.TEMPORAL_ACCESSOR, + FoundationType.TEMPORAL_AMOUNT) + .map(Enum::name) + .collect(Collectors.toSet()); + } else { + return Stream.of(FoundationType.STRING, FoundationType.CHAR, FoundationType.URI) + .map(Enum::name) + .collect(Collectors.toSet()); + } + } else { + throw new IllegalArgumentException( + "Unknown primitive type %s".formatted(value.getClass().getName())); + } + + } else { + return null; + } + } + + public static Map> createAttributeInfos(ANode rootNode) { + Map> infos = new HashMap<>(); + + Iterator nodeIt = iterateNodes(rootNode); + while (nodeIt.hasNext()) { + ANode node = nodeIt.next(); + Map attInfos = node.attributes.entrySet().stream() + .map(e -> Pair.of(e.getKey(), createAttributeInfo(node, e.getKey(), e.getValue()))) + .filter(p -> p.getValue() != null) + .collect(Collectors.toMap(Pair::getKey, Pair::getValue)); + if (!attInfos.isEmpty()) { + infos.put(node, attInfos); + } + } + return infos; + } + + private static Iterator iterateNodes(ANode rootNode) { + Queue stack = new LinkedList<>(); + stack.add(rootNode); + return new Iterator<>() { + @Override + public boolean hasNext() { + return !stack.isEmpty(); + } + + @Override + public ANode next() { + ANode node = stack.remove(); + stack.addAll(node.attributes.values()); + return node; + } + }; + } + + private static AttInfo createAttributeInfo(ANode node, String attName, ANode childNode) { + return AttributeInfos.attributeInfos.getOrDefault(attName, Map.of()).entrySet().stream() + .filter(e -> node.candidateTypes.contains(e.getKey())) + .map(Map.Entry::getValue) + .filter(a -> !Collections.disjoint(childNode.candidateTypes, a.targetTypes)) + .reduce((a, b) -> new AttInfo( + a.multipleValued || b.multipleValued, + a.nullable || b.nullable, + SetUtils.union(a.targetTypes, b.targetTypes))) + .orElse(null); + } + + /** + * Determines for each node of the path (resulting from the path hierarchy directly, or from predicates) + * a set of possible RM or Foundational types + * + * @param rootType + * @param variablePredicates + * @param rootPredicates + * @param path + * @param candidateTypes + * @return + */ + public static ANode analyzeAqlPathTypes( + String rootType, + List variablePredicates, + List rootPredicates, + AqlObjectPath path, + Set candidateTypes) { + // https://specifications.openehr.org/releases/QUERY/latest/AQL.html#_identified_paths + + // c[data[att0]/value = data[att1]/value and data[att1]/value = 1]/data[att0]/value + // ORDER BY c/data[att3]/value + ANode rootNode = new ANode(rootType, variablePredicates, rootPredicates); + appendPath(rootNode, path, candidateTypes); + + validateAttributeNamesExist(rootNode); + + while (applyChildAttributeConstraints(rootNode)) { + // NOOP + } + return rootNode; + } + + private static boolean applyChildAttributeConstraints(ANode node) { + if (node.attributes.isEmpty() || (node.candidateTypes != null && node.candidateTypes.isEmpty())) { + return false; + } + boolean changed = false; + for (Map.Entry att : node.attributes.entrySet()) { + changed |= applyAttributeConstraints(node, att.getKey(), att.getValue()); + changed |= applyChildAttributeConstraints(att.getValue()); + } + return changed; + } + + private static boolean applyAttributeConstraints(ANode parentNode, String attName, ANode childNode) { + + Map> typeConstellations = AttributeInfos.typedAttributes.get(attName); + if (typeConstellations == null) { + parentNode.candidateTypes = new HashSet<>(); + childNode.candidateTypes = new HashSet<>(); + return true; + } else if (parentNode.candidateTypes == null) { + if (childNode.candidateTypes == null) { + parentNode.candidateTypes = new HashSet<>(typeConstellations.keySet()); + childNode.candidateTypes = new HashSet<>(typeConstellations.values().stream() + .flatMap(Set::stream) + .collect(Collectors.toSet())); + } else { + Set childConstraints = typeConstellations.values().stream() + .flatMap(Set::stream) + .collect(Collectors.toSet()); + childNode.candidateTypes.removeIf(t -> !childConstraints.contains(t)); + parentNode.candidateTypes = typeConstellations.entrySet().stream() + .filter(e -> CollectionUtils.containsAny(e.getValue(), childNode.candidateTypes)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + return true; + } else { + boolean changed = parentNode.candidateTypes.removeIf(t -> { + Set supportedChildTypes = typeConstellations.get(t); + if (CollectionUtils.isEmpty(supportedChildTypes)) { + return true; + } + if (childNode.candidateTypes == null) { + return false; + } + return !CollectionUtils.containsAny(childNode.candidateTypes, supportedChildTypes); + }); + Set childConstraints = typeConstellations.entrySet().stream() + .filter(e -> parentNode.candidateTypes.contains(e.getKey())) + .map(Map.Entry::getValue) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + if (childNode.candidateTypes == null) { + childNode.candidateTypes = childConstraints; + changed = true; + } else { + changed |= childNode.candidateTypes.removeIf(t -> !childConstraints.contains(t)); + } + return changed; + } + } + + static void appendPath(ANode root, AqlObjectPath path, Set candidateTypes) { + if (path == null) { + return; + } + Iterator nodeIt = path.getPathNodes().iterator(); + + ANode n = root; + while (nodeIt.hasNext()) { + n = addAttributes(n, nodeIt.next()); + } + if (candidateTypes != null) { + if (n.candidateTypes == null) { + n.candidateTypes = new HashSet<>(candidateTypes); + } else { + n.candidateTypes.removeIf(t -> !candidateTypes.contains(t)); + } + } + } + + private static ANode addAttributes(ANode root, PathNode child) { + String attName = child.getAttribute(); + ANode childANode = root.attributes.get(attName); + + if (childANode == null) { + childANode = new ANode((String) null, null, child.getPredicateOrOperands()); + root.attributes.put(attName, childANode); + + } else { + // children collide and have to be merged + childANode.constrainByArchetype(child.getPredicateOrOperands()); + childANode.addPredicateConstraints(child.getPredicateOrOperands()); + } + + return childANode; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathCohesionAnalysis.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathCohesionAnalysis.java new file mode 100644 index 000000000..631348d8e --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathCohesionAnalysis.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.ehrbase.openehr.aqlengine.AqlQueryUtils; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentVersionExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.PathPredicateOperand; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPathUtil; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; +import org.ehrbase.openehr.util.TreeNode; + +/** + * + *

Cohesion of attribute paths

+ * + * Given an object type that has several attributes + * and a query that selects those attributes, + * the result list must not contain a combination of values from different ("not same") objects. + * + *

Constrained ATTRIBUTEs + * + *

In Archetypes and Templates the content of attributes of type ATTRIBUTE can be constrained. + * Effectively each constraint, identified by its node_id, constitutes a sub-attribute of the base attribute. + * + * ARCHETYPE_SLOT or ARCHETYPE_ROOT constraints also possess a node_id, but in the object representation + * the archetype_node_id + * features the archetype id, instead. + * Since multiple ARCHETYPE_SLOT and ARCHETYPE_ROOT constraints may allow the same archetype_id, + * it may not be sufficient to identify a specific sub-attribute. In this case, name/value can be used as additional identification criterion. + * Other predicates are merely acting as filters. + *

+ *

If multiple paths target the same base attribute, it must be determined at which resolution attributes are indicated: + * + *

    + *
  1. attribute without identifying predicates: If present, a base attribute is indicated: Identifying predicates in other paths at as filters
  2. + *
  3. name/value: if all paths (only) have name/value predicates, they induce sub-attributes. Otherwise, a base attribute is indicated.
  4. + *
  5. node_id: name/value acts as filter
  6. + *
  7. archetype_id: if a path with a certain archetype_id has no name/value predicate, name/value of other paths with the same archetype_id act as filter
  8. + *
  9. archetype_id + name/value: identifies a sub-attribute
  10. + *
+ *

+ */ +public final class PathCohesionAnalysis { + + private PathCohesionAnalysis() { + // NOOP + } + + /** + * For each containment expression that is referenced in the query, the paths are analyzed and a tree of its attributes is returned. + * + * @param query + * @return + */ + public static Map analyzePathCohesion(AqlQuery query) { + + Map> roots = AqlQueryUtils.allIdentifiedPaths(query) + .distinct() + .collect(Collectors.groupingBy(IdentifiedPath::getRoot)); + + return roots.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { + PathNode rootNode = createRootNode(e); + + PathCohesionTreeNode joinTree = PathCohesionTreeNode.root(rootNode, e.getValue()); + fillJoinTree(joinTree, 0); + return joinTree; + })); + } + + private static PathNode createRootNode(Map.Entry> e) { + String rootType; + AbstractContainmentExpression root = e.getKey(); + List rootPredicates; + if (root instanceof ContainmentVersionExpression cv) { + rootType = "VERSION"; + rootPredicates = new ArrayList<>(); + } else if (root instanceof ContainmentClassExpression cc) { + rootType = cc.getType(); + rootPredicates = Optional.of(cc) + .map(ContainmentClassExpression::getPredicates) + .orElseGet(ArrayList::new); + } else { + throw new IllegalArgumentException("Unsupported type: %s".formatted(root)); + } + + /* + * Note: IdentifiedPath.rootPredicates does not produce attributes, and merely acts as filter. + * Therefore, it needs not be merged (and no forest data structure is needed). + */ + var attributeType = AttributeType.getAttributeType(rootPredicates); + attributeType.cleanupPredicates(rootPredicates); + + return new PathNode(rootType, rootPredicates); + } + + private static void fillJoinTree(PathCohesionTreeNode node, int level) { + Map> baseAttributes = node.getPaths().stream() + .filter(o -> PathInfo.pathNodes(o.getPath()).size() > level) + .collect(Collectors.groupingBy( + p -> p.getPath().getPathNodes().get(level).getAttribute())); + + baseAttributes.forEach((k, v) -> { + var attributeType = v.stream() + .map(IdentifiedPath::getPath) + .map(AqlObjectPath::getPathNodes) + .map(n -> n.get(level)) + .map(PathNode::getPredicateOrOperands) + .map(AttributeType::getAttributeType) + .reduce(AttributeType::merge) + .get(); + + if (attributeType == AttributeType.BASE) { + node.addChild(new PathNode(k), v); + } else { + Map, List> byAttType = v.stream() + .collect(Collectors.groupingBy(p -> attributeType.cleanupPredicates( + p.getPath().getPathNodes().get(level).getPredicateOrOperands()))); + byAttType.forEach((cleanPredicates, paths) -> node.addChild(new PathNode(k, cleanPredicates), paths)); + } + }); + + node.getChildren().forEach(c -> fillJoinTree(c, level + 1)); + } + + enum AttributeType { + BASE, + ARCHETYPE, + NODE, + NAME; + + /** + * Remove predicates that are not relevant to this AttributeType + * + * @param predicateOrOperands + * @return + */ + public List cleanupPredicates(List predicateOrOperands) { + return predicateOrOperands.stream() + .map(and -> { + Optional archetypeNodeId = getOperand( + and, AqlObjectPathUtil.ARCHETYPE_NODE_ID) + .filter(this::prefilter) + .findFirst(); + Optional nameValue = getOperand(and, AqlObjectPathUtil.NAME_VALUE) + .filter(this::prefilter) + .findFirst(); + if (this == NODE) { + // remove name/value for nodeId entries + boolean isNodeId = archetypeNodeId + .map(ComparisonOperatorPredicate::getValue) + .map(Primitive.class::cast) + .map(Primitive::getValue) + .map(String.class::cast) + .filter(v -> !v.startsWith("openEHR-")) + .isPresent(); + if (isNodeId) { + nameValue = Optional.empty(); + } + } + if (archetypeNodeId.isEmpty() && nameValue.isEmpty()) { + return null; + } else { + return new AndOperatorPredicate(Stream.of(archetypeNodeId, nameValue) + .flatMap(Optional::stream) + .collect(Collectors.toList())); + } + }) + .filter(Objects::nonNull) + // sort lists by archetypeNodeId and nameValue + .sorted(Comparator.comparing( + and -> getStringValue(and, AqlObjectPathUtil.ARCHETYPE_NODE_ID)) + .thenComparing(and -> getStringValue(and, AqlObjectPathUtil.NAME_VALUE))) + .toList(); + } + + private static String getStringValue(AndOperatorPredicate and, AqlObjectPath archetypeNodeId) { + return getOperand(and, archetypeNodeId) + .map(ComparisonOperatorPredicate::getValue) + .map(Primitive.class::cast) + .map(p -> (String) p.getValue()) + .findFirst() + .orElse(null); + } + + private static Stream getOperand(AndOperatorPredicate and, AqlObjectPath path) { + return and.getOperands().stream().filter(op -> path.equals(op.getPath())); + } + + private boolean prefilter(ComparisonOperatorPredicate op) { + if (this == BASE) { + return false; + } + ComparisonOperatorPredicate.PredicateComparisonOperator operator = op.getOperator(); + if (operator != ComparisonOperatorPredicate.PredicateComparisonOperator.EQ) { + return false; + } + AqlObjectPath path = op.getPath(); + if (AqlObjectPathUtil.NAME_VALUE.equals(path)) { + // Note: for NODE, if a node_id is given, the name has to be removed later + return this != ARCHETYPE; + } else if (AqlObjectPathUtil.ARCHETYPE_NODE_ID.equals(path)) { + return this != NAME; + } else { + return false; + } + } + + public AttributeType merge(AttributeType type) { + if (this == type) { + return this; + } + return switch (this) { + case BASE -> this; + case ARCHETYPE -> type == NODE ? this : BASE; + case NODE -> type == NAME ? BASE : type; + case NAME -> BASE; + }; + } + + public AttributeType mergeAndPredicates(AttributeType type) { + if (this == BASE || type == BASE) { + throw new IllegalArgumentException("BASE cannot be merged"); + } + if (this == type) { + return this; + } + // Assumption: no duplicates, e.g. [name/value='a' and name/value='b'] + return NODE; + } + + public static AttributeType getAttributeType(List predicateOrOperands) { + return predicateOrOperands.stream() + .map(AttributeType::getAttributeType) + .reduce(AttributeType::merge) + .orElse(BASE); + } + + public static AttributeType getAttributeType(AndOperatorPredicate and) { + return and.getOperands().stream() + .map(AttributeType::getAttributeType) + .filter(Objects::nonNull) + .reduce(AttributeType::mergeAndPredicates) + .orElse(BASE); + } + + private static AttributeType getAttributeType(ComparisonOperatorPredicate op) { + ComparisonOperatorPredicate.PredicateComparisonOperator operator = op.getOperator(); + if (operator != ComparisonOperatorPredicate.PredicateComparisonOperator.EQ) { + return null; + } + AqlObjectPath path = op.getPath(); + if (AqlObjectPathUtil.NAME_VALUE.equals(path)) { + return NAME; + } else if (AqlObjectPathUtil.ARCHETYPE_NODE_ID.equals(path)) { + PathPredicateOperand value = op.getValue(); + if (value instanceof Primitive p && p.getValue() instanceof String v) { + if (v.startsWith("openEHR-")) { + return ARCHETYPE; + } else { + return NODE; + } + } else { + return null; + } + } else { + return null; + } + } + } + + public static class PathCohesionTreeNode extends TreeNode { + + private PathNode attribute; + private final List paths; + private final List pathsEndingAtNode; + private final boolean root; + + private PathCohesionTreeNode(PathNode attribute, List paths, boolean root) { + this.attribute = attribute; + this.paths = paths; + this.pathsEndingAtNode = new ArrayList<>(paths); + this.root = root; + } + + PathCohesionTreeNode addChild(PathNode attribute, List paths) { + pathsEndingAtNode.removeAll(paths); + return addChild(new PathCohesionTreeNode(attribute, paths, false)); + } + + public static PathCohesionTreeNode root(PathNode attribute, List paths) { + return new PathCohesionTreeNode(attribute, paths, true); + } + + public PathNode getAttribute() { + return attribute; + } + + public void setAttribute(PathNode attribute) { + this.attribute = attribute; + } + + /** + * all paths this attribute belongs to + */ + public List getPaths() { + return paths; + } + + public List getPathsEndingAtNode() { + return Collections.unmodifiableList(pathsEndingAtNode); + } + + public boolean isRoot() { + return root; + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathInfo.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathInfo.java new file mode 100644 index 000000000..73bcb3747 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathInfo.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import static org.ehrbase.openehr.aqlengine.AqlQueryUtils.streamWhereConditions; + +import com.nedap.archie.rm.datavalues.quantity.DvOrdered; +import com.nedap.archie.rminfo.ArchieRMInfoLookup; +import com.nedap.archie.rminfo.RMTypeInfo; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.openehr.aqlengine.AqlQueryUtils; +import org.ehrbase.openehr.aqlengine.pathanalysis.ANode.NodeCategory; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathAnalysis.AttInfo; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathCohesionAnalysis.PathCohesionTreeNode; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; + +/** + * Provides an analysis of a Path Cohesion Tree + */ +public final class PathInfo { + + private static final Set DV_ORDERED_TYPES = + ArchieRMInfoLookup.getInstance().getTypeInfo(DvOrdered.class).getAllDescendantClasses().stream() + .map(RMTypeInfo::getRmName) + .collect(Collectors.toSet()); + + /** + * The number of (structure) children and if data is retrieved determines how a path node needs to be joined. + * + * + * + * + * + * + * + * + * + *
ROOTnoChildoneChildmultipleChildren
dataROOTROOTROOT
no dataROOTROOT
sub-nodenoChildoneChildmultipleChildren
dataDATADATADATA
no dataINTERNAL_SINGLE_CHILDINTERNAL_FORK
+ */ + public enum JoinMode { + /** + * Root node stemming from the FROM clause. + * Is already "left-joined". + * Hence all children need to be left-joined. + */ + ROOT, + /** + * Node that contributes data to the result; + * The number of children is secondary. + * The children need to be "left-joined" (P(⟕Cn)). + */ + DATA, + /** + * Internal node with just a single child. + * It does not directly contribute data to the result. + * It must only result in tuples when the child does. + * It can be joined with the child (P⋈C), or may, under some conditions, be omitted. + */ + INTERNAL_SINGLE_CHILD, + /** + * Internal node with multiple children. + * It does not directly contribute data to the result. + * It must only result in tuples when at least one of the children does. + *
+ * This may be considered an inner join of the parent with the result of outer joining all children: + * P⋈(⟗i=1n(Ci)) + */ + INTERNAL_FORK + } + + public record NodeInfo( + NodeCategory category, + Set rmTypes, + List pathFromRoot, + boolean multipleValued, + Set dvOrderedTypes) {} + + private final PathCohesionTreeNode cohesionTreeRoot; + private final Map>>> pathAttributeInfo; + + private final Map nodeTypeInfo; + private final Map> pathToQueryClause; + + public PathInfo(PathCohesionTreeNode cohesionTreeRoot, Map> pathToQueryClause) { + this.cohesionTreeRoot = cohesionTreeRoot; + this.pathAttributeInfo = cohesionTreeRoot.getPaths().stream().collect(Collectors.toMap(ip -> ip, ip -> { + AbstractContainmentExpression root = ip.getRoot(); + ANode analyzed = PathAnalysis.analyzeAqlPathTypes( + root instanceof ContainmentClassExpression cce ? cce.getType() : RmConstants.ORIGINAL_VERSION, + ip.getRootPredicate(), + root.getPredicates(), + ip.getPath(), + null); + if (analyzed.getCandidateTypes().isEmpty()) { + throw new IllegalArgumentException("Path %s is not valid".formatted(ip.render())); + } + return Pair.of(analyzed, PathAnalysis.createAttributeInfos(analyzed)); + })); + + this.nodeTypeInfo = fillNodeTypeInfo(cohesionTreeRoot, -1, new HashMap<>()); + this.pathToQueryClause = pathToQueryClause; + } + + private Map fillNodeTypeInfo( + PathCohesionTreeNode currentNode, int level, Map nodeTypeInfo) { + NodeInfo nodeInfo = currentNode.getPaths().stream() + .map(ip -> nodeTypeInfoForPathAtLevel(ip, level)) + .reduce((a, b) -> new NodeInfo( + mergeNodeCategories(a.category(), b.category()), + mutableUnion(a.rmTypes(), b.rmTypes()), + a.pathFromRoot(), + a.multipleValued() || b.multipleValued(), + mutableUnion(a.dvOrderedTypes(), b.dvOrderedTypes()))) + .orElseThrow(); + nodeTypeInfo.put(currentNode, nodeInfo); + currentNode.getChildren().forEach(pcn -> fillNodeTypeInfo(pcn, level + 1, nodeTypeInfo)); + return nodeTypeInfo; + } + + private static Set mutableUnion(Set a, Set b) { + return new HashSet<>(SetUtils.union(a, b)); + } + + public static NodeCategory mergeNodeCategories(NodeCategory a, NodeCategory b) { + if (a == b) { + return a; + } + + // Make sure c0 < c1; + boolean sorted = a.ordinal() < b.ordinal(); + final NodeCategory c0 = sorted ? a : b; + final NodeCategory c1 = sorted ? b : a; + + // takes advantage of c0 < c1 + return switch (c0) { + case STRUCTURE, STRUCTURE_INTERMEDIATE -> throw new IllegalArgumentException( + "Incompatible node types: %s, %s".formatted(a, b)); + case RM_TYPE, FOUNDATION -> NodeCategory.FOUNDATION_EXTENDED; + case FOUNDATION_EXTENDED -> throw new IllegalArgumentException( + "Inconsistent node types: %s, %s".formatted(a, b)); + }; + } + + public static List pathNodes(AqlObjectPath path) { + return Optional.ofNullable(path).map(AqlObjectPath::getPathNodes).orElseGet(List::of); + } + + private NodeInfo nodeTypeInfoForPathAtLevel(IdentifiedPath ip, int level) { + Pair>> aNodeWithInfo = pathAttributeInfo.get(ip); + ANode aNode = aNodeWithInfo.getLeft(); + Map> attributeInfos = aNodeWithInfo.getRight(); + List pathNodes = pathNodes(ip.getPath()); + String attribute = null; + AttInfo attInfo = null; + for (int i = 0; i <= level; i++) { + attribute = pathNodes.get(i).getAttribute(); + attInfo = attributeInfos.get(aNode).get(attribute); + aNode = aNode.getAttribute(attribute); + } + + NodeCategory nodeCategory = aNode.getCategories().stream() + .reduce(PathInfo::mergeNodeCategories) + .orElseThrow(); + + return new NodeInfo( + nodeCategory, + Optional.ofNullable(attInfo).map(AttInfo::targetTypes).orElse(aNode.getCandidateTypes()), + level < 0 + ? List.of() + : Collections.unmodifiableList(pathNodes(ip.getPath()).subList(0, level + 1)), + Optional.ofNullable(attInfo).map(AttInfo::multipleValued).orElse(false), + Optional.ofNullable(attInfo) + .map(AttInfo::targetTypes) + .>map(t -> SetUtils.intersection(t, DV_ORDERED_TYPES)) + .orElse(Collections.emptySet())); + } + + private enum QueryClause { + SELECT, + WHERE, + ORDER_BY + } + + public static Map createPathInfos( + AqlQuery aqlQuery, Map containsDescs) { + Map pathCohesion = + PathCohesionAnalysis.analyzePathCohesion(aqlQuery); + + Map> pathToQueryClause = Collections.unmodifiableMap(Stream.of( + aqlQuery.getSelect().getStatement().stream() + .flatMap(AqlQueryUtils::allIdentifiedPaths) + .map(p -> Pair.of(p, QueryClause.SELECT)), + streamWhereConditions(aqlQuery.getWhere()) + .flatMap(AqlQueryUtils::allIdentifiedPaths) + .map(p -> Pair.of(p, QueryClause.WHERE)), + Optional.of(aqlQuery).map(AqlQuery::getOrderBy).stream() + .flatMap(Collection::stream) + .map(OrderByExpression::getStatement) + .map(p -> Pair.of(p, QueryClause.ORDER_BY))) + .flatMap(s -> s) + .collect(Collectors.groupingBy( + Pair::getLeft, + LinkedHashMap::new, + Collectors.mapping(Pair::getRight, Collectors.toUnmodifiableSet())))); + + return containsDescs.entrySet().stream() + .filter(e -> pathCohesion.containsKey(e.getKey())) + .filter(e -> !(e.getKey() instanceof ContainmentClassExpression cce + && RmConstants.EHR.equals(cce.getType()))) + .collect(Collectors.toMap( + Entry::getValue, + e -> new PathInfo(pathCohesion.get(e.getKey()), pathToQueryClause), + (a, b) -> null, + LinkedHashMap::new)); + } + + public PathCohesionTreeNode getCohesionTreeRoot() { + return cohesionTreeRoot; + } + + public NodeCategory getNodeCategory(PathCohesionTreeNode node) { + return Optional.of(node).map(nodeTypeInfo::get).map(NodeInfo::category).orElseThrow(); + } + + public Set getTargetTypes(PathCohesionTreeNode node) { + return Optional.of(node).map(nodeTypeInfo::get).map(NodeInfo::rmTypes).orElseThrow(); + } + + public Set getDvOrderedTypes(PathCohesionTreeNode node) { + return Optional.of(node) + .map(nodeTypeInfo::get) + .map(NodeInfo::dvOrderedTypes) + .orElseThrow(); + } + + public boolean isUsedInSelect(PathCohesionTreeNode node) { + return Optional.of(node).stream() + .map(PathCohesionTreeNode::getPathsEndingAtNode) + .flatMap(List::stream) + .map(pathToQueryClause::get) + .filter(Objects::nonNull) + .flatMap(Set::stream) + .anyMatch(QueryClause.SELECT::equals); + } + + public boolean isUsedInWhereOrOrderBy(PathCohesionTreeNode node) { + return Optional.of(node).stream() + .map(PathCohesionTreeNode::getPathsEndingAtNode) + .flatMap(List::stream) + .map(pathToQueryClause::get) + .filter(Objects::nonNull) + .flatMap(Set::stream) + .anyMatch(c -> QueryClause.WHERE.equals(c) || QueryClause.ORDER_BY.equals(c)); + } + + public boolean isMultipleValued(PathCohesionTreeNode node) { + return Optional.of(node) + .map(nodeTypeInfo::get) + // BYTES are multivalued, but we store them as single JSONB Base64 value + .map(info -> !info.rmTypes.contains("BYTE") && info.multipleValued) + .orElseThrow(); + } + + public List getPathToNode(PathCohesionTreeNode node) { + return Optional.of(node) + .map(nodeTypeInfo::get) + .map(NodeInfo::pathFromRoot) + .orElseThrow(); + } + + private static boolean isData(NodeCategory nc) { + return switch (nc) { + case STRUCTURE, STRUCTURE_INTERMEDIATE -> false; + case RM_TYPE, FOUNDATION, FOUNDATION_EXTENDED -> true; + }; + } + + public JoinMode joinMode(PathCohesionTreeNode node) { + if (node.isRoot()) { + return JoinMode.ROOT; + } + boolean hasData = !node.getPathsEndingAtNode().isEmpty() + || node.getChildren().stream().anyMatch(c -> isData(getNodeCategory(c))); + if (hasData) { + return JoinMode.DATA; + } + int structureChildCount = (int) node.getChildren().stream() + .filter(c -> !isData(getNodeCategory(c))) + .count(); + return switch (structureChildCount) { + case 0 -> throw new IllegalArgumentException("Internal node without children: %s".formatted(node)); + case 1 -> JoinMode.INTERNAL_SINGLE_CHILD; + default -> JoinMode.INTERNAL_FORK; + }; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathQueryDescriptor.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathQueryDescriptor.java new file mode 100644 index 000000000..d13bbc06b --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathQueryDescriptor.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import java.util.Set; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; + +public class PathQueryDescriptor { + + public Set getRmType() { + return rmType; + } + + public enum PathType { + // Represents an extracted column (always a leaf) + EXTRACTED, + // Navigation to a structure node + STRUCTURE, + // Special case for element + ITEM, + // Json object (only leaf) + OBJECT, + // primitive value (only leaf) + PRIMITIVE + } + + private final ContainmentClassExpression root; + private final PathQueryDescriptor parent; + private final AqlObjectPath representedPath; + private final PathType type; + private final Set rmType; + + public PathQueryDescriptor( + ContainmentClassExpression root, + PathQueryDescriptor parent, + AqlObjectPath representedPath, + PathType type, + Set rmType) { + this.root = root; + this.parent = parent; + this.representedPath = representedPath; + this.type = type; + this.rmType = rmType; + } + + public ContainmentClassExpression getRoot() { + return root; + } + + public PathQueryDescriptor getParent() { + return parent; + } + + public AqlObjectPath getRepresentedPath() { + return representedPath; + } + + public PathType getType() { + return type; + } + + @Override + public String toString() { + return "PathQueryDescriptor{" + "root=" + + root + ", parent=" + + parent + ", representedPath=" + + representedPath + ", type=" + + type + ", aliasedRmType=" + + rmType + '}'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/AqlQueryWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/AqlQueryWrapper.java new file mode 100644 index 000000000..4c6faaf16 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/AqlQueryWrapper.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathInfo; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsChain; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsSetOperationWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.RmContainsWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.VersionContainsWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.orderby.OrderByWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ComparisonOperatorConditionWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.where.LogicalOperatorConditionWrapper; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.condition.ComparisonOperatorCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.ExistsCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.LikeCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.LogicalOperatorCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.MatchesCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.NotCondition; +import org.ehrbase.openehr.sdk.aql.dto.condition.WhereCondition; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.Containment; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperator; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentVersionExpression; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression; +import org.ehrbase.openehr.sdk.aql.dto.select.SelectExpression; +import org.ehrbase.openehr.sdk.aql.util.AqlUtil; + +/** + * A wrapper for the AqlQuery providing context and convenience methods. + */ +public final class AqlQueryWrapper { + private final boolean distinct; + private final List selects; + private final ContainsChain containsChain; + private final ConditionWrapper where; + private final List orderBy; + private final Long limit; + private final Long offset; + private final Map pathInfos; + + /** + * @param distinct + * @param selects + * @param containsChain + * @param where + * @param orderBy + * @param limit + * @param offset + * @param pathInfos + */ + public AqlQueryWrapper( + boolean distinct, + List selects, + ContainsChain containsChain, + ConditionWrapper where, + List orderBy, + Long limit, + Long offset, + Map pathInfos) { + this.distinct = distinct; + this.selects = selects; + this.containsChain = containsChain; + this.where = where; + this.orderBy = orderBy; + this.limit = limit; + this.offset = offset; + this.pathInfos = pathInfos; + } + + public Stream nonPrimitiveSelects() { + return selects.stream().filter(sd -> sd.type() != SelectWrapper.SelectType.PRIMITIVE); + } + + /** + * Provides a wrapper for the AqlQuery providing context and convenience methods. + * + * @param aqlQuery + * @return + */ + public static AqlQueryWrapper create(AqlQuery aqlQuery) { + Map containsDescs; + ContainsChain fromClause; + { + containsDescs = new LinkedHashMap<>(); + AbstractContainmentExpression fromRoot = (AbstractContainmentExpression) aqlQuery.getFrom(); + AqlUtil.streamContainments(fromRoot) + .filter(ContainmentClassExpression.class::isInstance) + .map(ContainmentClassExpression.class::cast) + .forEach(c -> containsDescs.put(c, new RmContainsWrapper(c))); + // Version descriptors require the descriptor of the child + AqlUtil.streamContainments(fromRoot) + .filter(ContainmentVersionExpression.class::isInstance) + .map(ContainmentVersionExpression.class::cast) + .forEach(c -> containsDescs.put(c, new VersionContainsWrapper(c.getIdentifier(), (RmContainsWrapper) + containsDescs.get(c.getContains())))); + + fromClause = buildContainsChain(fromRoot, containsDescs); + } + + List selects = aqlQuery.getSelect().getStatement().stream() + .map(s -> buildSelectDescriptor(containsDescs, s)) + .toList(); + + ConditionWrapper where = Optional.of(aqlQuery) + .map(AqlQuery::getWhere) + .map(w -> buildWhereDescriptor(w, containsDescs, false)) + .orElse(null); + + List orderBy = CollectionUtils.emptyIfNull(aqlQuery.getOrderBy()).stream() + .map(o -> buildOrderByDescriptor(o, containsDescs)) + .toList(); + + Map pathInfos = PathInfo.createPathInfos(aqlQuery, containsDescs); + + return new AqlQueryWrapper( + aqlQuery.getSelect().isDistinct(), + selects, + fromClause, + where, + orderBy, + aqlQuery.getLimit(), + aqlQuery.getOffset(), + pathInfos); + } + + private static OrderByWrapper buildOrderByDescriptor( + OrderByExpression expression, Map containsDescs) { + // TODO: expression.statement.rootPredicate once we support them + return new OrderByWrapper( + expression.getStatement(), + expression.getSymbol(), + containsDescs.get(expression.getStatement().getRoot())); + } + + private static ContainsChain buildContainsChain( + Containment root, Map containsDescs) { + final List chain = new ArrayList<>(); + final ContainsSetOperationWrapper setOperator; + + Containment next = root; + while (next instanceof AbstractContainmentExpression c) { + + chain.add(containsDescs.get(next)); + if (next instanceof ContainmentVersionExpression) { + // Version descriptor represents itself and its child, so the child itself is not added + next = ((AbstractContainmentExpression) c.getContains()).getContains(); + } else { + next = c.getContains(); + } + } + + if (next instanceof ContainmentSetOperator o) { + setOperator = new ContainsSetOperationWrapper( + o.getSymbol(), + o.getValues().stream() + .map(c -> buildContainsChain(c, containsDescs)) + .toList()); + } else { + setOperator = null; + } + + return new ContainsChain(chain, setOperator); + } + + private static SelectWrapper buildSelectDescriptor( + Map containsDescs, SelectExpression s) { + Pair typeAndPath = + switch (s.getColumnExpression()) { + case IdentifiedPath i -> Pair.of(SelectWrapper.SelectType.PATH, i); + case AggregateFunction af -> Pair.of( + SelectWrapper.SelectType.AGGREGATE_FUNCTION, af.getIdentifiedPath()); + case Primitive __ -> Pair.of(SelectWrapper.SelectType.PRIMITIVE, null); + default -> throw new IllegalArgumentException("Unknown ColumnExpression type in SELECT"); + }; + return new SelectWrapper( + s, + typeAndPath.getLeft(), + Optional.of(typeAndPath) + .map(Pair::getRight) + .map(IdentifiedPath::getRoot) + .map(containsDescs::get) + .orElse(null)); + } + + private static ConditionWrapper buildWhereDescriptor( + WhereCondition where, Map containsDescs, boolean negate) { + return switch (where) { + case ComparisonOperatorCondition c -> new ComparisonOperatorConditionWrapper( + new ComparisonOperatorConditionWrapper.IdentifiedPathWrapper( + containsDescs.get(((IdentifiedPath) c.getStatement()).getRoot()), + (IdentifiedPath) c.getStatement()), + ConditionWrapper.ComparisonConditionOperator.valueOf( + c.getSymbol().name(), negate), + (Primitive) c.getValue()); + case MatchesCondition c -> negate + ? new LogicalOperatorConditionWrapper( + ConditionWrapper.LogicalConditionOperator.OR, + c.getValues().stream() + .map(Primitive.class::cast) + .map(v -> (ConditionWrapper) new ComparisonOperatorConditionWrapper( + new ComparisonOperatorConditionWrapper.IdentifiedPathWrapper( + containsDescs.get( + c.getStatement().getRoot()), + c.getStatement()), + ConditionWrapper.ComparisonConditionOperator.NEQ, + v)) + .toList()) + : new ComparisonOperatorConditionWrapper( + new ComparisonOperatorConditionWrapper.IdentifiedPathWrapper( + containsDescs.get(c.getStatement().getRoot()), c.getStatement()), + ConditionWrapper.ComparisonConditionOperator.MATCHES, + c.getValues().stream().map(Primitive.class::cast).toList()); + case LikeCondition c -> { + ComparisonOperatorConditionWrapper condition = new ComparisonOperatorConditionWrapper( + new ComparisonOperatorConditionWrapper.IdentifiedPathWrapper( + containsDescs.get(c.getStatement().getRoot()), c.getStatement()), + ConditionWrapper.ComparisonConditionOperator.LIKE, + (Primitive) c.getValue()); + yield negate + ? new LogicalOperatorConditionWrapper( + ConditionWrapper.LogicalConditionOperator.NOT, List.of(condition)) + : condition; + } + case ExistsCondition c -> { + ComparisonOperatorConditionWrapper comparisonOperatorConditionDescriptor = + new ComparisonOperatorConditionWrapper( + new ComparisonOperatorConditionWrapper.IdentifiedPathWrapper( + containsDescs.get(c.getValue().getRoot()), c.getValue()), + ConditionWrapper.ComparisonConditionOperator.EXISTS, + List.of()); + yield negate + ? new LogicalOperatorConditionWrapper( + ConditionWrapper.LogicalConditionOperator.NOT, + List.of(comparisonOperatorConditionDescriptor)) + : comparisonOperatorConditionDescriptor; + } + case LogicalOperatorCondition c -> new LogicalOperatorConditionWrapper( + switch (c.getSymbol()) { + case OR -> negate + ? ConditionWrapper.LogicalConditionOperator.AND + : ConditionWrapper.LogicalConditionOperator.OR; + case AND -> negate + ? ConditionWrapper.LogicalConditionOperator.OR + : ConditionWrapper.LogicalConditionOperator.AND; + }, + c.getValues().stream() + .map(w -> buildWhereDescriptor(w, containsDescs, negate)) + .toList()); + case NotCondition c -> buildWhereDescriptor(c.getConditionDto(), containsDescs, !negate); + case null -> throw new IllegalArgumentException( + "Encountered null reference instead of WhereCondition object"); + default -> throw new IllegalArgumentException( + "Unknown WhereCondition class: %s".formatted(where.getClass())); + }; + } + + public boolean distinct() { + return distinct; + } + + public List selects() { + return selects; + } + + public ContainsChain containsChain() { + return containsChain; + } + + public ConditionWrapper where() { + return where; + } + + public List orderBy() { + return orderBy; + } + + public Long limit() { + return limit; + } + + public Long offset() { + return offset; + } + + public Map pathInfos() { + return pathInfos; + } + + @Override + public String toString() { + return "AqlQueryWrapper[" + "distinct=" + + distinct + ", " + "selects=" + + selects + ", " + "containsChain=" + + containsChain + ", " + "where=" + + where + ", " + "orderBy=" + + orderBy + ", " + "limit=" + + limit + ", " + "offset=" + + offset + ", " + "pathInfos=" + + pathInfos + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsChain.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsChain.java new file mode 100644 index 000000000..fd9222f4f --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsChain.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.contains; + +import java.util.Collections; +import java.util.List; + +public final class ContainsChain { + private final List chain; + private final ContainsSetOperationWrapper trailingSetOperation; + + public ContainsChain(List chain, ContainsSetOperationWrapper trailingSetOperation) { + this.chain = Collections.unmodifiableList(chain); + this.trailingSetOperation = trailingSetOperation; + } + + public boolean hasTrailingSetOperation() { + return trailingSetOperation != null; + } + + public int size() { + return chain.size() + (hasTrailingSetOperation() ? 1 : 0); + } + + public List chain() { + return chain; + } + + public ContainsSetOperationWrapper trailingSetOperation() { + return trailingSetOperation; + } + + @Override + public String toString() { + return "ContainsChain[" + "chain=" + chain + ", " + "trailingSetOperation=" + trailingSetOperation + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsSetOperationWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsSetOperationWrapper.java new file mode 100644 index 000000000..4a72192a9 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsSetOperationWrapper.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.contains; + +import java.util.Collections; +import java.util.List; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperatorSymbol; + +public final class ContainsSetOperationWrapper { + private final ContainmentSetOperatorSymbol operator; + private final List operands; + + public ContainsSetOperationWrapper(ContainmentSetOperatorSymbol operator, List operands) { + this.operator = operator; + this.operands = Collections.unmodifiableList(operands); + } + + public ContainmentSetOperatorSymbol operator() { + return operator; + } + + public List operands() { + return operands; + } + + @Override + public String toString() { + return "ContainsSetOperationWrapper[" + "operator=" + operator + ", " + "operands=" + operands + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsWrapper.java new file mode 100644 index 000000000..e55e5b376 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/ContainsWrapper.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.contains; + +public sealed interface ContainsWrapper permits RmContainsWrapper, VersionContainsWrapper { + + default String getRmType() { + throw new UnsupportedOperationException(); + } + + default String alias() { + throw new UnsupportedOperationException(); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/RmContainsWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/RmContainsWrapper.java new file mode 100644 index 000000000..6766fc339 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/RmContainsWrapper.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.contains; + +import java.util.List; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; + +public final class RmContainsWrapper implements ContainsWrapper { + private final ContainmentClassExpression containment; + + public RmContainsWrapper(ContainmentClassExpression containment) { + this.containment = containment; + } + + public List getPredicate() { + return containment.getPredicates(); + } + + public StructureRmType getStructureRmType() { + return StructureRmType.byTypeName(containment.getType()).orElse(null); + } + + @Override + public String getRmType() { + return StructureRmType.byTypeName(containment.getType()) + .map(StructureRmType::name) + .orElse(containment.getType()); + } + + @Override + public String alias() { + return containment.getIdentifier(); + } + + public ContainmentClassExpression containment() { + return containment; + } + + @Override + public String toString() { + return "RmContainsWrapper[" + "containment=" + containment + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/VersionContainsWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/VersionContainsWrapper.java new file mode 100644 index 000000000..a67d6d60a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/contains/VersionContainsWrapper.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.contains; + +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; + +public final class VersionContainsWrapper implements ContainsWrapper { + private final String alias; + private final RmContainsWrapper child; + + public VersionContainsWrapper(String alias, RmContainsWrapper child) { + this.alias = alias; + this.child = child; + } + + @Override + public String getRmType() { + return RmConstants.ORIGINAL_VERSION; + } + + @Override + public String alias() { + return alias; + } + + public RmContainsWrapper child() { + return child; + } + + @Override + public String toString() { + return "VersionContainsWrapper[" + "alias=" + alias + ", " + "child=" + child + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/orderby/OrderByWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/orderby/OrderByWrapper.java new file mode 100644 index 000000000..76fbe89aa --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/orderby/OrderByWrapper.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.orderby; + +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression.OrderByDirection; + +public final class OrderByWrapper { + private final IdentifiedPath identifiedPath; + private final OrderByDirection direction; + private final ContainsWrapper root; + + public OrderByWrapper(IdentifiedPath identifiedPath, OrderByDirection direction, ContainsWrapper root) { + this.identifiedPath = identifiedPath; + this.direction = direction; + this.root = root; + } + + public IdentifiedPath identifiedPath() { + return identifiedPath; + } + + public OrderByDirection direction() { + return direction; + } + + public ContainsWrapper root() { + return root; + } + + @Override + public String toString() { + return "OrderByWrapper[" + "identifiedPath=" + + identifiedPath + ", " + "direction=" + + direction + ", " + "root=" + + root + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/select/SelectWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/select/SelectWrapper.java new file mode 100644 index 000000000..fdc2c164a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/select/SelectWrapper.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.select; + +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction.AggregateFunctionName; +import org.ehrbase.openehr.sdk.aql.dto.operand.CountDistinctAggregateFunction; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.select.SelectExpression; + +public final class SelectWrapper { + private final SelectExpression selectExpression; + private final SelectType type; + private final ContainsWrapper root; + + public SelectWrapper(SelectExpression selectExpression, SelectType type, ContainsWrapper root) { + this.selectExpression = selectExpression; + this.type = type; + this.root = root; + } + + public enum SelectType { + PATH, + PRIMITIVE, + AGGREGATE_FUNCTION, + FUNCTION + } + + public String getSelectAlias() { + return selectExpression.getAlias(); + } + + public Optional getIdentifiedPath() { + return Optional.ofNullable( + switch (type) { + case PATH -> selectExpression.getColumnExpression(); + case PRIMITIVE -> null; + case AGGREGATE_FUNCTION -> ((AggregateFunction) selectExpression.getColumnExpression()) + .getIdentifiedPath(); + case FUNCTION -> throw new UnsupportedOperationException("Not implemented"); + }) + .map(IdentifiedPath.class::cast); + } + + public AggregateFunctionName getAggregateFunctionName() { + if (type == SelectType.AGGREGATE_FUNCTION) { + return ((AggregateFunction) selectExpression.getColumnExpression()).getFunctionName(); + } + throw new UnsupportedOperationException(); + } + + public boolean isCountDistinct() { + if (type != SelectType.AGGREGATE_FUNCTION) { + throw new UnsupportedOperationException(); + } + return selectExpression.getColumnExpression() instanceof CountDistinctAggregateFunction; + } + + public Primitive getPrimitive() { + if (type != SelectType.PRIMITIVE) { + throw new UnsupportedOperationException(); + } + return (Primitive) selectExpression.getColumnExpression(); + } + + public Optional getSelectPath() { + if (type == SelectType.PATH) { + return Optional.of(Stream.of( + root.alias(), + getIdentifiedPath() + .map(IdentifiedPath::getPath) + .map(AqlObjectPath::render) + .orElse(null)) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("/"))); + } else { + return Optional.empty(); + } + } + + public SelectExpression selectExpression() { + return selectExpression; + } + + public SelectType type() { + return type; + } + + public ContainsWrapper root() { + return root; + } + + @Override + public String toString() { + return "SelectWrapper[" + "selectExpression=" + + selectExpression + ", " + "type=" + + type + ", " + "root=" + + root + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/ComparisonOperatorConditionWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/ComparisonOperatorConditionWrapper.java new file mode 100644 index 000000000..26f9fb0d5 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/ComparisonOperatorConditionWrapper.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.where; + +import java.util.Collections; +import java.util.List; +import org.ehrbase.openehr.aqlengine.querywrapper.contains.ContainsWrapper; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive; + +public final class ComparisonOperatorConditionWrapper implements ConditionWrapper { + private final IdentifiedPathWrapper leftComparisonOperand; + private final ComparisonConditionOperator operator; + private final List rightComparisonOperands; + + public ComparisonOperatorConditionWrapper( + IdentifiedPathWrapper leftComparisonOperand, + ComparisonConditionOperator operator, + List rightComparisonOperands) { + this.leftComparisonOperand = leftComparisonOperand; + this.operator = operator; + this.rightComparisonOperands = Collections.unmodifiableList(rightComparisonOperands); + } + + public ComparisonOperatorConditionWrapper( + IdentifiedPathWrapper leftComparisonOperand, + ComparisonConditionOperator operator, + Primitive rightComparisonOperand) { + this(leftComparisonOperand, operator, List.of(rightComparisonOperand)); + } + + public IdentifiedPathWrapper leftComparisonOperand() { + return leftComparisonOperand; + } + + public ComparisonConditionOperator operator() { + return operator; + } + + public List rightComparisonOperands() { + return rightComparisonOperands; + } + + @Override + public String toString() { + return "ComparisonOperatorConditionWrapper[" + "leftComparisonOperand=" + + leftComparisonOperand + ", " + "operator=" + + operator + ", " + "rightComparisonOperands=" + + rightComparisonOperands + ']'; + } + + public record IdentifiedPathWrapper(ContainsWrapper root, IdentifiedPath path) { + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/ConditionWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/ConditionWrapper.java new file mode 100644 index 000000000..f8d7ae1d0 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/ConditionWrapper.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.where; + +import java.util.List; +import java.util.function.Function; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslAndQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFalseQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslOrQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslTrueQueryCondition; + +public sealed interface ConditionWrapper permits ComparisonOperatorConditionWrapper, LogicalOperatorConditionWrapper { + enum LogicalConditionOperator { + AND(AslAndQueryCondition::new, AslTrueQueryCondition.class, AslFalseQueryCondition.class), + OR(AslOrQueryCondition::new, AslFalseQueryCondition.class, AslTrueQueryCondition.class), + NOT(l -> l.stream().findFirst().map(AslNotQueryCondition::new).orElse(null), Void.class, Void.class); + + LogicalConditionOperator( + Function, AslQueryCondition> setOperator, + Class noopCondition, + Class shortCircuitCondition) { + this.setOperator = setOperator; + this.noopCondition = noopCondition; + this.shortCircuitCondition = shortCircuitCondition; + } + + private final Function, AslQueryCondition> setOperator; + private final Class noopCondition; + private final Class shortCircuitCondition; + + public AslQueryCondition build(List params) { + return setOperator.apply(params); + } + + public boolean filterNotNoop(AslQueryCondition condition) { + return !noopCondition.isInstance(condition); + } + + public boolean filterShortCircuit(AslQueryCondition condition) { + return shortCircuitCondition.isInstance(condition); + } + } + + enum ComparisonConditionOperator { + EXISTS(AslConditionOperator.IS_NOT_NULL), + LIKE(AslConditionOperator.LIKE), + MATCHES(AslConditionOperator.IN), + EQ(AslConditionOperator.EQ), + NEQ(AslConditionOperator.NEQ), + GT_EQ(AslConditionOperator.GT_EQ), + GT(AslConditionOperator.GT), + LT_EQ(AslConditionOperator.LT_EQ), + LT(AslConditionOperator.LT); + + private final AslConditionOperator aslOperator; + + ComparisonConditionOperator(AslConditionOperator aslOperator) { + this.aslOperator = aslOperator; + } + + public AslConditionOperator getAslOperator() { + return aslOperator; + } + + public ComparisonConditionOperator negate() { + return switch (this) { + case EXISTS, MATCHES, LIKE -> throw new UnsupportedOperationException( + "No operator known to represent negated " + this); + case EQ -> NEQ; + case NEQ -> EQ; + case GT_EQ -> LT; + case GT -> LT_EQ; + case LT_EQ -> GT; + case LT -> GT_EQ; + }; + } + + public static ComparisonConditionOperator valueOf(String name, boolean negated) { + ComparisonConditionOperator operator = valueOf(name); + return negated ? operator.negate() : operator; + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/LogicalOperatorConditionWrapper.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/LogicalOperatorConditionWrapper.java new file mode 100644 index 000000000..af0f71e4a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/querywrapper/where/LogicalOperatorConditionWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.querywrapper.where; + +import java.util.Collections; +import java.util.List; + +public final class LogicalOperatorConditionWrapper implements ConditionWrapper { + private final LogicalConditionOperator operator; + private final List logicalOperands; + + public LogicalOperatorConditionWrapper(LogicalConditionOperator operator, List logicalOperands) { + this.operator = operator; + this.logicalOperands = Collections.unmodifiableList(logicalOperands); + } + + public LogicalConditionOperator operator() { + return operator; + } + + public List logicalOperands() { + return logicalOperands; + } + + @Override + public String toString() { + return "LogicalOperatorConditionWrapper[" + "operator=" + + operator + ", " + "logicalOperands=" + + logicalOperands + ']'; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/AqlQueryRepository.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/AqlQueryRepository.java new file mode 100644 index 000000000..448b3b5de --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/AqlQueryRepository.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.repository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.api.service.SystemService; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper.SelectType; +import org.ehrbase.openehr.aqlengine.sql.AqlSqlQueryBuilder; +import org.ehrbase.openehr.aqlengine.sql.postprocessor.AqlSqlResultPostprocessor; +import org.ehrbase.openehr.aqlengine.sql.postprocessor.DefaultResultPostprocessor; +import org.ehrbase.openehr.aqlengine.sql.postprocessor.ExtractedColumnResultPostprocessor; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction.AggregateFunctionName; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.jooq.Record; +import org.jooq.SelectQuery; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * Executes ASL queries as SQL, and converts the results + */ +@Repository +@Transactional(readOnly = true) +public class AqlQueryRepository { + + private static final AqlSqlResultPostprocessor NOOP_POSTPROCESSOR = v -> v; + private final SystemService systemService; + private final KnowledgeCacheService knowledgeCache; + private final AqlSqlQueryBuilder queryBuilder; + + public AqlQueryRepository( + SystemService systemService, KnowledgeCacheService knowledgeCache, AqlSqlQueryBuilder queryBuilder) { + this.queryBuilder = queryBuilder; + this.systemService = systemService; + this.knowledgeCache = knowledgeCache; + } + + /** + * Prepares the full SQL query. Build the structure from AQL and selects postprocess based on the given + * selects. + * + * @param aslQuery to create the actual SQL query from. + * @param selects to obtain {@link AqlSqlResultPostprocessor} for. + * + * @see #executeQuery(PreparedQuery) + * @see #getQuerySql(PreparedQuery) + * @see #explainQuery(boolean, PreparedQuery) + */ + public PreparedQuery prepareQuery(AslRootQuery aslQuery, List selects) { + + final SelectQuery selectQuery = queryBuilder.buildSqlQuery(aslQuery); + + final Map postProcessors; + if (selects.isEmpty()) { + // one column with COUNT: see AqlSqlLayer::addSyntheticSelect + postProcessors = Map.of(0, NOOP_POSTPROCESSOR); + } else { + postProcessors = IntStream.range(0, selects.size()) + .boxed() + .collect(Collectors.toMap(i -> i, i -> getPostProcessor(selects.get(i)))); + } + return new PreparedQuery(selectQuery, postProcessors); + } + + public List> executeQuery(PreparedQuery preparedQuery) { + return preparedQuery.selectQuery.stream() + .map(r -> postProcessDbRecord(r, preparedQuery.postProcessors)) + .toList(); + } + + public static String getQuerySql(PreparedQuery preparedQuery) { + return preparedQuery.selectQuery.getSQL(); + } + + public String explainQuery(boolean analyze, PreparedQuery preparedQuery) { + return queryBuilder.explain(analyze, preparedQuery.selectQuery).formatJSON(); + } + + private AqlSqlResultPostprocessor getPostProcessor(SelectWrapper select) { + // datatype must remain numeric for count, sum, avg + if (select.type() == SelectType.AGGREGATE_FUNCTION + && EnumSet.of(AggregateFunctionName.COUNT, AggregateFunctionName.SUM, AggregateFunctionName.AVG) + .contains(select.getAggregateFunctionName())) { + return NOOP_POSTPROCESSOR; + } + + Optional selectPath = select.getIdentifiedPath().map(IdentifiedPath::getPath); + List nodes = selectPath.map(AqlObjectPath::getPathNodes).orElseGet(Collections::emptyList); + // extracted column by full path + return AslExtractedColumn.find(select.root(), selectPath.orElse(null)) + // OR extracted column by archetype_node_id suffix + .or(() -> Optional.of(AslExtractedColumn.ARCHETYPE_NODE_ID) + .filter(e -> + selectPath.filter(p -> p.endsWith(e.getPath())).isPresent())) + // OR extracted column ORIGINAL_VERSION.commit_audit + .or(() -> AslExtractedColumn.find( + RmConstants.AUDIT_DETAILS, + new AqlObjectPath(nodes.stream().skip(1).toList())) + .filter(e -> RmConstants.ORIGINAL_VERSION.equals( + select.root().getRmType())) + .filter(e -> nodes.stream() + .limit(1) + .map(PathNode::getAttribute) + .allMatch("commit_audit"::equals))) + .map( + ec -> new ExtractedColumnResultPostprocessor(ec, knowledgeCache, systemService.getSystemId())) + .orElseGet(DefaultResultPostprocessor::new); + } + + private static List postProcessDbRecord(Record r, Map postProcessors) { + List resultRow = new ArrayList<>(r.size()); + for (int i = 0; i < r.size(); i++) { + resultRow.add(postProcessors.get(i).postProcessColumn(r.get(i))); + } + return resultRow; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/PreparedQuery.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/PreparedQuery.java new file mode 100644 index 000000000..2b0d040ba --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/PreparedQuery.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.repository; + +import java.util.List; +import java.util.Map; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.sql.postprocessor.AqlSqlResultPostprocessor; +import org.jooq.Record; +import org.jooq.SelectQuery; + +/** + * Represents a prepared but not executed SQL query for the {@link AqlQueryRepository} that is constructed by + * {@link AqlQueryRepository#prepareQuery(AslRootQuery, List)}. This prepared query can be executed by + * {@link AqlQueryRepository#executeQuery(PreparedQuery)} or can be used to obtain the raw SQL query using + * {@link AqlQueryRepository#getQuerySql(PreparedQuery)}} or the query planer output + * {@link AqlQueryRepository#explainQuery(boolean, PreparedQuery)}. + */ +public final class PreparedQuery { + + final SelectQuery selectQuery; + final Map postProcessors; + + public PreparedQuery(SelectQuery selectQuery, Map postProcessors) { + this.selectQuery = selectQuery; + this.postProcessors = postProcessors; + } + + @Override + public String toString() { + return selectQuery.getSQL(); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImp.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImp.java new file mode 100644 index 000000000..107bd8e68 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImp.java @@ -0,0 +1,460 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.service; + +import static org.ehrbase.openehr.aqlengine.AqlParameterReplacement.replaceParameters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.re2j.Pattern; +import java.lang.constant.Constable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.LongStream; +import java.util.stream.Stream; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.ehrbase.api.dto.AqlQueryContext; +import org.ehrbase.api.dto.AqlQueryRequest; +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.BadGatewayException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.api.exception.UnprocessableEntityException; +import org.ehrbase.api.service.AqlQueryService; +import org.ehrbase.openehr.aqlengine.AqlQueryUtils; +import org.ehrbase.openehr.aqlengine.asl.AqlSqlLayer; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.featurecheck.AqlQueryFeatureCheck; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper; +import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper.SelectType; +import org.ehrbase.openehr.aqlengine.repository.AqlQueryRepository; +import org.ehrbase.openehr.aqlengine.repository.PreparedQuery; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperator; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperatorSymbol; +import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.ehrbase.openehr.sdk.aql.parser.AqlParseException; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.ehrbase.openehr.sdk.aql.render.AqlRenderer; +import org.ehrbase.openehr.sdk.aql.util.AqlUtil; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.QueryResultDto; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.query.ResultHolder; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.ehrbase.openehr.sdk.validation.terminology.ExternalTerminologyValidation; +import org.jooq.exception.DataAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; + +@Service +public class AqlQueryServiceImp implements AqlQueryService { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final AqlQueryRepository aqlQueryRepository; + private final ExternalTerminologyValidation tsAdapter; + private final AqlSqlLayer aqlSqlLayer; + private final AqlQueryFeatureCheck aqlQueryFeatureCheck; + private final ObjectMapper objectMapper; + private final AqlQueryContext aqlQueryContext; + + @Value("${ehrbase.rest.aql.default-limit:}") + private Long defaultLimit; + + @Value("${ehrbase.rest.aql.max-limit:}") + private Long maxLimit; + + @Value("${ehrbase.rest.aql.max-fetch:}") + private Long maxFetch; + + enum FetchPrecedence { + /** + * Fail if both fetch and limit are present + */ + REJECT, + /** + * Take minimum of fetch and limit for limit; + * fail if query has offset + */ + MIN_FETCH; + } + + @Value("${ehrbase.rest.aql.fetch-precedence:REJECT}") + private FetchPrecedence fetchPrecedence = FetchPrecedence.REJECT; + + private static Long applyFetchPrecedence( + FetchPrecedence fetchPrecedence, Long queryLimit, Long queryOffset, Long fetchParam, Long offsetParam) { + if (fetchParam == null) { + if (offsetParam != null) { + throw new UnprocessableEntityException("Query parameter for offset provided, but no fetch parameter"); + } + return queryLimit; + } else if (queryLimit == null) { + assert queryOffset == null; + return fetchParam; + } + + return switch (fetchPrecedence) { + case REJECT -> { + throw new UnprocessableEntityException( + "Query contains a LIMIT clause, fetch and offset parameters must not be used (with fetch precedence %s)" + .formatted(fetchPrecedence)); + } + case MIN_FETCH -> { + if (queryOffset != null) { + throw new UnprocessableEntityException( + "Query contains a OFFSET clause, fetch parameter must not be used (with fetch precedence %s)" + .formatted(fetchPrecedence)); + } + yield Math.min(queryLimit, fetchParam); + } + }; + } + + @Autowired + public AqlQueryServiceImp( + AqlQueryRepository aqlQueryRepository, + ExternalTerminologyValidation tsAdapter, + AqlSqlLayer aqlSqlLayer, + AqlQueryFeatureCheck aqlQueryFeatureCheck, + ObjectMapper objectMapper, + AqlQueryContext aqlQueryContext) { + this.aqlQueryRepository = aqlQueryRepository; + this.tsAdapter = tsAdapter; + this.aqlSqlLayer = aqlSqlLayer; + this.aqlQueryFeatureCheck = aqlQueryFeatureCheck; + this.objectMapper = objectMapper; + this.aqlQueryContext = aqlQueryContext; + } + + @Override + public QueryResultDto query(AqlQueryRequest aqlQuery) { + return queryAql(aqlQuery); + } + + private QueryResultDto queryAql(AqlQueryRequest aqlQueryRequest) { + + if (defaultLimit != null) { + aqlQueryContext.setMetaProperty(AqlQueryContext.EhrbaseMetaProperty.DEFAULT_LIMIT, defaultLimit); + } + if (maxLimit != null) { + aqlQueryContext.setMetaProperty(AqlQueryContext.EhrbaseMetaProperty.MAX_LIMIT, maxLimit); + } + if (maxFetch != null) { + aqlQueryContext.setMetaProperty(AqlQueryContext.EhrbaseMetaProperty.MAX_FETCH, maxFetch); + } + + // TODO: check that select aliases are not duplicated + try { + AqlQuery aqlQuery = buildAqlQuery(aqlQueryRequest, fetchPrecedence, defaultLimit, maxLimit, maxFetch); + + aqlQueryFeatureCheck.ensureQuerySupported(aqlQuery); + + try { + if (logger.isTraceEnabled()) { + logger.trace(objectMapper.writeValueAsString(aqlQuery)); + } + + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + AslRootQuery aslQuery = aqlSqlLayer.buildAslRootQuery(queryWrapper); + List nonPrimitiveSelects = + queryWrapper.nonPrimitiveSelects().toList(); + + PreparedQuery preparedQuery = aqlQueryRepository.prepareQuery(aslQuery, nonPrimitiveSelects); + + // aql debug options + if (aqlQueryContext.showExecutedSql()) { + aqlQueryContext.setMetaProperty( + AqlQueryContext.EhrbaseMetaProperty.EXECUTED_SQL, + AqlQueryRepository.getQuerySql(preparedQuery)); + } + if (aqlQueryContext.showQueryPlan()) { + // for dry-run omit analyze + boolean analyze = !aqlQueryContext.isDryRun(); + String explainedQuery = aqlQueryRepository.explainQuery(analyze, preparedQuery); + TypeReference> typeRef = new TypeReference<>() {}; + aqlQueryContext.setMetaProperty( + AqlQueryContext.EhrbaseMetaProperty.QUERY_PLAN, + objectMapper.readValue(explainedQuery, typeRef)); + } + + if (aqlQueryContext.showExecutedAql()) { + aqlQueryContext.setExecutedAql(AqlRenderer.render(aqlQuery)); + } + + Optional.of(queryWrapper) + .map(AqlQueryWrapper::limit) + .map(Long::intValue) + .ifPresent(limit -> { + aqlQueryContext.setMetaProperty(AqlQueryContext.EhrbaseMetaProperty.FETCH, limit); + // in case only a limit was used we define the default offset as 0 + aqlQueryContext.setMetaProperty( + AqlQueryContext.EhrbaseMetaProperty.OFFSET, + Optional.of(queryWrapper) + .map(AqlQueryWrapper::offset) + .map(Long::intValue) + .orElse(0)); + }); + + List> resultData; + if (aqlQueryContext.isDryRun()) { + resultData = List.of(); + } else { + resultData = executeQuery(preparedQuery, queryWrapper, nonPrimitiveSelects); + aqlQueryContext.setMetaProperty(AqlQueryContext.EhrbaseMetaProperty.RESULT_SIZE, resultData.size()); + } + return formatResult(queryWrapper.selects(), resultData); + + } catch (IllegalArgumentException | JsonProcessingException e) { + // regular IllegalArgumentException, not due to illegal query parameters + throw new InternalServerException(e.getMessage(), e); + } + } catch (RestClientException e) { + throw new BadGatewayException(errorMessage("Bad gateway", e), e); + } catch (DataAccessException e) { + throw new InternalServerException(errorMessage("Data Access Error", e), e); + } catch (AqlParseException e) { + throw new IllegalAqlException(errorMessage("Could not parse AQL query", e), e); + } + } + + static AqlQuery buildAqlQuery( + AqlQueryRequest aqlQueryRequest, + FetchPrecedence fetchPrecedence, + Long defaultLimit, + Long maxLimit, + Long maxFetch) { + + AqlQuery aqlQuery = AqlQueryParser.parse(aqlQueryRequest.queryString()); + + // apply limit and offset - where the definitions from the aql are the precedence + Optional qr = Optional.of(aqlQueryRequest); + Long fetchParam = aqlQueryRequest.fetch(); + Long offsetParam = aqlQueryRequest.offset(); + + Long queryLimit = aqlQuery.getLimit(); + Long queryOffset = aqlQuery.getOffset(); + + if (queryLimit != null && maxLimit != null && queryLimit > maxLimit) { + throw new UnprocessableEntityException( + "Query LIMIT %d exceeds maximum limit %d".formatted(queryLimit, maxLimit)); + } + + if (fetchParam != null && maxFetch != null && fetchParam > maxFetch) { + throw new UnprocessableEntityException( + "Fetch parameter %d exceeds maximum fetch %d".formatted(fetchParam, maxFetch)); + } + + Long limit = applyFetchPrecedence(fetchPrecedence, queryLimit, queryOffset, fetchParam, offsetParam); + + aqlQuery.setLimit(ObjectUtils.firstNonNull(limit, defaultLimit)); + aqlQuery.setOffset(ObjectUtils.firstNonNull(offsetParam, queryOffset)); + + // postprocess + replaceParameters(aqlQuery, aqlQueryRequest.parameters()); + replaceEhrPaths(aqlQuery); + + return aqlQuery; + } + + private List> executeQuery( + PreparedQuery preparedQuery, AqlQueryWrapper queryWrapper, List nonPrimitiveSelects) { + + List> resultData = aqlQueryRepository.executeQuery(preparedQuery); + + if (nonPrimitiveSelects.isEmpty()) { + // only primitives selected: only a count() was performed, so the list must be constructed + resultData = LongStream.range(0, (long) resultData.getFirst().getFirst()) + .>mapToObj(i -> new ArrayList<>()) + .toList(); + } + + List selects = queryWrapper.selects(); + // Since we do not add primitive value selects to the SQL query, we add them after the query was + // executed + for (int i = 0; i < selects.size(); i++) { + SelectWrapper sd = selects.get(i); + if (sd.type() == SelectType.PRIMITIVE) { + Constable value = sd.getPrimitive().getValue(); + for (List row : resultData) { + row.add(i, value); + } + } + } + return resultData; + } + + private QueryResultDto formatResult(List selectFields, List> resultData) { + + Map columns = new LinkedHashMap<>(); + for (int i = 0; i < selectFields.size(); i++) { + SelectWrapper namePath = selectFields.get(i); + columns.put( + Optional.of(namePath).map(SelectWrapper::getSelectAlias).orElse("#" + i), + namePath.getSelectPath().orElse(null)); + } + + QueryResultDto dto = new QueryResultDto(); + dto.setVariables(columns); + + List resultList = resultData.stream() + .map(r -> { + ResultHolder fieldMap = new ResultHolder(); + for (int i = 0; i < r.size(); i++) { + Object c = r.get(i); + fieldMap.putResult( + Optional.ofNullable(selectFields.get(i).getSelectAlias()) + .orElse("#" + i), + c); + } + + return fieldMap; + }) + .toList(); + + dto.setResultSet(resultList); + return dto; + } + + /** + * Rephrases EHR.composition and EHR.status CONTAINS statements so that they can be handled regularly by the aql engine. + * E.g. SELECT e/ehr_status FROM EHR is rewritten as SELECT s FROM EHR e CONTAINS EHR_STATUS s, + * SELECT e/composition FROM EHR is rewritten as SELECT c FROM EHR e CONTAINS COMPOSITION c. + */ + static void replaceEhrPaths(AqlQuery aqlQuery) { + replaceEhrPath(aqlQuery, "compositions", "COMPOSITION", "c"); + replaceEhrPath(aqlQuery, "ehr_status", "EHR_STATUS", "s"); + } + + /** + * Rephrases a path from EHR to EHR_STATUS as CONTAINS statement so that it can be handled regularly by the aql engine. + * E.g. SELECT e/status FROM EHR is rewritten as SELECT s FROM EHR e CONTAINS EHR_STATUS s. + */ + static void replaceEhrPath(AqlQuery aqlQuery, String ehrPath, String type, String aliasPrefix) { + + // gather paths that contain EHR/status. + List ehrPaths = AqlQueryUtils.allIdentifiedPaths(aqlQuery) + // EHR + .filter(ip -> ip.getRoot() instanceof ContainmentClassExpression cce + && cce.getType().equals(RmConstants.EHR)) + // EHR.ehrPath... + .filter(ip -> Optional.of(ip) + .map(IdentifiedPath::getPath) + .map(AqlObjectPath::getPathNodes) + .map(List::getFirst) + .map(PathNode::getAttribute) + .filter(ehrPath::equals) + .isPresent()) + .toList(); + + if (ehrPaths.isEmpty()) { + return; + } + + if (ehrPaths.stream() + .map(IdentifiedPath::getRoot) + .map(AbstractContainmentExpression::getIdentifier) + .distinct() + .count() + > 1) { + throw new AqlFeatureNotImplementedException("Multiple EHR in FROM are not supported"); + } + + if (ehrPaths.stream().map(IdentifiedPath::getRootPredicate).anyMatch(CollectionUtils::isNotEmpty)) { + throw new AqlFeatureNotImplementedException( + "Root predicates for EHR/%s are not supported".formatted(ehrPath)); + } + + if (ehrPaths.stream() + .map(IdentifiedPath::getPath) + .map(p -> p.getPathNodes().getFirst().getPredicateOrOperands()) + .distinct() + .count() + > 1) { + // could result in multiple containments + throw new AqlFeatureNotImplementedException( + "Specifying different predicates for EHR/%s is not supported".formatted(ehrPath)); + } + // determine unused alias + String alias = AqlUtil.streamContainments(aqlQuery.getFrom()) + .map(AbstractContainmentExpression::getIdentifier) + .filter(Objects::nonNull) + .filter(s -> s.matches(Pattern.quote(aliasPrefix) + "\\d*")) + .map(s -> aliasPrefix.equals(s) ? 0 : Long.parseLong(s.substring(1))) + .max(Comparator.naturalOrder()) + .map(i -> aliasPrefix + (i + 1)) + .orElse(aliasPrefix); + + // insert CONTAINS [type] (AND if needed) + // what about "SELECT e[ehr_id=…]/status from EHR e"? + ContainmentClassExpression ehrContainment = + (ContainmentClassExpression) ehrPaths.getFirst().getRoot(); + + ContainmentClassExpression ehrStatusContainment = new ContainmentClassExpression(); + ehrStatusContainment.setType(type); + ehrStatusContainment.setIdentifier(alias); + + // copy first predicate (all all are the same) + ehrPaths.stream() + .findFirst() + .map(IdentifiedPath::getPath) + .map(p -> p.getPathNodes().getFirst().getPredicateOrOperands()) + .ifPresent(ehrStatusContainment::setPredicates); + + // add containment + if (ehrContainment.getContains() == null) { + ehrContainment.setContains(ehrStatusContainment); + } else if (ehrContainment.getContains() instanceof ContainmentSetOperator cse + && cse.getSymbol() == ContainmentSetOperatorSymbol.AND) { + cse.setValues(Stream.concat(Stream.of(ehrStatusContainment), cse.getValues().stream()) + .toList()); + } else { + ContainmentSetOperator and = new ContainmentSetOperator(); + and.setSymbol(ContainmentSetOperatorSymbol.AND); + and.setValues(List.of(ehrStatusContainment, ehrContainment.getContains())); + ehrContainment.setContains(and); + } + + // rewrite paths + ehrPaths.forEach(ip -> { + ip.setRoot(ehrStatusContainment); + List pathNodes = ip.getPath().getPathNodes(); + ip.setPath(pathNodes.size() == 1 ? null : new AqlObjectPath(pathNodes.subList(1, pathNodes.size()))); + }); + } + + private static String errorMessage(String prefix, Exception e) { + return prefix + ": " + Optional.of(e).map(Throwable::getCause).orElse(e).getMessage(); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilder.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilder.java new file mode 100644 index 000000000..0b737f0ef --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilder.java @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql; + +import static org.ehrbase.jooq.pg.Tables.COMP_DATA; +import static org.ehrbase.jooq.pg.Tables.COMP_VERSION; +import static org.ehrbase.jooq.pg.Tables.EHR_FOLDER_DATA; +import static org.ehrbase.jooq.pg.Tables.EHR_FOLDER_VERSION; +import static org.ehrbase.jooq.pg.Tables.EHR_STATUS_DATA; +import static org.ehrbase.jooq.pg.Tables.EHR_STATUS_VERSION; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.jooq.pg.util.AdditionalSQLFunctions; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslAggregatingField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslComplexExtractedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslConstantField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslSubqueryField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoin; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslEncapsulatingQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslFilteringQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslPathDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRmObjectDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; +import org.ehrbase.openehr.aqlengine.sql.postprocessor.AqlSqlQueryPostProcessor; +import org.ehrbase.openehr.dbformat.RmAttributeAlias; +import org.ehrbase.openehr.dbformat.jooq.prototypes.ObjectDataTablePrototype; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath.PathNode; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.JSONB; +import org.jooq.JSONObjectAggNullStep; +import org.jooq.JoinType; +import org.jooq.Operator; +import org.jooq.Record; +import org.jooq.Record1; +import org.jooq.Result; +import org.jooq.SelectConditionStep; +import org.jooq.SelectField; +import org.jooq.SelectFieldOrAsterisk; +import org.jooq.SelectHavingStep; +import org.jooq.SelectJoinStep; +import org.jooq.SelectQuery; +import org.jooq.SelectSelectStep; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableLike; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Builds an SQL query from an ASL query + */ +@Component +public class AqlSqlQueryBuilder { + + private final DSLContext context; + private final KnowledgeCacheService knowledgeCache; + private final Optional queryPostProcessor; + + @Value("${ehrbase.aql.pg-llj-workaround}") + private boolean pgLljWorkaround = false; + + public AqlSqlQueryBuilder( + DSLContext context, + KnowledgeCacheService knowledgeCache, + Optional queryPostProcessor) { + this.context = context; + this.knowledgeCache = knowledgeCache; + this.queryPostProcessor = queryPostProcessor; + } + + public static String subqueryAlias(AslQuery aslQuery) { + return aslQuery.getAlias() + "sq"; + } + + public static String versionSubqueryAlias(AslQuery aslQuery) { + return aslQuery.getAlias() + "_version_sq"; + } + + /** + * Resolves the data and version jooq tables (sql tables or sub-queries) the AslQuery is based on + */ + static class AslQueryTables { + + private Map> dataTables = new HashMap<>(); + private Map> versionTables = new HashMap<>(); + + private AslQueryTables() {} + + Table getDataTable(AslQuery q) { + return dataTables.get(q); + } + + Table getVersionTable(AslQuery q) { + return versionTables.get(q); + } + + public void put(AslQuery q, Table dataTable, Table versionTable) { + dataTables.put(q, dataTable); + versionTables.put(q, versionTable); + } + + public void remove(AslStructureQuery aq) { + dataTables.remove(aq); + versionTables.remove(aq); + } + } + + public SelectQuery buildSqlQuery(AslRootQuery aslRootQuery) { + + AslQueryTables aslQueryToTable = new AslQueryTables(); + SelectJoinStep encapsulatingQuery = + buildEncapsulatingQuery(aslRootQuery, context::select, aslQueryToTable); + + SelectQuery query = encapsulatingQuery.getQuery(); + + // LIMIT + if (aslRootQuery.getLimit() != null) { + query.addLimit(aslRootQuery.getOffset() == null ? 0L : aslRootQuery.getOffset(), aslRootQuery.getLimit()); + } + + queryPostProcessor.ifPresent(p -> p.afterBuildSqlQuery(aslRootQuery, query)); + + return query; + } + + public Result explain(boolean analyze, SelectQuery selectQuery) { + if (analyze) { + return context.fetch("EXPLAIN (SUMMARY, COSTS, VERBOSE, FORMAT JSON, ANALYZE, TIMING) {0}", selectQuery); + } else { + return context.fetch("EXPLAIN (SUMMARY, COSTS, VERBOSE, FORMAT JSON) {0}", selectQuery); + } + } + + @Nonnull + private SelectJoinStep buildEncapsulatingQuery( + AslEncapsulatingQuery aq, Supplier> creator, AslQueryTables aslQueryToTable) { + Iterator> childIt = aq.getChildren().iterator(); + + // from + + AslQuery aslRoot = childIt.next().getLeft(); + Table root = buildQuery(aslRoot, null, aslQueryToTable); + aslQueryToTable.put(aslRoot, root, root); + SelectJoinStep from = creator.get().from(root); + + while (childIt.hasNext()) { + Pair nextChild = childIt.next(); + AslQuery childQuery = nextChild.getLeft(); + AslJoin join = nextChild.getRight(); + AslQuery target = join.getLeft(); + Table toJoin = buildQuery(childQuery, target, aslQueryToTable); + + if (pgLljWorkaround) { + EncapsulatingQueryUtils.applyPgLljWorkaround(childQuery, join, toJoin); + } + + aslQueryToTable.put(childQuery, toJoin, toJoin); + from.join(toJoin, join.getJoinType()).on(ConditionUtils.buildJoinCondition(join, aslQueryToTable)); + } + + SelectQuery query = from.getQuery(); + // select + for (AslField field : aq.getSelect()) { + SelectField sqlField = EncapsulatingQueryUtils.selectField(field, aslQueryToTable); + query.addSelect(sqlField); + } + // where + query.addConditions( + Operator.AND, + Stream.concat( + Optional.of(aq).map(AslEncapsulatingQuery::getCondition).stream(), + aq.getStructureConditions().stream()) + .map(c -> ConditionUtils.buildCondition(c, aslQueryToTable, true)) + .toList()); + + if (aq instanceof AslRootQuery rq) { + rq.getGroupByFields().stream() + .flatMap(gb -> EncapsulatingQueryUtils.groupByFields(gb, aslQueryToTable)) + .forEach(query::addGroupBy); + + // if the magnitude is needed for ORDER BY, it is added to the GROUP BY + rq.getGroupByDvOrderedMagnitudeFields().stream() + .map(f -> AdditionalSQLFunctions.jsonb_dv_ordered_magnitude((Field) + FieldUtils.field(aslQueryToTable.getDataTable(f.getInternalProvider()), f, true))) + .forEach(query::addGroupBy); + + rq.getOrderByFields().stream() + .flatMap(ob -> EncapsulatingQueryUtils.orderFields(ob, aslQueryToTable, knowledgeCache)) + .forEach(query::addOrderBy); + } + + return from; + } + + private Table buildQuery(AslQuery aslQuery, AslQuery target, AslQueryTables aslQueryToTable) { + return switch (aslQuery) { + case AslStructureQuery aq -> buildStructureQuery(aq, aslQueryToTable) + .asTable(aq.getAlias()); + case AslEncapsulatingQuery aq -> buildEncapsulatingQuery(aq, DSL::select, aslQueryToTable) + .asTable(aq.getAlias()); + case AslRmObjectDataQuery aq -> DSL.lateral( + buildDataSubquery(aq, aslQueryToTable).asTable(aq.getAlias())); + case AslFilteringQuery aq -> DSL.lateral(buildFilteringQuery(aq, aslQueryToTable.getDataTable(target)) + .asTable(aq.getAlias())); + case AslPathDataQuery aq -> DSL.lateral( + buildPathDataQuery(aq, target, aslQueryToTable).asTable(aq.getAlias())); + }; + } + + private static AslSourceRelation getTargetType(AslQuery target) { + if (target instanceof AslStructureQuery sq) { + return sq.getType(); + } else { + throw new IllegalArgumentException("target is no StructureQuery: %s".formatted(target)); + } + } + + /** + * Has to be wrapped in DSL::lateral. + * Applies "jsonb_array_elements" function, if last node is multiple valued + *

+ * Structure based: + *

+ * select "cData"."data"->'N' as "pd_0_data" + * from "ehr"."comp" as "cData" + * where ( + * "sSE_s_0"."sSE_s_0_ehr_id" = "cData"."ehr_id" + * and "sSE_s_0"."sSE_s_0_vo_id" = "cData"."vo_id" + * and "sSE_s_0"."sSE_s_0_entity_idx" = "cData"."entity_idx" + * ) + *

+ * Path data based: + *

+ * select "cData"."data"->'N' as "pd_0_data" + * + * @param aslData + * @param target + * @return + */ + private static TableLike buildPathDataQuery( + AslPathDataQuery aslData, AslQuery target, AslQueryTables aslQueryToTable) { + Table targetTable = aslQueryToTable.getDataTable(target); + + AslQuery base = aslData.getBase(); + + Table data; + Function> dataFieldProvider; + if (base instanceof AslStructureQuery baseSq) { + data = baseSq.getType().getDataTable().as(subqueryAlias(aslData)); + dataFieldProvider = __ -> data.field(ObjectDataTablePrototype.INSTANCE.DATA); + } else { + data = targetTable; + dataFieldProvider = colName -> FieldUtils.aliasedField(data, aslData, colName, JSONB.class); + } + + SelectSelectStep select = DSL.select(aslData.getSelect().stream() + .map(AslColumnField.class::cast) + .map(f -> pathDataField(aslData, f, dataFieldProvider)) + .toList()); + + if (base instanceof AslStructureQuery) { + // primary key condition + List pkeyCondition = data.getPrimaryKey().getFields().stream() + .map(f -> FieldUtils.aliasedField(targetTable, aslData, f).eq((Field) data.field(f))) + .toList(); + + return select.from(data).where(pkeyCondition); + + } else { + return select; + } + } + + @Nonnull + private static Field pathDataField( + AslPathDataQuery aslData, AslColumnField f, Function> dataFieldProvider) { + Field dataField = dataFieldProvider.apply(f.getColumnName()); + Field jsonbField = buildJsonbPathField(aslData.getPathNodes(f), aslData.isMultipleValued(), dataField); + Field field; + if (f.getType() == String.class) { + field = DSL.jsonbGetElementAsText(jsonbField, 0); + } else { + field = jsonbField; + } + return field.as(f.getName(true)); + } + + private static Field buildJsonbPathField( + List pathNodes, boolean multipleValued, Field jsonbField) { + Iterator attributeIt = pathNodes.stream() + .map(PathNode::getAttribute) + .map(RmAttributeAlias::getAlias) + .iterator(); + + Field field = jsonbField; + + while (attributeIt.hasNext()) { + field = DSL.jsonbGetAttribute(field, DSL.inline(attributeIt.next())); + } + + if (multipleValued) { + field = AdditionalSQLFunctions.jsonb_array_elements(field); + } + + return field; + } + + private static SelectSelectStep buildFilteringQuery(AslFilteringQuery aq, Table target) { + Stream fields = + switch (aq.getSourceField()) { + case AslColumnField src -> Stream.of(FieldUtils.field(target, src, true) + .as(((AslColumnField) aq.getSelect().getFirst()).getAliasedName())); + case AslComplexExtractedColumnField src -> src.getExtractedColumn().getColumns().stream() + .map(fieldName -> FieldUtils.field(target, src, fieldName, true) + .as(src.aliasedName(fieldName))); + case AslConstantField cf -> Stream.of(DSL.inline(cf.getValue(), cf.getType())); + case AslAggregatingField __ -> throw new IllegalArgumentException( + "Filtering queries cannot be based on AslAggregatingField"); + case AslSubqueryField __ -> throw new IllegalArgumentException( + "Filtering queries cannot be based on AslSubqueryField"); + }; + return DSL.select(fields.toArray(Field[]::new)); + } + + @Nonnull + private static SelectConditionStep buildStructureQuery( + AslStructureQuery aq, AslQueryTables aslQueryToTable) { + Table dataTable = aq.getType().getDataTable().as(subqueryAlias(aq)); + Table primaryTable = aq.isRequiresVersionTableJoin() + ? aq.getType().getVersionTable().as(versionSubqueryAlias(aq)) + : dataTable; + + SelectJoinStep step = structureQueryBase(aq, primaryTable, dataTable, aq.isRequiresVersionTableJoin()); + + aslQueryToTable.put(aq, dataTable, primaryTable); + + // add regular and structure conditions + SelectConditionStep where = step.where(Stream.concat( + Optional.of(aq).map(AslStructureQuery::getCondition).stream(), + aq.getStructureConditions().stream()) + .map(c -> ConditionUtils.buildCondition(c, aslQueryToTable, false)) + .toArray(Condition[]::new)); + + // data and primary are local to this sub-query and can be removed + aslQueryToTable.remove(aq); + return where; + } + + @Nonnull + private static SelectJoinStep structureQueryBase( + AslStructureQuery aq, Table primaryTable, Table dataTable, boolean hasVersionTable) { + SelectJoinStep step = DSL.select(aq.getSelect().stream() + .map(AslColumnField.class::cast) + .map(f -> ((aq.isRequiresVersionTableJoin() && f.isVersionTableField()) + ? primaryTable + : dataTable) + .field(f.getColumnName()) + .as(f.getAliasedName())) + .toArray(SelectFieldOrAsterisk[]::new)) + .from(primaryTable); + + if (hasVersionTable) { + // join version and data table + step = switch (aq.getType()) { + case EHR, AUDIT_DETAILS -> throw new IllegalArgumentException( + "%s has no version table".formatted(aq.getType())); + case EHR_STATUS -> step.join(dataTable, JoinType.JOIN) + .on(primaryTable.field(EHR_STATUS_VERSION.EHR_ID).eq(dataTable.field(EHR_STATUS_DATA.EHR_ID))); + case COMPOSITION -> step.join(dataTable, JoinType.JOIN) + .on(primaryTable.field(COMP_VERSION.VO_ID).eq(dataTable.field(COMP_DATA.VO_ID))); + case FOLDER -> step.join(dataTable, JoinType.JOIN) + .on(primaryTable + .field(EHR_FOLDER_VERSION.EHR_ID) + .eq(dataTable.field(EHR_FOLDER_DATA.EHR_ID)) + .and(primaryTable + .field(EHR_FOLDER_VERSION.EHR_FOLDERS_IDX) + .eq(dataTable.field(EHR_FOLDER_DATA.EHR_FOLDERS_IDX))));}; + } + return step; + } + + /** + * select + * jsonb_object_agg( + * ( sub_string(d2."entity_idx" FROM char_length(c2."entity_idx") + 1) + * ), "data" + * ) as "data" + * from "ehr"."comp_one" d2 + * where + * c2."ehr_id" = "d2"."ehr_id" + * and c2."VO_ID" = "d2"."VO_ID" + * and c2."num" <= "d2"."num" + * and c2."num_cap" >= "d2"."num" + * group by "d2"."VO_ID" + */ + static SelectHavingStep> buildDataSubquery( + AslRmObjectDataQuery aslData, AslQueryTables aslQueryToTable, Condition... additionalConditions) { + AslQuery target = aslData.getBaseProvider(); + Table targetTable = aslQueryToTable.getDataTable(target); + AslSourceRelation type = getTargetType(aslData.getBase()); + + Table data = type.getDataTable().as(subqueryAlias(aslData)); + String dataFieldName = ((AslColumnField) aslData.getSelect().getFirst()).getName(true); + // XXX Data aggregation is not needed for "terminal" structure nodes, e.g. ELEMENT + Field jsonbField = dataAggregation( + data, FieldUtils.aliasedField(targetTable, aslData, COMP_DATA.ENTITY_IDX)) + .as(DSL.name(dataFieldName)); + + SelectJoinStep> from = DSL.select(jsonbField).from(data); + + // primary key condition + List pKeyFields = type.getPkeyFields().stream() + .map((TableField field) -> { + Field f = data.field(field); + // add EQ to WHERE + from.where( + FieldUtils.aliasedField(targetTable, aslData, field).eq(f)); + return f; + }) + .toList(); + + Condition[] conditions = Stream.concat( + // TODO can be skipped for roots + // TODO can be set to == for leafs (ELEMENT) + Stream.of(Objects.requireNonNull(data.field(COMP_DATA.NUM)) + .between( + FieldUtils.aliasedField(targetTable, aslData, COMP_DATA.NUM), + FieldUtils.aliasedField(targetTable, aslData, COMP_DATA.NUM_CAP))), + Arrays.stream(additionalConditions)) + .toArray(Condition[]::new); + + return from.where(conditions).groupBy(pKeyFields); + } + + /** + * The aggregated jsonb can be processed by DbToRmFormat::reconstructFromDbFormat + * + * @return + */ + private static JSONObjectAggNullStep dataAggregation(Table dataTable, Field baseEntityIndex) { + return DSL.jsonbObjectAgg( + DSL.substring( + dataTable.field(COMP_DATA.ENTITY_IDX), + DSL.length(baseEntityIndex).plus(DSL.inline(1))), + dataTable.field(COMP_DATA.DATA)); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/ConditionUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/ConditionUtils.java new file mode 100644 index 000000000..6f4c472c7 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/ConditionUtils.java @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql; + +import static org.ehrbase.jooq.pg.Tables.AUDIT_DETAILS; +import static org.ehrbase.jooq.pg.Tables.COMP_DATA; +import static org.ehrbase.jooq.pg.Tables.COMP_VERSION; +import static org.ehrbase.openehr.dbformat.DbToRmFormat.TYPE_ATTRIBUTE; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.jooq.pg.util.AdditionalSQLFunctions; +import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept; +import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslAndQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslDescendantCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslDvOrderedValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslEntityIdxOffsetCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFalseQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotNullQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslOrQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslPathChildCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslTrueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslAggregatingField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslComplexExtractedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslConstantField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslSubqueryField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslAuditDetailsJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslDelegatingJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoin; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; +import org.ehrbase.openehr.aqlengine.sql.AqlSqlQueryBuilder.AslQueryTables; +import org.ehrbase.openehr.dbformat.RmAttributeAlias; +import org.ehrbase.openehr.dbformat.RmTypeAlias; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.JSONB; +import org.jooq.Table; +import org.jooq.impl.DSL; + +final class ConditionUtils { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private ConditionUtils() {} + + public static Condition buildJoinCondition(AslJoin aslJoin, AslQueryTables aslQueryToTable) { + Table sqlLeft = aslQueryToTable.getDataTable(aslJoin.getLeft()); + Table sqlRight = aslQueryToTable.getDataTable(aslJoin.getRight()); + + List conditions = new ArrayList<>(); + for (AslJoinCondition jc : aslJoin.getOn()) { + switch (jc) { + case AslDelegatingJoinCondition desc -> addDelegatingJoinConditions( + desc, conditions, sqlLeft, sqlRight); + case AslPathFilterJoinCondition filterCondition -> conditions.add( + buildCondition(filterCondition.getCondition(), aslQueryToTable, true)); + case AslAuditDetailsJoinCondition ac -> conditions.add(FieldUtils.field( + sqlLeft, + aslJoin.getLeft(), + ac.getLeftOwner(), + AslStructureColumn.AUDIT_ID.getFieldName(), + UUID.class, + true) + .eq(FieldUtils.field( + sqlRight, + aslJoin.getRight(), + ac.getRightOwner(), + AUDIT_DETAILS.ID.getName(), + UUID.class, + true))); + } + } + + return conditions.stream().reduce(DSL.noCondition(), DSL::and); + } + + private static void addDelegatingJoinConditions( + AslDelegatingJoinCondition joinCondition, List conditions, Table sqlLeft, Table sqlRight) { + (switch (joinCondition.getDelegate()) { + case AslEntityIdxOffsetCondition c -> entityIdxOffsetConditions(c, sqlLeft, sqlRight, true); + case AslDescendantCondition c -> descendantConditions(c, sqlLeft, sqlRight, true); + case AslPathChildCondition c -> pathChildConditions(c, sqlLeft, sqlRight, true); + }) + .forEach(conditions::add); + } + + private static Stream pathChildConditions( + final AslPathChildCondition dc, + final Table sqlLeft, + final Table sqlRight, + final boolean isJoinCondition) { + AslSourceRelation parentRelation = dc.getParentRelation(); + if (!EnumSet.of(AslSourceRelation.COMPOSITION, AslSourceRelation.EHR_STATUS) + .contains(parentRelation)) { + throw new IllegalArgumentException("unexpected parent relation type %s".formatted(parentRelation)); + } + if (!EnumSet.of(AslSourceRelation.COMPOSITION, AslSourceRelation.EHR_STATUS) + .contains(dc.getChildRelation())) { + throw new IllegalArgumentException( + "unexpected descendant relation type %s".formatted(dc.getChildRelation())); + } + + return switch (parentRelation) { + case COMPOSITION, EHR_STATUS -> { + AslStructureColumn pKeyField = parentRelation == AslSourceRelation.COMPOSITION + ? AslStructureColumn.VO_ID + : AslStructureColumn.EHR_ID; + yield Stream.of( + // l.pKey == r.pKey + FieldUtils.field( + sqlLeft, + dc.getLeftProvider(), + dc.getLeftOwner(), + pKeyField.getFieldName(), + UUID.class, + true) + .eq(FieldUtils.field( + sqlRight, + dc.getRightProvider(), + dc.getRightOwner(), + pKeyField.getFieldName(), + UUID.class, + isJoinCondition)), + // l.num == r.parent_num + FieldUtils.field( + sqlLeft, + dc.getLeftProvider(), + dc.getLeftOwner(), + AslStructureColumn.NUM.getFieldName(), + Integer.class, + true) + .eq(FieldUtils.field( + sqlRight, + dc.getRightProvider(), + dc.getRightOwner(), + AslStructureColumn.PARENT_NUM.getFieldName(), + Integer.class, + isJoinCondition))); + } + case FOLDER -> throw new NotImplementedException("Joining FOLDER is not yet supported"); + case AUDIT_DETAILS -> throw new IllegalArgumentException( + "Path child condition not applicable to AUDIT_DETAILS"); + case EHR -> throw new IllegalArgumentException("Path child condition not applicable to EHR"); + }; + } + + private static Stream entityIdxOffsetConditions( + AslEntityIdxOffsetCondition ic, Table sqlLeft, Table sqlRight, boolean isJoinCondition) { + return Stream.of(FieldUtils.field( + sqlLeft, + ic.getLeftProvider(), + ic.getLeftOwner(), + AslStructureColumn.ENTITY_IDX_LEN.getFieldName(), + Integer.class, + true) + .add(DSL.inline(ic.getOffset())) + .eq(FieldUtils.field( + sqlRight, + ic.getRightProvider(), + ic.getRightOwner(), + AslStructureColumn.ENTITY_IDX_LEN.getFieldName(), + Integer.class, + isJoinCondition))); + } + + private static Stream descendantConditions( + AslDescendantCondition dc, Table sqlLeft, Table sqlRight, boolean isJoinCondition) { + + // TODO cleanup + AslSourceRelation parentRelation = dc.getParentRelation(); + if (!EnumSet.of(AslSourceRelation.COMPOSITION, AslSourceRelation.EHR_STATUS, AslSourceRelation.EHR) + .contains(parentRelation)) { + throw new IllegalArgumentException("unexpected parent relation type %s".formatted(parentRelation)); + } + if (!EnumSet.of(AslSourceRelation.COMPOSITION, AslSourceRelation.EHR_STATUS) + .contains(dc.getDescendantRelation())) { + throw new IllegalArgumentException( + "unexpected descendant relation type %s".formatted(dc.getDescendantRelation())); + } + + return switch (parentRelation) { + case EHR -> Stream.of( + FieldUtils.field(sqlLeft, dc.getLeftProvider(), dc.getLeftOwner(), "id", UUID.class, true) + .eq(FieldUtils.field( + sqlRight, + dc.getRightProvider(), + dc.getRightOwner(), + AslStructureColumn.EHR_ID.getFieldName(), + UUID.class, + isJoinCondition))); + case COMPOSITION, EHR_STATUS -> { + AslStructureColumn pKeyField = parentRelation == AslSourceRelation.COMPOSITION + ? AslStructureColumn.VO_ID + : AslStructureColumn.EHR_ID; + yield Stream.of( + // l.pKey == r.pKey + FieldUtils.field( + sqlLeft, + dc.getLeftProvider(), + dc.getLeftOwner(), + pKeyField.getFieldName(), + UUID.class, + true) + .eq(FieldUtils.field( + sqlRight, + dc.getRightProvider(), + dc.getRightOwner(), + pKeyField.getFieldName(), + UUID.class, + isJoinCondition)), + // l.num < r.num <= l.num_cap + FieldUtils.field( + sqlRight, + dc.getRightProvider(), + dc.getRightOwner(), + AslStructureColumn.NUM.getFieldName(), + Integer.class, + true) + .between( + FieldUtils.field( + sqlLeft, + dc.getLeftProvider(), + dc.getLeftOwner(), + AslStructureColumn.NUM.getFieldName(), + Integer.class, + isJoinCondition) + .add(DSL.inline(1)), + FieldUtils.field( + sqlLeft, + dc.getLeftProvider(), + dc.getLeftOwner(), + AslStructureColumn.NUM_CAP.getFieldName(), + Integer.class, + isJoinCondition))); + } + case FOLDER -> throw new NotImplementedException("Joining FOLDER is not yet supported"); + case AUDIT_DETAILS -> throw new IllegalArgumentException( + "Descendant condition not applicable to AUDIT_DETAILS"); + }; + } + + public static Condition buildCondition(AslQueryCondition c, AslQueryTables tables, boolean useAliases) { + return switch (c) { + case null -> DSL.noCondition(); + case AslAndQueryCondition and -> DSL.and(and.getOperands().stream() + .map(o -> buildCondition(o, tables, useAliases)) + .toList()); + case AslOrQueryCondition or -> DSL.or(or.getOperands().stream() + .map(o -> buildCondition(o, tables, useAliases)) + .toList()); + case AslNotQueryCondition not -> DSL.not(buildCondition(not.getCondition(), tables, useAliases)); + case AslFalseQueryCondition __ -> DSL.falseCondition(); + case AslTrueQueryCondition __ -> DSL.trueCondition(); + case AslNotNullQueryCondition nn -> notNullCondition(tables, useAliases, nn); + case AslFieldValueQueryCondition fv -> buildFieldValueCondition(tables, useAliases, fv); + case AslEntityIdxOffsetCondition ic -> DSL.and(entityIdxOffsetConditions( + ic, + tables.getDataTable(ic.getLeftProvider()), + tables.getDataTable(ic.getRightProvider()), + false) + .toList()); + case AslDescendantCondition dc -> DSL.and(descendantConditions( + dc, + tables.getDataTable(dc.getLeftProvider()), + dc.getParentRelation() == AslSourceRelation.EHR + ? tables.getVersionTable(dc.getRightProvider()) + : tables.getDataTable(dc.getRightProvider()), + false) + .toList()); + case AslPathChildCondition dc -> DSL.and(pathChildConditions( + dc, + tables.getDataTable(dc.getLeftProvider()), + dc.getParentRelation() == AslSourceRelation.EHR + ? tables.getVersionTable(dc.getRightProvider()) + : tables.getDataTable(dc.getRightProvider()), + false) + .toList()); + }; + } + + @Nonnull + private static Condition notNullCondition(AslQueryTables tables, boolean useAliases, AslNotNullQueryCondition nn) { + AslField field = nn.getField(); + if (field.getExtractedColumn() != null) { + return DSL.trueCondition(); + + } else if (field instanceof AslColumnField f) { + return (f.isVersionTableField() + ? tables.getVersionTable(field.getProvider()) + : tables.getDataTable(field.getProvider())) + .field(f.getName(useAliases)) + .isNotNull(); + + } else { + throw new IllegalArgumentException( + "Unsupported field type: %s".formatted(field.getClass().getSimpleName())); + } + } + + private static Condition buildFieldValueCondition( + AslQueryTables tables, boolean useAliases, AslFieldValueQueryCondition fv) { + AslField field = fv.getField(); + + AslQuery internalProvider = field.getInternalProvider(); + if (fv instanceof AslDvOrderedValueQueryCondition dvc) { + Field sqlDvOrderedField = FieldUtils.field( + tables.getDataTable(internalProvider), (AslColumnField) field, JSONB.class, useAliases); + Field sqlMagnitudeField = AdditionalSQLFunctions.jsonb_dv_ordered_magnitude(sqlDvOrderedField); + Field sqlTypeField = + DSL.jsonbGetAttributeAsText(sqlDvOrderedField, RmAttributeAlias.getAlias(TYPE_ATTRIBUTE)); + List types = + dvc.getTypesToCompare().stream().map(RmTypeAlias::getAlias).toList(); + return applyOperator(AslConditionOperator.IN, sqlTypeField, types) + .and(applyOperator(dvc.getOperator(), sqlMagnitudeField, dvc.getValues())); + } + + return switch (field) { + case AslComplexExtractedColumnField ecf -> complexExtractedColumnCondition( + useAliases, + fv, + ecf, + tables.getDataTable(internalProvider), + tables.getVersionTable(internalProvider)); + case AslColumnField f -> applyOperator( + fv.getOperator(), + FieldUtils.field( + (f.isVersionTableField() + ? tables.getVersionTable(internalProvider) + : tables.getDataTable(internalProvider)), + f, + useAliases), + fv.getValues()); + // XXX conditions on constant fields could be evaluated here instead of by the DB + case AslConstantField f -> applyOperator( + fv.getOperator(), DSL.inline(f.getValue(), f.getType()), fv.getValues()); + case AslAggregatingField __ -> throw new IllegalArgumentException( + "AslAggregatingField cannot be used in WHERE"); + case AslSubqueryField __ -> throw new IllegalArgumentException("AslSubqueryField cannot be used in WHERE"); + }; + } + + @Nonnull + private static Condition complexExtractedColumnCondition( + boolean useAliases, + AslFieldValueQueryCondition fv, + AslComplexExtractedColumnField ecf, + Table dataTable, + Table versionTable) { + return switch (ecf.getExtractedColumn()) { + case VO_ID -> { + AslConditionOperator op = + fv.getOperator() == AslConditionOperator.IN ? AslConditionOperator.EQ : fv.getOperator(); + yield fv.getValues().stream() + .map(String.class::cast) + .map(id -> voIdCondition(versionTable, useAliases, id, op, ecf)) + .reduce(DSL.noCondition(), DSL::or); + } + case ARCHETYPE_NODE_ID -> { + AslConditionOperator op = + fv.getOperator() == AslConditionOperator.IN ? AslConditionOperator.EQ : fv.getOperator(); + yield fv.getValues().stream() + .map(AslRmTypeAndConcept.class::cast) + .map(p -> archetypeNodeIdCondition(dataTable, useAliases, ecf, p, op)) + .reduce(DSL.noCondition(), DSL::or); + } + case TEMPLATE_ID, + NAME_VALUE, + EHR_ID, + ROOT_CONCEPT, + OV_CONTRIBUTION_ID, + OV_TIME_COMMITTED_DV, + OV_TIME_COMMITTED, + AD_SYSTEM_ID, + AD_DESCRIPTION_DV, + AD_DESCRIPTION_VALUE, + AD_CHANGE_TYPE_DV, + AD_CHANGE_TYPE_VALUE, + AD_CHANGE_TYPE_CODE_STRING, + AD_CHANGE_TYPE_PREFERRED_TERM, + AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE, + EHR_TIME_CREATED_DV, + EHR_TIME_CREATED, + EHR_SYSTEM_ID, + EHR_SYSTEM_ID_DV -> throw new IllegalArgumentException( + "Extracted column %s is not complex".formatted(ecf.getExtractedColumn())); + }; + } + + private static Condition archetypeNodeIdCondition( + Table src, + boolean aliasedNames, + AslComplexExtractedColumnField ecf, + AslRmTypeAndConcept rmTypeAndConcept, + AslConditionOperator op) { + return Stream.of( + Pair.of(COMP_DATA.RM_ENTITY, rmTypeAndConcept.aliasedRmType()), + Pair.of(COMP_DATA.ENTITY_CONCEPT, rmTypeAndConcept.concept())) + .filter(p -> p.getValue() != null) + .map(p1 -> applyOperator( + op, FieldUtils.field(src, ecf, p1.getKey().getName(), aliasedNames), List.of(p1.getValue()))) + .reduce(DSL.noCondition(), op == AslConditionOperator.NEQ ? DSL::or : DSL::and); + } + + @Nonnull + private static Condition voIdCondition( + Table versionTable, + boolean aliasedNames, + String id, + AslConditionOperator op, + AslComplexExtractedColumnField field) { + // id is expected to be valid + String[] split = id.split("::"); + + Field uuidField = FieldUtils.field(versionTable, field, COMP_VERSION.VO_ID.getName(), aliasedNames); + Field versionField = FieldUtils.field(versionTable, field, COMP_VERSION.SYS_VERSION.getName(), aliasedNames); + Field uuid = DSL.inline(split[0]).cast(UUID.class); + Optional> version = Optional.of(split) + .filter(s -> s.length > 2) + .map(s -> s[2]) + .map(Integer::parseInt) + .map(DSL::inline); + Field left = version.isPresent() ? DSL.field(DSL.row(uuidField, versionField)) : uuidField; + Field right = version.isPresent() ? DSL.field(DSL.row(uuid, version.get())) : uuid; + return switch (op) { + case IN, EQ -> left.eq(right); + case NEQ -> left.ne(right); + case LT -> left.lt(right); + case GT -> left.gt(right); + case GT_EQ -> left.ge(right); + case LT_EQ -> left.le(right); + case IS_NULL -> uuidField.isNull(); + case IS_NOT_NULL -> uuidField.isNotNull(); + case LIKE -> throw new IllegalArgumentException(); + }; + } + + private static Condition applyOperator(AslConditionOperator operator, Field field, Collection values) { + Class sqlFieldType = field.getType(); + boolean jsonbField = JSONB.class.isAssignableFrom(sqlFieldType); + boolean uuidField = !jsonbField && UUID.class.isAssignableFrom(sqlFieldType); + if (operator == AslConditionOperator.LIKE) { + String likePattern = (String) values.iterator().next(); + if (jsonbField) { + likePattern = escapeAsJsonString(likePattern); + } + return field.cast(String.class).like(likePattern); + } else if (operator == AslConditionOperator.IS_NULL) { + return field.isNull(); + } else if (operator == AslConditionOperator.IS_NOT_NULL) { + return field.isNotNull(); + } + + boolean orderOperator = EnumSet.of( + AslConditionOperator.GT_EQ, + AslConditionOperator.GT, + AslConditionOperator.LT_EQ, + AslConditionOperator.LT) + .contains(operator); + + List filteredValues = values.stream() + .map(v -> { + Object value = null; + if (uuidField && v instanceof String s) { + try { + value = UUID.fromString(s); + } catch (IllegalArgumentException e) { + // value stays null + } + } else if (jsonbField || sqlFieldType.isInstance(v) || orderOperator) { + value = v; + } + return value; + }) + .filter(Objects::nonNull) + .toList(); + return switch (filteredValues.size()) { + case 0 -> switch (operator) { + case IN, EQ -> DSL.falseCondition(); + case NEQ -> DSL.trueCondition(); + case GT_EQ, GT, LT_EQ, LT -> throw new IllegalArgumentException( + "%s-Condition needs one value, not 0".formatted(operator)); + default -> throw new IllegalStateException("Unexpected value: " + operator); + }; + case 1 -> { + Object val = filteredValues.getFirst(); + Field wrappedValue = jsonbField || orderOperator && !sqlFieldType.isInstance(val) + ? AdditionalSQLFunctions.to_jsonb(val) + : DSL.inline(val); + Field wrappedField = !jsonbField && orderOperator && !sqlFieldType.isInstance(val) + ? AdditionalSQLFunctions.to_jsonb(field) + : field; + yield switch (operator) { + case IN, EQ -> field.eq(wrappedValue); + case NEQ -> field.ne(wrappedValue); + case GT_EQ -> wrappedField.ge(wrappedValue); + case GT -> wrappedField.gt(wrappedValue); + case LT_EQ -> wrappedField.le(wrappedValue); + case LT -> wrappedField.lt(wrappedValue); + default -> throw new IllegalStateException("Unexpected value: " + operator); + }; + } + default -> switch (operator) { + case IN -> field.in(filteredValues.stream() + .map(v -> jsonbField ? AdditionalSQLFunctions.to_jsonb(v) : DSL.inline(v)) + .toList()); + case EQ, NEQ, GT_EQ, GT, LT_EQ, LT -> throw new IllegalArgumentException( + "%s-Condition needs one value, not %d".formatted(operator, filteredValues.size())); + default -> throw new IllegalStateException("Unexpected value: " + operator); + }; + }; + } + + static String escapeAsJsonString(String string) { + if (string == null) { + return null; + } + try { + return OBJECT_MAPPER.writeValueAsString(string); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e.getMessage(), e); + } + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/EncapsulatingQueryUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/EncapsulatingQueryUtils.java new file mode 100644 index 000000000..694b8aff1 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/EncapsulatingQueryUtils.java @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql; + +import static org.ehrbase.jooq.pg.Tables.COMP_DATA; +import static org.ehrbase.jooq.pg.Tables.COMP_VERSION; +import static org.ehrbase.openehr.aqlengine.ChangeTypeUtils.JOOQ_CHANGE_TYPE_TO_CODE; + +import java.text.Collator; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.api.knowledge.TemplateMetaData; +import org.ehrbase.jooq.pg.enums.ContributionChangeType; +import org.ehrbase.jooq.pg.util.AdditionalSQLFunctions; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslAggregatingField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslComplexExtractedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslConstantField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslDvOrderedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslOrderByField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslSubqueryField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoin; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRmObjectDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction.AggregateFunctionName; +import org.jooq.CaseWhenStep; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.JSONB; +import org.jooq.JoinType; +import org.jooq.Param; +import org.jooq.SelectField; +import org.jooq.SortField; +import org.jooq.Table; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class EncapsulatingQueryUtils { + private static final Logger LOG = LoggerFactory.getLogger(EncapsulatingQueryUtils.class); + + private EncapsulatingQueryUtils() {} + + private static SelectField sqlAggregatingField( + AslAggregatingField af, Table src, AqlSqlQueryBuilder.AslQueryTables aslQueryToTable) { + if ((src == null || af.getBaseField() == null) && af.getFunction() != AggregateFunctionName.COUNT) { + throw new IllegalArgumentException("only count does not require a source table"); + } + + boolean isExtractedColumn = Optional.of(af) + .map(AslAggregatingField::getBaseField) + .map(AslField::getExtractedColumn) + // treat VERSION.commit_audit.time_committed and EHR.time_created as primitive and not DV_ORDERED + .filter(ec -> !EnumSet.of( + AslExtractedColumn.OV_TIME_COMMITTED, + AslExtractedColumn.OV_TIME_COMMITTED_DV, + AslExtractedColumn.EHR_TIME_CREATED_DV, + AslExtractedColumn.EHR_TIME_CREATED) + .contains(ec)) + .isPresent(); + if (isExtractedColumn && af.getFunction() != AggregateFunctionName.COUNT) { + throw new IllegalArgumentException( + "Aggregate function %s is not allowed for extracted columns".formatted(af.getFunction())); + } + + Function, SelectField> aggregateFunction = toAggregatedFieldFunction(af); + Field field = fieldToAggregate(src, af, aslQueryToTable); + + return aggregateFunction.apply(field); + } + + @Nullable + private static Field fieldToAggregate( + Table src, AslAggregatingField af, AqlSqlQueryBuilder.AslQueryTables aslQueryToTable) { + return switch (af.getBaseField()) { + case null -> null; + case AslColumnField f -> FieldUtils.field(Objects.requireNonNull(src), f, true); + case AslAggregatingField __ -> throw new IllegalArgumentException("Can't aggregate on AslAggregatingField"); + case AslComplexExtractedColumnField ecf -> { + Objects.requireNonNull(src); + yield switch (ecf.getExtractedColumn()) { + case VO_ID -> FieldUtils.field(src, ecf, COMP_DATA.VO_ID.getName(), true); + case ARCHETYPE_NODE_ID -> DSL.field(DSL.row( + FieldUtils.field(src, ecf, COMP_DATA.RM_ENTITY.getName(), true), + FieldUtils.field(src, ecf, COMP_DATA.ENTITY_CONCEPT.getName(), true))); + case NAME_VALUE, + EHR_ID, + TEMPLATE_ID, + ROOT_CONCEPT, + OV_CONTRIBUTION_ID, + OV_TIME_COMMITTED_DV, + OV_TIME_COMMITTED, + AD_SYSTEM_ID, + AD_DESCRIPTION_DV, + AD_DESCRIPTION_VALUE, + AD_CHANGE_TYPE_DV, + AD_CHANGE_TYPE_VALUE, + AD_CHANGE_TYPE_CODE_STRING, + AD_CHANGE_TYPE_PREFERRED_TERM, + AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE, + EHR_TIME_CREATED_DV, + EHR_TIME_CREATED, + EHR_SYSTEM_ID, + EHR_SYSTEM_ID_DV -> throw new IllegalArgumentException( + "%s is not a complex extracted column".formatted(ecf.getExtractedColumn())); + }; + } + case AslConstantField cf -> DSL.inline(cf.getValue(), cf.getType()); + case AslSubqueryField sqfd -> subqueryField(sqfd, aslQueryToTable); + }; + } + + @Nonnull + private static Function, SelectField> toAggregatedFieldFunction(AslAggregatingField af) { + return switch (af.getFunction()) { + case COUNT -> f -> AdditionalSQLFunctions.count(af.isDistinct(), f); + case MIN -> f -> af.getBaseField() instanceof AslDvOrderedColumnField + ? AdditionalSQLFunctions.min_dv_ordered(f) + : DSL.min(f); + case MAX -> f -> af.getBaseField() instanceof AslDvOrderedColumnField + ? AdditionalSQLFunctions.max_dv_ordered(f) + : DSL.max(f); + case SUM -> f -> DSL.aggregate("sum", SQLDataType.NUMERIC, f); + case AVG -> f -> DSL.aggregate("avg", SQLDataType.NUMERIC, f); + }; + } + + static SelectField sqlSelectFieldForExtractedColumn(AslComplexExtractedColumnField ecf, Table src) { + return switch (ecf.getExtractedColumn()) { + case VO_ID -> DSL.row( + FieldUtils.field(src, ecf, COMP_DATA.VO_ID.getName(), true), + FieldUtils.field(src, ecf, COMP_VERSION.SYS_VERSION.getName(), true)); + case ARCHETYPE_NODE_ID -> DSL.row( + FieldUtils.field(src, ecf, COMP_DATA.ENTITY_CONCEPT.getName(), true), + FieldUtils.field(src, ecf, COMP_DATA.RM_ENTITY.getName(), true)); + case TEMPLATE_ID, + NAME_VALUE, + EHR_ID, + ROOT_CONCEPT, + OV_CONTRIBUTION_ID, + OV_TIME_COMMITTED_DV, + OV_TIME_COMMITTED, + AD_SYSTEM_ID, + AD_DESCRIPTION_DV, + AD_DESCRIPTION_VALUE, + AD_CHANGE_TYPE_DV, + AD_CHANGE_TYPE_VALUE, + AD_CHANGE_TYPE_CODE_STRING, + AD_CHANGE_TYPE_PREFERRED_TERM, + AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE, + EHR_TIME_CREATED_DV, + EHR_TIME_CREATED, + EHR_SYSTEM_ID, + EHR_SYSTEM_ID_DV -> throw new IllegalArgumentException( + "Extracted column %s is not complex".formatted(ecf.getExtractedColumn())); + }; + } + + private static Field subqueryField(AslSubqueryField sqf, AqlSqlQueryBuilder.AslQueryTables aslQueryToTable) { + AslQuery baseQuery = sqf.getBaseQuery(); + if (!(baseQuery instanceof AslRmObjectDataQuery aq)) { + throw new IllegalArgumentException("Subquery field not supported for type: " + baseQuery.getClass()); + } + return AqlSqlQueryBuilder.buildDataSubquery( + aq, + aslQueryToTable, + sqf.getFilterConditions().stream() + .map(c -> ConditionUtils.buildCondition(c, aslQueryToTable, true)) + .toArray(Condition[]::new)) + .asField(); + } + + /** + * substring(entity_concept, 1, 1) = '.', + * case when substring(entity_concept, 1, 1) = '.' then rm_entity else null end, + * entity_concept + * @param conceptField + * @param typeField + * @return + */ + private static Stream> archetypeNodeIdOrderFields(Field conceptField, Field typeField) { + Condition isArchetype = conceptField.like(DSL.inline(".%")); + + // order by type name, not alias + Map, Param> rmTypeOrderMap = new LinkedHashMap<>(); + { + Iterator it = Arrays.stream(StructureRmType.values()) + .sorted(Comparator.comparing(Enum::name)) + .iterator(); + int pos = 0; + while (it.hasNext()) { + rmTypeOrderMap.put(DSL.inline(it.next().getAlias()), DSL.inline(pos++)); + } + } + + CaseWhenStep typeOrderField = DSL.case_(typeField).mapValues(rmTypeOrderMap); + + // at… / id… before openEHR… + return Stream.of( + // at… / id… before openEHR… + isArchetype, + // for archetypes order by RM type + DSL.case_().when(isArchetype, typeOrderField), + conceptField); + } + + private static Field templateIdOrderField(Field templateUidField, KnowledgeCacheService knowledgeCache) { + // order lexicographically by template id + List templates = knowledgeCache.listAllOperationalTemplates(); + + if (templates.isEmpty()) { + LOG.warn("No template ids found: Fallback to ordering by internal UUID"); + return templateUidField; + } + + Map, Param> templateIdOrderMap = new LinkedHashMap<>(); + Iterator it = templates.stream() + .sorted(Comparator.comparing( + u -> u.getOperationaltemplate().getTemplateId().getValue(), + Collator.getInstance(Locale.ENGLISH))) + .map(TemplateMetaData::getInternalId) + .iterator(); + int pos = 0; + while (it.hasNext()) { + templateIdOrderMap.put(DSL.inline(it.next()), DSL.inline(pos++)); + } + + return DSL.case_(templateUidField).mapValues(templateIdOrderMap).else_(DSL.inline((Object) null)); + } + + /** + * Postgresql contains a bug where filters in lateral left joins inside a left join are not respected. + * This situation can be avoided by applying an identity function to each select expression. + *

+ * See Postgresql BUG #18284. + * + * @param childQuery + * @param join + * @param relation + */ + public static void applyPgLljWorkaround(AslQuery childQuery, AslJoin join, Table relation) { + boolean workaroundNeeded = join.getJoinType() != null + && join.getJoinType() != JoinType.JOIN + && !(childQuery instanceof AslStructureQuery); + if (workaroundNeeded) { + // wrap each field with COALESCE() as identity function + Field[] fields = relation.fieldsRow().fields(); + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + // DSL::function because DSL::coalesce would be liquidated + fields[i] = DSL.function("COALESCE", field.getDataType(), field).as(field.getName()); + } + } + } + + public static SelectField selectField(AslField field, AqlSqlQueryBuilder.AslQueryTables aslQueryToTable) { + Table src = Optional.of(field) + .map(AslField::getInternalProvider) + .map(aslQueryToTable::getDataTable) + .orElse(null); + return switch (field) { + case AslColumnField f -> FieldUtils.field(Objects.requireNonNull(src), f, true) + .as(f.getName(true)); + case AslComplexExtractedColumnField ecf -> sqlSelectFieldForExtractedColumn( + ecf, Objects.requireNonNull(src)); + case AslAggregatingField af -> sqlAggregatingField(af, src, aslQueryToTable); + case AslConstantField cf -> DSL.inline(cf.getValue(), cf.getType()); + case AslSubqueryField sqf -> subqueryField(sqf, aslQueryToTable); + }; + } + + @Nonnull + public static Stream> groupByFields(AslField gb, AqlSqlQueryBuilder.AslQueryTables aslQueryToTable) { + Table src = aslQueryToTable.getDataTable(gb.getInternalProvider()); + return switch (gb) { + case AslColumnField f -> Stream.of(FieldUtils.field(src, f, true)); + case AslComplexExtractedColumnField ecf -> { + switch (ecf.getExtractedColumn()) { + case VO_ID -> { + Field voIdField = FieldUtils.field(src, ecf, COMP_DATA.VO_ID.getName(), true); + Field versionField = FieldUtils.field(src, ecf, COMP_VERSION.SYS_VERSION.getName(), true); + yield Stream.of(voIdField, versionField); + } + case ARCHETYPE_NODE_ID -> { + Field conceptField = FieldUtils.field(src, ecf, COMP_DATA.ENTITY_CONCEPT.getName(), true); + Field typeField = FieldUtils.field(src, ecf, COMP_DATA.RM_ENTITY.getName(), true); + yield Stream.of(typeField, conceptField); + } + default -> throw new IllegalArgumentException( + "%s is not a complex extracted column".formatted(ecf.getExtractedColumn())); + } + } + case AslAggregatingField __ -> throw new IllegalArgumentException( + "Cannot aggregate by AslAggregatingField"); + case AslSubqueryField sqf -> Stream.of(subqueryField(sqf, aslQueryToTable)); + case AslConstantField __ -> Stream.empty(); + }; + } + + private static Stream> complexExtractedColumnOrderByFields( + AslComplexExtractedColumnField ecf, Table src) { + return switch (ecf.getExtractedColumn()) { + case VO_ID -> Stream.of(FieldUtils.field(src, ecf, COMP_DATA.VO_ID.getName(), true)); + case ARCHETYPE_NODE_ID -> { + Field conceptField = FieldUtils.field(src, ecf, COMP_DATA.ENTITY_CONCEPT.getName(), true); + Field typeField = FieldUtils.field(src, ecf, COMP_DATA.RM_ENTITY.getName(), true); + yield archetypeNodeIdOrderFields(conceptField, typeField); + } + default -> throw new IllegalArgumentException( + "Order by %s is not supported".formatted(ecf.getExtractedColumn())); + }; + } + + @Nonnull + public static Stream> orderFields( + AslOrderByField ob, + AqlSqlQueryBuilder.AslQueryTables aslQueryToTable, + KnowledgeCacheService knowledgeCache) { + AslField aslField = ob.field(); + Table src = aslQueryToTable.getDataTable(aslField.getInternalProvider()); + return (switch (aslField) { + case AslDvOrderedColumnField f -> Stream.of(AdditionalSQLFunctions.jsonb_dv_ordered_magnitude( + (Field) FieldUtils.field(src, f, true))); + case AslColumnField f -> columnOrderField(f, src, knowledgeCache); + case AslComplexExtractedColumnField ecf -> complexExtractedColumnOrderByFields(ecf, src); + case AslAggregatingField __ -> throw new IllegalArgumentException( + "ORDER BY AslAggregatingField is not allowed"); + case AslConstantField __ -> Stream.>empty(); + case AslSubqueryField sqf -> Stream.of(subqueryField(sqf, aslQueryToTable)); + }) + .map(f -> f.sort(ob.direction())); + } + + @Nonnull + private static Stream> columnOrderField( + AslColumnField f, Table src, KnowledgeCacheService knowledgeCache) { + Field field = FieldUtils.field(src, f, true); + + field = switch (f.getExtractedColumn()) { + // ensure order by name, not internal ID + case TEMPLATE_ID -> templateIdOrderField(field, knowledgeCache); + case AD_CHANGE_TYPE_VALUE, AD_CHANGE_TYPE_PREFERRED_TERM -> DSL.lower(field.cast(String.class)); + case AD_CHANGE_TYPE_CODE_STRING -> DSL.case_((Field) field) + .mapValues(JOOQ_CHANGE_TYPE_TO_CODE); + case null -> field; + default -> field;}; + return Stream.of(field); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/FieldUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/FieldUtils.java new file mode 100644 index 000000000..90443cd8b --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/FieldUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql; + +import java.util.Iterator; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslComplexExtractedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.jooq.Field; +import org.jooq.Table; +import org.jooq.TableField; + +final class FieldUtils { + + private FieldUtils() {} + + public static Field field( + Table sqlProvider, + AslQuery aslProvider, + AslQuery owner, + String fieldName, + Class type, + boolean aliased) { + return field(sqlProvider, findFieldByOwnerAndName(aslProvider, owner, fieldName), type, aliased); + } + + private static AslColumnField findFieldByOwnerAndName(AslQuery src, AslQuery owner, String columnName) { + Iterator fieldsIt = src.getSelect().stream() + .filter(AslColumnField.class::isInstance) + .map(AslColumnField.class::cast) + .filter(f -> owner == f.getOwner()) + .filter(f -> f.getColumnName().equals(columnName)) + .iterator(); + + if (!fieldsIt.hasNext()) { + throw new IllegalArgumentException("field with columnName %s not present".formatted(columnName)); + } + AslColumnField field = fieldsIt.next(); + if (fieldsIt.hasNext()) { + throw new IllegalArgumentException("found multiple fields with columnName %s".formatted(columnName)); + } + return field; + } + + public static Field field( + Table table, AslComplexExtractedColumnField aslField, String fieldName, boolean aliased) { + return table.field(aliased ? aslField.aliasedName(fieldName) : fieldName); + } + + public static Field field(Table table, AslColumnField aslField, boolean aliased) { + return table.field(aslField.getName(aliased)); + } + + public static Field field(Table table, AslColumnField aslField, Class type, boolean aliased) { + return table.field(aslField.getName(aliased), type); + } + + public static Field aliasedField(Table target, AslDataQuery aslData, TableField fieldTemplate) { + return field( + target, aslData.getBase(), aslData.getBase(), fieldTemplate.getName(), fieldTemplate.getType(), true); + } + + public static Field aliasedField( + Table target, AslDataQuery aslData, String fieldName, Class fieldType) { + return field(target, aslData.getBase(), aslData.getBase(), fieldName, fieldType, true); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/AqlSqlQueryPostProcessor.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/AqlSqlQueryPostProcessor.java new file mode 100644 index 000000000..9132fe792 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/AqlSqlQueryPostProcessor.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql.postprocessor; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.jooq.Record; +import org.jooq.SelectQuery; + +/** + * A post-processor that may modify the SelectQuery generated from the given AslRootQuery + */ +public interface AqlSqlQueryPostProcessor { + void afterBuildSqlQuery(AslRootQuery aslRootQuery, SelectQuery query); +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/AqlSqlResultPostprocessor.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/AqlSqlResultPostprocessor.java new file mode 100644 index 000000000..73d2fae05 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/AqlSqlResultPostprocessor.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql.postprocessor; + +/** + * Applied to one column of all records returned by the SQL query executed for a given {@link org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery}. + * Selection of the applicable post processor for each column is performed by {@link org.ehrbase.openehr.aqlengine.repository.AqlQueryRepository} + */ +@FunctionalInterface +public interface AqlSqlResultPostprocessor { + + Object postProcessColumn(Object columnValue); +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/DefaultResultPostprocessor.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/DefaultResultPostprocessor.java new file mode 100644 index 000000000..7d4b708f3 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/DefaultResultPostprocessor.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql.postprocessor; + +import com.nedap.archie.rm.RMObject; +import org.ehrbase.openehr.dbformat.DbToRmFormat; +import org.jooq.JSONB; + +/** + * Handles JSONB and primitive result columns. + * JSONB will be passed to {@link DbToRmFormat}. Everything else will not be altered. + */ +public class DefaultResultPostprocessor implements AqlSqlResultPostprocessor { + @Override + public Object postProcessColumn(Object columnValue) { + + return switch (columnValue) { + case null -> null; + case JSONB jsonb -> DbToRmFormat.reconstructFromDbFormat(RMObject.class, jsonb.data()); + default -> columnValue; + }; + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/ExtractedColumnResultPostprocessor.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/ExtractedColumnResultPostprocessor.java new file mode 100644 index 000000000..d5dad33e8 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/ExtractedColumnResultPostprocessor.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql.postprocessor; + +import com.nedap.archie.rm.datatypes.CodePhrase; +import com.nedap.archie.rm.datavalues.DvCodedText; +import com.nedap.archie.rm.datavalues.DvText; +import com.nedap.archie.rm.datavalues.quantity.datetime.DvDateTime; +import com.nedap.archie.rm.support.identification.HierObjectId; +import com.nedap.archie.rm.support.identification.TerminologyId; +import java.time.temporal.TemporalAccessor; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.jooq.pg.enums.ContributionChangeType; +import org.ehrbase.openehr.aqlengine.ChangeTypeUtils; +import org.ehrbase.openehr.aqlengine.asl.model.AslExtractedColumn; +import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept; +import org.ehrbase.openehr.dbformat.RmTypeAlias; +import org.ehrbase.openehr.sdk.util.OpenEHRDateTimeSerializationUtils; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.jooq.Record; + +/** + * Handles a result column based on the given extracted column (includes complex extracted columns). + */ +public class ExtractedColumnResultPostprocessor implements AqlSqlResultPostprocessor { + + private final AslExtractedColumn extractedColumn; + private final KnowledgeCacheService knowledgeCache; + private final String nodeName; + + public ExtractedColumnResultPostprocessor( + AslExtractedColumn extractedColumn, KnowledgeCacheService knowledgeCache, String nodeName) { + this.extractedColumn = extractedColumn; + this.knowledgeCache = knowledgeCache; + this.nodeName = nodeName; + } + + @Override + public Object postProcessColumn(Object columnValue) { + if (columnValue == null) { + return null; + } + + return switch (extractedColumn) { + case TEMPLATE_ID -> knowledgeCache + .findTemplateIdByUuid((UUID) columnValue) + .orElse(null); + case OV_TIME_COMMITTED_DV, EHR_TIME_CREATED_DV -> new DvDateTime((TemporalAccessor) columnValue); + case OV_TIME_COMMITTED, EHR_TIME_CREATED -> OpenEHRDateTimeSerializationUtils.formatDateTime( + (TemporalAccessor) columnValue); + case AD_DESCRIPTION_DV -> new DvText((String) columnValue); + case AD_CHANGE_TYPE_DV -> contributionChangeTypeAsDvCodedText((ContributionChangeType) columnValue); + case AD_CHANGE_TYPE_VALUE, AD_CHANGE_TYPE_PREFERRED_TERM -> ((ContributionChangeType) columnValue) + .getLiteral() + .toLowerCase(); + case AD_CHANGE_TYPE_CODE_STRING -> ChangeTypeUtils.getCodeByJooqChangeType( + (ContributionChangeType) columnValue); + case VO_ID -> restoreVoId((Record) columnValue, nodeName); + // the root is always archetyped + case ROOT_CONCEPT -> AslRmTypeAndConcept.ARCHETYPE_PREFIX + RmConstants.COMPOSITION + columnValue; + case ARCHETYPE_NODE_ID -> restoreArchetypeNodeId((Record) columnValue); + case EHR_SYSTEM_ID_DV -> new HierObjectId((String) columnValue); + case NAME_VALUE, + EHR_ID, + OV_CONTRIBUTION_ID, + AD_SYSTEM_ID, + AD_DESCRIPTION_VALUE, + AD_CHANGE_TYPE_TERMINOLOGY_ID_VALUE, + EHR_SYSTEM_ID -> columnValue; + }; + } + + private static String restoreArchetypeNodeId(Record srcRow) { + String entityConcept = (String) srcRow.get(0); + if (!entityConcept.startsWith(".")) { + // at or id code + return entityConcept; + } + String rmType = RmTypeAlias.getRmType((String) srcRow.get(1)); + return AslRmTypeAndConcept.ARCHETYPE_PREFIX + rmType + entityConcept; + } + + private static String restoreVoId(Record srcRow, String nodeName) { + if (srcRow.get(0) == null) { + return null; + } + return srcRow.get(0) + "::" + nodeName + "::" + srcRow.get(1); + } + + @Nonnull + private static DvCodedText contributionChangeTypeAsDvCodedText(ContributionChangeType changeType) { + return new DvCodedText( + changeType.getLiteral().toLowerCase(), + new CodePhrase( + new TerminologyId("openehr"), + ChangeTypeUtils.getCodeByJooqChangeType(changeType), + changeType.getLiteral().toLowerCase())); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/util/TreeNode.java b/aql-engine/src/main/java/org/ehrbase/openehr/util/TreeNode.java new file mode 100644 index 000000000..eaea7d844 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/util/TreeNode.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +/** + * A simple base for creating tree structures. + * + * @param + */ +public abstract class TreeNode> { + + protected T parent; + final List children = new ArrayList<>(); + + public T getParent() { + return parent; + } + + /** + * + * Add the child to children. + * If it is already contained, nothing is changed. + * If it already has a parent, it is removed from the parent. + * + * In order to keep the tree acyclic, a IllegalArgumentException is thrown if the child ia an ancestor of this node. + * + * @param child + * @return + */ + protected T addChild(T child) { + if (child.parent == this) { + return child; + } + + if (!child.children.isEmpty()) { + // check ancestors + var a = this.parent; + while (a != null) { + if (a == child) { + throw new IllegalArgumentException("The child is an ancestor of the current node"); + } + a = a.parent; + } + } + + child.removeFromParent(); + + child.parent = (T) this; + children.add(child); + return child; + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public void sortChildren(Comparator comparator) { + children.sort(comparator); + } + + void removeFromParent() { + if (parent != null) { + parent.children.remove(this); + parent = null; + } + } + + public Stream streamDepthFirst() { + return Stream.of(Stream.of((T) this), getChildren().stream().flatMap(TreeNode::streamDepthFirst)) + .flatMap(s -> s); + } +} diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/util/TreeUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/util/TreeUtils.java new file mode 100644 index 000000000..51c7d63e8 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/util/TreeUtils.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.util; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + +public class TreeUtils { + + /** + * Draws a tree. One node per line. Indentation: 2 spaces + * + * @param + * @param root + * @param childOrder sort the children in the output. May help when comparing rendered trees. + * @param nodeRenderer renders the contents of a node. Must not start with spaces or contain line breaks. + * @return + */ + public static > String renderTree( + T root, Comparator childOrder, Function nodeRenderer) { + var sb = new StringBuilder(); + renderTreeNode(root, sb, 0, childOrder, nodeRenderer); + return sb.toString(); + } + + private static > void renderTreeNode( + T node, StringBuilder sb, int level, Comparator childOrder, Function nodeRenderer) { + if (!sb.isEmpty()) { + sb.append("\n"); + } + for (int l = 0; l < level; l++) { + sb.append(" "); + } + String nodeStr = nodeRenderer.apply(node); + if (StringUtils.isBlank(nodeStr)) { + throw new IllegalArgumentException("rendered node must not be blank"); + } else if (Pattern.compile("^\\s").matcher(nodeStr).find()) { + throw new IllegalArgumentException("rendered node must not start with whitespace"); + } else if (Pattern.compile("\\R").matcher(nodeStr).find()) { + throw new IllegalArgumentException("rendered node must not contain line breaks"); + } else { + sb.append(nodeStr); + } + Stream childStream = node.getChildren().stream(); + if (childOrder != null) { + childStream = childStream.sorted(childOrder); + } + childStream.forEach(n -> renderTreeNode(n, sb, level + 1, childOrder, nodeRenderer)); + } + + /** + * Parses a tree from a String. + * One node per line. Indentation: 2 spaces + * + * @param treeGraph + * @param nodeParser parses the contents of a node + * @return + * @param + */ + public static > T parseTree(String treeGraph, Function nodeParser) { + Pattern p = Pattern.compile("((?: )*)(.+)"); + Iterator> it = treeGraph + .lines() + .map(l -> { + Matcher matcher = p.matcher(l); + if (!matcher.matches()) { + throw new IllegalArgumentException("illegal line: %s".formatted(l)); + } + return matcher; + }) + .map(m -> Pair.of(m.group(1).length() / 2, m.group(2))) + .iterator(); + + T root = nodeParser.apply(it.next().getRight()); + + T lastNode = root; + int lastLevel = 0; + + while (it.hasNext()) { + Pair next = it.next(); + int level = next.getLeft(); + + if (level <= 0) { + throw new IllegalArgumentException("Only one root allowed: %s".formatted(next.getRight())); + } + int parentLevel = level - 1; + if (parentLevel > lastLevel) { + throw new IllegalArgumentException( + "Inconsistent level of %s: %d >> %d".formatted(next.getRight(), level, lastLevel)); + } + + var parent = lastNode; + for (int i = lastLevel; i > parentLevel; i--) { + parent = parent.parent; + } + + lastNode = parent.addChild(nodeParser.apply(next.getRight())); + lastLevel = level; + } + + return root; + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacementTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacementTest.java new file mode 100644 index 000000000..eda0b07f1 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacementTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + +import java.util.Map; +import org.assertj.core.api.AbstractThrowableAssert; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlParseException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AqlParameterReplacementTest { + + @ParameterizedTest + @ValueSource( + strings = { + "2020-12-31", + "20201231", + "23:59:59", + "235959", + "23:59:59.9", + "23:59:59.98", + "23:59:59.987", + "23:59:59.9876", + "23:59:59.98765", + "23:59:59.987654", + "23:59:59.9876543", + "23:59:59.98765432", + "23:59:59.987654321", + "235959.987", + "23:59:59Z", + "235959Z", + "23:59:59.987Z", + "235959.987Z", + "23:59:59+12", + "235959-12:59", + "23:59:59.987+12", + "235959.987-12:59", + "235959.987+1259", + "235959.987-1259", + "2020-12-31T23:59:59", + "2020-12-31T23:59:59.9", + "2020-12-31T23:59:59.98", + "2020-12-31T23:59:59.987", + "2020-12-31T23:59:59.9876", + "2020-12-31T23:59:59.98765", + "2020-12-31T23:59:59.987654", + "2020-12-31T23:59:59.9876543", + "2020-12-31T23:59:59.98765432", + "2020-12-31T23:59:59.987654321", + "2020-12-31T23:59:59Z", + "2020-12-31T23:59:59-0200", + "2020-12-31T23:59:59.013-0200" + }) + void confirmTemporalPattern(String example) { + assertThat(AqlParameterReplacement.TemporalPrimitivePattern.matches(example)) + .isTrue(); + } + + @ParameterizedTest + @ValueSource( + strings = { + "", + "T", + "2020-1231", + "2020", + "2020:12:31", + "23-59-59", + "23-59", + "236060", + "23:60:59.987", + "23:59:59.", + "23:59:59.1234567890", + "23:59:59.987z", + "23:59:59+120", + "23:59:59.987+2", + "23:59:59.987+123", + "23:59:59.987+12345", + "23:59:59.987Z+1234", + "2020-12-31T23:59:59.", + "2020-12-31T23:59:59.9876543210", + "2020-12-31t23:59:59.013-0200", + "2020-12-31T235959", + "20201231T23:59:59", + "23:59:59T2020-12-31", + }) + void rejectTemporalPattern(String example) { + assertThat(AqlParameterReplacement.TemporalPrimitivePattern.matches(example)) + .isFalse(); + } + + @Test + void replaceWhereParameters() { + // Simple string replacement + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE d/foo = $bar", + Map.of("bar", "baz"), + "SELECT d FROM DUMMY d WHERE d/foo = 'baz'"); + + // Data types + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE (d/int = $int AND d/bool = $bool AND d/double = $double AND d/str = $str AND d/date = $date)", + Map.of("int", 42, "bool", true, "double", 1., "str", "foo", "date", "2012-12-31"), + "SELECT d FROM DUMMY d WHERE (d/int = 42 AND d/bool = true AND d/double = 1.0 AND d/str = 'foo' AND d/date = '2012-12-31')"); + + // IdentifiedPath: archetype_node_id + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE d[$ani]/foo[$ani2] = 42", + Map.of("ani", "at0001", "ani2", "at0002"), + "SELECT d FROM DUMMY d WHERE d[at0001]/foo[at0002] = 42"); + + // IdentifiedPath: nodeConstraint + name + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE d[at0001,$nameConstraint]/foo[at0002,$nameConstraint2] = 42", + Map.of("nameConstraint", "Results", "nameConstraint2", "Results2"), + "SELECT d FROM DUMMY d WHERE d[at0001, 'Results']/foo[at0002, 'Results2'] = 42"); + + // IdentifiedPath: nodeConstraint + local terminology => interpreted as String + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE d[at0001,$nameConstraint]/foo[at0002,$nameConstraint2] = 42", + Map.of("nameConstraint", "at0002", "nameConstraint2", "at0003"), + "SELECT d FROM DUMMY d WHERE d[at0001, 'at0002']/foo[at0002, 'at0003'] = 42"); + + // IdentifiedPath: nodeConstraint + TERM_CODE => interpreted as String + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE d[at0001,$nameConstraint]/foo[at0002,$nameConstraint2] = 42", + Map.of("nameConstraint", "ISO_639-1::en", "nameConstraint2", "ISO_639-1::de"), + "SELECT d FROM DUMMY d WHERE d[at0001, 'ISO_639-1::en']/foo[at0002, 'ISO_639-1::de'] = 42"); + + // IdentifiedPath: standard predicates + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE d[foo=$foo AND bar=$bar]/foo[foo=$foo2 AND bar=$bar2] = 42", + Map.of("foo", "FOO", "bar", 13, "foo2", "FOO2", "bar2", 31), + "SELECT d FROM DUMMY d WHERE d[foo='FOO' AND bar=13]/foo[foo='FOO2' AND bar=31] = 42"); + + // ignored + duplicate usage + assertReplaceParameters( + "SELECT d FROM DUMMY d WHERE (d/f1 = $bar AND d/f2 = $bar AND d/f3 = $baz)", + Map.of("foo", "bob", "bar", "alice", "baz", "charly"), + "SELECT d FROM DUMMY d WHERE (d/f1 = 'alice' AND d/f2 = 'alice' AND d/f3 = 'charly')"); + + // missing + assertReplaceParametersRejected( + "SELECT d FROM DUMMY d WHERE (d/f1 = $bar AND d/f2 = $bar AND d/f3 = $baz)", + Map.of("foo", "bob", "bar", "alice")) + .isExactlyInstanceOf(AqlParseException.class) + .hasMessageContaining("Missing parameter") + .hasMessageContaining("baz"); + } + + @Test + void replaceFromParameters() { + + // archetype_node_id + assertReplaceParameters( + "SELECT d FROM DUMMY d[$ani]", Map.of("ani", "at0001"), "SELECT d FROM DUMMY d[at0001]"); + assertReplaceParametersRejected("SELECT d FROM DUMMY d[$ani]", Map.of("ani", "invalid-id")) + .isInstanceOf(AqlParseException.class); + + // nodeConstraint + name + assertReplaceParameters( + "SELECT d FROM DUMMY d[at0001,$nameConstraint]", + Map.of("nameConstraint", "Results"), + "SELECT d FROM DUMMY d[at0001, 'Results']"); + + // nodeConstraint + local terminology => interpreted as String + assertReplaceParameters( + "SELECT d FROM DUMMY d[at0001,$nameConstraint]", + Map.of("nameConstraint", "at0002"), + "SELECT d FROM DUMMY d[at0001, 'at0002']"); + + // nodeConstraint + TERM_CODE => interpreted as String + assertReplaceParameters( + "SELECT d FROM DUMMY d[at0001,$nameConstraint]", + Map.of("nameConstraint", "ISO_639-1::en"), + "SELECT d FROM DUMMY d[at0001, 'ISO_639-1::en']"); + + // standard predicates + assertReplaceParameters( + "SELECT d FROM DUMMY d[foo=$foo AND bar=$bar]", + Map.of("foo", "FOO", "bar", 42), + "SELECT d FROM DUMMY d[foo='FOO' AND bar=42]"); + + // VERSION + assertReplaceParameters( + "SELECT v FROM VERSION v[commit_audit/time_committed>$time_committed]", + Map.of("time_committed", "2021-12-03T16:05:19.514097+01:00"), + "SELECT v FROM VERSION v[commit_audit/time_committed>'2021-12-03T16:05:19.514097+01:00']"); + } + + @Test + void replaceSelectParameters() { + assertReplaceParameters( + "SELECT d[$foo]/e[bar=$foo AND ba/z=$baz] FROM DUMMY d", + Map.of("foo", "at0001", "baz", 42), + "SELECT d[at0001]/e[bar='at0001' AND ba/z=42] FROM DUMMY d"); + + assertReplaceParameters( + "SELECT SUM(d[$foo]/e[bar=$foo AND ba/z=$baz]), LENGTH(d[$foo]/e[bar=$foo AND ba/z=$baz]) FROM DUMMY d", + Map.of("foo", "at0001", "baz", 42), + "SELECT SUM(d[at0001]/e[bar='at0001' AND ba/z=42]), LENGTH(d[at0001]/e[bar='at0001' AND ba/z=42]) FROM DUMMY d"); + + assertReplaceParametersRejected( + "SELECT d[$foo]/e[bar=$foo AND ba/z=$baz] FROM DUMMY d", Map.of("foo", "invalid-id", "baz", 42)) + .isInstanceOf(AqlParseException.class); + + assertReplaceParametersRejected("SELECT d/e[$foo] FROM DUMMY d", Map.of("foo", 42)) + .isInstanceOf(AqlParseException.class); + } + + @Test + void replaceOrderByParameters() { + assertReplaceParameters( + "SELECT d[$foo]/e[bar=$foo AND ba/z=$baz] FROM DUMMY d ORDER BY d[$foo]/e[bar=$foo AND ba/z=$baz] DESC", + Map.of("foo", "at0001", "baz", 42), + "SELECT d[at0001]/e[bar='at0001' AND ba/z=42] FROM DUMMY d ORDER BY d[at0001]/e[bar='at0001' AND ba/z=42] DESC"); + } + + private static void assertReplaceParameters(String srcAql, Map parameterMap, String expected) { + AqlQuery query = AqlQuery.parse(srcAql); + AqlParameterReplacement.replaceParameters(query, parameterMap); + String rendered = query.render(); + try { + AqlQuery.parse(rendered); + } catch (AqlParseException e) { + fail("Produced invalid query %s : \n %s", rendered, e.getMessage()); + } + assertThat(rendered).isEqualTo(expected); + } + + private static AbstractThrowableAssert assertReplaceParametersRejected( + String srcAql, Map parameterMap) { + AqlQuery query = AqlQuery.parse(srcAql); + return assertThatThrownBy(() -> AqlParameterReplacement.replaceParameters(query, parameterMap)); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtilTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtilTest.java new file mode 100644 index 000000000..536955556 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtilTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.service.ContributionService; +import org.ehrbase.jooq.pg.enums.ContributionChangeType; +import org.junit.jupiter.api.Test; + +class ChangeTypeUtilTest { + + @Test + void ensureJooqChangeTypeToCodeMappingsMatch() { + Arrays.stream(ContributionChangeType.values()) + .map(jct -> Pair.of( + Integer.toString(ContributionService.ContributionChangeType.valueOf( + jct.getLiteral().toUpperCase()) + .getCode()), + jct)) + .forEach(p -> { + assertEquals(p.getLeft(), ChangeTypeUtils.getCodeByJooqChangeType(p.getRight())); + assertEquals(p.getRight(), ChangeTypeUtils.getJooqChangeTypeByCode(p.getLeft())); + }); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayerTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayerTest.java new file mode 100644 index 000000000..ff1aa6eae --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayerTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslSubqueryField; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslEncapsulatingQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslPathDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.jooq.JSONB; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +public class AqlSqlLayerTest { + + private final KnowledgeCacheService mockKnowledgeCacheService = mock(); + + @BeforeEach + void setUp() { + Mockito.reset(mockKnowledgeCacheService); + Mockito.when(mockKnowledgeCacheService.findUuidByTemplateId(ArgumentMatchers.anyString())) + .thenReturn(Optional.of(UUID.randomUUID())); + } + + @Disabled + @Test + void printAslGraph() { + AslRootQuery aslQuery = buildSqlQuery( + """ + SELECT + c/feeder_audit, + c/uid/value, + c/context/other_context[at0004]/items[at0014]/value + FROM EHR e CONTAINS COMPOSITION c + WHERE e/ehr_id/value = 'e6fad8ba-fb4f-46a2-bf82-66edb43f142f' + """); + System.out.println(AslGraph.createAslGraph(aslQuery)); + } + + @Test + void testDataQueryPlacedLast() { + AslRootQuery aslQuery = buildSqlQuery( + """ + SELECT + c/content, + c/content[at0001], + c[openEHR-EHR-COMPOSITION.test.v0]/content[at0002], + c/uid/value, + c/context/other_context[at0004]/items[at0014]/value + FROM EHR e CONTAINS COMPOSITION c + WHERE e/ehr_id/value = 'e6fad8ba-fb4f-46a2-bf82-66edb43f142f' + """); + List queries = + aslQuery.getChildren().stream().map(Pair::getLeft).toList(); + + assertThat(queries).hasSize(5); + + assertThat(queries.get(0)).isInstanceOf(AslStructureQuery.class); + assertThat(queries.get(1)).isInstanceOf(AslStructureQuery.class); + assertThat(queries.get(2)).isInstanceOf(AslEncapsulatingQuery.class); + assertThat(queries.get(3)).isInstanceOf(AslEncapsulatingQuery.class); + assertThat(queries.get(4)).isInstanceOf(AslPathDataQuery.class); + + // feeder_audit + AslField contentField1 = aslQuery.getSelect().get(0); + AslField contentField2 = aslQuery.getSelect().get(1); + AslField contentField3 = aslQuery.getSelect().get(2); + + // check select + assertThat(contentField1).isInstanceOf(AslSubqueryField.class); + assertThat(((AslSubqueryField) contentField1).getFilterConditions()).isEmpty(); + assertThat(contentField2).isInstanceOf(AslSubqueryField.class); + assertThat(((AslSubqueryField) contentField2).getFilterConditions()).hasSize(1); + assertThat(contentField3).isInstanceOf(AslSubqueryField.class); + assertThat(((AslSubqueryField) contentField3).getFilterConditions()).hasSize(2); + + // assertThat(queries.get(5)).isInstanceOf(AslRmObjectDataQuery.class); + } + + @Test + void clusterDataSingleSelection() { + + AslRootQuery aslQuery = buildSqlQuery( + """ + SELECT + cluster/items[at0001]/value/data + FROM COMPOSITION CONTAINS CLUSTER cluster[openEHR-EHR-CLUSTER.media_file.v1] + """); + List queries = + aslQuery.getChildren().stream().map(Pair::getLeft).toList(); + + assertThat(queries).hasSize(4); + + assertThat(queries.get(0)).isInstanceOf(AslStructureQuery.class); + assertThat(queries.get(1)).isInstanceOf(AslStructureQuery.class); + assertThat(queries.get(2)).isInstanceOf(AslEncapsulatingQuery.class); + assertThat(queries.get(3)).isInstanceOfSatisfying(AslPathDataQuery.class, q -> { + assertThat(q.isMultipleValued()).isFalse(); + assertThat(q.getDataField().getColumnName()).isEqualTo("data"); + assertThat(q.getDataField().getType()).isSameAs(JSONB.class); + }); + } + + private AslRootQuery buildSqlQuery(String query) { + + AqlQuery aqlQuery = AqlQueryParser.parse(query); + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + AqlSqlLayer aqlSqlLayer = new AqlSqlLayer(mockKnowledgeCacheService, () -> "node"); + return aqlSqlLayer.buildAslRootQuery(queryWrapper); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraph.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraph.java new file mode 100644 index 000000000..fbdc3674b --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraph.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import static org.apache.commons.lang3.StringUtils.join; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslAndQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslDescendantCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslEntityIdxOffsetCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFalseQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotNullQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslOrQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslPathChildCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.condition.AslTrueQueryCondition; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslAggregatingField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslComplexExtractedColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslConstantField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslOrderByField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslSubqueryField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslAuditDetailsJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslDelegatingJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoin; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslPathFilterJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslEncapsulatingQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslPathDataQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; + +public class AslGraph { + + public static String createAslGraph(AslRootQuery query) { + return join( + indented(0, "AslRootQuery"), + selectGraph(1, query.getSelect()), + indented(1, "FROM"), + query.getChildren().stream() + .map(s -> sqToGraph(2, s.getLeft(), s.getRight())) + .collect(Collectors.joining()), + section(1, query.getCondition(), Objects::nonNull, __ -> "WHERE", AslGraph::conditionToGraph), + section( + 1, + query.getGroupByFields(), + CollectionUtils::isNotEmpty, + __ -> "GROUP BY", + (l, fs) -> fs.stream() + .map(f -> indented(l, fieldToGraph(l, f))) + .collect(Collectors.joining())), + section( + 1, + query.getOrderByFields(), + CollectionUtils::isNotEmpty, + __ -> "ORDER BY", + (l, fs) -> fs.stream().map(f -> orderByToGraph(l, f)).collect(Collectors.joining())), + section(1, query.getLimit(), Objects::nonNull, "LIMIT %d"::formatted, (l, v) -> ""), + section(1, query.getOffset(), Objects::nonNull, "OFFSET %d"::formatted, (l, v) -> "")); + } + + private static String selectGraph(int level, List select) { + return indented(level, "SELECT") + indented(level + 1, select.stream(), s -> fieldToGraph(level + 1, s)); + } + + private static String sqToGraph(int level, AslQuery subquery, AslJoin join) { + String fromStructure = section( + level + 1, + subquery, + AslStructureQuery.class::isInstance, + sq -> "FROM " + ((AslStructureQuery) sq).getType().name(), + (l, sq) -> ""); + + String fromEncapsulating = section( + level + 2, + subquery, + AslEncapsulatingQuery.class::isInstance, + __ -> "FROM", + (l, sq) -> indented( + l, + ((AslEncapsulatingQuery) sq).getChildren().stream(), + c -> sqToGraph(l + 1, c.getLeft(), c.getRight()))); + String base = section( + level + 1, + subquery, + AslDataQuery.class::isInstance, + sq -> "BASE " + ((AslDataQuery) sq).getBase().getAlias(), + (l, sq) -> ""); + + String joinStr = Optional.ofNullable(join) + .map(j -> indented( + level + 1, + j.getJoinType() + " " + j.getLeft().getAlias() + " -> " + + j.getRight().getAlias()) + + section( + level + 2, + j.getOn(), + CollectionUtils::isNotEmpty, + c -> "on", + AslGraph::conditionsToGraph)) + .orElse(""); + + String queryComment = + switch (subquery) { + case AslPathDataQuery pq -> pq.getPathNodes(pq.getDataField()).stream() + .map(p -> p.getAttribute() + p.getPredicateOrOperands()) + .collect(Collectors.joining(".", " -- ", "")); + default -> ""; + }; + + return indented(level == 2 ? 2 : 0, subquery.getAlias() + ": " + typeName(subquery) + queryComment) + + selectGraph(level + 1, subquery.getSelect()) + + base + + section( + level + 1, subquery.getCondition(), Objects::nonNull, c -> "WHERE", AslGraph::conditionToGraph) + + fromStructure + + fromEncapsulating + + section( + level + 1, + subquery.getStructureConditions(), + CollectionUtils::isNotEmpty, + c -> "STRUCTURE CONDITIONS", + (l, cs) -> cs.stream() + .map(c -> conditionToGraph(level + 2, c)) + .collect(Collectors.joining())) + + joinStr; + } + + private static String section( + int level, T t, Predicate condition, Function header, BiFunction body) { + if (!condition.test(t)) { + return ""; + } + Optional heading = + Optional.of(header.apply(t)).filter(StringUtils::isNotBlank).map(h -> indented(level, h)); + return heading.orElse("") + body.apply(level + (heading.isPresent() ? 1 : 0), t); + } + + private static String typeName(AslQuery subquery) { + String simpleName = subquery.getClass().getSimpleName(); + return StringUtils.removeStart(simpleName, "Asl"); + } + + private static String conditionToGraph(int level, AslQueryCondition condition) { + return switch (condition) { + case null -> ""; + case AslNotQueryCondition c -> indented(level, "NOT") + conditionToGraph(level + 1, c.getCondition()); + case AslFieldValueQueryCondition c -> indented( + level, fieldToGraph(level + 1, c.getField()) + " " + c.getOperator() + " " + c.getValues()); + case AslFalseQueryCondition aslFalseQueryCondition -> indented(level, "false"); + case AslTrueQueryCondition aslTrueQueryCondition -> indented(level, "true"); + case AslOrQueryCondition c -> indented(level, "OR") + + c.getOperands().stream() + .map(op -> conditionToGraph(level + 1, op)) + .collect(Collectors.joining()); + case AslAndQueryCondition c -> indented(level, "AND") + + c.getOperands().stream() + .map(op -> conditionToGraph(level + 1, op)) + .collect(Collectors.joining()); + case AslNotNullQueryCondition c -> indented(level, "NOT_NULL " + fieldToGraph(level + 1, c.getField())); + case AslEntityIdxOffsetCondition c -> indented( + level, + "EntityIdxOffset %s -%d-> %s" + .formatted( + c.getLeftOwner().getAlias(), + c.getOffset(), + c.getRightOwner().getAlias())); + case AslDescendantCondition c -> indented( + level, + "DescendantCondition %s %s -> %s %s" + .formatted( + c.getParentRelation(), + c.getLeftOwner().getAlias(), + c.getDescendantRelation(), + c.getRightOwner().getAlias())); + case AslPathChildCondition c -> indented( + level, + "PathChildCondition %s %s -> %s %s" + .formatted( + c.getParentRelation(), + c.getLeftOwner().getAlias(), + c.getChildRelation(), + c.getRightOwner().getAlias())); + }; + } + + private static String conditionsToGraph(int level, List joinConditions) { + return joinConditions.stream() + .map(jc -> switch (jc) { + case AslPathFilterJoinCondition c -> "PathFilterJoinCondition %s ->\n%s" + .formatted(c.getLeftOwner().getAlias(), conditionToGraph(level + 2, c.getCondition())); + case AslDelegatingJoinCondition c -> "DelegatingJoinCondition %s ->\n%s" + .formatted(c.getLeftOwner().getAlias(), conditionToGraph(level + 2, c.getDelegate())); + case AslAuditDetailsJoinCondition c -> "AuditDetailsJoinCondition %s -> %s" + .formatted( + c.getLeftOwner().getAlias(), + c.getRightOwner().getAlias()); + }) + .map(s -> indented(level, s)) + .collect(Collectors.joining()); + } + + private static String orderByToGraph(int level, AslOrderByField sortOrderPair) { + return fieldToGraph(level, sortOrderPair.field()) + " " + sortOrderPair.direction(); + } + + private static String fieldToGraph(int level, AslField field) { + String providerAlias = (field.getInternalProvider() != null) + ? (field.getInternalProvider().getAlias() + ".") + : ""; + return switch (field) { + case AslColumnField f -> providerAlias + + f.getAliasedName() + + Optional.of(f) + .map(AslColumnField::getExtractedColumn) + .map(e -> " -- " + e.getPath().render()) + .orElse(""); + case AslComplexExtractedColumnField f -> providerAlias + "??" + + Optional.of(f) + .map(AslComplexExtractedColumnField::getExtractedColumn) + .map(e -> " -- COMPLEX " + e.name() + " " + + e.getPath().render()) + .orElse(""); + case AslAggregatingField f -> "%s(%s%s)" + .formatted( + f.getFunction(), + f.isDistinct() ? "DISTINCT " : "", + Optional.of(f) + .map(AslAggregatingField::getBaseField) + .map(bf -> fieldToGraph(level, bf)) + .orElse("*")); + case AslSubqueryField f -> sqToGraph(level + 1, f.getBaseQuery(), null) + + (f.getFilterConditions().isEmpty() + ? "" + : indented(level + 1, "Filter:") + + f.getFilterConditions().stream() + .map(c -> conditionToGraph(level + 2, c)) + .collect(Collectors.joining("\n", "", ""))); + case AslConstantField f -> "CONSTANT (%s): %s".formatted(f.getType().getSimpleName(), f.getValue()); + }; + } + + private static String indented(int level, Stream entries, Function toString) { + String prefix = StringUtils.repeat(" ", level); + return entries.map(toString::apply).collect(Collectors.joining("\n" + prefix, prefix, "\n")); + } + + private static String indented(int level, String str) { + String prefix = StringUtils.repeat(" ", level); + return prefix + str + "\n"; + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraphTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraphTest.java new file mode 100644 index 000000000..08aa2d44c --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraphTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.junit.jupiter.api.Test; + +class AslGraphTest { + + @Test + void printDataQueryGraph() { + + AqlQuery aqlQuery = AqlQueryParser.parse( + """ + SELECT + -- c1/content[openEHR-EHR-SECTION.adhoc.v1], + -- c1/content[openEHR-EHR-SECTION.adhoc.v1]/name, + c1/content[openEHR-EHR-SECTION.adhoc.v1]/name/value + -- ,c1/content[openEHR-EHR-SECTION.adhoc.v1,'Diagnostic Results']/name/value + FROM EHR e + CONTAINS COMPOSITION c1 + """); + + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + AslRootQuery rootQuery = new AqlSqlLayer(null, () -> "node").buildAslRootQuery(queryWrapper); + + System.out.println(AslGraph.createAslGraph(rootQuery)); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslUtilsTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslUtilsTest.java new file mode 100644 index 000000000..f9b998dc6 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslUtilsTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class AslUtilsTest { + + @Test + void translateAqlLikePatern() { + assertEquals("abc", AslUtils.translateAqlLikePatternToSql("abc")); + assertEquals("X\\\\?*\\%\\_%_X", AslUtils.translateAqlLikePatternToSql("X\\\\\\?\\*%_*?X")); + assertEquals("\\\\?*\\%\\_%_X", AslUtils.translateAqlLikePatternToSql("\\\\\\?\\*%_*?X")); + assertEquals("X\\%\\_%_X\\\\?*", AslUtils.translateAqlLikePatternToSql("X%_*?X\\\\\\?\\*")); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/model/AslRmTypeAndConceptTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/model/AslRmTypeAndConceptTest.java new file mode 100644 index 000000000..570d66c66 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/model/AslRmTypeAndConceptTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.asl.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class AslRmTypeAndConceptTest { + @Test + void fromArchetypeNodeId() { + + assertThat(AslRmTypeAndConcept.fromArchetypeNodeId("openEHR-EHR-OBSERVATION.symptom_sign_screening.v0")) + .isEqualTo(new AslRmTypeAndConcept("OB", ".symptom_sign_screening.v0")); + assertThat(AslRmTypeAndConcept.fromArchetypeNodeId("at123")).isEqualTo(new AslRmTypeAndConcept(null, "at123")); + assertThat(AslRmTypeAndConcept.fromArchetypeNodeId("id123")).isEqualTo(new AslRmTypeAndConcept(null, "id123")); + assertThrows( + IllegalArgumentException.class, + () -> AslRmTypeAndConcept.fromArchetypeNodeId("openEHR-EHR-OBSERVATION")); + assertThrows(IllegalArgumentException.class, () -> AslRmTypeAndConcept.fromArchetypeNodeId("nr123")); + } + + @Test + void toEntityConcept() { + assertThat(AslRmTypeAndConcept.toEntityConcept("openEHR-EHR-OBSERVATION.symptom_sign_screening.v0")) + .isEqualTo(".symptom_sign_screening.v0"); + assertThat(AslRmTypeAndConcept.toEntityConcept("at123")).isEqualTo("at123"); + assertThat(AslRmTypeAndConcept.toEntityConcept("id123")).isEqualTo("id123"); + assertThrows( + IllegalArgumentException.class, () -> AslRmTypeAndConcept.toEntityConcept("openEHR-EHR-OBSERVATION")); + assertThrows(IllegalArgumentException.class, () -> AslRmTypeAndConcept.toEntityConcept("nr123")); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheckTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheckTest.java new file mode 100644 index 000000000..a846d057f --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheckTest.java @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.featurecheck; + +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AqlQueryFeatureCheckTest { + + @ParameterizedTest + @ValueSource( + strings = { + "SELECT s FROM EHR e CONTAINS EHR_STATUS s", + "SELECT e/ehr_id/value FROM EHR e CONTAINS COMPOSITION LIMIT 10 OFFSET 20", + "SELECT c from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] CONTAINS COMPOSITION c", + "SELECT c from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] CONTAINS COMPOSITION c CONTAINS OBSERVATION", + """ + SELECT c, it from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] + CONTAINS COMPOSITION c CONTAINS OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE it""", + """ + SELECT c from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] + CONTAINS COMPOSITION c + CONTAINS OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1]""", + """ + SELECT e/ehr_id/value, + c/uid/value, c/name/value, c/archetype_node_id, c/archetype_details/template_id/value, + o/name/value, o/archetype_node_id + FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o""", + """ + SELECT c from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] + CONTAINS COMPOSITION c + CONTAINS OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1]""", + """ + SELECT c from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] + CONTAINS COMPOSITION c + CONTAINS OBSERVATION[name/value='Blood pressure (Training sample)']""", + """ + SELECT c from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] + CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.sample_blood_pressure.v1,'Blood pressure (Training sample)'] + CONTAINS OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1,'Blood pressure (Training sample)']""", + """ + SELECT c from EHR [ehr_id/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea' OR ehr_id/value!='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] + CONTAINS COMPOSITION c[name/value!='Blood pressure (Training sample)' AND archetype_node_id='openEHR-EHR-COMPOSITION.sample_blood_pressure.v1' OR uid/value='b037bf7c-0ecb-40fb-aada-fc7d559815ea'] + CONTAINS OBSERVATION[name/value!='Blood pressure (Training sample)' AND archetype_node_id='openEHR-EHR-COMPOSITION.sample_blood_pressure.v1' OR name/value!='Blood pressure (Training sample)']""", + """ + SELECT o + FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o + WHERE e/ehr_id/value MATCHES {'b037bf7c-0ecb-40fb-aada-fc7d559815ea'} + AND (o/archetype_node_id LIKE 'openEHR-EHR-OBSERVATION.sample_blood_pressure.*' + OR o/name/value = 'Blood pressure (Training sample)') + AND c/uid/value != 'b037bf7c-0ecb-40fb-aada-fc7d559815ea' + AND c/archetype_details/template_id/value = 'some-template.v1'""", + """ + SELECT e/ehr_id/value, c1, c2, o, ev, a + FROM EHR e CONTAINS( + (COMPOSITION c1 + CONTAINS OBSERVATION o + AND EVALUATION ev) + AND COMPOSITION c2 CONTAINS ADMIN_ENTRY a)""", + """ + SELECT e/ehr_id/value, c1/content/name/value, c1/content/data/name/value, o, ev + FROM EHR e CONTAINS + COMPOSITION c1 + CONTAINS OBSERVATION o + CONTAINS EVALUATION ev + WHERE c1/content/name/value = 'My Observation'""", + """ + SELECT e/ehr_id/value, c/content/name/value + FROM EHR e CONTAINS COMPOSITION c + ORDER BY e/ehr_id/value, c/content/name/value""", + """ + SELECT c/context/start_time + FROM COMPOSITION c + ORDER BY c/context/start_time + """, + """ + SELECT ec/start_time/value + FROM EHR e CONTAINS COMPOSITION c CONTAINS EVENT_CONTEXT ec + ORDER BY ec/start_time ASC + """, + // """ + // SELECT c + // FROM COMPOSITION c + // ORDER BY c/language/code_string + // """, + """ + SELECT e/ehr_id/value, c/content + FROM EHR e CONTAINS COMPOSITION c + """, + "SELECT c/setting/defining_code/code_string FROM EVENT_CONTEXT c", + """ + SELECT + o/name/mappings, + o/name/mappings/target, + o/name/mappings/purpose/mappings, + o/name/mappings/purpose/mappings/target + FROM OBSERVATION o + """, + "SELECT c/start_time/value, e/value/value, e/value/magnitude FROM EVENT_CONTEXT c CONTAINS ELEMENT e", + """ + SELECT c + FROM EVENT_CONTEXT c CONTAINS ELEMENT e + WHERE e/value = '1' AND c/start_time < '2023-10-13' + """, + """ + SELECT l/name/value + FROM EHR e + CONTAINS EHR_STATUS + CONTAINS ELEMENT l + """, + """ + SELECT s/subject/external_ref/id/value, s/other_details/items[at0001]/value/id + FROM EHR e + CONTAINS EHR_STATUS s + """, + """ + SELECT s/other_details/items[at0001]/value/id + FROM EHR e + CONTAINS EHR_STATUS s + WHERE e/ehr_id/value = '10f23be7-fd39-4e71-a0a5-9d1624d662b7' + """, + """ + SELECT t FROM ENTRY t + """, + """ + SELECT + e/ehr_id/value, + -- All allowed usages of aggregate functions + COUNT(*), + COUNT(DISTINCT c/uid/value), + COUNT(el), + COUNT(el/name/mappings), + COUNT(el/value), + COUNT(el/value/value), + MAX(el/value/value), + MIN(el/value/value), + MAX(el/value), + MIN(el/value), + AVG(el/value/value), + SUM(el/value/value) + FROM EHR e CONTAINS COMPOSITION c CONTAINS ELEMENT el + """, + "SELECT 1 FROM EHR e", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::node::1'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::::1'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea'", + "SELECT e/ehr_id/value, e/time_created, e/time_created/value FROM EHR e WHERE e/time_created > '2021-01-02T12:13:14+01:00' ORDER BY e/time_created", + """ + SELECT + e/ehr_id/value, + e/system_id, + e/system_id/value + FROM EHR e + WHERE e/system_id/value = 'abc' + ORDER BY e/system_id/value + """ + }) + void ensureQuerySupported(String aql) { + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + new AqlQueryFeatureCheck(() -> "node").ensureQuerySupported(aqlQuery); + } + + @ParameterizedTest + @ValueSource( + strings = { + "SELECT e FROM EHR e", + "SELECT e/ehr_id FROM EHR e", + """ + SELECT c + FROM COMPOSITION c + WHERE c/uid/value = c/name/value + """, + """ + SELECT c + FROM COMPOSITION c + WHERE c/uid = '1' + """, + """ + SELECT c + FROM COMPOSITION c + WHERE EXISTS c/uid/value + """, + """ + SELECT c + FROM COMPOSITION c + ORDER BY c/context/start_time/value + """, + """ + SELECT o + FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o + WHERE e/ehr_id/value MATCHES {'b037bf7c-0ecb-40fb-aada-fc7d559815ea'} + AND (o/archetype_node_id LIKE 'openEHR-EHR-OBSERVATION.sample_blood_pressure.*' + OR o/name/value = 'Blood pressure (Training sample)') + AND c/uid/value != 'b037bf7c-0ecb-40fb-aada-fc7d559815ea' + AND EXISTS c/name/value + AND c/archetype_details/template_id/value = 'some-template.v1'""", + """ + SELECT e/ehr_id/value, AVG(c/context/start_time) + FROM EHR e CONTAINS COMPOSITION c + """, + """ + SELECT e/ehr_id/value, SUM(c/context/start_time) + FROM EHR e CONTAINS COMPOSITION c + """, + """ + SELECT e/ehr_id/value, MAX(c/uid/value) + FROM EHR e CONTAINS COMPOSITION c + """, + """ + SELECT e/ehr_id/value, MIN(c/uid/value) + FROM EHR e CONTAINS COMPOSITION c + """, + """ + SELECT e/ehr_id/value, AVG(c/uid/value) + FROM EHR e CONTAINS COMPOSITION c + """, + """ + SELECT e/ehr_id/value, SUM(c/uid/value) + FROM EHR e CONTAINS COMPOSITION c + """, + "SELECT e/ehr_id/value FROM EHR e WHERE e/time_created/value > '2021-01-02T12:13:14+01:00'", + "SELECT e/ehr_id/value FROM EHR e ORDER BY e/time_created/value" + }) + void ensureQueryNotSupported(String aql) { + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + Assertions.assertThrows(AqlFeatureNotImplementedException.class, () -> new AqlQueryFeatureCheck(() -> "node") + .ensureQuerySupported(aqlQuery)); + } + + @ParameterizedTest + @ValueSource( + strings = { + """ + SELECT c/content/content/name/value + FROM COMPOSITION c + """, + """ + SELECT c + FROM COMPOSITION c + WHERE c/content/content/name/value = 'invalid' + """ + }) + void ensureInvalidPathRejected(String aql) { + + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + org.assertj.core.api.Assertions.assertThat( + Assertions.assertThrows(IllegalAqlException.class, () -> new AqlQueryFeatureCheck(() -> "node") + .ensureQuerySupported(aqlQuery)) + .getMessage()) + .endsWith(" is not a valid RM path"); + } + + @ParameterizedTest + @ValueSource( + strings = { + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'foo'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'foo::node::1'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'foo::node'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'foo::::'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'foo::::1'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = ''", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = '::node::1'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = '::node'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = '::::'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = '::::1'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::invalid::1'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::node::foo'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::node::0'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::::foo'", + "SELECT c FROM COMPOSITION c WHERE c/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::::0'" + }) + void ensureInvalidConditionRejected(String aql) { + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + Assertions.assertThrows( + IllegalAqlException.class, () -> new AqlQueryFeatureCheck(() -> "node").ensureQuerySupported(aqlQuery)); + } + + @ParameterizedTest + @ValueSource( + strings = { + """ + SELECT c + FROM COMPOSITION c CONTAINS EHR_STATUS + """, + """ + SELECT c + FROM COMPOSITION c CONTAINS ELEMENT CONTAINS EHR_STATUS + """, + """ + SELECT el/name/value + FROM EHR CONTAINS COMPOSITION + CONTAINS EHR_STATUS + CONTAINS ELEMENT el + """ + }) + void ensureContainsRejected(String aql) { + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + org.assertj.core.api.Assertions.assertThat( + Assertions.assertThrows(IllegalAqlException.class, () -> new AqlQueryFeatureCheck(() -> "node") + .ensureQuerySupported(aqlQuery)) + .getMessage()) + .contains(" cannot CONTAIN "); + } + + @ParameterizedTest + @ValueSource( + strings = { + """ + SELECT c/uid/value + FROM VERSION cv CONTAINS COMPOSITION c + """, + """ + SELECT c/uid/value + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT e/ehr_id/value, c/uid/value + FROM EHR e CONTAINS VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + // all supported usages of all paths for (ORIGNINAL_)VERSION + """ + SELECT + cv/uid/value, + cv/commit_audit/time_committed, + cv/commit_audit/time_committed/value, + cv/commit_audit/system_id, + cv/commit_audit/description, + cv/commit_audit/description/value, + cv/commit_audit/change_type, + cv/commit_audit/change_type/value, + cv/commit_audit/change_type/defining_code/code_string, + cv/commit_audit/change_type/defining_code/preferred_term, + cv/commit_audit/change_type/defining_code/terminology_id/value, + cv/contribution/id/value + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + WHERE cv/uid/value = 'b037bf7c-0ecb-40fb-aada-fc7d559815ea::node::2' + AND cv/commit_audit/time_committed < '2021' + AND cv/commit_audit/system_id = 'system' + AND cv/commit_audit/description/value = 'description' + AND cv/commit_audit/change_type/value = 'ct' + AND cv/commit_audit/change_type/defining_code/code_string = 'ct' + AND cv/commit_audit/change_type/defining_code/preferred_term = 'ct' + AND cv/commit_audit/change_type/defining_code/terminology_id/value = 'ct' + AND cv/contribution/id/value = 'c037bf7c-0ecb-40fb-aada-fc7d559815eb' + ORDER BY + cv/commit_audit/change_type/defining_code/code_string, + cv/commit_audit/change_type/defining_code/preferred_term, + cv/commit_audit/change_type/value, + cv/commit_audit/description/value, + cv/commit_audit/time_committed, + cv/uid/value + """, + """ + SELECT es/uid/value + FROM VERSION cv[LATEST_VERSION] CONTAINS EHR_STATUS es + """, + """ + SELECT e/ehr_id/value, c1/uid/value, c2/uid/value + FROM EHR e CONTAINS + (VERSION[LATEST_VERSION] CONTAINS COMPOSITION c1) + OR (VERSION[LATEST_VERSION] CONTAINS COMPOSITION c2) + """, + """ + SELECT e/ehr_id/value, c1/uid/value, c2/uid/value + FROM EHR e CONTAINS + (VERSION[LATEST_VERSION] CONTAINS COMPOSITION c1) + AND (VERSION[LATEST_VERSION] CONTAINS COMPOSITION c2) + """, + }) + void ensureVersionSupported(String aql) { + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + new AqlQueryFeatureCheck(() -> "node").ensureQuerySupported(aqlQuery); + } + + @ParameterizedTest + @ValueSource( + strings = { + """ + SELECT cv/commit_audit/time_committed/value + FROM VERSION cv[LATEST_VERSION] + """, + """ + SELECT el/name/value + FROM VERSION cv[LATEST_VERSION] CONTAINS ELEMENT el + """, + """ + SELECT c/name/value + FROM VERSION cv[LATEST_VERSION] CONTAINS VERSION cv2[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT c/name/value + FROM COMPOSITION c CONTAINS VERSION cv[LATEST_VERSION] + """ + }) + void checkIllegalVersion(String aql) { + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + Assertions.assertThrows( + IllegalAqlException.class, () -> new AqlQueryFeatureCheck(() -> "node").ensureQuerySupported(aqlQuery)); + } + + @ParameterizedTest + @ValueSource( + strings = { + """ + SELECT c/uid/value + FROM VERSION cv[ALL_VERSIONS] CONTAINS COMPOSITION c + """, + """ + SELECT c/uid/value + FROM VERSION cv[commit_audit/time_committed > '2021-12-13'] CONTAINS COMPOSITION c + """, + """ + SELECT f/uid/value + FROM VERSION cv[LATEST_VERSION] CONTAINS FOLDER f + """, + """ + SELECT c1/name/value, c2/name/value + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c1 OR COMPOSITION c2 + """, + """ + SELECT c1/name/value, c2/name/value + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c1 AND COMPOSITION c2 + """, + """ + SELECT cv/preceding_version_uid + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT cv/other_input_version_uids + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT cv/data + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT cv/attestations + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT cv/lifecycle_state + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT cv/signature + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """, + """ + SELECT cv + FROM VERSION cv[LATEST_VERSION] CONTAINS COMPOSITION c + """ + }) + void ensureVersionNotSupported(String aql) { + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + Assertions.assertThrows(AqlFeatureNotImplementedException.class, () -> new AqlQueryFeatureCheck(() -> "node") + .ensureQuerySupported(aqlQuery)); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/ANodeTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/ANodeTest.java new file mode 100644 index 000000000..599e489fc --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/ANodeTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.junit.jupiter.api.Test; + +class ANodeTest { + @Test + void testNodeCategories() { + + // POINT_EVENT with ITEM_SINGLE data with ELEMENT item + // with DV_SCALE or DV_ORDINAL value (as its value is a number) + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "POINT_EVENT", null, null, AqlObjectPath.parse("data/item/value[value>=0]/value"), null); + + ANode node = rootNode; + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("data"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("item"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("value"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.RM_TYPE); + node = node.attributes.get("value"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.FOUNDATION); + } + + // POINT_EVENT with ITEM_STRUCTURE data with ELEMENT or CLUSTER items + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "POINT_EVENT", null, null, AqlObjectPath.parse("data/items/name[value!='foo']/value"), null); + + ANode node = rootNode; + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("data"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("items"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("name"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.RM_TYPE); + node = node.attributes.get("value"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.FOUNDATION); + } + + // POINT_EVENT with ITEM_STRUCTURE data with CLUSTER with ELEMENT + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "POINT_EVENT", null, null, AqlObjectPath.parse("data/items/items/name[value!='foo']/value"), null); + + ANode node = rootNode; + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("data"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("items"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("items"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + } + + // ACTION with INSTRUCTION_DETAILS instruction_details with ITEM_STRUCTURE wf_details + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "CARE_ENTRY", null, null, AqlObjectPath.parse("instruction_details/wf_details"), null); + + ANode node = rootNode; + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("instruction_details"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE_INTERMEDIATE); + node = node.attributes.get("wf_details"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + } + + // with ITEM with + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "CARE_ENTRY", null, null, AqlObjectPath.parse("instruction_details/wf_details"), null); + + ANode node = rootNode; + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + node = node.attributes.get("instruction_details"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE_INTERMEDIATE); + node = node.attributes.get("wf_details"); + assertThat(node.getCategories()).containsExactly(ANode.NodeCategory.STRUCTURE); + } + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationTypeTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationTypeTest.java new file mode 100644 index 000000000..0d339c62c --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/FoundationTypeTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.nedap.archie.rminfo.RMTypeInfo; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class FoundationTypeTest { + + @ParameterizedTest + @ValueSource(strings = {"STRING", "LONG", "TEMPORAL"}) + void hasFoundationType(String name) { + assertThat(FoundationType.valueOf(name)).isNotNull(); + } + + @Test + void foundationTypesComplete() { + + // make sure that FoundationType contains all needed for Compositions + + Queue remainingTypes = new LinkedList<>(); + remainingTypes.add(PathAnalysis.RM_INFOS.getTypeInfo(RmConstants.COMPOSITION)); + + Set seen = new HashSet<>(); + seen.add(remainingTypes.peek()); + + Set typeNames = new HashSet<>(); + + while (!remainingTypes.isEmpty()) { + RMTypeInfo typeInfo = remainingTypes.poll(); + + typeInfo.getDirectDescendantClasses().stream().filter(seen::add).forEach(remainingTypes::add); + + if (!Modifier.isAbstract(typeInfo.getJavaClass().getModifiers())) { + typeInfo.getAttributes().values().stream() + .filter(ti -> !ti.isComputed()) + .map(ai -> { + String typeName = ai.getTypeNameInCollection(); + + RMTypeInfo ti = PathAnalysis.RM_INFOS.getTypeInfo(typeName); + if (ti == null) { + typeNames.add(typeName); + } + + return typeName; + }) + .map(PathAnalysis.RM_INFOS::getTypeInfo) + .filter(Objects::nonNull) + .filter(seen::add) + .forEach(remainingTypes::add); + } + } + + assertThat(Arrays.stream(FoundationType.values()).map(Enum::name)) + .containsExactlyInAnyOrderElementsOf(typeNames); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathAnalysisTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathAnalysisTest.java new file mode 100644 index 000000000..33d20e495 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathAnalysisTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.ehrbase.openehr.dbformat.RmAttributeAlias; +import org.ehrbase.openehr.sdk.aql.dto.operand.LongPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive; +import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPathUtil; +import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; +import org.junit.jupiter.api.Test; + +class PathAnalysisTest { + + @Test + void compositionTypes() { + assertThat(PathAnalysis.AttributeInfos.rmTypes).isNotEmpty(); + + System.out.println(PathAnalysis.AttributeInfos.rmTypes); + } + + @Test + void baseTypesByAttribute() { + + Map> cut = PathAnalysis.AttributeInfos.baseTypesByAttribute; + + assertThat(cut).isNotEmpty(); + assertThat(cut).containsKey("other_participations"); + + assertThat(cut) + .containsEntry( + "other_participations", + Set.of( + "CARE_ENTRY", + "ADMIN_ENTRY", + "INSTRUCTION", + "OBSERVATION", + "ENTRY", + "ACTION", + "EVALUATION")); + } + + @Test + void analyzeAqlPathInvalid() { + assertThatThrownBy(() -> { + ANode node = PathAnalysis.analyzeAqlPathTypes( + RmConstants.COMPOSITION, + null, + null, + AqlObjectPath.parse("path/links/non/existent/attributes"), + null); + + // Map> attributeInfos = + // PathAnalysisUtil.createAttributeInfos(node); + + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(": non"); + } + + @Test + void analyzeAqlPath() { + + // simple composition + { + ANode node = PathAnalysis.analyzeAqlPathTypes(RmConstants.COMPOSITION, null, null, null, null); + assertThat(node.candidateTypes).containsExactly(RmConstants.COMPOSITION); + assertThat(node.attributes).isEmpty(); + } + + // simple composition + { + ANode node = PathAnalysis.analyzeAqlPathTypes( + "CARE_ENTRY", + archetypeNodeIdCondition("openEHR-EHR-OBSERVATION.my-observation.v3"), + null, + null, + null); + assertThat(node.candidateTypes).containsExactly(RmConstants.OBSERVATION); + assertThat(node.attributes).containsOnlyKeys("archetype_node_id"); + } + + // CARE_ENTRY with state + { + ANode node = PathAnalysis.analyzeAqlPathTypes("CARE_ENTRY", null, null, AqlObjectPath.parse("state"), null); + assertThat(node.candidateTypes).containsExactly(RmConstants.OBSERVATION); + assertThat(node.attributes).containsOnlyKeys("state"); + } + + // CARE_ENTRY with data + { + ANode node = PathAnalysis.analyzeAqlPathTypes("CARE_ENTRY", null, null, AqlObjectPath.parse("data"), null); + assertThat(node.candidateTypes).containsExactly(RmConstants.OBSERVATION, RmConstants.EVALUATION); + assertThat(node.attributes).containsOnlyKeys("data"); + } + + // CARE_ENTRY with data with items; SELECT c/data/events/state FROM CARE_ENTRY c + { + ANode node = PathAnalysis.analyzeAqlPathTypes( + "CARE_ENTRY", null, null, AqlObjectPath.parse("data/events/state"), null); + assertThat(node.candidateTypes).containsExactly(RmConstants.OBSERVATION); + assertThat(node.attributes).containsOnlyKeys("data"); + } + + // ITEM_SINGLE with one element; SELECT s/item/value FROM ITEM_STRUCTURE s + { + ANode node = PathAnalysis.analyzeAqlPathTypes( + "ITEM_STRUCTURE", null, null, AqlObjectPath.parse("item/value"), null); + assertThat(node.candidateTypes).containsExactly(RmConstants.ITEM_SINGLE); + assertThat(node.attributes).containsOnlyKeys("item"); + + ANode item = node.attributes.get("item"); + assertThat(item.candidateTypes).containsExactly(RmConstants.ELEMENT); + + assertThat(item.attributes).containsOnlyKeys("value"); + ANode elementValue = item.attributes.get("value"); + assertThat(elementValue.candidateTypes).isNotEmpty().allMatch(v -> v.startsWith("DV_")); + } + + // ITEM_SINGLE with one element with DvCodedText value + { + ANode node = PathAnalysis.analyzeAqlPathTypes( + "ITEM_STRUCTURE", + null, + null, + AqlObjectPath.parse("item/value[defining_code/terminology_id/value='openehr']/value"), + null); + assertThat(node.candidateTypes).containsExactly(RmConstants.ITEM_SINGLE); + assertThat(node.attributes).containsOnlyKeys("item"); + + ANode item = node.attributes.get("item"); + assertThat(item.candidateTypes).containsExactly(RmConstants.ELEMENT); + + assertThat(item.attributes).containsOnlyKeys("value"); + ANode elementValue = item.attributes.get("value"); + assertThat(elementValue.candidateTypes).allMatch(v -> v.startsWith("DV_")); + + assertThat(elementValue.attributes).containsOnlyKeys("value", "defining_code"); + ANode valueValue = elementValue.attributes.get("value"); + assertThat(valueValue.candidateTypes).containsExactly(FoundationType.STRING.name()); + } + + // ITEM_SINGLE with one element, type constrained via predicate value + { + ANode node = PathAnalysis.analyzeAqlPathTypes( + "ITEM_STRUCTURE", null, null, AqlObjectPath.parse("item/value[value=10.0]/value"), null); + assertThat(node.candidateTypes).containsExactly(RmConstants.ITEM_SINGLE); + assertThat(node.attributes).containsOnlyKeys("item"); + + ANode item = node.attributes.get("item"); + assertThat(item.candidateTypes).containsExactly(RmConstants.ELEMENT); + + assertThat(item.attributes).containsOnlyKeys("value"); + ANode elementValue = item.attributes.get("value"); + assertThat(elementValue.candidateTypes).containsExactly(RmConstants.DV_SCALE, RmConstants.DV_ORDINAL); + } + + // ITEM_SINGLE with one element, type constrained via value + { + Set candidateTypes = PathAnalysis.getCandidateTypes(new LongPrimitive(10L)); + + ANode node = PathAnalysis.analyzeAqlPathTypes( + "ITEM_STRUCTURE", null, null, AqlObjectPath.parse("item/value/value"), candidateTypes); + assertThat(node.candidateTypes).containsExactly(RmConstants.ITEM_SINGLE); + assertThat(node.attributes).containsOnlyKeys("item"); + + ANode item = node.attributes.get("item"); + assertThat(item.candidateTypes).containsExactly(RmConstants.ELEMENT); + + assertThat(item.attributes).containsOnlyKeys("value"); + ANode elementValue = item.attributes.get("value"); + assertThat(elementValue.candidateTypes).containsExactly(RmConstants.DV_SCALE, RmConstants.DV_ORDINAL); + } + } + + private static List archetypeNodeIdCondition(String archetypeNodeId) { + if (archetypeNodeId == null) { + return null; + } + + return new ArrayList<>(List.of(new AndOperatorPredicate(List.of(new ComparisonOperatorPredicate( + AqlObjectPathUtil.ARCHETYPE_NODE_ID, + ComparisonOperatorPredicate.PredicateComparisonOperator.EQ, + new StringPrimitive(archetypeNodeId)))))); + } + + @Test + void createAttributeInfos() { + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "ITEM_STRUCTURE", + null, + null, + AqlObjectPath.parse("item/value/value"), + PathAnalysis.getCandidateTypes(new LongPrimitive(10L))); + + Map> attributeInfos = PathAnalysis.createAttributeInfos(rootNode); + assertThat(attributeInfos).hasSize(3); + assertThat(attributeInfos.values()).map(Map::size).allMatch(i -> i == 1); + } + + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "ITEM_STRUCTURE", + null, + null, + AqlObjectPath.parse("item[value/value>3]/value[value < 100]/value"), + PathAnalysis.getCandidateTypes(new LongPrimitive(10L))); + + Map> attributeInfos = PathAnalysis.createAttributeInfos(rootNode); + assertThat(attributeInfos).hasSize(3); + assertThat(attributeInfos.values()).map(Map::size).allMatch(i -> i == 1); + } + + { + ANode rootNode = PathAnalysis.analyzeAqlPathTypes( + "ITEM_STRUCTURE", + null, + null, + AqlObjectPath.parse("item[name/value='My Item']/value[value < 100]/value"), + PathAnalysis.getCandidateTypes(new LongPrimitive(10L))); + + Map> attributeInfos = PathAnalysis.createAttributeInfos(rootNode); + assertThat(attributeInfos).hasSize(4); + assertThat(attributeInfos.values()).flatExtracting(Map::values).hasSize(5); + } + } + + @Test + void testRmAttributeAlias() { + + List rmAttributes = RmAttributeAlias.VALUES.stream() + .map(RmAttributeAlias::attribute) + .filter(s -> !List.of( + // synthetic + "_magnitude", "details", "folders", "_type", "_index") + .contains(s)) + .collect(Collectors.toList()); + // EHR-only + rmAttributes.addAll(List.of("timeCreated", "ehrId", "ehrStatus", "compositions")); + + assertThat(PathAnalysis.AttributeInfos.attributeInfos.keySet()).containsAll(rmAttributes); + + assertThat(rmAttributes).containsAll(PathAnalysis.AttributeInfos.attributeInfos.keySet()); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathCohesionAnalysisTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathCohesionAnalysisTest.java new file mode 100644 index 000000000..32792304f --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathCohesionAnalysisTest.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.pathanalysis; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; +import org.assertj.core.api.AbstractStringAssert; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathCohesionAnalysis.PathCohesionTreeNode; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression; +import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath; +import org.ehrbase.openehr.util.TreeUtils; +import org.junit.jupiter.api.Test; + +class PathCohesionAnalysisTest { + + @Test + void simplePath() { + var map = byIdentifier(analyzePathCohesion("SELECT c/uid/value FROM COMPOSITION c")); + assertThat(map).containsOnlyKeys("c"); + + PathCohesionTreeNode n = map.get("c"); + + assertTreeMatches(n, """ + COMPOSITION + uid + value"""); + } + + @Test + void multiContains() { + + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT c/uid/value, ev/name/value + FROM EHR e contains COMPOSITION c CONTAINS ( (OBSERVATION o CONTAINS CLUSTER cl) OR EVALUATION ev ) + WHERE cl/name/value = 'Values' + ORDER BY ev/name/value + """)); + + assertThat(map).containsOnlyKeys("c", "ev", "cl"); + + PathCohesionTreeNode n = map.values().stream().iterator().next(); + + assertTreeMatches(map.get("c"), """ + COMPOSITION + uid + value"""); + + assertTreeMatches(map.get("ev"), """ + EVALUATION + name + value"""); + + assertTreeMatches(map.get("cl"), """ + CLUSTER + name + value"""); + } + + @Test + void simpleWithPredicates() { + var map = byIdentifier( + analyzePathCohesion( + """ + + SELECT + c/content[at0001]/data/events[at0002, 'Irrelevant']/items[name/value='All Items']/items[openEHR-EHR-CLUSTER.myCluster.v1]/items[openEHR-EHR-ELEMENT.myElement.v1, 'Data']/value + FROM COMPOSITION c""")); + assertThat(map).containsOnlyKeys("c"); + + PathCohesionTreeNode n = map.get("c"); + + assertTreeMatches( + n, + """ + COMPOSITION + content[at0001] + data + events[at0002] + items[name/value='All Items'] + items[openEHR-EHR-CLUSTER.myCluster.v1] + items[openEHR-EHR-ELEMENT.myElement.v1, 'Data'] + value"""); + } + + @Test + void containsPredicate() { + var map = byIdentifier(analyzePathCohesion("SELECT c/uid FROM COMPOSITION c[openEHR-EHR-CLUSTER.myComp.v1]")); + assertThat(map).containsOnlyKeys("c"); + + PathCohesionTreeNode n = map.get("c"); + + assertTreeMatches(n, """ + COMPOSITION[openEHR-EHR-CLUSTER.myComp.v1] + uid"""); + } + + @Test + void ignoreRootPredicate() { + var map = byIdentifier(analyzePathCohesion("SELECT c[openEHR-EHR-CLUSTER.myComp.v1]/uid FROM COMPOSITION c")); + assertThat(map).containsOnlyKeys("c"); + + PathCohesionTreeNode n = map.get("c"); + + // c[openEHR-EHR-CLUSTER.myComp.v1] is ignored, because it only actas as filter + assertTreeMatches(n, """ + COMPOSITION + uid"""); + } + + @Test + void notMergingRootPredicate() { + var map = byIdentifier(analyzePathCohesion( + "SELECT c[name/value='My Comp']/uid FROM COMPOSITION c[openEHR-EHR-CLUSTER.myComp.v1]")); + assertThat(map).containsOnlyKeys("c"); + + PathCohesionTreeNode n = map.get("c"); + + // c[name/value='My Comp'] is ignored, because it only acts as filter + assertTreeMatches(n, """ + COMPOSITION[openEHR-EHR-CLUSTER.myComp.v1] + uid"""); + } + + @Test + void simpleAttributes() { + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT + t/items[at0004]/name/value AS SystolicName, + t/items[at0004]/value/magnitude AS SystolicValue + FROM OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE t""")); + + assertTreeMatches( + map.get("t"), + """ + ITEM_TREE + items[at0004] + name + value + value + magnitude"""); + } + + @Test + void simpleNodeAttributes() { + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT + t/items[at0004]/name/value AS SystolicName, + t/items[at0004]/value/magnitude AS SystolicValue, + t/items[at0004]/value/units AS SystolicUnit, + t/items[at0005]/name/value AS DiastolicName, + t/items[at0005]/value/magnitude AS DiastolicValue + FROM OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE t""")); + + assertTreeMatches( + map.get("t"), + """ + ITEM_TREE + items[at0004] + name + value + value + magnitude + units + items[at0005] + name + value + value + magnitude"""); + } + + @Test + void baseAttributes() { + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT + t/items/name/value AS Name, + t/items/value/magnitude AS Value, + t/items[at0005]/name/value AS DiastolicName, + t/items[at0005]/value/magnitude AS DiastolicValue, + t/items[at0005]/value/units AS DiastolicUnit + FROM OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE t""")); + + assertTreeMatches( + map.get("t"), + """ + ITEM_TREE + items + name + value + value + magnitude + units"""); + } + + @Test + void archetypeAttributes() { + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT + t/items[openEHR-EHR-ELEMENT.blood_pressure.v2]/name/value AS Name, + t/items[openEHR-EHR-ELEMENT.blood_pressure.v2, 'Systolic']/value/magnitude AS SystolicValue, + t/items[at0005, 'Diastolic']/name/value AS DiastolicName, + t/items[at0005]/value/magnitude AS DiastolicValue, + t/items[at0005]/value/units AS DiastolicUnit + FROM OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE t""")); + + assertTreeMatches( + map.get("t"), + """ + ITEM_TREE + items[at0005] + name + value + value + magnitude + units + items[openEHR-EHR-ELEMENT.blood_pressure.v2] + name + value + value + magnitude"""); + } + + @Test + void nameAttributes() { + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT + t/items[name/value='Systolic']/name/value AS Name, + t/items[name/value='Systolic']/value/magnitude AS SystolicValue, + t/items[name/value='Diastolic']/name/value AS DiastolicName, + t/items[name/value='Diastolic']/value/magnitude AS DiastolicValue, + t/items[name/value='Diastolic']/value/units AS DiastolicUnit + FROM OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE t""")); + + assertTreeMatches( + map.get("t"), + """ + ITEM_TREE + items[name/value='Diastolic'] + name + value + value + magnitude + units + items[name/value='Systolic'] + name + value + value + magnitude"""); + } + + @Test + void mixedAttributes() { + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT + t/items[openEHR-EHR-ELEMENT.blood_pressure.v2]/name/value AS Name, + t/items[openEHR-EHR-ELEMENT.blood_pressure.v2, 'Systolic']/value/magnitude AS SystolicValue, + t/items[name/value='Diastolic']/name/value AS DiastolicName, + t/items[at0005]/value/magnitude AS DiastolicValue, + t/items[at0005]/value/units AS DiastolicUnit + FROM OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE t""")); + + assertTreeMatches( + map.get("t"), + """ + ITEM_TREE + items + name + value + value + magnitude + units"""); + } + + @Test + void irrelevantPredicates() { + var map = byIdentifier( + analyzePathCohesion( + """ + SELECT + t/items[archetype_node_id=at0004 and value/magnitude > 3 and name/value='Systolic']/name/value AS SystolicName, + t/items[at0004]/value/magnitude AS SystolicValue + FROM OBSERVATION[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] + CONTAINS ITEM_TREE t""")); + + assertTreeMatches( + map.get("t"), + """ + ITEM_TREE + items[at0004] + name + value + value + magnitude"""); + } + + private static Map analyzePathCohesion(String aqlStr) { + return PathCohesionAnalysis.analyzePathCohesion(AqlQuery.parse(aqlStr)); + } + + private static Map byIdentifier( + Map map) { + return map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getIdentifier(), Map.Entry::getValue)); + } + + private static String renderTree(PathCohesionTreeNode node) { + return TreeUtils.renderTree( + node, + Comparator.comparing(n -> new AqlObjectPath(n.getAttribute()).render()), + n -> new AqlObjectPath(n.getAttribute()).render()); + } + + private static AbstractStringAssert assertTreeMatches(PathCohesionTreeNode root, String expected) { + return assertThat(renderTree(root)).isEqualTo(expected); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImpTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImpTest.java new file mode 100644 index 000000000..7661df288 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImpTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.ehrbase.api.dto.AqlQueryRequest; +import org.ehrbase.api.exception.UnprocessableEntityException; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class AqlQueryServiceImpTest { + + @ParameterizedTest + @CsvSource( + textBlock = + """ + SELECT e/ehr_status AS s FROM EHR e=>SELECT s AS s FROM EHR e CONTAINS EHR_STATUS s + SELECT s/uid/value, e/ehr_status/subject/external_ref/id FROM EHR e CONTAINS COMPOSITION s WHERE e/ehr_status/is_modifiable = true=>SELECT s/uid/value, s1/subject/external_ref/id FROM EHR e CONTAINS (EHR_STATUS s1 AND COMPOSITION s) WHERE s1/is_modifiable = true + """, + delimiterString = "=>") + void resolveEhrStatus(String srcAql, String expectedAql) { + + AqlQuery aqlQuery = AqlQueryParser.parse(srcAql); + AqlQueryServiceImp.replaceEhrPaths(aqlQuery); + assertThat(aqlQuery.render()).isEqualTo(expectedAql.replaceAll(" +", " ")); + } + + @ParameterizedTest + @CsvSource( + textBlock = + """ + SELECT e/compositions AS c FROM EHR e=>SELECT c AS c FROM EHR e CONTAINS COMPOSITION c + SELECT c/uid/value, e/compositions/uid/value FROM EHR e CONTAINS COMPOSITION c WHERE e/compositions/archetype_details/template_id/value = 'tpl.v0'=>SELECT c/uid/value, c1/uid/value FROM EHR e CONTAINS (COMPOSITION c1 AND COMPOSITION c) WHERE c1/archetype_details/template_id/value = 'tpl.v0' + """, + delimiterString = "=>") + void resolveEhrCompositions(String srcAql, String expectedAql) { + + AqlQuery aqlQuery = AqlQueryParser.parse(srcAql); + AqlQueryServiceImp.replaceEhrPaths(aqlQuery); + assertThat(aqlQuery.render()).isEqualTo(expectedAql.replaceAll(" +", " ")); + } + + @ParameterizedTest + @CsvSource( + textBlock = + """ + 5||10||REJECT||||Query contains a LIMIT clause, fetch and offset parameters must not be used (with fetch precedence REJECT) + 5|20||40|REJECT||||Query parameter for offset provided, but no fetch parameter + 5|20||40|MIN_FETCH||||Query parameter for offset provided, but no fetch parameter + 5|||30|REJECT||||Query parameter for offset provided, but no fetch parameter + |||42|REJECT||||Query parameter for offset provided, but no fetch parameter + 20||||REJECT||19||Query LIMIT 20 exceeds maximum limit 19 + 20||||MIN_FETCH||19||Query LIMIT 20 exceeds maximum limit 19 + ||20||REJECT|||19|Fetch parameter 20 exceeds maximum fetch 19 + ||20||MIN_FETCH|||19|Fetch parameter 20 exceeds maximum fetch 19 + 20|5|30||MIN_FETCH||||Query contains a OFFSET clause, fetch parameter must not be used (with fetch precedence MIN_FETCH) + """, + delimiterString = "|") + void queryOffsetLimitRejected( + String aqlLimit, + String aqlOffset, + String paramLimit, + String paramOffset, + AqlQueryServiceImp.FetchPrecedence fetchPrecedence, + String defaultLimit, + String maxLimit, + String maxFetch, + String message) { + + assertThatThrownBy(() -> runQueryTest( + aqlLimit, + aqlOffset, + paramLimit, + paramOffset, + fetchPrecedence, + defaultLimit, + maxLimit, + maxFetch)) + .isInstanceOf(UnprocessableEntityException.class) + .hasMessage(message); + } + + @ParameterizedTest + @CsvSource( + textBlock = + """ + ||||REJECT||| + 5||||REJECT||| + 5|15|||REJECT||| + ||20||REJECT||| + ||20|25|REJECT||| + ||||REJECT|20|10|10 + 20|30|||REJECT|20|20|20 + ||20|50|REJECT|20|20|20 + 30||20|50|MIN_FETCH|30|30|20 + 10||20|50|MIN_FETCH|30|30|20 + """, + delimiterString = "|") + void queryOffsetLimitAccepted( + String aqlLimit, + String aqlOffset, + String paramLimit, + String paramOffset, + AqlQueryServiceImp.FetchPrecedence fetchPrecedence, + String defaultLimit, + String maxLimit, + String maxFetch) { + runQueryTest(aqlLimit, aqlOffset, paramLimit, paramOffset, fetchPrecedence, defaultLimit, maxLimit, maxFetch); + } + + private void runQueryTest( + String aqlLimit, + String aqlOffset, + String paramLimit, + String paramOffset, + AqlQueryServiceImp.FetchPrecedence fetchPrecedence, + String defaultLimit, + String maxLimit, + String maxFetch) { + // @format:off + String query = "SELECT s FROM EHR_STATUS s %s %s".formatted( + parseLong(aqlLimit).map(s -> "LIMIT " + s).orElse(""), + parseLong(aqlOffset).map(s -> "OFFSET " + s).orElse("") + ); + + AqlQueryServiceImp.buildAqlQuery( + new AqlQueryRequest( + query, + Map.of(), + parseLong(paramLimit).orElse(null), + Optional.ofNullable(paramOffset) + .filter(s -> !s.isEmpty()) + .map(Long::parseLong) + .orElse(null)), + fetchPrecedence, + parseLong(defaultLimit).orElse(null), + parseLong(maxLimit).orElse(null), + parseLong(maxFetch).orElse(null)); + // @format:on + } + + private static Optional parseLong(String longStr) { + return Optional.ofNullable(longStr).filter(StringUtils::isNotEmpty).map(Long::parseLong); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilderTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilderTest.java new file mode 100644 index 000000000..6d203ecdf --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilderTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +import org.ehrbase.openehr.aqlengine.asl.AqlSqlLayer; +import org.ehrbase.openehr.aqlengine.asl.AslGraph; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathCohesionAnalysis; +import org.ehrbase.openehr.aqlengine.pathanalysis.PathInfo; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.jooq.Record; +import org.jooq.SQLDialect; +import org.jooq.SelectQuery; +import org.jooq.impl.DefaultDSLContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +public class AqlSqlQueryBuilderTest { + + private final KnowledgeCacheService mockKnowledgeCacheService = mock(); + + @BeforeEach + void setUp() { + Mockito.reset(mockKnowledgeCacheService); + Mockito.when(mockKnowledgeCacheService.findUuidByTemplateId(ArgumentMatchers.anyString())) + .thenReturn(Optional.of(UUID.randomUUID())); + } + + @Disabled + @Test + void printSqlQuery() { + AqlQuery aqlQuery = AqlQueryParser.parse( + """ + SELECT + c/content, + c/content[at0001], + c/content[at0002], + c/uid/value, + c/context/other_context[at0004]/items[at0014]/value + FROM EHR e CONTAINS COMPOSITION c + WHERE e/ehr_id/value = 'e6fad8ba-fb4f-46a2-bf82-66edb43f142f' + """); + + System.out.println("/*"); + System.out.println(aqlQuery.render()); + System.out.println("*/"); + + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + KnowledgeCacheService kcs = mock(KnowledgeCacheService.class); + Mockito.when(kcs.findUuidByTemplateId(ArgumentMatchers.anyString())).thenReturn(Optional.of(UUID.randomUUID())); + + AqlSqlLayer aqlSqlLayer = new AqlSqlLayer(kcs, () -> "node"); + AslRootQuery aslQuery = aqlSqlLayer.buildAslRootQuery(queryWrapper); + + System.out.println("/*"); + System.out.println(AslGraph.createAslGraph(aslQuery)); + System.out.println("*/"); + System.out.println(); + + AqlSqlQueryBuilder sqlQueryBuilder = + new AqlSqlQueryBuilder(new DefaultDSLContext(SQLDialect.POSTGRES), kcs, Optional.empty()); + + SelectQuery sqlQuery = sqlQueryBuilder.buildSqlQuery(aslQuery); + System.out.println(sqlQuery); + } + + @ParameterizedTest + @ValueSource( + strings = { + """ + SELECT o/data/events/data/items/value/magnitude + FROM OBSERVATION o [openEHR-EHR-OBSERVATION.conformance_observation.v0] + WHERE o/data[at0001]/events[at0002]/data[at0003]/items[at0008]/value = 82.0 + """ + }) + void canBuildSqlQuery(String aql) { + + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + KnowledgeCacheService kcs = mock(KnowledgeCacheService.class); + Mockito.when(kcs.findUuidByTemplateId(ArgumentMatchers.anyString())).thenReturn(Optional.of(UUID.randomUUID())); + + AqlSqlLayer aqlSqlLayer = new AqlSqlLayer(kcs, () -> "node"); + AslRootQuery aslQuery = aqlSqlLayer.buildAslRootQuery(queryWrapper); + AqlSqlQueryBuilder sqlQueryBuilder = + new AqlSqlQueryBuilder(new DefaultDSLContext(SQLDialect.POSTGRES), kcs, Optional.empty()); + + assertDoesNotThrow(() -> sqlQueryBuilder.buildSqlQuery(aslQuery)); + } + + @Test + void testDataQuery() { + AqlQuery aqlQuery = AqlQueryParser.parse( + """ + SELECT + c/content, + c/content[at0001], + c/content[at0002], + c/uid/value, + c/context/other_context[at0004]/items[at0014]/value + FROM EHR e CONTAINS COMPOSITION c + WHERE e/ehr_id/value = 'e6fad8ba-fb4f-46a2-bf82-66edb43f142f' + """); + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + assertDoesNotThrow(() -> buildSqlQuery(queryWrapper)); + } + + @Test + void clusterWithDataMultiplicitySelectSingle() { + AqlQuery aqlQuery = AqlQueryParser.parse( + """ + SELECT + cluster/items[at0001]/value/data + FROM COMPOSITION CONTAINS CLUSTER cluster[openEHR-EHR-CLUSTER.media_file.v1] + """); + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + assertThat(queryWrapper.pathInfos()).hasSize(1); + PathInfo pathInfo = queryWrapper.pathInfos().entrySet().stream() + .findFirst() + .map(Map.Entry::getValue) + .orElseThrow(); + + PathCohesionAnalysis.PathCohesionTreeNode cohesionTreeRoot = pathInfo.getCohesionTreeRoot(); + assertThat(pathInfo.isMultipleValued(cohesionTreeRoot)).isFalse(); + + // Ensure generated query does not try to perform jsonb array selection + SelectQuery selectQuery = buildSqlQuery(queryWrapper); + assertThat(selectQuery.toString()).doesNotContain("select jsonb_array_elements("); + } + + private SelectQuery buildSqlQuery(AqlQueryWrapper queryWrapper) { + + AqlSqlLayer aqlSqlLayer = new AqlSqlLayer(mockKnowledgeCacheService, () -> "node"); + AslRootQuery aslQuery = aqlSqlLayer.buildAslRootQuery(queryWrapper); + AqlSqlQueryBuilder sqlQueryBuilder = new AqlSqlQueryBuilder( + new DefaultDSLContext(SQLDialect.POSTGRES), mockKnowledgeCacheService, Optional.empty()); + + return sqlQueryBuilder.buildSqlQuery(aslQuery); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/ConditionUtilsTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/ConditionUtilsTest.java new file mode 100644 index 000000000..2ec2d5ecb --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/ConditionUtilsTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.aqlengine.sql; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ConditionUtilsTest { + + @Test + void escapeAsJsonString() { + assertThat(ConditionUtils.escapeAsJsonString(null)).isNull(); + assertThat(ConditionUtils.escapeAsJsonString(" Test ")).isEqualTo("\" Test \""); + assertThat(ConditionUtils.escapeAsJsonString("")).isEqualTo("\"\""); + assertThat(ConditionUtils.escapeAsJsonString("\"Test\"")).isEqualTo("\"\\\"Test\\\"\""); + assertThat(ConditionUtils.escapeAsJsonString("\"Test\"")).isEqualTo("\"\\\"Test\\\"\""); + assertThat(ConditionUtils.escapeAsJsonString("C:\\temp\\")).isEqualTo("\"C:\\\\temp\\\\\""); + assertThat(ConditionUtils.escapeAsJsonString("Cluck Ol' Hen")).isEqualTo("\"Cluck Ol' Hen\""); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/util/TreeNodeTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/util/TreeNodeTest.java new file mode 100644 index 000000000..0308c70c6 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/util/TreeNodeTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.openehr.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.Test; + +class TreeNodeTest { + + private static final class MyNode extends TreeNode { + + public final int id; + + private MyNode(int id) { + this.id = id; + } + + public MyNode addChild(int id) { + return addChild(new MyNode(id)); + } + + public static MyNode root(int id) { + return new MyNode(id); + } + } + + @Test + void testSimpleTree() { + MyNode root = MyNode.root(0); + MyNode n1 = root.addChild(1); + MyNode n11 = n1.addChild(11); + MyNode n12 = n1.addChild(12); + MyNode n123 = n12.addChild(123); + MyNode n13 = n1.addChild(13); + + MyNode n2 = root.addChild(2); + MyNode n3 = root.addChild(3); + MyNode n3_1 = n3.addChild(3_1); + + assertTreeMatches( + root, + """ + 0 + 1 + 11 + 12 + 123 + 13 + 2 + 3 + 31"""); + } + + @Test + void testMoveChild() { + MyNode root = parseTree( + """ + 0 + 1 + 11 + 12 + 123 + 13 + 2 + 3 + 31 + """); + + var n1 = root.getChildren().get(0); + var n12 = n1.getChildren().get(1); + var n3 = root.getChildren().get(2); + + assertThatThrownBy(() -> n12.addChild(root)).isInstanceOf(IllegalArgumentException.class); + + n3.addChild(n12); + + assertTreeMatches( + root, + """ + 0 + 1 + 11 + 13 + 2 + 3 + 31 + 12 + 123"""); + } + + @Test + void testCreateTree() { + var tree = + """ + 0 + 1 + 11 + 12 + 123 + 13 + 2 + 3 + 31"""; + + assertThat(renderTree(parseTree(tree))).matches(tree); + } + + private static MyNode parseTree(String treeGraph) { + return TreeUtils.parseTree(treeGraph, s -> MyNode.root(Integer.parseInt(s))); + } + + private static String renderTree(MyNode node) { + return TreeUtils.renderTree(node, null, n -> Integer.toString(n.id)); + } + + private static AbstractStringAssert assertTreeMatches(MyNode root, String expected) { + return assertThat(renderTree(root)).isEqualTo(expected); + } +} diff --git a/base/db-setup/add_restricted_user.sql b/base/db-setup/add_restricted_user.sql deleted file mode 100644 index 118985964..000000000 --- a/base/db-setup/add_restricted_user.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE ROLE ehrbase_restricted WITH LOGIN PASSWORD 'ehrbase_restricted'; -GRANT ALL PRIVILEGES ON DATABASE ehrbase TO ehrbase_restricted; -GRANT USAGE ON SCHEMA ehr to ehrbase_restricted; -alter default privileges for user ehrbase in schema ehr grant select,insert,update,delete on tables to ehrbase_restricted; -GRANT select,insert,update,delete ON ALL TABLES IN SCHEMA ehr TO ehrbase_restricted; -GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA ext TO ehrbase_restricted; -REVOKE CREATE ON SCHEMA public from PUBLIC; \ No newline at end of file diff --git a/base/db-setup/createdb.sql b/base/db-setup/createdb.sql deleted file mode 100644 index ace959139..000000000 --- a/base/db-setup/createdb.sql +++ /dev/null @@ -1,240 +0,0 @@ --- This script needs to be run as database superuser in order to create the database --- These operations can not be run by Flyway as they require super user privileged --- and/or can not be installed inside a transaction. --- --- Extentions are installed in a separate schema called 'ext' --- --- For production servers these operations should be performed by a configuration --- management system. --- --- If the username, password or database is changed, they also need to be changed --- in the root pom.xml file. --- --- On *NIX run this using: --- --- sudo -u postgres psql < createdb.sql --- --- You only have to run this script once. --- --- THIS WILL NOT CREATE THE ENTIRE DATABASE! --- It only contains those operations which require superuser privileges. --- The actual database schema is managed by flyway. --- - - - -CREATE ROLE ehrbase WITH LOGIN PASSWORD 'ehrbase'; -CREATE ROLE ehrbase_restricted WITH LOGIN PASSWORD 'ehrbase_restricted'; -CREATE DATABASE ehrbase ENCODING 'UTF-8' TEMPLATE template0; -GRANT ALL PRIVILEGES ON DATABASE ehrbase TO ehrbase; -GRANT ALL PRIVILEGES ON DATABASE ehrbase TO ehrbase_restricted; - - - --- install the extensions -\c ehrbase -REVOKE CREATE ON SCHEMA public from PUBLIC; -CREATE SCHEMA IF NOT EXISTS ehr AUTHORIZATION ehrbase; -GRANT USAGE ON SCHEMA ehr to ehrbase_restricted; -alter default privileges for user ehrbase in schema ehr grant select,insert,update,delete on tables to ehrbase_restricted; -CREATE SCHEMA IF NOT EXISTS ext AUTHORIZATION ehrbase; -CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA ext; -CREATE EXTENSION IF NOT EXISTS "ltree" SCHEMA ext; - --- setup the search_patch so the extensions can be found -ALTER DATABASE ehrbase SET search_path TO "$user",public,ext; --- ensure INTERVAL is ISO8601 encoded -alter database ehrbase SET intervalstyle = 'iso_8601'; - -GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA ext TO ehrbase_restricted; - --- load the temporal_tables PLPG/SQL functions to emulate the coded extension --- original source: https://github.com/nearform/temporal_tables/blob/master/versioning_function.sql -CREATE OR REPLACE FUNCTION ext.versioning() - RETURNS TRIGGER AS $$ -DECLARE - sys_period text; - history_table text; - manipulate jsonb; - ignore_unchanged_values bool; - commonColumns text[]; - time_stamp_to_use timestamptz := current_timestamp; - range_lower timestamptz; - transaction_info txid_snapshot; - existing_range tstzrange; - holder record; - holder2 record; - pg_version integer; -BEGIN - -- version 0.4.0 - - IF TG_WHEN != 'BEFORE' OR TG_LEVEL != 'ROW' THEN - RAISE TRIGGER_PROTOCOL_VIOLATED USING - MESSAGE = 'function "versioning" must be fired BEFORE ROW'; - END IF; - - IF TG_OP != 'INSERT' AND TG_OP != 'UPDATE' AND TG_OP != 'DELETE' THEN - RAISE TRIGGER_PROTOCOL_VIOLATED USING - MESSAGE = 'function "versioning" must be fired for INSERT or UPDATE or DELETE'; - END IF; - - IF TG_NARGS not in (3,4) THEN - RAISE INVALID_PARAMETER_VALUE USING - MESSAGE = 'wrong number of parameters for function "versioning"', - HINT = 'expected 3 or 4 parameters but got ' || TG_NARGS; - END IF; - - sys_period := TG_ARGV[0]; - history_table := TG_ARGV[1]; - ignore_unchanged_values := TG_ARGV[3]; - - IF ignore_unchanged_values AND TG_OP = 'UPDATE' AND NEW IS NOT DISTINCT FROM OLD THEN - RETURN OLD; - END IF; - - -- check if sys_period exists on original table - SELECT atttypid, attndims INTO holder FROM pg_attribute WHERE attrelid = TG_RELID AND attname = sys_period AND NOT attisdropped; - IF NOT FOUND THEN - RAISE 'column "%" of relation "%" does not exist', sys_period, TG_TABLE_NAME USING - ERRCODE = 'undefined_column'; - END IF; - IF holder.atttypid != to_regtype('tstzrange') THEN - IF holder.attndims > 0 THEN - RAISE 'system period column "%" of relation "%" is not a range but an array', sys_period, TG_TABLE_NAME USING - ERRCODE = 'datatype_mismatch'; - END IF; - - SELECT rngsubtype INTO holder2 FROM pg_range WHERE rngtypid = holder.atttypid; - IF FOUND THEN - RAISE 'system period column "%" of relation "%" is not a range of timestamp with timezone but of type %', sys_period, TG_TABLE_NAME, format_type(holder2.rngsubtype, null) USING - ERRCODE = 'datatype_mismatch'; - END IF; - - RAISE 'system period column "%" of relation "%" is not a range but type %', sys_period, TG_TABLE_NAME, format_type(holder.atttypid, null) USING - ERRCODE = 'datatype_mismatch'; - END IF; - - IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN - -- Ignore rows already modified in this transaction - transaction_info := txid_current_snapshot(); - IF OLD.xmin::text >= (txid_snapshot_xmin(transaction_info) % (2^32)::bigint)::text - AND OLD.xmin::text <= (txid_snapshot_xmax(transaction_info) % (2^32)::bigint)::text THEN - IF TG_OP = 'DELETE' THEN - RETURN OLD; - END IF; - - RETURN NEW; - END IF; - - SELECT current_setting('server_version_num')::integer - INTO pg_version; - - -- to support postgres < 9.6 - IF pg_version < 90600 THEN - -- check if history table exits - IF to_regclass(history_table::cstring) IS NULL THEN - RAISE 'relation "%" does not exist', history_table; - END IF; - ELSE - IF to_regclass(history_table) IS NULL THEN - RAISE 'relation "%" does not exist', history_table; - END IF; - END IF; - - -- check if history table has sys_period - IF NOT EXISTS(SELECT * FROM pg_attribute WHERE attrelid = history_table::regclass AND attname = sys_period AND NOT attisdropped) THEN - RAISE 'history relation "%" does not contain system period column "%"', history_table, sys_period USING - HINT = 'history relation must contain system period column with the same name and data type as the versioned one'; - END IF; - - EXECUTE format('SELECT $1.%I', sys_period) USING OLD INTO existing_range; - - IF existing_range IS NULL THEN - RAISE 'system period column "%" of relation "%" must not be null', sys_period, TG_TABLE_NAME USING - ERRCODE = 'null_value_not_allowed'; - END IF; - - IF isempty(existing_range) OR NOT upper_inf(existing_range) THEN - RAISE 'system period column "%" of relation "%" contains invalid value', sys_period, TG_TABLE_NAME USING - ERRCODE = 'data_exception', - DETAIL = 'valid ranges must be non-empty and unbounded on the high side'; - END IF; - - IF TG_ARGV[2] = 'true' THEN - -- mitigate update conflicts - range_lower := lower(existing_range); - IF range_lower >= time_stamp_to_use THEN - time_stamp_to_use := range_lower + interval '1 microseconds'; - END IF; - END IF; - - WITH history AS - (SELECT attname, atttypid - FROM pg_attribute - WHERE attrelid = history_table::regclass - AND attnum > 0 - AND NOT attisdropped), - main AS - (SELECT attname, atttypid - FROM pg_attribute - WHERE attrelid = TG_RELID - AND attnum > 0 - AND NOT attisdropped) - SELECT - history.attname AS history_name, - main.attname AS main_name, - history.atttypid AS history_type, - main.atttypid AS main_type - INTO holder - FROM history - INNER JOIN main - ON history.attname = main.attname - WHERE - history.atttypid != main.atttypid; - - IF FOUND THEN - RAISE 'column "%" of relation "%" is of type % but column "%" of history relation "%" is of type %', - holder.main_name, TG_TABLE_NAME, format_type(holder.main_type, null), holder.history_name, history_table, format_type(holder.history_type, null) - USING ERRCODE = 'datatype_mismatch'; - END IF; - - WITH history AS - (SELECT attname - FROM pg_attribute - WHERE attrelid = history_table::regclass - AND attnum > 0 - AND NOT attisdropped), - main AS - (SELECT attname - FROM pg_attribute - WHERE attrelid = TG_RELID - AND attnum > 0 - AND NOT attisdropped) - SELECT array_agg(quote_ident(history.attname)) INTO commonColumns - FROM history - INNER JOIN main - ON history.attname = main.attname - AND history.attname != sys_period; - - EXECUTE ('INSERT INTO ' || - history_table || - '(' || - array_to_string(commonColumns , ',') || - ',' || - quote_ident(sys_period) || - ') VALUES ($1.' || - array_to_string(commonColumns, ',$1.') || - ',tstzrange($2, $3, ''[)''))') - USING OLD, range_lower, time_stamp_to_use; - END IF; - - IF TG_OP = 'UPDATE' OR TG_OP = 'INSERT' THEN - manipulate := jsonb_set('{}'::jsonb, ('{' || sys_period || '}')::text[], to_jsonb(tstzrange(time_stamp_to_use, null, '[)'))); - - RETURN jsonb_populate_record(NEW, manipulate); - END IF; - - RETURN OLD; -END; -$$ LANGUAGE plpgsql; - diff --git a/base/db-setup/fix-dv_coded_text-locatable-names.sql b/base/db-setup/fix-dv_coded_text-locatable-names.sql deleted file mode 100644 index 8d951e8aa..000000000 --- a/base/db-setup/fix-dv_coded_text-locatable-names.sql +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -ALTER TABLE ehr.entry - DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.entry_history - DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context - DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context_history - DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.status - DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.status_history - DISABLE ROW LEVEL SECURITY; - ---composition content -UPDATE ehr.entry -SET entry=REPLACE(REPLACE(entry::text,'"codeString":', '"code_string":'),'"terminologyId":', '"terminology_id":')::jsonb; - -UPDATE ehr.entry_history -SET entry=REPLACE(REPLACE(entry::text,'"codeString":', '"code_string":'),'"terminologyId":', '"terminology_id":')::jsonb -WHERE entry IS NOT NULL; - ---event_context.other_context -UPDATE ehr.event_context -SET other_context=REPLACE(REPLACE(other_context::text,'"codeString":', '"code_string":'),'"terminologyId":', '"terminology_id":')::jsonb -WHERE other_context IS NOT NULL; - -UPDATE ehr.event_context_history -SET other_context=REPLACE(REPLACE(other_context::text,'"codeString":', '"code_string":'),'"terminologyId":', '"terminology_id":')::jsonb -WHERE other_context IS NOT NULL; - ---status.other_details -UPDATE ehr.status -SET other_details=REPLACE(REPLACE(other_details::text,'"codeString":', '"code_string":'),'"terminologyId":', '"terminology_id":')::jsonb -WHERE other_details IS NOT NULL; - -UPDATE ehr.status_history -SET other_details=REPLACE(REPLACE(other_details::text,'"codeString":', '"code_string":'),'"terminologyId":', '"terminology_id":')::jsonb -WHERE other_details IS NOT NULL; - -ALTER TABLE ehr.entry - ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.entry_history - ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context - ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context_history - ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.status - ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.status_history - ENABLE ROW LEVEL SECURITY; diff --git a/base/db-setup/migrate_to_cloud_db_setup.sql b/base/db-setup/migrate_to_cloud_db_setup.sql deleted file mode 100644 index 1f0f6d64f..000000000 --- a/base/db-setup/migrate_to_cloud_db_setup.sql +++ /dev/null @@ -1,41 +0,0 @@ --- use this script to apply triggers for temporal tables (either using the versioning() function or --- temporal_tables extension --- NB. This script should be run after DB migration is done (mvn flyway:migrate) --- This is f.e. required when performing --- DROP EXTENSION 'temporal_tables' CASCADE - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR DELETE OR UPDATE - ON ehr.folder - FOR EACH ROW -EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.folder_history', 'true'); - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR DELETE OR UPDATE - ON ehr.folder_hierarchy - FOR EACH ROW -EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.folder_hierarchy_history', 'true'); - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR DELETE OR UPDATE - ON ehr.folder_items - FOR EACH ROW -EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.folder_items_history', 'true'); - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR UPDATE OR DELETE - ON ehr.status - FOR EACH ROW -EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.status_history', true); - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR UPDATE OR DELETE - ON ehr.composition - FOR EACH ROW -EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.composition_history', true); - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR UPDATE OR DELETE - ON ehr.event_context - FOR EACH ROW -EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.event_context_history', true); \ No newline at end of file diff --git a/base/pom.xml b/base/pom.xml deleted file mode 100644 index dee61b79a..000000000 --- a/base/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - 4.0.0 - - - org.ehrbase.openehr - server - 0.32.0 - - - base - - - ../ - - - - - org.flywaydb - flyway-core - - - diff --git a/base/src/main/java/db/migration/V3__terminology.java b/base/src/main/java/db/migration/V3__terminology.java deleted file mode 100644 index d00d7a007..000000000 --- a/base/src/main/java/db/migration/V3__terminology.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2019-2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package db.migration; - -import java.io.InputStream; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import org.flywaydb.core.api.migration.BaseJavaMigration; -import org.flywaydb.core.api.migration.Context; -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -/** - * This migration reads in the terminology.xml file and stores its contents into the database. - *

- * This replaces the org.ehrbase.dao.access.support.TerminologySetter class - * - * @author Christian Chevalley - * @author Stefan Spiska - * @since 1.0 - */ -@SuppressWarnings("java:S101") -public class V3__terminology extends BaseJavaMigration { - - @Override - public void migrate(Context context) throws Exception { - try (InputStream in = getClass().getClassLoader().getResourceAsStream("terminology.xml")) { - - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - - final DocumentBuilder documentBuilder = factory.newDocumentBuilder(); - final Document document = documentBuilder.parse(in); - - setTerritory(context.getConnection(), document); - setLanguage(context.getConnection(), document); - setConcept(context.getConnection(), document); - } - } - - private void setTerritory(final Connection connection, final Document document) throws SQLException { - try (final PreparedStatement statement = connection.prepareStatement( - "INSERT INTO ehr.territory(code, twoletter, threeletter, text) VALUES (?, ?, ?, ?)")) { - final NodeList territory = document.getElementsByTagName("Territory"); - for (int idx = 0; idx < territory.getLength(); idx++) { - final Node item = territory.item(idx); - final NamedNodeMap attributes = item.getAttributes(); - - final int code = - Integer.parseInt(attributes.getNamedItem("NumericCode").getNodeValue()); - - final String two = attributes.getNamedItem("TwoLetter").getNodeValue(); - final String three = attributes.getNamedItem("ThreeLetter").getNodeValue(); - final String text = attributes.getNamedItem("Text").getNodeValue(); - - statement.setInt(1, code); - statement.setString(2, two); - statement.setString(3, three); - statement.setString(4, text); - statement.executeUpdate(); - } - } - } - - private void setLanguage(final Connection connection, final Document document) throws SQLException { - try (final PreparedStatement statement = - connection.prepareStatement("INSERT INTO ehr.language(code, description) VALUES (?, ?)")) { - final NodeList language = document.getElementsByTagName("Language"); - for (int idx = 0; idx < language.getLength(); idx++) { - final Node item = language.item(idx); - final NamedNodeMap attributes = item.getAttributes(); - - final String code = attributes.getNamedItem("code").getNodeValue(); - final String text = attributes.getNamedItem("Description").getNodeValue(); - - statement.setString(1, code); - statement.setString(2, text); - statement.executeUpdate(); - } - } - } - - private void setConcept(final Connection connection, final Document document) throws SQLException { - try (final PreparedStatement statement = connection.prepareStatement( - "INSERT INTO ehr.concept(conceptId, language, description) VALUES (?, ?, ?)")) { - final NodeList concept = document.getElementsByTagName("Concept"); - for (int idx = 0; idx < concept.getLength(); idx++) { - final Node item = concept.item(idx); - final NamedNodeMap attributes = item.getAttributes(); - - final int code = - Integer.parseInt(attributes.getNamedItem("ConceptID").getNodeValue()); - - final String language = attributes.getNamedItem("Language").getNodeValue(); - final String text = attributes.getNamedItem("Rubric").getNodeValue(); - - statement.setInt(1, code); - statement.setString(2, language); - statement.setString(3, text); - statement.executeUpdate(); - } - } - } -} diff --git a/base/src/main/resources/Terminology.xsd b/base/src/main/resources/Terminology.xsd deleted file mode 100644 index 7a5ac0d3f..000000000 --- a/base/src/main/resources/Terminology.xsd +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V10__stored_query.sql b/base/src/main/resources/db/migration/V10__stored_query.sql deleted file mode 100644 index d1ddd7a54..000000000 --- a/base/src/main/resources/db/migration/V10__stored_query.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Create table ehr.stored_query - -CREATE TABLE ehr.stored_query -( - -- check for a syntactically valid reverse domain name (https://en.wikipedia.org/wiki/Reverse_domain_name_notation) - reverse_domain_name VARCHAR NOT NULL - CHECK (reverse_domain_name ~* '^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$'), - -- match a string consisting of alphanumeric or '-' or '_' - semantic_id VARCHAR NOT NULL - CHECK (semantic_id ~* '[\w|\-|_|]+'), - -- match a valid SEMVER (from https://semver.org/) - semver VARCHAR DEFAULT '0.0.0' - CHECK (semver ~* - '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' ), - query_text VARCHAR NOT NULL, - creation_date TIMESTAMP default CURRENT_TIMESTAMP, - type VARCHAR DEFAULT 'AQL', - CONSTRAINT pk_qualified_name PRIMARY KEY (reverse_domain_name, semantic_id, semver) -) diff --git a/base/src/main/resources/db/migration/V11__raw_json_encoding_new_format.sql b/base/src/main/resources/db/migration/V11__raw_json_encoding_new_format.sql deleted file mode 100644 index b7b395a8c..000000000 --- a/base/src/main/resources/db/migration/V11__raw_json_encoding_new_format.sql +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- archetyped.sql -CREATE OR REPLACE FUNCTION ehr.js_archetyped(TEXT, TEXT) - RETURNS JSON AS - $$ - DECLARE - archetype_id ALIAS FOR $1; - template_id ALIAS FOR $2; - BEGIN - RETURN - json_build_object( - '_type', 'ARCHETYPED', - 'archetype_id', - json_build_object( - '_type', 'ARCHETYPE_ID', - 'value', archetype_id - ), - template_id, - json_build_object( - '_type', 'TEMPLATE_ID', - 'value', template_id - ), - 'rm_version', '1.0.1' - ); - END - $$ -LANGUAGE plpgsql; - ---code_phrase.sql -CREATE OR REPLACE FUNCTION ehr.js_code_phrase(TEXT, TEXT) - RETURNS JSON AS -$$ -DECLARE - code_string ALIAS FOR $1; - terminology ALIAS FOR $2; -BEGIN - RETURN - json_build_object( - '_type', 'CODE_PHRASE', - 'terminology_id', - json_build_object( - '_type', 'TERMINOLOGY_ID', - 'value', terminology - ), - 'code_string', code_string - ); -END -$$ -LANGUAGE plpgsql; - ---context.sql -CREATE OR REPLACE FUNCTION ehr.js_context(UUID) - RETURNS JSON AS - $$ - DECLARE - context_id ALIAS FOR $1; - json_context_query TEXT; - json_context JSON; - v_start_time TIMESTAMP; - v_start_time_tzid TEXT; - v_end_time TIMESTAMP; - v_end_time_tzid TEXT; - v_facility UUID; - v_location TEXT; - v_other_context JSONB; - v_other_context_text TEXT; - v_setting UUID; - BEGIN - - IF (context_id IS NULL) - THEN - RETURN NULL; - END IF; - - -- build the query - SELECT - start_time, - start_time_tzid, - end_time, - end_time_tzid, - facility, - location, - other_context, - setting - FROM ehr.event_context - WHERE id = context_id - INTO v_start_time, v_start_time_tzid, v_end_time, v_end_time_tzid, v_facility, v_location, v_other_context, v_setting; - - json_context_query := ' SELECT json_build_object( - ''_type'', ''EVENT_CONTEXT'', - ''start_time'', ehr.js_dv_date_time(''' || v_start_time || ''',''' || - v_start_time_tzid || '''),'; - - IF (v_end_time IS NOT NULL) - THEN - json_context_query := - json_context_query || '''end_date'', ehr.js_dv_date_time(''' || v_end_time || ''',''' || v_end_time_tzid || - '''),'; - END IF; - - IF (v_location IS NOT NULL) - THEN - json_context_query := json_context_query || '''location'', ''' || v_location || ''','; - END IF; - - IF (v_facility IS NOT NULL) - THEN - json_context_query := json_context_query || '''health_care_facility'', ehr.js_party('''||v_facility||'''),'; - END IF; - - json_context_query := json_context_query || '''setting'',ehr.js_context_setting(''' || v_setting || '''))'; - - - -- IF (participation IS NOT NULL) THEN - -- json_context_query := json_context_query || '''participation'', participation,'; - -- END IF; - - IF (json_context_query IS NULL) - THEN - RETURN NULL; - END IF; - - EXECUTE json_context_query - INTO STRICT json_context; - - IF (v_other_context IS NOT NULL) - THEN - -- v_other_context_text := regexp_replace(v_other_context::TEXT, '''', '''''', 'g'); - json_context := jsonb_insert( - json_context::JSONB, - '{other_context}', v_other_context::JSONB->'/context/other_context[at0001]' - ); - END IF; - - RETURN json_context; - END - $$ -LANGUAGE plpgsql; - --- context_setting.sql -CREATE OR REPLACE FUNCTION ehr.js_context_setting(UUID) - RETURNS JSON AS - $$ - DECLARE - concept_id ALIAS FOR $1; - BEGIN - - IF (concept_id IS NULL) THEN - RETURN NULL; - END IF; - - RETURN ( - SELECT ehr.js_dv_coded_text(description, ehr.js_code_phrase(conceptid :: TEXT, 'openehr')) - FROM ehr.concept - WHERE id = concept_id AND language = 'en' - ); - END - $$ -LANGUAGE plpgsql; - --- dv_coded_text.sql -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text(TEXT, JSON) - RETURNS JSON AS -$$ -DECLARE - value_string ALIAS FOR $1; - code_phrase ALIAS FOR $2; -BEGIN - RETURN - json_build_object( - '_type', 'DV_CODED_TEXT', - 'value', value_string, - 'defining_code', code_phrase - ); -END -$$ -LANGUAGE plpgsql; - --- dv_date_time.sql -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMPTZ, TEXT) - RETURNS JSON AS - $$ - DECLARE - date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; - BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; - END IF; - - IF (time_zone IS NULL) - THEN - time_zone := 'UTC'; - END IF; - - RETURN - json_build_object( - '_type', 'DV_DATE_TIME', - 'value', ehr.iso_timestamp(date_time)||time_zone - ); - END - $$ -LANGUAGE plpgsql; - --- dv_text.sql -CREATE OR REPLACE FUNCTION ehr.js_dv_text(TEXT) - RETURNS JSON AS -$$ -DECLARE - value_string ALIAS FOR $1; -BEGIN - RETURN - json_build_object( - '_type', 'DV_TEXT', - 'value', value_string - ); -END -$$ -LANGUAGE plpgsql; - --- iso_timestamp.sql -create or replace function ehr.iso_timestamp(timestamp with time zone) - returns varchar as $$ -select substring(xmlelement(name x, $1)::varchar from 4 for 19) -$$ language sql immutable; - --- json_composition_pg10.sql --- CTE enforces 1-to-1 entry-composition relationship since multiple entries can be --- associated to one composition. This is not supported at this stage. -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID) - RETURNS JSON AS - $$ - DECLARE - composition_uuid ALIAS FOR $1; - BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_party(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', content - ) - ) - FROM composition_data - ); - END - $$ -LANGUAGE plpgsql; --- object_version_id.sql -CREATE OR REPLACE FUNCTION ehr.object_version_id(UUID, TEXT, INT) - RETURNS JSON AS -$$ -DECLARE - object_uuid ALIAS FOR $1; - object_host ALIAS FOR $2; - object_version ALIAS FOR $3; -BEGIN - RETURN - json_build_object( - '_type', 'OBJECT_VERSION_ID', - 'value', object_uuid::TEXT || '::' || object_host || '::' || object_version::TEXT - ); -END -$$ -LANGUAGE plpgsql; --- party.sql -CREATE OR REPLACE FUNCTION ehr.js_party(UUID) - RETURNS JSON AS -$$ -DECLARE - party_id ALIAS FOR $1; -BEGIN - RETURN ( - SELECT ehr.js_party_identified(name, - ehr.js_party_ref(party_ref_value, party_ref_scheme, party_ref_namespace, party_ref_type)) - FROM ehr.party_identified - WHERE id = party_id - ); -END -$$ -LANGUAGE plpgsql; --- party_identified.sql -CREATE OR REPLACE FUNCTION ehr.js_party_identified(TEXT, JSON) - RETURNS JSON AS -$$ -DECLARE - name_value ALIAS FOR $1; - external_ref ALIAS FOR $2; -BEGIN - IF (external_ref IS NOT NULL) THEN - RETURN - json_build_object( - '_type', 'PARTY_IDENTIFIED', - 'name', name_value, - 'external_ref', external_ref - ); - ELSE - RETURN - json_build_object( - '_type', 'PARTY_IDENTIFIED', - 'name', name_value - ); - END IF; -END -$$ -LANGUAGE plpgsql; --- party_ref.sql -CREATE OR REPLACE FUNCTION ehr.js_party_ref(TEXT, TEXT, TEXT, TEXT) - RETURNS JSON AS -$$ -DECLARE - id_value ALIAS FOR $1; - id_scheme ALIAS FOR $2; - namespace ALIAS FOR $3; - party_type ALIAS FOR $4; -BEGIN - - IF (id_value IS NULL AND id_scheme IS NULL AND namespace IS NULL AND party_type IS NULL) THEN - RETURN NULL; - ELSE - RETURN - json_build_object( - '_type', 'PARTY_REF', - 'id', - json_build_object( - '_type', 'GENERIC_ID', - 'value', id_value, - 'scheme', id_scheme - ), - 'namespace', namespace - ); - END IF; -END -$$ -LANGUAGE plpgsql; - --- ehr_status -CREATE OR REPLACE FUNCTION ehr.js_ehr_status(UUID) - RETURNS JSON AS -$$ -DECLARE - ehr_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH ehr_status_data AS ( - SELECT - status.other_details as other_details, - status.party as subject, - status.is_queryable as is_queryable, - status.is_modifiable as is_modifiable, - status.sys_transaction as time_created - FROM ehr.status - WHERE status.ehr_id = ehr_uuid - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR_STATUS', - 'subject', ehr.js_party(subject), - 'is_queryable', is_queryable, - 'is_modifiable', is_modifiable, - 'other_details', other_details - ) - ) - FROM ehr_status_data - ); -END -$$ -LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V12__datatypes_canonical_json.sql b/base/src/main/resources/db/migration/V12__datatypes_canonical_json.sql deleted file mode 100644 index 050290c94..000000000 --- a/base/src/main/resources/db/migration/V12__datatypes_canonical_json.sql +++ /dev/null @@ -1,341 +0,0 @@ --- convert a db dv_quantity into its canonical representation --- DB representation: --- {"units": "mg", "accuracy": 0.0, "magnitude": 636.3397240638733, "precision": 0, "accuracyPercent": false, "measurementService": {}} --- Canonical comes out with type - -CREATE OR REPLACE FUNCTION ehr.js_canonical_dv_quantity(magnitude FLOAT, units TEXT, _precision INT, accuracy_percent BOOLEAN) - RETURNS JSON AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'DV_QUANTITY', - 'magnitude', magnitude, - 'units', units, - 'precision', _precision, - 'accuracy_is_percent', accuracy_percent - ) - ); -END -$$ -LANGUAGE plpgsql; - ---fixed bad encoding -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMP, TEXT) - RETURNS JSON AS -$$ -DECLARE - date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; -BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; - END IF; - - IF (time_zone IS NULL) - THEN - time_zone := 'UTC'; - END IF; - - RETURN - json_build_object( - '_type', 'DV_DATE_TIME', - 'value', ehr.iso_timestamp(date_time)||time_zone - ); -END -$$ - LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.js_canonical_hier_object_id(ehr_id UUID) - RETURNS JSON AS -$$ -BEGIN - RETURN - json_build_object( - '_type', 'HIER_OBJECT_ID', - 'value', ehr_id - ); -END -$$ - LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.js_canonical_generic_id(scheme TEXT, id TEXT) - RETURNS json AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'GENERIC_ID', - 'value', id, - 'scheme', scheme - ) - ); -END -$$ -LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.js_canonical_party_ref(namespace TEXT, type TEXT, scheme TEXT, id TEXT) - RETURNS json AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', type, - 'id', ehr.js_canonical_generic_id(scheme, id) - ) - ); -END -$$ -LANGUAGE plpgsql; - - --- some minor fixes to support the 'new' canonical json format -CREATE OR REPLACE FUNCTION ehr.js_context(UUID) - RETURNS JSON AS -$$ -DECLARE - context_id ALIAS FOR $1; - json_context_query TEXT; - json_context JSON; - v_start_time TIMESTAMP; - v_start_time_tzid TEXT; - v_end_time TIMESTAMP; - v_end_time_tzid TEXT; - v_facility UUID; - v_location TEXT; - v_other_context JSON; - v_setting UUID; -BEGIN - - IF (context_id IS NULL) - THEN - RETURN NULL; - END IF; - - -- build the query - SELECT - start_time, - start_time_tzid, - end_time, - end_time_tzid, - facility, - location, - other_context, - setting - FROM ehr.event_context - WHERE id = context_id - INTO v_start_time, v_start_time_tzid, v_end_time, v_end_time_tzid, v_facility, v_location, v_other_context, v_setting; - - json_context_query := ' SELECT json_build_object( - ''_time'', ''EVENT_CONTEXT'', - ''start_time'', ehr.js_dv_date_time(''' || v_start_time || ''',''' || - v_start_time_tzid || '''),'; - - IF (v_end_time IS NOT NULL) - THEN - json_context_query := - json_context_query || '''end_date'', ehr.js_dv_date_time(''' || v_end_time || ''',''' || v_end_time_tzid || - '''),'; - END IF; - - IF (v_location IS NOT NULL) - THEN - json_context_query := json_context_query || '''location'', ''' || v_location || ''','; - END IF; - - IF (v_facility IS NOT NULL) - THEN - json_context_query := json_context_query || '''health_care_facility'', ehr.js_party('''||v_facility||'''),'; - END IF; - - json_context_query := json_context_query || '''setting'',ehr.js_context_setting(''' || v_setting || '''))'; - - - -- IF (participation IS NOT NULL) THEN - -- json_context_query := json_context_query || '''participation'', participation,'; - -- END IF; - - IF (json_context_query IS NULL) - THEN - RETURN NULL; - END IF; - - EXECUTE json_context_query - INTO STRICT json_context; - - IF (v_other_context IS NOT NULL) - THEN - json_context := jsonb_insert( - json_context::JSONB, - '{other_context}', v_other_context::JSONB - )::JSON; - END IF; - - RETURN json_context; -END -$$ -LANGUAGE plpgsql;; - --- return the composition name as extracted from the jsonb entry -CREATE OR REPLACE FUNCTION ehr.composition_name(content JSONB) - RETURNS TEXT AS -$$ -BEGIN - RETURN - (with root_json as ( - select jsonb_object_keys(content) root) - select trim(LEADING '''' FROM (trim(TRAILING ''']' FROM (regexp_split_to_array(root_json.root, 'and name/value='))[2]))) - from root_json - where root like '/composition%'); -END -$$ -LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.composition_uid(composition_uid UUID, server_id TEXT) - RETURNS TEXT AS -$$ -BEGIN - select "composition_join"."id"||'::'||server_id||'::'||1 - + COALESCE( - (select count(*) - from "ehr"."composition_history" - where "composition_join"."id" = "ehr"."composition_history"."id" - group by "ehr"."composition_history"."id") - , 0) as "uid/value" - from "ehr"."entry" - right outer join "ehr"."composition" as "composition_join" - on "composition_join"."id" = composition_uid; -END -$$ -LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_archetype_details(archetype_node_id TEXT, template_id TEXT) - RETURNS jsonb AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'ARCHETYPED', - 'archetype_id', jsonb_build_object ( - '_type', 'ARCHETYPE_ID', - 'value', archetype_node_id - ), - 'template_id', jsonb_build_object ( - '_type', 'TEMPLATE_ID', - 'value', template_id - ), - 'rm_version', '1.0.2' - ) - ); -END -$$ -LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_object_version_id(version_id TEXT) - RETURNS jsonb AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'OBJECT_VERSION_ID', - 'value', version_id - ) - ); -END -$$ - LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.composition_uid(composition_uid UUID, server_id TEXT) - RETURNS TEXT AS -$$ -BEGIN - RETURN (select "composition"."id"||'::'||server_id||'::'||1 - + COALESCE( - (select count(*) - from "ehr"."composition_history" - where "composition"."id" = composition_uid - group by "ehr"."composition_history"."id") - , 0) - from ehr.composition - where composition.id = composition_uid); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - LIMIT 1 - ), - entry_content AS ( - WITH values as ( - select composition_data.*, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((composition_data.content)::jsonb)))) #>> '{value}' as jsonvalue - from composition_data - where composition_data.composition_id = composition_uuid - ) - select values.* - FROM values - where jsonvalue like '{"/content%' - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text(ehr.composition_name(entry_content.content)), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_party(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', entry_content.jsonvalue::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; - - diff --git a/base/src/main/resources/db/migration/V13__template_table.sql b/base/src/main/resources/db/migration/V13__template_table.sql deleted file mode 100644 index bc34e430c..000000000 --- a/base/src/main/resources/db/migration/V13__template_table.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Create table ehr.template_store - -CREATE TABLE ehr.template_store -( - id uuid PRIMARY KEY, - template_id text unique, - content text, - sys_transaction TIMESTAMP NOT NULL -) diff --git a/base/src/main/resources/db/migration/V14__remove_old_template_tables.sql b/base/src/main/resources/db/migration/V14__remove_old_template_tables.sql deleted file mode 100644 index d018df092..000000000 --- a/base/src/main/resources/db/migration/V14__remove_old_template_tables.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Remove old template tables -DROP TABLE ehr.template CASCADE; -DROP TABLE ehr.template_heading_xref CASCADE; -DROP TABLE ehr.template_meta CASCADE; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V15__fix_plpgsql_js_functions.sql b/base/src/main/resources/db/migration/V15__fix_plpgsql_js_functions.sql deleted file mode 100644 index 79b4d46d7..000000000 --- a/base/src/main/resources/db/migration/V15__fix_plpgsql_js_functions.sql +++ /dev/null @@ -1,28 +0,0 @@ -DROP FUNCTION IF EXISTS ehr.js_dv_date_time(TIMESTAMP WITH TIME ZONE, TEXT); - -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMPTZ, TEXT) - RETURNS JSON AS -$$ -DECLARE - date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; -BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; - END IF; - - IF (time_zone IS NULL) - THEN - time_zone := 'UTC'; - END IF; - - RETURN - json_build_object( - '@class', 'DV_DATE_TIME', - 'value', timezone(time_zone, date_time::timestamp) - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V16__fix_plpgsql_js_functions_composition_uid.sql b/base/src/main/resources/db/migration/V16__fix_plpgsql_js_functions_composition_uid.sql deleted file mode 100644 index cc1650a49..000000000 --- a/base/src/main/resources/db/migration/V16__fix_plpgsql_js_functions_composition_uid.sql +++ /dev/null @@ -1,17 +0,0 @@ -create or replace function composition_uid(composition_uid uuid, server_id text) returns text - language plpgsql -as -$$ -BEGIN - RETURN (select "composition"."id" || '::' || server_id || '::' || 1 - + COALESCE( - (select count(*) - from "ehr"."composition_history" - where "composition_history"."id" = composition_uid) - , 0) - from ehr.composition - where composition.id = composition_uid); -END -$$; - --- alter function composition_uid(uuid, text) owner to ehrbase; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V17__make_directory_paths_unique.sql b/base/src/main/resources/db/migration/V17__make_directory_paths_unique.sql deleted file mode 100644 index a7cfa7765..000000000 --- a/base/src/main/resources/db/migration/V17__make_directory_paths_unique.sql +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH, Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- Table ehr.folder_hierarchy - --- Add constraint for unique parent-child pairs -ALTER TABLE ehr.folder_hierarchy -ADD CONSTRAINT UQ_FolderHierarchy_Parent_Child -UNIQUE(parent_folder, child_folder); diff --git a/base/src/main/resources/db/migration/V18__new_js_canonical_party_identified.sql b/base/src/main/resources/db/migration/V18__new_js_canonical_party_identified.sql deleted file mode 100644 index 5967df2ec..000000000 --- a/base/src/main/resources/db/migration/V18__new_js_canonical_party_identified.sql +++ /dev/null @@ -1,40 +0,0 @@ -CREATE OR REPLACE FUNCTION ehr.js_canonical_party_identified(refid UUID) - RETURNS json AS -$$ -BEGIN - RETURN ( - WITH party_values AS ( - SELECT - party_identified.name as name, - party_identified.party_ref_value as value, - party_identified.party_ref_scheme as scheme, - party_identified.party_ref_namespace as namespace, - party_identified.party_ref_type as type - FROM ehr.party_identified - WHERE party_identified.id = refid - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_IDENTIFIED', - 'name', party_values.name, - 'identifiers', - jsonb_build_array( - jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',refid - ) - ), - 'external_ref', jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', party_values.namespace, - 'type', party_values.type, - 'id', ehr.js_canonical_generic_id(party_values.scheme, party_values.value) - ) - ) - ) - FROM party_values - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V19__qualified_party.sql b/base/src/main/resources/db/migration/V19__qualified_party.sql deleted file mode 100644 index 99f3d3f33..000000000 --- a/base/src/main/resources/db/migration/V19__qualified_party.sql +++ /dev/null @@ -1,104 +0,0 @@ --- modification of table PARTY_IDENTIFIED with added field as required --- NB. we keep this table name as to avoid heavy refactoring of the code base referencing this table. --- create an enum type to qualify parties -create type ehr.party_type as enum('party_identified', 'party_self', 'party_related'); - --- UDT for CODE_PHRASE -create type ehr.code_phrase as ( - terminology_id_value text, - code_string text - ); - --- UDT for DV_CODED_TEXT -create type ehr.dv_coded_text as ( - value text, - defining_code ehr.code_phrase, - formatting text, - -- mappings: has forward usage of type! - language ehr.code_phrase, - encoding ehr.code_phrase - ); - --- add support of qualification (type) and relationship for party_type == party_related -ALTER TABLE ehr.party_identified - ADD COLUMN party_type ehr.party_type DEFAULT 'party_identified', - ADD COLUMN relationship ehr.dv_coded_text, - ADD CONSTRAINT party_related_check check ( - (CASE - WHEN party_type = 'party_related' THEN relationship IS NOT NULL - END) - ); - --- update corresponding canonical functions --- TODO: add proper support for PARTY_RELATED - -CREATE OR REPLACE FUNCTION ehr.json_party_identified(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; -BEGIN - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_IDENTIFIED', - 'name', name, - 'identifiers', - jsonb_build_array( - jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',refid - ) - ), - 'external_ref', jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', ref_type, - 'id', ehr.js_canonical_generic_id(scheme, id_value) - ) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - - -CREATE OR REPLACE FUNCTION ehr.js_canonical_party_identified(refid UUID) - RETURNS json AS -$$ -BEGIN - RETURN ( - WITH party_values AS ( - SELECT - party_identified.name as name, - party_identified.party_ref_value as value, - party_identified.party_ref_scheme as scheme, - party_identified.party_ref_namespace as namespace, - party_identified.party_ref_type as ref_type, - party_identified.party_type as party_type, - party_identified.relationship as relationship - FROM ehr.party_identified - WHERE party_identified.id = refid - ) - SELECT - CASE - WHEN party_values.party_type = 'party_identified' - THEN - ehr.json_party_identified(party_values.name, refid, party_values.namespace, party_values.ref_type, party_values.scheme, party_values.value)::json - - WHEN party_values.party_type = 'party_self' - THEN - jsonb_build_object ( - '_type', 'PARTY_SELF' - )::json - WHEN party_values.party_type = 'party_related' - THEN - ehr.json_party_identified(party_values.name, refid, party_values.namespace, party_values.ref_type, party_values.scheme, party_values.value)::json - END - FROM party_values - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V1__empty.sql b/base/src/main/resources/db/migration/V1__empty.sql deleted file mode 100644 index 44fc22a0e..000000000 --- a/base/src/main/resources/db/migration/V1__empty.sql +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- this migration is intentionally left blank diff --git a/base/src/main/resources/db/migration/V20__raw_json_encoding_fix.sql b/base/src/main/resources/db/migration/V20__raw_json_encoding_fix.sql deleted file mode 100644 index 443f10163..000000000 --- a/base/src/main/resources/db/migration/V20__raw_json_encoding_fix.sql +++ /dev/null @@ -1,111 +0,0 @@ -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMPTZ, TEXT) - RETURNS JSON AS -$$ -DECLARE - date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; -BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; - END IF; - - IF (time_zone IS NULL) - THEN - time_zone := 'UTC'; - END IF; - - RETURN - json_build_object( - '_type', 'DV_DATE_TIME', - 'value', timezone(time_zone, date_time::timestamp) - ); -END -$$ - LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION ehr.js_context(UUID) - RETURNS JSON AS -$$ -DECLARE - context_id ALIAS FOR $1; - json_context_query TEXT; - json_context JSON; - v_start_time TIMESTAMP; - v_start_time_tzid TEXT; - v_end_time TIMESTAMP; - v_end_time_tzid TEXT; - v_facility UUID; - v_location TEXT; - v_other_context JSON; - v_setting UUID; -BEGIN - - IF (context_id IS NULL) - THEN - RETURN NULL; - END IF; - - -- build the query - SELECT start_time, - start_time_tzid, - end_time, - end_time_tzid, - facility, - location, - other_context, - setting - FROM ehr.event_context - WHERE id = context_id - INTO v_start_time, v_start_time_tzid, v_end_time, v_end_time_tzid, v_facility, v_location, v_other_context, v_setting; - - json_context_query := ' SELECT json_build_object( - ''_type'', ''EVENT_CONTEXT'', - ''start_time'', ehr.js_dv_date_time(''' || v_start_time || ''',''' || - v_start_time_tzid || '''),'; - - IF (v_end_time IS NOT NULL) - THEN - json_context_query := - json_context_query || '''end_date'', ehr.js_dv_date_time(''' || v_end_time || ''',''' || - v_end_time_tzid || - '''),'; - END IF; - - IF (v_location IS NOT NULL) - THEN - json_context_query := json_context_query || '''location'', ''' || v_location || ''','; - END IF; - - IF (v_facility IS NOT NULL) - THEN - json_context_query := json_context_query || '''health_care_facility'', ehr.js_party(''' || v_facility || '''),'; - END IF; - - json_context_query := json_context_query || '''setting'',ehr.js_context_setting(''' || v_setting || '''))'; - - - -- IF (participation IS NOT NULL) THEN - -- json_context_query := json_context_query || '''participation'', participation,'; - -- END IF; - - IF (json_context_query IS NULL) - THEN - RETURN NULL; - END IF; - - EXECUTE json_context_query - INTO STRICT json_context; - - IF (v_other_context IS NOT NULL) - THEN - json_context := jsonb_insert( - json_context::JSONB, - '{other_context}', v_other_context::JSONB - )::JSON; - END IF; - - RETURN json_context; -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V21__ehr_status_name_archetype_node_id.sql b/base/src/main/resources/db/migration/V21__ehr_status_name_archetype_node_id.sql deleted file mode 100644 index c62cc848d..000000000 --- a/base/src/main/resources/db/migration/V21__ehr_status_name_archetype_node_id.sql +++ /dev/null @@ -1,46 +0,0 @@ --- Add support for attribute name and archetype_node_id in ehr_status - --- modify table ehr.status to add the missing attributes - -ALTER TABLE ehr.status - ADD COLUMN archetype_node_id TEXT NOT NULL DEFAULT 'openEHR-EHR-EHR_STATUS.generic.v1', - ADD COLUMN name ehr.dv_coded_text NOT NULL DEFAULT ('EHR Status',NULL,NULL,NULL,NULL)::ehr.dv_coded_text ; - --- modify function to return ehr_status canonical json to support the new attributes -CREATE OR REPLACE FUNCTION ehr.js_ehr_status(UUID) - RETURNS JSON AS -$$ -DECLARE - ehr_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH ehr_status_data AS ( - SELECT - status.other_details as other_details, - status.party as subject, - status.is_queryable as is_queryable, - status.is_modifiable as is_modifiable, - status.sys_transaction as time_created, - status.name as status_name, - status.archetype_node_id as archetype_node_id - FROM ehr.status - WHERE status.ehr_id = ehr_uuid - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR_STATUS', - 'archetype_node_id', archetype_node_id, - 'name', status_name, - 'subject', ehr.js_party(subject), - 'is_queryable', is_queryable, - 'is_modifiable', is_modifiable, - 'other_details', other_details - ) - ) - FROM ehr_status_data - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V22__js_concept.sql b/base/src/main/resources/db/migration/V22__js_concept.sql deleted file mode 100644 index 44d52a421..000000000 --- a/base/src/main/resources/db/migration/V22__js_concept.sql +++ /dev/null @@ -1,20 +0,0 @@ --- concept as json -CREATE OR REPLACE FUNCTION ehr.js_concept(UUID) - RETURNS JSON AS -$$ -DECLARE - concept_id ALIAS FOR $1; -BEGIN - - IF (concept_id IS NULL) THEN - RETURN NULL; - END IF; - - RETURN ( - SELECT ehr.js_dv_coded_text(description, ehr.js_code_phrase(conceptid :: TEXT, 'openehr')) - FROM ehr.concept - WHERE id = concept_id AND language = 'en' - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V23__ehr_status_history_fix.sql b/base/src/main/resources/db/migration/V23__ehr_status_history_fix.sql deleted file mode 100644 index 2e9665cf5..000000000 --- a/base/src/main/resources/db/migration/V23__ehr_status_history_fix.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE ehr.status_history - ADD COLUMN archetype_node_id TEXT NOT NULL DEFAULT 'openEHR-EHR-EHR_STATUS.generic.v1', - ADD COLUMN name ehr.dv_coded_text NOT NULL DEFAULT ('EHR Status',NULL,NULL,NULL,NULL)::ehr.dv_coded_text; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V24__typed_element_value.sql b/base/src/main/resources/db/migration/V24__typed_element_value.sql deleted file mode 100644 index 8b13a38a9..000000000 --- a/base/src/main/resources/db/migration/V24__typed_element_value.sql +++ /dev/null @@ -1,53 +0,0 @@ --- convert to lower snake case -CREATE OR REPLACE FUNCTION ehr.camel_to_snake(literal TEXT) - RETURNS TEXT AS -$$ -DECLARE - out_literal TEXT := ''; - literal_size INT; - char_at TEXT; - ndx INT; -BEGIN - literal_size := length(literal); - if (literal_size = 0) then - return literal; - end if; - ndx = 1; - while ndx <= literal_size loop - char_at := substr(literal, ndx , 1); - if (char_at ~ '[A-Z]') then - if (ndx > 1 AND substr(literal, ndx - 1, 1) <> '<') then - out_literal = out_literal || '_'; - end if; - out_literal = out_literal || lower(char_at); - else - out_literal = out_literal || char_at; - end if; - ndx := ndx + 1; - end loop; - out_literal := replace(replace(replace(out_literal, 'u_r_i', 'uri'), 'i_d', 'id'), 'i_s_m', 'ism'); - return out_literal; -END -$$ - LANGUAGE plpgsql; - --- add the _type into an element value block -CREATE OR REPLACE FUNCTION ehr.js_typed_element_value(JSONB) - RETURNS JSONB AS -$$ -DECLARE - element_value ALIAS FOR $1; -BEGIN - RETURN ( - SELECT - jsonb_strip_nulls( - (element_value #>>'{/value}')::jsonb || - jsonb_build_object( - '_type', - upper(ehr.camel_to_snake(element_value #>>'{/$CLASS$}')) - ) - ) - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V25__fix_ehr_status_party_self.sql b/base/src/main/resources/db/migration/V25__fix_ehr_status_party_self.sql deleted file mode 100644 index 672405a29..000000000 --- a/base/src/main/resources/db/migration/V25__fix_ehr_status_party_self.sql +++ /dev/null @@ -1,415 +0,0 @@ --- supported OBJECT_ID subtypes -create type ehr.party_ref_id_type as enum('generic_id', 'object_version_id', 'hier_object_id', 'undefined'); -alter table ehr.party_identified add column object_id_type ehr.party_ref_id_type default 'generic_id'; - --- caused an exception when inserting a UDT for relationship -alter table ehr.party_identified drop constraint party_related_check; - -CREATE OR REPLACE FUNCTION ehr.js_party_self_identified(TEXT, JSON) - RETURNS JSON AS -$$ -DECLARE - name_value ALIAS FOR $1; - external_ref ALIAS FOR $2; -BEGIN - IF (external_ref IS NOT NULL) THEN - RETURN - json_build_object( - '_type', 'PARTY_SELF', - 'external_ref', external_ref - ); - ELSE - RETURN - json_build_object( - '_type', 'PARTY_SELF' - ); - END IF; -END -$$ - LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.js_party_self(UUID) - RETURNS JSON AS -$$ -DECLARE - party_id ALIAS FOR $1; -BEGIN - RETURN ( - SELECT ehr.js_party_self_identified(name, - ehr.js_party_ref(party_ref_value, party_ref_scheme, party_ref_namespace, party_ref_type)) - FROM ehr.party_identified - WHERE id = party_id - ); -END -$$ - LANGUAGE plpgsql; - --- modify function to return ehr_status canonical json to support the new attributes -CREATE OR REPLACE FUNCTION ehr.js_ehr_status(UUID) - RETURNS JSON AS -$$ -DECLARE - ehr_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH ehr_status_data AS ( - SELECT - status.other_details as other_details, - status.party as subject, - status.is_queryable as is_queryable, - status.is_modifiable as is_modifiable, - status.sys_transaction as time_created, - status.name as status_name, - status.archetype_node_id as archetype_node_id - FROM ehr.status - WHERE status.ehr_id = ehr_uuid - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR_STATUS', - 'archetype_node_id', archetype_node_id, - 'name', status_name, - 'subject', ehr.js_party_self(subject), - 'is_queryable', is_queryable, - 'is_modifiable', is_modifiable, - 'other_details', other_details - ) - ) - FROM ehr_status_data - ); -END -$$ - LANGUAGE plpgsql; - --- =================== AQL fixes ====================================================== -CREATE OR REPLACE FUNCTION ehr.js_code_phrase(codephrase ehr.code_phrase) - RETURNS JSON AS -$$ -DECLARE - -BEGIN - RETURN - json_build_object( - '_type', 'CODE_PHRASE', - 'terminology_id', - json_build_object( - '_type', 'TERMINOLOGY_ID', - 'value', codephrase.terminology_id_value - ), - 'code_string', codephrase.code_string - ); -END -$$ - LANGUAGE plpgsql; - --- borrowed from TERM_MAPPING fix -CREATE OR REPLACE FUNCTION ehr.js_code_phrase(codephrase ehr.code_phrase) - RETURNS JSON AS -$$ -DECLARE - -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'CODE_PHRASE', - 'terminology_id', - json_build_object( - '_type', 'TERMINOLOGY_ID', - 'value', codephrase.terminology_id_value - ), - 'code_string', codephrase.code_string - ) - ); -END -$$ - LANGUAGE plpgsql; - --- borrowed from TERM_MAPPING fix -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text(dvcodedtext ehr.dv_coded_text) - RETURNS JSON AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'DV_CODED_TEXT', - 'value', dvcodedtext.value, - 'defining_code', ehr.js_code_phrase(dvcodedtext.defining_code), - 'formatting', dvcodedtext.formatting, - 'language', dvcodedtext.language, - 'encoding', dvcodedtext.encoding - ) - ); -END -$$ - LANGUAGE plpgsql; - --- OBJECT_ID -DROP FUNCTION ehr.js_canonical_generic_id(text,text); - -CREATE OR REPLACE FUNCTION ehr.js_canonical_generic_id(scheme TEXT, id_value TEXT) - RETURNS json AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'GENERIC_ID', - 'value', id_value, - 'scheme', scheme - ) - ); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_canonical_hier_object_id(id_value TEXT) - RETURNS json AS -$$ -BEGIN - RETURN - json_build_object( - '_type', 'HIER_OBJECT_ID', - 'value', id_value - ); -END -$$ - LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.js_canonical_object_version_id(id_value TEXT) - RETURNS json AS -$$ -BEGIN - RETURN - json_build_object( - '_type', 'OBJECT_VERSION_ID', - 'value', id_value - ); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_canonical_object_id(objectid_type ehr.party_ref_id_type, scheme TEXT, id_value TEXT) - RETURNS json AS -$$ -BEGIN - RETURN ( - SELECT - CASE - WHEN objectid_type = 'generic_id' - THEN - ehr.js_canonical_generic_id(scheme, id_value) - WHEN objectid_type = 'hier_object_id' - THEN - ehr.js_canonical_hier_object_id(id_value) - WHEN objectid_type = 'object_version_id' - THEN - ehr.js_canonical_object_version_id(id_value) - WHEN objectid_type = 'undefined' - THEN - NULL - END - ); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.json_party_self(refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; -BEGIN - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_SELF', - 'external_ref', jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', ref_type, - 'id', ehr.js_canonical_object_id(objectid_type, scheme, id_value) - ) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - - -CREATE OR REPLACE FUNCTION ehr.json_party_identified(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_IDENTIFIED', - 'name', name, - 'identifiers', arr, - 'external_ref', jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', ref_type, - 'id', ehr.js_canonical_object_id(objectid_type, scheme, id_value) - ) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - - - -CREATE OR REPLACE FUNCTION ehr.json_party_related(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type, relationship ehr.dv_coded_text) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_RELATED', - 'name', name, - 'identifiers', arr, - 'external_ref', jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', ref_type, - 'id', ehr.js_canonical_object_id(objectid_type, scheme, id_value) - ), - 'relationship', ehr.js_dv_coded_text(relationship) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - -CREATE OR REPLACE FUNCTION ehr.js_canonical_party_identified(refid UUID) - RETURNS json AS -$$ -BEGIN - RETURN ( - WITH party_values AS ( - SELECT - party_identified.name as name, - party_identified.party_ref_value as value, - party_identified.party_ref_scheme as scheme, - party_identified.party_ref_namespace as namespace, - party_identified.party_ref_type as ref_type, - party_identified.party_type as party_type, - party_identified.relationship as relationship, - party_identified.object_id_type as objectid_type - FROM ehr.party_identified - WHERE party_identified.id = refid - ) - SELECT - CASE - WHEN party_values.party_type = 'party_identified' - THEN - ehr.json_party_identified(party_values.name, refid, party_values.namespace, party_values.ref_type, party_values.scheme, party_values.value, party_values.objectid_type)::json - WHEN party_values.party_type = 'party_self' - THEN - ehr.json_party_self(refid, party_values.namespace, party_values.ref_type, party_values.scheme, party_values.value, party_values.objectid_type)::json - WHEN party_values.party_type = 'party_related' - THEN - ehr.json_party_related(party_values.name, refid, party_values.namespace, party_values.ref_type, party_values.scheme, party_values.value, party_values.objectid_type, relationship)::json - END - FROM party_values - ); -END -$$ - LANGUAGE plpgsql; - - --- fix to support composition with no content -CREATE OR REPLACE FUNCTION ehr.js_composition(composition_uuid UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text(ehr.composition_name(entry_content.content)), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_party(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V26__archetype_details.sql b/base/src/main/resources/db/migration/V26__archetype_details.sql deleted file mode 100644 index 0758de1aa..000000000 --- a/base/src/main/resources/db/migration/V26__archetype_details.sql +++ /dev/null @@ -1,83 +0,0 @@ -ALTER TABLE ehr.entry - ADD COLUMN rm_version TEXT NOT NULL DEFAULT '1.0.4'; - -CREATE OR REPLACE FUNCTION ehr.js_archetype_details(archetype_node_id TEXT, template_id TEXT, rm_version TEXT) - RETURNS jsonb AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'ARCHETYPED', - 'archetype_id', jsonb_build_object ( - '_type', 'ARCHETYPE_ID', - 'value', archetype_node_id - ), - 'template_id', jsonb_build_object ( - '_type', 'TEMPLATE_ID', - 'value', template_id - ), - 'rm_version', rm_version - ) - ); -END -$$ - LANGUAGE plpgsql; - -DROP FUNCTION ehr.js_composition(UUID, server_node_id TEXT); - -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text(ehr.composition_name(entry_content.content)), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_party(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V27__participations.sql b/base/src/main/resources/db/migration/V27__participations.sql deleted file mode 100644 index 441d5c223..000000000 --- a/base/src/main/resources/db/migration/V27__participations.sql +++ /dev/null @@ -1,198 +0,0 @@ --- extension for DvInterval -ALTER TABLE ehr.participation - RENAME COLUMN start_time TO time_lower; - -ALTER TABLE ehr.participation - RENAME COLUMN start_time_tzid TO time_lower_tz; - -ALTER TABLE ehr.participation - ADD COLUMN time_upper TIMESTAMP WITHOUT TIME ZONE; - -ALTER TABLE ehr.participation - ADD COLUMN time_upper_tz TEXT; - --- ditto for history -ALTER TABLE ehr.participation_history - RENAME COLUMN start_time TO time_lower; - -ALTER TABLE ehr.participation_history - RENAME COLUMN start_time_tzid TO time_lower_tz; - -ALTER TABLE ehr.participation_history - ADD COLUMN time_upper TIMESTAMP WITHOUT TIME ZONE; - -ALTER TABLE ehr.participation_history - ADD COLUMN time_upper_tz TEXT; - --- used to convert existing mode as a proper ehr.dv_coded_text type -CREATE OR REPLACE FUNCTION ehr.migrate_participation_mode(mode TEXT) - RETURNS ehr.dv_coded_text AS -$$ -BEGIN - RETURN ( - WITH dv_coded_text_attributes AS ( - WITH mode_split AS ( - select - regexp_split_to_array(( - (regexp_split_to_array(mode,'{|}'))[2]), ',') - as arr - ) - select - (regexp_split_to_array(arr[1],'='))[2] as code_string, - (regexp_split_to_array(arr[2],'='))[2] as terminology_id, - (regexp_split_to_array(arr[3],'='))[2] as value - from mode_split - ) - select (value, (terminology_id, code_string)::ehr.code_phrase,null,null,null)::ehr.dv_coded_text from dv_coded_text_attributes - ); -END -$$ - LANGUAGE plpgsql; - - -ALTER TABLE ehr.participation - ALTER COLUMN mode TYPE ehr.dv_coded_text - USING ehr.migrate_participation_mode(mode); - -ALTER TABLE ehr.participation_history - ALTER COLUMN mode TYPE ehr.dv_coded_text - USING ehr.migrate_participation_mode(mode); - --- -CREATE OR REPLACE FUNCTION ehr.js_code_phrase(codephrase ehr.code_phrase) - RETURNS JSON AS -$$ -BEGIN - RETURN - json_build_object( - '_type', 'CODE_PHRASE', - 'terminology_id', - json_build_object( - '_type', 'TERMINOLOGY_ID', - 'value', codephrase.terminology_id_value - ), - 'code_string', codephrase.code_string - ); -END -$$ - LANGUAGE plpgsql; - --- from PR #232 TERM_MAPPING -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text_inner(dvcodedtext ehr.dv_coded_text) - RETURNS JSON AS -$$ -BEGIN - RETURN - json_build_object( - '_type', 'DV_CODED_TEXT', - 'value', dvcodedtext.value, - 'defining_code', ehr.js_code_phrase(dvcodedtext.defining_code) - ); -END -$$ - LANGUAGE plpgsql; - - --- returns an array of canonical participations -CREATE OR REPLACE FUNCTION ehr.js_participations(event_context_id UUID) - RETURNS JSONB[] AS -$$ -DECLARE - item JSONB; - arr JSONB[]; - participation_data RECORD; -BEGIN - - FOR participation_data IN - SELECT - participation.performer as performer, - participation.function as function, - participation.mode as mode, - participation.time_lower, - participation.time_lower_tz, - participation.time_upper, - participation.time_upper_tz - FROM ehr.participation - WHERE event_context = event_context_id - LOOP - item := - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'PARTICIPATION', - 'function', participation_data.function, - 'performer', ehr.js_canonical_party_identified(participation_data.performer), - 'mode', ehr.js_dv_coded_text_inner(participation_data.mode), - 'time', (SELECT ( - CASE WHEN (participation_data.time_lower IS NOT NULL OR participation_data.time_upper IS NOT NULL) THEN - jsonb_build_object( - '_type', 'DV_INTERVAL', - 'lower', ehr.js_dv_date_time(participation_data.time_lower, participation_data.time_lower_tz), - 'upper', ehr.js_dv_date_time(participation_data.time_upper, participation_data.time_upper_tz) - ) - ELSE - NULL - END - ) - ) - ) - ); - arr := array_append(arr, item); - END LOOP; - RETURN arr; -END -$$ - LANGUAGE plpgsql; - --- returns a canonical representation of participations -CREATE OR REPLACE FUNCTION ehr.js_canonical_participations(context_id UUID) - RETURNS JSON AS -$$ -BEGIN - RETURN (SELECT jsonb_array_elements(jsonb_build_array(ehr.js_participations(context_id)))); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_context(UUID) - RETURNS JSON AS -$$ -DECLARE - context_id ALIAS FOR $1; -BEGIN - - IF (context_id IS NULL) - THEN - RETURN NULL; - ELSE - RETURN ( - WITH context_attributes AS ( - SELECT - start_time, - start_time_tzid, - end_time, - end_time_tzid, - facility, - location, - other_context, - setting - FROM ehr.event_context - WHERE id = context_id - ) - SELECT jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EVENT_CONTEXT', - 'start_time', ehr.js_dv_date_time(start_time, start_time_tzid), - 'end_time', ehr.js_dv_date_time(end_time, end_time_tzid), - 'location', location, - 'health_care_facility', ehr.js_party(facility), - 'setting', ehr.js_context_setting(setting), - 'other_context',other_context, - 'participations', ehr.js_participations(context_id) - ) - ) - FROM context_attributes - ); - END IF; -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V28__contains_refactoring.sql b/base/src/main/resources/db/migration/V28__contains_refactoring.sql deleted file mode 100644 index 65e94e9a8..000000000 --- a/base/src/main/resources/db/migration/V28__contains_refactoring.sql +++ /dev/null @@ -1,97 +0,0 @@ -ALTER TABLE ehr.entry - ADD COLUMN name ehr.dv_coded_text NOT NULL DEFAULT ('_DEFAULT_NAME',NULL,NULL,NULL,NULL)::ehr.dv_coded_text ; - - -CREATE OR REPLACE FUNCTION ehr.json_entry_migrate(jsonb_entry jsonb, OUT out_composition_name TEXT, OUT out_new_entry JSONB) -AS $$ -DECLARE - composition_name TEXT; - composition_idx int; - str_left text; - str_right text; - new_entry jsonb; -BEGIN - - composition_idx := strpos(jsonb_entry::text, 'and name/value='); - str_left := left(jsonb_entry::text, composition_idx - 2); - -- get the right part from 'and name/value' - str_right := substr(jsonb_entry::text, composition_idx+16); - composition_idx := strpos(str_right, ']'); -- skip the name - composition_name := left(str_right, composition_idx - 2); -- remove trailing single-quote, closing bracket - str_right := substr(str_right::text, composition_idx); - - new_entry := (str_left||str_right)::jsonb; - - SELECT composition_name, new_entry INTO out_composition_name, out_new_entry; - - -- RAISE NOTICE 'left : %, right: %', str_left, str_right; - -END - -$$ LANGUAGE plpgsql; - --- use f.e. select (ehr.json_entry_migrate(entry.entry)).out_composition_name, (ehr.json_entry_migrate(entry.entry)).out_new_entry from ehr.entry - --- Perform the migration -UPDATE ehr.entry -SET - entry = ((ehr.json_entry_migrate(entry.entry)).out_new_entry)::jsonb, - name = ((ehr.json_entry_migrate(entry.entry)).out_composition_name,NULL,NULL,NULL,NULL)::ehr.dv_coded_text; - --- fix to support composition name as a DvCodedText -CREATE OR REPLACE FUNCTION ehr.js_composition(composition_uuid UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.name as composition_name, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"%/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', entry_content.composition_name, - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_party(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; - --- table ehr.containment is no more used with the new containment resolution strategy -drop table ehr.containment; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V29__feeder_audit.sql b/base/src/main/resources/db/migration/V29__feeder_audit.sql deleted file mode 100644 index 9a02392ac..000000000 --- a/base/src/main/resources/db/migration/V29__feeder_audit.sql +++ /dev/null @@ -1,70 +0,0 @@ --- modify table ehr.composition to add the missing locatable attributes - -ALTER TABLE ehr.composition - ADD COLUMN feeder_audit JSONB, - ADD COLUMN links JSONB; - -ALTER TABLE ehr.composition_history - ADD COLUMN feeder_audit JSONB, - ADD COLUMN links JSONB; - --- add feeder_audit encoding -DROP FUNCTION ehr.js_composition(uuid,text); - -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - composition.feeder_audit as feeder_audit, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"%/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text(ehr.composition_name(entry_content.content)), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_party(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'feeder_audit', entry_content.feeder_audit, - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V2__ehr.sql b/base/src/main/resources/db/migration/V2__ehr.sql deleted file mode 100644 index 87dc6c0c5..000000000 --- a/base/src/main/resources/db/migration/V2__ehr.sql +++ /dev/null @@ -1,434 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- Generate EtherCIS tables for PostgreSQL 9.3 --- Author: Christian Chevalley --- --- --- --- alter table com.ethercis.ehr.consult_req_attachement --- drop constraint FKC199A3AAB95913AB; --- --- alter table com.ethercis.ehr.consult_req_attachement --- drop constraint FKC199A3AA4204581F; --- - --- 20170605 RVE: --- this file is a copy of jooq-pg/src/main/resources/ddls/pgsql_ehr.ddl with the following --- modififactions: --- - places extensions in the ext schema due to flyway restrictions --- - replaced all VARCHAR with TEXT (because our tzid is longer than what fits) - - --- storeComposition schema common; - - --- storeComposition common_im entities --- CREATE TABLE "system" --------------------------------------- -CREATE TABLE ehr.system ( - id UUid PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - description TEXT NOT NULL, - settings TEXT NOT NULL - ); - -COMMENT ON TABLE ehr.system IS 'system table for reference'; - -CREATE TABLE ehr.territory ( - code int unique primary key, -- numeric code - twoLetter char(2), - threeLetter char(3), - text TEXT NOT NULL - ); - -COMMENT ON TABLE ehr.territory IS 'ISO 3166-1 countries codeset'; - -CREATE TABLE ehr.language ( - code varchar(5) unique primary key, - description TEXT NOT NULL - ); - -COMMENT ON TABLE ehr.language IS 'ISO 639-1 language codeset'; - -CREATE TABLE ehr.terminology_provider ( - code TEXT unique primary key, - source TEXT NOT NULL, - authority TEXT - ); - -COMMENT ON TABLE ehr.terminology_provider IS 'openEHR identified terminology provider'; - -CREATE TABLE ehr.concept ( - id UUID unique primary key DEFAULT ext.uuid_generate_v4(), - conceptID int, - language varchar(5) references ehr.language(code), - description TEXT - ); - -COMMENT ON TABLE ehr.concept IS 'openEHR common concepts (e.g. terminology) used in the system'; - -create table ehr.party_identified ( - id UUID primary key DEFAULT ext.uuid_generate_v4(), - name TEXT, - -- optional party ref attributes - party_ref_value TEXT, - party_ref_scheme TEXT, - party_ref_namespace TEXT, - party_ref_type TEXT -); - --- list of identifiers for a party identified -create table ehr.identifier ( - id_value TEXT, -- identifier value - issuer TEXT, -- authority responsible for the identification (ex. France ASIP, LDAP server etc.) - assigner TEXT, -- assigner of the identifier - type_name TEXT, -- coding origin f.ex. INS-C, INS-A, NHS etc. - party UUID not null references ehr.party_identified(id) -- entity identified with this identifier (normally a person, patient etc.) -); - -COMMENT ON TABLE ehr.identifier IS 'specifies an identifier for a party identified, more than one identifier is possible'; - --- defines the modality for accessing an com.ethercisrcis.ehr -create table ehr.access ( - id UUID PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - settings TEXT, - scheme TEXT -- name of access control scheme - ); - -COMMENT ON TABLE ehr.access IS 'defines the modality for accessing an com.ethercis.ehr (security strategy implementation)'; --- - --- storeComposition ehr_im entities --- EHR Class emr_im 4.7.1 -create table ehr.ehr ( - id UUID NOT NULL PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - date_created timestamp default CURRENT_DATE, - date_created_tzid TEXT, -- timezone id: GMT+/-hh:mm - access UUID references ehr.access(id), -- access decision support (f.e. consent) --- status UUID references ehr.status(id), - system_id UUID references ehr.system(id), - directory UUID null -); -COMMENT ON TABLE ehr.ehr IS 'EHR itself'; - -create table ehr.status ( - id UUID NOT NULL PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - ehr_id UUID references ehr.ehr(id) ON DELETE CASCADE, - is_queryable boolean default true, - is_modifiable boolean default true, - party UUID not null references ehr.party_identified(id), -- subject (e.g. patient) - other_details JSONB, - sys_transaction TIMESTAMP NOT NULL, - sys_period tstzrange NOT NULL -- temporal table -); - --- change history table -create table ehr.status_history (like ehr.status); -CREATE INDEX ehr_status_history ON ehr.status_history USING BTREE (id); - -CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON ehr.status -FOR EACH ROW EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.status_history', true); - -COMMENT ON TABLE ehr.status IS 'specifies an ehr modality and ownership (patient)'; - ---storeComposition table ehr.event_participation ( --- context UUID references ehr.event_context(id), --- participation UUID references ehr.participation(id) ---); - --- COMMENT ON TABLE ehr.event_participation IS 'specifies parties participating in an event context'; - --- TODO make it compliant with openEHR common IM section 6 --- storeComposition table ehr.versioned ( --- id UUID PRIMARY KEY DEFAULT ext.uuid_generate_v4(),-- this is used by the object which this version def belongs to (composition etc.) --- object UUID not null, -- a versioning strategy identifier, can be generated by the RDBMS (PG) --- created timestamp default NOW() --- ); - --- COMMENT ON TABLE ehr.versioned IS 'used to reference a versioning system'; -create type ehr.contribution_data_type as enum('composition', 'folder', 'ehr', 'system', 'other'); -create type ehr.contribution_state as enum('complete', 'incomplete', 'deleted'); -create type ehr.contribution_change_type as enum('creation', 'amendment', 'modification', 'synthesis', 'Unknown', 'deleted'); - --- COMMON IM --- change control - -create table ehr.contribution ( - id UUID primary key DEFAULT ext.uuid_generate_v4(), - ehr_id UUID references ehr.ehr(id) ON DELETE CASCADE , - contribution_type ehr.contribution_data_type, -- specifies the type of data it contains - state ehr.contribution_state, -- current state in lifeCycleState - signature TEXT, - system_id UUID references ehr.system(id), - committer UUID references ehr.party_identified(id), - time_committed timestamp default NOW(), - time_committed_tzid TEXT, -- timezone id - change_type ehr.contribution_change_type, - description TEXT, -- is a DvCodedText - sys_transaction TIMESTAMP NOT NULL, - sys_period tstzrange NOT NULL -- temporal table -); - -create table ehr.attestation ( - id UUID PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - contribution_id UUID REFERENCES ehr.contribution(id) ON DELETE CASCADE , - proof TEXT, - reason TEXT, - is_pending BOOLEAN -); - -CREATE TABLE ehr.attested_view ( - id UUID PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - attestation_id UUID REFERENCES ehr.attestation(id) ON DELETE CASCADE, - -- DvMultimedia - alternate_text TEXT, - compression_algorithm TEXT, - media_type TEXT, - data BYTEA, - integrity_check BYTEA, - integrity_check_algorithm TEXT, - thumbnail UUID, -- another multimedia holding the thumbnail - uri TEXT -); - --- change history table -CREATE TABLE ehr.contribution_history (like ehr.contribution); -CREATE INDEX ehr_contribution_history ON ehr.contribution_history USING BTREE (id); - -COMMENT ON TABLE ehr.contribution IS 'Contribution table, compositions reference this table'; - -CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON ehr.contribution -FOR EACH ROW EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.contribution_history', true); - -create table ehr.composition ( - id UUID PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - ehr_id UUID references ehr.ehr(id) ON DELETE CASCADE, --- version UUID references ehr.versioned(id), - in_contribution UUID references ehr.contribution(id) ON DELETE CASCADE , -- in contribution version - active boolean default true, -- true if this composition is still valid (e.g. not replaced yet) - is_persistent boolean default true, - language varchar(5) references ehr.language(code), -- pointer to the language codeset. Indicates what broad category this Composition is belogs to, e.g. �persistent� - of longitudinal validity, �event�, �process� etc. - territory int references ehr.territory(code), -- Name of territory in which this Composition was written. Coded fromBinder openEHR �countries� code set, which is an expression of the ISO 3166 standard. - composer UUID not null references ehr.party_identified(id), -- points to the PARTY_PROXY who has created the composition - sys_transaction TIMESTAMP NOT NULL, - sys_period tstzrange NOT NULL -- temporal table - -- item UUID not null, -- point to the first section in composition -); - --- change history table -CREATE TABLE ehr.composition_history (like ehr.composition); -CREATE INDEX ehr_composition_history ON ehr.composition_history USING BTREE (id); - -COMMENT ON TABLE ehr.composition IS 'Composition table'; - -CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON ehr.composition -FOR EACH ROW EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.composition_history', true); - -create table ehr.event_context ( - id UUID primary key DEFAULT ext.uuid_generate_v4(), - composition_id UUID references ehr.composition(id) ON DELETE CASCADE , -- belong to composition - start_time TIMESTAMP not null, - start_time_tzid TEXT, -- time zone id: format GMT +/- hh:mm - end_time TIMESTAMP null, - end_time_tzid TEXT, -- time zone id: format GMT +/- hh:mm - facility UUID references ehr.party_identified(id), -- points to a party identified - location TEXT, - other_context JSONB, -- supports a cluster for other context definition - setting UUID references ehr.concept(id), -- codeset setting, see ehr_im section 5 --- program UUID references ehr.program(id), -- the program defined for this context (only in full ddl version) - sys_transaction TIMESTAMP NOT NULL, - sys_period tstzrange NOT NULL -- temporal table -); - --- change history table -create table ehr.event_context_history (like ehr.event_context); -CREATE INDEX ehr_event_context_history ON ehr.event_context_history USING BTREE (id); - -CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON ehr.event_context -FOR EACH ROW EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.event_context_history', true); - -COMMENT ON TABLE ehr.event_context IS 'defines the context of an event (time, who, where... see openEHR IM 5.2'; - -create table ehr.participation ( - id UUID primary key DEFAULT ext.uuid_generate_v4(), - event_context UUID NOT NULL REFERENCES ehr.event_context(id) ON DELETE CASCADE, - performer UUID references ehr.party_identified(id), - function TEXT, - mode TEXT, - start_time timestamp, - start_time_tzid TEXT, -- timezone id - sys_transaction TIMESTAMP NOT NULL, - sys_period tstzrange NOT NULL -- temporal table -); - --- change history table -create table ehr.participation_history (like ehr.participation); -CREATE INDEX ehr_participation_history ON ehr.participation_history USING BTREE (id); - -CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON ehr.participation -FOR EACH ROW EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.participation_history', true); - -COMMENT ON TABLE ehr.participation IS 'define a participating party for an event f.ex.'; - -create type ehr.entry_type as enum('section','care_entry', 'admin', 'proxy'); - -create table ehr.entry ( - id UUID primary key DEFAULT ext.uuid_generate_v4(), - composition_id UUID references ehr.composition(id) ON DELETE CASCADE , -- belong to composition - sequence int, -- ordering sequence number - item_type ehr.entry_type, - template_id TEXT, -- operational template to rebuild the structure entry - template_uuid UUID, -- optional, used with operational template for consistency - archetype_id TEXT, -- ROOT archetype id (not sure still in use...) - category UUID null references ehr.concept(id), -- used to specify the type of content: Evaluation, Instruction, Observation, Action with different languages - entry JSONB, -- actual content version dependent (9.3: json, 9.4: jsonb). entry is either CARE_ENTRY or ADMIN_ENTRY - sys_transaction TIMESTAMP NOT NULL, - sys_period tstzrange NOT NULL -- temporal table -); - --- change history table -CREATE TABLE ehr.entry_history (like ehr.entry); -CREATE INDEX ehr_entry_history ON ehr.entry_history USING BTREE (id); - -COMMENT ON TABLE ehr.entry IS 'this table hold the actual archetyped data values (fromBinder a template)'; - -CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON ehr.entry -FOR EACH ROW EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.entry_history', true); - --- CONTAINMENT "pseudo" index for CONTAINS clause resolution -create TABLE ehr.containment ( - comp_id UUID, - label ltree, - path text -); - --- CREATE INDEX label_idx ON ehr.containment USING BTREE(label); --- CREATE INDEX comp_id_idx ON ehr.containment USING BTREE(comp_id); - --- meta data -CREATE TABLE ehr.template_meta ( - template_id TEXT, - array_path TEXT[] -- list of paths containing an item list with list size > 1 -); - -CREATE INDEX template_meta_idx ON ehr.template_meta(template_id); - --- simple cross reference table to link INSTRUCTIONS with ACTIONS or other COMPOSITION -CREATE TABLE ehr.compo_xref ( - master_uuid UUID REFERENCES ehr.composition(id), - child_uuid UUID REFERENCES ehr.composition(id), - sys_transaction TIMESTAMP NOT NULL -); -CREATE INDEX ehr_compo_xref ON ehr.compo_xref USING BTREE (master_uuid); - --- log user sessions with logon id, session id and other parameters -CREATE TABLE ehr.session_log ( - id UUID primary key DEFAULT uuid_generate_v4(), - subject_id TEXT NOT NULL, - node_id TEXT, - session_id TEXT, - session_name TEXT, - session_time TIMESTAMP, - ip_address TEXT -); - --- views to abstract querying --- EHR STATUS -CREATE VIEW ehr.ehr_status AS - SELECT ehr.id, party.name AS name, - party.party_ref_value AS ref, - party.party_ref_scheme AS scheme, - party.party_ref_namespace AS namespace, - party.party_ref_type AS type, - identifier.* - FROM ehr.ehr ehr - INNER JOIN ehr.status status ON status.ehr_id = ehr.id - INNER JOIN ehr.party_identified party ON status.party = party.id - LEFT JOIN ehr.identifier identifier ON identifier.party = party.id; - --- Composition expanded view (include context and other meta_data -CREATE OR REPLACE VIEW ehr.comp_expand AS - SELECT - ehr.id AS ehr_id, - party.party_ref_value AS subject_externalref_id_value, - party.party_ref_namespace AS subject_externalref_id_namespace, - entry.composition_id, - entry.template_id, - entry.archetype_id, - entry.entry, - trim(LEADING '''' FROM (trim(TRAILING ''']' FROM - (regexp_split_to_array(json_object_keys(entry.entry :: JSON), 'and name/value=')) [2 - ]))) AS composition_name, - compo.language, - compo.territory, - ctx.start_time, - ctx.start_time_tzid, - ctx.end_time, - ctx.end_time_tzid, - ctx.other_context, - ctx.location AS ctx_location, - fclty.name AS facility_name, - fclty.party_ref_value AS facility_ref, - fclty.party_ref_scheme AS facility_scheme, - fclty.party_ref_namespace AS facility_namespace, - fclty.party_ref_type AS facility_type, - compr.name AS composer_name, - compr.party_ref_value AS composer_ref, - compr.party_ref_scheme AS composer_scheme, - compr.party_ref_namespace AS composer_namespace, - compr.party_ref_type AS composer_type - FROM ehr.entry - INNER JOIN ehr.composition compo ON compo.id = ehr.entry.composition_id - INNER JOIN ehr.event_context ctx ON ctx.composition_id = ehr.entry.composition_id - INNER JOIN ehr.party_identified compr ON compo.composer = compr.id - INNER JOIN ehr.ehr ehr ON ehr.id = compo.ehr_id - INNER JOIN ehr.status status ON status.ehr_id = ehr.id - LEFT JOIN ehr.party_identified party ON status.party = party.id - -- LEFT JOIN ehr.system sys ON ctx.setting = sys.id - LEFT JOIN ehr.party_identified fclty ON ctx.facility = fclty.id; - ---- CREATED INDEX -CREATE INDEX label_idx ON ehr.containment USING GIST (label); -CREATE INDEX comp_id_idx ON ehr.containment USING BTREE(comp_id); -CREATE INDEX gin_entry_path_idx ON ehr.entry USING gin(entry jsonb_path_ops); -CREATE INDEX template_entry_idx ON ehr.entry (template_id); - --- to optimize comp_expand, index FK's -CREATE INDEX entry_composition_id_idx ON ehr.entry (composition_id); -CREATE INDEX composition_composer_idx ON ehr.composition (composer); -CREATE INDEX composition_ehr_idx ON ehr.composition (ehr_id); -CREATE INDEX status_ehr_idx ON ehr.status (ehr_id); -CREATE INDEX status_party_idx ON ehr.status (party); -CREATE INDEX context_facility_idx ON ehr.event_context (facility); -CREATE INDEX context_composition_id_idx ON ehr.event_context (composition_id); -CREATE INDEX context_setting_idx ON ehr.event_context (setting); - - --- AUDIT TRAIL has been replaced by CONTRIBUTION --- create table ehr.audit_trail ( --- id UUID PRIMARY KEY DEFAULT ext.uuid_generate_v4(), --- composition_id UUID references ehr.composition(id), --- committer UUID not null references ehr.party_identified(id), -- contributor --- date_created TIMESTAMP, --- date_created_tzid VARCHAR(15), -- timezone id --- party UUID not null references ehr.party_identified(id), -- patient --- serial_version VARCHAR(50), --- system_id UUID references ehr.system(id) --- ); diff --git a/base/src/main/resources/db/migration/V30__iso_timestamp.sql b/base/src/main/resources/db/migration/V30__iso_timestamp.sql deleted file mode 100644 index deb420754..000000000 --- a/base/src/main/resources/db/migration/V30__iso_timestamp.sql +++ /dev/null @@ -1,30 +0,0 @@ --- this is to fix the timezone drift and provide the correct encoding -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMPTZ, TEXT) - RETURNS JSON AS -$$ -DECLARE - date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; - value_date_time TEXT; -BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; - END IF; - - IF (time_zone IS NULL) - THEN - time_zone := 'UTC'; - END IF; - - value_date_time := timezone('UTC', timezone('UTC',date_time::TIMESTAMPTZ) AT TIME ZONE time_zone); - - RETURN - json_build_object( - '_type', 'DV_DATE_TIME', - 'value', ehr.iso_timestamp(value_date_time::TIMESTAMPTZ)||time_zone - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V31__canonical_composer_encoding.sql b/base/src/main/resources/db/migration/V31__canonical_composer_encoding.sql deleted file mode 100644 index 81f78ae40..000000000 --- a/base/src/main/resources/db/migration/V31__canonical_composer_encoding.sql +++ /dev/null @@ -1,57 +0,0 @@ -DROP FUNCTION ehr.js_composition(uuid,text); - -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text(ehr.composition_name(entry_content.content)), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_canonical_party_identified(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V32__fix_js_canonical_json.sql b/base/src/main/resources/db/migration/V32__fix_js_canonical_json.sql deleted file mode 100644 index cdfe5bdaf..000000000 --- a/base/src/main/resources/db/migration/V32__fix_js_canonical_json.sql +++ /dev/null @@ -1,57 +0,0 @@ -DROP FUNCTION ehr.js_composition(uuid,text); - -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"%/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text(ehr.composition_name(entry_content.content)), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_canonical_party_identified(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V33__fix_AQL_time_retrieval.sql b/base/src/main/resources/db/migration/V33__fix_AQL_time_retrieval.sql deleted file mode 100644 index 20d50fc08..000000000 --- a/base/src/main/resources/db/migration/V33__fix_AQL_time_retrieval.sql +++ /dev/null @@ -1,9 +0,0 @@ --- ensures that date/time handling is the same for time with or without timezone -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(datetime TIMESTAMP, timezone TEXT) - RETURNS JSON AS -$$ -BEGIN - RETURN ehr.js_dv_date_time(datetime::TIMESTAMPTZ, timezone); -END -$$ -LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V34__admin_delete_templates.sql b/base/src/main/resources/db/migration/V34__admin_delete_templates.sql deleted file mode 100644 index 9a079a0f3..000000000 --- a/base/src/main/resources/db/migration/V34__admin_delete_templates.sql +++ /dev/null @@ -1,85 +0,0 @@ --- ==================================================================== --- Author: Axel Siebert (axel.siebert@vitagroup.ag) --- Create date: 2020-09-08 --- Description: Retrieves a list of compositions uuids that are using a template --- Parameters: --- @target_id - Template id to search entries for, e.g. 'RIPPLE - Conformance Test template' --- Returns: Table with compositions uuids that use the template --- ===================================================================== -DROP FUNCTION IF EXISTS ehr.admin_get_template_usage; - -CREATE OR REPLACE FUNCTION ehr.admin_get_template_usage(target_id TEXT) -RETURNS TABLE (composition_id uuid) -AS $$ -BEGIN - RETURN query - SELECT e.composition_id - FROM ehr.entry e - WHERE e.template_id = target_id - UNION ( - SELECT eh.composition_id - FROM ehr.entry_history eh - WHERE eh.template_id = target_id - ); -END;$$ LANGUAGE plpgsql; - --- ==================================================================== --- Author: Axel Siebert (axel.siebert@vitagroup.ag) --- Create date: 2020-09-09 --- Description: Replace content of a given template with the new one --- Parameters: --- @target_id - Template id to replace content for, e.g. 'RIPPLE - Conformance Test template' --- @update_content - New content to put into db --- Returns: New content of the template after the update --- ===================================================================== -DROP FUNCTION IF EXISTS ehr.admin_update_template; - -CREATE OR REPLACE FUNCTION ehr.admin_update_template(target_id TEXT, update_content TEXT) -RETURNS TEXT -AS $$ -DECLARE - new_template TEXT; -BEGIN - UPDATE ehr.template_store - SET "content" = update_content - WHERE template_id = target_id; - SELECT ts."content" INTO new_template - FROM ehr.template_store ts - WHERE ts.template_id = target_id; - RETURN new_template; -END;$$ LANGUAGE plpgsql; - --- ==================================================================== --- Author: Axel Siebert (axel.siebert@vitagroup.ag) --- Create date: 2020-08-24 --- Description: Removes all templates from database --- Returns: Number of deleted rows --- ===================================================================== -DROP FUNCTION IF EXISTS ehr.admin_delete_all_templates; - -CREATE OR REPLACE FUNCTION ehr.admin_delete_all_templates() -RETURNS integer -AS $$ -DECLARE - deleted integer; -BEGIN - SELECT count(*) INTO deleted FROM ehr.template_store; - DELETE FROM ehr.template_store ts WHERE ts.id NOTNULL; - RETURN deleted; -END;$$ LANGUAGE plpgsql; - --- ==================================================================== --- Author: Axel Siebert (axel.siebert@vitagroup.ag) --- Create date: 2020-09-11 --- Description: Removes one dedicated template from database --- Returns: Number of deleted rows --- ===================================================================== -DROP FUNCTION IF EXISTS ehr.admin_delete_template; - -CREATE OR REPLACE FUNCTION ehr.admin_delete_template(target_id TEXT) -RETURNS integer -AS $$ -BEGIN - DELETE FROM ehr.template_store ts WHERE ts.template_id = target_id; - RETURN 1; -END;$$ LANGUAGE plpgsql; diff --git a/base/src/main/resources/db/migration/V35__term_mapping.sql b/base/src/main/resources/db/migration/V35__term_mapping.sql deleted file mode 100644 index 77dd8c9e2..000000000 --- a/base/src/main/resources/db/migration/V35__term_mapping.sql +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - --- V35 --- this migration implements term mapping in DvCodedText at DB level - --- alter defined ehr.dv_coded_text --- This representation is used as a clean typed definition fails at read time (jooq 3.12) -alter type ehr.dv_coded_text - add attribute term_mapping TEXT[]; -- array : match, purpose: value, terminology, code, target: terminology, code, delimited by '|' - - --- prepare the table migration -CREATE OR REPLACE FUNCTION ehr.migrate_concept_to_dv_coded_text(concept_id UUID) - RETURNS ehr.dv_coded_text AS -$$ -BEGIN - RETURN ( - WITH concept_val AS ( - SELECT - conceptid as code, - description - FROM ehr.concept - WHERE concept.id = concept_id - LIMIT 1 - ) - select (concept_val.code, ('openehr', concept_val.description)::ehr.code_phrase, null, null, null, null)::ehr.dv_coded_text - from concept_val - ); -END -$$ - LANGUAGE plpgsql; - --- setting as DvCodedText -alter table ehr.event_context drop constraint event_context_setting_fkey; - -alter table ehr.event_context - alter column setting type ehr.dv_coded_text - using ehr.migrate_concept_to_dv_coded_text(setting); - -alter table ehr.event_context_history - alter column setting type ehr.dv_coded_text - using ehr.migrate_concept_to_dv_coded_text(setting); - -alter table ehr.entry drop constraint entry_category_fkey; - -alter table ehr.entry - alter column category type ehr.dv_coded_text - using ehr.migrate_concept_to_dv_coded_text(category); - -alter table ehr.entry_history - alter column category type ehr.dv_coded_text - using ehr.migrate_concept_to_dv_coded_text(category); - --- AQL service functions -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text_inner(value TEXT, terminology_id TEXT, code_string TEXT) - RETURNS JSON AS -$$ -BEGIN - RETURN - json_build_object( - '_type', 'DV_CODED_TEXT', - 'value', value, - 'defining_code', ehr.js_code_phrase(code_string, terminology_id) - ); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_term_mappings(mappings TEXT[]) - RETURNS JSONB[] AS -$$ -DECLARE - encoded TEXT; - attributes TEXT[]; - item JSONB; - arr JSONB[]; -BEGIN - - IF (mappings IS NULL) THEN - RETURN NULL; - end if; - - FOREACH encoded IN ARRAY mappings - LOOP - -- RAISE NOTICE 'encoded %',encoded; - -- the encoding is required since ARRAY in PG only support base types (e.g. no UDTs) - attributes := regexp_split_to_array(encoded, '\|'); - item := jsonb_build_object( - '_type', 'TERM_MAPPING', - 'match', attributes[1], - 'purpose', ehr.js_dv_coded_text_inner(attributes[2], attributes[3], attributes[4]), - 'target', ehr.js_code_phrase(attributes[6], attributes[5]) - ); - arr := array_append(arr, item); - END LOOP; - RETURN arr; -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text(dvcodedtext ehr.dv_coded_text) - RETURNS JSON AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'DV_CODED_TEXT', - 'value', dvcodedtext.value, - 'defining_code', dvcodedtext.defining_code, - 'formatting', dvcodedtext.formatting, - 'language', dvcodedtext.language, - 'encoding', dvcodedtext.encoding, - 'mappings', ehr.js_term_mappings(dvcodedtext.term_mapping) - ) - ); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_context(UUID) - RETURNS JSON AS -$$ -DECLARE - context_id ALIAS FOR $1; -BEGIN - - IF (context_id IS NULL) - THEN - RETURN NULL; - ELSE - RETURN ( - WITH context_attributes AS ( - SELECT - start_time, - start_time_tzid, - end_time, - end_time_tzid, - facility, - location, - other_context, - setting - FROM ehr.event_context - WHERE id = context_id - ) - SELECT jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EVENT_CONTEXT', - 'start_time', ehr.js_dv_date_time(start_time, start_time_tzid), - 'end_time', ehr.js_dv_date_time(end_time, end_time_tzid), - 'location', location, - 'health_care_facility', ehr.js_party(facility), - 'setting', ehr.js_dv_coded_text(setting), - 'other_context',other_context, - 'participations', ehr.js_participations(context_id) - ) - ) - FROM context_attributes - ); - END IF; -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - entry.entry as content, - entry.category as category, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"%/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text(ehr.composition_name(entry_content.content)), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_canonical_party_identified(composer), - 'category', ehr.js_dv_coded_text(category), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V36__participations_function.sql b/base/src/main/resources/db/migration/V36__participations_function.sql deleted file mode 100644 index 560970384..000000000 --- a/base/src/main/resources/db/migration/V36__participations_function.sql +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - --- used to convert existing mode as a proper ehr.dv_coded_text type -CREATE OR REPLACE FUNCTION ehr.migrate_participation_function(mode TEXT) - RETURNS ehr.dv_coded_text AS -$$ -BEGIN - RETURN (mode, NULL, NULL, NULL, NULL)::ehr.dv_coded_text; -END -$$ - LANGUAGE plpgsql; - -ALTER TABLE ehr.participation - ALTER COLUMN function TYPE ehr.dv_coded_text - USING ehr.migrate_participation_function(function); - -ALTER TABLE ehr.participation_history - ALTER COLUMN function TYPE ehr.dv_coded_text - USING ehr.migrate_participation_function(function); - --- returns an array of canonical participations -CREATE OR REPLACE FUNCTION ehr.js_participations(event_context_id UUID) - RETURNS JSONB[] AS -$$ -DECLARE - item JSONB; - arr JSONB[]; - participation_data RECORD; -BEGIN - - FOR participation_data IN - SELECT participation.performer as performer, - participation.function as function, - participation.mode as mode, - participation.time_lower, - participation.time_lower_tz, - participation.time_upper, - participation.time_upper_tz - FROM ehr.participation - WHERE event_context = event_context_id - LOOP - item := - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'PARTICIPATION', - 'function', (SELECT ( - CASE - WHEN ((participation_data.function).defining_code IS NOT NULL) - THEN - ehr.js_dv_coded_text_inner(participation_data.function) - ELSE - ehr.js_dv_text((participation_data.function).value) - END - ) - ), - 'performer', ehr.js_canonical_party_identified(participation_data.performer), - 'mode', ehr.js_dv_coded_text_inner(participation_data.mode), - 'time', (SELECT ( - CASE - WHEN (participation_data.time_lower IS NOT NULL OR - participation_data.time_upper IS NOT NULL) THEN - jsonb_build_object( - '_type', 'DV_INTERVAL', - 'lower', ehr.js_dv_date_time( - participation_data.time_lower, - participation_data.time_lower_tz), - 'upper', ehr.js_dv_date_time( - participation_data.time_upper, - participation_data.time_upper_tz) - ) - ELSE - NULL - END - ) - ) - ) - ); - arr := array_append(arr, item); - END LOOP; - RETURN arr; -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V37__canonical_ehr.sql b/base/src/main/resources/db/migration/V37__canonical_ehr.sql deleted file mode 100644 index 1ab7874c6..000000000 --- a/base/src/main/resources/db/migration/V37__canonical_ehr.sql +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - - -- use this mapping until audit details change_type is a dv_coded_text - CREATE OR REPLACE FUNCTION ehr.map_change_type_to_codestring(literal TEXT) - RETURNS TEXT AS - $$ - BEGIN - RETURN ( - CASE - WHEN literal = 'creation' THEN '249' - WHEN literal = 'amendment' THEN '250' - WHEN literal = 'modification' THEN '251' - WHEN literal = 'synthesis' THEN '252' - WHEN literal = 'deleted' THEN '523' - WHEN literal = 'attestation' THEN '666' - WHEN literal = 'unknown' THEN '253' - ELSE - '253' - END - ); - END - $$ -LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.js_audit_details(UUID) - RETURNS JSON AS -$$ -DECLARE - audit_details_uuid ALIAS FOR $1; -BEGIN - RETURN( - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'AUDIT_DETAILS', - 'system_id', ehr.js_canonical_hier_object_id(system.settings), - 'time_committed', ehr.js_dv_date_time(audit_details.time_committed, audit_details.time_committed_tzid), - 'change_type', ehr.js_dv_coded_text_inner((audit_details.change_type, - (('openehr', ehr.map_change_type_to_codestring(audit_details.change_type::TEXT))::ehr.code_phrase), - NULL, - NULL, - NULL, - NULL)::ehr.dv_coded_text), - 'description', ehr.js_dv_text(audit_details.description), - 'committer', ehr.js_canonical_party_identified(audit_details.committer) - ) - ) - FROM ehr.audit_details - JOIN ehr.system ON system.id = audit_details.system_id - WHERE audit_details.id = audit_details_uuid - ); -END -$$ -LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.folder_uid(folder_uid UUID, server_id TEXT) - RETURNS TEXT AS -$$ -BEGIN - RETURN ( - select "folder_join"."id" || '::' || server_id || '::' || 1 - + COALESCE( - (select count(*) - from "ehr"."folder_history" - where folder_uid = "ehr"."folder_history"."id" - group by "ehr"."folder_history"."id") - , 0) as "uid/value" - from "ehr"."entry" - right outer join "ehr"."folder" as "folder_join" - on "folder_join"."id" = folder_uid - limit 1 - ); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_contribution(UUID, TEXT) - RETURNS JSON AS -$$ -DECLARE - contribution_uuid ALIAS FOR $1; - server_id ALIAS FOR $2; -BEGIN - RETURN( - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'CONTRIBUTION', - 'uid', ehr.js_canonical_hier_object_id(contribution.id), - 'audit', ehr.js_audit_details(contribution.has_audit) - ) - ) - FROM ehr.contribution - WHERE contribution.id = contribution_uuid - ); -END -$$ - LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION ehr.js_folder(folder_uid UUID, server_id TEXT) -RETURNS JSONB AS -$$ -BEGIN - - IF (NOT EXISTS(SELECT * FROM ehr.folder WHERE id = folder_uid)) THEN - RETURN NULL; - end if; - - RETURN ( - WITH folder_data AS ( - SELECT name, sys_transaction - FROM ehr.folder - WHERE id = folder_uid - ) - SELECT - jsonb_build_object( - '_type', 'VERSIONED_FOLDER', - 'id', ehr.js_object_version_id(ehr.folder_uid(folder_uid, server_id)), - 'name', ehr.js_dv_text(folder_data.name), - 'time_created', ehr.js_dv_date_time(folder_data.sys_transaction, 'Z') - ) - FROM folder_data - ); -END -$$ -LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.js_ehr(UUID, TEXT) - RETURNS JSON AS -$$ -DECLARE - ehr_uuid ALIAS FOR $1; - server_id ALIAS FOR $2; - contribution_json_array JSONB[]; - contribution_details JSONB; - composition_version_json_array JSONB[]; - composition_in_ehr_id RECORD; - folder_version_json_array JSONB[]; - folder_in_ehr_id RECORD; -BEGIN - - FOR contribution_details IN (SELECT ehr.js_contribution(contribution.id, server_id) - FROM ehr.contribution - WHERE contribution.ehr_id = ehr_uuid AND contribution.contribution_type != 'ehr') - LOOP - contribution_json_array := array_append(contribution_json_array, contribution_details); - END LOOP; - - FOR composition_in_ehr_id IN (SELECT composition.id, composition.sys_transaction - FROM ehr.composition - WHERE composition.ehr_id = ehr_uuid) - LOOP - composition_version_json_array := array_append( - composition_version_json_array, - jsonb_build_object( - '_type', 'VERSIONED_COMPOSITION', - 'id', ehr.js_object_version_id(ehr.composition_uid(composition_in_ehr_id.id, server_id)), - 'time_created', ehr.js_dv_date_time(composition_in_ehr_id.sys_transaction, 'Z') - ) - ); - END LOOP; - - FOR folder_in_ehr_id IN (SELECT folder.id, folder.sys_transaction - FROM ehr.folder - JOIN ehr.contribution ON folder.in_contribution = contribution.id - WHERE contribution.ehr_id = ehr_uuid) - LOOP - folder_version_json_array := array_append( - folder_version_json_array, - ehr.js_folder(folder_in_ehr_id.id, server_id) - ); - END LOOP; - - RETURN ( - WITH ehr_data AS ( - SELECT - ehr.id as ehr_id, - ehr.date_created as date_created, - ehr.date_created_tzid as date_created_tz, - ehr.access as access, - system.settings as system_value, - ehr.directory as directory - FROM ehr.ehr - JOIN ehr.system ON system.id = ehr.system_id - WHERE ehr.id = ehr_uuid - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR', - 'ehr_id', ehr.js_canonical_hier_object_id(ehr_data.ehr_id), - 'system_id', ehr.js_canonical_hier_object_id(ehr_data.system_value), - 'ehr_status', ehr.js_ehr_status(ehr_data.ehr_id), - 'time_created', ehr.js_dv_date_time(ehr_data.date_created, ehr_data.date_created_tz), - 'contributions', contribution_json_array, - 'compositions', composition_version_json_array, - 'folders', folder_version_json_array, - 'directory', ehr.js_folder(directory, server_id) - ) - -- 'ehr_access' - -- 'tags' - ) - - FROM ehr_data - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V38__fix_contribution_history.sql b/base/src/main/resources/db/migration/V38__fix_contribution_history.sql deleted file mode 100644 index 4a8307601..000000000 --- a/base/src/main/resources/db/migration/V38__fix_contribution_history.sql +++ /dev/null @@ -1,8 +0,0 @@ --- fix bug 320: syncing history table with main table -ALTER TABLE ehr.contribution_history - DROP COLUMN system_id, - DROP COLUMN committer, - DROP COLUMN time_committed, - DROP COLUMN time_committed_tzid, -- timezone id - DROP COLUMN change_type, - DROP COLUMN description; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V39__admin_delete_ehr.sql b/base/src/main/resources/db/migration/V39__admin_delete_ehr.sql deleted file mode 100644 index 4358d2c8f..000000000 --- a/base/src/main/resources/db/migration/V39__admin_delete_ehr.sql +++ /dev/null @@ -1,640 +0,0 @@ --- ==================================================================== --- Author: Jake Smolka --- Create date: 2020-09-22 --- Description: Admin API functions for physically deletion of objects. --- ===================================================================== - - --- ==================================================================== --- Description: Function to delete an audit, incl. system, if not referenced somewhere else. --- Parameters: --- @audit_input - UUID of target audit --- Returns: '1' and linked party UUID --- Requires: Afterwards deletion of returned party. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_audit(audit_input UUID) - RETURNS TABLE (num integer, party UUID) AS $$ - BEGIN - RETURN QUERY WITH - -- extract info about referenced system, before deleting audit - scope_system(system_id) AS ( -- get current scope's system ID - SELECT ehr.audit_details.system_id - FROM ehr.audit_details - WHERE id = audit_input - GROUP BY ehr.audit_details.system_id - ), - -- extract info about referenced audits, before deleting audit - systems_audits(system_id, audit_id) AS ( -- get table of audits and their system ID - SELECT ehr.system.id AS system_id, ehr.audit_details.id AS audit_id - FROM ehr.audit_details, ehr.system - WHERE ehr.system.id = ehr.audit_details.system_id - ), - linked_party(id) AS ( -- remember linked party before deletion - SELECT ehr.audit_details.committer FROM ehr.audit_details WHERE id = audit_input - ), - delete_audit_details AS ( - DELETE FROM ehr.audit_details WHERE id = audit_input - ) - - SELECT 1, linked_party.id FROM linked_party; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'AUDIT_DETAILS', audit_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete an attestation. --- Parameters: --- @attest_ref_input - UUID of target attestation --- Returns: linked audit UUID --- Requires: Afterwards deletion of returned audit. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_attestation(attest_ref_input UUID) - RETURNS TABLE (audit UUID) AS $$ - DECLARE - results RECORD; - BEGIN - RETURN QUERY WITH - -- extract info about referenced audit - linked_audit(id) AS ( - SELECT ehr.attestation.has_audit - FROM ehr.attestation - WHERE reference = attest_ref_input - ), - -- extract info about attestation linked by the given reference - linked_attestation(id) AS ( - SELECT ehr.attestation.id - FROM ehr.attestation - WHERE reference = attest_ref_input - ), - -- extract info about attested_view linked by the extracted attestations - linked_attested_view(id) AS ( - SELECT ehr.attested_view.id - FROM ehr.attested_view - WHERE attestation_id IN (SELECT linked_attestation.id FROM linked_attestation) - ), - -- delete attested_view - delete_attested_view AS ( - DELETE FROM ehr.attested_view WHERE id IN (SELECT linked_attested_view.id FROM linked_attested_view) - ), - -- delete attestation - delete_attestation AS ( - DELETE FROM ehr.attestation WHERE id IN (SELECT linked_attestation.id FROM linked_attestation) - ), - -- delete attestation_ref - delete_attestation_ref AS ( - DELETE FROM ehr.attestation_ref WHERE id = attest_ref_input - ) - - SELECT linked_audit.id FROM linked_audit; - - -- logging: - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT ehr.attested_view.id - FROM ehr.attested_view - WHERE attestation_id IN ( - SELECT a.id FROM ( - SELECT ehr.attestation.id - FROM ehr.attestation - WHERE reference = attest_ref_input) - AS a - ) - ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'ATTESTED_VIEW', results.id, now(); - END LOOP; - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT ehr.attestation.id - FROM ehr.attestation - WHERE reference = attest_ref_input) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'ATTESTATION', results.id, now(); - END LOOP; - - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'ATTESTATION_REF', attest_ref_input, now(); - - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete event_contexts and participations for a composition and return their parties (event_context.facility and participation.performer). --- Parameters: --- @compo_id_input - UUID of super composition --- Returns: '1' and linked party UUID --- Requires: Afterwards deletion of returned party. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_event_context_for_compo(compo_id_input UUID) -RETURNS TABLE (num integer, party UUID) AS $$ - DECLARE - results RECORD; - BEGIN - RETURN QUERY WITH - linked_events(id) AS ( -- get linked EVENT_CONTEXT entities -- 0..1 - SELECT id, facility FROM ehr.event_context WHERE composition_id = compo_id_input - ), - linked_participations_for_events(id) AS ( -- get linked EVENT_CONTEXT entities -- for 0..1 events, each with * participations - SELECT id, performer FROM ehr.participation WHERE event_context IN (SELECT linked_events.id FROM linked_events) - ), - parties(id) AS ( - SELECT facility FROM linked_events - UNION - SELECT performer FROM linked_participations_for_events - ), - delete_participation AS ( - DELETE FROM ehr.participation WHERE ehr.participation.id IN (SELECT linked_participations_for_events.id FROM linked_participations_for_events) - ), - delete_event_contexts AS ( - DELETE FROM ehr.event_context WHERE ehr.event_context.id IN (SELECT linked_events.id FROM linked_events) - ) - SELECT 1, parties.id FROM parties; - - -- logging: - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT b.id FROM ( - SELECT id, performer FROM ehr.participation WHERE event_context IN (SELECT a.id FROM ( - SELECT id, facility FROM ehr.event_context WHERE composition_id = compo_id_input - ) AS a ) - ) AS b - ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'PARTICIPATION', results.id, now(); - END LOOP; - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT id, facility - FROM ehr.event_context - WHERE composition_id = compo_id_input) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'EVENT_CONTEXT', results.id, now(); - END LOOP; - - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete a single composition, incl. their entries. --- Parameters: --- @compo_id_input - UUID of target composition --- Returns: '1' and linked contribution, party, audit and attestation UUID --- Requires: Afterwards deletion of returned entities. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_composition(compo_id_input UUID) -RETURNS TABLE (num integer, contribution UUID, party UUID, audit UUID, attestation UUID) AS $$ - DECLARE - results RECORD; - BEGIN - RETURN QUERY WITH linked_entries(id) AS ( -- get linked ENTRY entities - SELECT id FROM ehr.entry WHERE composition_id = compo_id_input - ), - linked_misc(contrib, party, audit, attestation) AS ( - SELECT in_contribution, composer, has_audit, attestation_ref FROM ehr.composition WHERE id = compo_id_input - ), - delete_entries AS ( - DELETE FROM ehr.entry WHERE ehr.entry.id IN (SELECT linked_entries.id FROM linked_entries) - ), - -- delete composition itself - delete_composition AS ( - DELETE FROM ehr.composition WHERE id = compo_id_input - ) - SELECT 1, linked_misc.contrib, linked_misc.party, linked_misc.audit, linked_misc.attestation FROM linked_misc; - - -- logging: - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT a.id - FROM ( - SELECT id FROM ehr.entry WHERE composition_id = compo_id_input - ) AS a ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'ENTRY', results.id, now(); - END LOOP; - - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'COMPOSITION', compo_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete a single Composition's history, in entries' history. --- Necessary as own function, because the former transaction needs to be done to populate the *_history table. --- Parameters: --- @compo_input - UUID of target composition --- Returns: '1' --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_composition_history(compo_input UUID) -RETURNS TABLE (num integer) AS $$ - BEGIN - RETURN QUERY WITH - delete_entry_history AS ( - DELETE FROM ehr.entry_history WHERE composition_id = compo_input - ), - delete_composition_history AS ( - DELETE FROM ehr.composition_history WHERE id = compo_input - ) - - SELECT 1; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - Linked to COMPOSITION ID: % - Time: %', 'entry_history', compo_input, now(); - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'COMPOSITION_HISTORY', compo_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete a single Contribution. --- Parameters: --- @contrib_id_input - UUID of target contribution --- Returns: '1' and linked audit UUID --- Requires: Afterwards deletion of returned audit. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_contribution(contrib_id_input UUID) -RETURNS TABLE (num integer, audit UUID) AS $$ - BEGIN - RETURN QUERY WITH linked_misc(audit) AS ( - SELECT has_audit FROM ehr.contribution WHERE id = contrib_id_input - ), - -- delete contribution itself - delete_composition AS ( - DELETE FROM ehr.contribution WHERE id = contrib_id_input - ) - SELECT 1, linked_misc.audit FROM linked_misc; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'CONTRIBUTION', contrib_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete an EHR, incl. Status. --- Parameters: --- @ehr_id_input - UUID of target EHR --- Returns: '1' and linked audit, party UUID --- Requires: Afterwards deletion of returned audit and party. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_ehr(ehr_id_input UUID) -RETURNS TABLE (num integer, status_audit UUID, status_party UUID) AS $$ - BEGIN - RETURN QUERY WITH linked_status(has_audit) AS ( -- get linked STATUS parameters - SELECT has_audit FROM ehr.status WHERE ehr_id = ehr_id_input - ), - -- delete the EHR itself - delete_ehr AS ( - DELETE FROM ehr.ehr WHERE id = ehr_id_input - ), - linked_party(id) AS ( -- formally always one - SELECT party FROM ehr.status WHERE ehr_id = ehr_id_input - ), - -- Note: not handling the system referenced by EHR, because there is always at least one audit referencing it, too. See separated audit handling. - -- delete status - delete_status AS ( - DELETE FROM ehr.status WHERE ehr_id = ehr_id_input - ) - - SELECT 1, linked_status.has_audit, linked_party.id FROM linked_status, linked_party; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'EHR', ehr_id_input, now(); - RAISE NOTICE 'Admin deletion - Type: % - Linked to EHR ID: % - Time: %', 'STATUS', ehr_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete a single EHR's history, meaning the Status' and Contribution's history. --- Necessary as own function, because the former transaction needs to be done to populate the *_history table. --- Parameters: --- @ehr_id_input - UUID of target EHR --- Returns: '1' --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_ehr_history(ehr_id_input UUID) -RETURNS TABLE (num integer) AS $$ - BEGIN - RETURN QUERY WITH - -- delete status_history - delete_status_history AS ( - DELETE FROM ehr.status_history WHERE ehr_id = ehr_id_input - ), - delete_contribution_history AS ( - DELETE FROM ehr.contribution_history WHERE ehr_id = ehr_id_input - ) - - SELECT 1; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - Linked to EHR ID: % - Time: %', 'STATUS_HISTORY', ehr_id_input, now(); - RAISE NOTICE 'Admin deletion - Type: % - Linked to EHR ID: % - Time: %', 'CONTRIBUTION_HISTORY', ehr_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete a Status. --- Parameters: --- @status_id_input - UUID of target Status --- Returns: '1' and linked audit, party UUID --- Requires: Afterwards deletion of returned audit and party. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_status(status_id_input UUID) -RETURNS TABLE (num integer, status_audit UUID, status_party UUID) AS $$ - BEGIN - RETURN QUERY WITH - linked_misc(has_audit, party) AS ( -- formally always one - SELECT has_audit, party FROM ehr.status WHERE id = status_id_input - ), - -- delete status - delete_status AS ( - DELETE FROM ehr.status WHERE id = status_id_input - ) - - SELECT 1, linked_misc.has_audit, linked_misc.party FROM linked_misc; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'STATUS', status_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete a single Status' history. --- Necessary as own function, because the former transaction needs to be done to populate the *_history table. --- Parameters: --- @status_id_input - UUID of target status --- Returns: '1' --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_status_history(status_id_input UUID) -RETURNS TABLE (num integer) AS $$ - BEGIN - RETURN QUERY WITH - -- delete status_history - delete_status_history AS ( - DELETE FROM ehr.status_history WHERE id = status_id_input - ) - - SELECT 1; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - Linked to STATUS ID: % - Time: %', 'STATUS_HISTORY', status_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete a Folder. --- Parameters: --- @folder_id_input - UUID of target Folder --- Returns: linked contribution, folder children UUIDs --- Requires: Afterwards deletion of all _HISTORY tables with the returned contributions and children. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_folder(folder_id_input UUID) -RETURNS TABLE (contribution UUID, child UUID) AS $$ - DECLARE - results RECORD; - BEGIN - RETURN QUERY WITH - -- order to delete things: - -- all folders (scope's parent + children) itself from FOLDER, order shouldn't matter - -- all their FOLDER_HIERARCHY entries - -- all FOLDER_ITEMS matching FOLDER.IDs - -- all OBJECT_REF mentioned in FOLDER_ITEMS - -- all CONTRIBUTIONs (1..*) collected along the way above - -- AFTERWARDS and separate: deletion of all matching *_HISTORY table entries - - linked_children AS ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ), - linked_object_ref AS ( - SELECT DISTINCT object_ref_id FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT linked_children.child_folder FROM linked_children)) - ), - linked_contribution AS ( - SELECT DISTINCT in_contribution FROM ehr.folder WHERE (id = folder_id_input) OR (id IN (SELECT linked_children.child_folder FROM linked_children)) - - UNION - - SELECT DISTINCT in_contribution FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT linked_children.child_folder FROM linked_children)) - - UNION - - SELECT DISTINCT in_contribution FROM ehr.object_ref WHERE id IN (SELECT linked_object_ref.object_ref_id FROM linked_object_ref) - - UNION - - SELECT DISTINCT in_contribution FROM linked_children - ), - remove_directory AS ( - UPDATE ehr.ehr -- remove link to ehr and then actually delete the folder - SET directory = NULL - WHERE directory = folder_id_input - ), - delete_folders AS ( - DELETE FROM ehr.folder WHERE (id = folder_id_input) OR (id IN (SELECT linked_children.child_folder FROM linked_children)) - ), - delete_hierarchy AS ( - DELETE FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ), - delete_items AS ( - DELETE FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT linked_children.child_folder FROM linked_children)) - ), - delete_object_ref AS ( - DELETE FROM ehr.object_ref WHERE id IN (SELECT linked_object_ref.object_ref_id FROM linked_object_ref) - ) - -- returning contribution IDs to delete separate; same with children IDs, as *_HISTORY tables of ID sets ((original input folder + children), and obj_ref via their contribs) needs to be deleted separate, too. - SELECT DISTINCT linked_contribution.in_contribution, linked_children.child_folder FROM linked_contribution, linked_children; - - -- logging: - - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'FOLDER', folder_id_input, now(); - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT a.child_folder FROM ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ) AS a ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'FOLDER', results.child_folder, now(); - END LOOP; - - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_HIERARCHY', folder_id_input, now(); - - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_ITEMS', folder_id_input, now(); - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT a.child_folder FROM ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ) AS a ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_ITEMS', results.child_folder, now(); - END LOOP; - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT a.object_ref_id FROM ( - SELECT DISTINCT object_ref_id FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT b.child_folder FROM ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ) AS b )) - ) AS a - ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'OBJECT_REF', results.object_ref_id, now(); - END LOOP; - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete some Folder history. --- Necessary as own function, because the former transaction needs to be done to populate the *_history table. --- Parameters: --- @folder_id_input - UUID of target Folder --- Returns: '1' --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_folder_history(folder_id_input UUID) -RETURNS TABLE (num integer) AS $$ - BEGIN - RETURN QUERY WITH - delete_folders AS ( - DELETE FROM ehr.folder_history WHERE id = folder_id_input - ), - delete_hierarchy AS ( - DELETE FROM ehr.folder_hierarchy_history WHERE parent_folder = folder_id_input - ), - delete_items AS ( - DELETE FROM ehr.folder_items_history WHERE folder_id = folder_id_input - ) - - SELECT 1; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_HISTORY', folder_id_input, now(); - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_HIERARCHY_HISTORY', folder_id_input, now(); - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_ITEMS_HISTORY', folder_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to delete the rest of the Folder history. --- Necessary as own function, because the former transaction needs to be done to populate the *_history table. --- Parameters: --- @contribution_id_input - UUID of target contribution, to find the correct object_ref --- Returns: '1' --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_folder_obj_ref_history(contribution_id_input UUID) -RETURNS TABLE (num integer) AS $$ - BEGIN - RETURN QUERY WITH - delete_object_ref AS ( - DELETE FROM ehr.object_ref_history WHERE in_contribution = contribution_id_input - ) - - SELECT 1; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - Linked to CONTRIBUTION ID: % - Time: %', 'OBJECT_REF_HISTORY', contribution_id_input, now(); - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to get linked Contributions for an EHR. --- Parameters: --- @ehr_id_input - UUID of target EHR --- Returns: Linked contributions and audits UUIDs --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_get_linked_contributions(ehr_id_input UUID) -RETURNS TABLE (contribution UUID, audit UUID) AS $$ - BEGIN - RETURN QUERY WITH - linked_contrib(id, audit) AS ( -- get linked CONTRIBUTION parameters - SELECT id, has_audit FROM ehr.contribution WHERE ehr_id = ehr_id_input - ) - - SELECT * FROM linked_contrib; - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to get linked Compositions for an EHR. --- Parameters: --- @ehr_id_input - UUID of target EHR --- Returns: Linked compositions UUIDs --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_get_linked_compositions(ehr_id_input UUID) -RETURNS TABLE (composition UUID ) AS $$ - BEGIN - RETURN QUERY WITH - linked_compo(id) AS ( -- get linked CONTRIBUTION parameters - SELECT id FROM ehr.composition WHERE ehr_id = ehr_id_input - ) - - SELECT * FROM linked_compo; - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to get linked Compositions for a Contribution. --- Parameters: --- @contrib_id_input - UUID of target Contribution --- Returns: Linked compositions UUIDs --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_get_linked_compositions_for_contrib(contrib_id_input UUID) -RETURNS TABLE (composition UUID ) AS $$ - BEGIN - RETURN QUERY WITH - linked_compo(id) AS ( -- get linked CONTRIBUTION parameters - SELECT id FROM ehr.composition WHERE in_contribution = contrib_id_input - ) - - SELECT * FROM linked_compo; - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - - --- ==================================================================== --- Description: Function to get linked Status for a Contribution. --- Parameters: --- @contrib_id_input - UUID of target Contribution --- Returns: Linked status UUIDs --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_get_linked_status_for_contrib(contrib_id_input UUID) -RETURNS TABLE (status UUID ) AS $$ - BEGIN - RETURN QUERY WITH - linked_status(id) AS ( -- get linked CONTRIBUTION parameters - SELECT id FROM ehr.status WHERE in_contribution = contrib_id_input - ) - - SELECT * FROM linked_status; - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V40__get_system_version_function.sql b/base/src/main/resources/db/migration/V40__get_system_version_function.sql deleted file mode 100644 index 790c08114..000000000 --- a/base/src/main/resources/db/migration/V40__get_system_version_function.sql +++ /dev/null @@ -1,18 +0,0 @@ --- ==================================================================== --- Author: Axel Siebert (axel.siebert@vitagroup.ag) --- Create date: 2020-11-24 --- Description: Retrieves all information on running db system including environment os by running VERSION() function. --- --- Returns: Version string of running db server including os information --- ===================================================================== -DROP FUNCTION IF EXISTS ehr.get_system_version; - -CREATE OR REPLACE FUNCTION ehr.get_system_version() -RETURNS TEXT -AS $$ -DECLARE - version_string TEXT; -BEGIN - SELECT VERSION() INTO version_string; - RETURN version_string; -END; $$ LANGUAGE plpgsql; diff --git a/base/src/main/resources/db/migration/V41__fct_wrappers.sql b/base/src/main/resources/db/migration/V41__fct_wrappers.sql deleted file mode 100644 index 8fde81d96..000000000 --- a/base/src/main/resources/db/migration/V41__fct_wrappers.sql +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- these are pg functions wrappers to be able to access them from within jOOQ -CREATE OR REPLACE FUNCTION ehr.jsonb_array_elements(jsonb_val JSONB) - RETURNS SETOF JSONB AS -$$ -BEGIN - RETURN QUERY SELECT jsonb_array_elements(jsonb_val); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.jsonb_array_elements(jsonb_val JSONB) - RETURNS SETOF JSONB AS -$$ -BEGIN - RETURN QUERY SELECT jsonb_array_elements(jsonb_val); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.jsonb_extract_path(from_json jsonb, VARIADIC path_elems text[]) - RETURNS JSONB AS -$$ -BEGIN - RETURN jsonb_extract_path(from_json, path_elems); -END -$$ - LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION ehr.jsonb_extract_path_text(from_json jsonb, VARIADIC path_elems text[]) - RETURNS TEXT AS -$$ -BEGIN - RETURN jsonb_extract_path_text(from_json, path_elems); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V42__drop_contribution_history.sql b/base/src/main/resources/db/migration/V42__drop_contribution_history.sql deleted file mode 100644 index 4feff3db6..000000000 --- a/base/src/main/resources/db/migration/V42__drop_contribution_history.sql +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Jake Smolka (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - --- removes the contribution_history table and linked triggers etc. - -DROP TRIGGER versioning_trigger ON ehr.contribution; - -DROP INDEX ehr_contribution_history; - -DROP TABLE ehr.contribution_history; - -ALTER TABLE ehr.contribution - DROP COLUMN sys_transaction, - DROP COLUMN sys_period; - --- following function needs to replaced by modified version without `contribution_history` reference too - --- ==================================================================== --- Description: Function to delete a single EHR's history, meaning the Status' history. --- Necessary as own function, because the former transaction needs to be done to populate the *_history table. --- Parameters: --- @ehr_id_input - UUID of target EHR --- Returns: '1' --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_ehr_history(ehr_id_input UUID) - RETURNS TABLE (num integer) AS $$ -BEGIN - RETURN QUERY WITH - -- delete status_history - delete_status_history AS ( - DELETE FROM ehr.status_history WHERE ehr_id = ehr_id_input - ) - - SELECT 1; - - -- logging: - RAISE NOTICE 'Admin deletion - Type: % - Linked to EHR ID: % - Time: %', 'STATUS_HISTORY', ehr_id_input, now(); -END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V43__node_name_predicate_fix.sql b/base/src/main/resources/db/migration/V43__node_name_predicate_fix.sql deleted file mode 100644 index ec1e75a12..000000000 --- a/base/src/main/resources/db/migration/V43__node_name_predicate_fix.sql +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- fixed to also support node name predicate for non array node --- (f.e. content[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1]/data[at0001,'history']/events[at0002] -CREATE OR REPLACE FUNCTION ehr.aql_node_name_predicate(entry JSONB, name_value_predicate TEXT, jsonb_path TEXT) - RETURNS JSONB AS -$$ -DECLARE - entry_segment JSONB; - jsquery_node_expression TEXT; - subnode JSONB; -BEGIN - - -- get the segment for the predicate - - SELECT jsonb_extract_path(entry, VARIADIC string_to_array(jsonb_path, ',')) INTO STRICT entry_segment; - - IF (entry_segment IS NULL) THEN - RETURN NULL ; - END IF ; - - -- identify structure with name/value matching argument - IF (jsonb_typeof(entry_segment) <> 'array') THEN - IF ((entry_segment #>> '{/name,0,value}') = name_value_predicate) THEN - RETURN entry_segment; - ELSE - RETURN NULL; - END IF; - END IF; - - FOR subnode IN SELECT jsonb_array_elements(entry_segment) - LOOP - IF ((subnode #>> '{/name,0,value}') = name_value_predicate) THEN - RETURN subnode; - END IF; - END LOOP; - - RETURN NULL; - -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V44__composition_name.sql b/base/src/main/resources/db/migration/V44__composition_name.sql deleted file mode 100644 index 520467f46..000000000 --- a/base/src/main/resources/db/migration/V44__composition_name.sql +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- fix type identification depending on defining code existence -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text(dvcodedtext ehr.dv_coded_text) - RETURNS JSON AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', (SELECT ( - CASE - WHEN ((dvcodedtext).defining_code IS NOT NULL) - THEN - 'DV_CODED_TEXT' - ELSE - 'DV_TEXT' - END - ) - ), - 'value', dvcodedtext.value, - 'defining_code', dvcodedtext.defining_code, - 'formatting', dvcodedtext.formatting, - 'language', dvcodedtext.language, - 'encoding', dvcodedtext.encoding, - 'mappings', ehr.js_term_mappings(dvcodedtext.term_mapping) - ) - ); -END -$$ - LANGUAGE plpgsql; - --- call js_dv_coded_text to properly reflect composition name in canonical json -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - entry.entry as content, - entry.category as category, - entry.name as name, - to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - WHERE composition.id = composition_uuid - ), - entry_content AS ( - SELECT * FROM composition_data - WHERE json_content::text like '{"%/content%' OR json_content = '{}' - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_coded_text(entry_content.name), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_canonical_party_identified(composer), - 'category', ehr.js_dv_coded_text(category), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V45__fix_canonical_comp.sql b/base/src/main/resources/db/migration/V45__fix_canonical_comp.sql deleted file mode 100644 index 235015bae..000000000 --- a/base/src/main/resources/db/migration/V45__fix_canonical_comp.sql +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- this fixes querying composition with no content -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH entry_content AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - entry.entry as content, - entry.category as category, - entry.name as name, - (SELECT jsonb_content FROM - (SELECT to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as jsonb_content) selcontent - WHERE jsonb_content::text like '{"%/content%' LIMIT 1) as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - WHERE composition.id = composition_uuid - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_coded_text(entry_content.name), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_canonical_party_identified(composer), - 'category', ehr.js_dv_coded_text(category), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V46__node_name_predicate_fix.sql b/base/src/main/resources/db/migration/V46__node_name_predicate_fix.sql deleted file mode 100644 index ec1e75a12..000000000 --- a/base/src/main/resources/db/migration/V46__node_name_predicate_fix.sql +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- fixed to also support node name predicate for non array node --- (f.e. content[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1]/data[at0001,'history']/events[at0002] -CREATE OR REPLACE FUNCTION ehr.aql_node_name_predicate(entry JSONB, name_value_predicate TEXT, jsonb_path TEXT) - RETURNS JSONB AS -$$ -DECLARE - entry_segment JSONB; - jsquery_node_expression TEXT; - subnode JSONB; -BEGIN - - -- get the segment for the predicate - - SELECT jsonb_extract_path(entry, VARIADIC string_to_array(jsonb_path, ',')) INTO STRICT entry_segment; - - IF (entry_segment IS NULL) THEN - RETURN NULL ; - END IF ; - - -- identify structure with name/value matching argument - IF (jsonb_typeof(entry_segment) <> 'array') THEN - IF ((entry_segment #>> '{/name,0,value}') = name_value_predicate) THEN - RETURN entry_segment; - ELSE - RETURN NULL; - END IF; - END IF; - - FOR subnode IN SELECT jsonb_array_elements(entry_segment) - LOOP - IF ((subnode #>> '{/name,0,value}') = name_value_predicate) THEN - RETURN subnode; - END IF; - END LOOP; - - RETURN NULL; - -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V47__fix_iso_dv_date_time.sql b/base/src/main/resources/db/migration/V47__fix_iso_dv_date_time.sql deleted file mode 100644 index 9dc2eadd2..000000000 --- a/base/src/main/resources/db/migration/V47__fix_iso_dv_date_time.sql +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - --- do not use the error prone XML date/time conversion -DROP FUNCTION ehr.js_dv_date_time(TIMESTAMPTZ, TEXT); -DROP FUNCTION ehr.js_dv_date_time(TIMESTAMP,text); - --- this is to fix the timezone drift and provide the correct encoding -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMP, TEXT) - RETURNS JSON AS -$$ -DECLARE - date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; - value_date_time TEXT; -BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; - END IF; - - IF (time_zone IS NULL) - THEN - time_zone := 'Z'; - END IF; - - RETURN - json_build_object( - '_type', 'DV_DATE_TIME', - 'value',to_char(date_time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"'||time_zone||'"') - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V48__fix_canonical.sql b/base/src/main/resources/db/migration/V48__fix_canonical.sql deleted file mode 100644 index 49c42c1ee..000000000 --- a/base/src/main/resources/db/migration/V48__fix_canonical.sql +++ /dev/null @@ -1,114 +0,0 @@ --- missing type... -CREATE OR REPLACE FUNCTION ehr.js_party_ref(TEXT, TEXT, TEXT, TEXT) - RETURNS JSON AS -$$ -DECLARE - id_value ALIAS FOR $1; - id_scheme ALIAS FOR $2; - namespace ALIAS FOR $3; - party_type ALIAS FOR $4; -BEGIN - - IF (id_value IS NULL AND id_scheme IS NULL AND namespace IS NULL AND party_type IS NULL) THEN - RETURN NULL; - ELSE - RETURN - json_build_object( - '_type', 'PARTY_REF', - 'id', - json_build_object( - '_type', 'GENERIC_ID', - 'value', id_value, - 'scheme', id_scheme - ), - 'namespace', namespace, - 'type', party_type - ); - END IF; -END -$$ - LANGUAGE plpgsql; - - --- fix wrong encoding of code_phrase -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text(dvcodedtext ehr.dv_coded_text) - RETURNS JSON AS -$$ -BEGIN - RETURN - jsonb_strip_nulls( - jsonb_build_object( - '_type', (SELECT ( - CASE - WHEN ((dvcodedtext).defining_code IS NOT NULL) - THEN - 'DV_CODED_TEXT' - ELSE - 'DV_TEXT' - END - ) - ), - 'value', dvcodedtext.value, - 'defining_code', ehr.js_code_phrase(dvcodedtext.defining_code), - 'formatting', dvcodedtext.formatting, - 'language', dvcodedtext.language, - 'encoding', dvcodedtext.encoding, - 'mappings', ehr.js_term_mappings(dvcodedtext.term_mapping) - ) - ); -END -$$ - LANGUAGE plpgsql; - --- make sure composition name is true DV_TEXT -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH entry_content AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - entry.entry as content, - entry.category as category, - entry.name as name, - (SELECT jsonb_content FROM - (SELECT to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as jsonb_content) selcontent - WHERE jsonb_content::text like '{"%/content%' LIMIT 1) as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - WHERE composition.id = composition_uuid - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text((entry_content.name).value), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_canonical_party_identified(composer), - 'category', ehr.js_dv_coded_text(category), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V49__deal_with_jsonb_empty_resultset.sql b/base/src/main/resources/db/migration/V49__deal_with_jsonb_empty_resultset.sql deleted file mode 100644 index d1a9d2003..000000000 --- a/base/src/main/resources/db/migration/V49__deal_with_jsonb_empty_resultset.sql +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- extend standard jsonb_array_elements to return an empty json object instead of an empty resultset --- this is required to avoid empty results due to performing cartesian product with an empty set. --- NB. this function is used when dealing with ITEM_STRUCTURE (composition entry f.e.) -CREATE OR REPLACE FUNCTION ehr.xjsonb_array_elements(entry JSONB) - RETURNS SETOF JSONB AS -$$ -BEGIN - IF (entry IS NULL) THEN - RETURN QUERY SELECT NULL::jsonb ; - ELSE - RETURN QUERY SELECT jsonb_array_elements(entry); - END IF; - -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V4_1__patient_summary.sql b/base/src/main/resources/db/migration/V4_1__patient_summary.sql deleted file mode 100644 index dd2ec0174..000000000 --- a/base/src/main/resources/db/migration/V4_1__patient_summary.sql +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- this script enhance an existing EtherCIS DB to support the cache summary extension it --- is called by prepare_db shell script --- PASS 1: prepare the cache summary configuration tables --- C.Chevalley May 2017 --- See LICENSE.txt for licensing details -------------------------------------------------------------------------------------------------- --- originally called: prepare_cache_summary_db_1.sql - -CREATE TABLE ehr.heading ( - code VARCHAR(16) PRIMARY KEY , - name TEXT, - description TEXT -); - -CREATE TABLE ehr.template ( - uid UUID PRIMARY KEY, - template_id TEXT UNIQUE, - concept TEXT -); - -CREATE TABLE ehr.template_heading_xref ( - heading_code VARCHAR(16) REFERENCES ehr.heading(code), - template_id UUID REFERENCES ehr.template(uid) -); --- fills in the headings -INSERT INTO ehr.heading -VALUES - ('ORDERS', 'Orders', 'Orders'), - ('RESULTS', 'Results', 'Results'), - ('VITALS', 'Vitals', 'Vitals'), ('DIAGNOSES', 'Diagnoses', 'Diagnoses'); diff --git a/base/src/main/resources/db/migration/V50__folder_audit.sql b/base/src/main/resources/db/migration/V50__folder_audit.sql deleted file mode 100644 index d9202aa05..000000000 --- a/base/src/main/resources/db/migration/V50__folder_audit.sql +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Jake Smolka (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - --- Adds commit_audit to each folder version - --- Migration function to create new dummy audits -CREATE OR REPLACE FUNCTION ehr.migrate_folder_audit(OUT ret_id UUID) AS -$$ -BEGIN - -- Add migration dummy party entry, only if not existing already - INSERT INTO ehr.party_identified ( - -- id will get generated - name, - party_type, - object_id_type - ) - SELECT 'migration_dummy_65a3da3a-476f-4b1e-ab04-fa1c42edeac0', - 'party_self', - 'undefined' - WHERE NOT EXISTS ( - SELECT 1 FROM ehr.party_identified WHERE name='migration_dummy_65a3da3a-476f-4b1e-ab04-fa1c42edeac0' - ); - - -- Helper queries to: - -- 1) Find the oldest audit to copy two attributes from - -- (Note: There will always be an audit, because this migration function is only run for existing folder, which require and EHR, which will have a Status, which will have an Audit. - WITH audits AS ( - SELECT ad.system_id, - ad.time_committed_tzid - FROM ehr.audit_details AS ad - WHERE ad.id IN ( - SELECT id FROM ehr.audit_details ORDER BY time_committed asc LIMIT 1 - ) - - ), - -- 2) Find the dummy party - party AS ( - SELECT id FROM ehr.party_identified WHERE name = 'migration_dummy_65a3da3a-476f-4b1e-ab04-fa1c42edeac0' LIMIT 1 - ) - - -- Copy the values of the oldest/initial audit - -- and change committer to the dummy party and the description to "migration_dummy" - INSERT INTO ehr.audit_details ( - -- id will get generated - system_id, - committer, - -- time_committed will get default value - time_committed_tzid, - change_type, - description - ) - SELECT - a.system_id, - p.id, -- set dummy committer - a.time_committed_tzid, - 'Unknown', -- change type set to unknown - 'migration_dummy' -- description to mark entry as dummy - FROM audits AS a, party AS p - - -- Finally take and return the ID of the inserted row - RETURNING id - INTO ret_id; -- returned at the end automatically -END -$$ -LANGUAGE plpgsql; - -ALTER TABLE ehr.folder - ADD COLUMN has_audit UUID references ehr.audit_details(id) ON DELETE CASCADE; -- has this audit_details instance - -ALTER TABLE ehr.folder - -- Set the type (again), to be able to call the migration function - ALTER COLUMN has_audit TYPE UUID - USING ehr.migrate_folder_audit(), - -- And finally set the column to NOT NULL - ALTER COLUMN has_audit SET NOT NULL; - -ALTER TABLE ehr.folder_history - ADD COLUMN has_audit UUID references ehr.audit_details(id) ON DELETE CASCADE; -- has this audit_details instance - -ALTER TABLE ehr.folder_history - -- Set the type (again), to be able to call the migration function - ALTER COLUMN has_audit TYPE UUID - USING ehr.migrate_folder_audit(), - -- And finally set the column to NOT NULL - ALTER COLUMN has_audit SET NOT NULL; - --- Also modify the admin deletion of a folder function to include the new audits. -DROP FUNCTION admin_delete_folder(uuid); --- ==================================================================== --- Description: Function to delete a Folder. --- Parameters: --- @folder_id_input - UUID of target Folder --- Returns: linked contribution, folder children UUIDs, linked audits --- Requires: Afterwards deletion of all _HISTORY tables with the returned contributions and children + audits. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_folder(folder_id_input UUID) -RETURNS TABLE (contribution UUID, child UUID, audit UUID) AS $$ - DECLARE - results RECORD; - BEGIN - RETURN QUERY WITH - -- order to delete things: - -- all folders (scope's parent + children) itself from FOLDER, order shouldn't matter - -- all their FOLDER_HIERARCHY entries - -- all FOLDER_ITEMS matching FOLDER.IDs - -- all OBJECT_REF mentioned in FOLDER_ITEMS - -- all CONTRIBUTIONs (1..*) collected along the way above - -- all audits - -- AFTERWARDS and separate: deletion of all matching *_HISTORY table entries - - -- recursively retrieve all layers of children - RECURSIVE linked_children AS ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - UNION - SELECT fh.child_folder, fh.in_contribution FROM ehr.folder_hierarchy fh - INNER JOIN linked_children lc ON lc.child_folder = fh.parent_folder - ), - linked_object_ref AS ( - SELECT DISTINCT object_ref_id FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT linked_children.child_folder FROM linked_children)) - ), - linked_contribution AS ( - SELECT DISTINCT in_contribution FROM ehr.folder WHERE (id = folder_id_input) OR (id IN (SELECT linked_children.child_folder FROM linked_children)) - - UNION - - SELECT DISTINCT in_contribution FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT linked_children.child_folder FROM linked_children)) - - UNION - - SELECT DISTINCT in_contribution FROM ehr.object_ref WHERE id IN (SELECT linked_object_ref.object_ref_id FROM linked_object_ref) - - UNION - - SELECT DISTINCT in_contribution FROM linked_children - ), - linked_audit AS ( - SELECT DISTINCT has_audit FROM ehr.folder WHERE (id = folder_id_input) OR (id IN (SELECT linked_children.child_folder FROM linked_children)) - ), - remove_directory AS ( - UPDATE ehr.ehr -- remove link to ehr and then actually delete the folder - SET directory = NULL - WHERE directory = folder_id_input - ), - delete_folders AS ( - DELETE FROM ehr.folder WHERE (id = folder_id_input) OR (id IN (SELECT linked_children.child_folder FROM linked_children)) - ), - delete_hierarchy AS ( - DELETE FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ), - delete_items AS ( - DELETE FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT linked_children.child_folder FROM linked_children)) - ), - delete_object_ref AS ( - DELETE FROM ehr.object_ref WHERE id IN (SELECT linked_object_ref.object_ref_id FROM linked_object_ref) - ) - -- returning contribution IDs to delete separate - -- same with children IDs, as *_HISTORY tables of ID sets ((original input folder + children), and obj_ref via their contribs) needs to be deleted separate, too. - -- as well as audits - SELECT DISTINCT linked_contribution.in_contribution, linked_children.child_folder, linked_audit.has_audit FROM linked_contribution, linked_children, linked_audit; - - -- logging: - - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'FOLDER', folder_id_input, now(); - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT a.child_folder FROM ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ) AS a ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'FOLDER', results.child_folder, now(); - END LOOP; - - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_HIERARCHY', folder_id_input, now(); - - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_ITEMS', folder_id_input, now(); - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT a.child_folder FROM ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ) AS a ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'FOLDER_ITEMS', results.child_folder, now(); - END LOOP; - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT a.object_ref_id FROM ( - SELECT DISTINCT object_ref_id FROM ehr.folder_items WHERE (folder_id = folder_id_input) OR (folder_id IN (SELECT b.child_folder FROM ( - SELECT child_folder, in_contribution FROM ehr.folder_hierarchy WHERE parent_folder = folder_id_input - ) AS b )) - ) AS a - ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - Linked to FOLDER ID: % - Time: %', 'OBJECT_REF', results.object_ref_id, now(); - END LOOP; - END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V51__CR_558_560.sql b/base/src/main/resources/db/migration/V51__CR_558_560.sql deleted file mode 100644 index 725500af5..000000000 --- a/base/src/main/resources/db/migration/V51__CR_558_560.sql +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- CR 560. Add missing uid attribute -DROP FUNCTION IF EXISTS ehr_status_uid(uuid,text); - -CREATE OR REPLACE FUNCTION ehr.ehr_status_uid(ehr_uuid UUID, server_id TEXT) - RETURNS TEXT AS -$$ -BEGIN - RETURN (select "status"."ehr_id"||'::'||server_id||'::'||1 - + COALESCE( - (select count(*) - from "ehr"."status_history" - where "status_history"."ehr_id" = ehr_uuid - group by "ehr"."status_history"."ehr_id") - , 0) - from ehr.status - where status.ehr_id = ehr_uuid); -END -$$ - LANGUAGE plpgsql; - -DROP FUNCTION IF EXISTS js_ehr_status_uid(uuid,text); - -CREATE OR REPLACE FUNCTION ehr.js_ehr_status_uid(ehr_uuid UUID, server_id TEXT) - RETURNS JSONB AS -$$ -BEGIN - RETURN jsonb_strip_nulls( - jsonb_build_object( - '_type', 'HIER_OBJECT_ID', - 'value', ehr.ehr_status_uid(ehr_uuid, server_id) - ) - ); -END -$$ - LANGUAGE plpgsql; - -DROP FUNCTION IF EXISTS js_ehr_status(uuid,text); - -CREATE OR REPLACE FUNCTION ehr.js_ehr_status(ehr_uuid UUID, server_id TEXT) - RETURNS JSON AS -$$ -BEGIN - RETURN ( - WITH ehr_status_data AS ( - SELECT - status.other_details as other_details, - status.party as subject, - status.is_queryable as is_queryable, - status.is_modifiable as is_modifiable, - status.sys_transaction as time_created, - status.name as status_name, - status.archetype_node_id as archetype_node_id - FROM ehr.status - WHERE status.ehr_id = ehr_uuid - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR_STATUS', - 'archetype_node_id', archetype_node_id, - 'name', status_name, - 'subject', ehr.js_party(subject), - 'uid', ehr.js_ehr_status_uid(ehr_uuid, server_id), - 'is_queryable', is_queryable, - 'is_modifiable', is_modifiable, - 'other_details', other_details - ) - ) - FROM ehr_status_data - ); -END -$$ - LANGUAGE plpgsql; - --- CR 558. Add missing attributes in DV_IDENTIFIER structure - -CREATE OR REPLACE FUNCTION ehr.json_party_identified(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_strip_nulls( - jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value, - 'assigner', identifier_attribute.assigner, - 'issuer', identifier_attribute.issuer, - 'type', identifier_attribute.type_name - ) - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_IDENTIFIED', - 'name', name, - 'identifiers', arr, - 'external_ref', jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', ref_type, - 'id', ehr.js_canonical_object_id(objectid_type, scheme, id_value) - ) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - - -CREATE OR REPLACE FUNCTION ehr.json_party_related(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type, relationship ehr.dv_coded_text) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_strip_nulls( - jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value, - 'assigner', identifier_attribute.assigner, - 'issuer', identifier_attribute.issuer, - 'type', identifier_attribute.type_name - ) - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_RELATED', - 'name', name, - 'identifiers', arr, - 'external_ref', jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', ref_type, - 'id', ehr.js_canonical_object_id(objectid_type, scheme, id_value) - ), - 'relationship', ehr.js_dv_coded_text(relationship) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; diff --git a/base/src/main/resources/db/migration/V52__audit_attributes_fix.sql b/base/src/main/resources/db/migration/V52__audit_attributes_fix.sql deleted file mode 100644 index baf6fe015..000000000 --- a/base/src/main/resources/db/migration/V52__audit_attributes_fix.sql +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Jake Smolka (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -CREATE - OR REPLACE FUNCTION ehr.migration_audit_system_id(system_id UUID) - RETURNS UUID AS -$$ -BEGIN - - -- Add migration dummy system entry, only if not existing already - INSERT INTO ehr.system ( - -- id will get generated - description, - settings) - SELECT 'migration_dummy_96715295-b63a-4d5e-b3d1-7da8bb6edb2e', - 'internal.migration_dummy_96715295-b63a-4d5e-b3d1-7da8bb6edb2e.org' - WHERE NOT EXISTS( - SELECT 1 - FROM ehr.system - WHERE description = 'migration_dummy_96715295-b63a-4d5e-b3d1-7da8bb6edb2e' - ); - - IF - system_id IS NULL THEN - RETURN ( - SELECT id - FROM ehr.system - WHERE description = 'migration_dummy_96715295-b63a-4d5e-b3d1-7da8bb6edb2e' - LIMIT 1 - ); - ELSE - RETURN system_id; - END IF; - -END -$$ - LANGUAGE plpgsql; - -CREATE - OR REPLACE FUNCTION ehr.migration_audit_committer(committer UUID) - RETURNS UUID AS -$$ -BEGIN - - -- Add migration dummy party entry, only if not existing already - INSERT INTO ehr.party_identified ( - -- id will get generated - name, - party_type, - object_id_type) - SELECT 'migration_dummy_65a3da3a-476f-4b1e-ab04-fa1c42edeac0', - 'party_self', - 'undefined' - WHERE NOT EXISTS( - SELECT 1 - FROM ehr.party_identified - WHERE name = 'migration_dummy_65a3da3a-476f-4b1e-ab04-fa1c42edeac0' - ); - - IF - committer IS NULL THEN - RETURN ( - SELECT id - FROM ehr.party_identified - WHERE name = 'migration_dummy_65a3da3a-476f-4b1e-ab04-fa1c42edeac0' - LIMIT 1 - ); - ELSE - RETURN committer; - END IF; - -END -$$ - LANGUAGE plpgsql; - -CREATE - OR REPLACE FUNCTION ehr.migration_audit_tzid(time_committed_tzid TEXT) - RETURNS TEXT AS -$$ -BEGIN - IF - time_committed_tzid IS NULL THEN - RETURN ( - 'Etc/UTC' - ); - ELSE - RETURN time_committed_tzid; - END IF; -END -$$ - LANGUAGE plpgsql; - --- Fix mandatory attributes with NOT NULL constraint -ALTER TABLE ehr.audit_details - -- Set the type (again), to be able to call the migration function - ALTER COLUMN system_id TYPE UUID - USING ehr.migration_audit_system_id(system_id), - -- And finally set the column to NOT NULL - ALTER COLUMN system_id SET NOT NULL, - - -- Set the type (again), to be able to call the migration function - ALTER - COLUMN committer TYPE UUID - USING ehr.migration_audit_committer(committer), - -- And finally set the column to NOT NULL - ALTER - COLUMN committer - SET NOT NULL, - - -- change_type is set to NOT NULL already - - -- time_committed has valid default now() - - -- Set the type (again), to be able to call the migration function - ALTER - COLUMN time_committed_tzid TYPE TEXT - USING ehr.migration_audit_tzid(time_committed_tzid), - -- And finally set the column to NOT NULL - ALTER - COLUMN time_committed_tzid - SET NOT NULL; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V53__admin_delete_status_fix.sql b/base/src/main/resources/db/migration/V53__admin_delete_status_fix.sql deleted file mode 100644 index b8ebfcbab..000000000 --- a/base/src/main/resources/db/migration/V53__admin_delete_status_fix.sql +++ /dev/null @@ -1,46 +0,0 @@ --- ==================================================================== --- Author: Jake Smolka --- Create date: 2021-07-21 --- Description: Fix for Admin API deletion of old status audits. --- ===================================================================== - --- The following function is copied from its latest state and modified with the fix. - --- ==================================================================== --- Description: Function to delete an EHR, incl. Status. --- Parameters: --- @ehr_id_input - UUID of target EHR --- Returns: '1' and linked audit, party UUID --- Requires: Afterwards deletion of returned audit and party. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_ehr(ehr_id_input UUID) -RETURNS TABLE (num integer, status_audit UUID, status_party UUID) AS $$ -BEGIN -RETURN QUERY WITH linked_status(has_audit) AS ( -- get linked STATUS parameters - SELECT has_audit FROM ehr.status AS s - WHERE ehr_id = ehr_id_input - UNION - SELECT has_audit FROM ehr.status_history AS sh - WHERE ehr_id = ehr_id_input - ), - -- delete the EHR itself - delete_ehr AS ( - DELETE FROM ehr.ehr WHERE id = ehr_id_input - ), - linked_party(id) AS ( -- formally always one - SELECT party FROM ehr.status WHERE ehr_id = ehr_id_input - ), - -- Note: not handling the system referenced by EHR, because there is always at least one audit referencing it, too. See separated audit handling. - -- delete status - delete_status AS ( - DELETE FROM ehr.status WHERE ehr_id = ehr_id_input - ) - -SELECT 1, linked_status.has_audit, linked_party.id FROM linked_status, linked_party; - --- logging: -RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'EHR', ehr_id_input, now(); - RAISE NOTICE 'Admin deletion - Type: % - Linked to EHR ID: % - Time: %', 'STATUS', ehr_id_input, now(); -END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V54__fix_subject_encoding.sql b/base/src/main/resources/db/migration/V54__fix_subject_encoding.sql deleted file mode 100644 index 27ed38f11..000000000 --- a/base/src/main/resources/db/migration/V54__fix_subject_encoding.sql +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -CREATE OR REPLACE FUNCTION ehr.js_ehr_status(ehr_uuid UUID, server_id TEXT) - RETURNS JSON AS -$$ -BEGIN - RETURN ( - WITH ehr_status_data AS ( - SELECT - status.other_details as other_details, - status.party as subject, - status.is_queryable as is_queryable, - status.is_modifiable as is_modifiable, - status.sys_transaction as time_created, - status.name as status_name, - status.archetype_node_id as archetype_node_id - FROM ehr.status - WHERE status.ehr_id = ehr_uuid - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR_STATUS', - 'archetype_node_id', archetype_node_id, - 'name', status_name, - 'subject', ehr.js_canonical_party_identified(subject), - 'uid', ehr.js_ehr_status_uid(ehr_uuid, server_id), - 'is_queryable', is_queryable, - 'is_modifiable', is_modifiable, - 'other_details', other_details - ) - ) - FROM ehr_status_data - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V55__purge_unused_party_identified.sql b/base/src/main/resources/db/migration/V55__purge_unused_party_identified.sql deleted file mode 100644 index c021cb1dd..000000000 --- a/base/src/main/resources/db/migration/V55__purge_unused_party_identified.sql +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- returns the count of occurrences of a party_identified accross table having it as argument -CREATE OR REPLACE FUNCTION ehr.party_usage(party_uuid UUID) - RETURNS BIGINT AS -$$ -BEGIN - RETURN ( - with usage_uuid as ( - SELECT facility as uuid from ehr.event_context where facility = party_uuid - UNION - SELECT facility as uuid from ehr.event_context_history where facility = party_uuid - UNION - SELECT composer as uuid from ehr.composition where composer = party_uuid - UNION - SELECT composer as uuid from ehr.composition_history where composer = party_uuid - UNION - SELECT performer as uuid from ehr.participation where performer = party_uuid - UNION - SELECT performer as uuid from ehr.participation_history where performer = party_uuid - UNION - SELECT party as uuid from ehr.status where party = party_uuid - UNION - SELECT party as uuid from ehr.status_history where party = party_uuid - UNION - SELECT committer as uuid from ehr.audit_details where committer = party_uuid - ) - SELECT count(usage_uuid.uuid) - FROM usage_uuid - ); -END -$$ -LANGUAGE plpgsql; - --- use this function for debugging purpose --- identifies where the party_identified is referenced -CREATE OR REPLACE FUNCTION ehr.party_usage_identification(party_uuid UUID) - RETURNS table(id UUID, entity TEXT) AS -$$ - with usage_uuid as ( - SELECT facility as uuid, 'FACILITY' as entity from ehr.event_context where facility = party_uuid - UNION - SELECT facility as uuid, 'FACILITY_HISTORY' as entity from ehr.event_context_history where facility = party_uuid - UNION - SELECT composer as uuid, 'COMPOSER' as entity from ehr.composition where composer = party_uuid - UNION - SELECT composer as uuid, 'COMPOSER_HISTORY' as entity from ehr.composition_history where composer = party_uuid - UNION - SELECT performer as uuid, 'PERFORMER' as entity from ehr.participation where performer = party_uuid - UNION - SELECT performer as uuid, 'PERFORMER_HISTORY' as entity from ehr.participation_history where performer = party_uuid - UNION - SELECT party as uuid, 'SUBJECT' as entity from ehr.status where party = party_uuid - UNION - SELECT party as uuid, 'SUBJECT_HISTORY' as entity from ehr.status_history where party = party_uuid - UNION - SELECT committer as uuid, 'AUDIT_DETAILS' as entity from ehr.audit_details where committer = party_uuid - ) - SELECT usage_uuid.uuid, usage_uuid.entity - FROM usage_uuid; -$$ -LANGUAGE sql; - --- alter table identifier to add the missing on delete...cascade -alter table ehr.identifier -drop constraint identifier_party_fkey, -add constraint identifier_party_fkey - foreign key (party) - references ehr.party_identified(id) - on delete cascade; - --- garbage collection: delete all party_identified where usage count is 0 --- DELETE FROM party_identified WHERE ehr.party_usage(party_identified.id) = 0; - --- MODIFICATION of existing function: fixes deletion of participation_history and event_context_history --- ==================================================================== --- Description: Function to delete event_contexts and participations for a composition and return their parties (event_context.facility and participation.performer). --- Parameters: --- @compo_id_input - UUID of super composition --- Returns: '1' and linked party UUID --- Requires: Afterwards deletion of returned party. --- ===================================================================== -CREATE OR REPLACE FUNCTION ehr.admin_delete_event_context_for_compo(compo_id_input UUID) - RETURNS TABLE (num integer, party UUID) AS $$ -DECLARE - results RECORD; -BEGIN - -- since for this admin op, we don't want to generate a history record for each delete! - ALTER TABLE ehr.event_context DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation DISABLE TRIGGER versioning_trigger; - - RETURN QUERY WITH - linked_events(id) AS ( -- get linked EVENT_CONTEXT entities -- 0..1 - SELECT id, facility FROM ehr.event_context WHERE composition_id = compo_id_input - ), - linked_event_history(id) AS ( -- get linked EVENT_CONTEXT entities -- 0..1 - SELECT id, facility FROM ehr.event_context_history WHERE composition_id = compo_id_input - ), - linked_participations_for_events(id) AS ( -- get linked EVENT_CONTEXT entities -- for 0..1 events, each with * participations - SELECT id, performer FROM ehr.participation WHERE event_context IN (SELECT linked_events.id FROM linked_events) - ), - linked_participations_for_events_history(id) AS ( -- get linked EVENT_CONTEXT entities -- for 0..1 events, each with * participations - SELECT id, performer FROM ehr.participation_history WHERE event_context IN (SELECT linked_event_history.id FROM linked_event_history) - ), - parties(id) AS ( - SELECT facility FROM linked_events - UNION - SELECT performer FROM linked_participations_for_events - ), - delete_participation AS ( - DELETE FROM ehr.participation WHERE ehr.participation.id IN (SELECT linked_participations_for_events.id FROM linked_participations_for_events) - ), - delete_participation_history AS ( - DELETE FROM ehr.participation_history WHERE ehr.participation_history.id IN (SELECT linked_participations_for_events_history.id FROM linked_participations_for_events_history) - ), - delete_event_contexts AS ( - DELETE FROM ehr.event_context WHERE ehr.event_context.id IN (SELECT linked_events.id FROM linked_events) - ), - delete_event_contexts_history AS ( - DELETE FROM ehr.event_context_history WHERE ehr.event_context_history.id IN (SELECT linked_event_history.id FROM linked_event_history) - ) - SELECT 1, parties.id FROM parties; - - -- logging: - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT b.id FROM ( - SELECT id, performer FROM ehr.participation - WHERE event_context IN (SELECT a.id FROM ( - SELECT id, facility FROM ehr.event_context WHERE composition_id = compo_id_input - ) AS a ) - ) AS b - ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'PARTICIPATION', results.id, now(); - END LOOP; - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT id, facility - FROM ehr.event_context - WHERE composition_id = compo_id_input) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'EVENT_CONTEXT', results.id, now(); - END LOOP; - - -- restore disabled triggers - ALTER TABLE ehr.event_context ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation ENABLE TRIGGER versioning_trigger; - -END; -$$ LANGUAGE plpgsql - RETURNS NULL ON NULL INPUT; - --- delete remaining history records from deleted parents -CREATE OR REPLACE FUNCTION ehr.delete_orphan_history() - RETURNS BOOLEAN AS -$$ - WITH - delete_orphan_compo_history as ( - delete from ehr.composition_history where not exists(select 1 from ehr.composition where id = ehr.composition_history.id) - ), - delete_orphan_event_context_history as ( - delete from ehr.event_context_history where not exists(select 1 from ehr.event_context where event_context.composition_id = ehr.event_context_history.composition_id) - ), - delete_orphan_participation_history as ( - delete from ehr.participation_history where not exists(select 1 from ehr.participation where participation.event_context = ehr.participation_history.event_context) - ), - delete_orphan_entry_history as ( - delete from ehr.entry_history where not exists(select 1 from ehr.composition where composition.id = ehr.entry_history.composition_id) - ), - delete_orphan_party_identified as ( - DELETE FROM ehr.party_identified WHERE ehr.party_usage(party_identified.id) = 0 - ) - select true; -$$ -LANGUAGE sql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V56__fix_canonical_party_identified.sql b/base/src/main/resources/db/migration/V56__fix_canonical_party_identified.sql deleted file mode 100644 index fcd3d6f27..000000000 --- a/base/src/main/resources/db/migration/V56__fix_canonical_party_identified.sql +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - --- invoke correct canonical encoder for health_care_facility -CREATE OR REPLACE FUNCTION ehr.js_context(UUID) - RETURNS JSON AS -$$ -DECLARE - context_id ALIAS FOR $1; -BEGIN - - IF (context_id IS NULL) - THEN - RETURN NULL; - ELSE - RETURN ( - WITH context_attributes AS ( - SELECT - start_time, - start_time_tzid, - end_time, - end_time_tzid, - facility, - location, - other_context, - setting - FROM ehr.event_context - WHERE id = context_id - ) - SELECT jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EVENT_CONTEXT', - 'start_time', ehr.js_dv_date_time(start_time, start_time_tzid), - 'end_time', ehr.js_dv_date_time(end_time, end_time_tzid), - 'location', location, - 'health_care_facility', ehr.js_canonical_party_identified(facility), - 'setting', ehr.js_dv_coded_text(setting), - 'other_context',other_context, - 'participations', ehr.js_participations(context_id) - ) - ) - FROM context_attributes - ); - END IF; -END -$$ - LANGUAGE plpgsql; - --- fix NULL external_ref representation -CREATE OR REPLACE FUNCTION ehr.party_ref(namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type) - RETURNS jsonb AS -$$ -BEGIN - RETURN - (SELECT ( - CASE - WHEN (namespace IS NOT NULL AND ref_type IS NOT NULL) THEN - jsonb_build_object( - '_type', 'PARTY_REF', - 'namespace', namespace, - 'type', ref_type, - 'id', - ehr.js_canonical_object_id(objectid_type, scheme, id_value) - ) - ELSE NULL - END - ) - ); -END; -$$ - language 'plpgsql'; - -CREATE OR REPLACE FUNCTION ehr.json_party_self(refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; -BEGIN - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_SELF', - 'external_ref', ehr.party_ref(namespace, ref_type, scheme, id_value, objectid_type) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - - -CREATE OR REPLACE FUNCTION ehr.json_party_identified(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_IDENTIFIED', - 'name', name, - 'identifiers', arr, - 'external_ref', ehr.party_ref(namespace, ref_type, scheme, id_value, objectid_type) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - -CREATE OR REPLACE FUNCTION ehr.json_party_related(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type, relationship ehr.dv_coded_text) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_RELATED', - 'name', name, - 'identifiers', arr, - 'external_ref', ehr.party_ref(namespace, ref_type, scheme, id_value, objectid_type), - 'relationship', ehr.js_dv_coded_text(relationship) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; diff --git a/base/src/main/resources/db/migration/V57__re_apply_CR_558_560.sql b/base/src/main/resources/db/migration/V57__re_apply_CR_558_560.sql deleted file mode 100644 index 28e3edb47..000000000 --- a/base/src/main/resources/db/migration/V57__re_apply_CR_558_560.sql +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - - - -CREATE OR REPLACE FUNCTION ehr.json_party_identified(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value, - 'assigner', identifier_attribute.assigner, - 'issuer', identifier_attribute.issuer, - 'type', identifier_attribute.type_name - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_IDENTIFIED', - 'name', name, - 'identifiers', arr, - 'external_ref', ehr.party_ref(namespace, ref_type, scheme, id_value, objectid_type) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; - -CREATE OR REPLACE FUNCTION ehr.json_party_related(name TEXT, refid UUID, namespace TEXT, ref_type TEXT, scheme TEXT, id_value TEXT, objectid_type ehr.party_ref_id_type, relationship ehr.dv_coded_text) - RETURNS json AS -$$ -DECLARE - json_party_struct JSON; - item JSONB; - arr JSONB[]; - identifier_attribute record; -BEGIN - -- build an array of json object from identifiers if any - FOR identifier_attribute IN SELECT * FROM ehr.identifier WHERE identifier.party = refid LOOP - item := jsonb_build_object( - '_type', 'DV_IDENTIFIER', - 'id',identifier_attribute.id_value - 'assigner', identifier_attribute.assigner, - 'issuer', identifier_attribute.issuer, - 'type', identifier_attribute.type_name - ); - arr := array_append(arr, item); - END LOOP; - - SELECT - jsonb_strip_nulls( - jsonb_build_object ( - '_type', 'PARTY_RELATED', - 'name', name, - 'identifiers', arr, - 'external_ref', ehr.party_ref(namespace, ref_type, scheme, id_value, objectid_type), - 'relationship', ehr.js_dv_coded_text(relationship) - ) - ) - INTO json_party_struct; - RETURN json_party_struct; -end; -$$ - language 'plpgsql'; diff --git a/base/src/main/resources/db/migration/V58__add_canonical_feeder_audit.sql b/base/src/main/resources/db/migration/V58__add_canonical_feeder_audit.sql deleted file mode 100644 index 29680985f..000000000 --- a/base/src/main/resources/db/migration/V58__add_canonical_feeder_audit.sql +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- added resolution for feeder_audit and links --- NB: links requires further tests but at least it is not ignored. - -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID, server_node_id TEXT) - RETURNS JSON AS -$$ -DECLARE - composition_uuid ALIAS FOR $1; -BEGIN - RETURN ( - WITH entry_content AS ( - SELECT - composition.id as composition_id, - composition.language as language, - composition.territory as territory, - composition.composer as composer, - composition.feeder_audit as feeder_audit, - composition.links as links, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - entry.rm_version as rm_version, - entry.entry as content, - entry.category as category, - entry.name as name, - (SELECT jsonb_content FROM - (SELECT to_jsonb(jsonb_each(to_jsonb(jsonb_each((entry.entry)::jsonb)))) #>> '{value}' as jsonb_content) selcontent - WHERE jsonb_content::text like '{"%/content%' LIMIT 1) as json_content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - WHERE composition.id = composition_uuid - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'COMPOSITION', - 'name', ehr.js_dv_text((entry_content.name).value), - 'archetype_details', ehr.js_archetype_details(entry_content.archetype_id, entry_content.template_id, entry_content.rm_version), - 'archetype_node_id', entry_content.archetype_id, - 'feeder_audit', entry_content.feeder_audit, - 'links', entry_content.links, - 'uid', ehr.js_object_version_id(ehr.composition_uid(entry_content.composition_id, server_node_id)), - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_canonical_party_identified(composer), - 'category', ehr.js_dv_coded_text(category), - 'context', ehr.js_context(context_id), - 'content', entry_content.json_content::jsonb - ) - ) - FROM entry_content - ); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V59__add_party_identified_idx.sql b/base/src/main/resources/db/migration/V59__add_party_identified_idx.sql deleted file mode 100644 index e205a98f2..000000000 --- a/base/src/main/resources/db/migration/V59__add_party_identified_idx.sql +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -CREATE INDEX party_identified_party_type_idx ON ehr.party_identified(party_type, name); - -CREATE INDEX party_identified_party_ref_idx ON ehr.party_identified(party_ref_namespace, party_ref_scheme, party_ref_value); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V5__raw_json_encoding.sql b/base/src/main/resources/db/migration/V5__raw_json_encoding.sql deleted file mode 100644 index 4e1df5884..000000000 --- a/base/src/main/resources/db/migration/V5__raw_json_encoding.sql +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- archetyped.sql -CREATE OR REPLACE FUNCTION ehr.js_archetyped(TEXT, TEXT) - RETURNS JSON AS - $$ - DECLARE - archetype_id ALIAS FOR $1; - template_id ALIAS FOR $2; - BEGIN - RETURN - json_build_object( - '@class', 'ARCHETYPED', - 'archetype_id', - json_build_object( - '@class', 'ARCHETYPE_ID', - 'value', archetype_id - ), - template_id, - json_build_object( - '@class', 'TEMPLATE_ID', - 'value', template_id - ), - 'rm_version', '1.0.1' - ); - END - $$ -LANGUAGE plpgsql; - ---code_phrase.sql -CREATE OR REPLACE FUNCTION ehr.js_code_phrase(TEXT, TEXT) - RETURNS JSON AS -$$ -DECLARE - code_string ALIAS FOR $1; - terminology ALIAS FOR $2; -BEGIN - RETURN - json_build_object( - '@class', 'CODE_PHRASE', - 'terminology_id', - json_build_object( - '@class', 'TERMINOLOGY_ID', - 'value', terminology - ), - 'code_string', code_string - ); -END -$$ -LANGUAGE plpgsql; - ---context.sql -CREATE OR REPLACE FUNCTION ehr.js_context(UUID) - RETURNS JSON AS - $$ - DECLARE - context_id ALIAS FOR $1; - json_context_query TEXT; - json_context JSON; - v_start_time TIMESTAMP; - v_start_time_tzid TEXT; - v_end_time TIMESTAMP; - v_end_time_tzid TEXT; - v_facility UUID; - v_location TEXT; - v_other_context JSONB; - v_other_context_text TEXT; - v_setting UUID; - BEGIN - - IF (context_id IS NULL) - THEN - RETURN NULL; - END IF; - - -- build the query - SELECT - start_time, - start_time_tzid, - end_time, - end_time_tzid, - facility, - location, - other_context, - setting - FROM ehr.event_context - WHERE id = context_id - INTO v_start_time, v_start_time_tzid, v_end_time, v_end_time_tzid, v_facility, v_location, v_other_context, v_setting; - - json_context_query := ' SELECT json_build_object( - ''@class'', ''EVENT_CONTEXT'', - ''start_time'', ehr.js_dv_date_time(''' || v_start_time || ''',''' || - v_start_time_tzid || '''),'; - - IF (v_end_time IS NOT NULL) - THEN - json_context_query := - json_context_query || '''end_date'', ehr.js_dv_date_time(''' || v_end_time || ''',''' || v_end_time_tzid || - '''),'; - END IF; - - IF (v_location IS NOT NULL) - THEN - json_context_query := json_context_query || '''location'', ''' || v_location || ''','; - END IF; - - IF (v_facility IS NOT NULL) - THEN - json_context_query := json_context_query || '''health_care_facility'', ehr.js_party('''||v_facility||'''),'; - END IF; - - json_context_query := json_context_query || '''setting'',ehr.js_context_setting(''' || v_setting || '''))'; - - - -- IF (participation IS NOT NULL) THEN - -- json_context_query := json_context_query || '''participation'', participation,'; - -- END IF; - - IF (json_context_query IS NULL) - THEN - RETURN NULL; - END IF; - - EXECUTE json_context_query - INTO STRICT json_context; - - IF (v_other_context IS NOT NULL) - THEN - -- v_other_context_text := regexp_replace(v_other_context::TEXT, '''', '''''', 'g'); - json_context := jsonb_insert( - json_context::JSONB, - '{other_context}', v_other_context::JSONB->'/context/other_context[at0001]' - ); - END IF; - - RETURN json_context; - END - $$ -LANGUAGE plpgsql; - --- context_setting.sql -CREATE OR REPLACE FUNCTION ehr.js_context_setting(UUID) - RETURNS JSON AS - $$ - DECLARE - concept_id ALIAS FOR $1; - BEGIN - - IF (concept_id IS NULL) THEN - RETURN NULL; - END IF; - - RETURN ( - SELECT ehr.js_dv_coded_text(description, ehr.js_code_phrase(conceptid :: TEXT, 'openehr')) - FROM ehr.concept - WHERE id = concept_id AND language = 'en' - ); - END - $$ -LANGUAGE plpgsql; - --- dv_coded_text.sql -CREATE OR REPLACE FUNCTION ehr.js_dv_coded_text(TEXT, JSON) - RETURNS JSON AS -$$ -DECLARE - value_string ALIAS FOR $1; - code_phrase ALIAS FOR $2; -BEGIN - RETURN - json_build_object( - '@class', 'DV_CODED_TEXT', - 'value', value_string, - 'defining_code', code_phrase - ); -END -$$ -LANGUAGE plpgsql; - --- dv_date_time.sql -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMPTZ, TEXT) - RETURNS JSON AS - $$ - DECLARE - date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; - BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; - END IF; - - IF (time_zone IS NULL) - THEN - time_zone := 'UTC'; - END IF; - - RETURN - json_build_object( - '@class', 'DV_DATE_TIME', - 'value', ehr.iso_timestamp(date_time)||time_zone - ); - END - $$ -LANGUAGE plpgsql; - --- dv_text.sql -CREATE OR REPLACE FUNCTION ehr.js_dv_text(TEXT) - RETURNS JSON AS -$$ -DECLARE - value_string ALIAS FOR $1; -BEGIN - RETURN - json_build_object( - '@class', 'DV_TEXT', - 'value', value_string - ); -END -$$ -LANGUAGE plpgsql; - --- iso_timestamp.sql -create or replace function ehr.iso_timestamp(timestamp with time zone) - returns varchar as $$ -select substring(xmlelement(name x, $1)::varchar from 4 for 19) -$$ language sql immutable; - --- json_composition_pg10.sql --- CTE enforces 1-to-1 entry-composition relationship since multiple entries can be --- associated to one composition. This is not supported at this stage. -CREATE OR REPLACE FUNCTION ehr.js_composition(UUID) - RETURNS JSON AS - $$ - DECLARE - composition_uuid ALIAS FOR $1; - BEGIN - RETURN ( - WITH composition_data AS ( - SELECT - composition.language as language, - composition.territory as territory, - composition.composer as composer, - event_context.id as context_id, - territory.twoletter as territory_code, - entry.template_id as template_id, - entry.archetype_id as archetype_id, - concept.conceptid as category_defining_code, - concept.description as category_description, - entry.entry as content - FROM ehr.composition - INNER JOIN ehr.entry ON entry.composition_id = composition.id - LEFT JOIN ehr.event_context ON event_context.composition_id = composition.id - LEFT JOIN ehr.territory ON territory.code = composition.territory - LEFT JOIN ehr.concept ON concept.id = entry.category - WHERE composition.id = composition_uuid - LIMIT 1 - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '@class', 'COMPOSITION', - 'language', ehr.js_code_phrase(language, 'ISO_639-1'), - 'territory', ehr.js_code_phrase(territory_code, 'ISO_3166-1'), - 'composer', ehr.js_party(composer), - 'category', - ehr.js_dv_coded_text(category_description, ehr.js_code_phrase(category_defining_code :: TEXT, 'openehr')), - 'context', ehr.js_context(context_id), - 'content', content - ) - ) - FROM composition_data - ); - END - $$ -LANGUAGE plpgsql; --- object_version_id.sql -CREATE OR REPLACE FUNCTION ehr.object_version_id(UUID, TEXT, INT) - RETURNS JSON AS -$$ -DECLARE - object_uuid ALIAS FOR $1; - object_host ALIAS FOR $2; - object_version ALIAS FOR $3; -BEGIN - RETURN - json_build_object( - '@class', 'OBJECT_VERSION_ID', - 'value', object_uuid::TEXT || '::' || object_host || '::' || object_version::TEXT - ); -END -$$ -LANGUAGE plpgsql; --- party.sql -CREATE OR REPLACE FUNCTION ehr.js_party(UUID) - RETURNS JSON AS -$$ -DECLARE - party_id ALIAS FOR $1; -BEGIN - RETURN ( - SELECT ehr.js_party_identified(name, - ehr.js_party_ref(party_ref_value, party_ref_scheme, party_ref_namespace, party_ref_type)) - FROM ehr.party_identified - WHERE id = party_id - ); -END -$$ -LANGUAGE plpgsql; --- party_identified.sql -CREATE OR REPLACE FUNCTION ehr.js_party_identified(TEXT, JSON) - RETURNS JSON AS -$$ -DECLARE - name_value ALIAS FOR $1; - external_ref ALIAS FOR $2; -BEGIN - IF (external_ref IS NOT NULL) THEN - RETURN - json_build_object( - '@class', 'PARTY_IDENTIFIED', - 'name', name_value, - 'external_ref', external_ref - ); - ELSE - RETURN - json_build_object( - '@class', 'PARTY_IDENTIFIED', - 'name', name_value - ); - END IF; -END -$$ -LANGUAGE plpgsql; --- party_ref.sql -CREATE OR REPLACE FUNCTION ehr.js_party_ref(TEXT, TEXT, TEXT, TEXT) - RETURNS JSON AS -$$ -DECLARE - id_value ALIAS FOR $1; - id_scheme ALIAS FOR $2; - namespace ALIAS FOR $3; - party_type ALIAS FOR $4; -BEGIN - - IF (id_value IS NULL AND id_scheme IS NULL AND namespace IS NULL AND party_type IS NULL) THEN - RETURN NULL; - ELSE - RETURN - json_build_object( - '@class', 'PARTY_REF', - 'id', - json_build_object( - '@class', 'GENERIC_ID', - 'value', id_value, - 'scheme', id_scheme - ), - 'namespace', namespace, - 'type', party_type - ); - END IF; -END -$$ -LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V60__add_indexes.sql b/base/src/main/resources/db/migration/V60__add_indexes.sql deleted file mode 100644 index 2aaee0ff6..000000000 --- a/base/src/main/resources/db/migration/V60__add_indexes.sql +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -CREATE UNIQUE INDEX CONCURRENTLY territory_code_index ON ehr.territory(code); -CREATE INDEX CONCURRENTLY context_participation_index ON ehr.participation(event_context); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V61__ehr_subject_id_indexing.sql b/base/src/main/resources/db/migration/V61__ehr_subject_id_indexing.sql deleted file mode 100644 index 7e8a05404..000000000 --- a/base/src/main/resources/db/migration/V61__ehr_subject_id_indexing.sql +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - --- Optimize query in the form: --- --- select distinct on ("/ehr_id/value") "alias_27994528"."/ehr_id/value" --- from ( --- select "ehr_join"."id" as "/ehr_id/value" --- from "ehr"."entry" --- right outer join "ehr"."composition" as "composition_join" --- on "composition_join"."id" = "ehr"."entry"."composition_id" --- right outer join "ehr"."ehr" as "ehr_join" --- on "ehr_join"."id" = "composition_join"."ehr_id" --- join "ehr"."status" as "status_join" --- on "status_join"."ehr_id" = "ehr_join"."id" --- join "ehr"."party_identified" as "subject_ref" --- on "subject_ref"."id" = "status_join"."party" --- where (jsonb_extract_path_text(cast("ehr"."js_party_ref"( --- "subject_ref"."party_ref_value", --- "subject_ref"."party_ref_scheme", --- "subject_ref"."party_ref_namespace", --- "subject_ref"."party_ref_type" --- ) as jsonb),'id','value') = '30123') --- ) as "alias_27994528" --- In the lack of proper indexing, the WHERE condition evaluation requires in a nested loop, with an index, it is --- done with a simple Bitmap index scan. This results in a > 10x performance optimization. --- NB. index can be applied only on IMMUTABLE function! - ---- --- FUNCTION: ehr.js_party_ref(text, text, text, text) - --- DROP FUNCTION ehr.js_party_ref(text, text, text, text); - -CREATE OR REPLACE FUNCTION ehr.js_party_ref( - text, - text, - text, - text) - RETURNS json - LANGUAGE 'plpgsql' - - COST 100 - IMMUTABLE -AS $BODY$ -DECLARE - id_value ALIAS FOR $1; - id_scheme ALIAS FOR $2; - namespace ALIAS FOR $3; - party_type ALIAS FOR $4; -BEGIN - - IF (id_value IS NULL AND id_scheme IS NULL AND namespace IS NULL AND party_type IS NULL) THEN - RETURN NULL; - ELSE - RETURN - json_build_object( - '_type', 'PARTY_REF', - 'id', - json_build_object( - '_type', 'GENERIC_ID', - 'value', id_value, - 'scheme', id_scheme - ), - 'namespace', namespace, - 'type', party_type - ); - END IF; -END -$BODY$; - --- ALTER FUNCTION ehr.js_party_ref(text, text, text, text) --- OWNER TO ehrbase; - --- create index -create index if not exists ehr_subject_id_index on ehr.party_identified(jsonb_extract_path_text(cast("ehr"."js_party_ref"( - ehr.party_identified.party_ref_value, - ehr.party_identified.party_ref_scheme, - ehr.party_identified.party_ref_namespace, - ehr.party_identified.party_ref_type - ) as jsonb),'id','value')) \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V62__add_entry_history_missing_columns.sql b/base/src/main/resources/db/migration/V62__add_entry_history_missing_columns.sql deleted file mode 100644 index cbe0241a7..000000000 --- a/base/src/main/resources/db/migration/V62__add_entry_history_missing_columns.sql +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -ALTER TABLE ehr.entry_history - ADD COLUMN rm_version TEXT; - -ALTER TABLE ehr.entry_history - ADD COLUMN name ehr.dv_coded_text; diff --git a/base/src/main/resources/db/migration/V63__add_missing_ehr_folder_fk.sql b/base/src/main/resources/db/migration/V63__add_missing_ehr_folder_fk.sql deleted file mode 100644 index c8c483c6a..000000000 --- a/base/src/main/resources/db/migration/V63__add_missing_ehr_folder_fk.sql +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -ALTER TABLE ehr.ehr - ADD CONSTRAINT ehr_directory_fkey - FOREIGN KEY (directory) - REFERENCES ehr.folder(id); - -CREATE UNIQUE INDEX ehr_folder_idx ON ehr.ehr(directory); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V64__delete_ehr.sql b/base/src/main/resources/db/migration/V64__delete_ehr.sql deleted file mode 100644 index e885d6341..000000000 --- a/base/src/main/resources/db/migration/V64__delete_ehr.sql +++ /dev/null @@ -1,167 +0,0 @@ --- Drop FK constraints on _history tables -ALTER TABLE ehr.composition_history - DROP CONSTRAINT composition_history_attestation_ref_fkey; -ALTER TABLE ehr.composition_history - DROP CONSTRAINT composition_history_has_audit_fkey; -ALTER TABLE ehr.folder_history - DROP CONSTRAINT folder_history_has_audit_fkey; -ALTER TABLE ehr.status_history - DROP CONSTRAINT status_history_attestation_ref_fkey; -ALTER TABLE ehr.status_history - DROP CONSTRAINT status_history_in_contribution_fkey; -ALTER TABLE ehr.status_history - DROP CONSTRAINT status_history_has_audit_fkey; - --- Create missing indexes -CREATE INDEX IF NOT EXISTS attestation_reference_idx ON ehr.attestation (reference); -CREATE INDEX IF NOT EXISTS attested_view_attestation_idx ON ehr.attested_view (attestation_id); -CREATE INDEX IF NOT EXISTS compo_xref_child_idx ON ehr.compo_xref (child_uuid); -CREATE INDEX IF NOT EXISTS composition_history_ehr_idx ON ehr.composition_history (ehr_id); -CREATE INDEX IF NOT EXISTS contribution_ehr_idx ON ehr.contribution (ehr_id); -CREATE INDEX IF NOT EXISTS entry_history_composition_idx ON ehr.entry_history (composition_id); -CREATE INDEX IF NOT EXISTS event_context_history_composition_idx ON ehr.event_context_history (composition_id); -CREATE INDEX IF NOT EXISTS folder_history_contribution_idx ON ehr.folder_history (in_contribution); -CREATE INDEX IF NOT EXISTS folder_items_contribution_idx ON ehr.folder_items (in_contribution); -CREATE INDEX IF NOT EXISTS folder_items_history_contribution_idx ON ehr.folder_items_history (in_contribution); -CREATE INDEX IF NOT EXISTS folder_hierarchy_history_contribution_idx ON ehr.folder_hierarchy_history (in_contribution); -CREATE INDEX IF NOT EXISTS object_ref_history_contribution_idx ON ehr.object_ref_history (in_contribution); -CREATE INDEX IF NOT EXISTS participation_history_event_context_idx ON ehr.participation_history (event_context); -CREATE INDEX IF NOT EXISTS status_history_ehr_idx ON ehr.status_history (ehr_id); - --- Delete complete EHR function -CREATE OR REPLACE FUNCTION ehr.admin_delete_ehr_full(ehr_id_param UUID) - RETURNS TABLE - ( - deleted boolean - ) -AS -$$ -BEGIN - -- Disable versioning triggers - ALTER TABLE ehr.composition - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_hierarchy - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_items - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.object_ref - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - DISABLE TRIGGER versioning_trigger; - - RETURN QUERY WITH - -- Query IDs - select_composition_ids - AS (SELECT id FROM ehr.composition WHERE ehr_id = ehr_id_param), - select_contribution_ids - AS (SELECT id FROM ehr.contribution WHERE ehr_id = ehr_id_param), - - -- Delete data - - -- ON DELETE CASCADE: - -- * ehr.attested_view - -- * ehr.entry - -- * ehr.event_context - -- * ehr.folder_hierarchy - -- * ehr.folder_items - -- * ehr.object_ref - -- * ehr.participation - - delete_compo_xref - AS (DELETE FROM ehr.compo_xref cx USING select_composition_ids sci WHERE cx.master_uuid = sci.id OR cx.child_uuid = sci.id), - delete_composition - AS (DELETE FROM ehr.composition WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - delete_status - AS (DELETE FROM ehr.status WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - select_attestation_ids AS (SELECT id - FROM ehr.attestation - WHERE reference IN - (SELECT attestation_ref FROM delete_composition) - OR reference IN (SELECT attestation_ref FROM delete_status)), - delete_attestation - AS (DELETE FROM ehr.attestation a USING select_attestation_ids sa WHERE a.id = sa.id RETURNING a.reference, a.has_audit), - delete_attestation_ref - AS (DELETE FROM ehr.attestation_ref ar USING delete_attestation da WHERE ar.ref = da.reference), - delete_folder_items - AS (DELETE FROM ehr.folder_items fi USING select_contribution_ids sci WHERE fi.in_contribution = sci.id), - delete_folder_hierarchy - AS (DELETE FROM ehr.folder_hierarchy fh USING select_contribution_ids sci WHERE fh.in_contribution = sci.id), - delete_folder - AS (DELETE FROM ehr.folder f USING select_contribution_ids sci WHERE f.in_contribution = sci.id RETURNING f.id, f.has_audit), - delete_contribution - AS (DELETE FROM ehr.contribution c WHERE c.ehr_id = ehr_id_param RETURNING c.id, c.has_audit), - delete_ehr - AS (DELETE FROM ehr.ehr e WHERE e.id = ehr_id_param RETURNING e.access), - delete_access - AS (DELETE FROM ehr.access a USING delete_ehr de WHERE a.id = de.access), - - -- Delete _history - delete_composition_history - AS (DELETE FROM ehr.composition_history WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - delete_entry_history - AS (DELETE FROM ehr.entry_history eh USING delete_composition_history dch WHERE eh.composition_id = dch.id), - delete_event_context_hisotry - AS (DELETE FROM ehr.event_context_history ech USING delete_composition_history dch WHERE ech.composition_id = dch.id RETURNING ech.id), - delete_folder_history - AS (DELETE FROM ehr.folder_history fh USING select_contribution_ids sc WHERE fh.in_contribution = sc.id RETURNING fh.id, fh.has_audit), - delete_folder_items_history - AS (DELETE FROM ehr.folder_items_history fih USING select_contribution_ids sc WHERE fih.in_contribution = sc.id), - delete_folder_hierarchy_history - AS (DELETE FROM ehr.folder_hierarchy_history fhh USING select_contribution_ids sc WHERE fhh.in_contribution = sc.id), - delete_participation_history - AS (DELETE FROM ehr.participation_history ph USING delete_event_context_hisotry dech WHERE ph.event_context = dech.id), - object_ref_history - AS (DELETE FROM ehr.object_ref_history orh USING select_contribution_ids sc WHERE orh.in_contribution = sc.id), - delete_status_history - AS (DELETE FROM ehr.status_history WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - - -- Delete audit_details - delete_composition_audit - AS (DELETE FROM ehr.audit_details ad USING delete_composition dc WHERE ad.id = dc.has_audit), - delete_status_audit - AS (DELETE FROM ehr.audit_details ad USING delete_status ds WHERE ad.id = ds.has_audit), - delete_attestation_audit - AS (DELETE FROM ehr.audit_details ad USING delete_attestation da WHERE ad.id = da.has_audit), - delete_folder_audit - AS (DELETE FROM ehr.audit_details ad USING delete_folder df WHERE ad.id = df.has_audit), - delete_contribution_audit - AS (DELETE FROM ehr.audit_details ad USING delete_contribution dc WHERE ad.id = dc.has_audit), - delete_composition_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_composition_history dch WHERE ad.id = dch.has_audit), - delete_status_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_status_history dsh WHERE ad.id = dsh.has_audit), - delete_folder_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_folder_history dfh WHERE ad.id = dfh.has_audit) - - SELECT true; - - -- Restore versioning triggers - ALTER TABLE ehr.composition - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_hierarchy - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_items - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.object_ref - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - ENABLE TRIGGER versioning_trigger; -END -$$ - LANGUAGE plpgsql; diff --git a/base/src/main/resources/db/migration/V65__fix_dv_date_time_function.sql b/base/src/main/resources/db/migration/V65__fix_dv_date_time_function.sql deleted file mode 100644 index d26096c5c..000000000 --- a/base/src/main/resources/db/migration/V65__fix_dv_date_time_function.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Removes 'Z' when timezone is NULL - -CREATE OR REPLACE FUNCTION ehr.js_dv_date_time(TIMESTAMP, TEXT) - RETURNS JSON AS -$$ -DECLARE -date_time ALIAS FOR $1; - time_zone ALIAS FOR $2; -BEGIN - - IF (date_time IS NULL) - THEN - RETURN NULL; -END IF; - - IF (time_zone IS NULL) - THEN - time_zone := ''; -END IF; - -RETURN - json_build_object( - '_type', 'DV_DATE_TIME', - 'value',to_char(date_time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"'||time_zone||'"') - ); -END -$$ -LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V66__fix_missing_wrong_status_uid.sql b/base/src/main/resources/db/migration/V66__fix_missing_wrong_status_uid.sql deleted file mode 100644 index 6f6ef61db..000000000 --- a/base/src/main/resources/db/migration/V66__fix_missing_wrong_status_uid.sql +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH and Christian Chevalley (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ --- added missing server_id -CREATE OR REPLACE FUNCTION ehr.js_ehr(UUID, TEXT) - RETURNS JSON AS -$$ -DECLARE - ehr_uuid ALIAS FOR $1; - server_id ALIAS FOR $2; - contribution_json_array JSONB[]; - contribution_details JSONB; - composition_version_json_array JSONB[]; - composition_in_ehr_id RECORD; - folder_version_json_array JSONB[]; - folder_in_ehr_id RECORD; -BEGIN - - FOR contribution_details IN (SELECT ehr.js_contribution(contribution.id, server_id) - FROM ehr.contribution - WHERE contribution.ehr_id = ehr_uuid AND contribution.contribution_type != 'ehr') - LOOP - contribution_json_array := array_append(contribution_json_array, contribution_details); - END LOOP; - - FOR composition_in_ehr_id IN (SELECT composition.id, composition.sys_transaction - FROM ehr.composition - WHERE composition.ehr_id = ehr_uuid) - LOOP - composition_version_json_array := array_append( - composition_version_json_array, - jsonb_build_object( - '_type', 'VERSIONED_COMPOSITION', - 'id', ehr.js_object_version_id(ehr.composition_uid(composition_in_ehr_id.id, server_id)), - 'time_created', ehr.js_dv_date_time(composition_in_ehr_id.sys_transaction, 'Z') - ) - ); - END LOOP; - - FOR folder_in_ehr_id IN (SELECT folder.id, folder.sys_transaction - FROM ehr.folder - JOIN ehr.contribution ON folder.in_contribution = contribution.id - WHERE contribution.ehr_id = ehr_uuid) - LOOP - folder_version_json_array := array_append( - folder_version_json_array, - ehr.js_folder(folder_in_ehr_id.id, server_id) - ); - END LOOP; - - RETURN ( - WITH ehr_data AS ( - SELECT - ehr.id as ehr_id, - ehr.date_created as date_created, - ehr.date_created_tzid as date_created_tz, - ehr.access as access, - system.settings as system_value, - ehr.directory as directory - FROM ehr.ehr - JOIN ehr.system ON system.id = ehr.system_id - WHERE ehr.id = ehr_uuid - ) - SELECT - jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR', - 'ehr_id', ehr.js_canonical_hier_object_id(ehr_data.ehr_id), - 'system_id', ehr.js_canonical_hier_object_id(ehr_data.system_value), - 'ehr_status', ehr.js_ehr_status(ehr_data.ehr_id, server_id), - 'time_created', ehr.js_dv_date_time(ehr_data.date_created, ehr_data.date_created_tz), - 'contributions', contribution_json_array, - 'compositions', composition_version_json_array, - 'folders', folder_version_json_array, - 'directory', ehr.js_folder(directory, server_id) - ) - -- 'ehr_access' - -- 'tags' - ) - - FROM ehr_data - ); -END -$$ - LANGUAGE plpgsql; - --- use the status id (was ehr_id!) -CREATE OR REPLACE FUNCTION ehr.ehr_status_uid(ehr_uuid UUID, server_id TEXT) - RETURNS TEXT AS -$$ -BEGIN - RETURN (select "status"."id"||'::'||server_id||'::'||1 - + COALESCE( - (select count(*) - from "ehr"."status_history" - where "status_history"."ehr_id" = ehr_uuid - group by "ehr"."status_history"."ehr_id") - , 0) - from ehr.status - where status.ehr_id = ehr_uuid); -END -$$ - LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V67__add_missing_index.sql b/base/src/main/resources/db/migration/V67__add_missing_index.sql deleted file mode 100644 index 30afcc480..000000000 --- a/base/src/main/resources/db/migration/V67__add_missing_index.sql +++ /dev/null @@ -1,24 +0,0 @@ --- --- Copyright 2022 vitasystems GmbH and Hannover Medical School. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- https://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -CREATE INDEX ehr_concept_id_language_idx ON ehr.concept(conceptid, language); -CREATE INDEX ehr_identifier_party_idx ON ehr.identifier(party); - -CREATE UNIQUE INDEX ehr_territory_twoletter_idx ON ehr.territory(twoletter); -CREATE UNIQUE INDEX ehr_system_settings_idx ON ehr.system(settings); - -DROP INDEX entry_composition_id_idx; -CREATE UNIQUE INDEX entry_composition_id_idx on ehr.entry(composition_id); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V68__add_unique_index_event_context.sql b/base/src/main/resources/db/migration/V68__add_unique_index_event_context.sql deleted file mode 100644 index 26ca98603..000000000 --- a/base/src/main/resources/db/migration/V68__add_unique_index_event_context.sql +++ /dev/null @@ -1,21 +0,0 @@ --- --- Copyright 2022 vitasystems GmbH and Hannover Medical School. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- https://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -DROP INDEX ehr.context_composition_id_idx; -CREATE UNIQUE INDEX context_composition_id_idx ON ehr.event_context(composition_id); - -DROP INDEX ehr.status_ehr_idx; -CREATE UNIQUE INDEX status_ehr_idx ON ehr.status(ehr_id); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V69__add_unique_constraints.sql b/base/src/main/resources/db/migration/V69__add_unique_constraints.sql deleted file mode 100644 index 8fbb002af..000000000 --- a/base/src/main/resources/db/migration/V69__add_unique_constraints.sql +++ /dev/null @@ -1,21 +0,0 @@ --- --- Copyright 2022 vitasystems GmbH and Hannover Medical School. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- https://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -ALTER TABLE ehr.entry - ADD CONSTRAINT entry_composition_id_key UNIQUE USING INDEX entry_composition_id_idx; - -ALTER TABLE ehr.status - ADD CONSTRAINT status_ehr_id_key UNIQUE USING INDEX status_ehr_idx; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V6__aql_v1_2_0.sql b/base/src/main/resources/db/migration/V6__aql_v1_2_0.sql deleted file mode 100644 index ede2cc098..000000000 --- a/base/src/main/resources/db/migration/V6__aql_v1_2_0.sql +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -CREATE OR REPLACE FUNCTION ehr.aql_node_name_predicate(entry JSONB, name_value_predicate TEXT, jsonb_path TEXT) - RETURNS JSONB AS - $$ - DECLARE - entry_segment JSONB; - jsquery_node_expression TEXT; - subnode JSONB; - BEGIN - - -- get the segment for the predicate - - SELECT jsonb_extract_path(entry, VARIADIC string_to_array(jsonb_path, ',')) INTO STRICT entry_segment; - - IF (entry_segment IS NULL) THEN - RETURN NULL ; - END IF ; - - -- identify structure with name/value matching argument - IF (jsonb_typeof(entry_segment) <> 'array') THEN - RETURN NULL; - END IF; - - FOR subnode IN SELECT jsonb_array_elements(entry_segment) - LOOP - IF ((subnode #>> '{/name,0,value}') = name_value_predicate) THEN - RETURN subnode; - END IF; - END LOOP; - - RETURN NULL; - - END - $$ -LANGUAGE plpgsql; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V70__add_index_to_party_identified.sql b/base/src/main/resources/db/migration/V70__add_index_to_party_identified.sql deleted file mode 100644 index 425cceda4..000000000 --- a/base/src/main/resources/db/migration/V70__add_index_to_party_identified.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX party_identified_namespace_value_idx ON party_identified(party_ref_namespace, party_ref_value); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V71__merge_duplicate_users.sql b/base/src/main/resources/db/migration/V71__merge_duplicate_users.sql deleted file mode 100644 index a321bbd83..000000000 --- a/base/src/main/resources/db/migration/V71__merge_duplicate_users.sql +++ /dev/null @@ -1,78 +0,0 @@ --- --- Copyright 2022 vitasystems GmbH and Hannover Medical School. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- https://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -create temporary table temp_replacements as -with user_identifier as ( - select * - from ehr.identifier i - where i.type_name = 'EHRbase Security Authentication User' -), -duplicate_user as ( - select i.id_value, - min(party::varchar)::uuid as replacement - from user_identifier i - group by i.id_value - having count(i.id_value) > 1 -) -select i.id_value, - i.party, - d.replacement -from user_identifier i -join duplicate_user d on (d.id_value = i.id_value and d.replacement != i.party); - -update ehr.audit_details -set committer = r.replacement -from temp_replacements r -where r.party = committer; - -delete -from ehr.identifier pi -where pi.party IN - (select r.party - from temp_replacements r); - ---temporarily remove foreign keys -alter table ehr.audit_details drop constraint audit_details_committer_fkey; -alter table ehr.identifier drop constraint identifier_party_fkey; -alter table ehr.composition drop constraint composition_composer_fkey; -alter table ehr.event_context drop constraint event_context_facility_fkey; -alter table ehr.participation drop constraint participation_performer_fkey; -alter table ehr.status drop constraint status_party_fkey; - -delete -from ehr.party_identified pi -where pi.id IN - (select r.party - from temp_replacements r); - -drop table temp_replacements; - -create index identifier_value_idx on ehr.identifier (id_value); - ---reinstate foreign keys -alter table ehr.audit_details add constraint audit_details_committer_fkey - foreign key (committer) references ehr.party_identified (id); -alter table ehr.identifier add constraint identifier_party_fkey - foreign key (party) references ehr.party_identified (id) - on delete cascade; -alter table ehr.composition add constraint composition_composer_fkey - foreign key (composer) references ehr.party_identified (id); -alter table ehr.event_context add constraint event_context_facility_fkey - foreign key (facility) references ehr.party_identified (id); -alter table ehr.participation add constraint participation_performer_fkey - foreign key (performer) references ehr.party_identified (id); -alter table ehr.status add constraint status_party_fkey - foreign key (party) references ehr.party_identified (id); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V72__add_key_value_store_for_plugins.sql b/base/src/main/resources/db/migration/V72__add_key_value_store_for_plugins.sql deleted file mode 100644 index 04e2230d9..000000000 --- a/base/src/main/resources/db/migration/V72__add_key_value_store_for_plugins.sql +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -CREATE TABLE ehr.plugin ( - id UUid PRIMARY KEY DEFAULT ext.uuid_generate_v4(), - pluginId TEXT NOT NULL, - key TEXT NOT NULL, - value TEXT -); - -COMMENT ON TABLE ehr.plugin IS 'key value store for plugin sub system'; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V73__add_tenantid_column.sql b/base/src/main/resources/db/migration/V73__add_tenantid_column.sql deleted file mode 100644 index bd0813290..000000000 --- a/base/src/main/resources/db/migration/V73__add_tenantid_column.sql +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- create tenant table - -CREATE TABLE tenant ( - id UUID primary key DEFAULT ext.uuid_generate_v4(), - tenant_id TEXT, - tenant_name TEXT -); - -ALTER TABLE ehr.tenant ADD UNIQUE (tenant_id); -ALTER TABLE ehr.tenant ADD UNIQUE (tenant_name); - -INSERT INTO ehr.tenant ( - tenant_id, - tenant_name -) VALUES ( - '1f332a66-0e57-11ed-861d-0242ac120002', - 'default_tenant' -); - - --- add namespace column to all non system tables - -ALTER TABLE ehr.access ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.attestation ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.attestation_ref ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.attested_view ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.audit_details ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.compo_xref ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; --- ALTER TABLE ehr.concept ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.contribution ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.folder ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.folder_hierarchy ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.folder_hierarchy_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.folder_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.folder_items ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.folder_items_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.heading ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.identifier ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; --- ALTER TABLE ehr.language ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.object_ref ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.object_ref_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.participation ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.participation_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.party_identified ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.status ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.status_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.stored_query ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.template_store ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.terminology_provider ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.session_log ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.ehr ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.entry ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.entry_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.composition ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.composition_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.event_context ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; -ALTER TABLE ehr.event_context_history ADD namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002'; - - --- change unique constraint on template_store - -ALTER TABLE ehr.template_store DROP CONSTRAINT template_store_template_id_key; -ALTER TABLE ehr.template_store DROP CONSTRAINT template_store_pkey, ADD PRIMARY KEY(id,template_id, namespace); - diff --git a/base/src/main/resources/db/migration/V74__add_rls_for_tenant.sql b/base/src/main/resources/db/migration/V74__add_rls_for_tenant.sql deleted file mode 100644 index 32891f9ac..000000000 --- a/base/src/main/resources/db/migration/V74__add_rls_for_tenant.sql +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- enable RLS - -ALTER TABLE ehr.access ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.attestation ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.attestation_ref ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.attested_view ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.audit_details ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.compo_xref ENABLE ROW LEVEL SECURITY; --- ALTER TABLE ehr.concept ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.contribution ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_hierarchy ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_hierarchy_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_items ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_items_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.heading ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.identifier ENABLE ROW LEVEL SECURITY; --- ALTER TABLE ehr.language ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.object_ref ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.object_ref_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.participation ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.participation_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.party_identified ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.status ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.status_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.stored_query ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.template_store ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.terminology_provider ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.session_log ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.ehr ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.entry ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.entry_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.composition ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.composition_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context_history ENABLE ROW LEVEL SECURITY; - -ALTER TABLE ehr.access FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.attestation FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.attestation_ref FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.attested_view FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.audit_details FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.compo_xref FORCE ROW LEVEL SECURITY; --- ALTER TABLE ehr.concept FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.contribution FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_hierarchy FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_hierarchy_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_items FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.folder_items_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.heading FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.identifier FORCE ROW LEVEL SECURITY; --- ALTER TABLE ehr.language FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.object_ref FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.object_ref_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.participation FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.participation_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.party_identified FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.status FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.status_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.stored_query FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.template_store FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.terminology_provider FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.session_log FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.ehr FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.entry FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.entry_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.composition FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.composition_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context_history FORCE ROW LEVEL SECURITY; - --- create policies - -CREATE POLICY ehr_policy_all ON ehr.access FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.attestation FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.attestation_ref FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.attested_view FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.audit_details FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.compo_xref FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); --- CREATE POLICY ehr_policy_all ON ehr.concept FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.contribution FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.folder FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.folder_hierarchy FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.folder_hierarchy_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.folder_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.folder_items FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.folder_items_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.heading FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.identifier FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); --- CREATE POLICY ehr_policy_all ON ehr.language FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.object_ref FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.object_ref_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.participation FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.participation_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.party_identified FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.status FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.status_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.stored_query FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.template_store FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.terminology_provider FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.session_log FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.ehr FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.entry FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.entry_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.composition FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.composition_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.event_context FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); -CREATE POLICY ehr_policy_all ON ehr.event_context_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); - diff --git a/base/src/main/resources/db/migration/V75_1__add_tenantId_to_index.sql b/base/src/main/resources/db/migration/V75_1__add_tenantId_to_index.sql deleted file mode 100644 index fb37fb176..000000000 --- a/base/src/main/resources/db/migration/V75_1__add_tenantId_to_index.sql +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- V60 -CREATE INDEX CONCURRENTLY context_participation_index ON ehr.participation(event_context, namespace); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V75__add_tenantId_to_index.sql b/base/src/main/resources/db/migration/V75__add_tenantId_to_index.sql deleted file mode 100644 index 765395ff1..000000000 --- a/base/src/main/resources/db/migration/V75__add_tenantId_to_index.sql +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- drop all index - --- V2 -DROP INDEX ehr_status_history; -DROP INDEX ehr_composition_history; -DROP INDEX ehr_event_context_history; -DROP INDEX ehr_participation_history; -DROP INDEX ehr_entry_history; -DROP INDEX ehr_compo_xref; -DROP INDEX gin_entry_path_idx; -DROP INDEX template_entry_idx; -DROP INDEX IF EXISTS entry_composition_id_idx; -DROP INDEX composition_composer_idx; -DROP INDEX composition_ehr_idx; -DROP INDEX IF EXISTS status_ehr_idx; -DROP INDEX status_party_idx; -DROP INDEX context_facility_idx; -DROP INDEX context_composition_id_idx; -DROP INDEX context_setting_idx; - --- V8 -DROP INDEX folder_in_contribution_idx; -DROP INDEX folder_hierarchy_in_contribution_idx; -DROP INDEX fki_folder_hierarchy_parent_fk; -DROP INDEX obj_ref_in_contribution_idx; -DROP INDEX folder_hist_idx; - --- V59 -DROP INDEX party_identified_party_type_idx; -DROP INDEX party_identified_party_ref_idx; - --- V60 --- DROP INDEX territory_code_index; -DROP INDEX context_participation_index; - --- V61 -DROP INDEX ehr_subject_id_index; - --- V63 -DROP INDEX ehr_folder_idx; - --- V64 -DROP INDEX attestation_reference_idx; -DROP INDEX attested_view_attestation_idx; -DROP INDEX compo_xref_child_idx; -DROP INDEX composition_history_ehr_idx; -DROP INDEX contribution_ehr_idx; -DROP INDEX entry_history_composition_idx; -DROP INDEX event_context_history_composition_idx; -DROP INDEX folder_history_contribution_idx; -DROP INDEX folder_items_contribution_idx; -DROP INDEX folder_items_history_contribution_idx; -DROP INDEX folder_hierarchy_history_contribution_idx; -DROP INDEX object_ref_history_contribution_idx; -DROP INDEX participation_history_event_context_idx; -DROP INDEX status_history_ehr_idx; - --- V67 -DROP INDEX ehr_identifier_party_idx; - --- V69 -ALTER TABLE ehr.entry DROP CONSTRAINT entry_composition_id_key; -ALTER TABLE ehr.status DROP CONSTRAINT status_ehr_id_key; - --- V70 -DROP INDEX party_identified_namespace_value_idx; - --- V71 -DROP INDEX identifier_value_idx; - --- create all idex again - --- V2 -CREATE INDEX ehr_status_history ON ehr.status_history USING BTREE (id, namespace); -CREATE INDEX ehr_composition_history ON ehr.composition_history USING BTREE (id, namespace); -CREATE INDEX ehr_event_context_history ON ehr.event_context_history USING BTREE (id, namespace); -CREATE INDEX ehr_participation_history ON ehr.participation_history USING BTREE (id, namespace); -CREATE INDEX ehr_entry_history ON ehr.entry_history USING BTREE (id, namespace); -CREATE INDEX ehr_compo_xref ON ehr.compo_xref USING BTREE (master_uuid, namespace); - -CREATE INDEX template_entry_idx ON ehr.entry (template_id, namespace); -CREATE INDEX composition_composer_idx ON ehr.composition (composer, namespace); -CREATE INDEX composition_ehr_idx ON ehr.composition (ehr_id, namespace); -CREATE INDEX status_party_idx ON ehr.status (party, namespace); -CREATE INDEX context_facility_idx ON ehr.event_context (facility, namespace); -CREATE INDEX context_setting_idx ON ehr.event_context (setting, namespace); - --- V8 -CREATE INDEX folder_in_contribution_idx ON ehr.folder USING btree (in_contribution, namespace) TABLESPACE pg_default; -CREATE INDEX folder_hierarchy_in_contribution_idx ON ehr.folder_hierarchy USING btree (in_contribution, namespace) TABLESPACE pg_default; -CREATE INDEX fki_folder_hierarchy_parent_fk ON ehr.folder_hierarchy USING btree (parent_folder, namespace) TABLESPACE pg_default; -CREATE INDEX obj_ref_in_contribution_idx ON ehr.object_ref USING btree (in_contribution, namespace) TABLESPACE pg_default; -CREATE INDEX folder_hist_idx ON ehr.folder_items_history USING btree (folder_id, object_ref_id, in_contribution, namespace) TABLESPACE pg_default; - --- V59 -CREATE INDEX party_identified_party_type_idx ON ehr.party_identified(party_type, name, namespace); -CREATE INDEX party_identified_party_ref_idx ON ehr.party_identified(party_ref_namespace, party_ref_scheme, party_ref_value, namespace); - --- V61 -CREATE INDEX IF NOT EXISTS ehr_subject_id_index ON ehr.party_identified( - jsonb_extract_path_text(cast("ehr"."js_party_ref"( - ehr.party_identified.party_ref_value, - ehr.party_identified.party_ref_scheme, - ehr.party_identified.party_ref_namespace, - ehr.party_identified.party_ref_type - ) as jsonb),'id','value') - , namespace -); - --- V63 -CREATE UNIQUE INDEX ehr_folder_idx ON ehr.ehr(directory, namespace); - --- V64 -CREATE INDEX IF NOT EXISTS attestation_reference_idx ON ehr.attestation (reference, namespace); -CREATE INDEX IF NOT EXISTS attested_view_attestation_idx ON ehr.attested_view (attestation_id, namespace); -CREATE INDEX IF NOT EXISTS compo_xref_child_idx ON ehr.compo_xref (child_uuid, namespace); -CREATE INDEX IF NOT EXISTS composition_history_ehr_idx ON ehr.composition_history (ehr_id, namespace); -CREATE INDEX IF NOT EXISTS contribution_ehr_idx ON ehr.contribution (ehr_id, namespace); -CREATE INDEX IF NOT EXISTS entry_history_composition_idx ON ehr.entry_history (composition_id, namespace); -CREATE INDEX IF NOT EXISTS event_context_history_composition_idx ON ehr.event_context_history (composition_id, namespace); -CREATE INDEX IF NOT EXISTS folder_history_contribution_idx ON ehr.folder_history (in_contribution, namespace); -CREATE INDEX IF NOT EXISTS folder_items_contribution_idx ON ehr.folder_items (in_contribution, namespace); -CREATE INDEX IF NOT EXISTS folder_items_history_contribution_idx ON ehr.folder_items_history (in_contribution, namespace); -CREATE INDEX IF NOT EXISTS folder_hierarchy_history_contribution_idx ON ehr.folder_hierarchy_history (in_contribution, namespace); -CREATE INDEX IF NOT EXISTS object_ref_history_contribution_idx ON ehr.object_ref_history (in_contribution, namespace); -CREATE INDEX IF NOT EXISTS participation_history_event_context_idx ON ehr.participation_history (event_context, namespace); -CREATE INDEX IF NOT EXISTS status_history_ehr_idx ON ehr.status_history (ehr_id, namespace); - --- V67 -CREATE INDEX ehr_identifier_party_idx ON ehr.identifier(party, namespace); -CREATE UNIQUE INDEX entry_composition_id_idx on ehr.entry(composition_id, namespace); - --- V68 -CREATE UNIQUE INDEX context_composition_id_idx ON ehr.event_context(composition_id, namespace); -CREATE UNIQUE INDEX status_ehr_idx ON ehr.status(ehr_id, namespace); - --- V69 -ALTER TABLE ehr.entry ADD CONSTRAINT entry_composition_id_key UNIQUE USING INDEX entry_composition_id_idx; -ALTER TABLE ehr.status ADD CONSTRAINT status_ehr_id_key UNIQUE USING INDEX status_ehr_idx; - --- V70 -CREATE INDEX party_identified_namespace_value_idx ON party_identified(party_ref_namespace, party_ref_value, namespace); - --- V71 -create index identifier_value_idx on ehr.identifier (id_value, namespace); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V76__move_deleted_participations_to_history.sql b/base/src/main/resources/db/migration/V76__move_deleted_participations_to_history.sql deleted file mode 100644 index 997158d05..000000000 --- a/base/src/main/resources/db/migration/V76__move_deleted_participations_to_history.sql +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -ALTER TABLE ehr.participation DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.participation_history DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context DISABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context_history DISABLE ROW LEVEL SECURITY; --- delete all participations that do not match any currently active event context -> trigger will move them to history -DELETE FROM ehr.participation p1 - WHERE NOT EXISTS (SELECT ec.id - FROM ehr.event_context ec - WHERE p1.event_context=ec.id AND p1.sys_transaction = ec.sys_transaction); - --- remove all participation's from history that do not have an associated event_context_history row -/* This is the case if i.e. a plugin was used to rollback a composition to a previous version, - leaving orphans in the participation_history table after moving them there from the participation table */ -DELETE FROM ehr.participation_history ph - WHERE NOT EXISTS(SELECT ech.id - FROM ehr.event_context_history ech - WHERE ech.id=ph.event_context AND ech.sys_transaction=ph.sys_transaction); - --- set the correct sys_period for all participation_history rows -UPDATE ehr.participation_history ph -SET sys_period = (SELECT ech.sys_period - FROM ehr.event_context_history ech - WHERE ech.id=ph.event_context AND ech.sys_transaction=ph.sys_transaction); - -ALTER TABLE ehr.participation ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.participation_history ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context ENABLE ROW LEVEL SECURITY; -ALTER TABLE ehr.event_context_history ENABLE ROW LEVEL SECURITY; diff --git a/base/src/main/resources/db/migration/V77__stored_query_without_build_metadata.sql b/base/src/main/resources/db/migration/V77__stored_query_without_build_metadata.sql deleted file mode 100644 index 7e9b9ee01..000000000 --- a/base/src/main/resources/db/migration/V77__stored_query_without_build_metadata.sql +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- prohibit build metadata for semver - -ALTER TABLE ehr.stored_query - DISABLE ROW LEVEL SECURITY; - --- remove build metadata -UPDATE ehr.stored_query -SET semver = regexp_replace(semver, '^([^+]+)\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)$', '\1') -WHERE semver ~ '\+'; - -ALTER TABLE ehr.stored_query - DROP CONSTRAINT stored_query_semver_check; --- prohibit build metadata -ALTER TABLE ehr.stored_query - ADD CONSTRAINT stored_query_semver_check - CHECK (semver ~* - '^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?$'); - -ALTER TABLE ehr.stored_query - ENABLE ROW LEVEL SECURITY; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V78__add_property_column_to_tenant_table.sql b/base/src/main/resources/db/migration/V78__add_property_column_to_tenant_table.sql deleted file mode 100644 index 92991f92a..000000000 --- a/base/src/main/resources/db/migration/V78__add_property_column_to_tenant_table.sql +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -ALTER TABLE ehr.tenant ADD tenant_properties JSON; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V79__admin_function.sql b/base/src/main/resources/db/migration/V79__admin_function.sql deleted file mode 100644 index d4649ded2..000000000 --- a/base/src/main/resources/db/migration/V79__admin_function.sql +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School. - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -create or replace function admin_delete_ehr_full(ehr_id_param uuid) - returns TABLE(deleted boolean) - language plpgsql - security definer - SET search_path = ehr, pg_temp -as -$$ -BEGIN - -- Disable versioning triggers - ALTER TABLE ehr.composition - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_hierarchy - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_items - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.object_ref - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - DISABLE TRIGGER versioning_trigger; - - RETURN QUERY WITH - -- Query IDs - select_composition_ids - AS (SELECT id FROM ehr.composition WHERE ehr_id = ehr_id_param), - select_contribution_ids - AS (SELECT id FROM ehr.contribution WHERE ehr_id = ehr_id_param), - - -- Delete data - - -- ON DELETE CASCADE: - -- * ehr.attested_view - -- * ehr.entry - -- * ehr.event_context - -- * ehr.folder_hierarchy - -- * ehr.folder_items - -- * ehr.object_ref - -- * ehr.participation - - delete_compo_xref - AS (DELETE FROM ehr.compo_xref cx USING select_composition_ids sci WHERE cx.master_uuid = sci.id OR cx.child_uuid = sci.id), - delete_composition - AS (DELETE FROM ehr.composition WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - delete_status - AS (DELETE FROM ehr.status WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - select_attestation_ids AS (SELECT id - FROM ehr.attestation - WHERE reference IN - (SELECT attestation_ref FROM delete_composition) - OR reference IN (SELECT attestation_ref FROM delete_status)), - delete_attestation - AS (DELETE FROM ehr.attestation a USING select_attestation_ids sa WHERE a.id = sa.id RETURNING a.reference, a.has_audit), - delete_attestation_ref - AS (DELETE FROM ehr.attestation_ref ar USING delete_attestation da WHERE ar.ref = da.reference), - delete_folder_items - AS (DELETE FROM ehr.folder_items fi USING select_contribution_ids sci WHERE fi.in_contribution = sci.id), - delete_folder_hierarchy - AS (DELETE FROM ehr.folder_hierarchy fh USING select_contribution_ids sci WHERE fh.in_contribution = sci.id), - delete_folder - AS (DELETE FROM ehr.folder f USING select_contribution_ids sci WHERE f.in_contribution = sci.id RETURNING f.id, f.has_audit), - delete_contribution - AS (DELETE FROM ehr.contribution c WHERE c.ehr_id = ehr_id_param RETURNING c.id, c.has_audit), - delete_ehr - AS (DELETE FROM ehr.ehr e WHERE e.id = ehr_id_param RETURNING e.access), - delete_access - AS (DELETE FROM ehr.access a USING delete_ehr de WHERE a.id = de.access), - - -- Delete _history - delete_composition_history - AS (DELETE FROM ehr.composition_history WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - delete_entry_history - AS (DELETE FROM ehr.entry_history eh USING delete_composition_history dch WHERE eh.composition_id = dch.id), - delete_event_context_hisotry - AS (DELETE FROM ehr.event_context_history ech USING delete_composition_history dch WHERE ech.composition_id = dch.id RETURNING ech.id), - delete_folder_history - AS (DELETE FROM ehr.folder_history fh USING select_contribution_ids sc WHERE fh.in_contribution = sc.id RETURNING fh.id, fh.has_audit), - delete_folder_items_history - AS (DELETE FROM ehr.folder_items_history fih USING select_contribution_ids sc WHERE fih.in_contribution = sc.id), - delete_folder_hierarchy_history - AS (DELETE FROM ehr.folder_hierarchy_history fhh USING select_contribution_ids sc WHERE fhh.in_contribution = sc.id), - delete_participation_history - AS (DELETE FROM ehr.participation_history ph USING delete_event_context_hisotry dech WHERE ph.event_context = dech.id), - object_ref_history - AS (DELETE FROM ehr.object_ref_history orh USING select_contribution_ids sc WHERE orh.in_contribution = sc.id), - delete_status_history - AS (DELETE FROM ehr.status_history WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - - -- Delete audit_details - delete_composition_audit - AS (DELETE FROM ehr.audit_details ad USING delete_composition dc WHERE ad.id = dc.has_audit), - delete_status_audit - AS (DELETE FROM ehr.audit_details ad USING delete_status ds WHERE ad.id = ds.has_audit), - delete_attestation_audit - AS (DELETE FROM ehr.audit_details ad USING delete_attestation da WHERE ad.id = da.has_audit), - delete_folder_audit - AS (DELETE FROM ehr.audit_details ad USING delete_folder df WHERE ad.id = df.has_audit), - delete_contribution_audit - AS (DELETE FROM ehr.audit_details ad USING delete_contribution dc WHERE ad.id = dc.has_audit), - delete_composition_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_composition_history dch WHERE ad.id = dch.has_audit), - delete_status_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_status_history dsh WHERE ad.id = dsh.has_audit), - delete_folder_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_folder_history dfh WHERE ad.id = dfh.has_audit) - - SELECT true; - - -- Restore versioning triggers - ALTER TABLE ehr.composition - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_hierarchy - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.folder_items - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.object_ref - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - ENABLE TRIGGER versioning_trigger; -END -$$; - - -create or replace function admin_delete_event_context_for_compo(compo_id_input uuid) - returns TABLE(num integer, party uuid) - strict - language plpgsql - security definer - SET search_path = ehr, pg_temp -as -$$ -DECLARE - results RECORD; -BEGIN - -- since for this admin op, we don't want to generate a history record for each delete! - ALTER TABLE ehr.event_context DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation DISABLE TRIGGER versioning_trigger; - - RETURN QUERY WITH - linked_events(id) AS ( -- get linked EVENT_CONTEXT entities -- 0..1 - SELECT id, facility FROM ehr.event_context WHERE composition_id = compo_id_input - ), - linked_event_history(id) AS ( -- get linked EVENT_CONTEXT entities -- 0..1 - SELECT id, facility FROM ehr.event_context_history WHERE composition_id = compo_id_input - ), - linked_participations_for_events(id) AS ( -- get linked EVENT_CONTEXT entities -- for 0..1 events, each with * participations - SELECT id, performer FROM ehr.participation WHERE event_context IN (SELECT linked_events.id FROM linked_events) - ), - linked_participations_for_events_history(id) AS ( -- get linked EVENT_CONTEXT entities -- for 0..1 events, each with * participations - SELECT id, performer FROM ehr.participation_history WHERE event_context IN (SELECT linked_event_history.id FROM linked_event_history) - ), - parties(id) AS ( - SELECT facility FROM linked_events - UNION - SELECT performer FROM linked_participations_for_events - ), - delete_participation AS ( - DELETE FROM ehr.participation WHERE ehr.participation.id IN (SELECT linked_participations_for_events.id FROM linked_participations_for_events) - ), - delete_participation_history AS ( - DELETE FROM ehr.participation_history WHERE ehr.participation_history.id IN (SELECT linked_participations_for_events_history.id FROM linked_participations_for_events_history) - ), - delete_event_contexts AS ( - DELETE FROM ehr.event_context WHERE ehr.event_context.id IN (SELECT linked_events.id FROM linked_events) - ), - delete_event_contexts_history AS ( - DELETE FROM ehr.event_context_history WHERE ehr.event_context_history.id IN (SELECT linked_event_history.id FROM linked_event_history) - ) - SELECT 1, parties.id FROM parties; - - -- logging: - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT b.id FROM ( - SELECT id, performer FROM ehr.participation - WHERE event_context IN (SELECT a.id FROM ( - SELECT id, facility FROM ehr.event_context WHERE composition_id = compo_id_input - ) AS a ) - ) AS b - ) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'PARTICIPATION', results.id, now(); - END LOOP; - - -- looping query is reconstructed from above CTEs, because they can't be reused here - FOR results IN ( - SELECT id, facility - FROM ehr.event_context - WHERE composition_id = compo_id_input) - LOOP - RAISE NOTICE 'Admin deletion - Type: % - ID: % - Time: %', 'EVENT_CONTEXT', results.id, now(); - END LOOP; - - -- restore disabled triggers - ALTER TABLE ehr.event_context ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation ENABLE TRIGGER versioning_trigger; - -END; -$$; - - - - - diff --git a/base/src/main/resources/db/migration/V7__meta_cache.sql b/base/src/main/resources/db/migration/V7__meta_cache.sql deleted file mode 100644 index 74f841bbc..000000000 --- a/base/src/main/resources/db/migration/V7__meta_cache.sql +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH and Hannover Medical School - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -alter table ehr.template add column if not exists introspect jsonb; -alter table ehr.template add column if not exists parsed_opt bytea; -alter table ehr.template add column if not exists visitor bytea; -alter table ehr.template add column if not exists crc BIGINT; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V80__new_folder_structure.sql b/base/src/main/resources/db/migration/V80__new_folder_structure.sql deleted file mode 100644 index 59f8e9970..000000000 --- a/base/src/main/resources/db/migration/V80__new_folder_structure.sql +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -DO -$$ - DECLARE - count_folder INTEGER; - count_folder_history INTEGER; - BEGIN - ALTER TABLE ehr.folder - DISABLE ROW LEVEL SECURITY; - ALTER TABLE ehr.folder_history - DISABLE ROW LEVEL SECURITY; - SELECT count(*) FROM ehr.folder INTO count_folder; - SELECT count(*) FROM ehr.folder_history INTO count_folder_history; - ALTER TABLE ehr.folder - ENABLE ROW LEVEL SECURITY; - ALTER TABLE ehr.folder_history - ENABLE ROW LEVEL SECURITY; - - IF count_folder != 0 or count_folder_history != 0 - THEN - RAISE EXCEPTION 'Systems with existing ehr directories cannot be migrated automatically; See UPDATING.md'; - END IF; - END -$$; - -create table ehr.ehr_folder -( - id uuid NOT NULL, - ehr_id uuid NOT NULL, - ehr_folders_idx int NOT NULL, - row_num int NOT NULL, - contribution_id uuid NOT NULL, - audit_id uuid NOT NULL, - archetype_node_id TEXT, - path TEXT[], - hierarchy_idx text collate "C" not null, - hierarchy_idx_cap text collate "C" not null, - hierarchy_idx_len int not null, - items uuid[], - fields jsonb, - namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002', - sys_version INT NOT NULL, - sys_period_lower timestamptz NOT NULL, - PRIMARY KEY (ehr_id, id), - FOREIGN KEY (ehr_id) REFERENCES ehr.ehr (id), - FOREIGN KEY (contribution_id) REFERENCES ehr.contribution (id), - FOREIGN KEY (audit_id) REFERENCES ehr.audit_details (id) -); - -create index folder2_path_idx ON ehr.ehr_folder USING btree ((path[2]), ehr_id); -create index archetype_node_idx ON ehr.ehr_folder USING btree (archetype_node_id, (path[2]), ehr_id); - -ALTER TABLE ehr.ehr_folder - ENABLE ROW LEVEL SECURITY; -CREATE POLICY ehr_policy_all ON ehr.ehr_folder FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); - -create table ehr.ehr_folder_history -( - id uuid, - ehr_id uuid NOT NULL, - ehr_folders_idx int NOT NULL, - row_num int NOT NULL, - contribution_id uuid NOT NULL, - audit_id uuid NOT NULL, - archetype_node_id TEXT, - path TEXT[], - hierarchy_idx text collate "C" not null, - hierarchy_idx_cap text collate "C" not null, - hierarchy_idx_len int not null, - items uuid[], - fields jsonb, - namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002', - sys_version INT NOT NULL, - sys_period_lower timestamptz NOT NULL, - sys_period_upper timestamptz, - sys_deleted boolean NOT NULL, - PRIMARY KEY (ehr_id, id, sys_version), - FOREIGN KEY (ehr_id) REFERENCES ehr.ehr (id), - FOREIGN KEY (contribution_id) REFERENCES ehr.contribution (id), - FOREIGN KEY (audit_id) REFERENCES ehr.audit_details (id) -); - -ALTER TABLE ehr.ehr_folder_history - ENABLE ROW LEVEL SECURITY; -CREATE POLICY ehr_policy_all ON ehr.ehr_folder_history FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); - --- Remove old folder structure - -alter table ehr.ehr - drop column if exists directory; -drop table ehr.folder, ehr.folder_hierarchy, ehr.folder_items,ehr.folder_history,ehr.folder_items_history,ehr.folder_hierarchy_history,ehr.object_ref, ehr.object_ref_history; - -drop function ehr.admin_delete_folder(folder_id_input uuid); -drop function ehr.admin_delete_folder_history(folder_id_input uuid); -drop function ehr.admin_delete_folder_obj_ref_history(contribution_id_input uuid); - -create or replace function ehr.admin_delete_ehr_full(ehr_id_param uuid) - returns TABLE - ( - deleted boolean - ) - language plpgsql - security definer - SET search_path = ehr, pg_temp -as -$$ -BEGIN - -- Disable versioning triggers - ALTER TABLE ehr.composition - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - DISABLE TRIGGER versioning_trigger; - - RETURN QUERY WITH - -- Query IDs - select_composition_ids - AS (SELECT id FROM ehr.composition WHERE ehr_id = ehr_id_param), - select_contribution_ids - AS (SELECT id FROM ehr.contribution WHERE ehr_id = ehr_id_param), - - -- Delete data - - -- ON DELETE CASCADE: - -- * ehr.attested_view - -- * ehr.entry - -- * ehr.event_context - -- * ehr.object_ref - -- * ehr.participation - -- ehr_folder will be deleted by the ehrbase backend - delete_compo_xref - AS (DELETE FROM ehr.compo_xref cx USING select_composition_ids sci WHERE cx.master_uuid = sci.id OR cx.child_uuid = sci.id), - delete_composition - AS (DELETE FROM ehr.composition WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - delete_status - AS (DELETE FROM ehr.status WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - select_attestation_ids AS (SELECT id - FROM ehr.attestation - WHERE reference IN - (SELECT attestation_ref FROM delete_composition) - OR reference IN (SELECT attestation_ref FROM delete_status)), - delete_attestation - AS (DELETE FROM ehr.attestation a USING select_attestation_ids sa WHERE a.id = sa.id RETURNING a.reference, a.has_audit), - delete_attestation_ref - AS (DELETE FROM ehr.attestation_ref ar USING delete_attestation da WHERE ar.ref = da.reference), - delete_contribution - AS (DELETE FROM ehr.contribution c WHERE c.ehr_id = ehr_id_param RETURNING c.id, c.has_audit), - delete_ehr - AS (DELETE FROM ehr.ehr e WHERE e.id = ehr_id_param RETURNING e.access), - delete_access - AS (DELETE FROM ehr.access a USING delete_ehr de WHERE a.id = de.access), - - -- Delete _history - delete_composition_history - AS (DELETE FROM ehr.composition_history WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - delete_entry_history - AS (DELETE FROM ehr.entry_history eh USING delete_composition_history dch WHERE eh.composition_id = dch.id), - delete_event_context_hisotry - AS (DELETE FROM ehr.event_context_history ech USING delete_composition_history dch WHERE ech.composition_id = dch.id RETURNING ech.id), - delete_participation_history - AS (DELETE FROM ehr.participation_history ph USING delete_event_context_hisotry dech WHERE ph.event_context = dech.id), - delete_status_history - AS (DELETE FROM ehr.status_history WHERE ehr_id = ehr_id_param RETURNING id, attestation_ref, has_audit), - - -- Delete audit_details - delete_composition_audit - AS (DELETE FROM ehr.audit_details ad USING delete_composition dc WHERE ad.id = dc.has_audit), - delete_status_audit - AS (DELETE FROM ehr.audit_details ad USING delete_status ds WHERE ad.id = ds.has_audit), - delete_attestation_audit - AS (DELETE FROM ehr.audit_details ad USING delete_attestation da WHERE ad.id = da.has_audit), - delete_contribution_audit - AS (DELETE FROM ehr.audit_details ad USING delete_contribution dc WHERE ad.id = dc.has_audit), - delete_composition_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_composition_history dch WHERE ad.id = dch.has_audit), - delete_status_history_audit - AS (DELETE FROM ehr.audit_details ad USING delete_status_history dsh WHERE ad.id = dsh.has_audit) - - SELECT true; - - -- Restore versioning triggers - ALTER TABLE ehr.composition - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - ENABLE TRIGGER versioning_trigger; -END -$$; - - -create or replace function ehr.js_ehr(uuid, text) returns json - language plpgsql -as -$$ -DECLARE - ehr_uuid ALIAS FOR $1; - server_id ALIAS FOR $2; - contribution_json_array JSONB[]; - contribution_details JSONB; - composition_version_json_array JSONB[]; - composition_in_ehr_id RECORD; -BEGIN - - FOR contribution_details IN (SELECT ehr.js_contribution(contribution.id, server_id) - FROM ehr.contribution - WHERE contribution.ehr_id = ehr_uuid - AND contribution.contribution_type != 'ehr') - LOOP - contribution_json_array := array_append(contribution_json_array, contribution_details); - END LOOP; - - FOR composition_in_ehr_id IN (SELECT composition.id, composition.sys_transaction - FROM ehr.composition - WHERE composition.ehr_id = ehr_uuid) - LOOP - composition_version_json_array := array_append( - composition_version_json_array, - jsonb_build_object( - '_type', 'VERSIONED_COMPOSITION', - 'id', ehr.js_object_version_id(ehr.composition_uid(composition_in_ehr_id.id, server_id)), - 'time_created', ehr.js_dv_date_time(composition_in_ehr_id.sys_transaction, 'Z') - ) - ); - END LOOP; - - - RETURN (WITH ehr_data AS (SELECT ehr.id as ehr_id, - ehr.date_created as date_created, - ehr.date_created_tzid as date_created_tz, - ehr.access as access, - system.settings as system_value - FROM ehr.ehr - JOIN ehr.system ON system.id = ehr.system_id - WHERE ehr.id = ehr_uuid) - SELECT jsonb_strip_nulls( - jsonb_build_object( - '_type', 'EHR', - 'ehr_id', ehr.js_canonical_hier_object_id(ehr_data.ehr_id), - 'system_id', ehr.js_canonical_hier_object_id(ehr_data.system_value), - 'ehr_status', ehr.js_ehr_status(ehr_data.ehr_id, server_id), - 'time_created', ehr.js_dv_date_time(ehr_data.date_created, ehr_data.date_created_tz), - 'contributions', contribution_json_array, - 'compositions', composition_version_json_array - ) - -- 'ehr_access' - -- 'tags' - ) - - FROM ehr_data); -END -$$; - - diff --git a/base/src/main/resources/db/migration/V81__save_user_extra_table.sql b/base/src/main/resources/db/migration/V81__save_user_extra_table.sql deleted file mode 100644 index 6365117e3..000000000 --- a/base/src/main/resources/db/migration/V81__save_user_extra_table.sql +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -CREATE TABLE ehr.users -( - username text, - party_id uuid not null REFERENCES ehr.party_identified (id), - namespace TEXT default '1f332a66-0e57-11ed-861d-0242ac120002', - PRIMARY KEY (username, namespace) -); - -ALTER TABLE ehr.identifier - disable row level security; - -insert into ehr.users -SELECT id_value, party, namespace -from ehr.identifier -where type_name = 'EHRbase Security Authentication User' - and issuer = 'EHRbase' - and assigner = 'EHRbase'; - -ALTER TABLE ehr.identifier - enable row level security; - -ALTER TABLE ehr.users - ENABLE ROW LEVEL SECURITY; -CREATE POLICY ehr_policy_all ON ehr.users FOR ALL USING (namespace = current_setting('ehrbase.current_tenant')) WITH CHECK (namespace = current_setting('ehrbase.current_tenant')); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V82__add_mising_force_rls.sql b/base/src/main/resources/db/migration/V82__add_mising_force_rls.sql deleted file mode 100644 index d780e8041..000000000 --- a/base/src/main/resources/db/migration/V82__add_mising_force_rls.sql +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2021 Vitasystems GmbH. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -ALTER TABLE ehr.ehr_folder FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.ehr_folder_history FORCE ROW LEVEL SECURITY; -ALTER TABLE ehr.users FORCE ROW LEVEL SECURITY; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V83__change_sys_tenant_to_short.sql b/base/src/main/resources/db/migration/V83__change_sys_tenant_to_short.sql deleted file mode 100644 index 2e828e172..000000000 --- a/base/src/main/resources/db/migration/V83__change_sys_tenant_to_short.sql +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- prepare id column - - - - -create sequence ehr.sys_tenant_seq; - -CREATE OR REPLACE FUNCTION ehr.next_sys_tenant() RETURNS SMALLINT - LANGUAGE plpgsql - SECURITY DEFINER SET search_path = ehr, pg_temp AS -$$ -BEGIN - RETURN nextval('ehr.sys_tenant_seq'); -END; -$$; -ALTER TABLE ehr.tenant - DROP - COLUMN id; - -ALTER TABLE ehr.tenant - ADD COLUMN id smallint default ehr.next_sys_tenant(); - --- Make sure the default tenant is 1 -DO -$$ - DECLARE - default_tenant smallint := (select id - from ehr.tenant - where tenant_id = '1f332a66-0e57-11ed-861d-0242ac120002'); - BEGIN - IF (default_tenant <> 1) THEN - update ehr.tenant t set id = default_tenant where t.id = 1; - update ehr.tenant t - set id = 1 - where t.tenant_id = '1f332a66-0e57-11ed-861d-0242ac120002'; - END IF; - END -$$; - -ALTER TABLE ehr.tenant - ADD CONSTRAINT tenant_pkey PRIMARY KEY (id); - -CREATE TEMP TABLE filtered_namespace_tables AS -SELECT c.table_name as namespace_table_name -FROM information_schema.columns c - INNER JOIN information_schema.tables t - ON c.table_schema = t.table_schema AND c.table_name = t.table_name -WHERE t.table_schema = 'ehr' - AND c.column_name = 'namespace' - AND t.table_type = 'BASE TABLE'; - --- Add new column sys_tenant to all tables that contain namespace -DO -$$ - DECLARE - table_name TEXT; - migration_executing_user TEXT := current_user; - BEGIN - FOR table_name IN SELECT namespace_table_name FROM filtered_namespace_tables - LOOP - EXECUTE format('CREATE POLICY ehr_policy_ehrbase_migration ON ehr.%I FOR ALL TO %I USING (TRUE)', - table_name, migration_executing_user); - EXECUTE format('ALTER TABLE ehr.%I ADD COLUMN sys_tenant SMALLINT default 1', table_name); - END LOOP; - END -$$; - --- updates the sys_tenant column in all tables in the ehr schema with the corresponding id value from the tenant table --- based on the namespace column, if there is more than one row in the tenant table. -DO -$$ - DECLARE - table_name TEXT; - BEGIN - IF ((SELECT count(id) FROM ehr.tenant) > 1) THEN - FOR table_name IN SELECT namespace_table_name FROM filtered_namespace_tables - LOOP - EXECUTE format('update ehr.%I t - set sys_tenant = tnt.id - from ehr.tenant tnt - where tnt.tenant_id = t.namespace - and t.namespace != ''1f332a66-0e57-11ed-861d-0242ac120002'' - and t.sys_tenant = 1', table_name); - END LOOP; - END IF; - END -$$; - - --- Create temporary tables before dropping constraints -CREATE TEMP TABLE filtered_tables_foreign_keys AS - (SELECT conrelid::regclass::text AS table_name, conname, pg_get_constraintdef(oid) as constraintdef - FROM pg_constraint - WHERE contype = 'f'); - -CREATE TEMP TABLE filtered_tables_primary_keys AS - (SELECT rel.relname, con.conname, pg_get_constraintdef(con.oid) - FROM pg_catalog.pg_constraint con - INNER JOIN pg_catalog.pg_class rel - ON rel.oid = con.conrelid - INNER JOIN pg_catalog.pg_namespace nsp - ON nsp.oid = connamespace - WHERE nsp.nspname = 'ehr' - AND rel.relname IN (SELECT c.table_name - FROM information_schema.columns c - INNER JOIN information_schema.tables t - ON c.table_schema = t.table_schema AND c.table_name = t.table_name - WHERE t.table_schema = 'ehr' - AND c.column_name = 'namespace' - AND t.table_type = 'BASE TABLE') - AND con.contype IN ('p')); - -CREATE TEMP TABLE tables_ AS (select * - from information_schema.tables); -CREATE TEMP TABLE columns_ AS (select * - from information_schema.columns); -CREATE TEMP TABLE table_constraints_ AS (select * - from information_schema.table_constraints); -CREATE TEMP TABLE key_column_usage_ AS (select * - from information_schema.key_column_usage); -CREATE TEMP TABLE constraint_column_usage_ AS (select * - from information_schema.constraint_column_usage); -CREATE TEMP TABLE table_indexdef AS (SELECT pg_get_indexdef(c.oid) AS indexdef, - c.relname AS index_table - FROM pg_class c - LEFT JOIN pg_attribute a ON a.attrelid = c.oid - WHERE a.attname = 'namespace' - and c.relname not in - ('template_store_pkey', 'entry_composition_id_key', 'status_ehr_id_key', - 'users_pkey') - and c.reltype = 0); - --- Dropping all foreign keys -DO -$$ - DECLARE - FK_records record; - foreign_table text; - query text; - s integer; - BEGIN - RAISE NOTICE '----Start dropping constraints-----'; - FOR FK_records IN (select * from filtered_tables_foreign_keys) - LOOP - query := ''; - foreign_table := (SELECT ccu.table_name AS foreign_table_name - FROM table_constraints_ AS tc - JOIN key_column_usage_ AS kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - JOIN constraint_column_usage_ AS ccu - ON ccu.constraint_name = tc.constraint_name - AND ccu.table_schema = tc.table_schema - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.constraint_schema = 'ehr' - AND tc.constraint_name = FK_records.conname - limit 1); - IF length(foreign_table) > 0 THEN - s := (SELECT count(*) - FROM tables_ t - INNER JOIN columns_ c - ON c.table_schema = t.table_schema AND c.table_name = t.table_name - WHERE t.table_schema = 'ehr' - AND c.table_name = foreign_table - AND c.column_name = 'sys_tenant' - AND t.table_type = 'BASE TABLE'); - - IF s > 0 THEN - query := 'ALTER TABLE ' || FK_records.table_name || ' DROP CONSTRAINT ' || - FK_records.conname; - RAISE NOTICE '%', query; - - EXECUTE (query); - - ELSE - query := 'Omitted query: ALTER TABLE ' || FK_records.table_name || ' DROP CONSTRAINT ' || - FK_records.conname; - RAISE NOTICE '%', query; - end if; - end if; - END LOOP; - RAISE NOTICE '----Stop dropping constraints-----'; - END -$$; - - --- Drop the indexes for the tables that contain namespace -DO -$$ - DECLARE - indexdef text; - index_table text; - query TEXT; - BEGIN - RAISE NOTICE '----Start dropping indexes where namespace participate----'; - FOR indexdef, index_table IN - SELECT pg_get_indexdef(c.oid) AS indexdef, - c.relname AS index_table - FROM pg_class c - LEFT JOIN pg_attribute a ON a.attrelid = c.oid - WHERE a.attname = 'namespace' - and c.relname not in - ('template_store_pkey', 'entry_composition_id_key', 'status_ehr_id_key', 'users_pkey') - and c.reltype = 0 - LOOP - IF indexdef IS NOT NULL THEN - query := format('DROP INDEX ehr.%I', index_table); - RAISE NOTICE '%', query; - - EXECUTE (query); - END IF; - END LOOP; - RAISE NOTICE '----Stop dropping indexes where namespace participate----'; - END -$$; -ALTER TABLE ehr.status - DROP CONSTRAINT status_ehr_id_key; -DROP INDEX IF EXISTS status_ehr_idx; -ALTER TABLE ehr.entry - DROP CONSTRAINT entry_composition_id_key; -DROP INDEX IF EXISTS ehr.entry_composition_id_idx; - --- Creating primary keys -DO -$$ - DECLARE - table_name TEXT; - constraint_name TEXT; - constraintdef TEXT; - query TEXT; - BEGIN - RAISE NOTICE '----Start recreating primary keys----'; - FOR table_name, constraint_name, constraintdef IN (select * from filtered_tables_primary_keys) - LOOP - query := 'ALTER TABLE ehr.' || table_name || ' DROP CONSTRAINT ' || constraint_name || ', ADD ' || - REPLACE(REPLACE(REPLACE(constraintdef, '(namespace', '('), ', namespace', ''), ')', - ', sys_tenant)'); - RAISE NOTICE '%', query; - - EXECUTE (query); - - END LOOP; - RAISE NOTICE '----Stop recreating primary keys----'; - END -$$; - --- Create the indexes for the tables that contain namespace -DO -$$ - DECLARE - indexdef TEXT; - index_table TEXT; - query TEXT; - BEGIN - RAISE NOTICE '----Start creating indexes where namespace participate----'; - FOR indexdef, index_table IN (select * from table_indexdef) - LOOP - query := ( - REPLACE(REPLACE(indexdef, '(namespace', '(sys_tenant'), ', namespace', ', sys_tenant')); - RAISE NOTICE '%', query; - - EXECUTE (query); - END LOOP; - RAISE NOTICE '----Stop creating indexes where namespace participate----'; - END -$$; - -ALTER TABLE ehr.status - ADD CONSTRAINT status_ehr_id_key UNIQUE (ehr_id, sys_tenant); -ALTER TABLE ehr.entry - ADD CONSTRAINT entry_composition_id_key UNIQUE (composition_id, sys_tenant); - --- Recreating all foreign keys including sys_tenant -DO -$$ - DECLARE - FK_records record; - foreign_table text; - query text; - s integer; - BEGIN - RAISE NOTICE '----Start recreating foreign keys-----'; - FOR FK_records IN (select * from filtered_tables_foreign_keys) - LOOP - query := ''; - foreign_table := (SELECT ccu.table_name AS foreign_table_name - FROM table_constraints_ AS tc - JOIN key_column_usage_ AS kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - JOIN constraint_column_usage_ AS ccu - ON ccu.constraint_name = tc.constraint_name - AND ccu.table_schema = tc.table_schema - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.constraint_schema = 'ehr' - AND tc.constraint_name = FK_records.conname - limit 1); - IF length(foreign_table) > 0 THEN - s := (SELECT count(*) - FROM tables_ t - INNER JOIN columns_ c - ON c.table_schema = t.table_schema AND c.table_name = t.table_name - WHERE t.table_schema = 'ehr' - AND c.table_name = foreign_table - AND c.column_name = 'sys_tenant' - AND t.table_type = 'BASE TABLE'); - - IF s > 0 THEN - query := 'ALTER TABLE ' || FK_records.table_name || ' ADD CONSTRAINT ' || - FK_records.conname || ' ' || - REPLACE(FK_records.constraintdef, ')', ', sys_tenant)'); - RAISE NOTICE '%', query; - EXECUTE (query); - ELSE - query := 'Omitted query: ALTER TABLE ' || FK_records.table_name || ' ADD CONSTRAINT ' || - FK_records.conname || - ' ' || - FK_records.constraintdef; - RAISE NOTICE '%', query; - end if; - end if; - END LOOP; - RAISE NOTICE '----Stop recreating foreign keys-----'; - END -$$; - --- Create foreign keys for sys_tenant -DO -$$ - DECLARE - table_name TEXT; - column_name TEXT; - query TEXT; - BEGIN - RAISE NOTICE '----Start creating tenant id foreign keys----'; - FOR table_name, column_name IN - SELECT c.table_name, c.column_name - FROM information_schema.columns c - INNER JOIN information_schema.tables t - ON c.table_schema = t.table_schema AND c.table_name = t.table_name - WHERE t.table_schema = 'ehr' - AND c.column_name = 'sys_tenant' - AND t.table_type = 'BASE TABLE' - LOOP - query := format( - 'ALTER TABLE ehr.%I ADD FOREIGN KEY (sys_tenant) REFERENCES ehr.tenant (id)', table_name); - RAISE NOTICE '%', query; - - EXECUTE (query); - END LOOP; - RAISE NOTICE '----Stop creating tenant id foreign keys----'; - END -$$; - --- Clean up and recreate policies -DO -$$ - DECLARE - table_name TEXT; - BEGIN - FOR table_name IN SELECT namespace_table_name FROM filtered_namespace_tables - LOOP - EXECUTE format('DROP POLICY ehr_policy_all ON ehr.%I', table_name); - EXECUTE format( - 'CREATE POLICY ehr_policy_all ON ehr.%I FOR ALL USING (sys_tenant = current_setting(''ehrbase.current_tenant'')::smallint)', - table_name); - EXECUTE format('ALTER TABLE ehr.%I DROP COLUMN namespace', table_name); - EXECUTE format('DROP POLICY ehr_policy_ehrbase_migration ON ehr.%I', table_name); - END LOOP; - END -$$; - --- Dropping temporary tables -DROP TABLE IF EXISTS filtered_namespace_tables; -DROP TABLE IF EXISTS filtered_tables_foreign_keys; -DROP TABLE IF EXISTS filtered_tables_primary_keys; -DROP TABLE IF EXISTS table_constraints_; -DROP TABLE IF EXISTS key_column_usage_; -DROP TABLE IF EXISTS constraint_column_usage_; -DROP TABLE IF EXISTS tables_; -DROP TABLE IF EXISTS columns_; -DROP TABLE IF EXISTS table_indexdef; diff --git a/base/src/main/resources/db/migration/V84__tenant_delete_cascade.sql b/base/src/main/resources/db/migration/V84__tenant_delete_cascade.sql deleted file mode 100644 index b00f2f5bc..000000000 --- a/base/src/main/resources/db/migration/V84__tenant_delete_cascade.sql +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ --- Create temporary tables before dropping constraints -CREATE TEMP TABLE filtered_tables_foreign_keys AS - (SELECT conrelid::regclass::text AS table_name, conname, pg_get_constraintdef(oid) as constraintdef - FROM pg_constraint - WHERE contype = 'f'); - -CREATE TEMP TABLE table_constraints_ AS (select * - from information_schema.table_constraints); -CREATE TEMP TABLE key_column_usage_ AS (select * - from information_schema.key_column_usage); -CREATE TEMP TABLE constraint_column_usage_ AS (select * - from information_schema.constraint_column_usage); - -CREATE TEMP TABLE filtered_sys_tenant_tables AS - (SELECT ccu.table_name AS foreign_table_name, - ccu.column_name as foreign_column_name, - tc.constraint_name, - kcu.table_name as table_name, - ftfk.constraintdef as constraint_def, - kcu.column_name as column_name - FROM table_constraints_ AS tc - JOIN key_column_usage_ AS kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - JOIN constraint_column_usage_ AS ccu - ON ccu.constraint_name = tc.constraint_name - AND ccu.table_schema = tc.table_schema - JOIN filtered_tables_foreign_keys AS ftfk - ON ftfk.conname = tc.constraint_name - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.constraint_schema = 'ehr' - AND kcu.column_name = 'sys_tenant' - AND ccu.table_name = 'tenant'); - --- create temporary policy -DO -$$ - DECLARE - table_name TEXT; - migration_executing_user TEXT := current_user; - BEGIN - FOR table_name IN SELECT ft.table_name FROM filtered_sys_tenant_tables ft - LOOP - EXECUTE format('CREATE POLICY ehr_policy_ehrbase_migration ON ehr.%I FOR ALL TO %I USING (TRUE)', - table_name, migration_executing_user); - END LOOP; - END -$$; - --- Recreate tenant related foreign keys -DO -$$ - DECLARE - FK_records record; - query text; - BEGIN - RAISE NOTICE '----Start recreating foreign keys-----'; - FOR FK_records IN (select * from filtered_sys_tenant_tables) - LOOP - query := 'ALTER TABLE ehr.' || FK_records.table_name || ' DROP CONSTRAINT ' || - FK_records.constraint_name || ', ADD CONSTRAINT ' || - FK_records.constraint_name || ' ' || - REPLACE(FK_records.constraint_def, ' ON DELETE CASCADE', '') || - ' ON DELETE CASCADE'; - - RAISE NOTICE '%', query; - EXECUTE (query); - END LOOP; - RAISE NOTICE '----Stop recreating foreign keys-----'; - END -$$; - --- Drop temporary policy -DO -$$ - DECLARE - table_name TEXT; - BEGIN - FOR table_name IN SELECT ft.table_name FROM filtered_sys_tenant_tables ft - LOOP - EXECUTE format('DROP POLICY ehr_policy_ehrbase_migration ON ehr.%I', table_name); - END LOOP; - END -$$; - --- Dropping temporary tables -DROP TABLE IF EXISTS filtered_tables_foreign_keys; -DROP TABLE IF EXISTS table_constraints_; -DROP TABLE IF EXISTS key_column_usage_; -DROP TABLE IF EXISTS constraint_column_usage_; -DROP TABLE IF EXISTS filtered_sys_tenant_tables; - -CREATE OR REPLACE FUNCTION ehr.admin_delete_tenant_full(tenant_id_param smallint) - RETURNS TABLE - ( - deleted boolean - ) - security definer - SET search_path = ehr, pg_temp -AS -$$ -BEGIN - -- Disable versioning triggers - ALTER TABLE ehr.composition - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - DISABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - DISABLE TRIGGER versioning_trigger; - - RETURN QUERY WITH - -- Delete data - -- ON DELETE CASCADE: - -- * ehr.ehr - -- * ehr.status - -- * ehr.status_history - -- * ehr.contribution - -- * ehr.attestation - -- * ehr.attested_view - -- * ehr.composition - -- * ehr.composition_history - -- * ehr.event_context - -- * ehr.event_context_history - -- * ehr.participation - -- * ehr.participation_history - -- * ehr.entry - -- * ehr.entry_history - -- * ehr.compo_xref - -- * ehr.session_log - -- * ehr.heading - -- * ehr.audit_details - -- * ehr.attestation_ref - -- * ehr.stored_query - -- * ehr.template_store - -- * ehr.ehr_folder - -- * ehr.ehr_folder_history - -- * ehr.users - -- * ehr.terminology_provider - -- * ehr.party_identified - -- * ehr.identifier - -- * ehr.access - - delete_tenant - AS (DELETE FROM ehr.tenant WHERE id = tenant_id_param) - - SELECT true; - --- Restore versioning triggers - ALTER TABLE ehr.composition - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.entry - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.event_context - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.participation - ENABLE TRIGGER versioning_trigger; - ALTER TABLE ehr.status - ENABLE TRIGGER versioning_trigger; -END -$$ - LANGUAGE plpgsql; diff --git a/base/src/main/resources/db/migration/V85__enforce_unique_template_id.sql b/base/src/main/resources/db/migration/V85__enforce_unique_template_id.sql deleted file mode 100644 index 6ffe7acfb..000000000 --- a/base/src/main/resources/db/migration/V85__enforce_unique_template_id.sql +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -DO -$$ - DECLARE - count_id INTEGER; count_template_id INTEGER; - BEGIN - ALTER TABLE ehr.template_store - DISABLE ROW LEVEL SECURITY; - SELECT count(*) INTO count_id FROM ehr.template_store GROUP BY id, sys_tenant HAVING COUNT(*) > 1; - SELECT count(*) INTO count_template_id FROM ehr.template_store GROUP BY template_id, sys_tenant HAVING COUNT(*) > 1; - ALTER TABLE ehr.template_store - ENABLE ROW LEVEL SECURITY; - IF count_id > 0 THEN - RAISE EXCEPTION 'Systems with duplicated internal IDs cannot be migrated automatically; See UPDATING.md'; - END IF; - IF count_template_id > 0 THEN - RAISE EXCEPTION 'Systems with duplicated template IDs cannot be migrated automatically; See UPDATING.md'; - END IF; - END -$$; - -ALTER TABLE ehr.template_store - DROP CONSTRAINT template_store_pkey, - ADD PRIMARY KEY (id, sys_tenant); - -create unique index template_store_template_id - on ehr.template_store (template_id, sys_tenant); diff --git a/base/src/main/resources/db/migration/V8__folders_support.sql b/base/src/main/resources/db/migration/V8__folders_support.sql deleted file mode 100644 index 49ef3e5d4..000000000 --- a/base/src/main/resources/db/migration/V8__folders_support.sql +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Modifications copyright (C) 2019 Vitasystems GmbH, Hannover Medical School, , and Luis Marco-Ruiz (Hannover Medical School). - - * This file is part of Project EHRbase - - * Copyright (c) 2015 Christian Chevalley - * This file is part of Project Ethercis - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- Table: ehr.folder - --- DROP TABLE ehr.folder; - -CREATE TABLE ehr.folder -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - in_contribution uuid NOT NULL, - name text COLLATE pg_catalog."default" NOT NULL, - archetype_node_id text COLLATE pg_catalog."default" NOT NULL, - active boolean DEFAULT true, - details jsonb, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT folder_pk PRIMARY KEY (id), - CONSTRAINT folder_in_contribution_fkey FOREIGN KEY (in_contribution) - REFERENCES ehr.contribution (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - - - --- Index: folder_in_contribution_idx - --- DROP INDEX ehr.folder_in_contribution_idx; - -CREATE INDEX folder_in_contribution_idx - ON ehr.folder USING btree - (in_contribution) - TABLESPACE pg_default; - --- Table: ehr.folder_hierarchy - --- DROP TABLE ehr.folder_hierarchy; - -CREATE TABLE ehr.folder_hierarchy -( - parent_folder uuid NOT NULL, - child_folder uuid NOT NULL, - in_contribution uuid, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT folder_hierarchy_pkey PRIMARY KEY (parent_folder, child_folder), - CONSTRAINT folder_hierarchy_in_contribution_fk FOREIGN KEY (in_contribution) - REFERENCES ehr.contribution (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE, - CONSTRAINT folder_hierarchy_parent_fk FOREIGN KEY (parent_folder) - REFERENCES ehr.folder (id) MATCH SIMPLE - ON UPDATE CASCADE - ON DELETE CASCADE - DEFERRABLE -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - --- Index: folder_hierarchy_in_contribution_idx - --- DROP INDEX ehr.folder_hierarchy_in_contribution_idx; - -CREATE INDEX folder_hierarchy_in_contribution_idx - ON ehr.folder_hierarchy USING btree - (in_contribution) - TABLESPACE pg_default; - --- DROP INDEX ehr.fki_folder_hierarchy_parent_fk; - -CREATE INDEX fki_folder_hierarchy_parent_fk - ON ehr.folder_hierarchy USING btree - (parent_folder) - TABLESPACE pg_default; - --- Table: ehr.folder_hierarchy_history - --- DROP TABLE ehr.folder_hierarchy_history; - -CREATE TABLE ehr.folder_hierarchy_history -( - parent_folder uuid NOT NULL, - child_folder uuid NOT NULL, - in_contribution uuid NOT NULL, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT folder_hierarchy_history_pkey PRIMARY KEY (parent_folder, child_folder, in_contribution) -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - --- Table: ehr.folder_history - --- DROP TABLE ehr.folder_history; - -CREATE TABLE ehr.folder_history -( - id uuid NOT NULL, - in_contribution uuid NOT NULL, - name text COLLATE pg_catalog."default" NOT NULL, - archetype_node_id text COLLATE pg_catalog."default" NOT NULL, - active boolean NOT NULL, - details jsonb, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT folder_history_pkey PRIMARY KEY (id, in_contribution) -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - - --- Trigger: versioning_trigger - --- DROP TRIGGER versioning_trigger ON ehr.folder; - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR DELETE OR UPDATE - ON ehr.folder - FOR EACH ROW - EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.folder_history', 'true'); - - --- Trigger: versioning_trigger - --- DROP TRIGGER versioning_trigger ON ehr.folder_hierarchy; - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR DELETE OR UPDATE - ON ehr.folder_hierarchy - FOR EACH ROW - EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.folder_hierarchy_history', 'true'); - - - --- Table: ehr.object_ref_history - --- DROP TABLE ehr.object_ref_history; - -CREATE TABLE ehr.object_ref_history -( - id_namespace text COLLATE pg_catalog."default" NOT NULL, - type text COLLATE pg_catalog."default" NOT NULL, - id uuid NOT NULL, - in_contribution uuid NOT NULL, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT object_ref_hist_pkey PRIMARY KEY (id, in_contribution) -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - ---ALTER TABLE ehr.object_ref_history - -- OWNER to postgres; -COMMENT ON TABLE ehr.object_ref_history - IS '*implements https://specifications.openehr.org/releases/RM/Release-1.0.3/support.html#_object_ref_history_class - -*id implemented as native UID from postgres instead of a separate table. -'; - --- Table: ehr.object_ref - --- DROP TABLE ehr.object_ref; - -CREATE TABLE ehr.object_ref -( - id_namespace text COLLATE pg_catalog."default" NOT NULL, - type text COLLATE pg_catalog."default" NOT NULL, - id uuid NOT NULL, - in_contribution uuid NOT NULL, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT object_ref_pkey PRIMARY KEY (id, in_contribution), - CONSTRAINT object_ref_in_contribution_fkey FOREIGN KEY (in_contribution) - REFERENCES ehr.contribution (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - ---ALTER TABLE ehr.object_ref - -- OWNER to postgres; -COMMENT ON TABLE ehr.object_ref - IS '*implements https://specifications.openehr.org/releases/RM/Release-1.0.3/support.html#_object_ref_class - -*id implemented as native UID from postgres instead of a separate table. -'; --- Index: obj_ref_in_contribution_idx - --- DROP INDEX ehr.obj_ref_in_contribution_idx; - -CREATE INDEX obj_ref_in_contribution_idx - ON ehr.object_ref USING btree - (in_contribution) - TABLESPACE pg_default; - --- Trigger: versioning_trigger - --- DROP TRIGGER versioning_trigger ON ehr.object_ref; - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR DELETE OR UPDATE - ON ehr.object_ref - FOR EACH ROW - EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.object_ref_history', 'true'); - --- Table: ehr.folder_items - --- DROP TABLE ehr.folder_items; - -CREATE TABLE ehr.folder_items -( - folder_id uuid NOT NULL, - object_ref_id uuid NOT NULL, - in_contribution uuid NOT NULL, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT folder_items_pkey PRIMARY KEY (folder_id, object_ref_id, in_contribution), - CONSTRAINT folder_items_folder_fkey FOREIGN KEY (folder_id) - REFERENCES ehr.folder (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE, - CONSTRAINT folder_items_in_contribution_fkey FOREIGN KEY (in_contribution) - REFERENCES ehr.contribution (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION, - CONSTRAINT folder_items_obj_ref_fkey FOREIGN KEY (in_contribution, object_ref_id) - REFERENCES ehr.object_ref (in_contribution, id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - - --- Table: ehr.folder_items_history - --- DROP TABLE ehr.folder_items_history; - -CREATE TABLE ehr.folder_items_history -( - folder_id uuid NOT NULL, - object_ref_id uuid NOT NULL, - in_contribution uuid NOT NULL, - sys_transaction timestamp without time zone NOT NULL, - sys_period tstzrange NOT NULL, - CONSTRAINT folder_items_hist_pkey PRIMARY KEY (folder_id, object_ref_id, in_contribution) -) -WITH ( - OIDS = FALSE -) -TABLESPACE pg_default; - --- Index: folder_hist_idx - - --- DROP INDEX ehr.folder_hist_idx; - -CREATE INDEX folder_hist_idx - ON ehr.folder_items_history USING btree - (folder_id, object_ref_id, in_contribution) - TABLESPACE pg_default; - - --- Trigger: versioning_trigger - --- DROP TRIGGER versioning_trigger ON ehr.folder_items; - -CREATE TRIGGER versioning_trigger - BEFORE INSERT OR DELETE OR UPDATE - ON ehr.folder_items - FOR EACH ROW - EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.folder_items_history', 'true'); - - - --- Trigger and function to maintain consistent the delete of FOLDER.items, deletes OBJECT_REF rows after a deletion on FOLDER_ITEMS occurs. -CREATE OR REPLACE FUNCTION ehr.tr_function_delete_folder_item() - RETURNS trigger - AS $$BEGIN -DELETE FROM ehr.object_ref -WHERE ehr.object_ref.id=OLD.object_ref_id AND - ehr.object_ref.in_contribution= OLD.in_contribution; - RETURN OLD; -END; -$$ LANGUAGE plpgsql; ---ALTER FUNCTION ehr.tr_function_delete_folder_item() - -- OWNER TO postgres; - -COMMENT ON FUNCTION ehr.tr_function_delete_folder_item() - IS 'fires after deletion of folder_items when the corresponding Object_ref needs to be deleted.'; - - -CREATE TRIGGER tr_folder_item_delete -AFTER DELETE ON ehr.folder_items -FOR EACH ROW -EXECUTE PROCEDURE ehr.tr_function_delete_folder_item(); \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V9_1__attestation_and_changes.sql b/base/src/main/resources/db/migration/V9_1__attestation_and_changes.sql deleted file mode 100644 index f0bf6ef37..000000000 --- a/base/src/main/resources/db/migration/V9_1__attestation_and_changes.sql +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2020 Vitasystems GmbH and Jake Smolka (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- Based on `v9__audit.sql` this migrations adds attestations and other modifications regarding audits --- like removal of "versioning" of audits - --- removing "versioning" of audit_details -DROP TRIGGER versioning_trigger ON ehr.audit_details; -DROP TABLE ehr.audit_details_history; -ALTER TABLE ehr.audit_details - DROP COLUMN sys_period; - --- modify constrains -ALTER TABLE ehr.audit_details - --ALTER COLUMN system_id SET ON DELETE CASCADE, - --ALTER COLUMN committer SET ON DELETE CASCADE, - ALTER COLUMN change_type SET NOT NULL; - --- 1-to-many relation-table to optionally reference attestations from version objects --- needs to be explicit table, instead of being embedded attribute in attestation table, because can't "references" to different table's IDs --- necessary because all versioned objects are valid values, but are implemented in their own table (without inheritance) -CREATE TABLE ehr.attestation_ref ( - ref UUID primary key DEFAULT ext.uuid_generate_v4() -- ref key to allow many-relationship -); - --- Also modify attestation (sub-class of audit_details) table -ALTER TABLE ehr.attestation - ADD COLUMN has_audit UUID NOT NULL references ehr.audit_details(id) ON DELETE CASCADE, -- attestation inherits "audit_details", so has one linked instance - DROP COLUMN contribution_id, -- contribution embedded audit handling was replaced with the above column - ADD COLUMN reference UUID NOT NULL references ehr.attestation_ref(ref) ON DELETE CASCADE; - --- Finally, modify existing object tables to include new attestations feature --- add audit capabilities to composition table -ALTER TABLE ehr.composition - ADD COLUMN attestation_ref UUID references ehr.attestation_ref(ref) ON DELETE CASCADE; -- can have this attestation list (through reference) - -ALTER TABLE ehr.composition_history - ADD COLUMN attestation_ref UUID references ehr.attestation_ref(ref) ON DELETE CASCADE; -- can have this attestation list (through reference) - --- add audit and attestations capabilities to (ehr_)status table --- (also adding audit columns because they weren't added yet) -ALTER TABLE ehr.status - ADD COLUMN has_audit UUID NOT NULL references ehr.audit_details(id) ON DELETE CASCADE, -- has this audit_details instance - ADD COLUMN attestation_ref UUID references ehr.attestation_ref(ref) ON DELETE CASCADE, -- can have this attestation list (through reference) - ADD COLUMN in_contribution UUID NOT NULL references ehr.contribution(id) ON DELETE CASCADE; -- not directly related to audit, but necessary: reference to contribution - -ALTER TABLE ehr.status_history - ADD COLUMN has_audit UUID NOT NULL references ehr.audit_details(id) ON DELETE CASCADE, -- has this audit_details instance - ADD COLUMN attestation_ref UUID references ehr.attestation_ref(ref) ON DELETE CASCADE, -- can have this attestation list (through reference) - ADD COLUMN in_contribution UUID NOT NULL references ehr.contribution(id) ON DELETE CASCADE; -- not directly related to audit, but necessary: reference to contribution - --- TODO include other object types like folders \ No newline at end of file diff --git a/base/src/main/resources/db/migration/V9__audit.sql b/base/src/main/resources/db/migration/V9__audit.sql deleted file mode 100644 index e7a291edf..000000000 --- a/base/src/main/resources/db/migration/V9__audit.sql +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (Hannover Medical School). - * - * This file is part of project EHRbase - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - --- First, add new audit_details table containing audit data columns -CREATE TABLE ehr.audit_details ( - id UUID primary key DEFAULT ext.uuid_generate_v4(), - system_id UUID references ehr.system(id), - committer UUID references ehr.party_identified(id), - time_committed timestamp default NOW(), - time_committed_tzid TEXT, -- timezone id - change_type ehr.contribution_change_type, - description TEXT, -- is a DvCodedText - sys_period tstzrange NOT NULL -- temporal table column -); - --- Second, setup change history table and trigger -CREATE TABLE ehr.audit_details_history (like ehr.audit_details); -CREATE INDEX ehr_audit_details_history ON ehr.audit_details_history USING BTREE (id); - -CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON ehr.audit_details -FOR EACH ROW EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.audit_details_history', true); - --- Finally, modify existing object tables to include new audit feature --- add audit capabilities to contribution table and remove older columns that were part of the early audit implementation -ALTER TABLE ehr.contribution - ADD COLUMN has_audit UUID references ehr.audit_details(id) ON DELETE CASCADE, -- has this audit_details instance - DROP COLUMN system_id, - DROP COLUMN committer, - DROP COLUMN time_committed, - DROP COLUMN time_committed_tzid, -- timezone id - DROP COLUMN change_type, - DROP COLUMN description; - -ALTER TABLE ehr.contribution_history - ADD COLUMN has_audit UUID references ehr.audit_details(id) ON DELETE CASCADE; -- has this audit_details instance - --- add audit capabilities to composition table -ALTER TABLE ehr.composition - ADD COLUMN has_audit UUID references ehr.audit_details(id) ON DELETE CASCADE; -- has this audit_details instance - -ALTER TABLE ehr.composition_history - ADD COLUMN has_audit UUID references ehr.audit_details(id) ON DELETE CASCADE; -- has this audit_details instance - --- TODO include other object types like folders \ No newline at end of file diff --git a/base/src/main/resources/db/migration/beforeMigrate.sql b/base/src/main/resources/db/migration/beforeMigrate.sql deleted file mode 100644 index 4665b9e1d..000000000 --- a/base/src/main/resources/db/migration/beforeMigrate.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ - DECLARE - current_value TEXT; - BEGIN - SELECT setting FROM pg_settings WHERE name = 'IntervalStyle' INTO current_value; - - IF current_value != 'iso_8601' - THEN - RAISE EXCEPTION 'Your database is not properly configured, IntervalStyle setting % must be change to iso_8601', current_value; - END IF; - END -$$; \ No newline at end of file diff --git a/base/src/main/resources/db/migration/beforeValidate.sql b/base/src/main/resources/db/migration/beforeValidate.sql deleted file mode 100644 index b33117f57..000000000 --- a/base/src/main/resources/db/migration/beforeValidate.sql +++ /dev/null @@ -1,37 +0,0 @@ --- Fix issues with V62__add_entry_history_missing_columns.sql etc. - -DO -$$ - BEGIN - IF (EXISTS - (SELECT 1 - FROM information_schema.tables - WHERE table_schema = 'ehr' - AND table_name = 'flyway_schema_history')) - THEN - - UPDATE ehr.flyway_schema_history - SET checksum = 1370938853 - WHERE (version, checksum) = ('16', -2009117355); - - UPDATE ehr.flyway_schema_history - SET checksum = -55836684 - WHERE (version, checksum) = ('61', -1745413492); - - UPDATE ehr.flyway_schema_history - SET checksum = 1440169380 - WHERE (version, checksum) = ('62', -307543225); - - UPDATE ehr.flyway_schema_history - SET checksum = 1953744080 - WHERE (version, checksum) = ('71', -1047639409); - - UPDATE ehr.flyway_schema_history - SET checksum = 861164608 - WHERE (version, checksum) = ('83', -1840801783) - OR (version, checksum) = ('83', 1051439940); - - END IF; - END -$$; - diff --git a/base/src/main/resources/terminology.xml b/base/src/main/resources/terminology.xml deleted file mode 100644 index 6ee911377..000000000 --- a/base/src/main/resources/terminology.xml +++ /dev/null @@ -1,6715 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/bom/pom.xml b/bom/pom.xml index 86c69b3d1..ccba5247e 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1,7 +1,7 @@ - 4.11.1 - 3.3.0 - 2.15.1 - 3.14.0 - 2.6.0 - 8.5.13 - 2.15.0 - 1.95.0 - 2.12.5 - 3.18.7 - 2.8.0 - 5.7.2 - 3.6.1 - 3.2.2 - 3.1.0 - 0.8.11 - 1.6.13 - 3.2.2 - 42.7.1 - 1.12.0 - 3.1.5 - 2.2.0 - - 2.22.0 - 1.4.13 - 2.16.2 - 0.8.0 - 4.4 - 2.8.9 - 1.3.2 - 4.9.10 - 3.19.4 - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - ossrh-snapshots - https://oss.sonatype.org/content/repositories/snapshots - - false - - - true - - - - - - - - com.fasterxml.jackson - jackson-bom - ${jackson-bom.version} - pom - import - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - org.ehrbase.openehr.sdk - bom - ${ehrbase.sdk.version} - pom - import - - - - org.ehrbase.openehr.sdk - serialisation - ${ehrbase.sdk.version} - - - commons-logging - commons-logging - - - - - org.ehrbase.openehr.sdk - validation - ${ehrbase.sdk.version} - - - commons-logging - commons-logging - - - - - commons-io - commons-io - ${commons-io.version} - - - org.apache.commons - commons-lang3 - ${commons-lang3.version} - - - - - org.ehrbase.openehr - base - ${project.version} - - - org.ehrbase.openehr - service - ${project.version} - - - org.ehrbase.openehr - rest-ehr-scape - ${project.version} - - - org.ehrbase.openehr - rest-openehr - ${project.version} - - - org.ehrbase.openehr - application - ${project.version} - - - org.ehrbase.openehr - api - ${project.version} - - - org.ehrbase.openehr - jooq-pg - ${project.version} - - - org.ehrbase.openehr - plugin - ${project.version} - - - - org.springframework.boot - spring-boot-starter-logging - ${spring-boot.version} - - - org.apache.logging.log4j - log4j-api - - - org.apache.logging.log4j - log4j-core - - - org.apache.logging.log4j - log4j-to-slf4j - - - - - - org.pf4j - pf4j-spring - ${pf4j-spring.version} - - - org.slf4j - slf4j-log4j12 - - - - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - org.postgresql - postgresql - ${postgresql.version} - - - org.antlr - antlr4-runtime - ${antlr4.version} - - - com.jayway.jsonpath - json-path - ${json-path.version} - - - com.nedap.healthcare.archie - openehr-rm - ${archie.version} - - - org.apache.commons - commons-collections4 - ${commons-collections4.version} - - - com.google.code.gson - gson - ${gson.version} - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${springdoc-openapi.version} - - - org.flywaydb - flyway-core - ${flyway.version} - - - javax.annotation - javax.annotation-api - ${javax.annotation-api.version} - - - pl.project13.maven - git-commit-id-plugin - ${git-commit-id-plugin.version} - - - io.micrometer - micrometer-registry-prometheus - ${prometheus.version} - - - org.springframework.security - spring-security-web - - - org.springframework.security - spring-security-config - - - - joda-time - joda-time - ${joda.version} - - - net.bull.javamelody - javamelody-spring-boot-starter - ${javamelody.version} - - - org.jooq - jooq - ${jooq.version} - - - com.auth0 - java-jwt - ${java-jwt.version} - - - com.sun.xml.bind - jaxb-impl - 2.3.6 - - - - - - - - + 4.0.0 + + org.ehrbase.openehr + bom + 2.7.0 + pom + + EHRbase + EHRbase is a Free, Libre, Open Source openEHR Clinical Data Repository + https://ehrbase.org + + + + The Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + scm:git:git://github.com/ehrbase/ehrbase.git + scm:git:ssh://github.com:ehrbase/ehrbase.git + https://github.com/ehrbase/ehrbase + + + + + Stefan Spiska + stefan.spiska@vitagroup.ag + vitasystems GmbH + https://www.vitagroup.ag/ + + + + + + 3.3.0 + 2.16.1 + 3.14.0 + 2.17.0 + 8.5.13 + 2.17.2 + 1.99.0 + 3.19.10 + 2.9.0 + 5.10.2 + + + 42.7.3 + + + 3.2.5 + 2.6.0 + + + 2.23.1 + 0.9.0 + 3.11.1 + 4.4 + + 1.3.2 + 4.9.10 + 3.19.4 + + + 3.13.0 + 3.4.1 + 3.7.1 + 3.3.0 + 3.2.4 + 0.8.12 + 2.43.0 + 1.7.0 + 3.2.5 + 2.17.0 + 1.4.13 + 3.11.0.3922 + + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + ossrh-snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + + + + com.fasterxml.jackson + jackson-bom + ${jackson-bom.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + org.ehrbase.openehr.sdk + bom + ${ehrbase.openehr.sdk.version} + pom + import + + + org.ehrbase.openehr.sdk + serialisation + ${ehrbase.openehr.sdk.version} + + + commons-logging + commons-logging + + + + + org.ehrbase.openehr.sdk + validation + ${ehrbase.openehr.sdk.version} + + + commons-logging + commons-logging + + + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + org.ehrbase.openehr + rm-db-format + ${project.version} + + + org.ehrbase.openehr + service + ${project.version} + + + org.ehrbase.openehr + rest-ehr-scape + ${project.version} + + + org.ehrbase.openehr + rest-openehr + ${project.version} + + + org.ehrbase.openehr + application + ${project.version} + + + org.ehrbase.openehr + api + ${project.version} + + + org.ehrbase.openehr + cli + ${project.version} + + + org.ehrbase.openehr + configuration + ${project.version} + + + org.ehrbase.openehr + jooq-pg + ${project.version} + + + org.ehrbase.openehr + plugin + ${project.version} + + + org.ehrbase.openehr + aql-engine + ${project.version} + + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot.version} + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + + org.pf4j + pf4j-spring + ${pf4j-spring.version} + + + org.slf4j + slf4j-log4j12 + + + slf4j-reload4j + org.slf4j + + + + + org.pf4j + pf4j + ${pf4j.version} + + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + + com.nedap.healthcare.archie + openehr-rm + ${archie.version} + + + org.apache.commons + commons-collections4 + ${commons-collections4.version} + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc-openapi.version} + + + org.flywaydb + flyway-core + ${flyway.version} + + + javax.annotation + javax.annotation-api + ${javax.annotation-api.version} + + + pl.project13.maven + git-commit-id-plugin + ${git-commit-id-plugin.version} + + + org.springframework.security + spring-security-web + + + org.springframework.security + spring-security-config + + + + net.bull.javamelody + javamelody-spring-boot-starter + ${javamelody.version} + + + org.jooq + jooq + ${jooq.version} + + + com.auth0 + java-jwt + ${java-jwt.version} + + + com.sun.xml.bind + jaxb-impl + 2.3.9 + + + + + + + + + + + org.codehaus.mojo + versions-maven-plugin + + false + + + + + + + com.diffplug.spotless + spotless-maven-plugin + ${maven-spotless-maven-plugin.version} + + + + @format:off + @format:on + + + 2.39.0 + + + ${maven.multiModuleProjectDirectory}/spotless-lic-header + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + org.codehaus.mojo + versions-maven-plugin + ${maven-versions-maven-plugin.version} + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${maven-nexus-staging-plugin.version} + + + com.spotify + dockerfile-maven-plugin + ${maven-dockerfile-maven-plugin.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + org.ehrbase.application.EhrBase + + + + build-info + + build-info + + + + ${archie.version} + ${ehrbase.openehr.sdk.version} + + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${maven-sonar.scanner.version} + + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + + + + no-snapshots + - - org.codehaus.mojo - versions-maven-plugin - - false - - - - org.sonatype.plugins - nexus-staging-maven-plugin - true - - ossrh - https://oss.sonatype.org/ - true - - + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + + No Snapshots Allowed! + + org.ehrbase.openehr + + + + true + + + + enforce-banned-dependencies + + enforce + + + + - - - - com.diffplug.spotless - spotless-maven-plugin - 2.35.0 - - - - @format:off - @format:on - - - 2.24.0 - - - ./license-header - - - - - - org.apache.maven.plugins - maven-dependency-plugin - ${maven-dependency-plugin.version} - - - org.codehaus.mojo - versions-maven-plugin - ${versions-maven-plugin.version} - - - org.apache.maven.plugins - maven-gpg-plugin - ${maven-gpg-plugin.version} - - - org.sonatype.plugins - nexus-staging-maven-plugin - ${maven-nexus-staging-plugin.version} - - - com.spotify - dockerfile-maven-plugin - ${dockerfile-maven-plugin.version} - - - - - - org.antlr - antlr4-maven-plugin - ${antlr4.version} - - true - true - - - - antlr - - antlr4 - - - - - - org.jooq - jooq-codegen-maven - ${jooq.version} - - - org.flywaydb - flyway-maven-plugin - ${flyway.version} - - - org.postgresql - postgresql - ${postgresql.version} - - - - - generate-sources - - migrate - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} - - org.ehrbase.application.EhrBase - - - - build-info - - build-info - - - - ${archie.version} - ${ehrbase.sdk.version} - - - - - - - - - - - - release - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - - - - - + + + + + diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 000000000..a6880a845 --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,55 @@ + + + + + + + 4.0.0 + + + org.ehrbase.openehr + server + 2.7.0 + + + cli + jar + + + + org.ehrbase.openehr + configuration + + + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + + diff --git a/cli/src/main/java/org/ehrbase/cli/CliConfiguration.java b/cli/src/main/java/org/ehrbase/cli/CliConfiguration.java new file mode 100644 index 000000000..8161d1316 --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/CliConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackages = "org.ehrbase.cli") +public class CliConfiguration {} diff --git a/cli/src/main/java/org/ehrbase/cli/CliRunner.java b/cli/src/main/java/org/ehrbase/cli/CliRunner.java new file mode 100644 index 000000000..74dc75a8a --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/CliRunner.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BinaryOperator; +import java.util.stream.Collectors; +import org.assertj.core.util.Lists; +import org.ehrbase.cli.cmd.CliCommand; +import org.ehrbase.cli.cmd.CliHelpCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class CliRunner { + + public static final String CLI = "cli"; + + private final Logger logger = LoggerFactory.getLogger(CliRunner.class); + + private final CliHelpCommand helpCommand; + private final List commands; + + public CliRunner(List commands, CliHelpCommand helpCommand) { + + this.helpCommand = helpCommand; + this.commands = commands.stream() + .sorted(Comparator.comparing(CliCommand::getName)) + .toList(); + } + + public void run(String... args) { + run( + (t, t2) -> { + throw new IllegalStateException(String.format( + "Duplicate command for name %s (attempted merging values %s and %s)", t.getName(), t, t2)); + }, + args); + } + + public void run(BinaryOperator onDuplicatedCmd, String... args) { + + List argList = Arrays.asList(args); + + Iterator argIter = argList.iterator(); + Optional commandName = extractCmd(argIter); + + Map namedCommands = + commands.stream().collect(Collectors.toMap(CliCommand::getName, item -> item, onDuplicatedCmd)); + + commandName.ifPresentOrElse( + name -> Optional.ofNullable(namedCommands.get(name)) + .ifPresentOrElse( + command -> runCommand(command, Lists.newArrayList(argIter)), + () -> helpCommand.exitFail("Unknown command %s".formatted(name))), + () -> helpCommand.exitFail("No command specified")); + } + + private Optional extractCmd(Iterator argIter) { + + if (!argIter.hasNext()) { + return Optional.empty(); + } + + // consume until cli commands + while (argIter.hasNext()) { + String next = argIter.next(); + if (next.equals(CLI)) { + break; + } + } + + if (!argIter.hasNext()) { + return Optional.empty(); + } + return Optional.of(argIter.next()); + } + + private void runCommand(CliCommand command, List args) { + try { + command.run(args); + } catch (Throwable e) { + logger.error("Failed to execute [%s]".formatted(command.getName()), e); + helpCommand.exitFail(e.getMessage()); + } + } +} diff --git a/cli/src/main/java/org/ehrbase/cli/cmd/CliCommand.java b/cli/src/main/java/org/ehrbase/cli/cmd/CliCommand.java new file mode 100644 index 000000000..0ab03b9f6 --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/cmd/CliCommand.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.cmd; + +import java.util.Iterator; +import java.util.List; +import javax.annotation.Nullable; +import org.ehrbase.cli.util.ExceptionFriendlyFunction; + +@SuppressWarnings("java:S5803") +public abstract class CliCommand { + + /** + * Represents a command CliArgument + * @param arg arg line + * @param key argument key= + * @param value argument =value + */ + public record CliArgument(String arg, String key, @Nullable String value) {} + + /** + * Represents the Result of a {@link CliArgument} execution + */ + public sealed interface Result permits Result.OK, Result.Unknown { + + Result OK = new OK(); + Result Unknown = new Unknown(); + + final class OK implements Result {} + + final class Unknown implements Result {} + } + + protected final String name; + + protected CliCommand(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @SuppressWarnings("java:S112") + public abstract void run(List args) throws Throwable; + + @SuppressWarnings("java:S106") + protected void println(String line) { + System.out.println(line); + } + + protected void printStep(String line) { + println("---------------------------------------------------------------------------"); + println(line); + println("---------------------------------------------------------------------------"); + } + + @SuppressWarnings("java:S106") + public void exitFail(String reason) { + + System.err.println(reason); + printUsage(); + exit(-1); + } + + void exit(int code) { + System.exit(code); + } + + protected abstract void printUsage(); + + protected void consumeArgs(Iterable args, ExceptionFriendlyFunction consumer) + throws Exception { + + Iterator argIter = args.iterator(); + if (!argIter.hasNext()) { + exitFail("No argument provided"); + return; + } + + String next; + while (argIter.hasNext()) { + next = argIter.next(); + + if (next.equals("help")) { + printUsage(); + return; + } + + String[] split = next.split("="); + CliArgument arg = new CliArgument(next, split[0].replace("--", ""), split.length > 1 ? split[1] : null); + + Result result = consumer.apply(arg); + if (result instanceof Result.Unknown) { + exitFail("Unknown argument [%s]".formatted(arg.arg())); + } + } + } +} diff --git a/cli/src/main/java/org/ehrbase/cli/cmd/CliDataBaseCommand.java b/cli/src/main/java/org/ehrbase/cli/cmd/CliDataBaseCommand.java new file mode 100644 index 000000000..7becdbc30 --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/cmd/CliDataBaseCommand.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.cmd; + +import com.zaxxer.hikari.HikariDataSource; +import java.sql.Connection; +import java.util.List; +import javax.sql.DataSource; +import org.ehrbase.configuration.config.flyway.MigrationStrategy; +import org.ehrbase.configuration.config.flyway.MigrationStrategyConfig; +import org.flywaydb.core.Flyway; +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; +import org.springframework.stereotype.Component; + +@Component +public class CliDataBaseCommand extends CliCommand { + + protected final DataSource dataSource; + + protected final Flyway flyway; + + protected final MigrationStrategyConfig migrationStrategyConfig; + + public CliDataBaseCommand(DataSource dataSource, Flyway flyway, MigrationStrategyConfig migrationStrategyConfig) { + super("database"); + this.dataSource = dataSource; + this.flyway = flyway; + this.migrationStrategyConfig = migrationStrategyConfig; + } + + @Override + public void run(List args) throws Exception { + + consumeArgs(args, arg -> switch (arg.key()) { + case "check-connection": + yield executeCheckConnection(); + case "migration-validate": + yield executeMigration(MigrationStrategy.VALIDATE); + case "migration-migrate": + yield executeMigration(MigrationStrategy.MIGRATE); + default: + yield Result.Unknown; + }); + } + + protected Result executeCheckConnection() { + + printStep("executing Database connection check: %s".formatted(jdbUrl())); + + try (Connection connection = dataSource.getConnection()) { + String url = connection.getMetaData().getURL(); + println("Connection established to %s".formatted(url)); + return Result.OK; + } catch (Exception e) { + exitFail("Failed to open connection %s".formatted(jdbUrl())); + return Result.Unknown; + } + } + + protected Result executeMigration(MigrationStrategy migrationStrategy) { + + printStep("executing Flyway with strategy: %s".formatted(migrationStrategy)); + + FlywayMigrationStrategy strategy = + migrationStrategyConfig.flywayMigrationStrategy(migrationStrategy, migrationStrategy); + strategy.migrate(flyway); + return Result.OK; + } + + protected String jdbUrl() { + return ((HikariDataSource) dataSource).getJdbcUrl(); + } + + @Override + protected void printUsage() { + println( + """ + Database related operation like connection verification or migration. + + Arguments: + --check-connection verifies database access by open/close a connection + --migration-validate validate flyway migration + --migration-migrate executes flyway migration + + Example: + + database --check-connection --migration-validate + """); + } +} diff --git a/cli/src/main/java/org/ehrbase/cli/cmd/CliHelpCommand.java b/cli/src/main/java/org/ehrbase/cli/cmd/CliHelpCommand.java new file mode 100644 index 000000000..ad303c7cb --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/cmd/CliHelpCommand.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.cmd; + +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class CliHelpCommand extends CliCommand { + + public CliHelpCommand() { + super("help"); + } + + @Override + public void run(List args) { + if (!args.isEmpty()) { + exitFail("illegal arguments %s".formatted(args)); + } + + printStep("Help"); + printUsage(); + } + + @Override + protected void printUsage() { + println( + """ + Run with subcommand + + cli [sub-command] [arguments] + + Sub-commands: + database database related operation + help print this help message + + Examples: + + # show this help message + cli help + + # show help message of a sub-command + cli database + cli database help + + # execute sub-command with arguments + cli database --check-connection + """); + } +} diff --git a/cli/src/main/java/org/ehrbase/cli/config/CliAqlQueryContext.java b/cli/src/main/java/org/ehrbase/cli/config/CliAqlQueryContext.java new file mode 100644 index 000000000..f6b5ffc3b --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/config/CliAqlQueryContext.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.config; + +import java.net.URI; +import org.ehrbase.api.dto.AqlQueryContext; +import org.ehrbase.openehr.sdk.response.dto.MetaData; +import org.springframework.stereotype.Component; + +/** + * AQL query context implementation used for EHRbase CLI that can be configured using + * -aql-mode=default|dry_run|show_executed_aql|show_executed_sql|show_query_plane params. + */ +@Component(AqlQueryContext.BEAN_NAME) +public class CliAqlQueryContext implements AqlQueryContext { + + private static final String UNSUPPORTED_MSG = "AQL is not supported on CLI"; + + @Override + public MetaData createMetaData(URI location) { + throw new UnsupportedOperationException(UNSUPPORTED_MSG); + } + + @Override + public boolean isDryRun() { + throw new UnsupportedOperationException(UNSUPPORTED_MSG); + } + + @Override + public boolean showExecutedAql() { + throw new UnsupportedOperationException(UNSUPPORTED_MSG); + } + + @Override + public boolean showExecutedSql() { + throw new UnsupportedOperationException(UNSUPPORTED_MSG); + } + + @Override + public boolean showQueryPlan() { + throw new UnsupportedOperationException(UNSUPPORTED_MSG); + } + + @Override + public void setExecutedAql(String executedAql) { + throw new UnsupportedOperationException(UNSUPPORTED_MSG); + } + + @Override + public void setMetaProperty(MetaProperty property, Object value) { + throw new UnsupportedOperationException(UNSUPPORTED_MSG); + } +} diff --git a/cli/src/main/java/org/ehrbase/cli/config/CliOverwriteConfig.java b/cli/src/main/java/org/ehrbase/cli/config/CliOverwriteConfig.java new file mode 100644 index 000000000..075364c9d --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/config/CliOverwriteConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.config; + +import org.ehrbase.configuration.config.validation.ValidationConfiguration; +import org.ehrbase.openehr.sdk.validation.terminology.ExternalTerminologyValidation; +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class CliOverwriteConfig { + + @Bean + @Primary + public FlywayMigrationStrategy flywayMigrationStrategy() { + return flyway -> { + // Nop - prevent any flyway interaction + }; + } + + @Bean + public ExternalTerminologyValidation externalTerminologyValidator() { + return ValidationConfiguration.nopTerminologyValidation(); + } +} diff --git a/cli/src/main/java/org/ehrbase/cli/config/NopServerConfig.java b/cli/src/main/java/org/ehrbase/cli/config/NopServerConfig.java new file mode 100644 index 000000000..49f062259 --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/config/NopServerConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.config; + +import org.ehrbase.api.definitions.ServerConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "server") +public class NopServerConfig implements ServerConfig { + + private boolean disableStrictValidation = false; + + @Override + public int getPort() { + return -1; + } + + public void setPort(@SuppressWarnings("unused") int port) { + // ignored + } + + public void setDisableStrictValidation(boolean disableStrictValidation) { + this.disableStrictValidation = disableStrictValidation; + } + + @Override + public boolean isDisableStrictValidation() { + return this.disableStrictValidation; + } +} diff --git a/cli/src/main/java/org/ehrbase/cli/util/ExceptionFriendlyFunction.java b/cli/src/main/java/org/ehrbase/cli/util/ExceptionFriendlyFunction.java new file mode 100644 index 000000000..49ee42624 --- /dev/null +++ b/cli/src/main/java/org/ehrbase/cli/util/ExceptionFriendlyFunction.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.util; + +@FunctionalInterface +public interface ExceptionFriendlyFunction { + + @SuppressWarnings("java:S112") + R apply(T value) throws Exception; +} diff --git a/cli/src/test/java/org/ehrbase/cli/CliRunnerTest.java b/cli/src/test/java/org/ehrbase/cli/CliRunnerTest.java new file mode 100644 index 000000000..d7fc4e453 --- /dev/null +++ b/cli/src/test/java/org/ehrbase/cli/CliRunnerTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import org.ehrbase.cli.cmd.CliCommand; +import org.ehrbase.cli.cmd.CliHelpCommand; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class CliRunnerTest { + + private class TestCliCommand extends CliCommand { + + public List args; + public boolean didRun = false; + public boolean didPrintUsage = false; + + public TestCliCommand(String name) { + super(name); + } + + @Override + public void run(List args) { + this.didRun = true; + this.args = args; + } + + @Override + protected void printUsage() { + this.didPrintUsage = true; + } + } + + private CliHelpCommand mockHelpCommand = mock(CliHelpCommand.class); + + @BeforeEach + void setUp() { + Mockito.reset(mockHelpCommand); + } + + private CliRunner cliRunner(CliCommand... commands) { + return new CliRunner(Arrays.stream(commands).toList(), mockHelpCommand); + } + + @Test + void duplicateCommandError() { + + var cmd1 = new TestCliCommand("duplicate-cmd"); + var cmd2 = new TestCliCommand("duplicate-cmd"); + CliRunner cliRunner = cliRunner(cmd1, cmd2); + String message = + assertThrows(IllegalStateException.class, cliRunner::run).getMessage(); + assertEquals( + "Duplicate command for name duplicate-cmd (attempted merging values %s and %s)".formatted(cmd1, cmd2), + message); + } + + @Test + void runErrorWithoutArguments() { + + cliRunner().run(); + verify(mockHelpCommand).exitFail("No command specified"); + } + + @Test + void runErrorWithoutCliArguments() { + + cliRunner().run("--some --other=true --command"); + verify(mockHelpCommand).exitFail("No command specified"); + } + + @Test + void runErrorCommandNotExist() { + + cliRunner().run("cli", "does-not-exist"); + verify(mockHelpCommand).exitFail("Unknown command does-not-exist"); + } + + @Test + void runCommand() { + + var cmd = new TestCliCommand("capture-the-flag"); + cliRunner(cmd).run(CliRunner.CLI, "capture-the-flag"); + + assertTrue(cmd.didRun, "command did not run"); + assertNotNull(cmd.args); + } + + @Test + void runCommandHelp() { + + var cmd = new TestCliCommand("capture-the-flag"); + cliRunner(cmd).run(CliRunner.CLI, "capture-the-flag", "help"); + + assertTrue(cmd.didRun, "command did not run"); + assertNotNull(cmd.args); + } + + @Test + void runCommandWithArgument() { + + var cmd = new TestCliCommand("capture-the-flag"); + cliRunner(cmd).run(CliRunner.CLI, "capture-the-flag", "--flag=true"); + + assertTrue(cmd.didRun, "command did not run"); + assertEquals(1, cmd.args.size()); + assertEquals("--flag=true", cmd.args.getFirst()); + } +} diff --git a/cli/src/test/java/org/ehrbase/cli/cmd/CliDataBaseCommandTest.java b/cli/src/test/java/org/ehrbase/cli/cmd/CliDataBaseCommandTest.java new file mode 100644 index 000000000..1bec63c05 --- /dev/null +++ b/cli/src/test/java/org/ehrbase/cli/cmd/CliDataBaseCommandTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.cmd; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.zaxxer.hikari.HikariDataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.util.List; +import org.ehrbase.configuration.config.flyway.MigrationStrategy; +import org.ehrbase.configuration.config.flyway.MigrationStrategyConfig; +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; + +class CliDataBaseCommandTest { + + private final HikariDataSource dataSource = mock(); + private final Flyway flyway = mock(); + private final MigrationStrategyConfig migrationStrategyConfig = spy(new MigrationStrategyConfig()); + private final FlywayMigrationStrategy strategy = mock(); + + private final CliDataBaseCommand cmd = spy(new CliDataBaseCommand(dataSource, flyway, migrationStrategyConfig)); + + @BeforeEach + void setUp() { + Mockito.reset(cmd, dataSource, flyway, migrationStrategyConfig, strategy); + doNothing().when(cmd).exit(any(Integer.class)); + doNothing().when(cmd).println(any()); + } + + @Test + void commandNameIsHelp() { + assertEquals("database", cmd.getName()); + } + + @Test + void runWithoutArgumentError() throws Exception { + + cmd.run(List.of()); + + verify(cmd, times(1)).printUsage(); + verify(cmd, times(1)).exitFail("No argument provided"); + verify(cmd, times(1)).exit(-1); + } + + @Test + void runWithUnknownArgumentError() throws Exception { + + cmd.run(List.of("illegal")); + + verify(cmd, times(1)).printUsage(); + verify(cmd, times(1)).exitFail("Unknown argument [illegal]"); + verify(cmd, times(1)).exit(-1); + } + + @Test + void runHelp() throws Exception { + + cmd.run(List.of("help")); + + verify(cmd, times(1)).printUsage(); + verify(cmd, times(1)).printUsage(); + verify(cmd, never()).exitFail(any()); + verify(cmd, never()).exit(any(Integer.class)); + } + + @Test + void runCheckConnection() throws Exception { + + var jdbUrl = "jdbc:test//localhost:1234/db"; + doReturn(jdbUrl).when(dataSource).getJdbcUrl(); + + DatabaseMetaData metaData = mock(); + doReturn(jdbUrl).when(metaData).getURL(); + + Connection connection = mock(); + doReturn(metaData).when(connection).getMetaData(); + + doReturn(connection).when(dataSource).getConnection(); + + cmd.run(List.of("--check-connection")); + + verify(cmd, never()).printUsage(); + verify(cmd, never()).exit(any(Integer.class)); + } + + @Test + void runMigrationVerify() throws Exception { + + runMigrationTest("--migration-validate", MigrationStrategy.VALIDATE); + } + + @Test + void runMigrationMigrate() throws Exception { + + runMigrationTest("--migration-migrate", MigrationStrategy.MIGRATE); + } + + private void runMigrationTest(String arg, MigrationStrategy migrationStrategy) throws Exception { + doReturn(strategy).when(migrationStrategyConfig).flywayMigrationStrategy(migrationStrategy, migrationStrategy); + + cmd.run(List.of(arg)); + + verify(strategy, times(1)).migrate(flyway); + } +} diff --git a/cli/src/test/java/org/ehrbase/cli/cmd/CliHelpCommandTest.java b/cli/src/test/java/org/ehrbase/cli/cmd/CliHelpCommandTest.java new file mode 100644 index 000000000..873df1777 --- /dev/null +++ b/cli/src/test/java/org/ehrbase/cli/cmd/CliHelpCommandTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.cmd; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class CliHelpCommandTest { + + private final CliHelpCommand cmd = spy(new CliHelpCommand()); + + @BeforeEach + void setUp() { + Mockito.reset(cmd); + doNothing().when(cmd).exit(any(Integer.class)); + doNothing().when(cmd).println(any()); + } + + @Test + void commandNameIsHelp() { + assertEquals("help", cmd.getName()); + } + + @Test + void runWithArgumentError() { + + cmd.run(List.of("invalid")); + + verify(cmd, times(1)).exitFail("illegal arguments [invalid]"); + verify(cmd, times(1)).exit(-1); + } + + @Test + void runWithoutArgument() { + + cmd.run(List.of()); + + verify(cmd, times(1)).printUsage(); + verify(cmd, never()).exit(any(Integer.class)); + } +} diff --git a/cli/src/test/java/org/ehrbase/cli/config/CliOverwriteConfigTest.java b/cli/src/test/java/org/ehrbase/cli/config/CliOverwriteConfigTest.java new file mode 100644 index 000000000..1f1859d6e --- /dev/null +++ b/cli/src/test/java/org/ehrbase/cli/config/CliOverwriteConfigTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.cli.config; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +import org.ehrbase.configuration.config.validation.NopExternalTerminologyValidation; +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CliOverwriteConfigTest { + + private final CliOverwriteConfig config = new CliOverwriteConfig(); + + @Test + void nopFlywayMigrationStrategy() { + + Flyway flyway = mock(); + config.flywayMigrationStrategy().migrate(flyway); + verifyNoInteractions(flyway); + } + + @Test + void nopExternalTerminologyValidator() { + + Assertions.assertInstanceOf(NopExternalTerminologyValidation.class, config.externalTerminologyValidator()); + } +} diff --git a/configuration/pom.xml b/configuration/pom.xml new file mode 100644 index 000000000..5bf570285 --- /dev/null +++ b/configuration/pom.xml @@ -0,0 +1,155 @@ + + + + + + + 4.0.0 + + + org.ehrbase.openehr + server + 2.7.0 + + + configuration + jar + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-validation + + + io.netty + netty-resolver-dns-native-macos + osx-aarch_64 + + + io.micrometer + micrometer-registry-prometheus + + + org.flywaydb + flyway-core + + + + org.ehrbase.openehr + service + + + org.ehrbase.openehr + rest-ehr-scape + + + org.ehrbase.openehr + api + + + org.ehrbase.openehr + rest-openehr + + + org.ehrbase.openehr.sdk + serialisation + + + + net.bull.javamelody + javamelody-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + test + + + org.springframework.security + spring-security-test + test + + + org.ehrbase.openehr.sdk + test-data + test + + + + + + diff --git a/configuration/src/main/java/org/ehrbase/configuration/EhrBaseCliConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/EhrBaseCliConfiguration.java new file mode 100644 index 000000000..c95985ebd --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/EhrBaseCliConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration; + +import org.ehrbase.ServiceModuleConfiguration; +import org.ehrbase.configuration.config.flyway.MigrationStrategyConfig; +import org.ehrbase.openehr.aqlengine.AqlEngineModuleConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({ServiceModuleConfiguration.class, AqlEngineModuleConfiguration.class, MigrationStrategyConfig.class}) +public class EhrBaseCliConfiguration {} diff --git a/configuration/src/main/java/org/ehrbase/configuration/EhrBaseServerConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/EhrBaseServerConfiguration.java new file mode 100644 index 000000000..39e60758f --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/EhrBaseServerConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration; + +import org.ehrbase.ServiceModuleConfiguration; +import org.ehrbase.openehr.aqlengine.AqlEngineModuleConfiguration; +import org.ehrbase.rest.RestModuleConfiguration; +import org.ehrbase.rest.ehrscape.RestEHRScapeModuleConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@ComponentScan +@Import({ + ServiceModuleConfiguration.class, + RestModuleConfiguration.class, + RestEHRScapeModuleConfiguration.class, + AqlEngineModuleConfiguration.class, +}) +// @ComponentScan("org.ehrbase.configuration") +public class EhrBaseServerConfiguration {} diff --git a/application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java b/configuration/src/main/java/org/ehrbase/configuration/config/HttpClientConfig.java similarity index 93% rename from application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java rename to configuration/src/main/java/org/ehrbase/configuration/config/HttpClientConfig.java index d023a532b..e8daa1021 100644 --- a/application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/HttpClientConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config; +package org.ehrbase.configuration.config; import java.net.InetSocketAddress; import java.net.ProxySelector; diff --git a/application/src/main/java/org/ehrbase/application/config/MeterRegistryCustomizerConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/MeterRegistryCustomizerConfiguration.java similarity index 88% rename from application/src/main/java/org/ehrbase/application/config/MeterRegistryCustomizerConfiguration.java rename to configuration/src/main/java/org/ehrbase/configuration/config/MeterRegistryCustomizerConfiguration.java index 5d4ae9cb8..ae4234ede 100644 --- a/application/src/main/java/org/ehrbase/application/config/MeterRegistryCustomizerConfiguration.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/MeterRegistryCustomizerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config; +package org.ehrbase.configuration.config; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Value; diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/ServerConfigImp.java b/configuration/src/main/java/org/ehrbase/configuration/config/ServerConfigImp.java new file mode 100644 index 000000000..ae5810993 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/ServerConfigImp.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "server") +public class ServerConfigImp implements org.ehrbase.api.definitions.ServerConfig { + + @Min(1025) + @Max(65536) + private int port; + + private boolean disableStrictValidation = false; + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + @Override + public boolean isDisableStrictValidation() { + return disableStrictValidation; + } + + public void setDisableStrictValidation(boolean disableStrictValidation) { + this.disableStrictValidation = disableStrictValidation; + } +} diff --git a/application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/SwaggerConfiguration.java similarity index 80% rename from application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java rename to configuration/src/main/java/org/ehrbase/configuration/config/SwaggerConfiguration.java index da0e7db60..94c15e801 100644 --- a/application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/SwaggerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,13 +15,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config; +package org.ehrbase.configuration.config; import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; +import java.util.stream.Stream; import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -68,6 +71,19 @@ public GroupedOpenApi actuatorApi() { .build(); } + @Bean + @ConditionalOnProperty(name = "ehrbase.rest.experimental.tags.enabled", havingValue = "true") + public GroupedOpenApi experimentalApi( + @Value("${ehrbase.rest.experimental.tags.context-path:/rest/experimental/tags}") String path) { + return GroupedOpenApi.builder() + .group("6. Experimental API") + .pathsToMatch(Stream.of(path) + .map(p -> "/%s/**".formatted(p.replaceFirst("/", "").replaceFirst("^/", ""))) + .toList() + .toArray(String[]::new)) + .build(); + } + @Bean public OpenAPI ehrBaseOpenAPI() { return new OpenAPI() diff --git a/application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientConfiguration.java similarity index 96% rename from application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java rename to configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientConfiguration.java index e886dade0..b8f6da519 100644 --- a/application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.client; +package org.ehrbase.configuration.config.client; import java.io.IOException; import java.security.KeyManagementException; diff --git a/application/src/main/java/org/ehrbase/application/config/client/HttpClientProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientProperties.java similarity index 95% rename from application/src/main/java/org/ehrbase/application/config/client/HttpClientProperties.java rename to configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientProperties.java index c6d5fdbcf..5f567d368 100644 --- a/application/src/main/java/org/ehrbase/application/config/client/HttpClientProperties.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.client; +package org.ehrbase.configuration.config.client; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/flyway/MigrationStrategy.java b/configuration/src/main/java/org/ehrbase/configuration/config/flyway/MigrationStrategy.java new file mode 100644 index 000000000..ab4ca86a7 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/flyway/MigrationStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.flyway; + +import java.util.function.Consumer; +import org.flywaydb.core.Flyway; + +public enum MigrationStrategy { + DISABLED(f -> {}), + MIGRATE(Flyway::migrate), + VALIDATE(Flyway::validate); + + private final Consumer strategy; + + MigrationStrategy(Consumer strategy) { + this.strategy = strategy; + } + + public void applyStrategy(Flyway flyway) { + strategy.accept(flyway); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/flyway/MigrationStrategyConfig.java b/configuration/src/main/java/org/ehrbase/configuration/config/flyway/MigrationStrategyConfig.java new file mode 100644 index 000000000..837191bca --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/flyway/MigrationStrategyConfig.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.flyway; + +import java.util.Map; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MigrationStrategyConfig { + + private static final Logger log = LoggerFactory.getLogger(MigrationStrategyConfig.class); + + @Value("${spring.flyway.ehr-schema:ehr}") + private String ehrSchema; + + @Value("${spring.flyway.ext-schema:ext}") + private String extSchema; + + @Value("${spring.flyway.ehr-location:classpath:db/migration/ehr}") + private String ehrLocation; + + @Value("${spring.flyway.ext-location:classpath:db/migration/ext}") + private String extLocation; + + @Value("${spring.flyway.ext-strategy:MIGRATE}") + private MigrationStrategy extStrategy = MigrationStrategy.MIGRATE; + + @Value("${spring.flyway.ehr-strategy:MIGRATE}") + private MigrationStrategy ehrStrategy = MigrationStrategy.MIGRATE; + + @Bean + public FlywayMigrationStrategy flywayMigrationStrategy() { + return flywayMigrationStrategy(extStrategy, ehrStrategy); + } + + public FlywayMigrationStrategy flywayMigrationStrategy( + MigrationStrategy extStrategy, MigrationStrategy ehrStrategy) { + return flyway -> { + if (extStrategy != MigrationStrategy.DISABLED) { + extStrategy.applyStrategy(setSchema(flyway, extSchema) + .locations(extLocation) + // ext was not yet managed by flyway + .baselineOnMigrate(true) + .baselineVersion("1") + .placeholders(Map.of("extSchema", extSchema)) + .load()); + } else { + log.info("Flyway migration for schema 'ext' is disabled"); + } + if (ehrStrategy != MigrationStrategy.DISABLED) { + ehrStrategy.applyStrategy(setSchema(flyway, ehrSchema) + .placeholders(Map.of("ehrSchema", ehrSchema)) + .locations(ehrLocation) + .load()); + } else { + log.info("Flyway migration for schema 'ehr' is disabled"); + } + }; + } + + private FluentConfiguration setSchema(Flyway flyway, String schema) { + return Flyway.configure() + .dataSource(flyway.getConfiguration().getDataSource()) + .schemas(schema); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/jackson/DtoDeSerializer.java b/configuration/src/main/java/org/ehrbase/configuration/config/jackson/DtoDeSerializer.java new file mode 100644 index 000000000..1b042d566 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/jackson/DtoDeSerializer.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.jackson; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import java.io.IOException; +import java.util.Objects; +import org.ehrbase.openehr.sdk.serialisation.jsonencoding.CanonicalJson; + +/** + * Deserializer that allows to use arbitrary object that contain RmObjects as parameters. Can be used + * for custom DTOs. + * @param Of the DTO to deserialize. + */ +class DtoDeSerializer extends StdDeserializer { + + private final String typeNode; + + public DtoDeSerializer(Class vc, String type) { + super(vc); + this.typeNode = "\"%s\"".formatted(type); + } + + @SuppressWarnings("unchecked") + public T deserialize(JsonParser parser, DeserializationContext context) throws IOException { + // rad as nodes + TreeNode root = parser.readValueAsTree(); + validateType(parser, root); + // return interpretation as handled type + return (T) CanonicalJson.MARSHAL_OM.convertValue(root, handledType()); + } + + private void validateType(JsonParser p, TreeNode root) throws JsonParseException { + TreeNode type = root.get("_type"); + if (type == null) { + throw new JsonParseException(p, "Missing [_type] value"); + } else if (!Objects.equals(type.toString(), typeNode)) { + throw new JsonParseException( + p, "Unexpected [_type] value [%s] not matching [%s]".formatted(type, typeNode)); + } + } +} diff --git a/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/jackson/JacksonConfiguration.java similarity index 85% rename from application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java rename to configuration/src/main/java/org/ehrbase/configuration/config/jackson/JacksonConfiguration.java index edf4b0f10..bd6f096f1 100644 --- a/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/jackson/JacksonConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2019-2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,18 +15,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config; +package org.ehrbase.configuration.config.jackson; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.nedap.archie.rm.RMObject; import com.nedap.archie.rm.directory.Folder; -import com.nedap.archie.rm.ehr.EhrStatus; +import org.ehrbase.api.dto.EhrStatusDto; import org.ehrbase.api.mapper.StructuredStringJSonSerializer; import org.ehrbase.openehr.sdk.response.dto.ehrscape.StructuredString; import org.ehrbase.openehr.sdk.serialisation.mapper.RmObjectJsonDeSerializer; import org.ehrbase.openehr.sdk.serialisation.mapper.RmObjectJsonSerializer; +import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,9 +42,11 @@ public class JacksonConfiguration { public Jackson2ObjectMapperBuilderCustomizer addCustomSerialization() { return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder .serializerByType(StructuredString.class, new StructuredStringJSonSerializer()) + // RMObject support .serializerByType(RMObject.class, new RmObjectJsonSerializer()) - .deserializerByType(EhrStatus.class, new RmObjectJsonDeSerializer()) .deserializerByType(Folder.class, new RmObjectJsonDeSerializer()) + // DTOs with RMObjects support + .deserializers(new DtoDeSerializer<>(EhrStatusDto.class, RmConstants.EHR_STATUS)) .modules(new JavaTimeModule()); } diff --git a/application/src/main/java/org/ehrbase/application/config/plugin/EhrBasePluginManager.java b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/EhrBasePluginManager.java similarity index 96% rename from application/src/main/java/org/ehrbase/application/config/plugin/EhrBasePluginManager.java rename to configuration/src/main/java/org/ehrbase/configuration/config/plugin/EhrBasePluginManager.java index ede9bab16..fa26b99cc 100644 --- a/application/src/main/java/org/ehrbase/application/config/plugin/EhrBasePluginManager.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/EhrBasePluginManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.plugin; +package org.ehrbase.configuration.config.plugin; import java.io.IOException; import java.nio.file.Files; @@ -41,9 +41,6 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.io.FileSystemResource; -/** - * @author Stefan Spiska - */ public class EhrBasePluginManager extends SpringPluginManager implements EhrBasePluginManagerInterface { private static final Map PROPERTY_SOURCE_LOADER_MAP = Stream.of( diff --git a/application/src/main/java/org/ehrbase/application/config/plugin/PluginConfig.java b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginConfig.java similarity index 93% rename from application/src/main/java/org/ehrbase/application/config/plugin/PluginConfig.java rename to configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginConfig.java index 982646cb1..74b0c30bb 100644 --- a/application/src/main/java/org/ehrbase/application/config/plugin/PluginConfig.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.plugin; +package org.ehrbase.configuration.config.plugin; import static org.ehrbase.plugin.PluginHelper.PLUGIN_MANAGER_PREFIX; @@ -38,9 +38,6 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.util.UriComponentsBuilder; -/** - * @author Stefan Spiska - */ @Configuration @EnableConfigurationProperties(PluginManagerProperties.class) @ConditionalOnProperty(prefix = PLUGIN_MANAGER_PREFIX, name = "enable", havingValue = "true") @@ -53,7 +50,7 @@ public EhrBasePluginManager pluginManager(Environment environment) { } // since this is used in a BeanFactoryPostProcessor the PluginManagerProperties must be bound // manually. - private PluginManagerProperties getPluginManagerProperties(Environment environment) { + private static PluginManagerProperties getPluginManagerProperties(Environment environment) { return Binder.get(environment) .bind(PLUGIN_MANAGER_PREFIX, PluginManagerProperties.class) .get(); @@ -61,7 +58,7 @@ private PluginManagerProperties getPluginManagerProperties(Environment environme /** Register the {@link DispatcherServlet} for all {@link WebMvcEhrBasePlugin} */ @Bean - public BeanFactoryPostProcessor beanFactoryPostProcessor( + public static BeanFactoryPostProcessor beanFactoryPostProcessor( EhrBasePluginManager pluginManager, Environment environment) { PluginManagerProperties pluginManagerProperties = getPluginManagerProperties(environment); @@ -87,7 +84,7 @@ public BeanFactoryPostProcessor beanFactoryPostProcessor( * @param registeredUrl * @param p */ - private void register( + private static void register( ConfigurableListableBeanFactory beanFactory, PluginManagerProperties pluginManagerProperties, Map registeredUrl, diff --git a/application/src/main/java/org/ehrbase/application/config/plugin/PluginManagerProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginManagerProperties.java similarity index 90% rename from application/src/main/java/org/ehrbase/application/config/plugin/PluginManagerProperties.java rename to configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginManagerProperties.java index 89b38d70f..1f37978fc 100644 --- a/application/src/main/java/org/ehrbase/application/config/plugin/PluginManagerProperties.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginManagerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.plugin; +package org.ehrbase.configuration.config.plugin; import static org.ehrbase.plugin.PluginHelper.PLUGIN_MANAGER_PREFIX; @@ -23,7 +23,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** - * @author Stefan Spiska *

{@link ConfigurationProperties} for {@link EhrBasePluginManager}. */ @ConfigurationProperties(prefix = PLUGIN_MANAGER_PREFIX) diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfig.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfig.java new file mode 100644 index 000000000..d81af8464 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfig.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.security; + +import static org.ehrbase.configuration.config.security.SecurityProperties.AccessType; +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * Common Security config interface that allows to secure the spring actuator endpoints in common way between basic-auth + * and oauth2 authentication. + */ +public abstract sealed class SecurityConfig permits SecurityConfigNoOp, SecurityConfigBasicAuth, SecurityConfigOAuth2 { + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Spring boot actuator properties + */ + protected final WebEndpointProperties webEndpointProperties; + /** + * Extended property on spring actuator config that defines who can access the management endpoint. + */ + @Value("${management.endpoints.web.access:ADMIN_ONLY}") + protected SecurityProperties.AccessType managementEndpointsAccessType; + + protected SecurityConfig(WebEndpointProperties webEndpointProperties) { + this.webEndpointProperties = webEndpointProperties; + } + + protected abstract HttpSecurity configureHttpSecurity(HttpSecurity http) throws Exception; + + /** + * Configures the /management/** endpoint access + */ + protected AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry + configureManagementEndpointAccess( + AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry auth, + String adminRoleSupplier, + List privateRolesSupplier) { + + logger.info("Management endpoint access type {}", managementEndpointsAccessType); + + var managementAuthorizedUrl = auth.requestMatchers(antMatcher(webEndpointProperties.getBasePath() + "/**")); + + logger.debug("Management endpoints base path {}", managementEndpointsAccessType); + + return switch (managementEndpointsAccessType) { + // management endpoints are locked behind an authorization + // and are only available for users with the admin role + case AccessType.ADMIN_ONLY -> managementAuthorizedUrl.hasRole(adminRoleSupplier); + // management endpoints are locked behind an authorization, but are available to any role + case AccessType.PRIVATE -> managementAuthorizedUrl.hasAnyRole( + privateRolesSupplier.toArray(new String[] {})); + // management endpoints can be accessed without an authorization + case AccessType.PUBLIC -> managementAuthorizedUrl.permitAll(); + }; + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigBasicAuth.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigBasicAuth.java new file mode 100644 index 000000000..4b670256a --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigBasicAuth.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.security; + +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +import java.util.List; +import javax.annotation.PostConstruct; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.provisioning.UserDetailsManager; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.stereotype.Component; + +/** + * {@link Component} for Basic authentication. + */ +@Component +@ConditionalOnProperty(prefix = "security", name = "authType", havingValue = "basic") +public final class SecurityConfigBasicAuth extends SecurityConfig { + + // Roles, when not using OAuth2 + public static final String ADMIN = "ADMIN"; + + public static final String USER = "USER"; + + public SecurityConfigBasicAuth(WebEndpointProperties webEndpointProperties) { + super(webEndpointProperties); + } + + @PostConstruct + public void initialize() { + logger.info("Using basic authentication"); + } + + @Override + public HttpSecurity configureHttpSecurity(HttpSecurity http) throws Exception { + + return http.addFilterBefore(new SecurityFilter(), BasicAuthenticationFilter.class) + .authorizeHttpRequests(auth -> { + + // respond with proper 404 instead of 401 when reaching a route that does not exist. To understand + // this case, since Spring 6 all paths are secured, when you use any path doesn't exist that will + // match to "/error" but "/error" have been secured, so you need to add "/error" to your permitAll() + // config to respond with a proper 404. + auth = auth.requestMatchers(antMatcher("/error/**")).permitAll(); + + // secure /rest/admin/** so that only admins can access it + auth = auth.requestMatchers(antMatcher("/rest/admin/**")).hasRole(ADMIN); + + // secure /management/** + auth = configureManagementEndpointAccess(auth, ADMIN, List.of(ADMIN, USER)); + + // secure all other requests using either user and/or admin roles + auth.anyRequest().hasAnyRole(ADMIN, USER); + }) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .httpBasic(Customizer.withDefaults()); + } + + @SuppressWarnings("deprecation") + @Bean + public PasswordEncoder passwordEncoder() { + // We use a nop encoder because BCrypt slows down request by 10x on some systems + return NoOpPasswordEncoder.getInstance(); + } + + @Bean + public UserDetailsManager userDetailsManager(SecurityProperties properties, PasswordEncoder passwordEncoder) { + + return new InMemoryUserDetailsManager( + User.withUsername(properties.getAuthUser()) + .password(properties.getAuthPassword()) + .roles(USER) + .passwordEncoder(passwordEncoder::encode) + .build(), + User.withUsername(properties.getAuthAdminUser()) + .password(properties.getAuthAdminPassword()) + .roles(ADMIN) + .passwordEncoder(passwordEncoder::encode) + .build()); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigNoOp.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigNoOp.java new file mode 100644 index 000000000..3b34dc30a --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigNoOp.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.security; + +import java.util.List; +import javax.annotation.PostConstruct; +import org.ehrbase.service.IAuthenticationFacade; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * {@link Component} used when security is disabled. + */ +@Component +@ConditionalOnProperty(prefix = "security", name = "auth-type", havingValue = "none", matchIfMissing = true) +public final class SecurityConfigNoOp extends SecurityConfig { + + public SecurityConfigNoOp(WebEndpointProperties webEndpointProperties) { + super(webEndpointProperties); + } + + @PostConstruct + public void initialize() { + logger.warn("Security is disabled. Configure 'security.auth-type' to disable this warning."); + } + + @Bean + @Primary + public IAuthenticationFacade anonymousAuthentication() { + var filter = new AnonymousAuthenticationFilter("key"); + return () -> new AnonymousAuthenticationToken("key", filter.getPrincipal(), filter.getAuthorities()); + } + + /** + * We already log our own warning in the {@link #initialize()} post construction. + * + * Here we suppress spring warning during + * {@link org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration} + * initialization: + * + * Using generated security password: {SOME UUID} + * + * This generated password is for development use only. Your security configuration must be updated before running your application in production. + * + * + * The reason for this warning is that spring.security.user.password is not configured and no auth + * is used at all. In such cases the spring.security.user.password will be a generated + * UUID4 {@link org.springframework.boot.autoconfigure.security.SecurityProperties.User}. But we start + * the app with security enabled to be able to use an external oauth2 client. + */ + @Bean + public InMemoryUserDetailsManager inMemoryUserDetailsManager( + org.springframework.boot.autoconfigure.security.SecurityProperties properties) { + final org.springframework.boot.autoconfigure.security.SecurityProperties.User user = properties.getUser(); + final List roles = user.getRoles(); + return new InMemoryUserDetailsManager(User.withUsername(user.getName()) + .password(user.getPassword()) + .roles(StringUtils.toStringArray(roles)) + .build()); + } + + /** + * Configure our used security chain by removing the default httpBasic config as well as + * logout config. + * + * Use @EnableWebSecurity(debug = true) on {@link SecurityConfiguration} to enable debug output and + * verify the actual used filter chain. + */ + @Override + public HttpSecurity configureHttpSecurity(HttpSecurity http) throws Exception { + return http + // there is no basic auth available -> so let's remove them completely from the filter chain + .httpBasic(AbstractHttpConfigurer::disable) + // without login -> logout makes no sense + .logout(AbstractHttpConfigurer::disable); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigOAuth2.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigOAuth2.java new file mode 100644 index 000000000..a83df4379 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigOAuth2.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.security; + +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import javax.annotation.PostConstruct; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; +import org.springframework.stereotype.Component; + +/** + * {@link Component} for OAuth2 authentication. + */ +@Component +@ConditionalOnProperty(prefix = "security", name = "auth-type", havingValue = "oauth") +public final class SecurityConfigOAuth2 extends SecurityConfig { + + public static final String PROFILE_SCOPE = "PROFILE"; + + private final SecurityProperties securityProperties; + + private final OAuth2ResourceServerProperties oAuth2Properties; + + public SecurityConfigOAuth2( + SecurityProperties securityProperties, + OAuth2ResourceServerProperties oAuth2Properties, + WebEndpointProperties webEndpointProperties) { + super(webEndpointProperties); + this.securityProperties = securityProperties; + this.oAuth2Properties = oAuth2Properties; + } + + @PostConstruct + public void initialize() { + logger.info("Using OAuth2 authentication"); + logger.debug("Using issuer URI: {}", oAuth2Properties.getJwt().getIssuerUri()); + logger.debug("Using user role: {}", securityProperties.getOauth2UserRole()); + logger.debug("Using admin role: {}", securityProperties.getOauth2AdminRole()); + } + + @Override + public HttpSecurity configureHttpSecurity(HttpSecurity http) throws Exception { + + final String userRole = securityProperties.getOauth2UserRole(); + final String adminRole = securityProperties.getOauth2AdminRole(); + + return http.addFilterBefore(new SecurityFilter(), BearerTokenAuthenticationFilter.class) + .authorizeHttpRequests(auth -> { + + // respond with proper 404 instead of 401 when reaching a route that does not exist. To understand + // this case, since Spring 6 all paths are secured, when you use any path doesn't exist that will + // match to "/error" but "/error" have been secured, so you need to add "/error" to your permitAll() + // config to respond with a proper 404. + auth = auth.requestMatchers(antMatcher("/error/**")).permitAll(); + + // secure /rest/admin/** so that only admins can access it + auth = auth.requestMatchers(antMatcher("/rest/admin/**")).hasRole(adminRole); + + // secure /management/** + auth = configureManagementEndpointAccess( + auth, adminRole, List.of(adminRole, userRole, PROFILE_SCOPE)); + + // secure all other requests using either user and/or admin roles + auth.anyRequest().hasAnyRole(adminRole, userRole, PROFILE_SCOPE); + }) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .oauth2ResourceServer( + server -> server.jwt(jwt -> jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter()))); + } + + // Converter creates list of "ROLE_*" (upper case) authorities for each "realm access" role + // and "roles" role from JWT + @SuppressWarnings("unchecked") + private Converter getJwtAuthenticationConverter() { + var converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + Map realmAccess; + realmAccess = (Map) jwt.getClaims().get("realm_access"); + + Collection authority = new HashSet<>(); + if (realmAccess != null && realmAccess.containsKey("roles")) { + authority.addAll(((List) realmAccess.get("roles")) + .stream() + .map(roleName -> "ROLE_" + roleName.toUpperCase()) + .map(SimpleGrantedAuthority::new) + .toList()); + } + + if (jwt.getClaims().containsKey("scope")) { + authority.addAll( + Arrays.stream(jwt.getClaims().get("scope").toString().split(" ")) + .map(roleName -> "ROLE_" + roleName.toUpperCase()) + .map(SimpleGrantedAuthority::new) + .toList()); + } + return authority; + }); + return converter; + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfiguration.java new file mode 100644 index 000000000..4d59ffcb5 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfiguration.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.security; + +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.web.SecurityFilterChain; + +/** + * {@link Configuration} for secured endpoint authentication. + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({SecurityProperties.class}) +@Import({SecurityConfigNoOp.class, SecurityConfigOAuth2.class, SecurityConfigBasicAuth.class}) +@EnableWebSecurity +public class SecurityConfiguration { + + private final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class); + + private final SecurityConfig securityConfig; + + @Value("${ehrbase.security.management.endpoints.web.csrf-validation-enabled:true}") + protected boolean managementEndpointsCSRFValidationEnabled; + + public SecurityConfiguration(SecurityConfig securityConfig) { + this.securityConfig = securityConfig; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + return securityConfig + .configureHttpSecurity(http) + // CORS will be always enabled + .cors(Customizer.withDefaults()) + // Exclude apis from CSRF protection, to allow POST, PUT, DELETE, because there are used by client + // implementation and not only restricted to a browser access. + .csrf(csrf -> { + csrf.ignoringRequestMatchers( + antMatcher("/rest/**"), // allow full access to the rest api + antMatcher("/plugin/**"), // allow full access to plugin apis + antMatcher("/error/**") // ensure we have access to error re-routing + ); + // disable csrf in case 'management.endpoints.web.csrf-validation-enabled=false' is defined + if (!managementEndpointsCSRFValidationEnabled) { + logger.info("Management endpoint csrf security is disabled"); + String path = StringUtils.removeEnd(securityConfig.webEndpointProperties.getBasePath(), "/"); + csrf.ignoringRequestMatchers(antMatcher(path + "/**")); + } + }) + .build(); + } + + @Bean + @Conditional({ClientsConfiguredCondition.class}) + public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegRep, OAuth2AuthorizedClientRepository authrClientRep) { + OAuth2AuthorizedClientProvider authrClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + + DefaultOAuth2AuthorizedClientManager authrClientMngr = + new DefaultOAuth2AuthorizedClientManager(clientRegRep, authrClientRep); + authrClientMngr.setAuthorizedClientProvider(authrClientProvider); + return authrClientMngr; + } +} diff --git a/application/src/main/java/org/ehrbase/application/config/security/SecurityFilter.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityFilter.java similarity index 75% rename from application/src/main/java/org/ehrbase/application/config/security/SecurityFilter.java rename to configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityFilter.java index 4618c0b65..10fcf61c4 100644 --- a/application/src/main/java/org/ehrbase/application/config/security/SecurityFilter.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,9 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.security; +package org.ehrbase.configuration.config.security; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import java.io.IOException; import org.springframework.security.core.context.SecurityContextHolder; diff --git a/application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityProperties.java similarity index 88% rename from application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java rename to configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityProperties.java index ab172aa40..5078dcf87 100644 --- a/application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,18 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.security; +package org.ehrbase.configuration.config.security; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "security") public class SecurityProperties { - // Roles, when not using OAuth2 - public static final String ADMIN = "ADMIN"; - - public static final String USER = "USER"; - /** * Authentication type. */ @@ -123,4 +118,13 @@ public enum AuthTypes { BASIC, OAUTH } + + /** + * Supported values for the management.endpoints.web.access property value. + */ + public enum AccessType { + ADMIN_ONLY, + PRIVATE, + PUBLIC + } } diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/validation/NopExternalTerminologyValidation.java b/configuration/src/main/java/org/ehrbase/configuration/config/validation/NopExternalTerminologyValidation.java new file mode 100644 index 000000000..deb0810e9 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/validation/NopExternalTerminologyValidation.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.validation; + +import com.nedap.archie.rm.datavalues.DvCodedText; +import java.util.Collections; +import java.util.List; +import org.ehrbase.openehr.sdk.util.functional.Try; +import org.ehrbase.openehr.sdk.validation.ConstraintViolation; +import org.ehrbase.openehr.sdk.validation.ConstraintViolationException; +import org.ehrbase.openehr.sdk.validation.terminology.ExternalTerminologyValidation; +import org.ehrbase.openehr.sdk.validation.terminology.TerminologyParam; + +public class NopExternalTerminologyValidation implements ExternalTerminologyValidation { + + private final ConstraintViolation err; + + NopExternalTerminologyValidation(String errorMessage) { + this.err = new ConstraintViolation(errorMessage); + } + + public Try validate(TerminologyParam param) { + return Try.failure(new ConstraintViolationException(List.of(err))); + } + + public boolean supports(TerminologyParam param) { + return false; + } + + public List expand(TerminologyParam param) { + return Collections.emptyList(); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/validation/ValidationConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/validation/ValidationConfiguration.java new file mode 100644 index 000000000..1f848dceb --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/validation/ValidationConfiguration.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.validation; + +import com.jayway.jsonpath.DocumentContext; +import java.util.Map; +import java.util.Optional; +import org.ehrbase.api.exception.BadGatewayException; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.cache.CacheProvider; +import org.ehrbase.openehr.sdk.validation.terminology.ExternalTerminologyValidation; +import org.ehrbase.openehr.sdk.validation.terminology.ExternalTerminologyValidationChain; +import org.ehrbase.service.validation.FhirTerminologyValidation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.Cache; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; + +/** + * {@link Configuration} for external terminology validation. + */ +@Configuration +@EnableConfigurationProperties(ValidationProperties.class) +@SuppressWarnings("java:S6212") +public class ValidationConfiguration { + + private static final String ERR_MSG = "External terminology validation is disabled, consider to enable it"; + private final Logger logger = LoggerFactory.getLogger(ValidationConfiguration.class); + private final ValidationProperties properties; + private final CacheProvider cacheProvider; + private final OAuth2AuthorizedClientManager authorizedClientManager; + + public ValidationConfiguration( + ValidationProperties properties, + CacheProvider cacheProvider, + @Nullable OAuth2AuthorizedClientManager authorizedClientManager) { + this.properties = properties; + this.cacheProvider = cacheProvider; + this.authorizedClientManager = authorizedClientManager; + } + + @Bean + public ExternalTerminologyValidation externalTerminologyValidator() { + if (!properties.isEnabled()) { + logger.warn(ERR_MSG); + return nopTerminologyValidation(); + } + + final Map providers = properties.getProvider(); + + if (providers.isEmpty()) { + throw new IllegalStateException("At least one external terminology provider must be defined " + + "if 'validation.external-validation.enabled' is set to 'true'"); + } else if (providers.size() == 1) { + return buildExternalTerminologyValidation( + providers.entrySet().iterator().next()); + } else { + ExternalTerminologyValidationChain chain = new ExternalTerminologyValidationChain(); + for (Map.Entry namedProvider : providers.entrySet()) { + chain.addExternalTerminologyValidationSupport(buildExternalTerminologyValidation(namedProvider)); + } + return chain; + } + } + + private ExternalTerminologyValidation buildExternalTerminologyValidation( + Map.Entry namedProvider) { + + final String name = namedProvider.getKey(); + final ValidationProperties.Provider provider = namedProvider.getValue(); + String oauth2Client = provider.getOauth2Client(); + + logger.info( + "Initializing '{}' external terminology provider (type: {}) at {} {}", + name, + provider.getType(), + provider.getUrl(), + Optional.ofNullable(oauth2Client) + .map(" secured by oauth2 client '%s'"::formatted) + .orElse("")); + + final WebClient webClient = buildWebClient(oauth2Client); + + if (provider.getType() == ValidationProperties.ProviderType.FHIR) { + return fhirTerminologyValidation(provider.getUrl(), webClient); + } + throw new IllegalArgumentException("Invalid provider type: " + provider.getType()); + } + + private WebClient buildWebClient(String clientId) { + WebClient.Builder builder = WebClient.builder(); + if (clientId != null) { + // sanity checks + if (authorizedClientManager == null) { + throw new IllegalArgumentException( + "Attempt to create an oauth2 client with id 'spring.security.oauth2.registration.%s' but no clients are registered." + .formatted(clientId)); + } + ServletOAuth2AuthorizedClientExchangeFilterFunction filter = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + filter.setDefaultClientRegistrationId(clientId); + builder = builder.apply(filter.oauth2Configuration()); + } + return builder.build(); + } + + public static ExternalTerminologyValidation nopTerminologyValidation() { + return new NopExternalTerminologyValidation(ERR_MSG); + } + + private FhirTerminologyValidation fhirTerminologyValidation(String url, WebClient webClient) { + return new FhirTerminologyValidation(url, properties.isFailOnError(), webClient) { + + @Override + protected DocumentContext internalGet(String uri) throws WebClientException { + try { + return cacheProvider.get( + CacheProvider.EXTERNAL_FHIR_TERMINOLOGY_CACHE, uri, () -> super.internalGet(uri)); + } catch (Cache.ValueRetrievalException e) { + final Throwable cause = e.getCause(); + // Something went wrong during downstream request - Forward as bad Gateway. We could also catch + // WebClientResponseException and add our own error message. The WebClientException happens also + // in case the connection is refused or the DNS lookup fails. + if (cause instanceof WebClientException) { + throw new BadGatewayException(cause.getMessage(), cause); + } else { + throw new InternalServerException( + "Failure during fhir terminology request: %s".formatted(cause.getMessage()), cause); + } + } + } + }; + } +} diff --git a/application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/validation/ValidationProperties.java similarity index 75% rename from application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java rename to configuration/src/main/java/org/ehrbase/configuration/config/validation/ValidationProperties.java index 2cc4f3aa3..40bcda220 100644 --- a/application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/validation/ValidationProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.validation; +package org.ehrbase.configuration.config.validation; import java.util.HashMap; import java.util.Map; @@ -29,6 +29,8 @@ public class ValidationProperties { private boolean enabled = false; + private boolean authenticate = false; + private boolean failOnError = false; private final Map provider = new HashMap<>(); @@ -41,6 +43,14 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + public boolean isAuthenticate() { + return authenticate; + } + + public void setAuthenticate(boolean authenticate) { + this.authenticate = authenticate; + } + public boolean isFailOnError() { return failOnError; } @@ -59,10 +69,20 @@ public enum ProviderType { public static class Provider { + private String oauth2Client; + private ProviderType type; private String url; + public String getOauth2Client() { + return oauth2Client; + } + + public void setOauth2Client(String oauth2Client) { + this.oauth2Client = oauth2Client; + } + public ProviderType getType() { return type; } diff --git a/application/src/main/java/org/ehrbase/application/config/web/CorsProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/web/CorsProperties.java similarity index 96% rename from application/src/main/java/org/ehrbase/application/config/web/CorsProperties.java rename to configuration/src/main/java/org/ehrbase/configuration/config/web/CorsProperties.java index b3fe256e7..82a1e76f0 100644 --- a/application/src/main/java/org/ehrbase/application/config/web/CorsProperties.java +++ b/configuration/src/main/java/org/ehrbase/configuration/config/web/CorsProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.config.web; +package org.ehrbase.configuration.config.web; import java.time.Duration; import java.time.temporal.ChronoUnit; diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/web/WebConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/web/WebConfiguration.java new file mode 100644 index 000000000..317d0bc3a --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/web/WebConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.config.web; + +import org.ehrbase.configuration.util.IsoDateTimeConverter; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * {@link Configuration} from Spring Web MVC. + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(CorsProperties.class) +public class WebConfiguration implements WebMvcConfigurer { + + private final CorsProperties properties; + + public WebConfiguration(CorsProperties properties) { + this.properties = properties; + } + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new IsoDateTimeConverter()); // Converter for version_at_time and other ISO date params + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").combine(properties.toCorsConfiguration()); + } + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseTrailingSlashMatch(true); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/exception/DefaultExceptionHandler.java b/configuration/src/main/java/org/ehrbase/configuration/exception/DefaultExceptionHandler.java new file mode 100644 index 000000000..f785ff178 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/exception/DefaultExceptionHandler.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2024 vitasystems GmbH. + * + * This file is part of project EHRbase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ehrbase.configuration.exception; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.BadGatewayException; +import org.ehrbase.api.exception.GeneralRequestProcessingException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.api.exception.InvalidApiParameterException; +import org.ehrbase.api.exception.NotAcceptableException; +import org.ehrbase.api.exception.ObjectNotFoundException; +import org.ehrbase.api.exception.PreconditionFailedException; +import org.ehrbase.api.exception.StateConflictException; +import org.ehrbase.api.exception.UnprocessableEntityException; +import org.ehrbase.api.exception.UnsupportedMediaTypeException; +import org.ehrbase.api.exception.ValidationException; +import org.ehrbase.openehr.sdk.serialisation.exception.UnmarshalException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +/** + * Default exception handler. + */ +@RestControllerAdvice +public class DefaultExceptionHandler { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + // 400 + @ExceptionHandler({ + // Spring MVC + HttpMessageNotReadableException.class, + MethodArgumentTypeMismatchException.class, + MissingServletRequestPartException.class, + BindException.class, + ServletRequestBindingException.class, + // Java/third party Library + IllegalArgumentException.class, + // ehrbase/SDK + GeneralRequestProcessingException.class, + InvalidApiParameterException.class, + ValidationException.class, + UnmarshalException.class, + AqlFeatureNotImplementedException.class, + IllegalAqlException.class, + }) + public ResponseEntity handleBadRequestExceptions(Exception ex) { + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + // 404 + @ExceptionHandler({ObjectNotFoundException.class}) + public ResponseEntity handleObjectNotFoundException(ObjectNotFoundException ex) { + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.NOT_FOUND); + } + + // 404 - Servlet resource mapping failure + @ExceptionHandler({NoResourceFoundException.class}) + public ResponseEntity handleResourceNotFoundException(NoResourceFoundException ex) { + // Raised by the dispatch servlet in case the path could not be mapped. + return handleExceptionInternal( + ex, "No resource found at path: %s".formatted(ex.getResourcePath()), HttpStatus.NOT_FOUND); + } + + // 405 + @ExceptionHandler({HttpRequestMethodNotSupportedException.class}) + public ResponseEntity handleMethodNotAllowedException(HttpRequestMethodNotSupportedException ex) { + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.METHOD_NOT_ALLOWED); + } + + // 406 + @ExceptionHandler({HttpMediaTypeNotAcceptableException.class, NotAcceptableException.class}) + public ResponseEntity handleNotAcceptableException(Exception ex) { + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.NOT_ACCEPTABLE); + } + + // 409 + @ExceptionHandler(StateConflictException.class) + public ResponseEntity handleStateConflictException(StateConflictException ex) { + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.CONFLICT); + } + + // 412 + @ExceptionHandler(PreconditionFailedException.class) + public ResponseEntity handlePreconditionFailedException(PreconditionFailedException ex) { + + var headers = new HttpHeaders(); + + if (ex.getUrl() != null && ex.getCurrentVersionUid() != null) { + headers.setETag("\"" + ex.getCurrentVersionUid() + "\""); + headers.setLocation(URI.create(ex.getUrl())); + } + + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.PRECONDITION_FAILED, headers); + } + + // 415 + @ExceptionHandler({HttpMediaTypeNotSupportedException.class, UnsupportedMediaTypeException.class}) + public ResponseEntity handleUnsupportedMediaTypeException(UnsupportedMediaTypeException ex) { + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.UNSUPPORTED_MEDIA_TYPE); + } + + // 422 + @ExceptionHandler(UnprocessableEntityException.class) + public ResponseEntity handleUnprocessableEntityException( + UnprocessableEntityException ex, WebRequest request) { + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY); + } + + // custom status + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleSpringResponseStatusException(ResponseStatusException ex) { + // rethrow will not work properly, so we handle it + return handleExceptionInternal(ex, ex.getReason(), ex.getStatusCode(), ex.getHeaders()); + } + + // 502 - bad gateway + @ExceptionHandler(BadGatewayException.class) + public ResponseEntity handleFooBadGatewayException(BadGatewayException ex) { + // var message = "An internal error has occurred. Please contact your administrator."; + return handleExceptionInternal(ex, ex.getMessage(), HttpStatus.BAD_GATEWAY); + } + + // 500 - general + @ExceptionHandler(Exception.class) + public ResponseEntity handleUncaughtException(Exception ex) { + var message = "An internal error has occurred. Please contact your administrator."; + return handleExceptionInternal(ex, message, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // 501 - not implemented + @ExceptionHandler(UnsupportedOperationException.class) + public ResponseEntity handleUncaughtException(UnsupportedOperationException ex) { + var message = "The current operation is not supported by this server. Please contact your administrator."; + return handleExceptionInternal(ex, message, HttpStatus.NOT_IMPLEMENTED); + } + + private ResponseEntity handleExceptionInternal(Exception ex, String message, HttpStatusCode status) { + return handleExceptionInternal(ex, message, status, HttpHeaders.EMPTY); + } + + private ResponseEntity handleExceptionInternal( + Exception ex, String message, HttpStatusCode status, HttpHeaders headers) { + + if (status.is5xxServerError()) { + logger.error("", ex); + } else { + logger.warn(ex.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug("Exception stack trace", ex); + } + } + + Map body = new HashMap<>(); + if (status instanceof HttpStatus httpStatus) { + body.put("error", httpStatus.getReasonPhrase()); + } + body.put("message", message); + return new ResponseEntity<>(body, headers, status); + } +} diff --git a/application/src/main/java/org/ehrbase/application/util/IsoDateTimeConverter.java b/configuration/src/main/java/org/ehrbase/configuration/util/IsoDateTimeConverter.java similarity index 88% rename from application/src/main/java/org/ehrbase/application/util/IsoDateTimeConverter.java rename to configuration/src/main/java/org/ehrbase/configuration/util/IsoDateTimeConverter.java index 8c605483d..cb9d87eba 100644 --- a/application/src/main/java/org/ehrbase/application/util/IsoDateTimeConverter.java +++ b/configuration/src/main/java/org/ehrbase/configuration/util/IsoDateTimeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.util; +package org.ehrbase.configuration.util; import java.time.Instant; import java.time.ZonedDateTime; diff --git a/application/src/main/java/org/ehrbase/application/web/ForwardFilter.java b/configuration/src/main/java/org/ehrbase/configuration/web/ForwardFilter.java similarity index 81% rename from application/src/main/java/org/ehrbase/application/web/ForwardFilter.java rename to configuration/src/main/java/org/ehrbase/configuration/web/ForwardFilter.java index c9dff20ca..b289a3620 100644 --- a/application/src/main/java/org/ehrbase/application/web/ForwardFilter.java +++ b/configuration/src/main/java/org/ehrbase/configuration/web/ForwardFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.web; +package org.ehrbase.configuration.web; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -24,9 +24,6 @@ /** * Handles X-Forwarded headers - * - * @author Stefan Spiska - * @since 1.0.0 */ @Component @Order(Ordered.HIGHEST_PRECEDENCE) diff --git a/application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java b/configuration/src/main/java/org/ehrbase/configuration/web/LoggingContextFilter.java similarity index 90% rename from application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java rename to configuration/src/main/java/org/ehrbase/configuration/web/LoggingContextFilter.java index 715c9fc31..8bf721182 100644 --- a/application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java +++ b/configuration/src/main/java/org/ehrbase/configuration/web/LoggingContextFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 vitasystems GmbH and Hannover Medical School. + * Copyright (c) 2024 vitasystems GmbH. * * This file is part of project EHRbase * @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.web; +package org.ehrbase.configuration.web; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -33,9 +33,6 @@ /** * Filter implementation that associates a unique traceId for logging purposes to each * incoming request. - * - * @author Renaud Subiger - * @since 1.0.0 */ @Component @Order(Ordered.HIGHEST_PRECEDENCE) diff --git a/configuration/src/main/resources/application-cloud.yml b/configuration/src/main/resources/application-cloud.yml new file mode 100644 index 000000000..6641a7018 --- /dev/null +++ b/configuration/src/main/resources/application-cloud.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:5432/ehrbase + username: ehrbase_restricted + password: ehrbase_restricted + hikari: + maximum-pool-size: 50 + max-lifetime: 1800000 + minimum-idle: 10 + flyway: + schemas: ehr + user: ehrbase + password: ehrbase + +security: + authType: NONE + + diff --git a/configuration/src/main/resources/application-docker.yml b/configuration/src/main/resources/application-docker.yml new file mode 100644 index 000000000..d42797f95 --- /dev/null +++ b/configuration/src/main/resources/application-docker.yml @@ -0,0 +1,31 @@ +# Copyright (c) 2024 vitasystems GmbH. +# +# This file is part of Project EHRbase +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASS} + hikari: + maximum-pool-size: 50 + max-lifetime: 1800000 + minimum-idle: 10 + flyway: + schemas: ehr + user: ${DB_USER_ADMIN} + password: ${DB_PASS_ADMIN} +security: + authType: NONE diff --git a/configuration/src/main/resources/application-local.yml b/configuration/src/main/resources/application-local.yml new file mode 100644 index 000000000..a0fb5fb30 --- /dev/null +++ b/configuration/src/main/resources/application-local.yml @@ -0,0 +1,32 @@ +# Copyright (c) 2024 vitasystems GmbH. +# +# This file is part of Project EHRbase +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +spring: + datasource: + url: jdbc:postgresql://localhost:5432/ehrbase + username: ehrbase_restricted + password: ehrbase_restricted + flyway: + schemas: ehr + user: ehrbase + password: ehrbase + +security: + authType: NONE + +#use admin for cleaning up the db during tests +admin-api: + active: true + allowDeleteAll: true diff --git a/configuration/src/main/resources/application.yml b/configuration/src/main/resources/application.yml new file mode 100644 index 000000000..5d57ab312 --- /dev/null +++ b/configuration/src/main/resources/application.yml @@ -0,0 +1,241 @@ +# Copyright (c) 2024 vitasystems GmbH. +# +# This file is part of Project EHRbase +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +# General How-to: +# +# You can set all config values here or via an corresponding environment variable which is named as the property you +# want to set. Replace camel case (aB) as all upper case (AB), dashes (-) and low dashes (_) just get ignored adn words +# will be in one word. Each nesting step of properties will be separated by low dash in environment variable name. +# E.g. if you want to allow the delete all endpoints in the admin api set an environment variable like this: +# ADMINAPI_ALLOWDELETEALL=true +# +# See https://docs.spring.io/spring-boot/docs/2.5.0/reference/html/features.html#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables +# for official documentation on this feature. +# +# Also see the documentation on externalized configuration in general: +# https://docs.spring.io/spring-boot/docs/2.5.0/reference/html/features.html#features.external-config + +spring: + application: + name: ehrbase + + cache: + # change to type redis if usage of redis distributed cache is intended. Also turn on management.health.redis.enabled + # if needed. + type: CAFFEINE + + # the following redis properties are only used if the cache.type=redis + data: + redis: + host: localhost + port: 6379 + + security: + oauth2: + resourceserver: + jwt: + issuer-uri: # http://localhost:8081/auth/realms/ehrbase # Example issuer URI - or set via env var + profiles: + active: local + + datasource: + driver-class-name: org.postgresql.Driver + + flyway: + driver-class-name: org.postgresql.Driver + ehr-schema: ehr + ext-schema: ext + ehr-location: classpath:db/migration/ehr + ext-location: classpath:db/migration/ext + ehr-strategy: MIGRATE + ext-strategy: MIGRATE + user: ehrbase + password: ehrbase + + jooq: + sql-dialect: POSTGRES + + jackson: + default-property-inclusion: NON_NULL + +security: + authType: BASIC + authUser: ehrbase-user + authPassword: SuperSecretPassword + authAdminUser: ehrbase-admin + authAdminPassword: EvenMoreSecretPassword + oauth2UserRole: USER + oauth2AdminRole: ADMIN + + +ehrbase: + aql: + pg-llj-workaround: true + rest: + aql: + # allows to control query execution using debug params + debugging-enabled: false + response: + # add an information about the running ehrbase instance to the AQL meta.generator property + generator-details-enabled: false + # include executed_aql in the AQL meta information + executed-aql-enabled: true + experimental: + tags: + enabled: false + context-path: /rest/experimental/tags + security: + # Configuration of actuator for reporting and health endpoints + management: + endpoints: + web: + # disables CSRF protection on management endpoints on base-path to use a client like curl or similar + csrf-validation-enabled: true + + +httpclient: +#proxy: 'localhost' +#proxyPort: 1234 + +cache: + template-init-on-startup: true + user-id-cache-config: + expire-after-access: + duration: 300 + unit: SECONDS + external-fhir-terminology-cache-config: + expire-after-write: + duration: 300 + unit: SECONDS + + +system: + allow-template-overwrite: false + +openehr-api: + context-path: /rest/openehr +admin-api: + active: false + allowDeleteAll: false + context-path: /rest/admin + +# Logging Properties +logging: + level: + org.ehcache: info + org.jooq: info + org.jooq.Constants: warn + org.springframework: info + org.springframework.security.web.DefaultSecurityFilterChain: warn + org.flywaydb.core.internal.license.VersionPrinter: warn + pattern: + console: '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr([%X]){faint} %clr(${PID}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx' + +server: + # Optional custom server nodename + # nodename: 'local.test.org' + port: 8080 + servlet: + contextPath: /ehrbase + + tomcat: + threads: + min-spare: 200 + max: 200 + + # Option to disable strict invariant validation. + # disable-strict-validation: true + + +# Configuration of actuator for reporting and health endpoints +management: + endpoints: + # Disable all endpoint by default to opt-in enabled endpoints + enabled-by-default: false + web: + base-path: '/management' + exposure: + include: 'env, health, info, metrics, prometheus' + # The access to management endpoints can be controlled + # ADMIN_ONLY - (default) endpoints are locked behind an authorization and are only available for users with the admin role + # PRIVATE - endpoints are locked behind an authorization, but are available to any role + # PUBLIC - endpoints can be accessed without an authorization + access: ADMIN_ONLY + # Per endpoint settings + endpoint: + # Env endpoint - Shows information on environment of EHRbase + env: + # Enable / disable env endpoint + enabled: false + # Health endpoint - Shows information on system status + health: + # Enable / disable health endpoint + enabled: false + # Show components in health endpoint. Can be "never", "when-authorized" or "always" + show-components: 'when-authorized' + # Show details in health endpoint. Can be "never", "when-authorized" or "always" + show-details: 'when-authorized' + # Show additional information on used systems. See https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-health-indicators for available keys + datasource: + # Enable / disable report if datasource connection could be established + enabled: true + # Info endpoint - Shows information on the application as build infor, etc. + info: + # Enable / disable info endpoint + enabled: false + # Metrics endpoint - Shows several metrics on running EHRbase + metrics: + # Enable / disable metrics endpoint + enabled: false + # Prometheus metric endpoint - Special metrics format to display in microservice observer solutions + prometheus: + metrics: + export: + enabled: true + # turn of redis per default, shall be enabled in case spring.cache.type: REDIS is used + health: + redis: + enabled: false + +# External Terminology Validation Properties +validation: + external-terminology: + enabled: false + # failOnError: true + # provider: + # fhir: + # # If set it must match a spring.security.oauth2.registration.[oauth2-client] that needs to be configured + # # oauth2-client: 'fhir-terminology-client' + # # request-timeout: 30S # 30 Seconds default + # type: FHIR + # url: https://r4.ontoserver.csiro.au/fhir/ + +# SSL Properties (used by Spring WebClient and Apache HTTP Client) +client: + ssl: + enabled: false + +# JavaMelody +javamelody: + enabled: false + +# plugin configuration +plugin-manager: + plugin-dir: ./plugin_dir + plugin-config-dir: ./plugin_config_dir + enable: true + plugin-context-path: /plugin diff --git a/application/src/main/resources/logback.xml b/configuration/src/main/resources/logback.xml similarity index 92% rename from application/src/main/resources/logback.xml rename to configuration/src/main/resources/logback.xml index 95528ce24..fdcac6190 100644 --- a/application/src/main/resources/logback.xml +++ b/configuration/src/main/resources/logback.xml @@ -1,6 +1,6 @@ - -- [Index](#index) -- [General notes](#general-notes) -- [1. basic](#1-basic) - - [1.1. Reference UML](#11-reference-uml) - - [1.2. basic.DV_BOOLEAN](#12-basicdv_boolean) - - [1.2.1. Test case anything allowed](#121-test-case-anything-allowed) - - [1.2.2. Test case only true allowed](#122-test-case-only-true-allowed) - - [1.2.3. Test case only false allowed](#123-test-case-only-false-allowed) - - [1.3. basic.DV_IDENTIFIER](#13-basicdv_identifier) - - [1.3.1. Test case validating all attributes using the pattern constraint](#131-test-case-validating-all-attributes-using-the-pattern-constraint) - - [1.3.2. Test case validating all attributes using the list constraint](#132-test-case-validating-all-attributes-using-the-list-constraint) - - [1.3. basic.DV_STATE](#13-basicdv_state) -- [2. text](#2-text) - - [2.1. Reference UML](#21-reference-uml) - - [2.2. text.DV_TEXT](#22-textdv_text) - - [2.2.1. Test case DV_TEXT with open constraint](#221-test-case-dv_text-with-open-constraint) - - [2.2.2. Test case DV_TEXT with pattern constraint](#222-test-case-dv_text-with-pattern-constraint) - - [2.2.3. Test case DV_TEXT with list constraint](#223-test-case-dv_text-with-list-constraint) - - [2.3. text.DV_CODED_TEXT](#23-textdv_coded_text) - - [2.3.1. Test case DV_CODED_TEXT with open constraint](#231-test-case-dv_coded_text-with-open-constraint) - - [2.3.2. Test case DV_CODED_TEXT with local codes](#232-test-case-dv_coded_text-with-local-codes) - - [2.3.3. Test case DV_CODED_TEXT with external terminology (constraint reference)](#233-test-case-dv_coded_text-with-external-terminology-constraint-reference) - - [2.4. text.DV_PARAGRAPH](#24-textdv_paragraph) -- [3. quantity](#3-quantity) - - [3.1. Reference UML](#31-reference-uml) - - [3.2. quantity.DV_ORDINAL](#32-quantitydv_ordinal) - - [3.2.1. Test case DV_ORDINAL open constraint](#321-test-case-dv_ordinal-open-constraint) - - [3.2.2. Test case DV_ORDINAL with constraints](#322-test-case-dv_ordinal-with-constraints) - - [3.3. quantity.DV_SCALE](#33-quantitydv_scale) - - [3.3.1. Test case DV_SCALE open constraint](#331-test-case-dv_scale-open-constraint) - - [3.3.2. Test case DV_SCALE with constraints](#332-test-case-dv_scale-with-constraints) - - [3.4. quantity.DV_COUNT](#34-quantitydv_count) - - [3.4.1. Test case DV_COUNT open constraint](#341-test-case-dv_count-open-constraint) - - [3.4.2. Test case DV_COUNT range constraint](#342-test-case-dv_count-range-constraint) - - [3.4.3. Test case DV_COUNT list constraint](#343-test-case-dv_count-list-constraint) - - [3.5. quantity.DV_QUANTITY](#35-quantitydv_quantity) - - [3.5.1. Test case DV_QUANTITY open constraint](#351-test-case-dv_quantity-open-constraint) - - [3.5.2. Test case DV_QUANTITY only property is constrained](#352-test-case-dv_quantity-only-property-is-constrained) - - [3.5.3. Test case DV_QUANTITY property and units are constrained, without magnitude range](#353-test-case-dv_quantity-property-and-units-are-constrained-without-magnitude-range) - - [3.5.4. Test case DV_QUANTITY property and units are constrained, with magnitude range](#354-test-case-dv_quantity-property-and-units-are-constrained-with-magnitude-range) - - [3.6. quantity.DV_PROPORTION](#36-quantitydv_proportion) - - [3.6.1. Test case DV_PROPORTION open constraint, validate RM rules](#361-test-case-dv_proportion-open-constraint-validate-rm-rules) - - [3.6.2. Test case DV_PROPORTION ratio](#362-test-case-dv_proportion-ratio) - - [3.6.3. Test case DV_PROPORTION unitary](#363-test-case-dv_proportion-unitary) - - [3.6.4. Test case DV_PROPORTION percent](#364-test-case-dv_proportion-percent) - - [3.6.5. Test case DV_PROPORTION fraction](#365-test-case-dv_proportion-fraction) - - [3.6.6. Test case DV_PROPORTION integer fraction](#366-test-case-dv_proportion-integer-fraction) - - [3.6.7. Test case DV_PROPORTION fraction or integer fraction](#367-test-case-dv_proportion-fraction-or-integer-fraction) - - [3.6.8. Test case DV_PROPORTION ratio with range limits](#368-test-case-dv_proportion-ratio-with-range-limits) - - [3.7. quantity.DV_INTERVAL](#37-quantitydv_intervaldv_count) - - [3.7.1. Test case DV_INTERVAL open constraint](#371-test-case-dv_intervaldv_count-open-constraint) - - [3.7.2. Test case DV_INTERVAL lower and upper range constraint.](#372-test-case-dv_intervaldv_count-lower-and-upper-range-constraint) - - [3.7.3. Test case DV_INTERVAL lower and upper list constraint.](#373-test-case-dv_intervaldv_count-lower-and-upper-list-constraint) - - [3.8. quantity.DV_INTERVAL](#38-quantitydv_intervaldv_quantity) - - [3.8.1. Test case DV_INTERVAL open constraint](#381-test-case-dv_intervaldv_quantity-open-constraint) - - [3.8.2. Test case DV_INTERVAL lower and upper constraints present](#382-test-case-dv_intervaldv_quantity-lower-and-upper-constraints-present) - - [3.9. quantity.DV_INTERVAL](#39-quantitydv_intervaldv_date_time) - - [3.9.1. Test case DV_INTERVAL open constraint](#391-test-case-dv_intervaldv_date_time-open-constraint) - - [3.9.2. Test case DV_INTERVAL lower and upper constraints are validity kind](#392-test-case-dv_intervaldv_date_time-lower-and-upper-constraints-are-validity-kind) - - [3.9.3. Test case DV_INTERVAL lower and upper constraints are range](#393-test-case-dv_intervaldv_date_time-lower-and-upper-constraints-are-range) - - [3.10. quantity.DV_INTERVAL](#310-quantitydv_intervaldv_date) - - [3.10.1. Test case DV_INTERVAL open constraint](#3101-test-case-dv_intervaldv_date-open-constraint) - - [3.10.2. Test case DV_INTERVAL validity kind constraint](#3102-test-case-dv_intervaldv_date-validity-kind-constraint) - - [3.10.3. Test case DV_INTERVAL range constraint](#3103-test-case-dv_intervaldv_date-range-constraint) - - [3.11. quantity.DV_INTERVAL](#311-quantitydv_intervaldv_time) - - [3.11.1. Test case DV_INTERVAL open constraint](#3111-test-case-dv_intervaldv_time-open-constraint) - - [3.11.2. Test case DV_INTERVAL validity kind constraint](#3112-test-case-dv_intervaldv_time-validity-kind-constraint) - - [3.11.3. Test case DV_INTERVAL range constraint](#3113-test-case-dv_intervaldv_time-range-constraint) - - [3.12. quantity.DV_INTERVAL](#312-quantitydv_intervaldv_duration) - - [3.12.1. Test case DV_INTERVAL open constraint](#3121-test-case-dv_intervaldv_duration-open-constraint) - - [3.12.2. Test case DV_INTERVAL xxx_allowed constraints](#3122-test-case-dv_intervaldv_duration-xxx_allowed-constraints) - - [3.12.3. Test case DV_INTERVAL range constraints](#3123-test-case-dv_intervaldv_duration-range-constraints) - - [3.13. quantity.DV_INTERVAL](#313-quantitydv_intervaldv_ordinal) - - [3.13.1. Test case DV_INTERVAL open constraint](#3131-test-case-dv_intervaldv_ordinal-open-constraint) - - [3.13.2. Test case DV_INTERVAL with constraints](#3132-test-case-dv_intervaldv_ordinal-with-constraints) - - [3.14. quantity.DV_INTERVAL](#314-quantitydv_intervaldv_scale) - - [3.14.1. Test case DV_SCALE open constraint](#3141-test-case-dv_scale-open-constraint) - - [3.14.2. Test case DV_SCALE with constraints](#3142-test-case-dv_scale-with-constraints) - - [3.15. quantity.DV_INTERVAL](#315-quantitydv_intervaldv_proportion) - - [3.15.1. Test case DV_INTERVAL open constraint](#3151-test-case-dv_intervaldv_proportion-open-constraint) - - [3.15.1.a. Data set both valid ratios](#3151a-data-set-both-valid-ratios) - - [3.15.1.b. Data set different limit types](#3151b-data-set-different-limit-types) - - [3.15.1.c. Data set greater lower](#3151c-data-set-greater-lower) - - [3.15.2. Test case DV_INTERVAL ratios](#3152-test-case-dv_intervaldv_proportion-ratios) - - [3.15.2.a. Data set valid ratios](#3152a-data-set-valid-ratios) - - [3.15.2.b. Data set no ratios](#3152b-data-set-no-ratios) - - [3.15.3. Test case DV_INTERVAL unitaries](#3153-test-case-dv_intervaldv_proportion-unitaries) - - [3.15.3.a. Data set valid unitaries](#3153a-data-set-valid-unitaries) - - [3.15.3.b. Data set no unitaries](#3153b-data-set-no-unitaries) - - [3.15.4. Test case DV_INTERVAL percentages](#3154-test-case-dv_intervaldv_proportion-percentages) - - [3.15.4.a. Data set valid percentages](#3154a-data-set-valid-percentages) - - [3.15.4.b. Data set no percentages](#3154b-data-set-no-percentages) - - [3.15.5. Test case DV_INTERVAL fractions](#3155-test-case-dv_intervaldv_proportion-fractions) - - [3.15.5.a. Data set valid fractions](#3155a-data-set-valid-fractions) - - [3.15.5.b. Data set no fractions](#3155b-data-set-no-fractions) - - [3.15.6. Test case DV_INTERVAL integer fractions](#3156-test-case-dv_intervaldv_proportion-integer-fractions) - - [3.15.6.a. Data set valid integer fractions](#3156a-data-set-valid-integer-fractions) - - [3.15.6.b. Data set no integer fractions](#3156b-data-set-no-integer-fractions) - - [3.15.7. Test case DV_INTERVAL ratios with range limits](#3157-test-case-dv_intervaldv_proportion-ratios-with-range-limits) - - [3.15.7.a. Data set valid ratios](#3157a-data-set-valid-ratios) - - [3.15.7.b. Data set ratios, invalid lower](#3157b-data-set-ratios-invalid-lower) - - [3.15.7.c. Data set ratios, invalid upper](#3157c-data-set-ratios-invalid-upper) -- [4. quantity.date_time](#4-quantitydate_time) - - [4.1. Reference UML](#41-reference-uml) - - [4.2. quantity.date_time.DV_DURATION](#42-quantitydate_timedv_duration) - - [4.2.1. Test case DV_DURATION open constraint](#421-test-case-dv_duration-open-constraint) - - [4.2.2. Test case DV_DURATION xxx_allowed field constraints](#422-test-case-dv_duration-xxx_allowed-field-constraints) - - [4.2.3. Test case DV_DURATION range constraint](#423-test-case-dv_duration-range-constraint) - - [4.2.4. Test case DV_DURATION fields allowed and range constraints combined](#424-test-case-dv_duration-fields-allowed-and-range-constraints-combined) - - [4.3. quantity.date_time.DV_TIME](#43-quantitydate_timedv_time) - - [4.3.1. Test case DV_TIME open constraint](#431-test-case-dv_time-open-constraint) - - [4.3.2. Test case DV_TIME validity kind constraint](#432-test-case-dv_time-validity-kind-constraint) - - [4.3.3. Test case DV_TIME range constraint](#433-test-case-dv_time-range-constraint) - - [4.4. quantity.date_time.DV_DATE](#44-quantitydate_timedv_date) - - [4.4.1. Test case DV_DATE open constraint](#441-test-case-dv_date-open-constraint) - - [4.4.2. Test Case DV_DATE validity kind constraint](#442-test-case-dv_date-validity-kind-constraint) - - [4.4.3. Test Case DV_DATE validity range constraint](#443-test-case-dv_date-validity-range-constraint) - - [4.5. quantity.date_time.DV_DATE_TIME](#45-quantitydate_timedv_date_time) - - [4.5.1. Test case DV_DATE_TIME open constraint](#451-test-case-dv_date_time-open-constraint) - - [4.5.2. Test Case DV_DATE_TIME validity kind constraint](#452-test-case-dv_date_time-validity-kind-constraint) - - [4.5.3. Test Case DV_DATE_TIME validity range](#453-test-case-dv_date_time-validity-range) -- [5. time_specification](#5-time_specification) - - [Reference UML](#reference-uml) - - [5.1. DV_GENERAL_TIME_SPECIFICATION](#51-dv_general_time_specification) - - [5.2. DV_PERIODIC_TIME_SPECIFICATION](#52-dv_periodic_time_specification) -- [6. encapsulated](#6-encapsulated) - - [6.1. Reference UML](#61-reference-uml) - - [6.2. encapsulated.DV_PARSABLE](#62-encapsulateddv_parsable) - - [6.2.1. Test case DV_PARSABLE open constraint](#621-test-case-dv_parsable-open-constraint) - - [6.2.2. Test case DV_PARSABLE value and formalism constrained](#622-test-case-dv_parsable-value-and-formalism-constrained) - - [6.3. encapsulated.DV_MULTIMEDIA](#63-encapsulateddv_multimedia) - - [6.3.1. Test ccase DV_MULTIMEDIA open constraint](#631-test-ccase-dv_multimedia-open-constraint) - - [6.3.2. Test case DV_MULTIMEDIA media type constraint](#632-test-case-dv_multimedia-media-type-constraint) -- [7. uri](#7-uri) - - [7.1. Reference UML](#71-reference-uml) - - [7.2. DV_URI](#72-dv_uri) - - [7.2.1. Test case DV_URI open constraint](#721-test-case-dv_uri-open-constraint) - - [7.2.2. Test case DV_URI C_STRING pattern constraint for value](#722-test-case-dv_uri-c_string-pattern-constraint-for-value) - - [7.2.3. Test case DV_URI C_STRING list constraint for value](#723-test-case-dv_uri-c_string-list-constraint-for-value) - - [7.3. DV_EHR_URI](#73-dv_ehr_uri) - - [7.3.1. Test case DV_EHR_URI open constraint](#731-test-case-dv_ehr_uri-open-constraint) - - [7.3.2. Test case DV_EHR_URI C_STRING pattern constraint for value](#732-test-case-dv_ehr_uri-c_string-pattern-constraint-for-value) - - [7.3.3. Test case DV_EHR_URI C_STRING list constraint for value](#733-test-case-dv_ehr_uri-c_string-list-constraint-for-value) - -# General notes - -1. All test data sets for date/time/datetime expressions are represented in the ISO 8601 extended format. An openEHR CDR could choose to use the extended (with field delimiter characters) or basic format (without field delimiters) of ISO 8601, or support any of the two formats. In the test implementations it is probable that the data sets are represented as JSON or XML documents, in which the date and time expressions are always representede in the ISO 8601 extended format, but internally the SUT could store any of the two formats. If the test implementation doesn't use JSON or XML, the date and time expression formats could use the ISO 8601 basic format. - -2. The combination of test case + test data set is what will generate a result when running the test implementation againts a SUT. - -3. The test data sets described inside each test case are not exhaustive. We can create more data sets here, including border cases and more failure cases and data set combinations. - -4. To have a full view of the Conformance Verification components, please check the document published here https://www.cabolabs.com/blog/article/openehr_conformance_framework-61ef4f513f7c5.html - -5. "TBD" means "To be defined". - -# 1. basic - -## 1.1. Reference UML - -![](https://specifications.openehr.org/releases/RM/Release-1.1.0/UML/diagrams/RM-data_types.basic.svg) - -## 1.2. basic.DV_BOOLEAN - -Internally DV_BOOLEAN is constrained by C_BOOLEAN. - -### 1.2.1. Test case anything allowed - -| value | C_BOOLEAN.true_valid | C_BOOLEAN.false_valid | expected | constraints violated | -|:----------|:----------------------|-----------------------|----------|----------------------| -| true | true | true | accepted | | -| false | true | true | accepted | | - - -### 1.2.2. Test case only true allowed - -| value | C_BOOLEAN.true_valid | C_BOOLEAN.false_valid | expected | constraints violated | -|:----------|:----------------------|-----------------------|----------|----------------------| -| true | true | false | accepted | | -| false | true | false | rejected | C_BOOLEAN.false_valid | - - -### 1.2.3. Test case only false allowed - -| value | C_BOOLEAN.true_valid | C_BOOLEAN.false_valid | expected | constraints violated | -|:----------|:----------------------|-----------------------|----------|----------------------| -| true | false | true | accepted | C_BOOLEAN.true_valid | -| false | false | true | accepted | | - - -## 1.3. basic.DV_IDENTIFIER - -Internally DV_IDENTIFIER attributes are constrainted by C_STRING. - -Note the constraints for each attribute are all checked, so the errors are accumulated. If one validation fails for one attribute, the validation for the whole type fails. - -### 1.3.1. Test case validating all attributes using the pattern constraint - -| issuer | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:------------------|---------------|----------|----------------------| -| NULL | XYZ.* | NULL | rejected | C_STRING.pattern | -| ABC | XYZ.* | NULL | rejected | C_STRING.pattern | -| XYZ | XYZ.* | NULL | accepted | | - - -| assigner | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:------------------|---------------|----------|----------------------| -| NULL | XYZ.* | NULL | rejected | C_STRING.pattern | -| ABC | XYZ.* | NULL | rejected | C_STRING.pattern | -| XYZ | XYZ.* | NULL | accepted | | - -| id | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:------------------|---------------|----------|----------------------| -| NULL | XYZ.* | NULL | rejected | RM/Schema: this is mandatory in the RM | -| ABC | XYZ.* | NULL | rejected | C_STRING.pattern | -| XYZ | XYZ.* | NULL | accepted | | - -| type | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:------------------|---------------|----------|----------------------| -| NULL | XYZ.* | NULL | rejected | C_STRING.pattern | -| ABC | XYZ.* | NULL | rejected | C_STRING.pattern | -| XYZ | XYZ.* | NULL | accepted | | - - -### 1.3.2. Test case validating all attributes using the list constraint - -| issuer | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:-----------------|---------------|----------|----------------------| -| NULL | NULL | [XYZ] | rejected | C_STRING.list | -| ABC | NULL | [XYZ] | rejected | C_STRING.list | -| XYZ | NULL | [XYZ] | accepted | | - - -| assigner | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:-----------------|---------------|----------|----------------------| -| NULL | NULL | [XYZ] | rejected | C_STRING.list | -| ABC | NULL | [XYZ] | rejected | C_STRING.list | -| XYZ | NULL | [XYZ] | accepted | | - -| id | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:-----------------|---------------|----------|----------------------| -| NULL | NULL | [XYZ] | rejected | RM/Schema: this is mandatory in the RM | -| ABC | NULL | [XYZ] | rejected | C_STRING.list | -| XYZ | NULL | [XYZ] | accepted | | - -| type | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:-----------------|---------------|----------|----------------------| -| NULL | NULL | [XYZ] | rejected | C_STRING.list | -| ABC | NULL | [XYZ] | rejected | C_STRING.list | -| XYZ | NULL | [XYZ] | accepted | | - - -## 1.3. basic.DV_STATE - - - -NOTE: this datatype is not used and not supported by modeling tools. See https://discourse.openehr.org/t/is-dv-state-and-its-profile-constraint-c-dv-state-used-anywhere-in-the-specs/2026 - - -# 2. text - -## 2.1. Reference UML - -![](https://specifications.openehr.org/releases/RM/Release-1.1.0/UML/diagrams/RM-data_types.text.svg) - - -## 2.2. text.DV_TEXT - -Internally DV_TEXT can be constrained by a C_STRING. This type also allows an instance of the subclass DV_CODED_TEXT at runtime. - - -### 2.2.1. Test case DV_TEXT with open constraint - -In ADL this would mean the C_OBJECT for DV_TEXT matches {\*}, but different Archetype Editors might model this differently, for instance LinkEHR does a DV_TEXT.value matches {'.*'} which is using the C_STRING pattern that matches anything. - -| value | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:------------------|---------------|----------|----------------------| -| NULL | NULL | NULL | rejected | RM/Schema mandatory | -| ABC | NULL | NULL | accepted | | -| XYZ | NULL | NULL | accepted | | - - -### 2.2.2. Test case DV_TEXT with pattern constraint - -> NOTE: if the type is DV_CODED_TEXT at runtime, the value attribte still needs to comply with the C_STRING constraint. - -| value | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:------------------|---------------|----------|----------------------| -| NULL | XYZ | NULL | rejected | RM/Schema mandatory | -| ABC | XYZ | NULL | rejected | C_STRING.pattern | -| XYZ | XYZ | NULL | accepted | | - - -### 2.2.3. Test case DV_TEXT with list constraint - -> NOTE: if the type is DV_CODED_TEXT at runtime, the value attribte still needs to comply with the C_STRING constraint. - -| value | C_STRING.pattern | C_STRING.list | expected | constraints violated | -|:-----------|:------------------|---------------|----------|----------------------| -| NULL | NULL | [XYZ, OPQ] | rejected | RM/Schema mandatory | -| ABC | NULL | [XYZ, OPQ] | rejected | C_STRING.list | -| XYZ | NULL | [XYZ, OPQ] | accepted | | - - - -## 2.3. text.DV_CODED_TEXT - -Internally the DV_CODED_TEXT can be constrained by a C_CODE_PHRASE. Note that in the cases for DV_TEXT we already tested when the type is constrained by a C_STRING (when the declared type is DV_TEXT but the runtime type is DV_CODED_TEXT). - -### 2.3.1. Test case DV_CODED_TEXT with open constraint - -In ADL this would mean the C_OBJECT for DV_CODED_TEXT matches {\*}. - -| code_string | terminology_id | C_CODE_PHRASE.code_list | C_CODE_PHRASE.terminology_id | expected | constraints violated | -|:------------|:---------------|-------------------------|------------------------------|----------|----------------------| -| NULL | NULL | NULL | NULL | rejected | RM/Schema mandatory both code_String and terminology_id | -| ABC | NULL | NULL | NULL | rejected | RM/Schema mandatory terminology_id | -| NULL | local | NULL | NULL | rejected | RM/Schema mandatory code_string | -| ABC | local | NULL | NULL | accepted | | -| 82272006 | SNOMED-CT | NULL | NULL | accepted | | - - -### 2.3.2. Test case DV_CODED_TEXT with local codes - -> NOTE: having C_CODE_PHRASE.terminology_id = local and C_CODE_PHRASE.code_list = EMPTY, would be possible at the archetype level, but would be invalid at the template level, so that case is not considered here since it should be validated when the template is uploaded to the SUT. - -| code_string | terminology_id | C_CODE_PHRASE.code_list | C_CODE_PHRASE.terminology_id | expected | constraints violated | -|:------------|:---------------|-------------------------|------------------------------|----------|----------------------| -| NULL | NULL | [ABC, OPQ] | local | rejected | RM/Schema mandatory both code_String and terminology_id | -| ABC | NULL | [ABC, OPQ] | local | rejected | RM/Schema mandatory terminology_id | -| NULL | local | [ABC, OPQ] | local | rejected | RM/Schema mandatory code_string | -| ABC | local | [ABC, OPQ] | local | accepted | | -| 82272006 | SNOMED-CT | [ABC, OPQ] | local | rejected | C_CODE_PHRASE.terminology_id | - - -### 2.3.3. Test case DV_CODED_TEXT with external terminology (constraint reference) - -In this case the DV_CODED_TEXT is constrained by a CONSTRAINT_REF. For the CONSTRAINT_REF to be valid in the template, there shoudld be a constraint_binding entry in the template ontology for the acNNNN code of the CONSTRAINT_REF. Without that, the SUT doesn't know which terminology_id can be used in that DV_CODED_TEXT. Note that multiple bindings are possible, so there could be more than one terminology_id for the coded text. The cases where there are no constraint_bindings are not tested here, that should be part of the OPT validation. - -> NOTE: the COSNTRAINT_REF in ADL is transformed by the Template Designer into a C_CODE_REFERENCE in OPT, which is a C_CODE_PHRASE subclass with an extra referenceSetUri attribute. - -| code_string | terminology_id | CONSTRAINT_REF.reference | constraint_bindings | expected | constraints violated | -|:------------|:---------------|--------------------------|---------------------|----------|----------------------| -| NULL | NULL | ac0001 | [SNOMED_CT] | rejected | RM/Schema mandatory both code_String and terminology_id | -| ABC | NULL | ac0001 | [SNOMED_CT] | rejected | RM/Schema mandatory terminology_id | -| NULL | local | ac0001 | [SNOMED_CT] | rejected | RM/Schema mandatory code_string | -| ABC | local | ac0001 | [SNOMED_CT] | rejected | constraint_binding: terminology_id not found | -| 82272006 | SNOMED-CT | ac0001 | [SNOMED_CT] | accepted | | - - -## 2.4. text.DV_PARAGRAPH - -Since this DB is not used or supported by modeling tools, this conformance test suite doesn't define test cases for valdiating this data type. For more info, see https://discourse.openehr.org/t/is-dv-paragraph-used/2187 - - -# 3. quantity - -## 3.1. Reference UML - -![](https://specifications.openehr.org/releases/RM/Release-1.1.0/UML/diagrams/RM-data_types.quantity.svg) - - -## 3.2. quantity.DV_ORDINAL - -DV_ORDINAL is constrained by C_DV_ORDINAL from AP (https://specifications.openehr.org/releases/1.0.2/architecture/am/openehr_archetype_profile.pdf), which contains a list of DV_ORDINAL that could be empty. - -> NOTE: in ADL it is possible to have a C_DV_ORDINAL constraint with an empty list constraint. At the OPT level this case should be invalid, since is like defining a constraint for a DV_CODED_TEXT with terminology_id `local` but no given codes, since all codes in a C_DV_ORDINAL have terminology_id `local`, at least one code in the list is required at the OPT level. This constraint is valid at the archetypel evel. See commend on 2.3.2. - - -### 3.2.1. Test case DV_ORDINAL open constraint - -This case is when the ADL has `DV_ORDINAL matches {*}` - -| symbol | value | expected | constraints violated | -|:---------------|:------|----------|----------------------| -| NULL | NULL | rejected | RM/Schema value and symbol are mandatory | -| NULL | 1 | rejected | RM/Schema symbol is mandatory | -| local::at0005 | NULL | rejected | RM/Schema value is mandatory | -| local::at0005 | 1 | accepted | | -| local::at0005 | 666 | accepted | | - - -### 3.2.2. Test case DV_ORDINAL with constraints - -| symbol | value | C_DV_ORDINAL.list | expected | constraints violated | -|:---------------|:------|--------------------------------------|----------|----------------------| -| local::at0005 | 1 | 1|[local::at0005], 2|[local::at0006] | accepted | | -| local::at0005 | 666 | 1|[local::at0005], 2|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching value | -| local::at0666 | 1 | 1|[local::at0005], 2|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching symbol | - - - -## 3.3. quantity.DV_SCALE - -DV_SCALE was introduced to the RM 1.1.0 (https://openehr.atlassian.net/browse/SPECRM-19), it is analogous to DV_ORDINAL with a Real value. So test cases for DV_SCALE and DV_ORDINAL are similar. - -NOTE: if this specification is implemented on a system that supports a RM < 1.1.0, then these tests shouldn't run against the system. - -### 3.3.1. Test case DV_SCALE open constraint - -This case is when the ADL has `DV_SCALE matches {*}` - -| symbol | value | expected | constraints violated | -|:---------------|:------|----------|----------------------| -| NULL | NULL | rejected | RM/Schema value and symbol are mandatory | -| NULL | 1.5 | rejected | RM/Schema symbol is mandatory | -| local::at0005 | NULL | rejected | RM/Schema value is mandatory | -| local::at0005 | 1.5 | accepted | | -| local::at0005 | 666 | accepted | | - -### 3.3.2. Test case DV_SCALE with constraints - -> NOTE: there is no current C_DV_SCALE constraint in the Archetype Profile, so modeling tools are not yet supporting constraints for this type. This is a [known issue](https://openehr.atlassian.net/browse/SPECPR-381). Though we can assume the constraint type will be analogous to the C_DV_ORDINAL. - -| symbol | value | C_DV_SCALE.list | expected | constraints violated | -|:---------------|:------|------------------------------------------|----------|-------------------------------------| -| local::at0005 | 1.5 | 1.5|[local::at0005], 2.0|[local::at0006] | accepted | | -| local::at0005 | 66.6 | 1.5|[local::at0005], 2.0|[local::at0006] | rejected | C_DV_SCALE.list: no matching value | -| local::at0666 | 1.5 | 1.5|[local::at0005], 2.0|[local::at0006] | rejected | C_DV_SCALE.list: no matching symbol | - - -## 3.4. quantity.DV_COUNT - -Internally this type is constrained by a C_INTEGER which could contain a range or a list of values. - -### 3.4.1. Test case DV_COUNT open constraint - -This case represents the DV_COUNT matching {*}, in this case the C_INTEGER is not present in the OPT. - -| magnitude | expected | constraints violated | -|:---------------|----------|----------------------| -| NULL | rejected | RM/Schema magnitude is mandatory | -| 0 | accepted | | -| 1 | accepted | | -| 15 | accepted | | -| 30 | accepted | | - -### 3.4.2. Test case DV_COUNT range constraint - -| magnitude | C_INTEGER.range | C_INTEGER.list | expected | constraints violated | -|:---------------|:----------------|-------------------|----------|----------------------| -| NULL | 10..20 | NULL | rejected | RM/Schema magnitude is mandatory | -| 0 | 10..20 | NULL | rejected | C_INTEGER.range | -| 1 | 10..20 | NULL | rejected | C_INTEGER.range | -| 15 | 10..20 | NULL | accepted | | -| 30 | 10..20 | NULL | rejected | C_INTEGER.range | - -### 3.4.3. Test case DV_COUNT list constraint - -> NOTE: some modeling tools might not support the list constraint. - -| magnitude | C_INTEGER.range | C_INTEGER.list | expected | constraints violated | -|:---------------|:----------------|-------------------|----------|----------------------| -| NULL | NULL | [10,15,20] | rejected | RM/Schema magnitude is mandatory | -| 0 | NULL | [10,15,20] | rejected | C_INTEGER.list | -| 1 | NULL | [10,15,20] | rejected | C_INTEGER.list | -| 15 | NULL | [10,15,20] | accepted | | -| 30 | NULL | [10,15,20] | rejected | C_INTEGER.list | - - -## 3.5. quantity.DV_QUANTITY - -Internally DV_QUANTITY is constrained by a C_DV_QUANTITY, which allows to specify an optional physical property and a list of C_QUANTITY_ITEM, which can contain a mandatory units and optional interval constraints for magnitude and precision. - -### 3.5.1. Test case DV_QUANTITY open constraint - -This case represents the DV_QUANTITY matching {*}, in this case the C_DV_QUANTITY is not present in the OPT. - -| magnitude | units | expected | constraints violated | -|:----------|:------|----------|----------------------| -| NULL | NULL | rejected | RM/Schema both magnitude and untis are mandatory | -| NULL | cm | rejected | RM/Schema magnitude is mandatory | -| 1.0 | NULL | rejected | RM/Schema untis is mandatory | -| 0.0 | cm | accepted | | -| 1.0 | cm | accepted | | -| 5.7 | cm | accepted | | -| 10.0 | cm | accepted | | - - -### 3.5.2. Test case DV_QUANTITY only property is constrained - -The C_DV_QUANTITY is present in the OPT and has a value for `property`, but doesn't have a list of C_QUANTITY_ITEM. - -> NOTE: in this case all units for the `property` are allowed, so the validation should look into UCUM for all the possible units of measure or that physical property (the possible values are not un the OPT). - -| magnitude | units | C_DV_QUANTITY.property | C_DV_QUANTITY.list | expected | constraints violated | -|:----------|:------|:------------------------|-------------------|----------|----------------------| -| NULL | NULL | openehr::122 (length) | NULL | rejected | RM/Schema both magnitude and untis are mandatory | -| NULL | cm | openehr::122 (length) | NULL | rejected | RM/Schema magnitude is mandatory | -| 1.0 | NULL | openehr::122 (length) | NULL | rejected | RM/Schema untis is mandatory | -| 0.0 | mg | openehr::122 (length) | NULL | rejected | C_DV_QUANTITY.property: `mg` is not a length unit | -| 0.0 | cm | openehr::122 (length) | NULL | accepted | | -| 1.0 | cm | openehr::122 (length) | NULL | accepted | | -| 5.7 | cm | openehr::122 (length) | NULL | accepted | | -| 10.0 | cm | openehr::122 (length) | NULL | accepted | | - - -### 3.5.3. Test case DV_QUANTITY property and units are constrained, without magnitude range - -| magnitude | units | C_DV_QUANTITY.property | C_DV_QUANTITY.list | expected | constraints violated | -|:----------|:------|:------------------------|-------------------|----------|----------------------| -| NULL | NULL | openehr::122 (length) | [cm, m] | rejected | RM/Schema both magnitude and untis are mandatory | -| NULL | cm | openehr::122 (length) | [cm, m] | rejected | RM/Schema magnitude is mandatory | -| 1.0 | NULL | openehr::122 (length) | [cm, m] | rejected | RM/Schema untis is mandatory | -| 0.0 | mg | openehr::122 (length) | [cm, m] | rejected | C_DV_QUANTITY.property: `mg` is not a length unit | -| 0.0 | cm | openehr::122 (length) | [cm, m] | accepted | | -| 0.0 | km | openehr::122 (length) | [cm, m] | rejected | C_DV_QUANTITY.list: `km` is not allowed | -| 1.0 | cm | openehr::122 (length) | [cm, m] | accepted | | -| 5.7 | cm | openehr::122 (length) | [cm, m] | accepted | | -| 10.0 | cm | openehr::122 (length) | [cm, m] | accepted | | - - -### 3.5.4. Test case DV_QUANTITY property and units are constrained, with magnitude range - -| magnitude | units | C_DV_QUANTITY.property | C_DV_QUANTITY.list | expected | constraints violated | -|:----------|:------|:------------------------|-----------------------|----------|----------------------| -| NULL | NULL | openehr::122 (length) | [cm 5.0..10.0, m] | rejected | RM/Schema both magnitude and untis are mandatory | -| NULL | cm | openehr::122 (length) | [cm 5.0..10.0, m] | rejected | RM/Schema magnitude is mandatory | -| 1.0 | NULL | openehr::122 (length) | [cm 5.0..10.0, m] | rejected | RM/Schema untis is mandatory | -| 0.0 | mg | openehr::122 (length) | [cm 5.0..10.0, m] | rejected | C_DV_QUANTITY.property: `mg` is not a length unit | -| 0.0 | cm | openehr::122 (length) | [cm 5.0..10.0, m] | rejected | C_DV_QUANTITY.list: magnitude not in range for unit | -| 0.0 | km | openehr::122 (length) | [cm 5.0..10.0, m] | rejected | C_DV_QUANTITY.list: `km` is not allowed | -| 1.0 | cm | openehr::122 (length) | [cm 5.0..10.0, m] | rejected | C_DV_QUANTITY.list: magnitude not in range for unit | -| 5.7 | cm | openehr::122 (length) | [cm 5.0..10.0, m] | accepted | | -| 10.0 | cm | openehr::122 (length) | [cm 5.0..10.0, m] | accepted | | - - -## 3.6. quantity.DV_PROPORTION - -The DV_PROPORTION is constrained by a C_COMPLEX_OBJECT, which internally has C_REAL constraints for `numerator` and `denominator`. C_REAL defines two types of constraints: range and list of values. Though current modeling tools only allow range constraints. For the `type` atribute, a C_INTEGER constraint is used, which can hold list and range constraints but modeling tools only use the list. - -This type has intrinsic constraints that should be semantically consistent depending on the value of the numerator, denominator, precision and type attributes. For instance, this if type = 2, the denominator value should be 100 and can't be anything else. In te table below we express the valid combinations of attribute values. - -| type | meaning (kind) | numerator | denominator | precision | comment | -|:----:|------------------|-----------|--------------|-----------|---------| -| 0 | ratio | any | any != 0 | any | | -| 1 | unitary | any | 1 | any | | -| 2 | percent | any | 100 | any | | -| 3 | fraction | integer | integer != 0 | 0 | presentation is num/den | -| 4 | integer fraction | integer | integer != 0 | 0 | presentation is integral(num/den) decimal(num/den), e.g. for num=3 den=2: 1 1/2 | - -> NOTE: the difference between fraction and integer fraction is the presentation, the data and constraints are the same. - - -### 3.6.1. Test case DV_PROPORTION open constraint, validate RM rules - -This test case is used to check the internal rules of the DV_PROPORTION are correctly implemented by the SUT. - -| type | meaning (kind) | numerator | denominator | precision | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------|----------------------------------| -| 0 | ratio | 10 | 500 | 0 | accepted | | -| 0 | ratio | 10 | 0 | 0 | rejected | valid_denominator (invariant) | -| 1 | unitary | 10 | 1 | 0 | accepted | | -| 1 | unitary | 10 | 0 | 0 | rejected | valid_denominator (invariant) | -| 1 | unitary | 10 | 500 | 0 | rejected | unitary_validity (invariant) | -| 2 | percent | 10 | 0 | 0 | rejected | valid_denominator (invariant) | -| 2 | percent | 10 | 100 | 0 | accepted | | -| 2 | percent | 10 | 500 | 0 | rejected | percent_validity (invariant) | -| 3 | fraction | 10 | 0 | 0 | rejected | valid_denominator (invariant) | -| 3 | fraction | 10 | 100 | 0 | accepted | | -| 3 | fraction | 10 | 500 | 1 | rejected | fraction_validity (invariant) | -| 3 | fraction | 10.5 | 500 | 1 | rejected | is_integral_validity (invariant) | -| 3 | fraction | 10 | 500.5 | 1 | rejected | is_integral_validity (invariant) | -| 4 | integer fraction | 10 | 0 | 0 | rejected | valid_denominator (invariant) | -| 4 | integer fraction | 10 | 100 | 0 | accepted | | -| 4 | integer fraction | 10 | 500 | 1 | rejected | fraction_validity (invariant) | -| 4 | integer fraction | 10.5 | 500 | 1 | rejected | is_integral_validity (invariant) | -| 4 | integer fraction | 10 | 500.5 | 1 | rejected | is_integral_validity (invariant) | -| 666 | | 10 | 500 | 0 | rejected | type_validity (invariant) | - - -### 3.6.2. Test case DV_PROPORTION ratio - -The C_INTEGER constraint applies to the `type` attribute. - -| type | meaning (kind) | numerator | denominator | precision | C_INTEGER.list | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------------|----------|----------------------------------| -| 0 | ratio | 10 | 500 | 0 | [0] | accepted | | -| 1 | unitary | 10 | 1 | 0 | [0] | rejected | C_INTEGER.list | -| 2 | percent | 10 | 100 | 0 | [0] | rejected | C_INTEGER.list | -| 3 | fraction | 10 | 500 | 0 | [0] | rejected | C_INTEGER.list | -| 4 | integer fraction | 10 | 500 | 0 | [0] | rejected | C_INTEGER.list | - -> NOTE: all the fail cases related with invariants were already contemplated in 3.6.1. - -### 3.6.3. Test case DV_PROPORTION unitary - -The C_INTEGER constraint applies to the `type` attribute. - -| type | meaning (kind) | numerator | denominator | precision | C_INTEGER.list | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------------|----------|----------------------------------| -| 0 | ratio | 10 | 500 | 0 | [1] | reejcted | C_INTEGER.list | -| 1 | unitary | 10 | 1 | 0 | [1] | accepted | | -| 2 | percent | 10 | 100 | 0 | [1] | rejected | C_INTEGER.list | -| 3 | fraction | 10 | 500 | 0 | [1] | rejected | C_INTEGER.list | -| 4 | integer fraction | 10 | 500 | 0 | [1] | rejected | C_INTEGER.list | - -### 3.6.4. Test case DV_PROPORTION percent - -The C_INTEGER constraint applies to the `type` attribute. - -| type | meaning (kind) | numerator | denominator | precision | C_INTEGER.list | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------------|----------|----------------------------------| -| 0 | ratio | 10 | 500 | 0 | [2] | reejcted | C_INTEGER.list | -| 1 | unitary | 10 | 1 | 0 | [2] | rejected | C_INTEGER.list | -| 2 | percent | 10 | 100 | 0 | [2] | accepted | | -| 3 | fraction | 10 | 500 | 0 | [2] | rejected | C_INTEGER.list | -| 4 | integer fraction | 10 | 500 | 0 | [2] | rejected | C_INTEGER.list | - -### 3.6.5. Test case DV_PROPORTION fraction - -The C_INTEGER constraint applies to the `type` attribute. - -| type | meaning (kind) | numerator | denominator | precision | C_INTEGER.list | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------------|----------|----------------------------------| -| 0 | ratio | 10 | 500 | 0 | [3] | rejected | C_INTEGER.list | -| 1 | unitary | 10 | 1 | 0 | [3] | rejected | C_INTEGER.list | -| 2 | percent | 10 | 100 | 0 | [3] | rejected | C_INTEGER.list | -| 3 | fraction | 10 | 500 | 0 | [3] | accepted | | -| 4 | integer fraction | 10 | 500 | 0 | [3] | rejected | C_INTEGER.list | - -### 3.6.6. Test case DV_PROPORTION integer fraction - -The C_INTEGER constraint applies to the `type` attribute. - -| type | meaning (kind) | numerator | denominator | precision | C_INTEGER.list | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------------|----------|----------------------------------| -| 0 | ratio | 10 | 500 | 0 | [4] | reejcted | C_INTEGER.list | -| 1 | unitary | 10 | 1 | 0 | [4] | rejected | C_INTEGER.list | -| 2 | percent | 10 | 100 | 0 | [4] | rejected | C_INTEGER.list | -| 3 | fraction | 10 | 500 | 0 | [4] | rejected | C_INTEGER.list | -| 4 | integer fraction | 10 | 500 | 0 | [4] | accepted | | - -### 3.6.7. Test case DV_PROPORTION fraction or integer fraction - -This case is similar to the previous one, it just tests a combination of possible types for the proportion. - -| type | meaning (kind) | numerator | denominator | precision | C_INTEGER.list | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------------|----------|----------------------------------| -| 0 | ratio | 10 | 500 | 0 | [3, 4] | reejcted | C_INTEGER.list | -| 1 | unitary | 10 | 1 | 0 | [3, 4] | rejected | C_INTEGER.list | -| 2 | percent | 10 | 100 | 0 | [3, 4] | rejected | C_INTEGER.list | -| 3 | fraction | 10 | 500 | 0 | [3, 4] | accepted | | -| 4 | integer fraction | 10 | 500 | 0 | [3, 4] | accepted | | - -### 3.6.8. Test case DV_PROPORTION ratio with range limits - -The C_INTEGER constraint applies to the `type` attribute. The C_REAL constraints apply to numerator and denominator respectively. - -| type | meaning (kind) | numerator | denominator | precision | C_INTEGER.list | C_REAL.range (num) | C_REAL.range (den) | expected | constraints violated | -|:----:|------------------|-----------|-------------|-----------|----------------|--------------------|--------------------|----------|----------------------| -| 0 | ratio | 10 | 500 | 0 | [0] | 5..20 | 200..600 | accepted | | -| 0 | ratio | 10 | 1 | 0 | [0] | 5..20 | 200..600 | rejected | C_REAL.range (den) | -| 0 | ratio | 30 | 500 | 0 | [0] | 5..20 | 200..600 | rejected | C_REAL.range (num) | -| 0 | ratio | 3 | 1000 | 0 | [0] | 5..20 | 200..600 | rejected | C_REAL.range (num), C_REAL.range (den) | - - - - -## 3.7. quantity.DV_INTERVAL - -### 3.7.1. Test case DV_INTERVAL open constraint - -The DV_INTERVAL constraint is {*}. - -> NOTE: the failure instance for this test case are related with violated interval semantics. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | -|-------|-------|-----------------|-----------------|----------------|----------------|----------|----------------------| -| NULL | NULL | true | true | false | false | accepted | | -| NULL | 100 | true | false | false | false | accepted | | -| NULL | 100 | true | false | false | true | accepted | | -| 0 | NULL | false | true | false | false | accepted | | -| 0 | NULL | false | true | true | false | accepted | | -| -20 | -5 | false | false | false | false | accepted | | -| 0 | 100 | false | false | true | true | accepted | | -| 10 | 100 | false | false | true | true | accepted | | -| -50 | 50 | false | false | true | true | accepted | | -| NULL | NULL | true | true | true | false | rejected | lower_included_valid (invariant) | -| 0 | NULL | false | true | false | true | rejected | upper_included_valid (invariant) | -| 200 | 100 | false | false | true | true | rejected | limits_consistent (invariant) | - - - -### 3.7.2. Test case DV_INTERVAL lower and upper range constraint. - -Lower and upper are DV_COUNT, which are constrainted internally by C_INTEGER. C_INTEGER has range and list constraints. - -> NOTE: the lower and upper limits are not constrained in terms of existence or occurrences, so both are optional. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | C_INTEGER.range (lower) | C_INTEGER.range (upper) | expected | constraints violated | -|-------|-------|-----------------|-----------------|----------------|----------------|-------------------------|-------------------------|----------|----------------------| -| NULL | NULL | true | true | false | false | 0..100 | 0..100 | accepted | | -| 0 | NULL | false | true | true | false | 0..100 | 0..100 | accepted | | -| NULL | 100 | true | false | false | true | 0..100 | 0..100 | accepted | | -| 0 | 100 | false | false | true | true | 0..100 | 0..100 | accepted | | -| -10 | 100 | false | false | true | true | 0..100 | 0..100 | rejected | C_INTEGER.range (lower) | -| 0 | 200 | false | false | true | true | 0..100 | 0..100 | rejected | C_INTEGER.range (upper) | -| -10 | 200 | false | false | true | true | 0..100 | 0..100 | rejected | C_INTEGER.range (lower), C_INTEGER.range (upper) | - - -### 3.7.3. Test case DV_INTERVAL lower and upper list constraint. - -Lower and upper are DV_COUNT, which are constrainted internally by C_INTEGER. C_INTEGER has range and list constraints. - -> NOTE: not all modeling tools allow a list constraint for the lower and upper attributes of the DV_INTERVAL. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | C_INTEGER.list (lower) | C_INTEGER.list (upper) | expected | constraints violated | -|-------|-------|-----------------|-----------------|----------------|----------------|-------------------------|-------------------------|----------|----------------------| -| NULL | NULL | true | true | false | false | [0, 5, 10, 100] | [0, 5, 10, 100] | accepted | | -| 0 | NULL | false | true | true | false | [0, 5, 10, 100] | [0, 5, 10, 100] | accepted | | -| NULL | 100 | true | false | false | true | [0, 5, 10, 100] | [0, 5, 10, 100] | accepted | | -| 0 | 100 | false | false | true | true | [0, 5, 10, 100] | [0, 5, 10, 100] | accepted | | -| -10 | 100 | false | false | true | true | [0, 5, 10, 100] | [0, 5, 10, 100] | rejected | C_INTEGER.list (lower) | -| 0 | 200 | false | false | true | true | [0, 5, 10, 100] | [0, 5, 10, 100] | rejected | C_INTEGER.list (upper) | -| -10 | 200 | false | false | true | true | [0, 5, 10, 100] | [0, 5, 10, 100] | rejected | C_INTEGER.list (lower), C_INTEGER.list (upper) | - - -## 3.8. quantity.DV_INTERVAL - -### 3.8.1. Test case DV_INTERVAL open constraint - -The DV_INTERVAL constraint is {*}. - -> NOTE: the failure instance for this test case are related with violated interval semantics. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | -|--------|--------|-----------------|-----------------|----------------|----------------|----------|----------------------| -| NULL | NULL | true | true | false | false | accepted | | -| NULL | 100 mg | true | false | false | false | accepted | | -| NULL | 100 mg | true | false | false | true | accepted | | -| 0 mg | NULL | false | true | false | false | accepted | | -| 0 mg | NULL | false | true | true | false | accepted | | -| 0 mg | 100 mg | false | false | true | true | accepted | | -| 10 mg | 100 mg | false | false | true | true | accepted | | -| NULL | NULL | true | true | true | false | rejected | lower_included_valid (invariant) | -| 0 mg | NULL | false | true | false | true | rejected | upper_included_valid (invariant) | -| 200 mg | 100 mg | false | false | true | true | rejected | limits_consistent (invariant) | - - -### 3.8.2. Test case DV_INTERVAL lower and upper constraints present - -The lower and upper constraints are C_DV_QUANTITY. - -> NOTE: in all cases the C_DV_QUANTITY.property referes to `temperature` to keep tests as simple as possible and be able to use negative values (for other physical properties negative values don't make sense). All temperatures will be measured in degree Celsius (`Cel` in UCUM). - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | C_DV_QUANTITY.list (lower) | C_DV_QUANTITY.list (upper) | expected | constraints violated | -|:---------:|:-------:|-----------------|-----------------|----------------|----------------|----------------------------|----------------------------|----------|-----------------------| -| NULL | NULL | true | true | false | false | [0..100 Cel] | [0..100 Cel] | accepted | | -| 0 Cel | NULL | false | true | true | false | [0..100 Cel] | [0..100 Cel] | accepted | | -| NULL | 100 Cel | true | false | false | true | [0..100 Cel] | [0..100 Cel] | accepted | | -| 0 Cel | 100 Cel | false | false | true | true | [0..100 Cel] | [0..100 Cel] | accepted | | -| -10 Cel | 100 Cel | false | false | true | true | [0..100 Cel] | [0..100 Cel] | rejected | C_DV_QUANTITY (lower) | -| 0 Cel | 200 Cel | false | false | true | true | [0..100 Cel] | [0..100 Cel] | rejected | C_DV_QUANTITY (upper) | -| -10 Cel | 200 Cel | false | false | true | true | [0..100 Cel] | [0..100 Cel] | rejected | C_DV_QUANTITY (lower),C_DV_QUANTITY (upper) | - - -## 3.9. quantity.DV_INTERVAL - -### 3.9.1. Test case DV_INTERVAL open constraint - -The DV_INTERVAL constraint is {*}. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | -|:----------------------------:|:----------------------------:|-----------------|-----------------|----------------|----------------|----------|-------------------------------| -| NULL | NULL | false | false | true | true | rejected | RM/Schema: value is mandatory for lower and upper | -| NULL | "" | false | false | true | true | rejected | RM/Schema: value is mandatory for lower. ISO8601: at least year is required for upper. | -| "" | NULL | false | false | true | true | rejected | ISO8601: at least year is required for lower. RM/Schema: value is mandatory for upper. -| 2021 | NULL | false | false | true | true | rejected | RM/Schema: value is mandatory for upper. | -| NULL | 2022 | false | false | true | true | rejected | RM/Schema: value is mandatory for lower. | -| 2021 | 2022 | false | false | true | true | accepted | | -| 2021-00 | 2022-01 | false | false | true | true | rejected | ISO8601: month in 01..12 for lower. | -| 2021-01 | 2022-01 | false | false | true | true | accepted | | -| 2021-01-00 | 2022-01-01 | false | false | true | true | rejected | ISO8601: day in 01..31 for lower. | -| 2021-01-32 | 2022-01-01 | false | false | true | true | rejected | ISO8601: day in 01..31 for lower. | -| 2021-01-01 | 2022-01-00 | false | false | true | true | rejected | ISO8601: day in 01..31 for upper. | -| 2021-01-30 | 2022-01-00 | false | false | true | true | rejected | ISO8601: day in 01..31 for upper. | -| 2021-01-30 | 2022-01-15 | false | false | true | true | accepted | | -| 2021-10-24T48 | 2022-01-15T10 | false | false | true | true | rejected | ISO8601: hours in 00..23 for lower. | -| 2021-10-24T21 | 2022-01-15T73 | false | false | true | true | rejected | ISO8601: hours in 00..23 for upper. | -| 2021-10-24T05 | 2022-01-15T10 | false | false | true | true | accepted | | -| 2021-10-24T05:95 | 2022-01-15T10:45 | false | false | true | true | rejected | ISO8601: minutes in 00..59 for lower. | -| 2021-10-24T05:30 | 2022-01-15T10:61 | false | false | true | true | rejected | ISO8601: minutes in 00..59 for upper. | -| 2021-10-24T05:30 | 2022-01-15T10:45 | false | false | true | true | accepted | | -| 2021-10-24T05:30:78 | 2022-01-15T10:45:13 | false | false | true | true | rejected | ISO8601: seconds in 00..59 for lower. | -| 2021-10-24T05:30:47 | 2022-01-15T10:45:69 | false | false | true | true | rejected | ISO8601: seconds in 00..59 for upper. | -| 2021-10-24T05:30:47 | 2022-01-15T10:45:13 | false | false | true | true | accepted | | -| 2021-10-24T05:30:47.5 | 2022-01-15T10:45:13.6 | false | false | true | true | accepted | | -| 2021-10-24T05:30:47.333 | 2022-01-15T10:45:13.555 | false | false | true | true | accepted | | -| 2021-10-24T05:30:47.333333 | 2022-01-15T10:45:13.555555 | false | false | true | true | accepted | | -| 2021-10-24T05:30:47Z | 2022-01-15T10:45:13Z | false | false | true | true | accepted | | -| 2021-10-24T05:30:47-03:00 | 2022-01-15T10:45:13-03:00 | false | false | true | true | accepted | | - - -### 3.9.2. Test case DV_INTERVAL lower and upper constraints are validity kind - -> NOTE: the C_DATE_TIME has invariants that define if a higher precision component is optional or prohibited, lower precision components should be optional or prohibited. In other words, if `month` is optional, `day`, `hours`, `minutes`, etc. are optional or prohibited. These invariants should be checked in an archetype editor and template editor, we consider the following tests to follow those rules without checking them, since that is related to archetype/template validation, not with data validation. - -> NOTE: if different components of each lower/upper date time expression fail the validity constraint for `mandatory`, the only required constraint violated to be reported is the higher precision one, since it implies the lower precision components will also fail. For instance if the hour, second and millisecond are `mandatory`, and the corresponding date time expression doesn't have hour, it is accepted if the reported constraints violated is only the hour_validity, and optionally the SUT can report the minute_validity, second_validity and millisecond_validity constraints as violated too. In the data sets below we show all the constraints violated. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | month_val. (lower) | day_val. (lower) | month_val. (upper) | day_val. (upper) | hour_val. (lower) | minute_val. (lower) | second_val. (lower) | millisecond_val. (lower) | timezone_val. (lower) | hour_val. (upper) | minute_val. (upper) | second_val. (upper) | millisecond_val. (upper) | timezone_val. (upper) | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|--------------------|------------------|--------------------|------------------|-------------------|---------------------|---------------------|-------------------------|-----------------------|-------------------|---------------------|---------------------|--------------------------|-----------------------|----------|-------------------------------| -| 2021 | 2022 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | month_val. (lower), day_val. (lower), month_val. (upper), day_val. (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper), millisecond_val. (lower), millisecond_val. (upper), timezone_val. (lower), timezone__val. (upper) | -| 2021 | 2022 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | month_validity (lower), month_validity (upper), timezone_val. (lower), timezone__val. (upper) | -| 2021 | 2022 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | rejected | month_validity (lower), month_validity (upper) | -| 2021 | 2022 | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021 | 2022 | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021 | 2022 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | mandatory | rejected | month_validity (lower), month_validity (upper), timezone_val. (lower), timezone__val. (upper) | -| 2021 | 2022 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), month_validity (upper) | -| 2021 | 2022 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021 | 2022 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | accepted | | -| 2021-10 | 2022-10 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | day_validity (lower), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper), millisecond_val. (lower), millisecond_val. (upper), timezone_val. (lower), timezone__val. (upper)| -| 2021-10 | 2022-10 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone_val. (upper) | -| 2021-10 | 2022-10 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021-10 | 2022-10 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | mandatory | prohibited | prohibited | prohibited | mandatory | mandatory | rejected | timezone_val. (lower), timezone_val. (upper) | -| 2021-10 | 2022-10 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | accepted | | -| 2021-10 | 2022-10 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | mandatory | rejected | month_validity (lower), month_validity (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10 | 2022-10 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), month_validity (upper) | - - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | month_val. (lower) | day_val. (lower) | month_val. (upper) | day_val. (upper) | hour_val. (lower) | minute_val. (lower) | second_val. (lower) | millisecond_val. (lower) | timezone_val. (lower) | hour_val. (upper) | minute_val. (upper) | second_val. (upper) | millisecond_val. (upper) | timezone_val. (upper) | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|--------------------|------------------|--------------------|------------------|-------------------|---------------------|---------------------|-------------------------|-----------------------|-------------------|---------------------|---------------------|--------------------------|-----------------------|----------|-------------------------------| -| 2021-10-24 | 2022-10-24 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper), millisecond_val. (lower), millisecond_val. (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | mandatory | mandatory | optional | optional | optional | mandatory | rejected | hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | optional | mandatory | optional | optional | optional | optional | rejected | hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021-10-24 | 2022-10-24 | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | day_validity (lower), day_validity (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper) | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper), millisecond_val. (lower), millisecond_val. (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | mandatory | mandatory | optional | optional | optional | mandatory | rejected | minute_val. (lower), minute_val. (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | optional | mandatory | optional | optional | optional | optional | rejected | minute_val. (lower), minute_val. (upper) | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | day_validity (lower), day_validity (upper), hour_val. (lower), hour_val. (upper) | -| 2021-10-24T22 | 2022-10-24T07 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper), hour_val. (lower), hour_val. (upper) | - - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | month_val. (lower) | day_val. (lower) | month_val. (upper) | day_val. (upper) | hour_val. (lower) | minute_val. (lower) | second_val. (lower) | millisecond_val. (lower) | timezone_val. (lower) | hour_val. (upper) | minute_val. (upper) | second_val. (upper) | millisecond_val. (upper) | timezone_val. (upper) | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|--------------------|------------------|--------------------|------------------|-------------------|---------------------|---------------------|-------------------------|-----------------------|-------------------|---------------------|---------------------|--------------------------|-----------------------|----------|-------------------------------| -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | second_val. (lower), second_val. (upper), millisecond_val. (lower), millisecond_val. (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | mandatory | mandatory | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | optional | mandatory | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | day_validity (lower), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper) | -| 2021-10-24T22:10 | 2022-10-24T07:47 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper) | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | millisecond_val. (lower), millisecond_val. (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | mandatory | mandatory | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | optional | mandatory | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | day_validity (lower), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper) | -| 2021-10-24T22:10:45 | 2022-10-24T07:47:13 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper) | - - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | month_val. (lower) | day_val. (lower) | month_val. (upper) | day_val. (upper) | hour_val. (lower) | minute_val. (lower) | second_val. (lower) | millisecond_val. (lower) | timezone_val. (lower) | hour_val. (upper) | minute_val. (upper) | second_val. (upper) | millisecond_val. (upper) | timezone_val. (upper) | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|--------------------|------------------|--------------------|------------------|-------------------|---------------------|---------------------|-------------------------|-----------------------|-------------------|---------------------|---------------------|--------------------------|-----------------------|----------|-------------------------------| -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | mandatory | mandatory | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | optional | mandatory | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | rejected | timezone_val. (lower), timezone__val. (upper) | -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | day_validity (lower), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), seoncd_val. (lower), second_val. (upper), millisecond_val. (lower), millisecond_val. (upper) | -| 2021-10-24T22:10:45.5 | 2022-10-24T07:47:13.666666 | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), seoncd_val. (lower), second_val. (upper), millisecond_val. (lower), millisecond_val. (upper) | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | millisecond_val. (lower), millisecond_val. (upper) | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | mandatory | mandatory | optional | optional | optional | mandatory | accepted | | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | optional | optional | optional | mandatory | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | accepted | | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | mandatory | optional | mandatory | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | optional | accepted | | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | optional | optional | optional | optional | optional | optional | optional | optional | mandatory | optional | optional | optional | optional | mandatory | accepted | | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | mandatory | prohibited | mandatory | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | day_validity (lower), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper), timezone_val. (lower), timezone_val. (upper) | -| 2021-10-24T22:10:45Z | 2022-10-24T07:47:13Z | false | false | true | true | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper), hour_val. (lower), hour_val. (upper), minute_val. (lower), minute_val. (upper), second_val. (lower), second_val. (upper), timezone_val. (lower), timezone_val. (upper) | - - - -### 3.9.3. Test case DV_INTERVAL lower and upper constraints are range - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | C_DATE_TIME.range (lower) | C_DATE_TIME.range (upper) | expected | constraints violated | -|:------------------:|:------------------:|-----------------|-----------------|----------------|----------------|---------------------------------|---------------------------------|----------|-------------------------------------------| -| 2021 | 2022 | false | false | true | true | 2020..2030 | 2020..2030 | accepted | | -| 2021 | 2022 | false | false | true | true | 2000..2010 | 2020..2030 | rejected | C_DATE_TIME.range (lower) | -| 2021 | 2022 | false | false | true | true | 2020..2030 | 2020..2021 | rejected | C_DATE_TIME.range (upper) | -| 2021-10 | 2022-11 | false | false | true | true | 2020-01..2030-12 | 2020-01..2030-12 | accepted | | -| 2021-10 | 2022-11 | false | false | true | true | 2000-01..2010-12 | 2020-01..2030-12 | rejected | C_DATE_TIME.range (lower) | -| 2021-10 | 2022-11 | false | false | true | true | 2020-01..2030-12 | 2020-01..2021-12 | rejected | C_DATE_TIME.range (upper) | -| 2021-10-24 | 2022-11-02 | false | false | true | true | 2020-01-01..2030-12-31 | 2020-01-01..2030-12-31 | accepted | | -| 2021-10-24 | 2022-11-02 | false | false | true | true | 2000-01-01..2010-12-31 | 2020-01-01..2030-12-31 | rejected | C_DATE_TIME.range (lower) | -| 2021-10-24 | 2022-11-02 | false | false | true | true | 2020-01-01..2030-12-31 | 2020-01-01..2021-12-31 | rejected | C_DATE_TIME.range (upper) | -| 2021-10-24T10 | 2022-11-02T19 | false | false | true | true | 2020-01-01T00..2030-12-31T23 | 2020-01-01T00..2030-12-31T23 | accepted | | -| 2021-10-24T10 | 2022-11-02T19 | false | false | true | true | 2000-01-01T00..2010-12-31T23 | 2020-01-01T00..2030-12-31T23 | rejected | C_DATE_TIME.range (lower) | -| 2021-10-24T10 | 2022-11-02T19 | false | false | true | true | 2020-01-01T00..2030-12-31T23 | 2020-01-01T00..2021-12-31T23 | rejected | C_DATE_TIME.range (upper) | -| 2021-10-24T10:00 | 2022-11-02T19:32 | false | false | true | true | 2020-01-01T00:00..2030-12-31T23:59 | 2020-01-01T00:00..2030-12-31T23:59 | accepted | | -| 2021-10-24T10:00 | 2022-11-02T19:32 | false | false | true | true | 2000-01-01T00:00..2010-12-31T23:59 | 2020-01-01T00:00..2030-12-31T23:59 | rejected | C_DATE_TIME.range (lower) | -| 2021-10-24T10:00 | 2022-11-02T19:32 | false | false | true | true | 2020-01-01T00:00..2030-12-31T23:59 | 2020-01-01T00:00..2021-12-31T23:59 | rejected | C_DATE_TIME.range (upper) | -| 2021-10-24T10:00:10 | 2022-11-02T19:32:40 | false | false | true | true | 2020-01-01T00:00:00..2030-12-31T23:59:59 | 2020-01-01T00:00..2030-12-31T23:59 | accepted | | -| 2021-10-24T10:00:10 | 2022-11-02T19:32:40 | false | false | true | true | 2000-01-01T00:00:00..2010-12-31T23:59:59 | 2020-01-01T00:00..2030-12-31T23:59 | rejected | C_DATE_TIME.range (lower) | -| 2021-10-24T10:00:10 | 2022-11-02T19:32:40 | false | false | true | true | 2020-01-01T00:00:00..2030-12-31T23:59:59 | 2020-01-01T00:00..2021-12-31T23:59 | rejected | C_DATE_TIME.range (upper) | -| 2021-10-24T10:00:10.5 | 2022-11-02T19:32:40.333 | false | false | true | true | 2020-01-01T00:00:00.0..2030-12-31T23:59:59.999999 | 2020-01-01T00:00..2030-12-31T23:59 | accepted | | -| 2021-10-24T10:00:10.5 | 2022-11-02T19:32:40.333 | false | false | true | true | 2000-01-01T00:00:00.0..2010-12-31T23:59:59.999999 | 2020-01-01T00:00..2030-12-31T23:59 | rejected | C_DATE_TIME.range (lower) | -| 2021-10-24T10:00:10.5 | 2022-11-02T19:32:40.333 | false | false | true | true | 2020-01-01T00:00:00.0..2030-12-31T23:59:59.999999 | 2020-01-01T00:00..2021-12-31T23:59 | rejected | C_DATE_TIME.range (upper) | -| 2021-10-24T10:00:10Z | 2022-11-02T19:32:40Z | false | false | true | true | 2020-01-01T00:00:00Z..2030-12-31T23:59:59Z | 2020-01-01T00:00..2030-12-31T23:59 | accepted | | -| 2021-10-24T10:00:10Z | 2022-11-02T19:32:40Z | false | false | true | true | 2000-01-01T00:00:00Z..2010-12-31T23:59:59Z | 2020-01-01T00:00..2030-12-31T23:59 | rejected | C_DATE_TIME.range (lower) | -| 2021-10-24T10:00:10Z | 2022-11-02T19:32:40Z | false | false | true | true | 2020-01-01T00:00:00Z..2030-12-31T23:59:59Z | 2020-01-01T00:00..2021-12-31T23:59 | rejected | C_DATE_TIME.range (upper) | - - -## 3.10. quantity.DV_INTERVAL - -### 3.10.1. Test case DV_INTERVAL open constraint - -On this case, the own rules/invariants of the DV_INTERVAL apply to the validation. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|----------|-------------------------------| -| NULL | NULL | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | -| NULL | 2022 | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | -| 2021 | NULL | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | -| 2021 | 2022 | false | false | true | true | accepted | | -| 2021-01 | 2022-08 | false | false | true | true | accepted | | -| 2021-01-20 | 2022-08-11 | false | false | true | true | accepted | | -| 2021 | 2021-10 | false | false | true | true | rejected | IMO two dates with different components and common higher order components (year on this case) shouldn't be strictly comparable, see https://discourse.openehr.org/t/issues-with-date-time-comparison-for-partial-date-time-expressions/2173 | -| NULL | NULL | true | true | false | false | accepted | | - - -### 3.10.2. Test case DV_INTERVAL validity kind constraint - -``` -NOTE: this test case doesn't include all the possible combinations of lower/upper data and constraints for the internal since there could be tens of possible combinations. It would be in the scope of a revision to add more combinations of an exhaustive test case. -``` - -> NOTE: the C_DATE has invariants that define if a higher precision component is optional or prohibited, lower precision components should be optional or prohibited. In other words, if `month` is optional, `day` should be optional or prohibited. These invariants should be checked in an archetype editor and template editor, we consider the following tests to follow those rules without checking them, since that is related to archetype/template validation, not with data validation. - - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | month_val. (lower) | day_val. (lower) | month_val. (upper) | day_val. (upper) | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|--------------------|------------------|--------------------|------------------|----------|-------------------------------| -| 2021 | 2022 | false | false | true |true | mandatory | mandatory | mandatory | mandatory | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper) | -| 2021 | 2022 | false | false | true |true | mandatory | optional | mandatory | optional | rejected | month_validity (lower), month_validity (upper) | -| 2021 | 2022 | false | false | true |true | optional | optional | optional | optional | accepted | | -| 2021 | 2022 | false | false | true |true | mandatory | prohibited | mandatory | prohibited | rejected | month_validity (lower), month_validity (upper) | -| 2021 | 2022 | false | false | true |true | prohibited | prohibited | prohibited | prohibited | accepted | | -| 2021-10 | 2022-10 | false | false | true |true | mandatory | mandatory | mandatory | mandatory | rejected | day_validity (lower), day_validity (upper) | -| 2021-10 | 2022-10 | false | false | true |true | mandatory | optional | mandatory | optional | accepted | | -| 2021-10 | 2022-10 | false | false | true |true | optional | optional | optional | optional | accepted | | -| 2021-10 | 2022-10 | false | false | true |true | mandatory | prohibited | mandatory | prohibited | accepted | | -| 2021-10 | 2022-10 | false | false | true |true | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), month_validity (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true |true | mandatory | mandatory | mandatory | mandatory | accepted | | -| 2021-10-24 | 2022-10-24 | false | false | true |true | mandatory | optional | mandatory | optional | accepted | | -| 2021-10-24 | 2022-10-24 | false | false | true |true | optional | optional | optional | optional | accepted | | -| 2021-10-24 | 2022-10-24 | false | false | true |true | mandatory | prohibited | mandatory | prohibited | rejected | day_validity (lower), day_validity (upper) | -| 2021-10-24 | 2022-10-24 | false | false | true |true | prohibited | prohibited | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper) | -| 2021 | 2022 | false | false | true |true | mandatory | mandatory | mandatory | optional | rejected | month_validity (lower), day_validity (lower), month_validity (upper) | -| 2021 | 2022 | false | false | true |true | mandatory | mandatory | optional | optional | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022 | false | false | true |true | mandatory | mandatory | mandatory | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper) | -| 2021 | 2022 | false | false | true |true | mandatory | mandatory | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022-10 | false | false | true |true | mandatory | mandatory | mandatory | mandatory | rejected | month_validity (lower), day_validity (lower), day_validity (upper) | -| 2021 | 2022-10 | false | false | true |true | mandatory | mandatory | mandatory | optional | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022-10 | false | false | true |true | mandatory | mandatory | optional | optional | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022-10 | false | false | true |true | mandatory | mandatory | mandatory | prohibited | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022-10 | false | false | true |true | mandatory | mandatory | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper) | -| 2021 | 2022-10-24 | false | false | true |true | mandatory | mandatory | mandatory | mandatory | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022-10-24 | false | false | true |true | mandatory | mandatory | mandatory | optional | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022-10-24 | false | false | true |true | mandatory | mandatory | optional | optional | rejected | month_validity (lower), day_validity (lower) | -| 2021 | 2022-10-24 | false | false | true |true | mandatory | mandatory | mandatory | prohibited | rejected | month_validity (lower), day_validity (lower), day_validity (upper) | -| 2021 | 2022-10-24 | false | false | true |true | mandatory | mandatory | prohibited | prohibited | rejected | month_validity (lower), day_validity (lower), month_validity (upper), day_validity (upper) | - - - -### 3.10.3. Test case DV_INTERVAL range constraint - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | C_DATE.range (lower) | C_DATE.range (upper) | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|----------------------|----------------------|----------|---------------------------| -| 2021 | 2022 | false | false | true | true | 1900..2030 | 1900..2030 | accepted | | -| 2021 | 2022 | false | false | true | true | 2022..2030 | 1900..2030 | rejected | C_DATE.range (lower) | -| 2021 | 2022 | false | false | true | true | 1900..2030 | 2023..2030 | rejected | C_DATE.range (upper) | -| 2021 | 2022 | false | false | true | true | 2022..2030 | 2023..2030 | rejected | C_DATE.range (lower), C_DATE.range (upper) | - - - - - -## 3.11. quantity.DV_INTERVAL - -### 3.11.1. Test case DV_INTERVAL open constraint - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|----------|-------------------------------| -| NULL | NULL | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | -| NULL | T11:00:00 | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | -| T10:00:00 | NULL | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | -| T10 | T11 | false | false | true | true | accepted | | -| T10:00 | T11:00 | false | false | true | true | accepted | | -| T10:00:00 | T11:00:00 | false | false | true | true | accepted | | -| T10 | T10:45:00 | false | false | true | true | rejected | IMO two times with different components and common higher order components (hour on this case) shouldn't be strictly comparable, see https://discourse.openehr.org/t/issues-with-date-time-comparison-for-partial-date-time-expressions/2173 | -| NULL | NULL | true | true | false | false | accepted | | - - -### 3.11.2. Test case DV_INTERVAL validity kind constraint - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | minute_val. (lower) | second_val. (lower) | millisecond_val. (lower) | timezone_val. (lower) | minute_val. (upper) | second_val. (upper) | millisecond_val. (upper) | timezone_val. (upper) | expected | constraints violated | -|:------------:|:------------:|-----------------|-----------------|----------------|----------------|---------------------|---------------------|-------------------------|-----------------------|---------------------|---------------------|--------------------------|-----------------------|---------|-------------------------------| -| T10 | T11 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | minute_val. (lower), second_val. (lower), millisecond_val. (lower), timezone_val. (lower), minute_val. (upper), second_val. (upper), millisecond_val. (upper), timezone_val. (upper) | -| T10:00 | T11:00 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | second_val. (lower), millisecond_val. (lower), timezone_val. (lower), second_val. (upper), millisecond_val. (upper), timezone_val. (upper) | -| T10:00:00 | T11:00:00 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | millisecond_val. (lower), timezone_val. (lower), millisecond_val. (upper), timezone_val. (upper) | -| T10:00:00.5 | T11:00:00.5 | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | rejected | timezone_val. (lower) timezone_val. (upper) | -| T10:00:00.5Z | T11:00:00.5Z | false | false | true | true | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | mandatory | accepted | | - -TBD: combinations of other values for validity. - - -### 3.11.3. Test case DV_INTERVAL range constraint - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | C_TIME.range (lower) | C_TIME.range (upper) | expected | constraints violated | -|:-------------:|:-------------:|-----------------|-----------------|----------------|----------------|---------------------------|----------------------------|----------|---------------------------| -| T10 | T11 | false | false | true | true | T09..T11 | T10..T12 | accepted | | -| T10:00 | T11:00 | false | false | true | true | T09:00..T11:00 | T10:00..T12:00 | accepted | | -| T10:00:00 | T11:00:00 | false | false | true | true | T09:00:00..T11:00:00 | T10:00:00..T12:00:00 | accepted | | -| T10:00:00.5 | T11:00:00.5 | false | false | true | true | T09:00:00.0..T11:00:00.0 | T10:00:00.0..T12:00:00.0 | accepted | | -| T10:00:00.5Z | T11:00:00.5Z | false | false | true | true | T09:00:00.0..T11:00:00.0Z | T10:00:00.0Z..T12:00:00.0Z | accepted | | -| T10 | T11 | false | false | true | true | T11..T12 | T11..T12 | rejected | C_TIME.range (lower) | -| T10 | T12 | false | false | true | true | T10..T11 | T10..T11 | rejected | C_TIME.range (upper) | -| null | T11 | true | false | false | true | T09..T11 | T10..T12 | rejected | C_TIME.range (lower) | -| T10 | null | false | true | true | false | T09..T11 | T10..T12 | accepted | C_TIME.range (upper) | - - - -## 3.12. quantity.DV_INTERVAL - -### 3.12.1. Test case DV_INTERVAL open constraint - -> NOTE: this considers the `lower` value of the interval should have all it's components lower or equals to the corresponding component in the `upper` value. This is to avoid normalization problems. For instance we could have an interval `P1Y6M..P2Y` which is semantically correct. But if we have values outside the normal boundaries of each component, like `P1Y37M..P2Y` there is a need of normalization to know if `P1Y37M` is really lower or equals to `P2Y`, which is the check ofr a valid internal. In some cases this normalization is doable, but in other cases it is not. For instance, some implementations might not know how many days in a month are, since months have a variable number of days. In the previous case, we know each year has 12 months so `P1Y37M` can actually be normalized to `P4Y1M`, but `P61D` can't be strictly compared with, let's say, `P3M`, since months could have 28, 29, 30 or 31 days, so without other information `P61D` could be lower or greater than `P3M`. To simplify this, some implementations might consider the measure of a `month`, in a duration expression, to be exactly 30 days. These considerations should be stated in the SUT Conformance Statement Document. To simplify writing the test cases for any implementation, we consider if `lower` is `P1Y37M`, the valid `upper` values have Y >= 1 and M >= 37, so `P2Y` wouldn't be valid in this context, but `P1Y37M..P1Y38M` or `P1Y37M..P2Y37M` would be valid intervals for the test cases. One extra simplification would be to consider values are inside their normal boundaries (hours < 24, days < 31, etc.) but this won't be encouraged but these test cases. If each component is inside it's constrainsts it is possible to compare expressions that differ in the components like `P1D3H` and `P10D`, since comparison doesn't require normalization and both values form a semantically valid interval. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | comment | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|----------|-------------------------------|---------| -| NULL | NULL | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | | -| NULL | PT2H | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | | -| PT1H | NULL | false | false | true | true | rejected | IMO should fail, see https://discourse.openehr.org/t/is-dv-interval-missing-invariants/2210 | | -| PT1H | PT2H | false | false | true | true | accepted | | | -| PT1H | PT2H | false | false | true | true | accepted | | | -| P1Y7M3D | P1Y8M3D | false | false | true | true | accepted | | | -| P1M5DT3H | P10M | false | false | true | true | accepted | | Note this case has different components in the lower and upper values, this is possible because the values don't exceed their normal boundaries, e.g. `days` > 31. Without this condition a normalization of the values would be needed, and in some cases the normalization is not possible without some extra constraints, for instance considering `P1M` is equivalent to `P30D`. | -| P2M | P1M | false | false | true | true | rejected | limits_consistent (invariant) | | -| P10M | P1M5DT3H | false | false | true | true | rejected | limits_consistent (invariant) | | - -### 3.12.2. Test case DV_INTERVAL xxx_allowed constraints - -> NOTE: in the openEHR specifications only the seconds can have a fraction, but in the ISO8601 standard, the component at the lowest precision can have a fraction, for instance `P0.5Y` is a valid ISO 8601 duration. - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | years_allowed (lower) | months_allowed (lower) | weeks_allowed (lower) | days_allowed (lower) | hours_allowed (lower) | minutes_allowed (lower) | seconds_allowed (lower) | fractional_seconds_allowed (lower) | years_allowed (upper) | months_allowed (upper) | weeks_allowed (upper) | days_allowed (upper) | hours_allowed (upper) | minutes_allowed (upper) | seconds_allowed (upper) | fractional_seconds_allowed (upper) | expected | constraints violated | comment | -|:----------------:|:----------:|-----------------|-----------------|----------------|----------------|-----------------------|------------------------|-----------------------|----------------------|-----------------------|-------------------------|-------------------------|------------------------------------|-----------------------|------------------------|-----------------------|----------------------|-----------------------|-------------------------|-------------------------|------------------------------------|----------|------------------------------------|---------| -| P1Y | P2Y | false | false | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | accepted | | | -| P3W | P5W | false | false | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | accepted | | | -| P1Y | P2Y | false | false | true | true | false | true | true | true | true | true | true | true | true | true | true | true | true | true | true | true | rejected | years_allowed (lower) | | -| P1Y | P2Y | false | false | true | true | true | true | true | true | true | true | true | true | false | true | true | true | true | true | true | true | rejected | years_allowed (upper) | | -| P1Y1M1DT1H1M1.5S | P2Y | false | false | true | true | true | false | true | true | true | true | true | true | true | true | true | true | true | true | true | true | rejected | months_allowed (lower) | | -| P2W | P2Y | false | false | true | true | true | true | false | true | true | true | true | true | true | true | true | true | true | true | true | true | rejected | weeks_allowed (lower) | | -| P1Y1M1DT1H1M1.5S | P2Y | false | false | true | true | true | true | true | false | true | true | true | true | true | true | true | true | true | true | true | true | rejected | days_allowed (lower) | | -| P1Y1M1DT1H1M1.5S | P2Y | false | false | true | true | true | true | true | true | false | true | true | true | true | true | true | true | true | true | true | true | rejected | hours_allowed (lower) | | -| P1Y1M1DT1H1M1.5S | P2Y | false | false | true | true | true | true | true | true | true | false | true | true | true | true | true | true | true | true | true | true | rejected | minutes_allowed (lower) | | -| P1Y1M1DT1H1M1.5S | P2Y | false | false | true | true | true | true | true | true | true | true | false | true | true | true | true | true | true | true | true | true | rejected | seconds_allowed (lower) | | -| P1Y1M1DT1H1M1.5S | P2Y | false | false | true | true | true | true | true | true | true | true | true | false | true | true | true | true | true | true | true | true | rejected | fractional_seconds_allowed (lower) | | - - -### 3.12.3. Test case DV_INTERVAL range constraints - -| lower | upper | lower_unbounded | upper_unbounded | lower_included | upper_included | range.lower (lower) | range.upper (lower) | range.lower (upper) | range.upper (upper) | expected | constraints violated | comment | -|:----------:|:----------:|-----------------|-----------------|----------------|----------------|---------------------|---------------------|---------------------|---------------------|----------|-------------------------------|---------| -| P1Y | P2Y | false | false | true | true | P1Y | P3Y | P1Y | P3Y | accepted | | | -| P1Y | P2Y | false | false | true | true | P2Y | P3Y | P1Y | P3Y | rejected | range.lower (lower) | | -| P1Y | P2Y | false | false | true | true | P1Y | P3Y | P3Y | P4Y | rejected | range.lower (upper) | | -| P5Y | P10Y | false | false | true | true | P2Y | P3Y | P5Y | P15Y | rejected | range.upper (lower) | | -| P5Y | P10Y | false | false | true | true | P1Y | P9Y | P3Y | P9Y | rejected | range.upper (upper) | | -| P5Y4M | P10Y | false | false | true | true | P1Y | P9Y | P3Y | P15Y | accepted | | | -| P5Y4M | P10Y | false | false | true | true | P6Y | P9Y | P3Y | P15Y | rejected | range.lower (lower) | | -| P5Y4M | P10Y | false | false | true | true | P5Y4M2D | P9Y | P3Y | P15Y | rejected | range.lower (lower) | | -| P5Y4M20D | P10Y | false | false | true | true | P1Y | P9Y | P3Y | P15Y | accepted | | | -| P5Y4M20D | P10Y | false | false | true | true | P5Y6M | P9Y | P3Y | P15Y | rejected | range.lower (lower) | | - - - - -## 3.13. quantity.DV_INTERVAL - -> NOTE: some modeling tools don't support representing DV_INTERVAL. - -### 3.13.1. Test case DV_INTERVAL open constraint - -This case is when the ADL has `DV_ORDINAL matches {*}` - -| lower.symbol | lower.value | upper.symbol | upper.value | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | -|:---------------|------------:|:---------------|------------:|-----------------|-----------------|----------------|----------------|----------|----------------------| -| NULL | NULL | NULL | NULL | false | false | true | true | rejected | RM/Schema value and symbol are mandatory for lower and upper | -| NULL | 1 | NULL | 5 | false | false | true | true | rejected | RM/Schema symbol is mandatory for lower and upper | -| local::at0005 | NULL | local::at0003 | NULL | false | false | true | true | rejected | RM/Schema value is mandatory for lower and upper | -| local::at0005 | 1 | local::at0002 | 5 | false | false | true | true | accepted | | -| local::at0004 | 666 | local::at0003 | 777 | false | false | true | true | accepted | | -| local::at0003 | 777 | local::at0004 | 666 | false | false | true | true | rejected | RM invariante Interval.Limits_comparable | - - -### 3.13.2. Test case DV_INTERVAL with constraints - -| lower.symbol | lower.value | upper.symbol | upper.value | lower_unbounded | upper_unbounded | lower_included | upper_included | lower.C_DV_ORDINAL.list | upper.C_DV_ORDINAL.list | expected | constraints violated | -|:---------------|------------:|:---------------|------------:|-----------------|-----------------|----------------|----------------|----------------------------------------|----------------------------------------|----------|----------------------| -| local::at0005 | 1 | local::at0002 | 5 | false | false | true | true | 1|[local::at0005], 2|[local::at0006] | 5|[local::at0002], 2|[local::at0006] | accepted | | -| local::at0004 | 666 | local::at0003 | 777 | false | false | true | true | 8|[local::at0004], 2|[local::at0006] | 9|[local::at0003], 2|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching value for lower and upper | -| local::at0666 | 1 | local::at0777 | 2 | false | false | true | true | 1|[local::at0005], 2|[local::at0006] | 1|[local::at0005], 2|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching symbol for lower and upper | -| local::at0004 | 666 | local::at0003 | 777 | false | false | true | true | 8|[local::at0004], 2|[local::at0006] | 777|[local::at0003], 2|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching value for lower | -| local::at0666 | 1 | local::at0777 | 2 | false | false | true | true | 1|[local::at0005], 2|[local::at0006] | 1|[local::at0005], 2|[local::at0777] | rejected | C_DV_ORDINAL.list: no matching symbol for lower | -| local::at0004 | 666 | local::at0003 | 777 | false | false | true | true | 666|[local::at0004], 2|[local::at0006] | 9|[local::at0003], 2|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching value for upper | -| local::at0005 | 1 | local::at0777 | 5 | false | false | true | true | 1|[local::at0005], 2|[local::at0006] | 1|[local::at0005], 5|[local::at0999] | rejected | C_DV_ORDINAL.list: no matching symbol for upper | - - - -## 3.14. quantity.DV_INTERVAL - -DV_SCALE was introduced to the RM 1.1.0 (https://openehr.atlassian.net/browse/SPECRM-19), it is analogous to DV_ORDINAL with a Real value. So test cases for DV_SCALE and DV_ORDINAL are similar. - -NOTE: if this specification is implemented on a system that supports a RM < 1.1.0, then these tests shouldn't run against the system. - -> NOTE: some modeling tools don't support representing DV_INTERVAL - -### 3.14.1. Test case DV_SCALE open constraint - -This case is when the ADL has `DV_ORDINAL matches {*}` - -| lower.symbol | lower.value | upper.symbol | upper.value | lower_unbounded | upper_unbounded | lower_included | upper_included | expected | constraints violated | -|:---------------|------------:|:---------------|------------:|-----------------|-----------------|----------------|----------------|----------|----------------------| -| NULL | NULL | NULL | NULL | false | false | true | true | rejected | RM/Schema value and symbol are mandatory for lower and upper | -| NULL | 1.5 | NULL | 5.3 | false | false | true | true | rejected | RM/Schema symbol is mandatory for lower and upper | -| local::at0005 | NULL | local::at0003 | NULL | false | false | true | true | rejected | RM/Schema value is mandatory for lower and upper | -| local::at0005 | 1.5 | local::at0002 | 5.3 | false | false | true | true | accepted | | -| local::at0004 | 666.1 | local::at0003 | 777.1 | false | false | true | true | accepted | | -| local::at0003 | 777.1 | local::at0004 | 666.1 | false | false | true | true | rejected | RM invariante Interval.Limits_comparable | - - -### 3.14.2. Test case DV_SCALE with constraints - -| lower.symbol | lower.value | upper.symbol | upper.value | lower_unbounded | upper_unbounded | lower_included | upper_included | lower.C_DV_ORDINAL.list | upper.C_DV_ORDINAL.list | expected | constraints violated | -|:---------------|------------:|:---------------|------------:|-----------------|-----------------|----------------|----------------|--------------------------------------------|--------------------------------------------|----------|----------------------| -| local::at0005 | 1.5 | local::at0002 | 5.3 | false | false | true | true | 1.5|[local::at0005], 2.4|[local::at0006] | 5.3|[local::at0002], 2.4|[local::at0006] | accepted | | -| local::at0004 | 666.1 | local::at0003 | 777.1 | false | false | true | true | 8.9|[local::at0004], 2.4|[local::at0006] | 9.7|[local::at0003], 2.4|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching value for lower and upper | -| local::at0666 | 1.5 | local::at0777 | 2.4 | false | false | true | true | 1.5|[local::at0005], 2.4|[local::at0006] | 1.5|[local::at0005], 2.4|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching symbol for lower and upper | -| local::at0004 | 666.1 | local::at0003 | 777.1 | false | false | true | true | 8.9|[local::at0004], 2.4|[local::at0006] | 777.1|[local::at0003], 2.4|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching value for lower | -| local::at0666 | 1.5 | local::at0777 | 2.4 | false | false | true | true | 1.5|[local::at0005], 2.4|[local::at0006] | 1.5|[local::at0005], 2.4|[local::at0777] | rejected | C_DV_ORDINAL.list: no matching symbol for lower | -| local::at0004 | 666.1 | local::at0003 | 777.1 | false | false | true | true | 666.1|[local::at0004], 2.4|[local::at0006] | 9.7|[local::at0003], 2.4|[local::at0006] | rejected | C_DV_ORDINAL.list: no matching value for upper | -| local::at0005 | 1.5 | local::at0777 | 5.3 | false | false | true | true | 1.5|[local::at0005], 2.4|[local::at0006] | 1.5|[local::at0005], 5.3|[local::at0999] | rejected | C_DV_ORDINAL.list: no matching symbol for upper | - - - - -## 3.15. quantity.DV_INTERVAL - -> NOTE: some modeling tools don't support representing DV_INTERVAL. - -### 3.15.1. Test case DV_INTERVAL open constraint - -The test data sets for lower and upper are divided into multiple tables because there are many attributes in the DV_PROPORTION. - -#### 3.15.1.a. Data set both valid ratios - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 20 | 500 | 0 | - -| expected | constraints violated | -|----------|----------------------------------| -| accepted | | - -#### 3.15.1.b. Data set different limit types - -This data set fails beacause DV_INTERVAL.Limits_consistent need both lower and upper to have the same `type`. - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | unitary | 10 | 1 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -| expected | constraints violated | -|----------|-------------------------------------------| -| rejected | DV_INTERVAL.Limits_consistent (invariant) | - -#### 3.15.1.c. Data set greater lower - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 5 | 500 | 0 | - -| expected | constraints violated | -|----------|-------------------------------------------| -| rejected | DV_INTERVAL.Limits_consistent (invariant) | - - - -### 3.15.2. Test case DV_INTERVAL ratios - -The constraint is on the `type` of each limit of the interval as a C_INTEGER.list = [0], constraining the type as a ratio. - -#### 3.15.2.a. Data set valid ratios - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 20 | 500 | 0 | - -| expected | constraints violated | -|----------|----------------------------------| -| accepted | | - -#### 3.15.2.b. Data set no ratios - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 1 | unitary | 10 | 1 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 1 | unitary | 20 | 1 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| rejected | C_INTEGER.list for lower and upper | - - - -### 3.15.3. Test case DV_INTERVAL unitaries - -The constraint is on the `type` of each limit of the interval as a C_INTEGER.list = [1], constraining the type as unitary. - -#### 3.15.3.a. Data set valid unitaries - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 1 | unitary | 10 | 1 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 1 | unitary | 20 | 1 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| accepted | | - -#### 3.15.3.b. Data set no unitaries - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 20 | 500 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| rejected | C_INTEGER.list for lower and upper | - - - -### 3.15.4. Test case DV_INTERVAL percentages - -The constraint is on the `type` of each limit of the interval as a C_INTEGER.list = [2], constraining the type as percentage. - -#### 3.15.4.a. Data set valid percentages - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 2 | percent | 10 | 100 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 2 | percent | 20 | 100 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| accepted | | - -#### 3.15.4.b. Data set no percentages - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 20 | 500 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| rejected | C_INTEGER.list for lower and upper | - - - -### 3.15.5. Test case DV_INTERVAL fractions - -The constraint is on the `type` of each limit of the interval as a C_INTEGER.list = [3], constraining the type as fraction. - -#### 3.15.5.a. Data set valid fractions - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 3 | fraction | 3 | 4 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 3 | fraction | 5 | 4 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| accepted | | - -#### 3.15.5.b. Data set no fractions - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 20 | 500 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| rejected | C_INTEGER.list for lower and upper | - - - -### 3.15.6. Test case DV_INTERVAL integer fractions - -The constraint is on the `type` of each limit of the interval as a C_INTEGER.list = [3], constraining the type as fraction. - -#### 3.15.6.a. Data set valid integer fractions - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 4 | integer fraction | 3 | 4 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 4 | integer fraction | 5 | 4 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| accepted | | - -#### 3.15.6.b. Data set no integer fractions - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 10 | 500 | 0 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | -|:----:|------------------|-----------|-------------|-----------| -| 0 | ratio | 20 | 500 | 0 | - -| expected | constraints violated | -|----------|------------------------------------| -| rejected | C_INTEGER.list for lower and upper | - - - -### 3.15.7. Test case DV_INTERVAL ratios with range limits - -The constraint is on the `type` of each limit of the interval as a C_INTEGER.list = [0], constraining the type as a ratio. For the limits, the constraints are C_REAL using the range attribute. - -#### 3.15.7.a. Data set valid ratios - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | C_REAL.range (num) | C_REAL.range (den) | -|:----:|------------------|-----------|-------------|-----------|--------------------|--------------------| -| 0 | ratio | 10 | 500 | 0 | 0..15 | 100..1000 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | C_REAL.range (num) | C_REAL.range (den) | -|:----:|------------------|-----------|-------------|-----------|--------------------|--------------------| -| 0 | ratio | 20 | 500 | 0 | 0..50 | 100..1000 | - -| expected | constraints violated | -|----------|----------------------------------| -| accepted | | - - -#### 3.15.7.b. Data set ratios, invalid lower - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | C_REAL.range (num) | C_REAL.range (den) | -|:----:|------------------|-----------|-------------|-----------|--------------------|--------------------| -| 0 | ratio | 10 | 500 | 0 | 0..5 | 100..1000 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | C_REAL.range (num) | C_REAL.range (den) | -|:----:|------------------|-----------|-------------|-----------|--------------------|--------------------| -| 0 | ratio | 20 | 500 | 0 | 0..50 | 100..1000 | - -| expected | constraints violated | -|----------|----------------------------------| -| rejected | C_REAL.range (num) for lower | - - -#### 3.15.7.c. Data set ratios, invalid upper - -DV_INTERVAL.lower - -| type | meaning (kind) | numerator | denominator | precision | C_REAL.range (num) | C_REAL.range (den) | -|:----:|------------------|-----------|-------------|-----------|--------------------|--------------------| -| 0 | ratio | 10 | 500 | 0 | 0..15 | 100..1000 | - -DV_INTERVAL.upper - -| type | meaning (kind) | numerator | denominator | precision | C_REAL.range (num) | C_REAL.range (den) | -|:----:|------------------|-----------|-------------|-----------|--------------------|--------------------| -| 0 | ratio | 20 | 500 | 0 | 0..10 | 100..1000 | - -| expected | constraints violated | -|----------|----------------------------------| -| rejected | C_REAL.range (num) for upper | - - -# 4. quantity.date_time - -## 4.1. Reference UML - -![](https://specifications.openehr.org/releases/RM/Release-1.1.0/UML/diagrams/RM-data_types.quantity.date_time.svg) - - -## 4.2. quantity.date_time.DV_DURATION - -> NOTE: different duration implementations might affect the DV_DURATION related test cases. For instance, some implementations might not support `days` in the same duration -> expression that contains `months`, since there is no exact correspondence between the number of `days` and `months` (months could have 28, 29, 30 or 31 days). Then other -> implementations might simplify the `month` measurement to be 30 days. This also happens with some implementations that consider a `day` is exactly `24 hours` as a simplification. -> It is worth mentioning that openEHR provides means for calculating this based on averages, in DV_DURATION.magnitude(), which is implemented in terms of Iso8601_duration.to_seconds(), -> it uses `Time_Definitions.Average_days_in_year` as an approximation to the numbers of days in a year, and `Time_Definitions.Average_days_in_month` as an approximation to the numbers of -> days in a month. So to normalize an expression that is P1Y3M5D to `days` we would have `1 * Average_days_in_year + 3 * Average_days_in_month + 5`. -> In case the SUT has an implementation decision to be considered, the developers should mention it in the Conformance Statement Document. - -The openEHR specifications have two exceptions to the ISO 8601-1 rules: - -1. a negative sign may be used before a Duration expression, for example `-P10D`, meaning '10 days before [origin]', where the 'origin' is a timepoint understood as the origin for the duration; -2. the `W` designator may be mixed with other designators in the duration expression. - -Note those exceptions are invalid in terms of ISO 8601-1_2019, but, those are valid in terms for ISO 8601-2_2019, which defines some extensions to the ISO 8601-1 standard. From ISO 8601-2: - -> Expressions in the following four examples below are not valid in ISO 8601-1, but are valid as specified in this clause. -> -> EXAMPLE 3 'P3W2D', duration of three weeks and two days, which is 23 days (equivalent to the expression 'P23D'). In ISO 8601-1, ["W"] is not permitted to occur along with any other component. -> -> EXAMPLE 4 'P5Y10W', duration of five years and ten weeks. -> ... -> EXAMPLE 7 '-P2M1D' is equivalent to 'P-2M-1D'. - - -### 4.2.1. Test case DV_DURATION open constraint - -| value | expected | violated constraints | -|--------------------|----------|--------------------------------------------------------------| -| NULL | rejected | DV_DURATION.value is mandatory in the RM | -| 1Y | rejected | invalid ISO 8601-1 duration: missing duration desingator 'P' | -| P1Y | accepted | | -| P1Y3M | accepted | | -| P1W | accepted | | -| P1Y3M4D | accepted | | -| P1Y3M4DT2H | accepted | | -| P1Y3M4DT2H14M | accepted | | -| P1Y3M4DT2H14M5S | accepted | | -| P1Y3M4DT2H14M15.5S | accepted | | -| P1Y3M4DT2H14.5M | rejected | openEHR: fractions for minutes are not allowed | -| P1Y3M4DT2.5H | rejected | openEHR: fractions for hours are not allowed | -| P3M1W | accepted | | -| -P2M | accepted | | - - -### 4.2.2. Test case DV_DURATION xxx_allowed field constraints - -The `xxx_allowed` fields are defined in the `C_DURATION` class, which allows to constraint the `DV_DURATION.value` attribute. - -| value | years_allowed | months_allowed | weeks_allowed | days_allowed | hours_allowed | minutes_allowed | seconds_allowed | fractional_seconds_allowed | expected | violated constraints | -|----------------------|---------------|----------------|---------------|--------------|---------------|-----------------|-----------------|----------------------------|----------|--------------------------| -| P1Y | true | true | true | true | true | true | true | true | accepted | | -| P1Y | false | true | true | true | true | true | true | true | rejected | C_DURATION.years_allowed | -| P1Y3M | true | true | true | true | true | true | true | true | accepted | | -| P1Y3M | true | false | true | true | true | true | true | true | rejected | C_DURATION.months_allowed | -| P1Y3M15D | true | true | true | true | true | true | true | true | accepted | | -| P1Y3M15D | true | true | true | false | true | true | true | true | rejected | C_DURATION.days_allowed | -| P1W | true | true | true | true | true | true | true | true | accepted | | -| P7W | true | true | false | true | true | true | true | true | rejected | C_DURATION.weeks_allowed | -| P1Y3M15DT23H | true | true | true | true | true | true | true | true | accepted | | -| P1Y3M15DT23H | true | true | true | true | false | true | true | true | rejected | C_DURATION.hours_allowed | -| P1Y3M15DT23H35M | true | true | true | true | true | true | true | true | accepted | | -| P1Y3M15DT23H35M | true | true | true | true | true | false | true | true | rejected | C_DURATION.minutes_allowed | -| P1Y3M15DT23H35M22S | true | true | true | true | true | true | true | true | accepted | | -| P1Y3M15DT23H35M22S | true | true | true | true | true | true | false | true | rejected | C_DURATION.seconds_allowed | -| P1Y3M15DT23H35M22.5S | true | true | true | true | true | true | true | true | accepted | | -| P1Y3M15DT23H35M22.5S | true | true | true | true | true | true | true | false | rejected | C_DURATION.fractional_seconds_allowed | -| P1W3D | true | true | true | true | true | true | true | true | accepted | | -| P1W3D | true | true | false | true | true | true | true | true | rejected | C_DURATION.weeks_allowed | - - -### 4.2.3. Test case DV_DURATION range constraint - -In order to compare durations, the DV_DURATION.magnitude() should be used, which will calculate the seconds in the duration based on the avg. days in year and days in month. If the SUT does calculate the `magnitude()` in a different way, it should be stated in the Conformance Statement Document. - -| value | range.lower | range.upper | expected | violated constraints | -|-------------------|----------------|----------------|----------|------------------------| -| P1Y | P0Y | P50Y | accepted | | -| P1Y | P1Y | P50Y | accepted | | -| P1Y | P2Y | P50Y | rejected | C_DURATION.range.lower | -| P1M | P0M | P50M | accepted | | -| P1M | P1M | P50M | accepted | | -| P1M | P2M | P50M | rejected | C_DURATION.range.lower | -| P1D | P0D | P50D | accepted | | -| P1D | P1D | P50D | accepted | | -| P1D | P2D | P50D | rejected | C_DURATION.range.lower | -| P1Y2M | P0Y | P50Y | accepted | | -| P1Y2M | P1Y | P50Y | accepted | | -| P1Y2M | P2Y | P50Y | rejected | C_DURATION.range.lower | -| P1Y20M | P0Y | P50Y | accepted | | -| P1Y20M | P1Y | P50Y | accepted | | -| P1Y20M | P2Y | P50Y | accepted | | -| P2W | P0W | P3W | accepted | | -| P2W | P2W | P3W | accepted | | -| P2W | P3W | P3W | rejected | C_DURATION.range.lower | -| P2W3D | P3W | P4W | rejected | C_DURATION.range.lower | -| P2W8D | P3W | P4W | accepted | | -| P2W15D | P3W | P4W | rejected | C_DURATION.range.upper | - - -### 4.2.4. Test case DV_DURATION fields allowed and range constraints combined - -In the AOM specification it is allowed to combine allowed and range: "Both range and the constraint pattern can be set at the same time, corresponding to the ADL constraint PWD/|P0W..P50W|. (https://specifications.openehr.org/releases/AM/Release-2.2.0/AOM1.4.html#_c_duration_class)" - -| value | years_allowed | months_allowed | weeks_allowed | days_allowed | hours_allowed | minutes_allowed | seconds_allowed | fractional_seconds_allowed | range.lower | range.upper | expected | violated constraints | -|-------------------|---------------|----------------|---------------|--------------|---------------|-----------------|-----------------|----------------------------|-------------|-------------|----------|--------------------------------------------------| -| P1Y | true | true | true | true | true | true | true | true | P0Y | P50Y | accepted | | -| P1Y | true | true | true | true | true | true | true | true | P2Y | P50Y | rejected | C_DURATION.range.lower | -| P1Y | false | true | true | true | true | true | true | true | P0Y | P50Y | rejected | C_DURATION.years_allowed | -| P1Y | false | true | true | true | true | true | true | true | P2Y | P50Y | rejected | C_DURATION.years_allowed, C_DURATION.range.lower | -| P1Y3M | true | true | true | true | true | true | true | true | P1Y | P50Y | accepted | | -| P1Y3M | true | false | true | true | true | true | true | true | P1Y | P50Y | rejected | C_DURATION.months_allowed | -| P1Y3M | true | true | true | true | true | true | true | true | P3Y | P50Y | rejected | C_DURATION.lower | -| P1Y3M | true | false | true | true | true | true | true | true | P3Y | P50Y | rejected | C_DURATION.months_allowed. C_DURATION.lower | -| PT2M43.5S | true | true | true | true | true | true | true | false | PT1M | PT60M | rejected | C_DURATION.fractional_seconds_allowed | - - - -## 4.3. quantity.date_time.DV_TIME - -DV_TIME constraints are defined by C_TIME, which specifies two types of constraints: validity kind and range. The validity kind constraints are expressed in terms of mandatory/optional/prohibited flags for each part of the time expression: minute, second, millisecond and timezone. The range constraint is an Interval