diff --git a/.circleci/bump_version.sh b/.circleci/bump_version.sh deleted file mode 100644 index 54f1b37992..0000000000 --- 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 0ddb2f7fa5..0000000000 --- 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 fc96bfb42a..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,2528 +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: - - build-ehrbase: - context: org-global - filters: - branches: - ignore: - - /release\/.*/ - - master - - /sync\/.*/ - - /feature/sync\/.*/ - - - run-SDK-integration-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 - - - 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 - - - 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 - - - SECURITY-test: - context: org-global - requires: - - build-ehrbase - - QUERYSERVICE-test-1 - - - ADMIN-test: - context: org-global - requires: - - build-ehrbase - - - ROBOT-TEST-REPORT: - context: org-global - requires: - - 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 - - CONTRIBUTION-test - - DIRECTORY-test - - EHRSERVICE-test - - EHRSTATUS-test - - KNOWLEDGE-test - - QUERYSERVICE-test-1 - - QUERYSERVICE-test-2 - - SECURITY-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 - - - 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 - - - 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 - - - QUERYSERVICE-test-1: - context: org-global - requires: - - build-ehrbase - - - QUERYSERVICE-test-2: - context: org-global - requires: - - build-ehrbase - - - SECURITY-test: - context: org-global - requires: - - build-ehrbase - - - ADMIN-test: - context: org-global - requires: - - build-ehrbase - - - ROBOT-TEST-REPORT: - context: org-global - requires: - - 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 - - CONTRIBUTION-test - - DIRECTORY-test - - EHRSERVICE-test - - EHRSTATUS-test - - KNOWLEDGE-test - - QUERYSERVICE-test-1 - - QUERYSERVICE-test-2 - - SECURITY-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: - - "pull build install sdk from sync-branch" - - "build package ehrbase with locally installed sdk" - - "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: - - pull build install sdk from sync-branch: - filters: - branches: - only: - - /^sync\/.*/ - - /^feature\/sync\/.*/ - - build package ehrbase with locally installed sdk: - requires: - - pull build install sdk from sync-branch - - run java integration tests - sdk: - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-1: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-2: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-3: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-4: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-5: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-6: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-7: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-8: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-9: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-10: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-11: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-12: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-13: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - COMPOSITION-tests-14: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - CONTRIBUTION-test: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - DIRECTORY-test: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - EHRSERVICE-test: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - EHRSTATUS-test: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - KNOWLEDGE-test: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - QUERYSERVICE-test-1: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - QUERYSERVICE-test-2: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - SECURITY-test: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - ADMIN-test: - context: org-global - requires: - - build package ehrbase with locally installed sdk - - ROBOT-TEST-REPORT: - context: org-global - requires: - - 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 - - CONTRIBUTION-test - - DIRECTORY-test - - EHRSERVICE-test - - EHRSTATUS-test - - KNOWLEDGE-test - - QUERYSERVICE-test-1 - - QUERYSERVICE-test-2 - - SECURITY-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" - - - pull build install sdk from sync-branch: - # executor: machine-ubuntu-2004 - executor: docker-python3-java11 - steps: - - checkout - - 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 - - save-local-sdk-installation - - collect-sdk-unittest-results - - save-skd-test-results - - - build package ehrbase with locally installed sdk: - # executor: machine-ubuntu-2004 - executor: docker-py3-java11-postgres - steps: - - checkout - - install-maven - - restore-local-sdk-installation - - 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-java11-postgres - steps: - - checkout - - install-maven - - restore-local-sdk-installation - - restore-packaged-ehrbase-jar - - cache-out-ehrbase-m2-dependencies-syncbranch - - start-ehrbase-and-run-java-integration-sdk-tests - - collect-sdk-integrationtest-results - - save-skd-test-results - - - build-ehrbase: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - 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 - # - 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-java11-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-skd-test-results - - - COMPOSITION-tests-1: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "compositionANDcomposition_create" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_1" - - COMPOSITION-tests-2: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - run: echo MOCKED JOB 3 - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "compositionANDcomposition_get" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_2" - - COMPOSITION-tests-3: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "compositionANDcomposition_get_versioned" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_3" - - COMPOSITION-tests-4: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "compositionANDcomposition_update" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_4" - - COMPOSITION-tests-5: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "compositionANDcomposition_delete" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_5" - - COMPOSITION-tests-6: - executor: docker-py3-java11-postgres-ci-timezone-berlin-postgres-timezone-berlin - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-berlin-postgres-timezone-utc - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-berlin-postgres-timezone-shanghai - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-utc-postgres-timezone-berlin - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-utc-postgres-timezone-utc - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-utc-postgres-timezone-shanghai - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-shanghai-postgres-timezone-berlin - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-shanghai-postgres-timezone-utc - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - 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-java11-postgres-ci-timezone-shanghai-postgres-timezone-shanghai - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "compositionANDcomposition_dtz" - test-suite-path: "COMPOSITION_TESTS" - test-suite-name: "COMPOSITION_CI_TIMEZONE_SHANGHAI_POSTGRES_TIMEZONE_SHANGHAI" - - CONTRIBUTION-test: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "CONTRIBUTION" - test-suite-path: "CONTRIBUTION_TESTS" - test-suite-name: "CONTRIBUTION" - - DIRECTORY-test: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "directory" - test-suite-path: "DIRECTORY_TESTS" - test-suite-name: "DIRECTORY" - - EHRSERVICE-test: - executor: docker-py3-java11-postgres - # executor: machine-ubuntu-2004 - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "EHR_SERVICE" - test-suite-path: "EHR_SERVICE_TESTS" - test-suite-name: "EHR_SERVICE" - - EHRSTATUS-test: - executor: docker-py3-java11-postgres - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "EHR_STATUS" - test-suite-path: "EHR_STATUS_TESTS" - test-suite-name: "EHR_STATUS" - - KNOWLEDGE-test: - executor: docker-py3-java11-postgres - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "OPT" - test-suite-path: "KNOWLEDGE_TESTS" - test-suite-name: "KNOWLEDGE" - allow-template-overwrite: false - cache-enabled: false - - QUERYSERVICE-test-1: - executor: docker-py3-java11-postgres - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - include-tags: "aql_adhoc-queryANDaql_empty_db" - test-suite-path: "QUERY_SERVICE_TESTS" - test-suite-name: "ADHOC-QUERY-1" - - - QUERYSERVICE-test-2: - # executor: docker-py3-java11-postgres - executor: machine-ubuntu-2004 - environment: - SUT: TEST - steps: - - checkout - - restore-packaged-ehrbase-jar - # - cache-out-ehrbase-workspace # USE FOR PIPELINE DEBUGGING ONLY - - restore_cache: - keys: - - expected-results-loaded-db-v8 - - 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-v8 - # 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-v8-{{ 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 - environment: - SUT: TEST - SECURITY_AUTHTYPE: OAUTH - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI: "http://localhost:8081/auth/realms/ehrbase" - steps: - - checkout - - install-and-configure-keycloak - - restore-packaged-ehrbase-jar - - run-robot-tests: - sut: "TEST -v AUTH_TYPE:OAUTH" - include-tags: "SECURITY_oauth" - # test-suite-path: "" - test-suite-name: "SECURITY" - - ADMIN-test: - # executor: docker-py3-java11-postgres - executor: machine-ubuntu-2004 - environment: - SUT: TEST - ADMINAPI_ACTIVE: "TRUE" - SYSTEM_ALLOWTEMPLATEOVERWRITE: "TRUE" - steps: - - checkout - - restore-packaged-ehrbase-jar - - run-robot-tests: - sut: "ADMIN-TEST" - include-tags: "ADMIN" - test-suite-path: "ADMIN_TESTS" - test-suite-name: "ADMIN" - - - ROBOT-TEST-REPORT: - executor: docker-python3-java11 - steps: - - restore-test-results-folder - - merge-robot-outputs - - sonar-analysis: - executor: docker-python3-java11 - steps: - - checkout - - 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-maven - - install-python3-requirements - # - 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=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) - echo ${EHRbase_VERSION} - java -jar "application/target/application-${EHRbase_VERSION}.jar" \ - --system.allow-template-overwrite=<< parameters.allow-template-overwrite >> \ - --server.nodename=<< parameters.nodename >> \ - --cache.enabled=<< parameters.cache-enabled >> > log & app_pid=$! - timeout=60 - 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 ..."; - 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: Titel of generated Robot Outputs - type: string - steps: - - run: - name: PERSIST ROBOT TEST RESULTS - when: always - command: echo "persist test results" - - 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: | - cd tests - pip install -r 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/ - - - - - - # /////////////////////////////////////////////////////////////////////////// - # /// 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-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: - - 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) versions:commit - SDK_VERSION=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) - 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 -Dmaven.test.skip - - - 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: - - run: - name: Start EHRbase server and run SDK's java integration tests - command: | - ls -la - cd ~/projects - EHRbase_VERSION=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) - echo ${EHRbase_VERSION} - java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true > 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-maven - - run: - name: Start EHRbase server and run all test of SDK - command: | - ls -la - EHRbase_VERSION=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) - echo ${EHRbase_VERSION} - cd ~/projects # NOTE: This is where the target folder w/ artifacts were persisted to in previous step. - 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 > 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-skd-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 pom.xml - - run: - name: Show EHRbase's pom - command: cat pom.xml - - - build-and-test-ehrbase: - steps: - # - install-maven - - 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-maven - - run: - name: Start EHRbase server and wait for it to be ready - background: true - command: | - ls -la - EHRbase_VERSION=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) - echo ${EHRbase_VERSION} - java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true > 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-packaged-ehrbase-jar: - steps: - - attach_workspace: - at: /home/circleci/ - - - 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" -exec cp {} ~/ehrbase-test-results/unit-tests/ \; - find ~/projects -type f -regex ".*/target/surefire-reports/.*txt" -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-java-11: - description: Install Zulu Java 11 - steps: - - run: - name: Install Zulu Java 11 - command: | - wget https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.3%2B7/OpenJDK11U-jdk_x64_linux_hotspot_11.0.3_7.tar.gz -O /tmp/openjdk-11.tar.gz - sudo mkdir -p /usr/lib/jvm - sudo tar xfvz /tmp/openjdk-11.tar.gz --directory /usr/lib/jvm - rm -f /tmp/openjdk-11.tar.gz - sudo sh -c 'for bin in /usr/lib/jvm/jdk-11.0.3+7/bin/*; do update-alternatives --install /usr/bin/$(basename $bin) $(basename $bin) $bin 100; done' - sudo sh -c 'for bin in /usr/lib/jvm/jdk-11.0.3+7/bin/*; do update-alternatives --set $(basename $bin) $bin; done' - - install-maven: - description: Install Maven tool only if it's not already installed - steps: - - run: - name: Install Maven tool - command: | - sudo killall -9 apt-get || true - sudo apt -y update - [ -f /usr/bin/mvn ] && echo "Maven is already installed." || sudo apt install maven -y - - 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: | - 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 - - - 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.6/jacoco-0.8.6.zip - mkdir -p ~/jacoco - unzip -uo jacoco-0.8.6.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 # -DskipTests - - - - - - - - # 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@1.0.2 - -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-java11: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - - docker-py3-java11-postgres: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - - docker-py3-java11-postgres-ci-timezone-berlin-postgres-timezone-berlin: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Europe/Berlin" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Europe/Berlin" - - docker-py3-java11-postgres-ci-timezone-berlin-postgres-timezone-utc: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Europe/Berlin" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "UTC" - - docker-py3-java11-postgres-ci-timezone-berlin-postgres-timezone-shanghai: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Europe/Berlin" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Asia/Shanghai" - - docker-py3-java11-postgres-ci-timezone-utc-postgres-timezone-berlin: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "UTC" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Europe/Berlin" - - docker-py3-java11-postgres-ci-timezone-utc-postgres-timezone-utc: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "UTC" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "UTC" - - docker-py3-java11-postgres-ci-timezone-utc-postgres-timezone-shanghai: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "UTC" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Asia/Shanghai" - - docker-py3-java11-postgres-ci-timezone-shanghai-postgres-timezone-berlin: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Asia/Shanghai" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Europe/Berlin" - - docker-py3-java11-postgres-ci-timezone-shanghai-postgres-timezone-utc: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Asia/Shanghai" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "UTC" - - docker-py3-java11-postgres-ci-timezone-shanghai-postgres-timezone-shanghai: - working_directory: ~/projects - docker: - # - image: circleci/python@sha256:e1c98a85c5ee62ac52a2779fe5abe2677f021c8e3158e4fb2d569c7b9c6ac073 - # - image: circleci/python:3.8.5-buster-node-browsers - - image: circleci/python:3.8.5-node-browsers - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - TZ: "Asia/Shanghai" - - image: ehrbase/ehrbase-postgres:13.4 - auth: - username: $DOCKER_USER - password: $DOCKER_HUB_PASSWORD - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /tmp - TZ: "Asia/Shanghai" - - 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 - - - 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 ] - -# 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 032a519456..0000000000 --- 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 2b31a5df0a..0000000000 --- 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 975edfb419..0000000000 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 8f2716e0e0..0000000000 --- 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 23089527f8..0000000000 --- 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 b5848a7821..0000000000 --- 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 bdb485457a..0000000000 --- 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 a4e35bbaf7..0000000000 --- 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 8c01380495..5838513204 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 ad6f8fa196..5b2891a426 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/.gitattributes b/.gitattributes index bad0bb9921..dde6693ab4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -42,6 +42,7 @@ gradlew text eol=lf *.scm text *.scss text *.sh text eol=lf +Dockerfile text eol=lf *.sql text *.styl text *.tag text diff --git a/.github/ISSUE_TEMPLATE/defect(robot).md b/.github/ISSUE_TEMPLATE/defect(robot).md deleted file mode 100644 index 29e9d17a69..0000000000 --- a/.github/ISSUE_TEMPLATE/defect(robot).md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Defect(Robot) -about: Report issues found by Robot tests -labels: bug ---- - - -## Test Case/s To Reproduce Issue - -``` -# path to test case -tests/robot/CONTRIBUTION_TESTS/... -``` - -``` -# robot command to execute related test case(s) in your terminal/console - -# by test case name (wildcards possible) -robot -t "*Bug Case 01*" -d results -L TRACE robot/TEST_SUITE FOLDER - -# by tag -robot -i failing -d results -L TRACE robot/TEST_SUITE_FOLDER - -# Valid test suite folder names -COMPOSITION_TESTS -CONTRIBUTION_TESTS -DIRECTORY_TESTS -EHR_SERVICE_TESTS -EHR_STATUS_TESTS -KNOWLEDGE_TESTS -QUERY_SERVICE_TESTS -``` - -## Actual Result - - - - -## Expected Result - - diff --git a/.github/ISSUE_TEMPLATE/defect.md b/.github/ISSUE_TEMPLATE/defect.md deleted file mode 100644 index da2f4187e6..0000000000 --- a/.github/ISSUE_TEMPLATE/defect.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Bug -about: Something is not working as expected and must be fixed -labels: bug ---- - -## Configuration information - - - -- EHRbase version: -- openEHR_SDK version: -- Archie version: -- PostgreSQL version: -- Java Runtime version: -- Operating System version: - -## Steps to reproduce ## - - - -## Actual result ## - - - -## Expected result (Acceptance Criteria) ## - - - -## Definition of Done ## - - - -- [ ] The defect is checked by an unit or an integration test (Robot) -- [ ] Merge Request approved -- [ ] Unit tests passed -- [ ] Build without errors -- [ ] Release notes prepared -- [ ] No additional runtime warnings diff --git a/.github/ISSUE_TEMPLATE/defect.yml b/.github/ISSUE_TEMPLATE/defect.yml new file mode 100644 index 0000000000..dda323d047 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/defect.yml @@ -0,0 +1,50 @@ +name: Bug Report +description: Report a bug encountered when working with EHRbase +labels: ["bug"] +body: + - type: checkboxes + attributes: + label: Before reporting an issue + description: Please search to see if the issue is already reported, and try to reproduce the issue on the latest release. + options: + - label: I have searched existing issues + required: true + - label: I have reproduced the issue with the latest release + required: true + - type: textarea + attributes: + label: Environment information + description: To help reproducing your problem it is mandatory to give some information on the environment. You can get an aggregate of this data by starting EHRbase and performing a GET request on `%ehrbaseBaseUri%/ehrbase/rest/status` + placeholder: | + - EHRbase version: + - openEHR_SDK version: + - Archie version: + - PostgreSQL version: + - Java Runtime version: + - Operating System version: + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: Describe the steps that you have taken until the unexpected behavior occurred. Please try to add as many details as possible, and include data and templates as attachments. + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: Describe the expected output / behavior. + validations: + required: true + - type: textarea + attributes: + label: Actual result + description: Describe the wrong output / behavior. + validations: + required: true + - type: textarea + attributes: + label: Further information + description: Add additional information, if needed. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 0000000000..d88c6bc3c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,28 @@ +name: Enhancement Request +description: Enhancement request for a new / extended functionality of EHRbase +labels: ["enhancement"] +body: + - type: textarea + attributes: + label: Background + description: Describe the context and intention for this enhancement, such as the benefits and impacts on the user. + validations: + required: true + - type: textarea + attributes: + label: Enhancement + description: Provide specific preconditions, steps, requirements or expectations on theproposed enhancement. + validations: + required: true + - type: input + attributes: + label: Discussion + description: | + If there has been a discussion around the enhancement, provide a link to the discussion. + Please also check for ongoing discussions on the [openEHR discourse](https://discourse.openehr.org/tag/ehrbase). + - type: textarea + attributes: + label: Further information + description: Add additional information, if needed. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-story.md b/.github/ISSUE_TEMPLATE/feature-story.md deleted file mode 100644 index 5cacd56f27..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-story.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Feature Story -about: A new feature to implement that enhances the functionality of EHRbase -labels: story, enhancement ---- - -## Background ## - - - -## Acceptance criteria ## - - - -- [ ] Item 1 - -## Definition of Done ## - -- [ ] Review / Merge request approved (P2P session) -- [ ] Unit tests passed -- [ ] Updated documentation (Javadoc and Sphinx) -- [ ] Acceptance criteria fulfilled -- [ ] Build without errors -- [ ] Release notes prepared -- [ ] Runtime warnings diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md deleted file mode 100644 index be6f7e7e76..0000000000 --- a/.github/ISSUE_TEMPLATE/task.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Task -about: A general task that has to be done. Can be a research or other common work to be done. -labels: task ---- -## Background ## - - - -## Acceptance Criteria ## - - - -- [ ] Item1 - -## Definition of Done ## - -- [ ] Output has been reviewed or presented and has been approved -- [ ] Documentation has been updated (Sphinx, Javadoc, Guidelines, etc.) if applicable diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000000..96f862bb6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,28 @@ +name: Task Request +description: A general task, e.g. research or other common work to be done +labels: ["task"] +body: + - type: textarea + attributes: + label: Background + description: Describe the goal of this task, and the expectations or impacts on EHRbase after this task has been completed. + validations: + required: true + - type: textarea + attributes: + label: Task specification + description: Add specific steps or work descriptions with the expected results. + validations: + required: true + - type: input + attributes: + label: Discussion + description: | + If there has been a discussion around the task, provide a link to the discussion. + Please also check for ongoing discussions on the [openEHR discourse](https://discourse.openehr.org/tag/ehrbase). + - type: textarea + attributes: + label: Further information + description: Add additional information, if needed. + validations: + required: false \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..93a9bbf202 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 604147bb88..4027f6d9b0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1,22 @@ -## Changes - +# Changes -- First change +> Describe your changes in a short and concise list. -## Related issue +# Related issue - +> Reference related issues, and use one of the [closing keywords, e.g. closes or fixes](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) to link the corresponding issue, if any. +# Additional information -## Additional information and checks +> Provide additional information for this change, if needed. - +# Pre-Merge checklist -- [ ] Pull request linked in changelog +- [ ] New code is tested +- [ ] Present and new tests pass +- [ ] Documentation is updated +- [ ] The build is working without errors +- [ ] No new Sonar issues introduced +- [ ] Changelog is updated +- [ ] Code has been reviewed \ No newline at end of file diff --git a/.github/workflows/build-multiarch-image-latest.yml b/.github/workflows/build-multiarch-image-latest.yml deleted file mode 100644 index d2b201afc8..0000000000 --- a/.github/workflows/build-multiarch-image-latest.yml +++ /dev/null @@ -1,83 +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@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push (AMD64) - uses: docker/build-push-action@v2 - with: - context: . - platforms: linux/amd64 - push: true - tags: ehrbase/ehrbase:latest-amd64 - - - name: Build and push (ARM64) - uses: docker/build-push-action@v2 - 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 - - - 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 6980f25de2..0000000000 --- a/.github/workflows/build-multiarch-image-next.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Build & Deploy Docker Image (next) - -on: - push: - branches: - - 'develop' - paths-ignore: - - '**/*.md' - - 'doc/**' - - 'tests/**' - -jobs: - build-docker: - runs-on: ubuntu-20.04 - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push (AMD64) - uses: docker/build-push-action@v2 - with: - context: . - platforms: linux/amd64 - push: true - tags: ehrbase/ehrbase:next-amd64 - - - name: Build and push (ARM64) - uses: docker/build-push-action@v2 - with: - context: . - platforms: linux/arm64 - push: true - tags: ehrbase/ehrbase:next-arm64 - - - - name: Create and push MultiArch Manifest - run: | - docker buildx imagetools create \ - ehrbase/ehrbase:next-arm64 \ - ehrbase/ehrbase:next-amd64 \ - -t ehrbase/ehrbase:next - docker pull ehrbase/ehrbase:next - - - name: Inspect MultiArch Manifest - run: docker manifest inspect ehrbase/ehrbase:next - - - - - -# 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 74c770bece..0000000000 --- a/.github/workflows/build-multiarch-image-tag.yml +++ /dev/null @@ -1,93 +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@v2 - - - 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@v1 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push (AMD64) - uses: docker/build-push-action@v2 - with: - context: . - platforms: linux/amd64 - push: true - tags: ehrbase/ehrbase:tag-amd64 - - - name: Build and push (ARM64) - uses: docker/build-push-action@v2 - 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 }} - - - 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 4f7667e0e0..0000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: build - -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 - ports: - - 5432:5432 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - EHRBASE_USER: ehrbase - EHRBASE_PASSWORD: ehrbase - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Setup Java - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: '11' - cache: 'maven' - - - name: Build with Maven - run: mvn -B verify - - - name: Setup Maven Central - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: '11' - 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 }} \ No newline at end of file diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000000..0b349864bf --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,644 @@ +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_EHRSCAPE_ENABLED=true" >> .env.ehrbase + 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 10 + done + fi + docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml ps + + - name: Restart jmeter if not started + run: | + docker compose exec jmeter ls -al /tests || docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml restart jmeter + + - 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 - Logs jmeter + if: always() + run: | + docker compose -f docker-compose.yml -f tests/docker-compose-perf-test.yml logs jmeter + #docker compose ps -a + + - 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: Coverage-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_SUMMARY: false + 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 0000000000..32a90c73c2 --- /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 0000000000..d3528b85a5 --- /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/collect-junit-results.yml b/.github/workflows/collect-junit-results.yml new file mode 100644 index 0000000000..bca25c0f59 --- /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 0000000000..24fe388d36 --- /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/job-docker-build-push.yml b/.github/workflows/job-docker-build-push.yml new file mode 100644 index 0000000000..2dca8df798 --- /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 0000000000..d6194f1f87 --- /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 0000000000..8ac288970c --- /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 new file mode 100644 index 0000000000..4143f72328 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,148 @@ +# Create a Release +name: "Release" + +on: + workflow_dispatch: + inputs: + version: + description: "Version to release, defaults to project.version" + required: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Setup - Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Checkout + uses: actions/checkout@v4 + with: + 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.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 + version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout | sed 's/-SNAPSHOT//') + else + version=${{ github.event.inputs.version }} + fi + echo "Release version: ${version}" + # Set as Environment for all further steps + 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: | + # create branch + git checkout -b release/v${VERSION} + # Update version + mvn versions:set -DnewVersion=${VERSION} -DprocessAllModules=true + # 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 + run: | + STATUS="pending" + # Get commit last commit of release branch + COMMIT=$(git rev-parse HEAD) + echo "Listen for commit $COMMIT" + WAITED="0" + # Time between calls + 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" + done + echo "status : $STATUS" + if [ "$STATUS" != "success" ] + then exit 1 + fi + env: + GITHUB_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }} + + # + # 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 push --follow-tags + + # + # 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 + env: + GITHUB_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }} + + - name: Prepare next dev version + run: | + # increment minor version and add SNAPSHOT + ARRAY_VERSION=( ${VERSION//./ } ) + git checkout release/v${VERSION} + NEXT_VERSION=${ARRAY_VERSION[0]}.$((ARRAY_VERSION[1]+1)).0-SNAPSHOT + echo "next version: $NEXT_VERSION" + # update version + mvn versions:set -DnewVersion=${NEXT_VERSION} -DprocessAllModules=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 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 0000000000..43bf1c2bd7 --- /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.5 + 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 0000000000..d055829624 --- /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 new file mode 100644 index 0000000000..7e9801812e --- /dev/null +++ b/.github/workflows/status.yml @@ -0,0 +1,26 @@ +# Adds the results of a workflow as a commit 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 & Test"] + types: + - completed + +jobs: + set_status: + runs-on: ubuntu-latest + permissions: + statuses: write + steps: + - name: Create status + run: | + gh api repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \ + -f "state"="${{ github.event.workflow_run.conclusion }}" \ + -f "context"="${{ github.event.workflow_run.event }}.${{ github.event.workflow_run.id}}"\ + -f "target_url"="${{ github.event.workflow_run.html_url }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 1f447fad74..ceb398528e 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 @@ -217,3 +204,5 @@ vulnerability_analysis.json # Docker .pgdata application/.pgdata +/plugin_dir/ +/plugin_config_dir/ diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000000..79ecf92923 --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c840c428..a40cf06c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,453 +2,143 @@ 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). +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). ## [unreleased] - -### Added - -### Changed - -### Fixed - -- Remove unused Operational Template cache ([#759](https://github.com/ehrbase/ehrbase/pull/759)). - -## [0.19.0] - -### 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] - -### Added - -### Changed - -- removed log4j (see https://github.com/ehrbase/ehrbase/pull/711) - -### Fixed - -## [0.18.2] - -### Fixed - -- updated log4j from 1.15.0 to 1.60.0 - -## [0.18.1] - -### Fixed - -- Fix deployment issue with Flyway migration V62__add_entry_history_missing_columns.sql - -## [0.18.0] - -### 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] - + ### Added + ### Changed + ### Fixed + +## [2.12.0] + ### Added + ### Changed +* Added check for duplicate version IDs during Folder creation as well as check for Folder uid and if-match header id during update ([#1410](https://github.com/ehrbase/ehrbase/pull/1410)) +* Remove EHR from AQL if not needed for the query ([#1448](https://github.com/ehrbase/ehrbase/pull/1448)) +* Update flyway to V11 to Gracefully exit in case installed flyway DB is newer than supported ([#1450](https://github.com/ehrbase/ehrbase/pull/1450)) + ### Fixed +* Allow Template overwrite with renamed property from `system.allow-template-overwrite` to `ehrbase.template.allow-overwrite` ([#1440](https://github.com/ehrbase/ehrbase/pull/1440)) + +## [2.11.0] + ### Added + ### Changed +* EHR directory: Restriction of FOLDER items to only contain local VERSIONED_COMPOSITION references ([#1433](https://github.com/ehrbase/ehrbase/pull/1433)) + ### Fixed + +## [2.10.0] + ### Added +* Revived option to pre-fill template cache (`ehrbase.cache.template-init-on-startup`, default: `false`) ([#1394](https://github.com/ehrbase/ehrbase/pull/1394)) +* Added experimental `AQL` support for `FOLDER` (`ehrbase.aql.experimental.aqlOnFolder`, default: `false`) ([#1401](https://github.com/ehrbase/ehrbase/pull/1401)) + ### Changed +* Feature toggle for ehrscape API (`ehrbase.rest.ehrscape.enabled`, default: `false`) ([#1415](https://github.com/ehrbase/ehrbase/pull/1415)) + ### Fixed +* Validate that compositions only contain nodes that are defined by the template (`ehrbase.validation.checkForExtraNodes`, default: `true`) ([#1424](https://github.com/ehrbase/ehrbase/pull/1424)) + +## [2.9.0] + ### Added + ### Changed + ### Fixed +* Improved transaction-awareness of caches ([#1407](https://github.com/ehrbase/ehrbase/pull/1407)) + +## [2.8.1] + ### Added + ### Changed + ### Fixed +* Fixed HTTP 500 in case stored query does not exist [#1409](https://github.com/ehrbase/ehrbase/pull/1409) + +## [2.8.0] + ### Added +* AQL: Support query parameter with multiple values ([1399](https://github.com/ehrbase/ehrbase/pull/1399)) + ### Changed +* Enable user access to the welcome page while authentication is enabled ([#1400](https://github.com/ehrbase/ehrbase/pull/1400)) + ### Fixed +* Stored AQL queries: Correctly clean up query cache when storing queries ([#1405](https://github.com/ehrbase/ehrbase/pull/1405)) +* Updating an `EHR_STATUS` or `FOLDER` did not check the `If-Match header` ([1398](https://github.com/ehrbase/ehrbase/pull/1398)) + +## [2.7.0] + ### Added +* 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 + ### Fixed +* Require EHR_STATUS `is_queryable` and `is_modifiable` to be present ([#1377](https://github.com/ehrbase/ehrbase/pull/1377)) + +## [2.6.0] + ### Added + ### Changed +* 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 + +## [2.5.0] + ### Added +* 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 + + ### 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)) + +## [2.4.0] + ### Added +- 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 2.13.0 see https://github.com/ehrbase/openEHR_SDK/blob/develop/CHANGELOG.md + ### Fixed + +## [2.3.0] + ### Added + ### Changed + - 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 - -- 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) - -### 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 - -[unreleased]: https://github.com/ehrbase/ehrbase/compare/v0.17.2...HEAD - -[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 +* Added AQL debug support ([#1296](https://github.com/ehrbase/ehrbase/pull/1296)) +### Changed + - 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 + +## [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 +[2.8.0]: https://github.com/ehrbase/ehrbase/compare/v2.7.0...v2.8.0 +[2.8.1]: https://github.com/ehrbase/ehrbase/compare/v2.8.0...v2.8.1 +[2.9.0]: https://github.com/ehrbase/ehrbase/compare/v2.8.1...v2.9.0 +[2.10.0]: https://github.com/ehrbase/ehrbase/compare/v2.9.0...v2.10.0 +[2.11.0]: https://github.com/ehrbase/ehrbase/compare/v2.10.0...v2.11.0 +[2.12.0]: https://github.com/ehrbase/ehrbase/compare/v2.11.0...v2.12.0 +[unreleased]: https://github.com/ehrbase/ehrbase/compare/v2.12.0...HEAD diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..198bd46bd3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[ehrbase-code-of-conduct@vitagroup.ag](mailto:ehrbase-code-of-conduct@vitagroup.ag). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CODING_CONVENTIONS.md b/CODING_CONVENTIONS.md new file mode 100644 index 0000000000..5e75719d0d --- /dev/null +++ b/CODING_CONVENTIONS.md @@ -0,0 +1,28 @@ +# EHRbase Coding Conventions + +## Best practices + +- Follow general best practices in software development as much as possible +- For PR readability and easing reviews, commit frequently, and break down large changes into logical series of easier understandable patches +- For Git history readability, squash and merge commits before pulling the change into the mainline +- Write [good commit messages](https://cbea.ms/git-commit/) + +## Code Conventions and Housekeeping + +### Documentation + +- On new files, add the license header, and at least minimal documentation +- Identify yourself as the author in the license copyright header +- If applicable, add appropiate Sphinx documentation + +### Code + +- format your code +- update and merge your local tree +- add tests for your change + +### Process + +- GitHub checks must pass for a PR to be accepted, meaning no new Sonar issues, and a sensible test coverage and duplication +- The change is reviewed before merging + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..825f7a487a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# EHRbase Contribution Guidelines + +Thank you for your interest in contributing to the openEHR ecosystem and EHRbase! + +EHRbase is release open source under the Apache 2.0 license, but many of the people working on it do so as their day job. The goal is to establish some semi-formal development processes and hopefully make those efforts and communication go more smoothly. Enhancement proposals are very welcome at this stage as well. + +## How to contribute + +All contributions are welcome at all times. There is no need to be a developer to actively contribute, and your first contribution is taking interest in EHRbase and openEHR and / or by using it. + +For general questions the probably best place to ask is the [official openEHR discourse](https://discourse.openehr.org/). + +For more technical questions, enhancements or problems with EHRbase itself, open an according issue and fill in the template, so that adequate steps can be taken to resolve your issue. + +### Contributing code + +If you wish to contribute code, there are a few more things to consider. + +If you want to implement a bigger enhancement, addition or change to EHRbase, consider opening an enhancement issue first, proposing and explaining your intended change, giving other contributors the option to discuss the need and impact of the idea. + +Otherwise, assuming regular code and software development best practices and the [coding conventions](CODING_CONVENTIONS.md), simply open a pull request, and also fill in the template and describe your change. One of the core contributors will review the change as soon as possible. + +Please be respectful in giving and receiving feedback and potentially criticism, and keep the [code of conduct](CODE_OF_CONDUCT.md) in mind. + +When your change has been merged, you've successfully contributed to EHRbase development! \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0feb265ab1..d8de0de045 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,138 +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/cloud-db-setup.sql /postgres/cloud-db-setup.sql -RUN su - postgres -c "pg_ctl -D ${PGDATA} -w start" && \ - su - postgres -c "psql < /postgres/cloud-db-setup.sql" && \ - su - postgres -c "pg_ctl -D ${PGDATA} -w stop" +WORKDIR /app -# INSTALL JAVA 11 JDK -RUN apk --no-cache add openjdk11 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community && \ - 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.6.3 -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 - -# CACHE EHRBASE DEPENDENCIES -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 -RUN mvn dependency:go-offline -B - -# 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 -RUN mvn compile dependency:go-offline \ - -Dflyway.skip=true \ - -Djooq.codegen.skip=true \ - -Dmaven.main.skip - -# START DB AND COMPILE EHRBASE -RUN su - postgres -c "pg_ctl -D ${PGDATA} -w start" && \ - mvn compile -Dmaven.test.skip && \ - su - postgres -c "pg_ctl -D ${PGDATA} -w stop" - -# 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 openjdk:11-jre-slim 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 0000000000..ddae8bd24e --- /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 new file mode 100755 index 0000000000..7a4a3ea242 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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/LICENSE.md b/LICENSE.md deleted file mode 100755 index 0e3db4f5dc..0000000000 --- a/LICENSE.md +++ /dev/null @@ -1,601 +0,0 @@ -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 . - ----- diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..7489c7b090 --- /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/Notice .md b/Notice .md deleted file mode 100644 index 55186e0721..0000000000 --- a/Notice .md +++ /dev/null @@ -1,10 +0,0 @@ -EHRbase openEHR Server -Copyright 2018-2019 Vitasystems GmbH and Hannover Medical School. - -This product includes software developed by the EHRbase team, -working for Vitasystems GmbH and Hannover Medical School. - -This software 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 as well. \ No newline at end of file diff --git a/README.md b/README.md index 64efd2d64e..3195bd06dc 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,77 @@ # EHRbase -EHRbase is an [openEHR](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.0.4) 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). +![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 Logo](ehrbase.png)](ehrbase.png) -
-

Release Notes

(click to expand)
+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). -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. - -##### WIP: 2021, XYZ 00 -v?.?.? - (...) +---- -**Important:** Please note that this release introduces [Archie's](https://github.com/openEHR/archie) new strict invariant checks. Depending on existing data and clients this might be a breaking change. Please carefully check the EHRbase output and update your input data if EHRbase rejects it. The strict validation can also be deactivated via configuration, but caution in advised! +## Release notes -##### 2021, Sep 9 -v0.17.2 - **beta** release. Bug fixes, enhancements, automatic Docker Hub deployments via Github Actions. +Please check the [CHANGELOG](https://github.com/ehrbase/ehrbase/blob/develop/CHANGELOG.md) -##### 2021, Aug 12 -v0.17.1 - **beta** release. Default handling for audit metadata, bug fixes and SDK version update. - -##### 2021, Aug 04 -v0.17.0 - fifth **beta** release. validation using an external terminology server, Attribute-based Access Control, AQL fixes and other enhancements - -##### 2021, March 30 -v0.16.0 - fourth **beta** release. New endpoints for versioned Compositions, ATNA Logging, AQL fixes and other enhancements. - -##### 2021, February 25 -v0.15.0 - third **beta** release. New admin API endpoints for EHRs, Compositions and Contributions. Fixes and other enhancements. - -##### 2020, October 1 -v0.14.0 - second **beta** release. - -##### 2020, May 14 -This release of EHRbase (v0.13.0) is the first **beta** release. - -
- -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. +## Documentation +Check out the documentation at https://docs.ehrbase.org +## Quick Start: Run EHRbase with Docker -## 📝 Documentation -[EHRbase Documentation](https://ehrbase.readthedocs.io/en/latest/) is build with Sphinx and hosted on [Read the Docs](https://readthedocs.org/). +> [!TIP] +> The fastest way to get started with EHRbase and openEHR is the **EHRbase Sandbox** available at https://sandkiste.ehrbase.org/. +> +> For a deployment on premise read below. -## 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. +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. + +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 -You will need Java JDK/JRE 11 (preferably openJDK: e.g. from https://adoptopenjdk.net/) - -You will need a Postgres Database (Docker image or local installation). We recommend the Docker image to get started quickly. - -When installing locally, the Postgres Database (at least Version 10.4) needs the following extensions: - * [temporal tables](https://github.com/arkhipov/temporal_tables) - ```bash - git clone https://github.com/arkhipov/temporal_tables.git - make - sudo make install - make installcheck - ``` - * [jsquery](https://github.com/postgrespro/jsquery) - ```bash - git clone https://github.com/postgrespro/jsquery.git - cd jsquery - make USE_PGXS=1 - sudo make USE_PGXS=1 install - make USE_PGXS=1 installcheck - ``` +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 `./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:latest + 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 @@ -117,7 +90,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 @@ -127,53 +100,89 @@ 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` claim of the JWT used for authentication. -## Running the tests +## Contributing -This command will run all tests from `tests/robot` folder. -DB and server application will be started/stopped by the tests accordingly. You *must not* start them by hand. +### Codestyle/Formatting -> NOTE: Make sure you meet the PREREQUISITES mentioned in tests/README.md prior to test execution. -> -> Please Check the README in `tests` folder for more details. +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. -```bash -cd tests -./run_local_tests.sh +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. + +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.. + +// @formatter:off + + This is not affected by spotless-plugin reformatting... + And will stay as is it is! + +// @formatter:on + +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 + +For integration tests please refer to the [integration-test](https://github.com/ehrbase/integration-tests) repository ## 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 +Before updating to a new version of EHRBase check [UPDATING.md](UPDATING.md) for any backwards-incompatible changes and additional +steps needed in EHRBase. New Releases may introduce DB changes. It is thus recommend to make a DB backup before +updating. ## Built With * [Maven](https://maven.apache.org/) - Dependency Management +---- +## Acknowledgments -## License - -EHRbase uses the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) +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. -## Acknowledgments +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 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 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/) -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](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/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..e4ae0ac5e7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Responsible disclosure of security issues + +## Report a vulnerability + +We're extremely grateful for users that identify and report vulnerabilities to the EHRbase community. All reports are thoroughly investigated, confirmed and patched as soon as possible. + +Given that EHRbase is used to handle sensitive, medical data, we would like to ask you to submit the vulnerability to [ehrbase-security@vitagroup.ag](mailto:ehrbase-security@vitagroup.ag), to allow triaging and handling of the vulnerability with standardized processes and response times. + +Reporting vulnerabilities according to [regular responsible disclosure policies](https://www.bugcrowd.com/resource/what-is-responsible-disclosure/) allows us to confirm and provide a patch based on your report before a full disclosure, preventing in-the-wild exploitation of the vulnerability. + +## When should I report a vulnerability? + +- You think you discovered a potential security vulnerability in EHRbase +- You are unsure how a vulnerability affects EHRbase + +## When should I **not** report a vulnerability? + +- You need support in securely deploying/operating EHRbase +- You need support for additional environment-dependent security measures +- You need support in security related updates +- Your issue is not related to security + +## Security vulnerability response + +Each report is acknowledged, analyzed and responded to by the team as soon as possible. + +We will notify you as soon as the issue is triaged and we identify a fix and a release date, and create a full disclosure after a patch is released. \ No newline at end of file diff --git a/UPDATING.md b/UPDATING.md index fea8e9f9ca..ed77ced282 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,14 +1,45 @@ # Updating EHRbase -This file documents any backwards-incompatible changes in EHRBase deployment and -assists users migrating to a new version. - -## EHRbase 1.0.0 -### Database Configuration -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: +This file documents any backwards-incompatible changes in EHRBase and assists users migrating to a new version. + +## EHRbase 2.0.0 + +### Migrating data + +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. + +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. + + +## EHRbase 2.7.0 + +### EHR_STATUS and FOLDER consistency check + +Updating an `EHR_STATUS` or `FOLDER` did not check the `If-Match header` against the DB. This allowed to pass in an +invalid identifier that does not match the existing in the DB. This may have lead to inconsistent data in some systems. + +To check if any `EHR_STATUS` or `FOLDER` is affected run [ehrbase_2.7.0_check_ehr_status_and_folder_void](db_scripts/ehrbase_2.7.0_check_ehr_status_and_folder_void.sql). +In case you see an output like: +```text +Inconsistent EHR_STATUS found +Inconsistent FOLDER found ``` --- ensure INTERVAL is ISO8601 encoded -alter database ehrbase SET intervalstyle = 'iso_8601'; -``` \ No newline at end of file +Please open an issue so that a fix can be provided. + +## EHRbase 2.10.0 + +Starting from version 2.0.0 the ehrscape API was deprecated. +With the release of version 2.10.0, the API is now disabled by default, +but can still be enabled by setting the configuration property or environment variable `ehrbase.rest.ehrscape.enabled` to `true`. + +A validation that compositions only contain nodes that are defined by the template has been added. +This behavior can be disabled by setting the configuration property or environment variable `ehrbase.validation.checkForExtraNodes` to `false`. + +## EHRbase 2.11.0 + +The new data model for FOLDER items requires a full migration of the ehr_folder_data table, which may take a while. +As only local VERSIONED_COMPOSITION references are supported, existing data is rewritten accordingly. +If entries of FOLDER.items.id.value exist that do not comply with the UUID format, the migration will fail. +These entries will have to be fixed manually. diff --git a/api/pom.xml b/api/pom.xml index 3ef80a53ae..4bdc9b66b9 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -1,7 +1,7 @@ - + 4.0.0 org.ehrbase.openehr server - 0.20.0-SNAPSHOT + 2.13.0-SNAPSHOT api + + org.aspectj + aspectjweaver + + com.fasterxml.jackson.datatype jackson-datatype-jsr310 @@ -68,9 +71,12 @@ - junit - junit + org.junit.jupiter + junit-jupiter test + - \ No newline at end of file + + + diff --git a/api/src/main/java/org/ehrbase/api/annotations/EhrbaseSecurity.java b/api/src/main/java/org/ehrbase/api/annotations/EhrbaseSecurity.java new file mode 100644 index 0000000000..b616e380f1 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/annotations/EhrbaseSecurity.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.api.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface EhrbaseSecurity {} diff --git a/api/src/main/java/org/ehrbase/api/aspect/AnnotationAspect.java b/api/src/main/java/org/ehrbase/api/aspect/AnnotationAspect.java new file mode 100644 index 0000000000..f116c068b2 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/aspect/AnnotationAspect.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.api.aspect; + +import java.lang.annotation.Annotation; +import java.util.List; +import org.aspectj.lang.ProceedingJoinPoint; + +public interface AnnotationAspect { + public List> matchAnnotations(); + + public Object action(ProceedingJoinPoint pjp, List annotations) throws Throwable; +} diff --git a/api/src/main/java/org/ehrbase/api/aspect/AuthorizationAspect.java b/api/src/main/java/org/ehrbase/api/aspect/AuthorizationAspect.java new file mode 100644 index 0000000000..fc3048866f --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/aspect/AuthorizationAspect.java @@ -0,0 +1,20 @@ +/* + * 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.api.aspect; + +public interface AuthorizationAspect extends AnnotationAspect {} diff --git a/api/src/main/java/org/ehrbase/api/definitions/OperationalTemplateFormat.java b/api/src/main/java/org/ehrbase/api/definitions/OperationalTemplateFormat.java index e4674a9870..a8b8392560 100644 --- a/api/src/main/java/org/ehrbase/api/definitions/OperationalTemplateFormat.java +++ b/api/src/main/java/org/ehrbase/api/definitions/OperationalTemplateFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.definitions; public enum OperationalTemplateFormat { - XML, JSON + XML, + JSON } diff --git a/api/src/main/java/org/ehrbase/api/definitions/QueryMode.java b/api/src/main/java/org/ehrbase/api/definitions/QueryMode.java deleted file mode 100644 index 131350b2b1..0000000000 --- a/api/src/main/java/org/ehrbase/api/definitions/QueryMode.java +++ /dev/null @@ -1,33 +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. - */ - -package org.ehrbase.api.definitions; - -public enum QueryMode { - SQL("sql"), AQL("aql"); - - private final String code; - - QueryMode(String code) { - this.code = code; - } - - public String getCode() { - return code; - } -} diff --git a/api/src/main/java/org/ehrbase/api/definitions/ServerConfig.java b/api/src/main/java/org/ehrbase/api/definitions/ServerConfig.java deleted file mode 100644 index c07d243d43..0000000000 --- a/api/src/main/java/org/ehrbase/api/definitions/ServerConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.ehrbase.api.definitions; - -public interface ServerConfig { - - int getPort(); - - void setPort(int port); - - String getNodename(); - - void setNodename(String nodename); - - String getAqlIterationSkipList(); - - Integer getAqlDepth(); - - Boolean getUseJsQuery(); - - void setUseJsQuery(boolean b); - - public boolean isDisableStrictValidation(); -} diff --git a/api/src/main/java/org/ehrbase/api/dto/AqlQueryContext.java b/api/src/main/java/org/ehrbase/api/dto/AqlQueryContext.java new file mode 100644 index 0000000000..117e0c7276 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/dto/AqlQueryContext.java @@ -0,0 +1,69 @@ +/* + * 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.api.dto; + +import java.net.URI; +import org.ehrbase.openehr.sdk.response.dto.MetaData; + +public interface AqlQueryContext { + + String BEAN_NAME = "scopedAqlQueryContext"; + + interface MetaProperty { + String propertyName(); + } + + enum EhrbaseMetaProperty implements MetaProperty { + OFFSET("offset"), + FETCH("fetch"), + DEFAULT_LIMIT("default-limit"), + MAX_LIMIT("max-limit"), + MAX_FETCH("max-fetch"), + RESULT_SIZE("resultsize"), + DRY_RUN("dry_run"), + EXECUTED_SQL("executed_sql"), + QUERY_PLAN("query_plan"); + + private final String propertyName; + + EhrbaseMetaProperty(String propertyName) { + this.propertyName = propertyName; + } + + @Override + public String propertyName() { + return propertyName; + } + } + + String OPENEHR_REST_API_VERSION = "1.0.3"; + + MetaData createMetaData(URI location); + + boolean showExecutedAql(); + + boolean isDryRun(); + + boolean showExecutedSql(); + + boolean showQueryPlan(); + + void setExecutedAql(String executedAql); + + void setMetaProperty(MetaProperty property, Object value); +} diff --git a/api/src/main/java/org/ehrbase/api/dto/AqlQueryRequest.java b/api/src/main/java/org/ehrbase/api/dto/AqlQueryRequest.java new file mode 100644 index 0000000000..e42b3b4fee --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/dto/AqlQueryRequest.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.api.dto; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.ehrbase.api.service.AqlQueryService; + +/** + * The requested AQL to be executed by {@link AqlQueryService#query(AqlQueryRequest)}. + * + * @param queryString the actual aql query string + * @param parameters additional query parameters + * @param fetch query limit to apply + * @param offset query offset to apply + */ +public record AqlQueryRequest( + @Nonnull String queryString, + @Nullable Map parameters, + @Nullable Long fetch, + @Nullable Long offset) { + + public AqlQueryRequest( + @Nonnull String queryString, + @Nullable Map parameters, + @Nullable Long fetch, + @Nullable Long offset) { + this.queryString = queryString; + this.parameters = rewriteExplicitParameterTypes(parameters); + this.fetch = fetch; + this.offset = offset; + } + + public static Map rewriteExplicitParameterTypes(Map parameters) { + if (parameters == null) { + return Map.of(); + } + parameters.entrySet().forEach(e -> { + Object ov = e.getValue(); + Object nv = handleExplicitParameterTypes(ov); + if (ov != nv) { + e.setValue(nv); + } + }); + return parameters; + } + + /** + * Allows for explicit types via xml: 1 in query parameters. + */ + private static Object handleExplicitParameterTypes(Object paramValue) { + final Object result; + if (paramValue instanceof Map m) { + if (m.get("type") instanceof String type) { + result = switch (type) { + case "int" -> intValue(m, "").orElse(paramValue); + case "num" -> numValue(m, "").orElse(paramValue); + default -> handleExplicitParameterTypes(m.get(""));}; + } else if (m.get("") instanceof List children && !children.isEmpty()) { + result = children.stream() + .map(AqlQueryRequest::handleExplicitParameterTypes) + .toList(); + } else { + result = intValue(m, "int").orElseGet(() -> numValue(m, "num").orElse(paramValue)); + } + } else if (paramValue instanceof List l) { + for (int i = 0, s = l.size(); i < s; i++) { + var v = l.get(i); + var n = handleExplicitParameterTypes(v); + if (v != n) { + l.set(i, n); + } + } + result = paramValue; + } else { + result = paramValue; + } + return result; + } + + private static Optional intValue(Map paramValues, String key) { + return Optional.of(key).map(paramValues::get).map(Object::toString).map(Integer::parseInt); + } + + private static Optional numValue(Map paramValues, String key) { + return Optional.of(key).map(paramValues::get).map(Object::toString).map(Double::parseDouble); + } +} diff --git a/api/src/main/java/org/ehrbase/api/dto/EhrDto.java b/api/src/main/java/org/ehrbase/api/dto/EhrDto.java new file mode 100644 index 0000000000..3e53ce5c0d --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/dto/EhrDto.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 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.api.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.nedap.archie.rm.datavalues.quantity.datetime.DvDateTime; +import com.nedap.archie.rm.support.identification.HierObjectId; +import java.util.List; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.CompositionDto; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.ContributionDto; + +/** + * Basic set of response data regarding EHR_STATUS. operations. Used as default or when PREFER + * header requests minimal response. + */ +@JsonRootName(value = "EHR") +@JacksonXmlRootElement(localName = "ehr") +public record EhrDto( + @JsonProperty(value = "system_id") HierObjectId systemId, + @JsonProperty(value = "ehr_id") HierObjectId ehrId, + @JsonProperty(value = "ehr_status") EhrStatusDto ehrStatus, + @JsonProperty(value = "time_created") DvDateTime timeCreated, + @JsonProperty(value = "compositions") List compositions, + @JsonProperty(value = "contributions") List contributions) {} diff --git a/api/src/main/java/org/ehrbase/api/dto/EhrStatusDto.java b/api/src/main/java/org/ehrbase/api/dto/EhrStatusDto.java new file mode 100644 index 0000000000..555ecb2968 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/dto/EhrStatusDto.java @@ -0,0 +1,59 @@ +/* + * 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.api.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.nedap.archie.rm.archetyped.Archetyped; +import com.nedap.archie.rm.archetyped.FeederAudit; +import com.nedap.archie.rm.datastructures.ItemStructure; +import com.nedap.archie.rm.datavalues.DvText; +import com.nedap.archie.rm.generic.PartySelf; +import com.nedap.archie.rm.support.identification.UIDBasedId; +import javax.annotation.Nullable; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + * Request/Response data of an EHR_STATUS. + */ +@JsonRootName(value = "EHR_STATUS") +@JacksonXmlRootElement(localName = "ehr_status") +@XmlType(name = "EHR_STATUS") +public record EhrStatusDto( + @JsonProperty(value = "uid") @XmlElement UIDBasedId uid, + @JsonProperty(value = "archetype_node_id", required = true) @XmlAttribute(name = "archetype_node_id") + String archetypeNodeId, + @JsonProperty(value = "name") @XmlElement DvText name, + @JsonProperty(value = "archetype_details") @XmlElement(name = "archetype_details") @Nullable + Archetyped archetypeDetails, + @JsonProperty(value = "feeder_audit") @XmlElement(name = "feeder_audit") @Nullable FeederAudit feederAudit, + @JsonProperty(value = "subject") @XmlElement PartySelf subject, + @JsonProperty(value = "is_queryable") @XmlElement(name = "is_queryable") Boolean isQueryable, + @JsonProperty(value = "is_modifiable") @XmlElement(name = "is_modifiable") Boolean isModifiable, + @JsonProperty(value = "other_details") @XmlElement(name = "other_details") @Nullable + ItemStructure otherDetails) { + + @JsonProperty(value = "_type", required = true) + @XmlElement(name = "_type") + public String type() { + return "EHR_STATUS"; + } +} diff --git a/api/src/main/java/org/ehrbase/api/dto/FolderDto.java b/api/src/main/java/org/ehrbase/api/dto/FolderDto.java deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/api/src/main/java/org/ehrbase/api/dto/VersionedCompositionDto.java b/api/src/main/java/org/ehrbase/api/dto/VersionedCompositionDto.java new file mode 100644 index 0000000000..c33960cea5 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/dto/VersionedCompositionDto.java @@ -0,0 +1,41 @@ +/* + * 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.api.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.nedap.archie.rm.support.identification.HierObjectId; +import com.nedap.archie.rm.support.identification.ObjectId; +import com.nedap.archie.rm.support.identification.ObjectRef; + +/** + * Response data of a VERSIONED_COMPOSITION. + */ +@JsonRootName(value = "VERSIONED_COMPOSITION") +@JacksonXmlRootElement(localName = "versioned_composition") +public record VersionedCompositionDto( + @JsonProperty(value = "uid") HierObjectId uid, + @JsonProperty(value = "owner_id") ObjectRef ownerId, + @JsonProperty(value = "time_created") String timeCreated) { + + @JsonProperty(value = "_type") + public String type() { + return "VERSIONED_COMPOSITION"; + } +} diff --git a/api/src/main/java/org/ehrbase/api/dto/VersionedEhrStatusDto.java b/api/src/main/java/org/ehrbase/api/dto/VersionedEhrStatusDto.java new file mode 100644 index 0000000000..b9d24292a4 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/dto/VersionedEhrStatusDto.java @@ -0,0 +1,41 @@ +/* + * 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.api.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.nedap.archie.rm.support.identification.HierObjectId; +import com.nedap.archie.rm.support.identification.ObjectId; +import com.nedap.archie.rm.support.identification.ObjectRef; + +/** + * Response data of a VERSIONED_EHR_STATUS. + */ +@JsonRootName(value = "VERSIONED_EHR_STATUS") +@JacksonXmlRootElement(localName = "versioned_ehr_status") +public record VersionedEhrStatusDto( + @JsonProperty(value = "uid") HierObjectId uid, + @JsonProperty(value = "owner_id") ObjectRef ownerId, + @JsonProperty(value = "time_created") String timeCreated) { + + @JsonProperty(value = "_type") + public String type() { + return "VERSIONED_EHR_STATUS"; + } +} diff --git a/api/src/main/java/org/ehrbase/api/dto/experimental/ItemTagDto.java b/api/src/main/java/org/ehrbase/api/dto/experimental/ItemTagDto.java new file mode 100644 index 0000000000..7d7705aac9 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/dto/experimental/ItemTagDto.java @@ -0,0 +1,166 @@ +/* + * 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.api.dto.experimental; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class ItemTagDto { + private UUID id; + private UUID ownerId; + private UUID target; + private ItemTagRMType targetType; + private String targetPath; + private String key; + private String value; + + public ItemTagDto() { + // NOOP - For Jackson + } + + /** + * @param id Identifier of the tag + * @param ownerId Identifier of owner object, such as EHR. + * @param target Identifier of target, which may be a VERSIONED_OBJECT<T> or a VERSION<T>. + * @param targetType RM type of the tag + * @param targetPath Optional archetype (i.e. AQL) or RM path within target, used to tag a fine-grained element. + * @param key The tag key. May not be empty or contain leading or trailing whitespace. + * @param value The value. If set, may not be empty. + */ + public ItemTagDto( + UUID id, + UUID ownerId, // obtained by path for PUT + UUID target, // obtained by path for PUT + ItemTagRMType targetType, // obtained by path for PUT + String targetPath, + String key, + String value) { + this.id = id; + this.ownerId = ownerId; + this.target = target; + this.targetType = targetType; + this.targetPath = targetPath; + this.key = key; + this.value = value; + } + + @Nullable + public UUID getId() { + return id; + } + + public void setId(@Nullable final UUID id) { + this.id = id; + } + + @JsonProperty(value = "owner_id") + @Nullable + public UUID getOwnerId() { + return ownerId; + } + + public void setOwnerId(@Nullable final UUID ownerId) { + this.ownerId = ownerId; + } + + @Nullable + public UUID getTarget() { + return target; + } + + public void setTarget(@Nullable final UUID target) { + this.target = target; + } + + @JsonProperty(value = "target_type") + @Nullable + public ItemTagRMType getTargetType() { + return targetType; + } + + public void setTargetType(@Nullable final ItemTagRMType targetType) { + this.targetType = targetType; + } + + @JsonProperty(value = "target_path") + @Nullable + public String getTargetPath() { + return targetPath; + } + + public void setTargetPath(@Nullable final String targetPath) { + this.targetPath = targetPath; + } + + @Nonnull + public String getKey() { + return key; + } + + public void setKey(@Nonnull final String key) { + this.key = key; + } + + @Nullable + public String getValue() { + return value; + } + + public void setValue(@Nullable final String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (ItemTagDto) obj; + return Objects.equals(this.id, that.id) + && Objects.equals(this.ownerId, that.ownerId) + && Objects.equals(this.target, that.target) + && Objects.equals(this.targetType, that.targetType) + && Objects.equals(this.targetPath, that.targetPath) + && Objects.equals(this.key, that.key) + && Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(id, ownerId, target, targetType, targetPath, key, value); + } + + @Override + public String toString() { + return "ItemTagDto[" + "id=" + + id + ", " + "ownerId=" + + ownerId + ", " + "target=" + + target + ", " + "targetType=" + + targetType + ", " + "targetPath=" + + targetPath + ", " + "key=" + + key + ", " + "value=" + + value + ']'; + } + + public enum ItemTagRMType { + EHR_STATUS, + COMPOSITION + } +} diff --git a/api/src/main/java/org/ehrbase/api/exception/AqlFeatureNotImplementedException.java b/api/src/main/java/org/ehrbase/api/exception/AqlFeatureNotImplementedException.java new file mode 100644 index 0000000000..f4fdb4bbad --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/exception/AqlFeatureNotImplementedException.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.api.exception; + +public class AqlFeatureNotImplementedException extends AqlRuntimeException { + public AqlFeatureNotImplementedException(String message) { + super("Not implemented: %s".formatted(message)); + } +} diff --git a/api/src/main/java/org/ehrbase/api/exception/AqlRuntimeException.java b/api/src/main/java/org/ehrbase/api/exception/AqlRuntimeException.java new file mode 100644 index 0000000000..83d8ee2930 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/exception/AqlRuntimeException.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.api.exception; + +public abstract class AqlRuntimeException extends RuntimeException { + + public AqlRuntimeException(String s) { + super(s); + } + + public AqlRuntimeException(String message, Throwable cause) { + super(message, cause); + } + + public AqlRuntimeException(Throwable cause) { + super(cause.getMessage(), cause); + } +} diff --git a/api/src/main/java/org/ehrbase/api/exception/BadGatewayException.java b/api/src/main/java/org/ehrbase/api/exception/BadGatewayException.java index 8dff1e63ae..27a49f961a 100644 --- a/api/src/main/java/org/ehrbase/api/exception/BadGatewayException.java +++ b/api/src/main/java/org/ehrbase/api/exception/BadGatewayException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -23,7 +22,7 @@ * status 502 "Bad gateway" or whatever is appropriate. * Proxied connection failed, e.g. the server could not contact the clustered note that processes the specified query (EhrScape API example). */ -public class BadGatewayException extends RuntimeException{ +public class BadGatewayException extends RuntimeException { public BadGatewayException(String message) { super(message); diff --git a/api/src/main/java/org/ehrbase/api/exception/DuplicateObjectException.java b/api/src/main/java/org/ehrbase/api/exception/DuplicateObjectException.java deleted file mode 100644 index 38d00b10a2..0000000000 --- a/api/src/main/java/org/ehrbase/api/exception/DuplicateObjectException.java +++ /dev/null @@ -1,38 +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. - */ - -package org.ehrbase.api.exception; - -public class DuplicateObjectException extends RuntimeException { - - private String type; - - public DuplicateObjectException(String type, String message) { - super(message); - this.type = type; - } - - public DuplicateObjectException(String type, String message, Throwable cause) { - super(message, cause); - this.type = type; - } - - public String getType() { - return type; - } -} diff --git a/api/src/main/java/org/ehrbase/api/exception/GeneralRequestProcessingException.java b/api/src/main/java/org/ehrbase/api/exception/GeneralRequestProcessingException.java index 02300e3021..3cbcfaec01 100644 --- a/api/src/main/java/org/ehrbase/api/exception/GeneralRequestProcessingException.java +++ b/api/src/main/java/org/ehrbase/api/exception/GeneralRequestProcessingException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -23,7 +22,7 @@ * status 400 "Bad Request" or whatever is appropriate. * To be thrown in all cases where part of the request leads to problems, like malformed queries or non-existent referenced objects. */ -public class GeneralRequestProcessingException extends RuntimeException { +public class GeneralRequestProcessingException extends RuntimeException { public GeneralRequestProcessingException(String message) { super(message); diff --git a/api/src/main/java/org/ehrbase/api/exception/IllegalAqlException.java b/api/src/main/java/org/ehrbase/api/exception/IllegalAqlException.java new file mode 100644 index 0000000000..a004d9ef3d --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/exception/IllegalAqlException.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.api.exception; + +public class IllegalAqlException extends AqlRuntimeException { + public IllegalAqlException(String message) { + super(message); + } + + public IllegalAqlException(String message, Throwable cause) { + super(message, cause); + } + + public IllegalAqlException(Throwable cause) { + super(cause.getMessage(), cause); + } +} diff --git a/api/src/main/java/org/ehrbase/api/exception/InternalServerException.java b/api/src/main/java/org/ehrbase/api/exception/InternalServerException.java index 2dd28bc87c..07a9bf6f11 100644 --- a/api/src/main/java/org/ehrbase/api/exception/InternalServerException.java +++ b/api/src/main/java/org/ehrbase/api/exception/InternalServerException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -29,11 +28,11 @@ public InternalServerException(String message) { super(message); } - public InternalServerException(String message, Exception cause) { + public InternalServerException(String message, Throwable cause) { super(message, cause); } - public InternalServerException(Exception cause) { - super(cause); + public InternalServerException(Throwable cause) { + super(cause.getMessage(), cause); } } diff --git a/api/src/main/java/org/ehrbase/api/exception/InvalidApiParameterException.java b/api/src/main/java/org/ehrbase/api/exception/InvalidApiParameterException.java index 4371f3d6b1..9e65c6dd66 100644 --- a/api/src/main/java/org/ehrbase/api/exception/InvalidApiParameterException.java +++ b/api/src/main/java/org/ehrbase/api/exception/InvalidApiParameterException.java @@ -1,11 +1,13 @@ /* - * Copyright 2019-2022 vitasystems GmbH and Hannover Medical School. + * 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 + * 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, @@ -13,16 +15,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; public class InvalidApiParameterException extends RuntimeException { - public InvalidApiParameterException(String message) { - super(message); - } + public InvalidApiParameterException(String message) { + super(message); + } - public InvalidApiParameterException(String message, Throwable cause) { - super(message, cause); - } + public InvalidApiParameterException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/api/src/main/java/org/ehrbase/api/exception/NotAcceptableException.java b/api/src/main/java/org/ehrbase/api/exception/NotAcceptableException.java index 3ce803bc98..99a9f42f2b 100644 --- a/api/src/main/java/org/ehrbase/api/exception/NotAcceptableException.java +++ b/api/src/main/java/org/ehrbase/api/exception/NotAcceptableException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; public class NotAcceptableException extends RuntimeException { diff --git a/api/src/main/java/org/ehrbase/api/exception/ObjectNotFoundException.java b/api/src/main/java/org/ehrbase/api/exception/ObjectNotFoundException.java index 1fda474069..28b286e061 100644 --- a/api/src/main/java/org/ehrbase/api/exception/ObjectNotFoundException.java +++ b/api/src/main/java/org/ehrbase/api/exception/ObjectNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -25,7 +24,7 @@ */ public class ObjectNotFoundException extends RuntimeException { - private String type; + private final String type; public ObjectNotFoundException(String type, String message) { super(message); diff --git a/api/src/main/java/org/ehrbase/api/exception/PreconditionFailedException.java b/api/src/main/java/org/ehrbase/api/exception/PreconditionFailedException.java index 8e583eca36..5cc4a0ea75 100644 --- a/api/src/main/java/org/ehrbase/api/exception/PreconditionFailedException.java +++ b/api/src/main/java/org/ehrbase/api/exception/PreconditionFailedException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -23,7 +22,7 @@ * status 412 "Precondition Failed" or whatever is appropriate. * To be thrown in all cases where part of the request leads to problems, like malformed queries or non-existent referenced objects. */ -public class PreconditionFailedException extends RuntimeException { +public class PreconditionFailedException extends RuntimeException { private final String currentVersionUid; private final String url; @@ -56,4 +55,4 @@ public String getCurrentVersionUid() { public String getUrl() { return url; } -} \ No newline at end of file +} diff --git a/api/src/main/java/org/ehrbase/api/exception/StateConflictException.java b/api/src/main/java/org/ehrbase/api/exception/StateConflictException.java index 715afb1052..5b92d265e4 100644 --- a/api/src/main/java/org/ehrbase/api/exception/StateConflictException.java +++ b/api/src/main/java/org/ehrbase/api/exception/StateConflictException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -23,7 +22,7 @@ * status 409 "Conflict" or whatever is appropriate. * Request is in conflict with current state, e.g. referenced version of object is not the last version. */ -public class StateConflictException extends RuntimeException{ +public class StateConflictException extends RuntimeException { public StateConflictException(String message) { super(message); diff --git a/api/src/main/java/org/ehrbase/api/exception/UnexpectedSwitchCaseException.java b/api/src/main/java/org/ehrbase/api/exception/UnexpectedSwitchCaseException.java index 761961c9fb..be48a4f875 100644 --- a/api/src/main/java/org/ehrbase/api/exception/UnexpectedSwitchCaseException.java +++ b/api/src/main/java/org/ehrbase/api/exception/UnexpectedSwitchCaseException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 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 * - * http://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,38 +15,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; import org.apache.commons.lang3.StringUtils; /** *

- * Diese Exception wird geworfen, wenn in einem Switch-Block ein Case eingetreten ist, der nicht korrekt behandelt werden kann. Eigentlich - * sollte dieser Fall nicht auftreten (außer man ignoriert die Eclipse-Warning zum unvollständigen Switch). + * This exception is thrown when a case has occurred in a switch block that cannot be handled correctly. *

*

- * Typische Verwendungen in einer Switch-Definition: + * Typical uses in a switch statement: *

    - *
  1. Im unerwarteten default-Case
  2. - *
  3. Im explizit nicht sinnvoll behandelbaren Case
  4. - *
  5. Im unerwarteten default-Case eines Switch mit Fall-Through von nicht sinnvoll behandelbaren Cases (Kombination aus 1. und 2.)
  6. + *
  7. unexpected default case
  8. + *
  9. explicitly not sensibly treatable case
  10. + *
  11. unexpected default case with fall-through of not sensibly treatable case (combination of 1. and 2.)
  12. *
- *

- * Siehe dazu auch: Code Conventions - - * Vollständige Switch-Statements - *

* - * @author Jan Falkenstern, Stefan Kock */ public class UnexpectedSwitchCaseException extends RuntimeException { private static final long serialVersionUID = 5695009820197756438L; /** - * Erzeugt aus dem übergebenen enumValue eine message.
- * Ausgegeben in der Exception-Message wird enumValue.name(), - * damit z.B. bei lokalisierten Enums immer denselbe Wert ausgegeben wird. + * Creates a message from the enumValue parameter.
+ * The message is based on enumValue.name(), + * so that the format is stable, e.g. in case of i18n. * * @param enumValue must not be null */ @@ -55,9 +48,9 @@ public UnexpectedSwitchCaseException(Enum enumValue) { } /** - * Erzeugt aus dem übergebenen enumValue eine message.
- * Ausgegeben in der Exception-Message wird enumValue.name(), - * damit z.B. bei lokalisierten Enums immer denselbe Wert ausgegeben wird. + * Creates a message from the enumValue parameter.
+ * The message is based on enumValue.name(), + * so that the format is stable, e.g. in case of i18n. * * @param enumValue must not be null * @param additionalMessage additional text for generated message @@ -67,7 +60,7 @@ public UnexpectedSwitchCaseException(Enum enumValue, String additionalMessage } /** - * Erzeugt aus dem übergebenen intValue eine message. + * Creates a message from the intValue parameter * * @param intValue */ @@ -76,7 +69,7 @@ public UnexpectedSwitchCaseException(Integer intValue) { } /** - * Erzeugt aus dem übergebenen intValue eine message. + * Creates a message from the intValue parameter * * @param intValue * @param additionalMessage additional text for generated message @@ -86,13 +79,7 @@ public UnexpectedSwitchCaseException(Integer intValue, String additionalMessage) } /** - * Erzeugt aus dem übergebenen stringValue eine message. - * - * - *

Achtung: Breaking Change!

- *

Mit Version 0.1.3-SNAPSHOT hat sich die Bedeutung des Konstruktors geändert!
- * Statt der Meldung wird jetzt nur der Wert angegeben!

- *
+ * Creates a message from the stringValue parameter * * @param stringValue */ @@ -101,7 +88,7 @@ public UnexpectedSwitchCaseException(String stringValue) { } /** - * Erzeugt aus dem übergebenen stringValue eine message. + * Creates a message from the stringValue parameter * * @param stringValue * @param additionalMessage additional text for generated message @@ -111,10 +98,9 @@ public UnexpectedSwitchCaseException(String stringValue, String additionalMessag } /** - * Erzeugt die Message der {@link UnexpectedSwitchCaseException} anhand der übergebenen Parameter mit Ausgabe von - * enumValue.getClass().getSimpleName().
- * Ausgegeben in der Exception-Message wird enumValue.name(), - * damit z.B. bei lokalisierten Enums immer denselbe Wert ausgegeben wird. + * Creates the message of a {@link UnexpectedSwitchCaseException} containing the simple class name of the enumValue parameter. + * The message is based on enumValue.name(), + * so that the format is stable, e.g. in case of i18n. * * @param enumValue must not be null * @param additionalMessage additional text for generated message (optional) @@ -126,7 +112,7 @@ public static String formatMessage(Enum enumValue, String additionalMessage) } /** - * Erzeugt die Message der {@link UnexpectedSwitchCaseException} anhand der übergebenen Parameter. + * Creates the message of a {@link UnexpectedSwitchCaseException} based on the given parameters. * * @param value * @param additionalMessage additional text for generated message (optional) @@ -138,7 +124,7 @@ public static String formatMessage(Object value, String additionalMessage) { } /** - * Erzeugt die Message der {@link UnexpectedSwitchCaseException} anhand der übergebenen Parameter. + * Creates the message of a {@link UnexpectedSwitchCaseException} based on the given parameters. * * @param type e.g. enumValue.getClass().getSimpleName() * @param value unsupported value in switch @@ -166,4 +152,4 @@ public static String formatMessage(String type, Object value, String additionalM return sb.toString(); } -} \ No newline at end of file +} diff --git a/api/src/main/java/org/ehrbase/api/exception/UnprocessableEntityException.java b/api/src/main/java/org/ehrbase/api/exception/UnprocessableEntityException.java index d8da677be5..05ae453c58 100644 --- a/api/src/main/java/org/ehrbase/api/exception/UnprocessableEntityException.java +++ b/api/src/main/java/org/ehrbase/api/exception/UnprocessableEntityException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -27,4 +26,8 @@ public class UnprocessableEntityException extends RuntimeException { public UnprocessableEntityException(String message) { super(message); } + + public UnprocessableEntityException(String message, Throwable e) { + super(message, e); + } } diff --git a/api/src/main/java/org/ehrbase/api/exception/UnsupportedMediaTypeException.java b/api/src/main/java/org/ehrbase/api/exception/UnsupportedMediaTypeException.java index 6f013b262e..11d7e3b26f 100644 --- a/api/src/main/java/org/ehrbase/api/exception/UnsupportedMediaTypeException.java +++ b/api/src/main/java/org/ehrbase/api/exception/UnsupportedMediaTypeException.java @@ -1,3 +1,20 @@ +/* + * 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.api.exception; /** @@ -14,6 +31,7 @@ public class UnsupportedMediaTypeException extends RuntimeException { public UnsupportedMediaTypeException(String message) { super(message); } + public UnsupportedMediaTypeException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/org/ehrbase/api/exception/ValidationException.java b/api/src/main/java/org/ehrbase/api/exception/ValidationException.java index ccfced4fb3..af5a4d9838 100644 --- a/api/src/main/java/org/ehrbase/api/exception/ValidationException.java +++ b/api/src/main/java/org/ehrbase/api/exception/ValidationException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Christian Chevalley (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 * - * http://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,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.exception; /** @@ -23,7 +22,7 @@ * status 400 "Bad Request" or whatever is appropriate. * To be thrown in all cases where part of the request leads to problems, like malformed queries or non-existent referenced objects. */ -public class ValidationException extends RuntimeException { +public class ValidationException extends RuntimeException { public ValidationException(String message) { super(message); @@ -33,7 +32,7 @@ public ValidationException(String message, Throwable cause) { super(message, cause); } - public ValidationException(Exception otherException){ - super(otherException); + public ValidationException(Exception cause) { + super(cause.getMessage(), cause); } } diff --git a/api/src/main/java/org/ehrbase/api/knowledge/KnowledgeCacheService.java b/api/src/main/java/org/ehrbase/api/knowledge/KnowledgeCacheService.java new file mode 100644 index 0000000000..2b6fea3c47 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/knowledge/KnowledgeCacheService.java @@ -0,0 +1,63 @@ +/* + * 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.api.knowledge; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.ehrbase.openehr.sdk.webtemplate.model.WebTemplate; +import org.openehr.schemas.v1.OPERATIONALTEMPLATE; + +public interface KnowledgeCacheService { + + String addOperationalTemplate(OPERATIONALTEMPLATE template); + + List listAllOperationalTemplates(); + + Map findAllTemplateIds(); + + /** + * retrieve an operational template document instance + * + * @param key the name of the operational template + * @return an OPERATIONALTEMPLATE document instance or null + * @see org.openehr.schemas.v1.OPERATIONALTEMPLATE + */ + Optional retrieveOperationalTemplate(String key); + + WebTemplate getWebTemplate(String templateId); + + /** + * Deletes a given operational template physically from cache and from template storage and from cache. Should only + * be executed if the template is no longer referenced by any Composition. Make sure you check for references before + * deleting a template otherwise this causes inconsistencies and no longer deliverable Composition entries. + * + * @param template - The template instance to delete + */ + void deleteOperationalTemplate(OPERATIONALTEMPLATE template); + + Optional findTemplateIdByUuid(UUID uuid); + + Optional findUuidByTemplateId(String templateId); + + String adminUpdateOperationalTemplate(InputStream content); + + int deleteAllOperationalTemplates(); +} diff --git a/api/src/main/java/org/ehrbase/api/knowledge/TemplateMetaData.java b/api/src/main/java/org/ehrbase/api/knowledge/TemplateMetaData.java new file mode 100644 index 0000000000..f420c25064 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/knowledge/TemplateMetaData.java @@ -0,0 +1,71 @@ +/* + * 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.api.knowledge; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.openehr.schemas.v1.OPERATIONALTEMPLATE; + +public class TemplateMetaData { + private OPERATIONALTEMPLATE operationaltemplate; + private OffsetDateTime createdOn; + + private UUID internalId; + + private List errorList; + + public OPERATIONALTEMPLATE getOperationaltemplate() { + return operationaltemplate; + } + + public void setOperationalTemplate(OPERATIONALTEMPLATE operationaltemplate) { + this.operationaltemplate = operationaltemplate; + } + + public OffsetDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(OffsetDateTime createdOn) { + this.createdOn = createdOn; + } + + public void setInternalId(UUID internalId) { + this.internalId = internalId; + } + + public UUID getInternalId() { + return internalId; + } + + public List getErrorList() { + if (this.errorList == null) { + this.errorList = new ArrayList<>(); + } + return errorList; + } + + public void addError(String error) { + if (this.errorList == null) { + this.errorList = new ArrayList<>(); + } + this.errorList.add(error); + } +} diff --git a/api/src/main/java/org/ehrbase/api/mapper/StructuredStringJSonSerializer.java b/api/src/main/java/org/ehrbase/api/mapper/StructuredStringJSonSerializer.java index b258e96523..22a88f39ad 100644 --- a/api/src/main/java/org/ehrbase/api/mapper/StructuredStringJSonSerializer.java +++ b/api/src/main/java/org/ehrbase/api/mapper/StructuredStringJSonSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) Stefan Spiska (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 * - * http://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,17 +15,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.mapper; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; -import org.ehrbase.api.exception.UnexpectedSwitchCaseException; -import org.ehrbase.response.ehrscape.StructuredString; - import java.io.IOException; +import org.ehrbase.api.exception.UnexpectedSwitchCaseException; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.StructuredString; public class StructuredStringJSonSerializer extends JsonSerializer { @Override @@ -55,5 +53,3 @@ public void serialize(StructuredString value, JsonGenerator jgen, SerializerProv } } } - - diff --git a/api/src/main/java/org/ehrbase/api/repository/KeyValuePair.java b/api/src/main/java/org/ehrbase/api/repository/KeyValuePair.java new file mode 100644 index 0000000000..58e21db89e --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/repository/KeyValuePair.java @@ -0,0 +1,70 @@ +/* + * 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.api.repository; + +import java.util.UUID; + +public interface KeyValuePair { + + public static KeyValuePair of(String pluginId, String key, String value) { + return KeyValuePair.of(UUID.randomUUID(), pluginId, key, value); + } + + public static KeyValuePair of(UUID id, String pluginId, String key, String value) { + return new KeyValueEntry(id, pluginId, key, value); + } + + public UUID getId(); + + public String getContext(); + + public String getKey(); + + public String getValue(); +} + +class KeyValueEntry implements KeyValuePair { + + private final UUID id; + private final String context; + private final String key; + private final String value; + + KeyValueEntry(UUID id, String pluginId, String key, String value) { + this.id = id; + this.context = pluginId; + this.key = key; + this.value = value; + } + + public UUID getId() { + return id; + } + + public String getContext() { + return context; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } +} diff --git a/api/src/main/java/org/ehrbase/api/repository/KeyValuePairRepository.java b/api/src/main/java/org/ehrbase/api/repository/KeyValuePairRepository.java new file mode 100644 index 0000000000..e654330f01 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/repository/KeyValuePairRepository.java @@ -0,0 +1,35 @@ +/* + * 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.api.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface KeyValuePairRepository { + + public List findAllBy(String context); + + public Optional findBy(String context, String key); + + public Optional findBy(UUID uid); + + public KeyValuePair save(KeyValuePair kve); + + public boolean deleteBy(UUID uid); +} diff --git a/api/src/main/java/org/ehrbase/api/rest/EHRbaseHeader.java b/api/src/main/java/org/ehrbase/api/rest/EHRbaseHeader.java new file mode 100644 index 0000000000..43eb7f8b3b --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/rest/EHRbaseHeader.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.api.rest; + +/** + * EHRbase specific HTTP headers that are not part of the openEHR standard. + */ +public final class EHRbaseHeader { + + private EHRbaseHeader() {} + + public static final String TEMPLATE_ID = "EHRBase-Template-ID"; + + /** + * Used by the /query endpoint to perform only a dry run query. + */ + public static final String AQL_DRY_RUN = "EHRbase-AQL-Dry-Run"; + + /** + * Used by the /query endpoint to provide the executed SQL statement in the return metadata. + */ + public static final String AQL_EXECUTED_SQL = "EHRbase-AQL-Executed-SQL"; + + /** + * Used by the /query endpoint to provide the database query plan in the return metadata. + */ + public static final String AQL_QUERY_PLAN = "EHRbase-AQL-Query-Plan"; +} diff --git a/api/src/main/java/org/ehrbase/api/rest/HttpRestContext.java b/api/src/main/java/org/ehrbase/api/rest/HttpRestContext.java new file mode 100644 index 0000000000..da0311a1ad --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/rest/HttpRestContext.java @@ -0,0 +1,100 @@ +/* + * 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.api.rest; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class HttpRestContext { + + public interface HttpCtx { + T get(CtxAttr attr); + } + + public static final class CtxAttr {} + + public static final CtxAttr QUERY = new CtxAttr<>(); + public static final CtxAttr QUERY_ID = new CtxAttr<>(); + public static final CtxAttr LOCATION = new CtxAttr<>(); + public static final CtxAttr VERSION = new CtxAttr<>(); + public static final CtxAttr EHR_ID = new CtxAttr<>(); + public static final CtxAttr TEMPLATE_ID = new CtxAttr<>(); + public static final CtxAttr COMPOSITION_ID = new CtxAttr<>(); + public static final CtxAttr DIRECTORY_ID = new CtxAttr<>(); + public static final CtxAttr CONTRIBUTION_ID = new CtxAttr<>(); + public static final CtxAttr> REMOVED_PATIENTS = new CtxAttr<>(); + public static final CtxAttr QUERY_EXECUTE_ENDPOINT = new CtxAttr<>(); + + private static final ThreadLocal, Object>> httpContext = ThreadLocal.withInitial(HashMap::new); + + public static void clear() { + httpContext.remove(); + } + + public static void register(CtxAttr key, V value) { + httpContext.get().put(key, value); + } + + public static void register(CtxAttr key0, V0 value0, CtxAttr key1, V1 value1) { + Map, Object> map = httpContext.get(); + map.put(key0, value0); + map.put(key1, value1); + } + + public static void register( + CtxAttr key0, V0 value0, CtxAttr key1, V1 value1, CtxAttr key2, V2 value2) { + Map, Object> map = httpContext.get(); + map.put(key0, value0); + map.put(key1, value1); + map.put(key2, value2); + } + + public static void register( + CtxAttr key0, + V0 value0, + CtxAttr key1, + V1 value1, + CtxAttr key2, + V2 value2, + CtxAttr key3, + V3 value3) { + Map, Object> map = httpContext.get(); + map.put(key0, value0); + map.put(key1, value1); + map.put(key2, value2); + map.put(key3, value3); + } + + @SuppressWarnings("unchecked") + public static T get(CtxAttr attr) { + Map, Object> ctxAttrObjectMap = httpContext.get(); + return (T) ctxAttrObjectMap.get(attr); + } + + public static void handle(HttpRestContextHandler handler) { + Map, Object> ctxAttrObjectMap = httpContext.get(); + handler.handle(new HttpCtx() { + @Override + public T get(CtxAttr attr) { + return (T) ctxAttrObjectMap.get(attr); + } + }); + } +} diff --git a/api/src/main/java/org/ehrbase/api/rest/HttpRestContextHandler.java b/api/src/main/java/org/ehrbase/api/rest/HttpRestContextHandler.java new file mode 100644 index 0000000000..1569a1d4d0 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/rest/HttpRestContextHandler.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.api.rest; + +import org.ehrbase.api.rest.HttpRestContext.HttpCtx; + +public interface HttpRestContextHandler { + + void handle(HttpCtx context); +} diff --git a/api/src/main/java/org/ehrbase/api/service/AqlQueryService.java b/api/src/main/java/org/ehrbase/api/service/AqlQueryService.java new file mode 100644 index 0000000000..73eb7fe6ad --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/service/AqlQueryService.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.api.service; + +import org.ehrbase.api.dto.AqlQueryRequest; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.QueryResultDto; + +public interface AqlQueryService { + + /** + * simple query where the full json expression contains both query (key = 'q') and optional + * parameters (key = 'query-parameters') + * + * @param aqlQueryRequest to perform + * @return aqlQueryResult + */ + QueryResultDto query(AqlQueryRequest aqlQueryRequest); +} diff --git a/api/src/main/java/org/ehrbase/api/service/BaseService.java b/api/src/main/java/org/ehrbase/api/service/BaseService.java deleted file mode 100644 index 3d9cc96679..0000000000 --- a/api/src/main/java/org/ehrbase/api/service/BaseService.java +++ /dev/null @@ -1,33 +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. - */ - -package org.ehrbase.api.service; - -import org.ehrbase.api.definitions.ServerConfig; - -import java.util.UUID; - -/** - * Basic service interface, foundation of all service interfaces - */ -public interface BaseService { - - UUID getSystemUuid(); - - ServerConfig getServerConfig(); -} diff --git a/api/src/main/java/org/ehrbase/api/service/CompositionService.java b/api/src/main/java/org/ehrbase/api/service/CompositionService.java index 50b61860f1..b1e67a8c84 100644 --- a/api/src/main/java/org/ehrbase/api/service/CompositionService.java +++ b/api/src/main/java/org/ehrbase/api/service/CompositionService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Stefan Spiska (Vitasystems GmbH), Jake Smolka (Hannover Medical School), and Luis Marco-Ruiz (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 * - * http://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,49 +15,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.service; import com.nedap.archie.rm.changecontrol.OriginalVersion; import com.nedap.archie.rm.composition.Composition; import com.nedap.archie.rm.ehr.VersionedComposition; import com.nedap.archie.rm.generic.RevisionHistory; -import org.ehrbase.api.exception.InternalServerException; -import org.ehrbase.api.exception.ObjectNotFoundException; -import org.ehrbase.response.ehrscape.CompositionDto; -import org.ehrbase.response.ehrscape.CompositionFormat; -import org.ehrbase.response.ehrscape.StructuredString; - -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.Optional; import java.util.UUID; +import javax.annotation.Nullable; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.CompositionDto; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.CompositionFormat; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.StructuredString; + +public interface CompositionService extends VersionedObjectService { + + static CompositionDto from(UUID ehrId, Composition composition) { + + return new CompositionDto( + composition, + composition.getArchetypeDetails().getTemplateId().getValue(), + UUID.fromString(composition.getUid().getRoot().getValue()), + ehrId); + } -public interface CompositionService extends BaseService, VersionedObjectService { /** * @param compositionId The {@link UUID} of the composition to be returned. + * @param ehrId The {@link UUID} of the ehr wich contains the composition * @param version The version to returned. If null return the latest * @return * @throws InternalServerException */ - Optional retrieve(UUID compositionId, Integer version); + Optional retrieve(UUID ehrId, UUID compositionId, Integer version); /** - * TODO: untested because not needed, yet - * - * Gets the composition that is closest in time before timestamp - * - * @param compositionId UUID (versioned_object_id) of composition - * @param timestamp Given time - * @return Optional of CompositionDto closest in time before timestamp - */ - Optional retrieveByTimestamp(UUID compositionId, LocalDateTime timestamp); - - /** - * Public serializer entry point which will be called with - * composition dto fetched from database and the - * desired target serialized string format. - * Will parse the composition dto into target format either - * with a custom lambda expression for desired target format + * Public serializer entry point which will be called with composition dto fetched from database + * and the desired target serialized string format. Will parse the composition dto into target + * format either with a custom lambda expression for desired target format * * @param composition Composition dto from database * @param format Target format @@ -65,75 +61,90 @@ public interface CompositionService extends BaseService, VersionedObjectService< */ StructuredString serialize(CompositionDto composition, CompositionFormat format); - Integer getLastVersionNumber(UUID compositionId); - /** - * Helper function to read UUID from given composition input in stated format. - * @param content Composition input - * @param format Composition format - * @return The UUID or null when not available. + * Retrieve the latest version number for the given composition ID. + * + * @param compositionId The {@link UUID} of the composition to be returned. + * @return latestVersion of the existing composition. */ - String getUidFromInputComposition(String content, CompositionFormat format); + int getLastVersionNumber(UUID compositionId); /** - * Helper function to read the template ID from given composition input in stated format. - * @param content Composition input - * @param format Composition format - * @return The UUID or null when not available. + * Retrieves the template ID associated with a given composition ID. + * + * @param compositionId The UUID of the composition for which to retrieve the template ID + * @return The template ID associated with the given composition ID */ - String getTemplateIdFromInputComposition(String content, CompositionFormat format); + String retrieveTemplateId(UUID compositionId); /** * Gets the version of a composition that is closest in time before timestamp + * * @param compositionId UUID (versioned_object_id) of composition - * @param timestamp Given time + * @param timestamp Given time * @return Version closest in time before given timestamp, or `null` in case of error. */ - Integer getVersionByTimestamp(UUID compositionId, LocalDateTime timestamp); + int getVersionByTimestamp(UUID compositionId, OffsetDateTime timestamp); /** - * Checks if given ID is a valid composition ID. + * Checks if given ID is already used. + * * @param versionedObjectId ID to check - * @return True if ID exists - * @throws ObjectNotFoundException if ID does not exist + * @return if ID already exists */ boolean exists(UUID versionedObjectId); /** * Checks if given composition ID is ID of a logically deleted composition. + * + * @param ehrId EHRid to delete composition for * @param versionedObjectId ID to check + * @param version Version to delete, option uses head as default * @return True if deleted, false if not */ - boolean isDeleted(UUID versionedObjectId); + boolean isDeleted(UUID ehrId, UUID versionedObjectId, @Nullable Integer version); /** * Admin method to delete a Composition from the DB. See EHRbase Admin API specification for details. + * * @param compositionId Composition to delete */ void adminDelete(UUID compositionId); /** * Gets version container Composition associated with given EHR and Composition ID. - * @param ehrUid Given EHR ID - * @param composition Given Composition ID + * + * @param ehrUid Given EHR ID + * @param compositionId Given Composition ID * @return Version container object */ - VersionedComposition getVersionedComposition(UUID ehrUid, UUID composition); + VersionedComposition getVersionedComposition(UUID ehrUid, UUID compositionId); /** * Gets revision history of given composition. - * @param composition Given composition. + * + * @param compositionId Given composition. * @return Revision history */ - RevisionHistory getRevisionHistoryOfVersionedComposition(UUID composition); + RevisionHistory getRevisionHistoryOfVersionedComposition(UUID ehrUid, UUID compositionId); /** * Gets Original Version container class representation of the given composition at given version. + * * @param versionedObjectUid Given composition Uid. - * @param version Given version number. + * @param version Given version number. * @return Original Version container class representation. */ - Optional> getOriginalVersionComposition(UUID versionedObjectUid, int version); + Optional> getOriginalVersionComposition( + UUID ehrUid, UUID versionedObjectUid, int version); Composition buildComposition(String content, CompositionFormat format, String templateId); + + /** + * Gets the EHR id for the given Composition id. + * + * @param compositionId Given composition Uid. + * @return EHR UID for the given Composition + */ + Optional getEhrIdForComposition(UUID compositionId); } diff --git a/api/src/main/java/org/ehrbase/api/service/ContributionService.java b/api/src/main/java/org/ehrbase/api/service/ContributionService.java index fc8c1e4224..59905ca70a 100644 --- a/api/src/main/java/org/ehrbase/api/service/ContributionService.java +++ b/api/src/main/java/org/ehrbase/api/service/ContributionService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (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 * - * http://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,61 +15,103 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.service; -import java.util.Set; -import org.ehrbase.api.exception.InternalServerException; -import org.ehrbase.response.ehrscape.CompositionFormat; -import org.ehrbase.response.ehrscape.ContributionDto; - -import java.util.Optional; +import com.nedap.archie.rm.datavalues.DvCodedText; +import com.nedap.archie.rm.generic.AuditDetails; import java.util.UUID; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.api.exception.ValidationException; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.ContributionDto; /** * Interface for contribution service roughly based on openEHR SM "I_EHR_CONTRIBUTION Interface", * see: https://specifications.openehr.org/releases/SM/latest/openehr_platform.html#_i_ehr_contribution_interface */ -public interface ContributionService extends BaseService { +public interface ContributionService { - /** - * Check if given contribution exists and is part of given EHR. - * @param ehrId ID of EHR - * @param contributionId ID of contribution - * @return True if exists and part of EHR, false if not - */ - boolean hasContribution(UUID ehrId, UUID contributionId); + enum ContributionChangeType { + CREATION(249), + AMENDMENT(250), + MODIFICATION(251), + SYNTHESIS(252), + UNKNOWN(253), + DELETED(523); + final int code; + + ContributionChangeType(int code) { + this.code = code; + } + + public static ContributionChangeType fromAuditDetails(AuditDetails commitAudit) { + DvCodedText changeType = commitAudit.getChangeType(); + + if (!"openehr" + .equals(changeType.getDefiningCode().getTerminologyId().getValue())) { + throw new ValidationException("Unsupported change type terminology: %s" + .formatted( + changeType.getDefiningCode().getTerminologyId().getValue())); + } + + ContributionChangeType byCode = + getByCode(changeType.getDefiningCode().getCodeString()); + if (byCode.name().equalsIgnoreCase(changeType.getValue())) { + return byCode; + } else { + throw new ValidationException("Inconsistent change type: %s for code %s" + .formatted( + changeType.getValue(), + changeType.getDefiningCode().getCodeString())); + } + } + + private static ContributionChangeType getByCode(String codeString) { + + int code; + try { + code = Integer.parseInt(codeString); + } catch (NumberFormatException e) { + throw new ValidationException("Unknown change type code %s".formatted(codeString)); + } + + return Stream.of(ContributionChangeType.values()) + .filter(t -> t.code == code) + .findFirst() + .orElseThrow(() -> new ValidationException("Unknown change type code %s".formatted(codeString))); + } + + public int getCode() { + return code; + } + } /** * Return the Contribution with given id in given EHR. - * @param ehrId ID of EHR + * + * @param ehrId ID of EHR * @param contributionId ID of contribution - * @return {@link Optional} containing a {@link ContributionDto} if successful, empty if not */ - Optional getContribution(UUID ehrId, UUID contributionId); + @Nonnull + ContributionDto getContribution(UUID ehrId, UUID contributionId); /** * Commit a CONTRIBUTION containing any number of serialized VERSION objects. - * @param ehrId ID of EHR + * + * @param ehrId ID of EHR * @param content serialized content, containing version objects and audit object in given format - * @param format format of serialized versions * @return ID of successfully committed contribution * @throws IllegalArgumentException when input can't be processed - * @throws InternalServerException when DB is inconsistent + * @throws InternalServerException when DB is inconsistent */ - UUID commitContribution(UUID ehrId, String content, CompositionFormat format); + UUID commitContribution(UUID ehrId, String content); /** * Admin method to delete a Contribution from the DB. See EHRbase Admin API specification for details. + * + * @param ehrId * @param contributionId Contribution to delete */ - void adminDelete(UUID contributionId); - - /** - * Extracts set of used templates in payload's compositions. - * @param contribution Contribution request content - * @param format Format of that content - * @return Set of templates used by compositions - */ - Set getListOfTemplates(String contribution, CompositionFormat format); + void adminDelete(UUID ehrId, UUID contributionId); } diff --git a/api/src/main/java/org/ehrbase/api/service/DirectoryService.java b/api/src/main/java/org/ehrbase/api/service/DirectoryService.java new file mode 100644 index 0000000000..6284d941a3 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/service/DirectoryService.java @@ -0,0 +1,85 @@ +/* + * 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.api.service; + +import com.nedap.archie.rm.directory.Folder; +import com.nedap.archie.rm.support.identification.ObjectVersionId; +import java.time.OffsetDateTime; +import java.util.Optional; +import java.util.UUID; +import javax.annotation.Nullable; + +public interface DirectoryService { + + int EHR_DIRECTORY_FOLDER_IDX = 1; + + /** + * Get the Folder for Ehr with id equal ehrId + * + * @param ehrId + * @param folderId if null latest version will be returned + * @param path optional return folder at path + * @return + */ + Optional get(UUID ehrId, @Nullable ObjectVersionId folderId, @Nullable String path); + + /** + * Get the Folder for Ehr with id equal ehrId for a specific point in time; + * + * @param ehrId + * @param time + * @param path optional return folder at path + * @return + */ + Optional getByTime(UUID ehrId, OffsetDateTime time, @Nullable String path); + + /** + * Create a new folder for Ehr with id equal ehrId + * + * @param ehrId + * @param folder + * @return + */ + Folder create(UUID ehrId, Folder folder); + + /** + * Update the folder for Ehr with id equal ehrId + * + * @param ehrId + * @param folder + * @param ifMatches expected version before update for optimistic looking + * @return + */ + Folder update(UUID ehrId, Folder folder, ObjectVersionId ifMatches); + + /** + * delete the folder for Ehr with id equal ehrId + * + * @param ehrId + * @param ifMatches expected version before delete for optimistic looking + */ + void delete(UUID ehrId, ObjectVersionId ifMatches); + + /** + * Physical delete a folder with all History + * + * @param ehrId + * @param folderId + */ + void adminDeleteFolder(UUID ehrId, UUID folderId); +} diff --git a/api/src/main/java/org/ehrbase/api/service/EhrService.java b/api/src/main/java/org/ehrbase/api/service/EhrService.java index d8e3c0e914..5aa35c400e 100644 --- a/api/src/main/java/org/ehrbase/api/service/EhrService.java +++ b/api/src/main/java/org/ehrbase/api/service/EhrService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Stefan Spiska (Vitasystems GmbH) and Jake Smolka (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 * - * http://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,165 +15,172 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.service; import com.nedap.archie.rm.changecontrol.OriginalVersion; +import com.nedap.archie.rm.changecontrol.VersionedObject; import com.nedap.archie.rm.datavalues.quantity.datetime.DvDateTime; -import com.nedap.archie.rm.ehr.EhrStatus; -import com.nedap.archie.rm.ehr.VersionedEhrStatus; import com.nedap.archie.rm.generic.RevisionHistory; -import org.ehrbase.api.exception.DuplicateObjectException; -import org.ehrbase.api.exception.InternalServerException; -import org.ehrbase.response.ehrscape.CompositionFormat; -import org.ehrbase.response.ehrscape.EhrStatusDto; - -import java.sql.Timestamp; +import com.nedap.archie.rm.support.identification.ObjectVersionId; +import java.time.OffsetDateTime; import java.util.Optional; import java.util.UUID; +import javax.annotation.Nullable; +import org.ehrbase.api.dto.EhrStatusDto; +import org.ehrbase.api.exception.ObjectNotFoundException; +import org.ehrbase.api.exception.StateConflictException; +import org.ehrbase.api.exception.ValidationException; + +public interface EhrService { + + /** + * Wrapper for {@link #create(UUID, EhrStatusDto)} response that contains the EHR id as well as the + * {@link EhrStatusDto} and it's {@link ObjectVersionId}. This prevents to call {@link #getEhrStatus(UUID)} with an + * additional DB round trip. + * + * @param ehrId ID of the created EHR + * @param statusVersionId the {@link ObjectVersionId} of the @{@link EhrStatusDto} + * @param status initial {@link EhrStatusDto} version + */ + record EhrResult(UUID ehrId, ObjectVersionId statusVersionId, EhrStatusDto status) {} -public interface EhrService extends BaseService { /** * Creates new EHR instance, with default settings and values when no status or ID is supplied. - * @param status Optional, sets custom status + * * @param ehrId Optional, sets custom ID - * @return UUID of new EHR instance - * @throws DuplicateObjectException when given party/subject already has an EHR - * @throws InternalServerException when unspecified error occurs + * @param status Optional, sets custom status + * @return {@link EhrResult} of new EHR instance + * @throws StateConflictException when an EHR with the given id already exist + * @throws ValidationException when given status is invalid, e.g. not a valid openEHR RM object */ - UUID create(EhrStatus status, UUID ehrId); + EhrResult create(@Nullable UUID ehrId, @Nullable EhrStatusDto status); - @Deprecated - Optional getEhrStatusEhrScape(UUID ehrUuid, CompositionFormat format); + /** + * Update the EHR_STATUS linked to the given EHR + * + * @param ehrId ID of linked EHR + * @param status input EHR_STATUS + * @param contribution Optional ID of custom contribution. Can be null. + * @param audit Audit event id + * @return {@link EhrResult} of the updated status + * @throws ObjectNotFoundException if no EHR is found + * @throws ValidationException when given status is invalid, e.g. not a valid openEHR RM object + */ + EhrResult updateStatus(UUID ehrId, EhrStatusDto status, ObjectVersionId targetObjId, UUID contribution, UUID audit); /** * Gets latest EHR_STATUS of the given EHR. + * * @param ehrUuid EHR subject - * @return Latest EHR_STATUS or empty + * @return Latest EHR_STATUS + * @throws ObjectNotFoundException if no EHR is found */ - Optional getEhrStatus(UUID ehrUuid); + EhrResult getEhrStatus(UUID ehrUuid); /** * Gets particular EHR_STATUS matching the given version Uid. - * @param ehrUuid Root EHR + * + * @param ehrUuid Root EHR * @param versionedObjectUid Given Uid of EHR_STATUS - * @param version Given version of EHR_STATUS + * @param version Given version of EHR_STATUS * @return Matching EHR_STATUS or empty + * @throws ObjectNotFoundException if no EHR is found */ - Optional> getEhrStatusAtVersion(UUID ehrUuid, UUID versionedObjectUid, int version); + Optional> getEhrStatusAtVersion(UUID ehrUuid, UUID versionedObjectUid, int version); /** - * Update the EHR_STATUS linked to the given EHR - * @param ehrId ID of linked EHR - * @param status input EHR_STATUS - * @param contribution Optional ID of custom contribution. Can be null. - * @return {@link Optional} containing the updated status on success - * @throws org.ehrbase.api.exception.ObjectNotFoundException when given ehrId cannot be found - * @throws org.ehrbase.api.exception.InvalidApiParameterException when given status is invalid, e.g. not a valid openEHR RM object - */ - Optional updateStatus(UUID ehrId, EhrStatus status, UUID contribution); - - Optional findBySubject(String subjectId, String nameSpace); - - /** - * Checks if there is an ehr entry existing for specified ehrId. + * Search for an EHR_STATUS based on the given subject id and namespace * - * @param ehrId - Target EHR identified - * @return EHR with id exists + * @param subjectId ID of the EHR_STATUS subject + * @param nameSpace of the EHR_STATUS subject + * @return {@link Optional} of the matching EHR_STATUS */ - boolean doesEhrExist(UUID ehrId); + Optional findBySubject(String subjectId, String nameSpace); /** * Get latest version UID of an EHR_STATUS by given associated EHR UID. + * * @param ehrId EHR ID * @return EHR_STATUS version UID + * @throws ObjectNotFoundException if no EHR is found */ - String getLatestVersionUidOfStatus(UUID ehrId); - - DvDateTime getCreationTime(UUID ehrId); + ObjectVersionId getLatestVersionUidOfStatus(UUID ehrId); /** * Get version number of EHR_STATUS associated with given EHR UID at given timestamp. - * @param ehrUid EHR UID + * + * @param ehrUid EHR UID * @param timestamp Timestamp of point in time * @return version number + * @throws ObjectNotFoundException if no EHR is found + */ + ObjectVersionId getEhrStatusVersionByTimestamp(UUID ehrUid, OffsetDateTime timestamp); + + /** + * Provides the creation time of the given EHR id. + * + * @param ehrId ID of the EHR + * @return {@link DvDateTime} of the EHR creation */ - Integer getEhrStatusVersionByTimestamp(UUID ehrUid, Timestamp timestamp); + DvDateTime getCreationTime(UUID ehrId); /** * Return True if a EHR with identifier ehrId exists. * Implements has_ehr from the openEHR Platform Abstract Service Model. + * * @param ehrId identifier to test * @return True when existing, false if not */ boolean hasEhr(UUID ehrId); - /** - * Return True if a EHR_STATUS with identifier statusId exists. - * @param statusId identifier to test - * @return True when existing, false if not - */ - boolean hasStatus(UUID statusId); - - /** - * Helper to get (Versioned Object) Uid of EHR_STATUS of given EHR. - * @param ehrUid Uid of EHR - * @return UUID of corresponding EHR_STATUS - */ - UUID getEhrStatusVersionedObjectUidByEhr(UUID ehrUid); - /** * Gets version container EhrStatus associated with given EHR. - * @param ehrUid Given EHR ID + * + * @param ehrId Given EHR ID * @return Version container object + * @throws ObjectNotFoundException if no EHR is found */ - VersionedEhrStatus getVersionedEhrStatus(UUID ehrUid); + VersionedObject getVersionedEhrStatus(UUID ehrId); /** * Gets revision history of EhrStatus associated with given EHR. - * @param ehrUid Given EHR ID + * + * @param ehrId Given EHR ID * @return Revision history object + * @throws ObjectNotFoundException if no EHR is found */ - RevisionHistory getRevisionHistoryOfVersionedEhrStatus(UUID ehrUid); + RevisionHistory getRevisionHistoryOfVersionedEhrStatus(UUID ehrId); /** - * Reads the EHR entry from database and returns the ID of the root directory entry. + * Admin method to delete an EHR from the DB. See EHRbase Admin API specification for details. * - * @param ehrId - EHR id to find the directory for - * @return UUID of the root directory if existing + * @param ehrId EHR to delete */ - UUID getDirectoryId(UUID ehrId); + void adminDeleteEhr(UUID ehrId); /** - * Removes the directory information from EHR table entry after deletion of the corresponding folder from - * folders table. If there were no such folder it will return a successful deletion. + * Helper to directly get the external subject reference form the linked subject to given EHR. * - * @param ehrId - Target EHR id - * @return Directory entry is now 'null' - */ - boolean removeDirectory(UUID ehrId); - - /** - * Admin method to delete an EHR from the DB. See EHRbase Admin API specification for details. - * @param ehrId EHR to delete + * @param ehrId Given EHR ID + * @return Linked external subject reference or null + * @throws ObjectNotFoundException if no EHR is found */ - void adminDeleteEhr(UUID ehrId); - - void adminPurgePartyIdentified(); - - void adminDeleteOrphanHistory(); + String getSubjectExtRef(String ehrId); /** - * Helper to directly get the linked subject ID to given EHR. - * @param ehrId Given EHR ID - * @return Linked subject ID or null + * Checks if an EHR with the given UUID exists. + * + * @param ehrId EHR ID to check + * @throws ObjectNotFoundException if no EHR is found */ - UUID getSubjectUuid(String ehrId); + void checkEhrExists(UUID ehrId); /** - * Helper to directly get the external subject reference form the linked subject to given EHR. - * @param ehrId Given EHR ID - * @return Linked external subject reference or null + * Checks if the EHR with the given UUID is modifiable. + * + * @param ehrId EHR ID to check + * @throws ObjectNotFoundException if no EHR is found. + * @throws StateConflictException if the EHR is not modifiable. */ - String getSubjectExtRef(String ehrId); + void checkEhrExistsAndIsModifiable(UUID ehrId); } diff --git a/api/src/main/java/org/ehrbase/api/service/FolderService.java b/api/src/main/java/org/ehrbase/api/service/FolderService.java deleted file mode 100644 index 8fbe57c96e..0000000000 --- a/api/src/main/java/org/ehrbase/api/service/FolderService.java +++ /dev/null @@ -1,108 +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. - */ - -package org.ehrbase.api.service; - -import com.nedap.archie.rm.directory.Folder; -import com.nedap.archie.rm.support.identification.ObjectVersionId; -import org.ehrbase.api.exception.ObjectNotFoundException; -import org.ehrbase.response.ehrscape.FolderDto; -import org.ehrbase.response.ehrscape.StructuredString; -import org.ehrbase.response.ehrscape.StructuredStringFormat; - -import java.sql.Timestamp; -import java.util.Optional; -import java.util.UUID; - -public interface FolderService extends BaseService, VersionedObjectService { - - /** - * Retrieves a folder from database identified by object_version_uid and - * extracts the given sub path of existing. If the object_version_uid does - * not contain a version number the latest entry will be returned. A path - * with common unix like root notation '/' will be treated as if there is - * no path specified and the full tree will be returned. - * - * @param folderId - object_version_uid for target folder - * @param path - Optional path to sub folder to extract - * @return FolderDTO for further usage in upper layers - */ - Optional get(ObjectVersionId folderId, String path); - - /** - * Fetches the latest entry from database. This will be - * @param folderId - object_version_uid for target folder - * @param path - Optional path to sub folder to extract - * @return FolderDTO for further usage in other layers - */ - Optional getLatest(ObjectVersionId folderId, String path); - - /** - * Fetches an folder entry from database identified by the root folder uid - * and the given timestamp. If the current version has ben modified after - * the given timestamp the folder will be searched inside the folder history - * table. - * - * @param folderId - object_version_uid for target folder - * @param timestamp - Timestamp of folder version to find - * @param path - Optional path to sub folder to extract - * @return FolderDTO for further usage in other layers - */ - Optional getByTimeStamp(ObjectVersionId folderId, Timestamp timestamp, String path); - - /** - * Serializes folder content from request body into a structured string - * that can be used by database save mechanism to parse in into jsonb - * format that will be saved at the database. - * - * @param folder - Folder to serialize - * @param format - Source format of the folder - * @return Structured string that can be parsed into jsonb - */ - StructuredString serialize(Folder folder, StructuredStringFormat format); - - /** - * Searches the last version number of a given folder entry from database. - * This will be executed against the folder table which contains the latest - * entry of a folder. The version will be extracted from there. - * - * @param folderId - Id of the folder to search for the latest version - * @return Version number of the latest folder entry - * @throws ObjectNotFoundException - Folder entry does not exist - */ - Integer getLastVersionNumber(ObjectVersionId folderId); - - - /** - * Searches for the folder version that was the current version at the - * given timestamp. If the entry from folder table has a later timestamp - * the folder history will be queried to find the version at the timestamp - * - * @param folderId - Id of the folder to search the version - * @param timestamp - Timestamp to look for the version number - * @return - Version number that was actual at the timestamp - * @throws ObjectNotFoundException - Folder entry does not exist at the time - */ - Integer getVersionNumberForTimestamp(ObjectVersionId folderId, Timestamp timestamp); - - /** - * Admin method to delete a Folder from the DB. See EHRbase Admin API specification for details. - * @param folderId Folder to delete - */ - void adminDeleteFolder(UUID folderId); -} diff --git a/api/src/main/java/org/ehrbase/api/service/QueryService.java b/api/src/main/java/org/ehrbase/api/service/QueryService.java deleted file mode 100644 index 1a25530f79..0000000000 --- a/api/src/main/java/org/ehrbase/api/service/QueryService.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2019 Stefan Spiska (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. - */ - -package org.ehrbase.api.service; - -import org.ehrbase.api.definitions.QueryMode; -import org.ehrbase.response.ehrscape.QueryDefinitionResultDto; -import org.ehrbase.response.ehrscape.QueryResultDto; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -public interface QueryService extends BaseService { - /** - * simple query where the full json expression contains both query (key = 'q') and optional - * parameters (key = 'query-parameters') - * @param queryString - * @param queryMode - * @param explain - * @return - */ - QueryResultDto query(String queryString, QueryMode queryMode, boolean explain); - - QueryResultDto query(String queryString, Map parameters, QueryMode queryMode, boolean explain); - - //=== DEFINITION: manage stored queries - List retrieveStoredQueries(String fullyQualifiedName); - - QueryDefinitionResultDto retrieveStoredQuery(String qualifiedName, String version); - - //=== DEFINITION: manage stored queries - QueryDefinitionResultDto createStoredQuery(String qualifiedName, String version, String queryString); - - QueryDefinitionResultDto updateStoredQuery(String qualifiedName, String version, String queryString); - - QueryDefinitionResultDto deleteStoredQuery(String qualifiedName, String version); - - //the audit variables - Map> getAuditResultMap(); -} diff --git a/api/src/main/java/org/ehrbase/api/service/StatusService.java b/api/src/main/java/org/ehrbase/api/service/StatusService.java index 3c126c4e66..29c2bb8341 100644 --- a/api/src/main/java/org/ehrbase/api/service/StatusService.java +++ b/api/src/main/java/org/ehrbase/api/service/StatusService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Axel Siebert (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 * - * http://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, @@ -20,7 +20,7 @@ /** * Status service to get information about the running EHRbase instance */ -public interface StatusService extends BaseService { +public interface StatusService { /** * Returns information on the current operating system this EHRbase instance is running on. @@ -64,9 +64,9 @@ public interface StatusService extends BaseService { String getArchieVersion(); /** - * Returns the current version of openEHR_SDK which has been used to build the running EHRbase instance. + * Returns the current version of ehrbase SDK which has been used to build the running EHRbase instance. * - * @return Current used openEHR_SDK version + * @return Current used ehrbase SDK version */ String getOpenEHR_SDK_Version(); } diff --git a/api/src/main/java/org/ehrbase/api/service/StoredQueryService.java b/api/src/main/java/org/ehrbase/api/service/StoredQueryService.java new file mode 100644 index 0000000000..701052e581 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/service/StoredQueryService.java @@ -0,0 +1,35 @@ +/* + * 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.api.service; + +import java.util.List; +import org.ehrbase.api.exception.ObjectNotFoundException; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.QueryDefinitionResultDto; + +public interface StoredQueryService { + + // === DEFINITION: manage stored queries + List retrieveStoredQueries(String fullyQualifiedName); + + QueryDefinitionResultDto retrieveStoredQuery(String qualifiedName, String version) throws ObjectNotFoundException; + + // === DEFINITION: manage stored queries + QueryDefinitionResultDto createStoredQuery(String qualifiedName, String version, String queryString); + + void deleteStoredQuery(String qualifiedName, String version); +} diff --git a/api/src/main/java/org/ehrbase/api/service/SystemService.java b/api/src/main/java/org/ehrbase/api/service/SystemService.java new file mode 100644 index 0000000000..196043a025 --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/service/SystemService.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.api.service; + +public interface SystemService { + String getSystemId(); +} diff --git a/api/src/main/java/org/ehrbase/api/service/TemplateService.java b/api/src/main/java/org/ehrbase/api/service/TemplateService.java index 045536d0bf..e3e5766f29 100644 --- a/api/src/main/java/org/ehrbase/api/service/TemplateService.java +++ b/api/src/main/java/org/ehrbase/api/service/TemplateService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Stefan Spiska (Vitasystems GmbH) and Jake Smolka (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 * - * http://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,46 +15,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.service; -import org.ehrbase.api.definitions.OperationalTemplateFormat; -import org.ehrbase.response.ehrscape.CompositionFormat; -import org.ehrbase.response.ehrscape.StructuredString; -import org.ehrbase.response.ehrscape.TemplateMetaDataDto; - +import com.nedap.archie.rm.composition.Composition; import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.ehrbase.api.definitions.OperationalTemplateFormat; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.TemplateMetaDataDto; +import org.ehrbase.openehr.sdk.webtemplate.model.WebTemplate; +import org.openehr.schemas.v1.OPERATIONALTEMPLATE; -public interface TemplateService extends BaseService { +public interface TemplateService { List getAllTemplates(); - StructuredString buildExample(String templateId, CompositionFormat format); + Map findAllTemplateIds(); + + Composition buildExample(String templateId); - org.ehrbase.webtemplate.model.WebTemplate findTemplate(String templateId); + WebTemplate findTemplate(String templateId); /** * Finds and returns the given operational template as string represented in requested format. + * * @param templateId - Unique name of operational template * @param format - As enum value from {@link OperationalTemplateFormat} * @return - * @throws RuntimeException When the template couldn't be found, the format isn't support or in case of another error. + * @throws RuntimeException When the template couldn't be found, the format isn't support or in + * case of another error. */ String findOperationalTemplate(String templateId, OperationalTemplateFormat format) throws RuntimeException; - String create(String content); + String create(OPERATIONALTEMPLATE content); /** - * Deletes a given template from storage physically. The template is no longer available. If you try to delete a - * template that is used in at least one Composition Entry or in one history entry the deletion will be rejected. + * Deletes a given template from storage physically. The template is no longer available. If you + * try to delete a template that is used in at least one Composition Entry or in one history entry + * the deletion will be rejected. * * @param templateId - Template id to delete, e.g. "IDCR Allergies List.v0" - * @return - Whether the template could be removed or not */ - boolean adminDeleteTemplate(String templateId); + void adminDeleteTemplate(String templateId); /** - * Replaces a given template in the storage and updates the cache with the new template content. Will be rejected - * if the template has referencing Compositions. + * Replaces a given template in the storage and updates the cache with the new template content. + * Will be rejected if the template has referencing Compositions. * * @param templateId - Tempalte id to update, e.g. "IDCR Allergies List.v0" * @param content - New content to overwrite the template with @@ -63,11 +68,11 @@ public interface TemplateService extends BaseService { String adminUpdateTemplate(String templateId, String content); /** - * Deletes all templates from target template storage and returns the number of deleted templates. If any template - * is referenced by at least one Composition the deletion will be rejected and no template will be removed. + * Deletes all templates from target template storage and returns the number of deleted templates. + * If any template is referenced by at least one Composition the deletion will be rejected and no + * template will be removed. * * @return - Number of deleted templates */ int adminDeleteAllTemplates(); - } diff --git a/api/src/main/java/org/ehrbase/api/service/TerminologyServer.java b/api/src/main/java/org/ehrbase/api/service/TerminologyServer.java index c3c05e839a..9a4ba1ca73 100644 --- a/api/src/main/java/org/ehrbase/api/service/TerminologyServer.java +++ b/api/src/main/java/org/ehrbase/api/service/TerminologyServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Vitasystems GmbH, Hannover Medical School, and Luis Marco-Ruiz (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 * - * http://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, @@ -19,8 +19,7 @@ import java.util.EnumSet; import java.util.List; - -import com.nedap.archie.rm.datavalues.DvCodedText; +import org.apache.commons.lang3.EnumUtils; /*** *@Created by Luis Marco-Ruiz on Feb 12, 2020 @@ -29,83 +28,85 @@ * @param generic id type to specify the data type used for the identifier of the concept in the terminology server of choice. * @param generic type for parameters that are custom to each operation implementation. */ - public interface TerminologyServer { -/** - * Expands the value set identified by the provided ID. - * @param valueSetId - * @return Returns the list of concepts of type T that conform the expansion of the value set. - */ - List expand(ID valueSetId); - - /** - * Expands the value set identified by the provided ID. - * @param valueSetId - * @return Returns the list of concepts of type T that conform the expansion of the value set. - */ - List expandWithParameters(ID valueSetId, @SuppressWarnings("unchecked") U...operationParams);//warning is ignored because the specific implementation will type the method avoiding possible heap pollution - - /** - * Searches all the attributes associated with the concept that corresponds to the provided ID. - * @param conceptId - * @return A complex Object of type T that contains all the attributes directly associated to the concept identified by the provided ID. - */ - T lookUp(ID conceptId); - /** - * Evaluates if the concept provided T belongs to the value set identified by the provided ID. - * @param concept to evaluate. - * @param valueSetId - * @return true if the concept belongs to the specified value set. - */ - Boolean validate(T concept, ID valueSetId); - - /** - * Evaluates if the concept provided as one operationParams belongs to the value set provided as another operationParam. - * @param dynamic list of parameters to perform the operation against an external terminology server. - * @return true if the concept belongs to the specified value set. - */ - Boolean validate(U...operationParams); - /** - * Evaluates if the concept B subsumes concept A. - * @param concept that is subsumed by the concept in the second param. - * @param concept that subsumes the concept in the first param. - * @return {@link org.ehrbase.aql.compiler.tsclient.TerminologyServer.SubsumptionResult} indicating the result of the subsumption evaluation. - */ - SubsumptionResult subsumes(T conceptA, T conceptB); - /** - * - * Possible subsumption evaluation results. - * - */ - public enum SubsumptionResult{ - - EQUIVALENT, SUBSUMES, SUBSUMEDBY, NOTSUBSUMED; - } - - public enum TerminologyAdapter{ - FHIR("hl7.org/fhir/R4"), - OCEAN("OTS.OCEANHEALTHSYSTEMS.COM"), - BETTER("bts.better.care"), - DTS4("dts4.apelon.com"), - INDIZEN("cts2.indizen.com"); - - private String adapterId; - public String getAdapterId() { - return adapterId; - } + /** + * Expands the value set identified by the provided ID. + * @param valueSetId + * @return Returns the list of concepts of type T that conform the expansion of the value set. + */ + List expand(ID valueSetId); + + /** + * Expands the value set identified by the provided ID. + * @param valueSetId + * @return Returns the list of concepts of type T that conform the expansion of the value set. + */ + List expandWithParameters( + ID valueSetId, + @SuppressWarnings("unchecked") + U... operationParams); // warning is ignored because the specific implementation will type the + // method avoiding possible heap pollution + + /** + * Searches all the attributes associated with the concept that corresponds to the provided ID. + * @param conceptId + * @return A complex Object of type T that contains all the attributes directly associated to the concept identified by the provided ID. + */ + T lookUp(ID conceptId); + /** + * Evaluates if the concept provided T belongs to the value set identified by the provided ID. + * @param concept to evaluate. + * @param valueSetId + * @return true if the concept belongs to the specified value set. + */ + Boolean validate(T concept, ID valueSetId); + + /** + * Evaluates if the concept provided as one operationParams belongs to the value set provided as another operationParam. + * @param operationParams dynamic list of parameters to perform the operation against an external terminology server. + * @return true if the concept belongs to the specified value set. + */ + Boolean validate(U... operationParams); + /** + * Evaluates if the concept B subsumes concept A. + * @param conceptA concept that is subsumed by the concept in the second param. + * @param conceptB concept that subsumes the concept in the first param. + * @return {@link TerminologyServer.SubsumptionResult} indicating the result of the subsumption evaluation. + */ + SubsumptionResult subsumes(T conceptA, T conceptB); + /** + * + * Possible subsumption evaluation results. + * + */ + public enum SubsumptionResult { + EQUIVALENT, + SUBSUMES, + SUBSUMEDBY, + NOTSUBSUMED; + } + + enum TerminologyAdapter { + FHIR("hl7.org/fhir/R4"), + OCEAN("OTS.OCEANHEALTHSYSTEMS.COM"), + BETTER("bts.better.care"), + DTS4("dts4.apelon.com"), + INDIZEN("cts2.indizen.com"); + + private String adapterId; + + public String getAdapterId() { + return adapterId; + } + + private static EnumSet supportedAdapters = EnumSet.of(FHIR); + + TerminologyAdapter(String adapterId) { + this.adapterId = adapterId; + } - private static EnumSet supportedAdapters = EnumSet.of(FHIR); - - private TerminologyAdapter(String adapterId) { - this.adapterId = adapterId; - } - - public static boolean isAdapterSupported(String adapterToCheck) { - for(TerminologyAdapter ta: supportedAdapters) { - if(ta.name().equals(adapterToCheck)) - {return true;} } - return false; - } - } - + public static boolean isAdapterSupported(String adapterToCheck) { + return EnumUtils.isValidEnum(TerminologyAdapter.class, adapterToCheck); + } + } } diff --git a/api/src/main/java/org/ehrbase/api/service/ValidationService.java b/api/src/main/java/org/ehrbase/api/service/ValidationService.java index 8af9b5c6f9..1e4b33f946 100644 --- a/api/src/main/java/org/ehrbase/api/service/ValidationService.java +++ b/api/src/main/java/org/ehrbase/api/service/ValidationService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 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 * - * http://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,11 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.service; import com.nedap.archie.rm.composition.Composition; -import com.nedap.archie.rm.ehr.EhrStatus; +import javax.annotation.Nonnull; +import org.ehrbase.api.dto.EhrStatusDto; +import org.ehrbase.openehr.sdk.response.dto.ContributionCreateDto; /** * ValidationService @@ -32,29 +33,27 @@ */ public interface ValidationService { - /** - * check a composition based on the operation template constraints - * - * @param templateID the template Id (String) - * @param composition the RM composition - * @throws Exception if the validation fails or the template cannot be resolved - */ - void check(String templateID, Composition composition) throws Exception; - - /** - * initially check if the composition is valid for further processing - * - * @param composition - * @throws IllegalArgumentException - */ - void check(Composition composition) throws Exception; + /** + * Initially check if the composition is valid for further processing. + * + * @param composition to validate + * @throws IllegalArgumentException in case the given composition is invalid. + */ + void check(Composition composition); - /** - * initially check if ehrstatus is valid for further processing - * - * @param ehrStatus - * @throws IllegalArgumentException - */ - void check(EhrStatus ehrStatus); + /** + * Initially check if ehrStatus is valid for further processing. + * + * @param ehrStatus to validate + * @throws IllegalArgumentException in case the given ehrStatus is invalid. + */ + void check(@Nonnull EhrStatusDto ehrStatus); + /** + * Initially check if contribution is valid for further processing. + * + * @param contribution to validate + * @throws IllegalArgumentException in case the given contribution is invalid. + */ + void check(ContributionCreateDto contribution); } diff --git a/api/src/main/java/org/ehrbase/api/service/VersionedObjectService.java b/api/src/main/java/org/ehrbase/api/service/VersionedObjectService.java index 6056ee6e9c..65283124fa 100644 --- a/api/src/main/java/org/ehrbase/api/service/VersionedObjectService.java +++ b/api/src/main/java/org/ehrbase/api/service/VersionedObjectService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Jake Smolka (Hannover Medical School) and Vitasystems GmbH. + * 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 * - * http://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,108 +15,122 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.ehrbase.api.service; import com.nedap.archie.rm.archetyped.Locatable; import com.nedap.archie.rm.support.identification.ObjectVersionId; import java.util.Optional; import java.util.UUID; +import org.ehrbase.openehr.sdk.response.dto.ehrscape.FolderDto; /** * Service layer interface for versioned openEHR objects.
* Helps to always handle the necessary metadata and streamlines C(R)UD operations.
* Retrieval is * @param Class of target versioned object, e.g. {@link com.nedap.archie.rm.directory.Folder}. - * @param Class of return value of create and update methods. For instance, {@link org.ehrbase.response.ehrscape.FolderDto}. Will be wrapped in an {@link Optional}. + * @param Class of return value of create and update methods. For instance, {@link FolderDto}. Will be wrapped in an {@link Optional}. */ public interface VersionedObjectService { - /** - * Creation with given audit meta-data. Will create a new ad-hoc contribution. - * @param ehrId EHR ID of context - * @param objData Payload object data - * @param systemId Audit system ID - * @param committerId Audit committer ID - * @param description Optional audit description text - * @return {@link T} typed response wrapped in {@link Optional} - */ - Optional create(UUID ehrId, T objData, UUID systemId, UUID committerId, String description); - - /** - * Creation with a given contribution, and its audit meta-data. - * @param ehrId EHR ID of context - * @param objData Payload object data - * @param contribution Contribution for operation - * @return {@link T} typed response wrapped in {@link Optional} - */ - Optional create(UUID ehrId, T objData, UUID contribution); + /** + * Creation with given audit meta-data. Will create a new ad-hoc contribution. + * @param ehrId EHR ID of context + * @param objData Payload object data + * @param systemId Audit system ID + * @param committerId Audit committer ID + * @param description Optional audit description text + * @return {@link T} typed response wrapped in {@link Optional} + */ + @Deprecated(forRemoval = true) + default Optional create(UUID ehrId, T objData, UUID systemId, UUID committerId, String description) { + throw new UnsupportedOperationException(); + } - /** - * Creation with default audit meta-data. Will create a new ad-hoc contribution. - * @param ehrId EHR ID of context - * @param objData Payload object data - * @return {@link T} typed response wrapped in {@link Optional} - */ - Optional create(UUID ehrId, T objData); + /** + * Creation with a given contribution, and its audit meta-data. + * + * @param ehrId EHR ID of context + * @param objData Payload object data + * @param contribution Contribution for operation + * @param audit + * @return {@link T} typed response wrapped in {@link Optional} + */ + Optional create(UUID ehrId, T objData, UUID contribution, UUID audit); - /** - * Update with given audit meta-data. Will create a new ad-hoc contribution. - * @param ehrId EHR ID of context - * @param targetObjId ID of target object - * @param objData Payload object data - * @param systemId Audit system ID - * @param committerId Audit committer ID - * @param description Optional audit description text - * @return {@link T} typed response wrapped in {@link Optional} - */ - Optional update(UUID ehrId, ObjectVersionId targetObjId, T objData, UUID systemId, UUID committerId, String description); + /** + * Creation with default audit meta-data. Will create a new ad-hoc contribution. + * @param ehrId EHR ID of context + * @param objData Payload object data + * @return {@link T} typed response wrapped in {@link Optional} + */ + Optional create(UUID ehrId, T objData); - /** - * Update with a given contribution, and its audit meta-data. - * @param ehrId EHR ID of context - * @param targetObjId ID of target object - * @param objData Payload object data - * @param contribution Contribution for operation - * @return {@link T} typed response wrapped in {@link Optional} - */ - Optional update(UUID ehrId, ObjectVersionId targetObjId, T objData, UUID contribution); + /** + * Update with given audit meta-data. Will create a new ad-hoc contribution. + * @param ehrId EHR ID of context + * @param targetObjId ID of target object + * @param objData Payload object data + * @param systemId Audit system ID + * @param committerId Audit committer ID + * @param description Optional audit description text + * @return {@link T} typed response wrapped in {@link Optional} + */ + @Deprecated(forRemoval = true) + default Optional update( + UUID ehrId, ObjectVersionId targetObjId, T objData, UUID systemId, UUID committerId, String description) { + throw new UnsupportedOperationException(); + } - /** - * Update with default audit meta-data. Will create a new ad-hoc contribution. - * @param ehrId EHR ID of context - * @param targetObjId ID of target object - * @param objData Payload object data - * @return {@link T} typed response wrapped in {@link Optional} - */ - Optional update(UUID ehrId, ObjectVersionId targetObjId, T objData); + /** + * Update with a given contribution, and its audit meta-data. + * + * @param ehrId EHR ID of context + * @param targetObjId ID of target object + * @param objData Payload object data + * @param contribution Contribution for operation + * @param audit + * @return {@link T} typed response wrapped in {@link Optional} + */ + Optional update(UUID ehrId, ObjectVersionId targetObjId, T objData, UUID contribution, UUID audit); - /** - * Deletion with given audit meta-data. Will create a new ad-hoc contribution. - * @param ehrId EHR ID of context - * @param targetObjId ID of target object - * @param systemId Audit system ID - * @param committerId Audit committer ID - * @param description Optional audit description text - * @return True if successful - */ - boolean delete(UUID ehrId, ObjectVersionId targetObjId, UUID systemId, UUID committerId, String description); + /** + * Update with default audit meta-data. Will create a new ad-hoc contribution. + * @param ehrId EHR ID of context + * @param targetObjId ID of target object + * @param objData Payload object data + * @return {@link T} typed response wrapped in {@link Optional} + */ + Optional update(UUID ehrId, ObjectVersionId targetObjId, T objData); - /** - * Deletion with a given contribution, and its audit meta-data. - * @param ehrId EHR ID of context - * @param targetObjId ID of target object - * @param contribution Contribution for operation - * @return True if successful - */ - boolean delete(UUID ehrId, ObjectVersionId targetObjId, UUID contribution); + /** + * Deletion with given audit meta-data. Will create a new ad-hoc contribution. + * + * @param ehrId EHR ID of context + * @param targetObjId ID of target object + * @param systemId Audit system ID + * @param committerId Audit committer ID + * @param description Optional audit description text + */ + @Deprecated(forRemoval = true) + default void delete(UUID ehrId, ObjectVersionId targetObjId, UUID systemId, UUID committerId, String description) { + throw new UnsupportedOperationException(); + } - /** - * Deletion with default audit meta-data. Will create a new ad-hoc contribution. - * @param ehrId EHR ID of context - * @param targetObjId ID of target object - * @return True if successful - */ - boolean delete(UUID ehrId, ObjectVersionId targetObjId); + /** + * Deletion with a given contribution, and its audit meta-data. + * + * @param ehrId EHR ID of context + * @param targetObjId ID of target object + * @param contribution Contribution for operation + * @param audit + */ + void delete(UUID ehrId, ObjectVersionId targetObjId, UUID contribution, UUID audit); + /** + * Deletion with default audit meta-data. Will create a new ad-hoc contribution. + * + * @param ehrId EHR ID of context + * @param targetObjId ID of target object + */ + void delete(UUID ehrId, ObjectVersionId targetObjId); } diff --git a/api/src/main/java/org/ehrbase/api/service/experimental/ItemTagService.java b/api/src/main/java/org/ehrbase/api/service/experimental/ItemTagService.java new file mode 100644 index 0000000000..5b968a80ed --- /dev/null +++ b/api/src/main/java/org/ehrbase/api/service/experimental/ItemTagService.java @@ -0,0 +1,83 @@ +/* + * 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.api.service.experimental; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.ehrbase.api.dto.experimental.ItemTagDto; +import org.ehrbase.api.dto.experimental.ItemTagDto.ItemTagRMType; + +/** + * Service manages the ITEM_TAG + * Class. + *
+ * All operations on tags are implemented as bulk operations. + */ +public interface ItemTagService { + + /** + * Performs a bulk update/create operation for the given ItemTag into the tag list + * of the owner. + * + * @param ownerId Identifier of owner object, such as EHR. + * @param targetId VERSIONED_OBJECT<T> Identifier of target. + * @param targetType Type of the target object. + * @param itemTagsDto Content of the ItemTag containing the key, value parameter. + * @return tagUUIDs of the update/create ItemTags + */ + List bulkUpsert( + @Nonnull UUID ownerId, + @Nonnull UUID targetId, + @Nonnull ItemTagRMType targetType, + @Nonnull List itemTagsDto); + + /** + * Performs a bulk get operation for the given ItemTag IDs and/or + * keys. + * + * @param ownerId Identifier of owner object, such as EHR. + * @param targetId VERSIONED_OBJECT<T> Identifier of target. + * @param targetType Type of the target object. + * @param ids Identifier ItemTag to search for. + * @param keys ItemTag keys to search for. + * @return tags Matching the ownerId, targetId and optional ids, keys + */ + List findItemTag( + @Nonnull UUID ownerId, + @Nonnull UUID targetId, + @Nonnull ItemTagRMType targetType, + @Nonnull Collection ids, + @Nonnull Collection keys); + + /** + * Performs a bulk delete operation for the given ItemTag ids. This method + * will simply return in cases where the given IDs does not exist. + * + * @param ownerId Identifier of owner object, such as EHR. + * @param targetId VERSIONED_OBJECT<T> Identifier of target. + * @param targetType Type of the target object. + * @param ids Identifier ItemTag to delete. + */ + void bulkDelete( + @Nonnull UUID ownerId, + @Nonnull UUID targetId, + @Nonnull ItemTagRMType targetType, + @Nonnull Collection ids); +} diff --git a/api/src/main/java/org/ehrbase/api/util/VersionUidHelper.java b/api/src/main/java/org/ehrbase/api/util/VersionUidHelper.java deleted file mode 100644 index ba33ee8c3f..0000000000 --- a/api/src/main/java/org/ehrbase/api/util/VersionUidHelper.java +++ /dev/null @@ -1,117 +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. - */ - -package org.ehrbase.api.util; - - -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class VersionUidHelper { - - public static final Pattern UUID_PATTERN = Pattern.compile("^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})"); - public static final Pattern VERSION_PATTERN = Pattern.compile("::(\\d+)$"); - public static final Pattern SYSTEM_ID_PATTERN = Pattern.compile("::(\\w+.\\w+.\\w+)"); - public static final Pattern VERSION_UID_PATTERN = - Pattern.compile( - UUID_PATTERN.toString() + - SYSTEM_ID_PATTERN.toString() + - VERSION_PATTERN.toString() - ); - - private UUID uuid; - private String systemId; - private int version; - - public VersionUidHelper(String versionUid) { - if (!VERSION_UID_PATTERN.matcher(versionUid).matches()) { - throw new IllegalArgumentException("Version uid " + versionUid + " is not a valid version_uid,"); - } - this.uuid = extractUUID(versionUid); - this.systemId = extractSystemId(versionUid); - this.version = extractVersion(versionUid); - } - - public static boolean isVersionUid(String testString) { - return VERSION_UID_PATTERN.matcher(testString).matches(); - } - - public static boolean isUUID(String testString) { - return UUID_PATTERN.matcher(testString).matches(); - } - - public static boolean isSystemId(String testString) { - return SYSTEM_ID_PATTERN.matcher(testString).matches(); - } - - public static boolean isVersion(String testString) { - return VERSION_PATTERN.matcher(testString).matches(); - } - - - public static UUID extractUUID(String versionUid) { - Matcher matcher = UUID_PATTERN.matcher(versionUid); - if (matcher.find()) { - return UUID.fromString(matcher.group(1)); - } - return null; - } - - - public static String extractSystemId(String versionUid) { - Matcher matcher = SYSTEM_ID_PATTERN.matcher(versionUid); - if (matcher.find()) { - return matcher.group(1); - } - return null; - } - - - public static int extractVersion(String versionUid) { - Matcher matcher = VERSION_PATTERN.matcher(versionUid); - if (matcher.find()) { - return Integer.parseInt(matcher.group(1)); - } - return 1; - } - - public String toString() { - return this.uuid.toString() + "::" + this.systemId + "::" + this.version; - } - - public UUID getUuid() { - return this.uuid; - } - public void setUuid(UUID uuid) { - this.uuid = uuid; - } - public String getSystemId() { - return systemId; - } - public void setSystemId(String systemId) { - this.systemId = systemId; - } - public int getVersion() { - return version; - } - public void setVersion(int version) { - this.version = version; - } - -} diff --git a/api/src/test/java/org/ehrbase/api/util/VersionUidHelperTest.java b/api/src/test/java/org/ehrbase/api/util/VersionUidHelperTest.java deleted file mode 100644 index d714c12853..0000000000 --- a/api/src/test/java/org/ehrbase/api/util/VersionUidHelperTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.ehrbase.api.util; - -import java.util.UUID; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - - -public class VersionUidHelperTest { - - static final String VALID_VERSION_UID = "1234abcd-5678-ef12-ab34-cd56ef78ab90::test.ehrbase.org::10"; - - @Test - public void acceptsValidVersionUid() { - assertThat(VersionUidHelper.isVersionUid(VALID_VERSION_UID)).isTrue(); - } - - @Test - public void rejectsInvalidVersionUid() { - String testString = "1234invalid"; - assertThat(VersionUidHelper.isVersionUid(testString)).isFalse(); - } - - @Test - public void acceptsValidUUID() { - String testString = "abcd1234-ef56-ab78-cd90-ef12ab34cd56"; - assertThat(VersionUidHelper.isUUID(testString)).isTrue(); - } - - @Test - public void rejectsInvalidUUID() { - String testString = "invalid-uuid"; - assertThat(VersionUidHelper.isUUID(testString)).isFalse(); - } - - @Test - public void acceptsValidSystemId() { - String testString = "::test123.ehrbase.org"; - assertThat(VersionUidHelper.isSystemId(testString)).isTrue(); - } - - @Test - public void rejectsInvalidSystemId() { - String testString = "this/is/not/a/valid/system/id"; - assertThat(VersionUidHelper.isSystemId(testString)).isFalse(); - } - - @Test - public void acceptsValidVersion() { - String testString = "::562"; - assertThat(VersionUidHelper.isVersion(testString)).isTrue(); - } - - @Test - public void rejectsInvalidVersion() { - String testString = "::invalid123"; - assertThat(VersionUidHelper.isVersion(testString)).isFalse(); - } - - @Test - public void extractsUUID() { - UUID expected = UUID.fromString("1234abcd-5678-ef12-ab34-cd56ef78ab90"); - assertThat(VersionUidHelper.extractUUID(VALID_VERSION_UID)).isEqualTo(expected); - } - - @Test - public void extractsSystemId() { - String expected = "test.ehrbase.org"; - assertThat(VersionUidHelper.extractSystemId(VALID_VERSION_UID)).isEqualTo(expected); - } - - @Test - public void extractsVersion() { - int expected = 10; - assertThat(VersionUidHelper.extractVersion(VALID_VERSION_UID)).isEqualTo(expected); - } -} \ No newline at end of file diff --git a/application/Dockerfile b/application/Dockerfile deleted file mode 100644 index 6a7b9d3f90..0000000000 --- a/application/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM adoptopenjdk:11-jre-openj9 -ARG JAR_FILE -ENV AUTH_TYPE="BASIC" -ENV AUTH_USER="ehrbase-user" -ENV AUTH_PASSWORD="SuperSecretPassword" -COPY application/target/${JAR_FILE} app.jar -RUN mkdir -p file_repo/knowledge/archetypes && mkdir -p file_repo/knowledge/operational_templates && mkdir -p file_repo/knowledge/templates -EXPOSE 8080 -ENTRYPOINT ["java","-Dspring.profiles.active=docker", "-Dsecurity.authType=${AUTH_TYPE}", "-Dsecurity.authUser=${AUTH_USER}", "-Dsecurity.authPassword=${AUTH_PASSWORD}" ,"-jar","/app.jar"] diff --git a/application/pom.xml b/application/pom.xml index 36bc6df005..fb75f93fb9 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -1,7 +1,7 @@ - - 4.0.0 + 4.0.0 - - org.ehrbase.openehr - server - 0.20.0-SNAPSHOT - + + org.ehrbase.openehr + server + 2.13.0-SNAPSHOT + - application + 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-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 - - - org.springdoc - springdoc-openapi-ui - - - 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-oauth2-client - test - - - 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 + + + org.ehrbase.openehr + configuration + + + org.ehrbase.openehr + cli + + + + + ehrbase + + + + src/main/resources + true + + + + + + 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 4d2c97c36c..dd7292e340 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 Stefan Spiska (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 * - * http://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,30 +15,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - 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.security.servlet.SecurityAutoConfiguration; -import org.springframework.context.annotation.Import; -@SpringBootApplication(exclude = { - SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class -}) -@Import({ - ServiceModuleConfiguration.class, - RestEHRScapeModuleConfiguration.class, - RestModuleConfiguration.class, -}) public class EhrBase { - public static void main(String[] args) { - SpringApplication.run(EhrBase.class, args); - } + public static void main(String[] 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 0fe4fb9060..0000000000 --- a/application/src/main/java/org/ehrbase/application/abac/AbacConfig.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2021 Jake Smolka (Hannover Medical School) and 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. - */ - -package org.ehrbase.application.abac; - -import com.fasterxml.jackson.databind.ObjectMapper; -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; - -import java.io.IOException; -import java.net.URI; -import java.util.Map; - -@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 738d70ac12..0000000000 --- a/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2021 Jake Smolka (Hannover Medical School) and 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. - */ - -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 c55945be4f..0000000000 --- a/application/src/main/java/org/ehrbase/application/abac/CustomMethodSecurityExpressionRoot.java +++ /dev/null @@ -1,547 +0,0 @@ -/* - * Copyright (c) 2021 Jake Smolka (Hannover Medical School) and 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. - */ - -package org.ehrbase.application.abac; - -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 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.response.ehrscape.CompositionDto; -import org.ehrbase.response.ehrscape.CompositionFormat; -import org.ehrbase.response.openehr.OriginalVersionResponseData; -import org.ehrbase.rest.BaseController; -import org.ehrbase.rest.openehr.OpenehrQueryController; -import org.springframework.http.HttpStatus; -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.core.Authentication; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; - -/** - * Implementation of custom security expression, to be used in e.g. @PreAuthorize(..) to allow ABAC - * requests. - */ -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 - * @throws InterruptedException On error while communicating with the ABAC server - */ - public boolean checkAbacPost(String type, String subject, Object payload, - String contentType) - throws IOException, InterruptedException { - - return checkAbac(type, subject, payload, contentType, POST); - } - - public boolean checkAbacPostQuery(Object payload) - throws IOException, InterruptedException { - - return checkAbac(OpenehrQueryController.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 - * @throws InterruptedException On error while communicating with the ABAC server - */ - public boolean checkAbacPre(String type, String subject, Object payload, - String contentType) - throws IOException, InterruptedException { - - // @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, InterruptedException { - - 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 - * @throws InterruptedException On error while communicating with the ABAC server - */ - private boolean checkAbac(String type, String subject, Object payload, - String contentType, String authType) throws IOException, InterruptedException { - // 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.authentication); - - // 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 jwt Token - * @param requestMap ABAC request attribute map to add the result - */ - private void organizationHandling(JwtAuthenticationToken jwt, Map requestMap) { - if (jwt.getTokenAttributes().containsKey(abacConfig.getOrganizationClaim())) { - String orgaId = (String) jwt.getTokenAttributes().get(abacConfig.getOrganizationClaim()); - requestMap.put(ORGANIZATION, orgaId); - } else { - // organization configured but claim not available - throw new IllegalArgumentException("ABAC use of an organization claim is configured but " - + "can't be retrieved from the given JWT."); - } - } - - /** - * Handles patient ID extraction. Either uses token's claim or EHR's subject. - * @param jwt Token - * @param subject Subject from EHR - * @param requestMap ABAC request attribute map to add the result - */ - private boolean patientHandling(JwtAuthenticationToken jwt, String subject, - Map requestMap, String type, Object payload) { - - if (!jwt.getTokenAttributes().containsKey(abacConfig.getPatientClaim())) { - throw new IllegalArgumentException("ABAC: Patient parameter configured, but no claim " - + "attribute available."); - } - String tokenPatient = (String) jwt.getTokenAttributes().get(abacConfig.getPatientClaim()); - - if (type.equals(BaseController.QUERY)) { - // special case of type QUERY, where multiple subjects are possible - if (payload instanceof Map) { - if (((Map) payload).containsKey(AuditVariables.EHR_PATH)) { - Set ehrs = (Set) ((Map) payload).get(AuditVariables.EHR_PATH); - Set patientSet = new HashSet<>(); - for (UUID ehr : ehrs) { - String subjectId = ehrService.getSubjectExtRef(ehr.toString()); - // check if patient token is available and if it matches OR internal reference is null - if (tokenPatient.equals(subjectId) || subjectId == null) { - // matches OR EHR's external ref is null, so add our subject from token - patientSet.add(tokenPatient); - } else { - // doesn't match -> requesting data for patient X with token for patient Y - return false; - } - - } - // put result set into the requestMap and exit - requestMap.put(PATIENT, patientSet); - return true; - } else { - throw new InternalServerException("ABAC: AQL audit patient data unavailable."); - } - } else { - throw new InternalServerException("ABAC: AQL audit patient data malformed."); - } - } - - // in all other cases just handle the one String "subject" variable - // check if matches (to block accessing patient X with token from patient Y) OR null reference - if (tokenPatient.equals(subject) || subject == null) { - // matches OR EHR's external ref is null, so add our subject from token - requestMap.put(PATIENT, tokenPatient); - } else { - // doesn't match -> requesting data for patient X with token for patient Y - return false; - } - - return true; - } - - /** - * 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) - */ - 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 (((ResponseEntity) payload).hasBody()) { - Object body = ((ResponseEntity) payload).getBody(); - // can have "No content" here (even with some data in the body) if the compo was (logically) deleted - if (((ResponseEntity) payload).getStatusCode().equals(HttpStatus.NO_CONTENT)) { - if (body instanceof Map) { - Object error = ((Map) body).get("error"); - if (error != null) { - if (((String) error).contains("delet")) { - //composition was deleted, so nothing to check here, skip - break; - } - } - } - throw new InternalServerException("ABAC: Unexpected empty response from composition reuquest"); - } - if (body instanceof OriginalVersionResponseData) { - // case of versioned_composition --> fast path, because template is easy to get - if (((OriginalVersionResponseData) body).getData() instanceof Composition) { - String template = Objects.requireNonNull( - ((Composition) ((OriginalVersionResponseData) body).getData()) - .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 { - throw new InternalServerException("ABAC: unexpected empty response body"); - } - } 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(compositionUid, null); - if (compoDto.isPresent()) { - Composition c = compoDto.get().getComposition(); - 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"); - } - } - - private boolean abacCheckRequest(String url, Map bodyMap) - throws IOException, InterruptedException { - // 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 - * @throws InterruptedException On error during HTTP handling - */ - private boolean sendRequestForEach(String type, String url, Map bodyMap, - Map request) throws IOException, InterruptedException { - 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 JwtAuthenticationToken getJwtAuthenticationToken(Authentication auth) { - JwtAuthenticationToken jwt; - if (auth instanceof JwtAuthenticationToken) { - jwt = (JwtAuthenticationToken) auth; - } else { - throw new IllegalArgumentException("ABAC: Invalid authentication, no JWT available."); - } - return jwt; - } - - @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 a18cb87274..0000000000 --- a/application/src/main/java/org/ehrbase/application/abac/MethodSecurityConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2021 Jake Smolka (Hannover Medical School) and 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. - */ - -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 0000000000..8bf902a44e --- /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/HttpClientConfig.java b/application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java deleted file mode 100644 index 53a2d7131c..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java +++ /dev/null @@ -1,80 +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. - */ - - -package org.ehrbase.application.config; - -import java.net.InetSocketAddress; -import java.net.ProxySelector; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpClient.Redirect; -import java.net.http.HttpClient.Version; -import java.time.Duration; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "httpclient") -public class HttpClientConfig { - - private HttpClient client; - - private URI proxy; - private int proxyPort; - - /** - * General HTTP client with central configuration. - */ - public HttpClient getClient() { - if (this.client == null) { - var builder = HttpClient.newBuilder() - .version(Version.HTTP_2) - .followRedirects(Redirect.NEVER) - .connectTimeout(Duration.ofSeconds(20)); - - if (proxy != null && proxyPort != 0) { - builder.proxy(ProxySelector.of(new InetSocketAddress(proxy.toString(), proxyPort))); - } - - // TODO: allow configuration of authentication - //builder.authenticator(Authenticator.getDefault()); - - this.client = builder.build(); - } - return client; - } - - public URI getProxy() { - return proxy; - } - - public void setProxy(URI proxy) { - this.proxy = proxy; - } - - public int getProxyPort() { - return proxyPort; - } - - public void setProxyPort(int proxyPort) { - this.proxyPort = proxyPort; - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java b/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java deleted file mode 100644 index eaca874549..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2019 Stefan Spiska (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. - */ - -package org.ehrbase.application.config; - -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.mapper.StructuredStringJSonSerializer; -import org.ehrbase.response.ehrscape.StructuredString; -import org.ehrbase.serialisation.mapper.RmObjectJsonDeSerializer; -import org.ehrbase.serialisation.mapper.RmObjectJsonSerializer; -import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; - -@Configuration -public class JacksonConfiguration { - - @Bean - public Jackson2ObjectMapperBuilderCustomizer addCustomSerialization() { - return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder - .serializerByType(StructuredString.class, new StructuredStringJSonSerializer()) - .serializerByType(RMObject.class, new RmObjectJsonSerializer()) - .deserializerByType(EhrStatus.class, new RmObjectJsonDeSerializer()) - .deserializerByType(Folder.class, new RmObjectJsonDeSerializer()) - .modules(new JavaTimeModule()); - } - - @Bean - @Primary - public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter( - Jackson2ObjectMapperBuilder builder) { - XmlMapper objectMapper = builder.createXmlMapper(true).build(); - objectMapper.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true); - return new MappingJackson2XmlHttpMessageConverter( - objectMapper); - } -} \ No newline at end of file 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 9b6e6e9200..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/ServerConfigImp.java +++ /dev/null @@ -1,120 +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 - * - * 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. - */ -package org.ehrbase.application.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; - -@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(); - } - - @Override - public Boolean getUseJsQuery() { - return aqlConfig.getUseJsQuery(); - } - - @Override - public void setUseJsQuery(boolean b) { - aqlConfig.setUseJsQuery(b); - } - - public AqlConfig getAqlConfig() { - return aqlConfig; - } - - public void setAqlConfig(AqlConfig aqlConfig) { - this.aqlConfig = aqlConfig; - } - - public static class AqlConfig { - - private Boolean useJsQuery; - private String ignoreIterativeNodeList; - private Integer iterationScanDepth = 1; - - public Boolean getUseJsQuery() { - return useJsQuery; - } - - public String getIgnoreIterativeNodeList() { - return ignoreIterativeNodeList; - } - - public Integer getIterationScanDepth() { - return iterationScanDepth; - } - - public void setUseJsQuery(Boolean useJsQuery) { - this.useJsQuery = useJsQuery; - } - - 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/SwaggerConfiguration.java b/application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java deleted file mode 100644 index 628eec5bf2..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.ehrbase.application.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 org.springdoc.core.GroupedOpenApi; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class SwaggerConfiguration { - - @Bean - public GroupedOpenApi openEhrApi() { - return GroupedOpenApi.builder() - .group("1. openEHR API") - .pathsToMatch("/rest/openehr/**") - .build(); - } - - @Bean - public GroupedOpenApi ehrScapeApi() { - return GroupedOpenApi.builder() - .group("2. EhrScape API") - .pathsToMatch("/rest/ecis/**") - .build(); - } - - @Bean - public GroupedOpenApi statusApi() { - return GroupedOpenApi.builder() - .group("3. EHRbase Status Endpoint") - .pathsToMatch("/rest/status") - .build(); - } - - @Bean - public GroupedOpenApi adminApi() { - return GroupedOpenApi.builder() - .group("4. EHRbase Admin API") - .pathsToMatch("/rest/admin/**") - .build(); - } - - @Bean - public GroupedOpenApi actuatorApi() { - return GroupedOpenApi.builder() - .group("5. Management API") - .pathsToMatch("/management/**") - .build(); - } - - @Bean - public OpenAPI ehrBaseOpenAPI() { - return new OpenAPI() - .info( - new Info() - .title("EHRbase API") - .description("EHRbase implements the [official openEHR REST API](https://specifications.openehr.org/releases/ITS-REST/latest/) and " - + "a subset of the [EhrScape API](https://www.ehrscape.com/). " - + "Additionally, EHRbase provides a custom `status` heartbeat endpoint, " - + "an [Admin API](https://ehrbase.readthedocs.io/en/latest/03_development/07_admin/index.html) (if activated) " - + "and a [Status and Metrics API](https://ehrbase.readthedocs.io/en/latest/03_development/08_status_and_metrics/index.html?highlight=status) (if activated) " - + "for monitoring and maintenance. " - + "Please select the definition in the top right." - + " " - + "Note: The openEHR REST API and the EhrScape API are documented in their official documentation, not here. Please refer to their separate documentation.") - .version("v1") - .license( - new License() - .name("Apache 2.0") - .url("https://github.com/ehrbase/ehrbase/blob/develop/LICENSE.md"))) - .externalDocs( - new ExternalDocumentation() - .description("EHRbase Documentation") - .url("https://ehrbase.readthedocs.io/")); - } -} 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 542e54ddec..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/cache/CacheConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2021 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. - */ - -package org.ehrbase.application.config.cache; - -import org.ehrbase.cache.CacheOptions; -import org.ehrbase.service.KnowledgeCacheService; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cache.annotation.EnableCaching; -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()); - return options; - } - - @Bean - @ConditionalOnProperty(prefix = "cache", name = "init-on-startup", havingValue = "true") - public CacheInitializer cacheInitializer(KnowledgeCacheService knowledgeCacheService) { - return new CacheInitializer(knowledgeCacheService); - } -} diff --git a/application/src/main/java/org/ehrbase/application/config/cache/CacheInitializer.java b/application/src/main/java/org/ehrbase/application/config/cache/CacheInitializer.java deleted file mode 100644 index d8d5c1afdc..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/cache/CacheInitializer.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2021-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. - */ - -package org.ehrbase.application.config.cache; - -import javax.annotation.PostConstruct; -import org.ehrbase.service.KnowledgeCacheService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Initializes caches during application startup. - * - * @author Renaud Subiger - * @since 1.0.0 - */ -public class CacheInitializer { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private final KnowledgeCacheService knowledgeCacheService; - - public CacheInitializer(KnowledgeCacheService knowledgeCacheService) { - this.knowledgeCacheService = knowledgeCacheService; - } - - @PostConstruct - public void initialize() { - logger.info("Initializing EHRbase caches"); - knowledgeCacheService.initializeCaches(); - } -} 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 8fbc23c782..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/cache/CacheProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2021 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. - */ - -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/client/HttpClientConfiguration.java b/application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java deleted file mode 100644 index d9de537376..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java +++ /dev/null @@ -1,98 +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. - */ - -package org.ehrbase.application.config.client; - -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.HttpClient; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.conn.ssl.TrustAllStrategy; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.ssl.SSLContextBuilder; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.ResourceUtils; - -import javax.net.ssl.SSLContext; -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; - -/** - * {@link Configuration} for Apache HTTP Client. - */ -@Configuration -@EnableConfigurationProperties(HttpClientProperties.class) -@SuppressWarnings("java:S6212") -public class HttpClientConfiguration { - - @Bean - public HttpClient httpClient(HttpClientProperties properties) throws UnrecoverableKeyException, CertificateException, - NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { - - HttpClientBuilder builder = HttpClients.custom(); - - if (properties.getSsl().isEnabled()) { - builder.setSSLContext(buildSSLContext(properties.getSsl())); - builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); - } - - if (properties.getProxy().getHost() != null && properties.getProxy().getPort() != null) { - builder.setProxy(new HttpHost(properties.getProxy().getHost(), properties.getProxy().getPort())); - - if (properties.getProxy().getUsername() != null && properties.getProxy().getPassword() != null) { - UsernamePasswordCredentials credentials = - new UsernamePasswordCredentials(properties.getProxy().getUsername(), properties.getProxy().getPassword()); - BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(AuthScope.ANY, credentials); - builder.setDefaultCredentialsProvider(credentialsProvider); - } - } - - return builder.build(); - } - - private SSLContext buildSSLContext(HttpClientProperties.Ssl properties) throws UnrecoverableKeyException, CertificateException, - NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { - - SSLContextBuilder builder = SSLContextBuilder.create(); - - if (properties.getKeyStoreType() != null) { - builder.setKeyStoreType(properties.getKeyStoreType()); - } - builder.loadKeyMaterial(ResourceUtils.getFile(properties.getKeyStore()), - properties.getKeyStorePassword().toCharArray(), - properties.getKeyPassword().toCharArray()); - - if (properties.getTrustStoreType() != null) { - builder.setKeyStoreType(properties.getTrustStoreType()); - } - builder.loadTrustMaterial(ResourceUtils.getFile(properties.getTrustStore()), - properties.getTrustStorePassword().toCharArray(), TrustAllStrategy.INSTANCE); - - return builder.build(); - } -} 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 c1ec1b8c7e..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/BasicAuthSecurityConfiguration.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2021-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. - */ - -package org.ehrbase.application.config.security; - -import static org.ehrbase.application.config.security.SecurityProperties.ADMIN; -import static org.ehrbase.application.config.security.SecurityProperties.USER; - -import javax.annotation.PostConstruct; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -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.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; - -/** - * {@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 extends WebSecurityConfigurerAdapter { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private final SecurityProperties properties; - - public BasicAuthSecurityConfiguration(SecurityProperties securityProperties) { - this.properties = securityProperties; - } - - @PostConstruct - public void initialize() { - logger.info("Using basic authentication"); - } - - @Override - public void configure(AuthenticationManagerBuilder auth) throws Exception { - // @formatter:off - auth - .inMemoryAuthentication() - .withUser(properties.getAuthUser()) - .password("{noop}" + properties.getAuthPassword()) - .roles(USER) - .and() - .withUser(properties.getAuthAdminUser()) - .password("{noop}" + properties.getAuthAdminPassword()) - .roles(ADMIN); - // @formatter:on - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - http - .cors() - .and() - .csrf() - .ignoringAntMatchers("/rest/**") - .and() - .authorizeRequests() - .antMatchers("/rest/admin/**", "/management/**").hasRole(ADMIN) - .anyRequest().hasAnyRole(ADMIN, USER) - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .httpBasic(); - // @formatter:on - } -} 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 b0489f151e..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/NoOpSecurityConfiguration.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2021-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. - */ - -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 ddfab5812a..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/OAuth2SecurityConfiguration.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2021-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. - */ - -package org.ehrbase.application.config.security; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import javax.annotation.PostConstruct; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; -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.configuration.WebSecurityConfigurerAdapter; -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; - -/** - * {@link Configuration} for OAuth2 authentication. - * - * @author Jake Smolka - * @since 1.0.0 - */ -@Configuration -@ConditionalOnProperty(prefix = "security", name = "auth-type", havingValue = "oauth") -@EnableWebSecurity -public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter { - - public static final String PROFILE_SCOPE = "PROFILE"; - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private final SecurityProperties securityProperties; - - private final OAuth2ResourceServerProperties oAuth2rProperties; - - public OAuth2SecurityConfiguration(SecurityProperties securityProperties, - OAuth2ResourceServerProperties oAuth2rProperties) { - this.securityProperties = securityProperties; - this.oAuth2rProperties = oAuth2rProperties; - } - - @PostConstruct - public void initialize() { - logger.info("Using OAuth2 authentication"); - logger.debug("Using issuer URI: {}", oAuth2rProperties.getJwt().getIssuerUri()); - logger.debug("Using user role: {}", securityProperties.getOauth2UserRole()); - logger.debug("Using admin role: {}", securityProperties.getOauth2AdminRole()); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - String userRole = securityProperties.getOauth2UserRole(); - String adminRole = securityProperties.getOauth2AdminRole(); - - // @formatter:off - http - .cors() - .and() - .authorizeRequests() - .antMatchers("/rest/admin/**", "/management/**").hasRole(adminRole) - .anyRequest().hasAnyRole(adminRole, userRole, PROFILE_SCOPE) - .and() - .oauth2ResourceServer() - .jwt() - .jwtAuthenticationConverter(getJwtAuthenticationConverter()); - // @formatter:on - } - - // 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) - .collect(Collectors.toList())); - } - - if (jwt.getClaims().containsKey("scope")) { - authority.addAll(Arrays.stream(jwt.getClaims().get("scope").toString().split(" ")) - .map(roleName -> "ROLE_" + roleName.toUpperCase()).map(SimpleGrantedAuthority::new) - .collect(Collectors.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 79c34b0f76..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/SecurityConfiguration.java +++ /dev/null @@ -1,30 +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 - * - * 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. - */ - -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/security/SecurityProperties.java b/application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java deleted file mode 100644 index 8d485b3128..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java +++ /dev/null @@ -1,124 +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 - * - * 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. - */ -package org.ehrbase.application.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. - */ - private AuthTypes authType; - - /** - * Username. - */ - private String authUser; - - /** - * Password for the user. - */ - private String authPassword; - - /** - * Admin username. - */ - private String authAdminUser; - - /** - * Password for the admin user. - */ - private String authAdminPassword; - - /** - * User role name used with OAuth2 authentication type. - */ - private String oauth2UserRole; - - /** - * Admin role name used with OAuth2 authentication type. - */ - private String oauth2AdminRole; - - public AuthTypes getAuthType() { - return authType; - } - - public void setAuthType(AuthTypes authType) { - this.authType = authType; - } - - public String getAuthUser() { - return authUser; - } - - public void setAuthUser(String authUser) { - this.authUser = authUser; - } - - public String getAuthPassword() { - return authPassword; - } - - public void setAuthPassword(String authPassword) { - this.authPassword = authPassword; - } - - public String getAuthAdminUser() { - return authAdminUser; - } - - public void setAuthAdminUser(String authAdminUser) { - this.authAdminUser = authAdminUser; - } - - public String getAuthAdminPassword() { - return authAdminPassword; - } - - public void setAuthAdminPassword(String authAdminPassword) { - this.authAdminPassword = authAdminPassword; - } - - public String getOauth2UserRole() { - return oauth2UserRole; - } - - public void setOauth2UserRole(String oauth2UserRole) { - this.oauth2UserRole = oauth2UserRole.toUpperCase(); - } - - public String getOauth2AdminRole() { - return oauth2AdminRole; - } - - public void setOauth2AdminRole(String oauth2AdminRole) { - this.oauth2AdminRole = oauth2AdminRole.toUpperCase(); - } - - public enum AuthTypes { - NONE, BASIC, OAUTH - } -} 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 f227f9c5a9..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/validation/ValidationConfiguration.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2021-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. - */ - -package org.ehrbase.application.config.validation; - -import java.util.Map; -import org.apache.http.client.HttpClient; -import org.ehrbase.validation.terminology.ExternalTerminologyValidation; -import org.ehrbase.validation.terminology.ExternalTerminologyValidationChain; -import org.ehrbase.validation.terminology.FhirTerminologyValidation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -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 -@ConditionalOnProperty(name = "validation.external-terminology.enabled", havingValue = "true") -@EnableConfigurationProperties(ValidationProperties.class) -@SuppressWarnings("java:S6212") -public class ValidationConfiguration { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private final ValidationProperties properties; - - private final HttpClient httpClient; - - public ValidationConfiguration(ValidationProperties properties, HttpClient httpClient) { - this.properties = properties; - this.httpClient = httpClient; - } - - @Bean - public ExternalTerminologyValidation externalTerminologyValidator() { - 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/validation/ValidationProperties.java b/application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java deleted file mode 100644 index b703a5bab6..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.ehrbase.application.config.validation; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.HashMap; -import java.util.Map; - -/** - * {@link ConfigurationProperties} for external terminology validation. - */ -@ConfigurationProperties(prefix = "validation.external-terminology") -public class ValidationProperties { - - private boolean enabled = false; - - private boolean failOnError = false; - - private final Map provider = new HashMap<>(); - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public boolean isFailOnError() { - return failOnError; - } - - public void setFailOnError(boolean failOnError) { - this.failOnError = failOnError; - } - - public Map getProvider() { - return provider; - } - - public enum ProviderType { - - FHIR - } - - public static class Provider { - - private ProviderType type; - - private String url; - - public ProviderType getType() { - return type; - } - - public void setType(ProviderType type) { - this.type = type; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - } -} 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 26dad5c30f..0000000000 --- a/application/src/main/java/org/ehrbase/application/config/web/WebConfiguration.java +++ /dev/null @@ -1,87 +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. - */ - -package org.ehrbase.application.config.web; - -import org.ehrbase.api.service.CompositionService; -import org.ehrbase.api.service.EhrService; -import org.ehrbase.application.util.IsoDateTimeConverter; -import org.ehrbase.rest.openehr.audit.CompositionAuditInterceptor; -import org.ehrbase.rest.openehr.audit.EhrAuditInterceptor; -import org.ehrbase.rest.openehr.audit.QueryAuditInterceptor; -import org.openehealth.ipf.commons.audit.AuditContext; -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.WebMvcConfigurer; - -/** - * {@link Configuration} from Spring Web MVC. - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(CorsProperties.class) -public class WebConfiguration implements WebMvcConfigurer { - - private final CorsProperties properties; - - private final AuditContext auditContext; - - private final EhrService ehrService; - - private final CompositionService compositionService; - - public WebConfiguration(CorsProperties properties, AuditContext auditContext, - EhrService ehrService, CompositionService compositionService) { - this.properties = properties; - this.auditContext = auditContext; - this.ehrService = ehrService; - this.compositionService = compositionService; - } - - @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) { - if (auditContext.isAuditEnabled()) { - // Composition endpoint - registry - .addInterceptor(new CompositionAuditInterceptor(auditContext, ehrService, compositionService)) - .addPathPatterns("/rest/openehr/v1/**/composition/**"); - // Ehr endpoint - registry - .addInterceptor(new EhrAuditInterceptor(auditContext, ehrService)) - .addPathPatterns("/rest/openehr/v1/ehr", "/rest/openehr/v1/ehr/*"); - // Query endpoint - registry - .addInterceptor(new QueryAuditInterceptor(auditContext, ehrService)) - .addPathPatterns("/rest/openehr/v1/query/**"); - } - } -} 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 0000000000..e5559057a1 --- /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/java/org/ehrbase/application/web/LoggingContextFilter.java b/application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java deleted file mode 100644 index 12805d58c9..0000000000 --- a/application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java +++ /dev/null @@ -1,62 +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. - */ - -package org.ehrbase.application.web; - -import java.io.IOException; -import java.util.UUID; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.slf4j.MDC; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -/** - * 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) -public class LoggingContextFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) - throws ServletException, IOException { - - try { - MDC.put("traceId", generateId()); - logger.trace("Set traceId for current request"); - - filterChain.doFilter(request, response); - } finally { - MDC.remove("traceId"); - } - } - - private String generateId() { - return UUID.randomUUID().toString(); - } -} diff --git a/application/src/main/resources/application-cloud.yml b/application/src/main/resources/application-cloud.yml deleted file mode 100644 index fcdba00efe..0000000000 --- a/application/src/main/resources/application-cloud.yml +++ /dev/null @@ -1,29 +0,0 @@ -spring: - datasource: - url: jdbc:postgresql://localhost:5432/ehrbase - username: ehrbase - password: ehrbase - hikari: - maximum-pool-size: 50 - max-lifetime: 1800000 - minimum-idle: 10 - -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 10e0552948..0000000000 --- a/application/src/main/resources/application-docker.yml +++ /dev/null @@ -1,43 +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 - -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 diff --git a/application/src/main/resources/application-local.yml b/application/src/main/resources/application-local.yml deleted file mode 100644 index 97da688f5a..0000000000 --- a/application/src/main/resources/application-local.yml +++ /dev/null @@ -1,53 +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 - password: ehrbase - tomcat: - maxIdle: 10 - max-active: 50 - max-wait: 10000 - -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 ef979602dd..0000000000 --- a/application/src/main/resources/application.yml +++ /dev/null @@ -1,208 +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: - jcache: - config: classpath:ehcache.xml - - 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 - - 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.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 - - # Option to disable strict invariant validation. - # disable-strict-validation: true - - -terminology-server: - tsUrl: 'https://r4.ontoserver.csiro.au/fhir/' - codePath: '$["expansion"]["contains"][*]["code"]' - systemPath: '$["expansion"]["contains"][*]["system"]' - displayPath: '$["expansion"]["contains"][*]["display"]' - -# 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' - # 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: - # Enable / disable prometheus endpoint - enabled: false - # Metrics settings - metrics: - export: - prometheus: - enabled: true - -# Audit Properties -ipf: - atna: - audit-enabled: false - -# External Terminology Validation Properties -validation: - external-terminology: - enabled: false - -# SSL Properties (used by Spring WebClient and Apache HTTP Client) -client: - ssl: - enabled: false - -# JavaMelody -javamelody: - enabled: false \ No newline at end of file diff --git a/application/src/main/resources/banner-cli.txt b/application/src/main/resources/banner-cli.txt new file mode 100644 index 0000000000..4b5d029f60 --- /dev/null +++ b/application/src/main/resources/banner-cli.txt @@ -0,0 +1 @@ +${AnsiColor.BLUE}EHRbase CLI (Spring Boot ${spring-boot.version} EHRbase @project.version@ https://ehrbase.org/)${AnsiBackground.DEFAULT}${AnsiColor.DEFAULT} \ No newline at end of file diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt index 842966cbae..aaea7d2c91 100644 --- a/application/src/main/resources/banner.txt +++ b/application/src/main/resources/banner.txt @@ -6,5 +6,5 @@ ${AnsiColor.BLUE} | |____| | | | | \ \| |_) | (_| \__ \ __/ |______|_| |_|_| \_\_.__/ \__,_|___/\___| Spring Boot ${spring-boot.version} -EHRbase ${application.formatted-version} +EHRbase @project.version@ https://ehrbase.org/ ${AnsiBackground.DEFAULT}${AnsiColor.DEFAULT} \ No newline at end of file diff --git a/application/src/main/resources/ehcache.xml b/application/src/main/resources/ehcache.xml deleted file mode 100644 index 5bbc8b860c..0000000000 --- a/application/src/main/resources/ehcache.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - java.util.UUID - org.ehrbase.webtemplate.model.WebTemplate - - - - org.ehrbase.aql.containment.TemplateIdQueryTuple - org.ehrbase.aql.containment.JsonPathQueryResult - - - - org.ehrbase.aql.containment.TemplateIdAqlTuple - org.ehrbase.aql.sql.queryimpl.ItemInfo - - - - java.lang.String - java.util.List - - - - - - - - 300 - 400 - - - - - - - - - 200 - 400 - - - \ 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 0000000000..684e11c78d 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 0000000000..d84a25a295 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 0000000000..7232b8bf53 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 0000000000..416ad41cc6 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 0000000000..e8ba74a888 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 0000000000..6c5d991539 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 0000000000..0a49b0e9a7 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 0000000000..2c11459ef9 --- /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 511d5be6b3..0000000000 --- a/application/src/test/java/org/ehrbase/application/abac/AbacIntegrationTest.java +++ /dev/null @@ -1,437 +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. - */ - -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.test_data.composition.CompositionTestDataCanonicalJson; -import org.ehrbase.test_data.operationaltemplate.OperationalTemplateTestData; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.junit.runner.RunWith; -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.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.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.ResourceUtils; - -@RunWith(SpringRunner.class) -@SpringBootTest -@ActiveProfiles({"local", "test"}) -@EnabledIfEnvironmentVariable(named = "EHRBASE_ABAC_IT_TEST", matches = "true") -@AutoConfigureMockMvc -class AbacIntegrationTest { - - private static final String ORGA_ID = "f47bfc11-ec8d-412e-aebf-c6953cc23e7d"; - - @MockBean - private AbacConfig.AbacCheck abacCheck; - - @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"); - }}); - - } -} \ No newline at end of file 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 861be79ba7..0000000000 --- a/application/src/test/java/org/ehrbase/application/abac/FlywayTestConfiguration.java +++ /dev/null @@ -1,17 +0,0 @@ -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 98d13ada4d..0000000000 --- a/application/src/test/java/org/ehrbase/application/cors/CorsBasicAuthIT.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.ehrbase.application.cors; - -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; - -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; - -@SpringBootTest(properties = { - "security.authType=basic", - "spring.cache.type=simple" -}) -@AutoConfigureMockMvc -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 422c1664f2..0000000000 --- a/application/src/test/java/org/ehrbase/application/cors/CorsNoAuthIT.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.ehrbase.application.cors; - -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; - -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; - -@SpringBootTest(properties = { - "spring.cache.type=simple" -}) -@AutoConfigureMockMvc -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 0000000000..96aa580ae3 --- /dev/null +++ b/aql-engine/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + org.ehrbase.openehr + server + 2.13.0-SNAPSHOT + + + 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.springframework.boot + spring-boot + + + + + org.apache.commons + commons-lang3 + + + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + + client + org.ehrbase.openehr.sdk + test + + + org.ehrbase.openehr.sdk + test-data + test + + + org.springframework.boot + spring-boot-starter-test + test + + + net.java + quickcheck + 0.6 + test + + + diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlConfigurationProperties.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlConfigurationProperties.java new file mode 100644 index 0000000000..9a897a9a12 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlConfigurationProperties.java @@ -0,0 +1,36 @@ +/* + * 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.openehr.aqlengine; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * AQL features that can be optionally enabled. + * + *

    + *
  • pg-llj-workaround Enables fix for an old postgresql bug where filters in lateral left joins inside a left join are not respected, default: true
  • + *
  • experimental.aql-on-folder.enabled if enabled allow to query EHR FOLDER using AQL, default: false
  • + *
+ */ +@ConfigurationProperties(prefix = "ehrbase.aql") +public record AqlConfigurationProperties(boolean pgLljWorkaround, Experimental experimental) { + public record Experimental(AqlOnFolder aqlOnFolder) { + + public record AqlOnFolder(boolean enabled) {} + } +} 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 0000000000..c56cd87e16 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlEngineModuleConfiguration.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; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@Configuration +@ComponentScan +@EnableAspectJAutoProxy +@EnableConfigurationProperties(AqlConfigurationProperties.class) +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 0000000000..be027faa35 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacement.java @@ -0,0 +1,566 @@ +/* + * 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.Iterator; +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.Consumer; +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, parameterMap)); + ObjectPathParams.replaceParameters(identifiedPath.getPath(), parameterMap) + .ifPresent(identifiedPath::setPath); + } + + /** + * @param operand + * @param parameterMap + * @return a Stream of new primitive, if the operand needs to be replaced + */ + private static Stream replaceOperandParameters(Operand operand, Map parameterMap) { + + return switch (operand) { + case QueryParameter qp -> resolveParameters(qp, parameterMap); + case IdentifiedPath path -> { + replaceIdentifiedPathParameters(path, parameterMap); + yield null; + } + case SingleRowFunction func -> { + replaceFunctionParameters(func, parameterMap); + yield null; + } + case TerminologyFunction __ -> null; + case Primitive __ -> null; + }; + } + + private static void replaceFunctionParameters(SingleRowFunction func, Map parameterMap) { + Utils.reviseList(func.getOperandList(), o -> replaceOperandParameters(o, parameterMap)); + } + + private static Stream resolveParameters(QueryParameter param, Map parameterMap) { + String paramName = param.getName(); + Object paramValue = parameterMap.get(paramName); + + return switch (paramValue) { + case Collection c -> c.stream().map(e -> toPrimitive(param.getName(), e)); + case null -> throw new AqlParseException("Missing parameter '%s'".formatted(paramName)); + default -> Stream.of(toPrimitive(param.getName(), paramValue)); + }; + } + + private static Primitive toPrimitive(String name, Object paramValue) { + return switch (paramValue) { + case null -> throw new AqlParseException("Missing parameter '%s'".formatted(name)); + 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(name)); + } + }; + } + + 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); + ensureSingleElement(replaceOperandParameters(c.getValue(), parameterMap), 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 Stream replaceMatchesParameters( + MatchesOperand operand, Map parameterMap) { + if (operand instanceof QueryParameter qp) { + return resolveParameters(qp, parameterMap); + } else { + return null; + } + } + + 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, parameterMap)); + replaceParameters(cce.getContains(), parameterMap); + } + + private static void replaceContainmentVersionExpressionParameters( + ContainmentVersionExpression cve, Map parameterMap) { + Optional.of(cve) + .map(ContainmentVersionExpression::getPredicate) + .ifPresent(op -> ObjectPathParams.replaceComparisonOperatorParameters(op, 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 object 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.of((PathPredicateOperand) ensureSingleElement( + resolveParameters(qp, parameterMap), p -> validateParameterSyntax(n.getPath(), p))); + 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)
  • + *
+ * + * @param op + * @param parameterMap + */ + public static void replaceComparisonOperatorParameters( + ComparisonOperatorPredicate op, Map parameterMap) { + Optional newPath = replaceParameters(op.getPath(), parameterMap); + + Object newValue = + switch (op.getValue()) { + case null -> throw new NullPointerException( + "Missing value for path " + op.getPath().render()); + case QueryParameter qp -> ensureSingleElement( + resolveParameters(qp, parameterMap), p -> validateParameterSyntax(op.getPath(), p)); + case Primitive __ -> null; + case AqlObjectPath path -> replaceParameters(path, parameterMap); + default -> throw new IllegalArgumentException("Unexpected type of value: " + + op.getValue().getClass().getSimpleName()); + }; + + newPath.ifPresent(op::setPath); + if (newValue != null) { + op.setValue((PathPredicateOperand) newValue); + } + } + } + + 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 new entries, the old one is replaced. + * + * @param list + * @param replacementFunc + * @param + */ + public static void reviseList(List list, Function> replacementFunc) { + if (list.isEmpty()) { + return; + } + final ListIterator li = list.listIterator(); + while (li.hasNext()) { + Stream replacementsStream = replacementFunc.apply(li.next()); + if (replacementsStream != null) { + li.remove(); + replacementsStream.forEach(li::add); + } + } + if (list.isEmpty()) { + throw new AqlParseException("Parameter replacement resulted in empty operand list"); + } + } + + /** + * 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)); + } + } + + /** + * Makes sure that if singleElementStream is not null, it contains exactly one element. + * The element is presented to the elementConsumer and then returned. + * + * @param singleElementStream + * @param elementConsumer + * @return + * @param + */ + private static T ensureSingleElement(Stream singleElementStream, Consumer elementConsumer) { + if (singleElementStream == null) { + return null; + } + Iterator it = singleElementStream.iterator(); + if (it.hasNext()) { + T first = it.next(); + if (it.hasNext()) { + throw new AqlParseException("One of the parameters does not support multiple values"); + } + elementConsumer.accept(first); + return first; + } else { + throw new AqlParseException("Empty parameter replacement results"); + } + } +} 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 0000000000..65f91af01d --- /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 0000000000..bf71de5f08 --- /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 0000000000..1b4a2ae5d8 --- /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 0000000000..3443df82b7 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslFromCreator.java @@ -0,0 +1,429 @@ +/* + * 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.field.AslFolderItemIdVirtualField; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslAbstractJoinCondition; +import org.ehrbase.openehr.aqlengine.asl.model.join.AslFolderItemJoinCondition; +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.Containment; +import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression; +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; + } + } + + AslSourceRelation parentType = Optional.ofNullable(currentParent) + .map(AslStructureQuery::getType) + .orElse(null); + AslSourceRelation sourceRelation = getSourceRelation(usedWrapper, currentParent); + + boolean requiresVersionJoin; + if (isOriginalVersion || parentType == AslSourceRelation.EHR) { + requiresVersionJoin = true; + } + // In case we have FOLDER CONTAINS COMPOSITION c it could be that the c/uid/value is selected. In such cases + // EncapsulatingQueryUtils.sqlSelectFieldForExtractedColumn uses the VO_ID and adds the COMP_VERSION.SYS_VERSION + // field what is only available in the comp_version table. + else if (parentType == AslSourceRelation.FOLDER && sourceRelation == AslSourceRelation.COMPOSITION) { + requiresVersionJoin = true; + } else if (currentParent != null || sourceRelation == AslSourceRelation.EHR) { + 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) { + + final AslJoin join; + if (joinParent == null || container.getChildren().isEmpty()) { + join = null; + } else { + JoinType joinType = asLeftJoin ? JoinType.LEFT_OUTER_JOIN : JoinType.JOIN; + join = new AslJoin(joinParent, joinType, toAdd, aslJoinCondition(toAdd, joinParent)); + } + container.addChild(toAdd, join); + } + + private static AslAbstractJoinCondition aslJoinCondition(AslStructureQuery toAdd, AslStructureQuery joinParent) { + + AslSourceRelation parentType = joinParent.getType(); + AslSourceRelation targetType = toAdd.getType(); + if (parentType == AslSourceRelation.FOLDER && targetType == AslSourceRelation.COMPOSITION) { + return new AslFolderItemJoinCondition(joinParent, joinParent, targetType, toAdd, toAdd); + } + return new AslDescendantCondition(parentType, joinParent, joinParent, targetType, toAdd, toAdd) + .provideJoinCondition(); + } + + 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.EHR_STATUS.equals(rmType) || RmConstants.COMPOSITION.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)); + } + + // (Only) for FOLDER containing COMPOSITIONs we include the data items/id/value as complex extracted column + Containment containment = nextDesc.containment().getContains(); + if (RmConstants.FOLDER.equals(nextDesc.getRmType()) + && containment instanceof ContainmentClassExpression cs + && Objects.equals(cs.getType(), RmConstants.COMPOSITION)) { + fields.add(new AslFolderItemIdVirtualField()); + } + } + 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 0000000000..4e76b23417 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AslPathCreator.java @@ -0,0 +1,738 @@ +/* + * 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,FOLDER,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 0000000000..1e8ec53975 --- /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 0000000000..1483891be8 --- /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 0000000000..8cd9778e26 --- /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 0000000000..23ac2b5c02 --- /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 0000000000..a638e2aa03 --- /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 0000000000..e080b1ff78 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/AslStructureColumn.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.openehr.aqlengine.asl.model; + +import static org.ehrbase.jooq.pg.Tables.COMP_VERSION; +import static org.ehrbase.jooq.pg.Tables.EHR_FOLDER_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 FOLDER querying + EHR_FOLDER_IDX(EHR_FOLDER_VERSION.EHR_FOLDERS_IDX, 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 0000000000..d69fcc1e8d --- /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 0000000000..b53aacf593 --- /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 0000000000..173ad24d63 --- /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 0000000000..b382d109f8 --- /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 0000000000..2c5957cbc9 --- /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 0000000000..a30ba5e104 --- /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 0000000000..b2935c80d4 --- /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 0000000000..5fab0d0e60 --- /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 0000000000..cdb5a67948 --- /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 0000000000..85ff78cbda --- /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 0000000000..170ea3c5ff --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/condition/AslProvidesJoinCondition.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.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(); + + AslQuery getLeftProvider(); + + AslQuery getRightProvider(); + + 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 0000000000..00d64a1994 --- /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 0000000000..d6e6d69b42 --- /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 0000000000..9cd091924b --- /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 0000000000..cc381e986e --- /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 0000000000..21c7f34d7a --- /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 0000000000..b6d881e986 --- /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 0000000000..48492754f6 --- /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 0000000000..0c52cea69c --- /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/AslFolderItemIdVirtualField.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslFolderItemIdVirtualField.java new file mode 100644 index 0000000000..07595408bf --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/field/AslFolderItemIdVirtualField.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.field; + +import java.util.UUID; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; + +/** + * A virtual field representing a FOLDER.items[].id.value path. + */ +public final class AslFolderItemIdVirtualField extends AslVirtualField { + + private static final String FIELD_NAME = "item_id_value"; + + public AslFolderItemIdVirtualField() { + this(null); + } + + public AslFolderItemIdVirtualField(FieldSource fieldSource) { + super(UUID[].class, fieldSource, null); + } + + @Override + public AslFolderItemIdVirtualField withProvider(AslQuery provider) { + return new AslFolderItemIdVirtualField(fieldSource.withProvider(provider)); + } + + @Override + public AslFolderItemIdVirtualField copyWithOwner(AslQuery owner) { + return new AslFolderItemIdVirtualField(FieldSource.withOwner(owner)); + } + + public String getFieldName() { + return FIELD_NAME; + } + + public String aliasedName() { + return super.aliasedName(FIELD_NAME); + } +} 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 0000000000..363e18a9fb --- /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 0000000000..96c39149dc --- /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 0000000000..04ef41d6e9 --- /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, AslFolderItemIdVirtualField { + 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 0000000000..65edf1e099 --- /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, AslFolderItemJoinCondition { + 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 0000000000..18b0e3eaae --- /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 0000000000..48272d0880 --- /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/AslFolderItemJoinCondition.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslFolderItemJoinCondition.java new file mode 100644 index 0000000000..b15ea1599a --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/model/join/AslFolderItemJoinCondition.java @@ -0,0 +1,54 @@ +/* + * 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.openehr.aqlengine.asl.model.join; + +import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery.AslSourceRelation; + +/** + * Specialized Join condition used to COMPOSITIONs by FOLDER.items[].id.value + */ +public final class AslFolderItemJoinCondition extends AslAbstractJoinCondition { + private final AslQuery leftProvider; + private final AslSourceRelation descendantRelation; + private final AslQuery rightProvider; + + public AslFolderItemJoinCondition( + AslQuery leftProvider, + AslQuery leftOwner, + AslSourceRelation descendantRelation, + AslQuery rightProvider, + AslQuery rightOwner) { + super(leftOwner, rightOwner); + this.leftProvider = leftProvider; + this.descendantRelation = descendantRelation; + this.rightProvider = rightProvider; + } + + public AslQuery getLeftProvider() { + return leftProvider; + } + + public AslSourceRelation descendantRelation() { + return descendantRelation; + } + + public AslQuery rightProvider() { + return rightProvider; + } +} 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 0000000000..9acb34761f --- /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 0000000000..b2edd306a3 --- /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 0000000000..436501bddf --- /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 0000000000..46df743aaf --- /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 0000000000..c0d72972aa --- /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 0000000000..d091716050 --- /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 0000000000..65c6e63127 --- /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 0000000000..54eb1075b5 --- /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 0000000000..d0f3e59114 --- /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 0000000000..9a8c3e1b68 --- /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 0000000000..36df10cb44 --- /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 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; + } + + public boolean isRequiresVersionTableJoin() { + return requiresVersionTableJoin; + } + + public boolean isRepresentsOriginalVersionExpression() { + return representsOriginalVersionExpression; + } + + public void setRepresentsOriginalVersionExpression(boolean representsOriginalVersionExpression) { + this.representsOriginalVersionExpression = representsOriginalVersionExpression; + } +} 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 0000000000..d9cd2af97c --- /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 0000000000..6dfb0fd571 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheck.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.featurecheck; + +import org.ehrbase.api.service.SystemService; +import org.ehrbase.openehr.aqlengine.AqlConfigurationProperties; +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, AqlConfigurationProperties aqlConfigurationProperties) { + this.featureChecks = new FeatureCheck[] { + new FromCheck(systemService, aqlConfigurationProperties), + 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 0000000000..d3834b9e9b --- /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 0000000000..293a4d537b --- /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 0000000000..3fe5510acb --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FeatureCheckUtils.java @@ -0,0 +1,345 @@ +/* + * 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()); + + // TODO CDR-1663 FOLDER.items is not yet supported + if (parentTargetTypes.contains(RmConstants.FOLDER) && "items".equals(attribute)) { + throw new AqlFeatureNotImplementedException("Path FOLDER/items"); + } + + 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 0000000000..93d96ce65f --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/featurecheck/FromCheck.java @@ -0,0 +1,252 @@ +/* + * 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.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.AqlConfigurationProperties; +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; +import org.springframework.util.CollectionUtils; + +final class FromCheck implements FeatureCheck { + + private final SystemService systemService; + + private final AqlConfigurationProperties aqlConfiguration; + + public FromCheck(SystemService systemService, AqlConfigurationProperties aqlConfiguration) { + this.systemService = systemService; + this.aqlConfiguration = aqlConfiguration; + } + + @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 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 (!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())); + } + + // check FOLDERS enabled and contains is supported + if (aqlConfiguration.experimental().aqlOnFolder().enabled()) { + if (structure == StructureRoot.FOLDER + && !CollectionUtils.containsAny( + structureRmTypes, EnumSet.of(StructureRmType.FOLDER, StructureRmType.COMPOSITION))) { + throw new AqlFeatureNotImplementedException( + "FOLDER CONTAINS %s is currently not supported".formatted(nextContainment.getType())); + } + } + // otherwise ensure we are not querying folders + else if (structureRmTypes.contains(StructureRmType.FOLDER)) { + throw new AqlFeatureNotImplementedException("CONTAINS %s is an experimental feature and currently disabled." + .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 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 -> ensureVersionContainmentSupported(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 void ensureVersionContainmentSupported(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 0000000000..9f6c5b80ae --- /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 0000000000..946e888986 --- /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 0000000000..3b9206e012 --- /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 0000000000..82b8ddd714 --- /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 0000000000..4329de46d1 --- /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 0000000000..d40ea642ad --- /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 0000000000..d38d94391d --- /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.EHR_STATUS, RmConstants.COMPOSITION, RmConstants.FOLDER, 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 0000000000..c78e86cd10 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathCohesionAnalysis.java @@ -0,0 +1,347 @@ +/* + * 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) + .findFirst() + .map(p -> (String) ((Primitive) p.getValue()).getValue()) + .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 0000000000..73bcb37479 --- /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 0000000000..d13bbc06b4 --- /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 0000000000..4c6faaf162 --- /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 0000000000..fd9222f4f5 --- /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 0000000000..4a72192a96 --- /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 0000000000..e55e5b376e --- /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 0000000000..6766fc3390 --- /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 0000000000..a67d6d60a6 --- /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 0000000000..76fbe89aa5 --- /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 0000000000..fdc2c164a8 --- /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 0000000000..26f9fb0d5b --- /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 0000000000..f8d7ae1d06 --- /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 0000000000..af0f71e4a3 --- /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 0000000000..b4f1c4d2d9 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/AqlQueryRepository.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.repository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +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); + + AqlSqlResultPostprocessor[] postProcessors; + if (selects.isEmpty()) { + // one column with COUNT: see AqlSqlLayer::addSyntheticSelect + postProcessors = new AqlSqlResultPostprocessor[] {NOOP_POSTPROCESSOR}; + } else { + postProcessors = selects.stream().map(this::getPostProcessor).toArray(AqlSqlResultPostprocessor[]::new); + } + return new PreparedQuery(selectQuery, postProcessors); + } + + public List> executeQuery(PreparedQuery preparedQuery) { + try (Stream stream = preparedQuery.selectQuery.stream()) { + return 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, AqlSqlResultPostprocessor[] postProcessors) { + List resultRow = new ArrayList<>(r.size()); + for (int i = 0; i < r.size(); i++) { + resultRow.add(postProcessors[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 0000000000..ab8212c9fc --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/repository/PreparedQuery.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.repository; + +import java.util.List; +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 AqlSqlResultPostprocessor[] postProcessors; + + public PreparedQuery(SelectQuery selectQuery, AqlSqlResultPostprocessor[] 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 0000000000..7b38f08904 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImp.java @@ -0,0 +1,478 @@ +/* + * 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; + + public 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); + + optimizeQuery(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); + } + } + + public 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; + } + + protected void optimizeQuery(AqlQuery aqlQuery) { + // remove unused FROM EHR + if (aqlQuery.getFrom() instanceof ContainmentClassExpression containment + && RmConstants.EHR.equals(containment.getType()) + && containment.getContains() instanceof ContainmentClassExpression childContainment + && !isReferenced(containment, aqlQuery)) { + aqlQuery.setFrom(childContainment); + } + } + + protected static boolean isReferenced(ContainmentClassExpression containment, AqlQuery aqlQuery) { + if (containment.getPredicates() != null) { + return true; + } + String identifier = containment.getIdentifier(); + if (identifier == null) { + return false; + } else { + return AqlQueryUtils.allIdentifiedPaths(aqlQuery) + .anyMatch(p -> p.getRoot().equals(containment)); + } + } + + private List> executeQuery( + PreparedQuery preparedQuery, AqlQueryWrapper queryWrapper, List nonPrimitiveSelects) { + + List> resultData = aqlQueryRepository.executeQuery(preparedQuery); + List selects = queryWrapper.selects(); + + 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<>(selects.size())) + .toList(); + } + + // Since we do not add primitive value selects to the SQL query, we add them after the query was + // executed + for (int i = 0, s = selects.size(); i < s; 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) { + + String[] columnNames = new String[selectFields.size()]; + Map columns = new LinkedHashMap<>(); + for (int i = 0, s = selectFields.size(); i < s; i++) { + SelectWrapper namePath = selectFields.get(i); + columnNames[i] = + Optional.of(namePath).map(SelectWrapper::getSelectAlias).orElse("#" + i); + columns.put(columnNames[i], namePath.getSelectPath().orElse(null)); + } + + QueryResultDto dto = new QueryResultDto(); + dto.setVariables(columns); + + dto.setResultSet(resultData.stream() + .map(r -> { + ResultHolder fieldMap = new ResultHolder(); + for (int i = 0, s = r.size(); i < s; i++) { + fieldMap.putResult(columnNames[i], r.get(i)); + } + return fieldMap; + }) + .toList()); + return dto; + } + + /** + * Rephrases EHR.status to CONTAINS statements so that they can be handled regularly by the aql engine. + * I.e. SELECT e/ehr_status FROM EHR is rewritten as SELECT s FROM EHR e CONTAINS EHR_STATUS s. + * EHR/composition, EHR/directory, and EHR/folders are not supported because the path syntax implies that the objects are optional (vs. CONTAINS). + */ + static void replaceEhrPaths(AqlQuery aqlQuery) { + replaceEhrPath(aqlQuery, "ehr_status", RmConstants.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 0000000000..b9ee9fb902 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilder.java @@ -0,0 +1,671 @@ +/* + * 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.Collection; +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.UUID; +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.lang3.tuple.Pair; +import org.ehrbase.api.service.TemplateService; +import org.ehrbase.jooq.pg.tables.EhrFolderData; +import org.ehrbase.jooq.pg.util.AdditionalSQLFunctions; +import org.ehrbase.openehr.aqlengine.AqlConfigurationProperties; +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.AslFolderItemIdVirtualField; +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.DbToRmFormat; +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.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.SelectOnConditionStep; +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.stereotype.Component; + +/** + * Builds an SQL query from an ASL query + */ +@Component +public class AqlSqlQueryBuilder { + + private final AqlConfigurationProperties aqlConfigurationProperties; + private final DSLContext context; + private final TemplateService templateService; + private final Optional queryPostProcessor; + + public AqlSqlQueryBuilder( + AqlConfigurationProperties aqlConfigurationProperties, + DSLContext context, + TemplateService templateService, + Optional queryPostProcessor) { + this.aqlConfigurationProperties = aqlConfigurationProperties; + this.context = context; + this.templateService = templateService; + 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 final Map> dataTables = new HashMap<>(); + private final 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 (aqlConfigurationProperties.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, templateService)) + .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"); + case AslFolderItemIdVirtualField __ -> throw new IllegalArgumentException( + "Filtering queries cannot be based on AslFolderItemIdValuesColumnField"); + }; + 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) { + + Map, List> aslFields = + aq.getSelect().stream().collect(Collectors.groupingBy(AslField::getClass)); + + Stream> columnFields = consumeFieldsOfType( + aslFields, + AslColumnField.class, + cf -> ((aq.isRequiresVersionTableJoin() && cf.isVersionTableField()) ? primaryTable : dataTable) + .field(cf.getColumnName()) + .as(cf.getAliasedName())); + + Stream folderFields = + consumeFieldsOfType(aslFields, AslFolderItemIdVirtualField.class, Function.identity()); + + if (!aslFields.isEmpty()) { + throw new IllegalStateException("StructureQueryBase could not handle AslFields of type %s" + .formatted(aslFields.values().stream() + .flatMap(Collection::stream) + .map(Object::getClass) + .map(Class::getSimpleName) + .toList())); + } + + final SelectJoinStep step; + if (hasVersionTable) { + step = structureQueryBaseVersionToDataTable(aq, primaryTable, dataTable, columnFields, folderFields); + } else { + step = structureQueryBaseUsingDataTable(aq, primaryTable, columnFields, folderFields); + } + return step; + } + + /** + * + * select + * "base".*, "fi_uuids" + * from "ehr"."ehr_folder_data" as "base" + * join "ehr"."ehr_folder_data" as "descendant" + * on ( + * "descendant"."ehr_id" = "base"."ehr_id" + * and "descendant"."ehr_folders_idx" = "base"."ehr_folders_idx" + * and "descendant"."num" between "base"."num" and "base"."num_cap" + * ) + * join UNNEST("descendant"."item_uuids") as "fi_uuids" + * on (1=1) + * + */ + private static Pair, List> buildFolderItemIdNestedSelect( + Table dataTable, AslFolderItemIdVirtualField column, boolean subAlias) { + + String fieldName = column.getFieldName(); + + EhrFolderData baseFolderTable = EHR_FOLDER_DATA.as("base"); + EhrFolderData descendantFolderTable = EHR_FOLDER_DATA.as("descendant"); + + // -------------------------------------------------------------- + Table itemsUUIDArrayTable = DSL.unnest(descendantFolderTable.field(EHR_FOLDER_DATA.ITEM_UUIDS)) + .as("fi_uuids"); + + Field itemUUIDs = + DSL.field(DSL.name("fi_uuids").quotedName().toString(), UUID.class, itemsUUIDArrayTable); + // @format:off + // we need all fields at this point + the item id array + SelectOnConditionStep selectOnConditionStep = DSL.select(baseFolderTable.asterisk(), itemUUIDs) + .from(baseFolderTable) + // -- 1st join on folder data where the folders are subfolder of the root one + .join(descendantFolderTable) + .on(descendantFolderTable.EHR_ID.eq(baseFolderTable.EHR_ID)) + .and(descendantFolderTable.EHR_FOLDERS_IDX.eq(baseFolderTable.EHR_FOLDERS_IDX)) + .and(descendantFolderTable.NUM.between(baseFolderTable.NUM, baseFolderTable.NUM_CAP)) + // -- take the item_uuids column, unnest them and then join with the each column + .join(itemsUUIDArrayTable) + .on("1=1"); + + // in case we join using the folder_data table we need to pick a dedicated alias for the items to prevent clashes + Table joinTable = subAlias + ? selectOnConditionStep.asTable(dataTable.getName() + "_items") + : selectOnConditionStep.asTable(dataTable); + + // @format:on + List selectFields = + List.of(FieldUtils.virtualAliasedField(joinTable, itemUUIDs, column, fieldName)); + return Pair.of(joinTable, selectFields); + } + + @Nonnull + private static SelectJoinStep structureQueryBaseVersionToDataTable( + AslStructureQuery aq, + Table primaryTable, + Table dataTable, + Stream> columnFields, + Stream folderFields) { + + return switch (aq.getType()) { + case EHR_STATUS -> DSL.select(columnFields.toArray(SelectFieldOrAsterisk[]::new)) + .from(primaryTable) + .join(dataTable) + .on(primaryTable.field(EHR_STATUS_VERSION.EHR_ID).eq(dataTable.field(EHR_STATUS_DATA.EHR_ID))); + case COMPOSITION -> DSL.select(columnFields.toArray(SelectFieldOrAsterisk[]::new)) + .from(primaryTable) + .join(dataTable) + .on(primaryTable.field(COMP_VERSION.VO_ID).eq(dataTable.field(COMP_DATA.VO_ID))); + case FOLDER -> { + Optional folderItemColumn = folderFields.findFirst(); + + final Condition onCondition = 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))); + + if (folderItemColumn.isEmpty()) { + yield DSL.select(columnFields.toArray(SelectFieldOrAsterisk[]::new)) + .from(primaryTable) + .join(dataTable) + .on(onCondition); + } else { + AslFolderItemIdVirtualField column = folderItemColumn.get(); + Pair, List> tableToSelect = + buildFolderItemIdNestedSelect(dataTable, column, false); + + // we need all fields at this point + the item id array + Table joinTable = tableToSelect.getLeft(); + List selectFields = tableToSelect.getRight(); + + yield DSL.select(Stream.concat(columnFields, selectFields.stream()) + .toArray(SelectFieldOrAsterisk[]::new)) + .from(primaryTable) + .join(joinTable) + .on(onCondition); + } + } + default -> throw new IllegalArgumentException("%s has no version table".formatted(aq.getType())); + }; + } + + @Nonnull + private static SelectJoinStep structureQueryBaseUsingDataTable( + AslStructureQuery aq, + Table primaryTable, + Stream> columnFields, + Stream folderFields) { + + if (aq.getType() == AslSourceRelation.FOLDER) { + + Optional columnField = folderFields.findFirst(); + if (columnField.isPresent()) { + AslFolderItemIdVirtualField column = columnField.get(); + Pair, List> tableToSelect = + buildFolderItemIdNestedSelect(primaryTable, column, true); + + // we need all fields at this point + the item id array + Table joinTable = tableToSelect.getLeft(); + List selectFields = tableToSelect.getRight(); + + // We join by num to reduce the parent child scanning to a single folder + // on "sF_f2_0_data_sq"."num" = "sF_f2_0sq"."num" + // and "sF_f2_0_data_sq"."ehr_id" = "sF_f2_0sq"."ehr_id" + // and "sF_f2_0_data_sq"."ehr_folders_idx" = "sF_f2_0sq"."ehr_folders_idx" + Condition onCondition = primaryTable + .field(EHR_FOLDER_DATA.NUM) + .eq(joinTable.field(EHR_FOLDER_DATA.NUM)) + .and(primaryTable.field(EHR_FOLDER_DATA.EHR_ID).eq(joinTable.field(EHR_FOLDER_DATA.EHR_ID))) + .and(primaryTable + .field(EHR_FOLDER_DATA.EHR_FOLDERS_IDX) + .eq(joinTable.field(EHR_FOLDER_DATA.EHR_FOLDERS_IDX))); + + return DSL.select(Stream.concat(columnFields, selectFields.stream()) + .toArray(SelectFieldOrAsterisk[]::new)) + .from(primaryTable) + .join(joinTable) + .on(onCondition); + } + } + + return DSL.select(columnFields.toArray(SelectFieldOrAsterisk[]::new)).from(primaryTable); + } + + private static Stream consumeFieldsOfType( + Map, List> aslFields, + Class type, + Function mapper) { + return Optional.ofNullable(aslFields.remove(type)).orElseGet(List::of).stream() + .filter(type::isInstance) + .map(type::cast) + .map(mapper); + } + + private static SelectJoinStep structureQueryBaseVersion( + Stream> columnFields, Table primaryTable, Table dataTable, TableField tableField) { + return DSL.select(columnFields.toArray(SelectFieldOrAsterisk[]::new)) + .from(primaryTable) + .join(dataTable) + .on(Objects.requireNonNull(primaryTable.field(tableField)).eq(dataTable.field(tableField))); + } + + /** + * 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), type) + .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, AslSourceRelation type) { + + Field keyField = DSL.substring( + dataTable.field(COMP_DATA.ENTITY_IDX), + DSL.length(baseEntityIndex).plus(DSL.inline(1))); + Field dataField = dataTable.field(COMP_DATA.DATA); + + Field valueField; + if (type == AslSourceRelation.FOLDER) { + Field uuidsField = dataTable.field(EhrFolderData.EHR_FOLDER_DATA.ITEM_UUIDS); + valueField = DSL.case_() + .when(DSL.cardinality(uuidsField).eq(DSL.inline(0)), dataField) + .else_(AdditionalSQLFunctions.jsonb_set( + dataField, + AdditionalSQLFunctions.array_to_jsonb(uuidsField), + DbToRmFormat.FOLDER_ITEMS_UUID_ARRAY_ALIAS)); + } else { + valueField = dataField; + } + return DSL.jsonbObjectAgg(keyField, valueField); + } +} 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 0000000000..56b81a10cd --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/ConditionUtils.java @@ -0,0 +1,604 @@ +/* + * 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.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.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.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.AslFolderItemIdVirtualField; +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.AslFolderItemJoinCondition; +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 static final EnumSet SUPPORTED_DESCENDANT_PARENT_RELATIONS = EnumSet.of( + AslSourceRelation.COMPOSITION, + AslSourceRelation.EHR_STATUS, + AslSourceRelation.FOLDER, + AslSourceRelation.EHR); + private static final EnumSet SUPPORTED_DESCENDANT_CONDITIONS = EnumSet.of( + AslSourceRelation.COMPOSITION, + AslSourceRelation.EHR_STATUS, + AslSourceRelation.FOLDER // FOLDER CONTAINS FOLDER + ); + + 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))); + case AslFolderItemJoinCondition c -> conditions.add( + joinFolderItemIdEqualVoIdCondition(c, sqlLeft, sqlRight)); + } + } + + return conditions.stream().reduce(DSL.noCondition(), DSL::and); + } + + private static void addDelegatingJoinConditions( + AslDelegatingJoinCondition joinCondition, List conditions, Table sqlLeft, Table sqlRight) { + (switch (joinCondition.getDelegate()) { + case AslPathChildCondition c -> pathChildConditions(c, sqlLeft, sqlRight, true); + case AslEntityIdxOffsetCondition c -> entityIdxOffsetConditions(c, sqlLeft, sqlRight, true); + case AslDescendantCondition c -> descendantConditions(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, AslSourceRelation.FOLDER) + .contains(parentRelation)) { + throw new IllegalArgumentException("unexpected parent relation type %s".formatted(parentRelation)); + } + if (!EnumSet.of(AslSourceRelation.COMPOSITION, AslSourceRelation.EHR_STATUS, AslSourceRelation.FOLDER) + .contains(dc.getChildRelation())) { + throw new IllegalArgumentException( + "unexpected descendant relation type %s".formatted(dc.getChildRelation())); + } + + return switch (parentRelation) { + case EHR_STATUS -> Stream.of( + joinColumnEqualCondition(AslStructureColumn.EHR_ID, dc, sqlLeft, sqlRight, isJoinCondition), + joinNumEqualParentNumCondition(dc, sqlLeft, sqlRight, isJoinCondition)); + // l.vo_id == r.vo_id and l.num == r.parent_num + case COMPOSITION -> Stream.of( + joinColumnEqualCondition(AslStructureColumn.VO_ID, dc, sqlLeft, sqlRight, isJoinCondition), + joinNumEqualParentNumCondition(dc, sqlLeft, sqlRight, isJoinCondition)); + // l.ehr_id == r.ehr_id and l.folder_idx = r.folder_idx and l.num == r.parent_num + case FOLDER -> Stream.of( + joinColumnEqualCondition(AslStructureColumn.EHR_ID, dc, sqlLeft, sqlRight, isJoinCondition), + joinColumnEqualCondition(AslStructureColumn.EHR_FOLDER_IDX, dc, sqlLeft, sqlRight, isJoinCondition), + joinNumEqualParentNumCondition(dc, sqlLeft, sqlRight, isJoinCondition)); + 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 (!SUPPORTED_DESCENDANT_PARENT_RELATIONS.contains(parentRelation)) { + throw new IllegalArgumentException("unexpected parent relation type %s".formatted(parentRelation)); + } + AslSourceRelation descendantRelation = dc.getDescendantRelation(); + if (!SUPPORTED_DESCENDANT_CONDITIONS.contains(descendantRelation)) { + throw new IllegalArgumentException("unexpected descendant relation type %s".formatted(descendantRelation)); + } + + 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 EHR_STATUS -> Stream.of( + joinColumnEqualCondition(AslStructureColumn.EHR_ID, dc, sqlLeft, sqlRight, isJoinCondition), + joinNumCapBetweenCondition(dc, sqlLeft, sqlRight, isJoinCondition)); + // l.vo_id == r.vo_id and l.num < r.num <= l.num_cap + case COMPOSITION -> Stream.of( + joinColumnEqualCondition(AslStructureColumn.VO_ID, dc, sqlLeft, sqlRight, isJoinCondition), + joinNumCapBetweenCondition(dc, sqlLeft, sqlRight, isJoinCondition)); + // l.ehr_id == r.ehr_id and l.folder_idx == r.folder_idx and l.num < r.num <= l.num_cap + case FOLDER -> Stream.of( + joinColumnEqualCondition(AslStructureColumn.EHR_ID, dc, sqlLeft, sqlRight, isJoinCondition), + joinColumnEqualCondition(AslStructureColumn.EHR_FOLDER_IDX, dc, sqlLeft, sqlRight, isJoinCondition), + joinNumCapBetweenCondition(dc, sqlLeft, sqlRight, isJoinCondition)); + 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"); + case AslFolderItemIdVirtualField __ -> throw new IllegalArgumentException( + "AslFolderItemIdValuesColumnField 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); + }; + }; + } + + /** + * Provides a join conditions using the given column name + * [sqlLeft].column = [sqlRight].column + * Example: + * "p_data__0"."p_data__0_vo_id" = "p_events__0"."p_events__0_vo_id" + */ + private static Condition joinColumnEqualCondition( + AslStructureColumn column, + AslProvidesJoinCondition dc, + Table sqlLeft, + Table sqlRight, + boolean aliased) { + final String cName = column.getFieldName(); + return FieldUtils.field(sqlLeft, dc.getLeftProvider(), dc.getLeftOwner(), cName, UUID.class, true) + .eq(FieldUtils.field(sqlRight, dc.getRightProvider(), dc.getRightOwner(), cName, UUID.class, aliased)); + } + + /** + * Provides a parent child join conditions using the left num to right parent_num + * [sqlLeft].num = [sqlRight].parent_name + * Example: + * "p_data__0"."p_data__0_vo_id" = "p_events__0"."p_events__0_vo_id" + */ + private static Condition joinNumEqualParentNumCondition( + AslProvidesJoinCondition dc, Table sqlLeft, Table sqlRight, boolean aliased) { + + final String num = AslStructureColumn.NUM.getFieldName(); + final String parentNum = AslStructureColumn.PARENT_NUM.getFieldName(); + + return FieldUtils.field(sqlLeft, dc.getLeftProvider(), dc.getLeftOwner(), num, Integer.class, true) + .eq(FieldUtils.field( + sqlRight, dc.getRightProvider(), dc.getRightOwner(), parentNum, Integer.class, aliased)); + } + + /** + * Provides a parent child join conditions using the left num to right parent_num + * [sqlRight].num between ([sqlLeft].num + 1) and [sqlLeft].num_cap + * Example: + * "sAN_d_0"."sAN_d_0_num" between ("sCO_c_0"."sCO_c_0_num" + 1) and "sCO_c_0"."sCO_c_0_num_cap" + */ + private static Condition joinNumCapBetweenCondition( + AslProvidesJoinCondition dc, Table sqlLeft, Table sqlRight, boolean aliased) { + + final String numFieldName = AslStructureColumn.NUM.getFieldName(); + final String numCapFieldName = AslStructureColumn.NUM_CAP.getFieldName(); + + final AslQuery leftProvider = dc.getLeftProvider(); + final AslQuery leftOwner = dc.getLeftOwner(); + + return FieldUtils.field(sqlRight, dc.getRightProvider(), dc.getRightOwner(), numFieldName, Integer.class, true) + .between( + FieldUtils.field(sqlLeft, leftProvider, leftOwner, numFieldName, Integer.class, aliased) + .add(DSL.inline(1)), + FieldUtils.field(sqlLeft, leftProvider, leftOwner, numCapFieldName, Integer.class, aliased)); + } + + /** + * Provides the FOLDER contains COMPOSITION join condition using + * [sqlRight]_vo_id = [sqlLeft]_item_id_value + * Example: + * on "sCO_c_0_vo_id" = "sF_0"."sF_0_item_id_value" + * + * @param dc {@link AslFolderItemJoinCondition} + * @param sqlLeft structure query on folder_data + * @param sqlRight structure query on comp_data + * @return joinByItemId matching the composition void against the folder item id + */ + private static Condition joinFolderItemIdEqualVoIdCondition( + AslFolderItemJoinCondition dc, Table sqlLeft, Table sqlRight) { + + AslQuery leftOwner = dc.getLeftOwner(); + + AslQuery rightProvider = dc.rightProvider(); + AslQuery rightOwner = dc.getRightOwner(); + + AslFolderItemIdVirtualField column = leftOwner.getSelect().stream() + .filter(AslFolderItemIdVirtualField.class::isInstance) + .map(AslFolderItemIdVirtualField.class::cast) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "AslFolderItemJoinCondition requires an AslFolderItemIdValuesColumnField")); + + // comp.vo_id == folder.data /items/id/value + // on "sCO_c_0_vo_id" = "sF_0_data_item_id_value" + return FieldUtils.field( + sqlRight, rightProvider, rightOwner, AslStructureColumn.VO_ID.getFieldName(), UUID.class, true) + .eq(FieldUtils.field(sqlLeft, column, column.getFieldName(), UUID.class, true)); + } + + 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 0000000000..f63b43bd17 --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/EncapsulatingQueryUtils.java @@ -0,0 +1,386 @@ +/* + * 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.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.service.TemplateService; +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.AslFolderItemIdVirtualField; +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 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); + case AslAggregatingField __ -> throw new IllegalArgumentException( + "Cannot aggregate on AslAggregatingField"); + case AslFolderItemIdVirtualField __ -> throw new IllegalArgumentException( + "Cannot aggregate on AslFolderItemIdValuesColumnField"); + }; + } + + @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, TemplateService templateService) { + // order lexicographically by template id + Map templates = templateService.findAllTemplateIds(); + + 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.entrySet().stream() + .sorted(Comparator.comparing(Map.Entry::getValue, Collator.getInstance(Locale.ENGLISH))) + .map(Map.Entry::getKey) + .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); + case AslFolderItemIdVirtualField fidv -> throw new IllegalArgumentException( + "%s is not support as select field".formatted(fidv.getExtractedColumn())); + }; + } + + @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 AslSubqueryField sqf -> Stream.of(subqueryField(sqf, aslQueryToTable)); + case AslConstantField __ -> Stream.empty(); + case AslAggregatingField __ -> throw new IllegalArgumentException( + "Cannot aggregate by AslAggregatingField"); + case AslFolderItemIdVirtualField __ -> throw new IllegalArgumentException( + "Cannot aggregate by AslFolderItemIdValuesColumnField"); + }; + } + + 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, TemplateService templateService) { + 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, templateService); + case AslComplexExtractedColumnField ecf -> complexExtractedColumnOrderByFields(ecf, src); + case AslConstantField __ -> Stream.>empty(); + case AslSubqueryField sqf -> Stream.of(subqueryField(sqf, aslQueryToTable)); + case AslAggregatingField __ -> throw new IllegalArgumentException( + "ORDER BY AslAggregatingField is not allowed"); + case AslFolderItemIdVirtualField __ -> throw new IllegalArgumentException( + "ORDER BY AslFolderItemIdValuesColumnField is not allowed"); + }) + .map(f -> f.sort(ob.direction())); + } + + @Nonnull + private static Stream> columnOrderField(AslColumnField f, Table src, TemplateService templateService) { + Field field = FieldUtils.field(src, f, true); + + field = switch (f.getExtractedColumn()) { + // ensure order by name, not internal ID + case TEMPLATE_ID -> templateIdOrderField(field, templateService); + 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 0000000000..b4a454562f --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/FieldUtils.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.aqlengine.sql; + +import java.util.Iterator; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField; +import org.ehrbase.openehr.aqlengine.asl.model.field.AslVirtualField; +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; +import org.jooq.impl.DSL; + +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, AslVirtualField aslField, String fieldName, boolean aliased) { + return table.field(aliased ? aslField.aliasedName(fieldName) : fieldName); + } + + public static Field field( + Table table, AslVirtualField aslField, String fieldName, Class type, boolean aliased) { + return table.field(aliased ? aslField.aliasedName(fieldName) : fieldName, type); + } + + 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); + } + + public static Field virtualAliasedField( + Table target, Field field, AslVirtualField column, String columnName) { + return DSL.field("{0}.{1}", target, field).as(column.aliasedName(columnName)); + } +} 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 0000000000..9132fe7924 --- /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 0000000000..73d2fae05c --- /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 0000000000..7d4b708f3a --- /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 0000000000..9b6cb466ce --- /dev/null +++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/ExtractedColumnResultPostprocessor.java @@ -0,0 +1,117 @@ +/* + * 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) { + Object id = srcRow.get(0); + if (id == null) { + return null; + } + return id + "::" + 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 0000000000..eaea7d8446 --- /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 0000000000..51c7d63e8d --- /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 0000000000..e5fc07a860 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacementTest.java @@ -0,0 +1,330 @@ +/* + * 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.List; +import java.util.Map; +import java.util.stream.Stream; +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.MethodSource; +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(); + } + + @ParameterizedTest + @MethodSource("replaceWhereParametersSrc") + void replaceWhereParameters(ReplacementTestParam check) { + check.doAssert(); + } + + static Stream replaceWhereParametersSrc() { + return Stream.of( + // Simple string replacement + ReplacementTestParam.success( + "SELECT d FROM DUMMY d WHERE d/foo = $bar", + Map.of("bar", "baz"), + "SELECT d FROM DUMMY d WHERE d/foo = 'baz'"), + + // Data types + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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 + ReplacementTestParam.rejected( + "SELECT d FROM DUMMY d WHERE (d/f1 = $bar AND d/f2 = $bar AND d/f3 = $baz)", + Map.of("foo", "bob", "bar", "alice"), + "Missing parameter")); + } + + @ParameterizedTest + @MethodSource("replaceFromParametersSrc") + void replaceFromParameters(ReplacementTestParam check) { + check.doAssert(); + } + + static Stream replaceFromParametersSrc() { + return Stream.of( + // archetype_node_id + ReplacementTestParam.success( + "SELECT d FROM DUMMY d[$ani]", Map.of("ani", "at0001"), "SELECT d FROM DUMMY d[at0001]"), + ReplacementTestParam.rejected("SELECT d FROM DUMMY d[$ani]", Map.of("ani", "invalid-id"), null), + + // nodeConstraint + name + ReplacementTestParam.success( + "SELECT d FROM DUMMY d[at0001,$nameConstraint]", + Map.of("nameConstraint", "Results"), + "SELECT d FROM DUMMY d[at0001, 'Results']"), + + // nodeConstraint + local terminology => interpreted as String + ReplacementTestParam.success( + "SELECT d FROM DUMMY d[at0001,$nameConstraint]", + Map.of("nameConstraint", "at0002"), + "SELECT d FROM DUMMY d[at0001, 'at0002']"), + + // nodeConstraint + TERM_CODE => interpreted as String + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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 + ReplacementTestParam.success( + "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']")); + } + + @ParameterizedTest + @MethodSource("replaceSelectParametersSrc") + void replaceSelectParameters(ReplacementTestParam check) { + check.doAssert(); + } + + static Stream replaceSelectParametersSrc() { + return Stream.of( + ReplacementTestParam.success( + "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"), + ReplacementTestParam.rejected( + "SELECT d[$foo]/e[bar=$foo AND ba/z=$baz] FROM DUMMY d", + Map.of("foo", List.of("at0001"), "baz", List.of(42, 24)), + "One of the parameters does not support multiple values"), + ReplacementTestParam.success( + "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"), + ReplacementTestParam.rejected( + "SELECT d[$foo]/e[bar=$foo AND ba/z=$baz] FROM DUMMY d", + Map.of("foo", "invalid-id", "baz", 42), + null), + ReplacementTestParam.rejected("SELECT d/e[$foo] FROM DUMMY d", Map.of("foo", 42), null)); + } + + @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"); + } + + @ParameterizedTest + @MethodSource("replaceMatchesParametersSrc") + void replaceMatchesParameters(ReplacementTestParam check) { + check.doAssert(); + } + + static Stream replaceMatchesParametersSrc() { + return Stream.of( + ReplacementTestParam.success( + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {$m}", + Map.of("m", "v1"), + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {'v1'}"), + ReplacementTestParam.success( + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {$m1, $m2}", + Map.of("m1", "v1", "m2", "v2"), + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {'v1', 'v2'}"), + ReplacementTestParam.success( + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {$ma}", + Map.of("ma", List.of("v1", "v2")), + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {'v1', 'v2'}"), + ReplacementTestParam.success( + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {$a, $b, $c}", + Map.of("a", List.of("v1", "v2"), "b", List.of(), "c", "v3"), + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {'v1', 'v2', 'v3'}"), + ReplacementTestParam.rejected( + "SELECT d FROM DUMMY d WHERE d/name/value MATCHES {$a}", + Map.of("a", List.of()), + "Parameter replacement resulted in empty operand list")); + } + + 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)); + } + + record ReplacementTestParam( + String srcAql, + Map parameterMap, + Class expectedException, + String expected) { + + static ReplacementTestParam success(String srcAql, Map parameterMap, String expectedAql) { + return new ReplacementTestParam(srcAql, parameterMap, null, expectedAql); + } + + static ReplacementTestParam rejected(String srcAql, Map parameterMap, String expectedMessage) { + return new ReplacementTestParam(srcAql, parameterMap, AqlParseException.class, expectedMessage); + } + + void doAssert() { + if (expectedException == null) { + assertReplaceParameters(srcAql, parameterMap, expected); + } else { + var ta = assertReplaceParametersRejected(srcAql, parameterMap).isInstanceOf(expectedException); + if (expected != null) { + ta.hasMessageContaining(expected); + } + } + } + } +} 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 0000000000..5369555565 --- /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 0000000000..ff1aa6eae5 --- /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/AslFromCreatorTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslFromCreatorTest.java new file mode 100644 index 0000000000..1734b06271 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslFromCreatorTest.java @@ -0,0 +1,190 @@ +/* + * 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.openehr.aqlengine.asl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.knowledge.KnowledgeCacheService; +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.AslRootQuery; +import org.ehrbase.openehr.aqlengine.asl.model.query.AslStructureQuery; +import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.jooq.JoinType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.mockito.ThrowingConsumer; + +class AslFromCreatorTest { + + private final KnowledgeCacheService mockKnowledgeCacheService = mock(); + + @BeforeEach + void setUp() { + Mockito.reset(mockKnowledgeCacheService); + } + + private AslRootQuery addFromClause(String aql) { + var aliasProvider = new AslUtils.AliasProvider(); + var aslQuery = new AslRootQuery(); + + var aqlQuery = AqlQueryParser.parse(aql); + var queryWrapper = AqlQueryWrapper.create(aqlQuery); + + var aslFromCreator = new AslFromCreator(aliasProvider, mockKnowledgeCacheService); + aslFromCreator.addFromClause(aslQuery, queryWrapper); + + return aslQuery; + } + + @Nested + class Folder { + + @Test + void simple() { + + AslRootQuery aslQuery = addFromClause( + """ + SELECT f/uid/value + FROM FOLDER f + """); + + // FROM FOLDER f + assertThat(aslQuery.getChildren()).singleElement().satisfies(isStructureQueryRootWithVersionOnFolder()); + } + + @Test + void containsFolder() { + + AslRootQuery aslQuery = addFromClause( + """ + SELECT f1/uid/value, f2/uid/value + FROM FOLDER f1 CONTAINS FOLDER f2 + """); + + assertThat(aslQuery.getChildren()).hasSize(2); + + // FROM FOLDER f + assertThat(aslQuery.getChildren()).element(0).satisfies(isStructureQueryRootWithVersionOnFolder()); + // CONTAINS FOLDER f2 + assertThat(aslQuery.getChildren()) + .element(1) + .satisfies(isStructureQueryWithDataContains( + AslStructureQuery.AslSourceRelation.FOLDER, + AslStructureQuery.AslSourceRelation.FOLDER, + false)); + } + + @Test + void containsComposition() { + + AslRootQuery aslQuery = addFromClause( + """ + SELECT c/uid/value + FROM FOLDER CONTAINS COMPOSITION c + """); + + assertThat(aslQuery.getChildren()).hasSize(2); + + // FROM FOLDER + assertThat(aslQuery.getChildren()).element(0).satisfies(isStructureQueryRootWithVersionOnFolder()); + // CONTAINS COMPOSITION c + assertThat(aslQuery.getChildren()) + .element(1) + .satisfies(isStructureQueryWithDataContains( + AslStructureQuery.AslSourceRelation.FOLDER, + AslStructureQuery.AslSourceRelation.COMPOSITION, + true)); + } + + @Test + void containsFolderContainsComposition() { + + AslRootQuery aslQuery = addFromClause( + """ + SELECT c/uid/value + FROM FOLDER CONTAINS FOLDER f2[openEHR-EHR-FOLDER.episode_of_care.v1] CONTAINS COMPOSITION c + """); + + assertThat(aslQuery.getChildren()).hasSize(3); + + // FROM FOLDER + assertThat(aslQuery.getChildren()).element(0).satisfies(isStructureQueryRootWithVersionOnFolder()); + // CONTAINS FOLDER f2 + assertThat(aslQuery.getChildren()) + .element(1) + .satisfies(isStructureQueryWithDataContains( + AslStructureQuery.AslSourceRelation.FOLDER, + AslStructureQuery.AslSourceRelation.FOLDER, + false)); + // FOLDER f2 CONTAINS COMPOSITION c + assertThat(aslQuery.getChildren()) + .element(2) + .satisfies(isStructureQueryWithDataContains( + AslStructureQuery.AslSourceRelation.FOLDER, + AslStructureQuery.AslSourceRelation.COMPOSITION, + true)); + } + + private static ThrowingConsumer> isStructureQueryRootWithVersionOnFolder() { + return isStructureQueryRootWithVersion(AslStructureQuery.AslSourceRelation.FOLDER); + } + + private static ThrowingConsumer> isStructureQueryWithDataContains( + AslStructureQuery.AslSourceRelation leftType, + AslStructureQuery.AslSourceRelation rightType, + boolean requiresVersionJoin) { + return pair -> { + assertThat(pair.getValue()).isNotNull().satisfies(join -> { + assertThat(join.getJoinType()).isSameAs(JoinType.JOIN); + assertThat(join.getLeft()) + .isInstanceOfSatisfying(AslStructureQuery.class, left -> assertThat(left.getType()) + .isSameAs(leftType)); + assertThat(join.getRight()) + .isInstanceOfSatisfying(AslStructureQuery.class, right -> assertThat(right.getType()) + .isSameAs(rightType)); + }); + assertThat(pair.getKey()).isInstanceOfSatisfying(AslStructureQuery.class, sq -> { + + // Source relation FOLDER with version table join and no condition + assertThat(sq.getType()).isSameAs(rightType); + assertThat(sq.isRequiresVersionTableJoin()).isEqualTo(requiresVersionJoin); + }); + }; + } + } + + private static ThrowingConsumer> isStructureQueryRootWithVersion( + AslStructureQuery.AslSourceRelation type) { + return pair -> { + assertThat(pair.getKey()).isInstanceOfSatisfying(AslStructureQuery.class, sq -> { + + // Source relation FOLDER with version table join and no condition + assertThat(sq.getType()).isSameAs(type); + assertThat(sq.isRequiresVersionTableJoin()).isTrue(); + assertThat(sq.getCondition()).isNull(); + }); + assertThat(pair.getValue()).isNull(); + }; + } +} 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 0000000000..65c7ade34b --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraph.java @@ -0,0 +1,290 @@ +/* + * 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.AslFolderItemIdVirtualField; +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.AslFolderItemJoinCondition; +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()); + case AslFolderItemJoinCondition + c -> "FolderItemJoinCondition FOLDER -> %s [%s.vo_id in %s.data.items[].id.value]" + .formatted( + c.descendantRelation(), + c.getRightOwner().getAlias(), + c.getLeftOwner().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()); + case AslFolderItemIdVirtualField f -> providerAlias + f.aliasedName() + " -- FOLDER.items"; + }; + } + + 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 0000000000..563c733d92 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/asl/AslGraphTest.java @@ -0,0 +1,50 @@ +/* + * 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.Disabled; +import org.junit.jupiter.api.Test; + +class AslGraphTest { + + @Test + @Disabled("What is this testing - there is no assert?") + 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 0000000000..f9b998dc6f --- /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 0000000000..570d66c66e --- /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 0000000000..829e63e9f5 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/featurecheck/AqlQueryFeatureCheckTest.java @@ -0,0 +1,603 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.ehrbase.api.exception.AqlFeatureNotImplementedException; +import org.ehrbase.api.exception.IllegalAqlException; +import org.ehrbase.openehr.aqlengine.AqlConfigurationProperties; +import org.ehrbase.openehr.dbformat.StructureRmType; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +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) { + + assertDoesNotThrow(() -> runEnsureQuerySupported(aql)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "SELECT f FROM FOLDER f", + """ + SELECT f/uid/value, f/name/value, f/archetype_node_id + FROM FOLDER f[openEHR-EHR-FOLDER.generic.v1] + """, + """ + SELECT f2/uid/value, f2/name/value + FROM FOLDER f1[openEHR-EHR-FOLDER.generic.v1,'root'] + CONTAINS FOLDER f2[openEHR-EHR-FOLDER.generic.v1,'Encounter'] + """, + """ + SELECT e/ehr_id/value, f/uid/value + FROM EHR e + CONTAINS FOLDER f[openEHR-EHR-FOLDER.generic.v1,'Encounter'] + """, + """ + SELECT c/uid/value, f/name/value + FROM FOLDER f + CONTAINS COMPOSITION c + """ + }) + void ensureQuerySupportedAqlOnFolderEnabled(String aql) { + assertDoesNotThrow(() -> runEnsureQuerySupportedAqlOnFolderEnabled(aql)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "SELECT f/items FROM FOLDER f", + "SELECT f/folders/items FROM FOLDER f", + "SELECT f/items/namespace FROM FOLDER f", + "SELECT f/items/type FROM FOLDER f", + "SELECT f/items/id FROM FOLDER f", + "SELECT f/items/id/value FROM FOLDER f" + }) + void ensureQueryNotSupportedAqlOnFolderEnabled(String aql) { + assertThrows(AqlFeatureNotImplementedException.class, () -> runEnsureQuerySupportedAqlOnFolderEnabled(aql)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "SELECT e FROM EHR e", + "SELECT e/ehr_id FROM EHR e", + // ehr_status is rewritten as CONTAINS + "SELECT e/ehr_status FROM EHR e", + "SELECT e/compositions FROM EHR e", + "SELECT e/directory FROM EHR e", + "SELECT e/folders FROM EHR e", + """ + SELECT f + FROM FOLDER f + """, + """ + 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) { + + assertThrows(AqlFeatureNotImplementedException.class, () -> runEnsureQuerySupported(aql)); + } + + @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) { + + assertThatThrownBy(() -> runEnsureQuerySupported(aql)) + .isInstanceOf(IllegalAqlException.class) + .hasMessageEndingWith(" 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) { + + assertThrows(IllegalAqlException.class, () -> runEnsureQuerySupported(aql)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "SELECT c FROM COMPOSITION c CONTAINS EHR_STATUS", + "SELECT c FROM COMPOSITION c CONTAINS ELEMENT CONTAINS EHR_STATUS", + "SELECT e FROM EHR CONTAINS COMPOSITION CONTAINS EHR_STATUS CONTAINS ELEMENT e" + }) + void ensureContainsRejected(String aql) { + + assertThatThrownBy(() -> runEnsureQuerySupported(aql)) + .isInstanceOf(IllegalAqlException.class) + .hasMessageContainingAll("Structure ", " cannot CONTAIN ", " (of structure "); + } + + @ParameterizedTest + @EnumSource( + value = StructureRmType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"INSTRUCTION_DETAILS", "FEEDER_AUDIT_DETAILS"}) + void ensureContainsRejectedNonStructureEntries(StructureRmType structureRmType) { + + String aql = "SELECT f FROM COMPOSITION f CONTAINS %s".formatted(structureRmType.name()); + assertThatThrownBy(() -> runEnsureQuerySupported(aql)) + .isInstanceOf(AqlFeatureNotImplementedException.class) + .hasMessage( + "Not implemented: CONTAINS %s is currently not supported".formatted(structureRmType.name())); + } + + @Test + void ensureContainsRejectedExperimentalAqlOnFolderDisabled() { + + assertThatThrownBy(() -> runEnsureQuerySupported("SELECT f FROM FOLDER f")) + .isInstanceOf(AqlFeatureNotImplementedException.class) + .hasMessageContainingAll("CONTAINS FOLDER is an experimental feature and currently disabled."); + } + + @ParameterizedTest + @ValueSource( + strings = {"SELECT f FROM COMPOSITION CONTAINS FOLDER f", "SELECT f FROM EHR_STATUS CONTAINS FOLDER f"}) + void ensureContainsRejectedExperimentalAqlOnFolder(String aql) { + + assertThatThrownBy(() -> runEnsureQuerySupportedAqlOnFolderEnabled(aql)) + .isInstanceOf(IllegalAqlException.class) + .hasMessageContainingAll("Structure ", " cannot CONTAIN ", " (of structure "); + } + + @ParameterizedTest + @EnumSource( + value = StructureRmType.class, + mode = EnumSource.Mode.EXCLUDE, + names = {"FOLDER", "COMPOSITION", "INSTRUCTION_DETAILS", "FEEDER_AUDIT_DETAILS"}) + void ensureContainsExperimentalAqlOnFolderRestrictedToTypes(StructureRmType structureRmType) { + + String aql = "SELECT f FROM FOLDER f CONTAINS %s".formatted(structureRmType.name()); + assertThatThrownBy(() -> runEnsureQuerySupportedAqlOnFolderEnabled(aql)) + .isInstanceOf(AqlFeatureNotImplementedException.class) + .hasMessage("Not implemented: FOLDER CONTAINS %s is currently not supported" + .formatted(structureRmType.name())); + } + + @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) { + + assertDoesNotThrow(() -> runEnsureQuerySupported(aql)); + } + + @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) { + + assertThatThrownBy(() -> runEnsureQuerySupported(aql)) + .isInstanceOf(IllegalAqlException.class) + .message() + .isNotBlank(); + } + + @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) { + + assertThatThrownBy(() -> runEnsureQuerySupported(aql)) + .isInstanceOf(AqlFeatureNotImplementedException.class) + .hasMessageStartingWith("Not implemented: "); + } + + @Test + void ensureVersionSupportedAqlOnFolderEnabled() { + + assertDoesNotThrow( + () -> runEnsureQuerySupportedAqlOnFolderEnabled( + """ + SELECT f/uid/value + FROM VERSION cv[LATEST_VERSION] CONTAINS FOLDER f + """)); + } + + private void runEnsureQuerySupported(String aql) { + + runEnsureQuerySupported(propertiesWithAqlOnFolderEnabled(false), aql); + } + + private void runEnsureQuerySupportedAqlOnFolderEnabled(String aql) { + + runEnsureQuerySupported(propertiesWithAqlOnFolderEnabled(true), aql); + } + + private void runEnsureQuerySupported(AqlConfigurationProperties aqlFeature, String aql) { + + AqlQuery aqlQuery = AqlQueryParser.parse(aql); + AqlQueryFeatureCheck aqlQueryFeatureCheck = new AqlQueryFeatureCheck(() -> "node", aqlFeature); + aqlQueryFeatureCheck.ensureQuerySupported(aqlQuery); + } + + private AqlConfigurationProperties propertiesWithAqlOnFolderEnabled(boolean aqlOnFolderEnabled) { + return new AqlConfigurationProperties( + false, + new AqlConfigurationProperties.Experimental( + new AqlConfigurationProperties.Experimental.AqlOnFolder(aqlOnFolderEnabled))); + } +} 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 0000000000..599e489fc1 --- /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 0000000000..0d339c62cc --- /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 0000000000..a3e94ef5e6 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/pathanalysis/PathAnalysisTest.java @@ -0,0 +1,267 @@ +/* + * 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(); + } + + @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 synthetic = List.of("_magnitude", "_type", "_index"); + List rmAttributes = RmAttributeAlias.VALUES.stream() + .map(RmAttributeAlias::attribute) + .filter(s -> !synthetic.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 0000000000..32792304fb --- /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 0000000000..0c8b38b2ef --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/service/AqlQueryServiceImpTest.java @@ -0,0 +1,174 @@ +/* + * 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 = + """ + 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); + } + + @ParameterizedTest + @CsvSource( + textBlock = + """ + SELECT e FROM EHR e | + SELECT e/ehr_id/value FROM EHR e CONTAINS COMPOSITION c | + SELECT c FROM EHR e[ehr_id/value = '5dd64358-76b4-4ffe-8d05-554406d9d023'] CONTAINS COMPOSITION c | + SELECT s_el FROM EHR e CONTAINS (COMPOSITION c AND EHR_STATUS CONTAINS ELEMENT s_el) | + SELECT c/uid/value FROM EHR e CONTAINS COMPOSITION c WHERE e/ehr_id/value = '5dd64358-76b4-4ffe-8d05-554406d9d023' | + SELECT c FROM EHR e CONTAINS COMPOSITION c WHERE c/uid/value = '5dd64358-76b4-4ffe-8d05-554406d9d023' | SELECT c FROM COMPOSITION c WHERE c/uid/value = '5dd64358-76b4-4ffe-8d05-554406d9d023' + """, + delimiterString = "|") + void optimizeQuery(String originalAql, String optimizedAql) { + AqlQuery query = AqlQueryParser.parse(originalAql); + AqlQueryServiceImp cut = new AqlQueryServiceImp(null, null, null, null, null, null); + cut.optimizeQuery(query); + + String expected = AqlQueryParser.parse(StringUtils.isBlank(optimizedAql) ? originalAql : optimizedAql) + .render(); + assertThat(query.render()).isEqualTo(expected); + } +} 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 0000000000..ad333bce98 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/AqlSqlQueryBuilderTest.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.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.api.service.TemplateService; +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.aqlengine.querywrapper.select.SelectWrapper; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; +import org.ehrbase.openehr.util.TestConfig; +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; + +class AqlSqlQueryBuilderTest { + + private final KnowledgeCacheService mockKnowledgeCacheService = mock(); + + private AqlSqlQueryBuilder aqlSqlQueryBuilder() { + TemplateService templateService = mock(); + Mockito.when(templateService.findAllTemplateIds()) + .thenReturn(Map.of(UUID.randomUUID(), "template1.v1", UUID.randomUUID(), "template2.v3")); + + return new AqlSqlQueryBuilder( + TestConfig.aqlConfigurationProperties(), + new DefaultDSLContext(SQLDialect.POSTGRES), + templateService, + Optional.empty()); + } + + @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 + enc/name/value, + c/uid/value + FROM EHR e + CONTAINS FOLDER[name/value="Encounter"] + CONTAINS FOLDER enc + CONTAINS COMPOSITION c + WHERE e/ehr_id/value = 'e6fad8ba-fb4f-46a2-bf82-66edb43f142f' and c/archetype_details/template_id/value = 'template1.v1' + order by c/archetype_details/template_id/value + """); + + System.out.println("/*"); + System.out.println(aqlQuery.render()); + System.out.println("*/"); + + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + AqlSqlLayer aqlSqlLayer = new AqlSqlLayer(mockKnowledgeCacheService, () -> "node"); + AslRootQuery aslQuery = aqlSqlLayer.buildAslRootQuery(queryWrapper); + + System.out.println("/*"); + System.out.println(AslGraph.createAslGraph(aslQuery)); + System.out.println("*/"); + System.out.println(); + + AqlSqlQueryBuilder sqlQueryBuilder = aqlSqlQueryBuilder(); + + 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); + + AqlSqlLayer aqlSqlLayer = new AqlSqlLayer(mockKnowledgeCacheService, () -> "node"); + AslRootQuery aslQuery = aqlSqlLayer.buildAslRootQuery(queryWrapper); + AqlSqlQueryBuilder sqlQueryBuilder = aqlSqlQueryBuilder(); + + assertDoesNotThrow(() -> sqlQueryBuilder.buildSqlQuery(aslQuery)); + } + + @Test + void queryOnData() { + 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 queryOnFolder() { + AqlQuery aqlQuery = AqlQueryParser.parse( + """ + SELECT + f/uid/value + FROM EHR + CONTAINS FOLDER f + """); + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + assertThat(queryWrapper.pathInfos()).hasSize(1); + assertThat(queryWrapper.selects()).singleElement().satisfies(select -> { + assertThat(select.type()).isEqualTo(SelectWrapper.SelectType.PATH); + assertThat(select.getSelectPath()).hasValueSatisfying(path -> { + assertThat(path).isEqualTo("f/uid/value"); + }); + assertThat(select.root()).satisfies(root -> { + assertThat(root.getRmType()).isEqualTo("FOLDER"); + assertThat(root.alias()).isEqualTo("f"); + }); + }); + + assertDoesNotThrow(() -> buildSqlQuery(queryWrapper)); + } + + @Test + void queryOnFolderWithComposition() { + AqlQuery aqlQuery = AqlQueryParser.parse( + """ + SELECT + c/uid/value + FROM FOLDER CONTAINS COMPOSITION c + """); + AqlQueryWrapper queryWrapper = AqlQueryWrapper.create(aqlQuery); + + assertThat(queryWrapper.pathInfos()).hasSize(1); + assertThat(queryWrapper.selects()).singleElement().satisfies(select -> { + assertThat(select.type()).isEqualTo(SelectWrapper.SelectType.PATH); + assertThat(select.getSelectPath()).hasValueSatisfying(path -> { + assertThat(path).isEqualTo("c/uid/value"); + }); + assertThat(select.root()).satisfies(root -> { + assertThat(root.getRmType()).isEqualTo("COMPOSITION"); + assertThat(root.alias()).isEqualTo("c"); + }); + }); + + SelectQuery selectQuery = buildSqlQuery(queryWrapper); + assertThat(selectQuery.toString()) + // items_id_value are selected from folder + .contains("\"sF_0sq\".\"fi_uuids\" as \"sF_0_item_id_value\"") + .contains("and \"descendant\".\"num\" between \"base\".\"num\" and \"base\".\"num_cap\"") + // compositions are joined on item_id_value + .contains("on \"sCO_c_0\".\"sCO_c_0_vo_id\" = \"sF_0\".\"sF_0_item_id_value\""); + } + + @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 = aqlSqlQueryBuilder(); + + 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 0000000000..2ec2d5ecbb --- /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/aqlengine/sql/FieldUtilsTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/FieldUtilsTest.java new file mode 100644 index 0000000000..cf9b1d478d --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/FieldUtilsTest.java @@ -0,0 +1,50 @@ +/* + * 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.openehr.aqlengine.sql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import org.ehrbase.openehr.aqlengine.asl.model.field.AslFolderItemIdVirtualField; +import org.jooq.Field; +import org.jooq.impl.DSL; +import org.jooq.impl.QOM; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("UnstableApiUsage") +class FieldUtilsTest { + + @Test + void virtualAliasedField() { + + String columnName = "items_id_value"; + + AslFolderItemIdVirtualField aslField = mock(AslFolderItemIdVirtualField.class); + doReturn("aliased_" + columnName).when(aslField).aliasedName(columnName); + + Field field = FieldUtils.virtualAliasedField( + DSL.table("test_table"), DSL.field("some_field_on_table"), aslField, columnName); + + assertThat(field) + .hasToString("\"aliased_items_id_value\"") + .isInstanceOf(QOM.FieldAlias.class) + .satisfies(aliasedField -> assertThat(((QOM.FieldAlias) field).$aliased()) + .hasToString("test_table.some_field_on_table")); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/ExtractedColumnResultPostprocessorTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/ExtractedColumnResultPostprocessorTest.java new file mode 100644 index 0000000000..d8e167a44b --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/sql/postprocessor/ExtractedColumnResultPostprocessorTest.java @@ -0,0 +1,196 @@ +/* + * 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.openehr.aqlengine.sql.postprocessor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +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.OffsetDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Optional; +import java.util.UUID; +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.sdk.util.OpenEHRDateTimeSerializationUtils; +import org.jooq.Record; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.Mockito; + +class ExtractedColumnResultPostprocessorTest { + + private final KnowledgeCacheService knowledgeCacheService = mock(KnowledgeCacheService.class); + private final Record dbRecord = mock(Record.class); + + private ExtractedColumnResultPostprocessor processor(AslExtractedColumn extractedColumn) { + return new ExtractedColumnResultPostprocessor(extractedColumn, knowledgeCacheService, "test-node"); + } + + @BeforeEach + void setUp() { + + Mockito.reset(knowledgeCacheService, dbRecord); + } + + @Test + void nullSafe() { + + assertThat(processor(AslExtractedColumn.VO_ID).postProcessColumn(null)).isNull(); + } + + @Test + void templateId() { + + var uuid = UUID.fromString("93e01a9a-041e-4bf6-89c2-e63f8a74a4d5"); + doReturn(Optional.of("test-template")).when(knowledgeCacheService).findTemplateIdByUuid(uuid); + assertThat(processor(AslExtractedColumn.TEMPLATE_ID).postProcessColumn(uuid)) + .isEqualTo("test-template"); + } + + @Test + void ehrSystemId() { + assertThat(processor(AslExtractedColumn.EHR_SYSTEM_ID_DV) + .postProcessColumn("e290acd1-0fa4-4eb0-97c6-e884e6ea74f3")) + .isEqualTo(new HierObjectId("e290acd1-0fa4-4eb0-97c6-e884e6ea74f3")); + } + + @Test + void rootConcept() { + assertThat(processor(AslExtractedColumn.ROOT_CONCEPT).postProcessColumn("root_concept")) + .isEqualTo("openEHR-EHR-COMPOSITIONroot_concept"); + } + + @Test + void archetypeNodeId() { + + doReturn(".entityConcept").when(dbRecord).get(0); + doReturn("HX").when(dbRecord).get(1); + + assertThat(processor(AslExtractedColumn.ARCHETYPE_NODE_ID).postProcessColumn(dbRecord)) + .isEqualTo("openEHR-EHR-HIER_OBJECT_ID.entityConcept"); + } + + @Test + void vo_id() { + + doReturn("c0817101-94fd-48e5-b4f9-cb8f0556923a").when(dbRecord).get(0); + doReturn("42").when(dbRecord).get(1); + + assertThat(processor(AslExtractedColumn.VO_ID).postProcessColumn(dbRecord)) + .isEqualTo("c0817101-94fd-48e5-b4f9-cb8f0556923a::test-node::42"); + } + + @Test + void auditDetailsDescription() { + + assertThat(processor(AslExtractedColumn.AD_DESCRIPTION_DV).postProcessColumn("lorem ipsum")) + .isEqualTo(new DvText("lorem ipsum")); + } + + @Test + void auditDetailsChangeType() { + + ContributionChangeType changeType = ContributionChangeType.creation; + assertThat(processor(AslExtractedColumn.AD_CHANGE_TYPE_DV).postProcessColumn(changeType)) + .isEqualTo(new DvCodedText( + changeType.getLiteral().toLowerCase(), + new CodePhrase( + new TerminologyId("openehr"), + ChangeTypeUtils.getCodeByJooqChangeType(changeType), + changeType.getLiteral().toLowerCase()))); + } + + @Test + void auditDetailsChangeTypeCode() { + + assertThat(processor(AslExtractedColumn.AD_CHANGE_TYPE_CODE_STRING) + .postProcessColumn(ContributionChangeType.deleted)) + .isEqualTo("523"); + } + + @ParameterizedTest + @EnumSource( + value = AslExtractedColumn.class, + mode = EnumSource.Mode.INCLUDE, + names = {"AD_CHANGE_TYPE_VALUE", "AD_CHANGE_TYPE_PREFERRED_TERM"}) + void auditDetailsChangeTypeValue(AslExtractedColumn aslExtractedColumn) { + + assertThat(processor(aslExtractedColumn).postProcessColumn(ContributionChangeType.amendment)) + .isEqualTo("amendment"); + } + + @ParameterizedTest + @EnumSource( + value = AslExtractedColumn.class, + mode = EnumSource.Mode.INCLUDE, + names = {"OV_TIME_COMMITTED_DV", "EHR_TIME_CREATED_DV"}) + void dateTime(AslExtractedColumn aslExtractedColumn) { + + TemporalAccessor now = OffsetDateTime.now(); + assertThat(processor(aslExtractedColumn).postProcessColumn(now)).isEqualTo(new DvDateTime(now)); + } + + @ParameterizedTest + @EnumSource( + value = AslExtractedColumn.class, + mode = EnumSource.Mode.INCLUDE, + names = {"OV_TIME_COMMITTED", "EHR_TIME_CREATED"}) + void time(AslExtractedColumn aslExtractedColumn) { + + TemporalAccessor now = OffsetDateTime.now(); + assertThat(processor(aslExtractedColumn).postProcessColumn(now)) + .isEqualTo(OpenEHRDateTimeSerializationUtils.formatDateTime(now)); + } + + @ParameterizedTest + @EnumSource( + value = AslExtractedColumn.class, + mode = EnumSource.Mode.EXCLUDE, + names = { + "TEMPLATE_ID", + "EHR_SYSTEM_ID_DV", + "ROOT_CONCEPT", + "ARCHETYPE_NODE_ID", + "VO_ID", + "AD_DESCRIPTION_DV", + "AD_CHANGE_TYPE_DV", + "AD_CHANGE_TYPE_CODE_STRING", + "AD_CHANGE_TYPE_VALUE", + "AD_CHANGE_TYPE_PREFERRED_TERM", + "OV_TIME_COMMITTED_DV", + "EHR_TIME_CREATED_DV", + "OV_TIME_COMMITTED", + "EHR_TIME_CREATED" + }) + void string(AslExtractedColumn aslExtractedColumn) { + + var testValue = "test_value"; + assertThat(processor(aslExtractedColumn).postProcessColumn(testValue)).isSameAs(testValue); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/CompositionSupport.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/CompositionSupport.java new file mode 100644 index 0000000000..b8a2e3944e --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/CompositionSupport.java @@ -0,0 +1,62 @@ +/* + * 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.testdata; + +import com.nedap.archie.rm.composition.Composition; +import com.nedap.archie.rm.support.identification.ObjectVersionId; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.function.Function; +import org.apache.commons.io.IOUtils; +import org.ehrbase.openehr.sdk.client.openehrclient.OpenEhrClientConfig; +import org.ehrbase.openehr.sdk.client.openehrclient.defaultrestclient.DefaultRestClient; +import org.ehrbase.openehr.sdk.client.openehrclient.defaultrestclient.DefaultRestCompositionEndpoint; +import org.ehrbase.openehr.sdk.serialisation.jsonencoding.CanonicalJson; + +public class CompositionSupport { + + private final CanonicalJson json = new CanonicalJson(); + private final Function endpoint; + + private final TemplateSupport templateSupport; + + private static final String COMPOSITION; + + static { + try { + URL url = CompositionSupport.class.getResource("composition.json"); + COMPOSITION = IOUtils.toString(url, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public CompositionSupport(OpenEhrClientConfig cfg) { + endpoint = uuid -> new DefaultRestCompositionEndpoint(new DefaultRestClient(cfg), uuid); + templateSupport = new TemplateSupport(cfg); + } + + public ObjectVersionId create(UUID ehrId) { + Composition composition = json.unmarshal(COMPOSITION, Composition.class); + templateSupport.ensureTemplateExistence(composition); + return endpoint.apply(ehrId).mergeRaw(composition); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/CreateFolderDataTest.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/CreateFolderDataTest.java new file mode 100644 index 0000000000..706884ea57 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/CreateFolderDataTest.java @@ -0,0 +1,177 @@ +/* + * 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.testdata; + +import java.net.URI; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import net.java.quickcheck.generator.PrimitiveGenerators; +import org.ehrbase.openehr.sdk.client.openehrclient.OpenEhrClientConfig; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@Disabled +class CreateFolderDataTest { + + interface CreateEhr { + T createEhr(); + } + + interface CreateFolder { + T createRootFolder(); + } + + interface CreateEncounterFolder { + T createEncounterFolder(int num); + } + + interface CreateSubEncounterFolder { + T createSubEncounterFolder(int num); + } + + interface CreateItemsPerSubEncounterFolder { + T createItemsperSubEncounterFolder(int num); + } + + private static final String RESULT = "Created EHR[%s] With Root-Folder[%s]"; + private static final String FOLDER_ITEMS = "Creating %ds Items For Folder[%s]"; + + public class TestDataGenerator { + private final OpenEhrClientConfig cfg = new OpenEhrClientConfig(URI.create("http://localhost:8080/ehrbase/")); + private final CompositionSupport compositionSupport = new CompositionSupport(cfg); + private final FolderSupport folderSupport = new FolderSupport(cfg); + private final EhrSupport ehrSupport = new EhrSupport(cfg); + + CreateEhr< + CreateFolder< + CreateEncounterFolder< + CreateSubEncounterFolder>>>> + create() { + return () -> () -> numEncFolder -> numSubEncFolder -> numItems -> { + EhrSpec ehrSpec = new EhrSpec(ehrSupport) { + String build() { + UUID ehrId = this.ehrSupport.create(UUID.randomUUID(), "namespace_8480722"); + + String allEncounterFolder = IntStream.range(0, anyNumberLessThan(numEncFolder)) + .mapToObj(i -> { + String allSubFolder = IntStream.range(0, anyNumberLessThan(numSubEncFolder)) + .mapToObj(i1 -> { + int itemNum = anyNumberLessThan(numItems); + System.out.println( + FOLDER_ITEMS.formatted(itemNum, "subEncounterFolder" + i1)); + return createFolder( + "subEncounterFolder" + i1, + NO_FOLDER, + createItems(compositionSupport, ehrId, itemNum)); + }) + .collect(Collectors.joining(",")); + return createFolder("encounter" + i, allSubFolder, NO_ITEMS); + }) + .collect(Collectors.joining(",")); + + UUID folderUUID = + folderSupport.create(ehrId, createFolder("rootFolder", allEncounterFolder, NO_ITEMS)); + System.out.println(RESULT.formatted(ehrId, folderUUID)); + return ehrId.toString(); + } + }; + + return ehrSpec.build(); + }; + } + } + + @Test + void generateTestData() { + String ehrId = new TestDataGenerator() + .create() + .createEhr() + .createRootFolder() + .createEncounterFolder(1) + .createSubEncounterFolder(anyNumberLessThan(10)) + .createItemsperSubEncounterFolder(anyNumberLessThan(50)); + + System.out.println("Ehr[%s] Created".formatted(ehrId)); + } + + private static final String NO_ITEMS = ""; + private static final String NO_FOLDER = ""; + + private static int anyNumberLessThan(int num) { + return PrimitiveGenerators.integers(1, num).next(); + } + + private String createFolder(String name, String subFolder, String items) { + FolderSpec folderSpec = new FolderSpec(name, UUID.randomUUID()) { + String build() { + return FolderSupport.render(UUID.randomUUID(), this.name, subFolder, items); + } + }; + return folderSpec.build(); + } + + private String createItems(CompositionSupport compositionSupport, UUID ehrId, int numItems) { + return IntStream.range(0, numItems) + .mapToObj(i2 -> { + ItemSpec item = new ItemSpec(compositionSupport, "localNS") { + String build() { + return ItemSupport.create( + compositionSupport.create(ehrId).getRoot().getValue(), this.ns); + } + }; + + return item.build(); + }) + .collect(Collectors.joining(",")); + } + + abstract static class EhrSpec { + final EhrSupport ehrSupport; + + public EhrSpec(EhrSupport ehrSupport) { + this.ehrSupport = ehrSupport; + } + + abstract String build(); + } + + abstract static class FolderSpec { + final String name; + final UUID uuid; + + public FolderSpec(String name, UUID uuid) { + this.name = name; + this.uuid = uuid; + } + + abstract String build(); + } + + abstract static class ItemSpec { + final CompositionSupport compositionSupport; + final String ns; + + public ItemSpec(CompositionSupport compositionSupport, String ns) { + this.compositionSupport = compositionSupport; + this.ns = ns; + } + + abstract String build(); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/EhrSupport.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/EhrSupport.java new file mode 100644 index 0000000000..57fd076443 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/EhrSupport.java @@ -0,0 +1,69 @@ +/* + * 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.testdata; + +import com.nedap.archie.rm.ehr.EhrStatus; +import java.util.UUID; +import org.ehrbase.openehr.sdk.client.openehrclient.OpenEhrClientConfig; +import org.ehrbase.openehr.sdk.client.openehrclient.defaultrestclient.DefaultRestClient; +import org.ehrbase.openehr.sdk.client.openehrclient.defaultrestclient.DefaultRestEhrEndpoint; +import org.ehrbase.openehr.sdk.serialisation.jsonencoding.CanonicalJson; + +public class EhrSupport { + private static final String PARTY_REF_ID = "_REF_ID_"; + private static final String EHR_NS = "_EHR_NS_"; + + private static final String EHR_TEMPLATE = + """ + { + "_type": "EHR_STATUS", + "archetype_node_id": "openEHR-EHR-EHR_STATUS.generic.v1", + "name": { + "value": "EHR Status" + }, + "subject": { + "external_ref": { + "_type": "PARTY_REF", + "id": { + "_type": "GENERIC_ID", + "value": "_REF_ID_", + "scheme": "id_scheme" + }, + "namespace": "_EHR_NS_", + "type": "PERSON" + } + }, + "is_modifiable": true, + "is_queryable": true + } + """; + + private final CanonicalJson json = new CanonicalJson(); + private final DefaultRestEhrEndpoint endpoint; + + public EhrSupport(OpenEhrClientConfig cfg) { + endpoint = new DefaultRestEhrEndpoint(new DefaultRestClient(cfg)); + } + + public UUID create(UUID partyRefId, String partyRefNs) { + String ehr = EHR_TEMPLATE.replace(PARTY_REF_ID, partyRefId.toString()).replace(EHR_NS, partyRefNs); + + EhrStatus ehrStatus = json.unmarshal(ehr, EhrStatus.class); + return endpoint.createEhr(ehrStatus); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/FolderSupport.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/FolderSupport.java new file mode 100644 index 0000000000..93a690a138 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/FolderSupport.java @@ -0,0 +1,90 @@ +/* + * 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.testdata; + +import com.nedap.archie.rm.support.identification.ObjectVersionId; +import java.net.URI; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.entity.ContentType; +import org.ehrbase.openehr.sdk.client.openehrclient.OpenEhrClientConfig; +import org.ehrbase.openehr.sdk.client.openehrclient.defaultrestclient.DefaultRestClient; + +public class FolderSupport { + private static final String FOLDER_ID = "_FOLDER_ID_"; + private static final String FOLDER_NAME = "_FOLDER_NAME_"; + private static final String SUB_FOLDER = "_SUB_FOLDER_"; + private static final String FOLDER_ITEM = "_FOLDER_ITEM_"; + + private static final String FOLDER_TEMPLATE = + """ + { + "_type": "FOLDER", + "uid": { + "_type": "HIER_OBJECT_ID", + "value": "_FOLDER_ID_" + }, + "name": { + "_type": "DV_TEXT", + "value": "_FOLDER_NAME_" + }, + "archetype_node_id": "openEHR-EHR-FOLDER.generic.v1", + "folders": [ + _SUB_FOLDER_ + ], + "items": [ + _FOLDER_ITEM_ + ] + } + """; + + public static String render(UUID folderUUID, String folderName, String subFolder, String items) { + return FOLDER_TEMPLATE + .replace(FOLDER_ID, folderUUID.toString()) + .replace(FOLDER_NAME, folderName) + .replace(SUB_FOLDER, subFolder) + .replace(FOLDER_ITEM, items); + } + + public static final String EHR_PATH = "rest/openehr/v1/ehr/"; + public static final String DIR_PATH = "/directory"; + private final OpenEhrClientConfig cfg; + + public FolderSupport(OpenEhrClientConfig cfg) { + this.cfg = cfg; + } + + public UUID create(UUID ehrId, String folder) { + var restClient = new DefaultRestClient(cfg) { + ObjectVersionId doHttpPost(URI uri, String body) { + HttpResponse response = internalPost( + uri, null, body, ContentType.APPLICATION_JSON, ContentType.APPLICATION_JSON.getMimeType()); + Header eTag = response.getFirstHeader(HttpHeaders.ETAG); + return new ObjectVersionId(StringUtils.unwrap(StringUtils.removeStart(eTag.getValue(), "W/"), '"')); + } + }; + + return UUID.fromString(restClient + .doHttpPost(cfg.getBaseUri().resolve(EHR_PATH + ehrId.toString() + DIR_PATH), folder) + .getObjectId() + .getValue()); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/ItemSupport.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/ItemSupport.java new file mode 100644 index 0000000000..2ed68aa686 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/ItemSupport.java @@ -0,0 +1,39 @@ +/* + * 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.testdata; + +public class ItemSupport { + private static final String ITEM_ID = "_ITEM_ID_"; + private static final String ITEM_NS = "_ITEM_NS_"; + + private static final String ITEM_TEMPLATE = + """ + { + "id": { + "_type": "HIER_OBJECT_ID", + "value": "_ITEM_ID_" + }, + "namespace": "ITEM_NS", + "type": "VERSIONED_COMPOSITION" + } + """; + + public static String create(String itemUUID, String namespace) { + return ITEM_TEMPLATE.replace(ITEM_ID, itemUUID).replace(ITEM_NS, namespace); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/TemplateSupport.java b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/TemplateSupport.java new file mode 100644 index 0000000000..39f7bcec8b --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/aqlengine/testdata/TemplateSupport.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.testdata; + +import com.nedap.archie.rm.composition.Composition; +import java.io.IOException; +import java.util.Optional; +import java.util.function.Supplier; +import org.apache.xmlbeans.XmlException; +import org.ehrbase.openehr.sdk.client.openehrclient.OpenEhrClientConfig; +import org.ehrbase.openehr.sdk.client.openehrclient.defaultrestclient.DefaultRestClient; +import org.ehrbase.openehr.sdk.client.openehrclient.defaultrestclient.DefaultRestTemplateEndpoint; +import org.ehrbase.openehr.sdk.test_data.operationaltemplate.OperationalTemplateTestData; +import org.ehrbase.openehr.sdk.webtemplate.model.WebTemplate; +import org.ehrbase.openehr.sdk.webtemplate.templateprovider.TemplateProvider; +import org.openehr.schemas.v1.OPERATIONALTEMPLATE; +import org.openehr.schemas.v1.TemplateDocument; + +public class TemplateSupport { + + private final Supplier endpoint; + + public TemplateSupport(OpenEhrClientConfig cfg) { + endpoint = () -> new DefaultRestTemplateEndpoint(new DefaultRestClient(cfg, new TemplateProvider() { + @Override + public Optional find(String templateId) { + try { + OPERATIONALTEMPLATE template = TemplateDocument.Factory.parse( + OperationalTemplateTestData.findByTemplateId(templateId) + .getStream()) + .getTemplate(); + return Optional.of(template); + } catch (XmlException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Optional buildIntrospect(String templateId) { + return TemplateProvider.super.buildIntrospect(templateId); + } + })); + } + + public void ensureTemplateExistence(Composition composition) { + String templateId = composition.getArchetypeDetails().getTemplateId().getValue(); + endpoint.get().ensureExistence(templateId); + } +} diff --git a/aql-engine/src/test/java/org/ehrbase/openehr/util/TestConfig.java b/aql-engine/src/test/java/org/ehrbase/openehr/util/TestConfig.java new file mode 100644 index 0000000000..428a61c753 --- /dev/null +++ b/aql-engine/src/test/java/org/ehrbase/openehr/util/TestConfig.java @@ -0,0 +1,30 @@ +/* + * 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.openehr.util; + +import org.ehrbase.openehr.aqlengine.AqlConfigurationProperties; + +public class TestConfig { + + public static AqlConfigurationProperties aqlConfigurationProperties() { + return new AqlConfigurationProperties( + false, + new AqlConfigurationProperties.Experimental( + new AqlConfigurationProperties.Experimental.AqlOnFolder(false))); + } +} 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 0000000000..0308c70c6c --- /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/aql-engine/src/test/resources/org/ehrbase/openehr/aqlengine/testdata/composition.json b/aql-engine/src/test/resources/org/ehrbase/openehr/aqlengine/testdata/composition.json new file mode 100644 index 0000000000..dfbf0724dd --- /dev/null +++ b/aql-engine/src/test/resources/org/ehrbase/openehr/aqlengine/testdata/composition.json @@ -0,0 +1,1301 @@ +{ + "_type": "COMPOSITION", + "name": { + "_type": "DV_TEXT", + "value": "Bericht" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-COMPOSITION.report.v1" + }, + "template_id": { + "value": "Corona_Anamnese" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "territory": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_3166-1" + }, + "code_string": "DE" + }, + "category": { + "_type": "DV_CODED_TEXT", + "value": "event", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "openehr" + }, + "code_string": "433" + } + }, + "composer": { + "_type": "PARTY_IDENTIFIED", + "name": "birger.haarbrandt@plri.de" + }, + "context": { + "_type": "EVENT_CONTEXT", + "start_time": { + "_type": "DV_DATE_TIME", + "value": "2021-11-24T12:00:00.000+01:00" + }, + "setting": { + "_type": "DV_CODED_TEXT", + "value": "other care", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "openehr" + }, + "code_string": "238" + } + } + }, + "content": [ + { + "_type": "SECTION", + "name": { + "_type": "DV_TEXT", + "value": "Symptome" + }, + "items": [ + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Körpertemperatur" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.body_temperature.v2" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Single" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Temperatur" + }, + "value": { + "_type": "DV_QUANTITY", + "units": "°C", + "magnitude": 39.0 + }, + "archetype_node_id": "at0004" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "at0003" + } + ], + "archetype_node_id": "at0002" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.body_temperature.v2" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Husten" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Spezifisches Symptom/Anzeichen" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bezeichnung des Symptoms oder Anzeichens." + }, + "value": { + "_type": "DV_TEXT", + "value": "Husten" + }, + "archetype_node_id": "at0004" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Vorhanden?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Vorhanden", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0023" + } + }, + "archetype_node_id": "at0005" + } + ], + "archetype_node_id": "at0022" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Schnupfen" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Spezifisches Symptom/Anzeichen" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bezeichnung des Symptoms oder Anzeichens." + }, + "value": { + "_type": "DV_TEXT", + "value": "Schnupfen" + }, + "archetype_node_id": "at0004" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Vorhanden?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Vorhanden", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0023" + } + }, + "archetype_node_id": "at0005" + } + ], + "archetype_node_id": "at0022" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Heiserkeit" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Spezifisches Symptom/Anzeichen" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bezeichnung des Symptoms oder Anzeichens." + }, + "value": { + "_type": "DV_TEXT", + "value": "Heiserkeit" + }, + "archetype_node_id": "at0004" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Vorhanden?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Nicht vorhanden", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0024" + } + }, + "archetype_node_id": "at0005" + } + ], + "archetype_node_id": "at0022" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Fieber oder erhöhte Körpertemperatur" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Spezifisches Symptom/Anzeichen" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bezeichnung des Symptoms oder Anzeichens." + }, + "value": { + "_type": "DV_TEXT", + "value": "Fieber oder erhöhte Körpertemperatur" + }, + "archetype_node_id": "at0004" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Vorhanden?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Vorhanden", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0023" + } + }, + "archetype_node_id": "at0005" + } + ], + "archetype_node_id": "at0022" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Gestörter Geruchssinn" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Spezifisches Symptom/Anzeichen" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bezeichnung des Symptoms oder Anzeichens." + }, + "value": { + "_type": "DV_TEXT", + "value": "gestörter Geruchssinn" + }, + "archetype_node_id": "at0004" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Vorhanden?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Nicht vorhanden", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0024" + } + }, + "archetype_node_id": "at0005" + } + ], + "archetype_node_id": "at0022" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Gestörter Geschmackssinn" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Spezifisches Symptom/Anzeichen" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bezeichnung des Symptoms oder Anzeichens." + }, + "value": { + "_type": "DV_TEXT", + "value": "gestörter Geschmackssinn" + }, + "archetype_node_id": "at0004" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Vorhanden?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Nicht vorhanden", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0024" + } + }, + "archetype_node_id": "at0005" + } + ], + "archetype_node_id": "at0022" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Durchfall" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Spezifisches Symptom/Anzeichen" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bezeichnung des Symptoms oder Anzeichens." + }, + "value": { + "_type": "DV_TEXT", + "value": "Durchfall" + }, + "archetype_node_id": "at0004" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Vorhanden?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Nicht vorhanden", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0024" + } + }, + "archetype_node_id": "at0005" + } + ], + "archetype_node_id": "at0022" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.symptom_sign_screening.v0" + } + ], + "archetype_node_id": "openEHR-EHR-SECTION.adhoc.v1" + }, + { + "_type": "SECTION", + "name": { + "_type": "DV_TEXT", + "value": "Risikogebiet" + }, + "items": [ + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Reisefall" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.travel_event.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "INTERVAL_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Intervallereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Letzte Reise?" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Ja - national", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0006" + } + }, + "archetype_node_id": "at0004" + }, + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Bestimmte Reise" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Bestimmtes Reiseziel" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Land" + }, + "value": { + "_type": "DV_TEXT", + "value": "Deutschland" + }, + "archetype_node_id": "at0011" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Bundesland / Region" + }, + "value": { + "_type": "DV_TEXT", + "value": "Baden-Württemberg" + }, + "archetype_node_id": "at0012" + }, + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Stadt" + }, + "value": { + "_type": "DV_TEXT", + "value": "Mannheim" + }, + "archetype_node_id": "at0013" + } + ], + "archetype_node_id": "at0010" + } + ], + "archetype_node_id": "at0008" + } + ], + "archetype_node_id": "at0003" + }, + "width": { + "_type": "DV_DURATION", + "value": "P0D" + }, + "math_function": { + "_type": "DV_CODED_TEXT", + "value": "mean", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "openehr" + }, + "code_string": "146" + } + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.travel_event.v0" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Historie der Reise" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.travel_history.v0" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "History" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Jedes Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Aufenthalt in den letzten 14 Tage in einem der Risikogebiete für Coronainfektion oder Kontakt zu Menschen, die dort waren" + }, + "value": { + "_type": "DV_CODED_TEXT", + "value": "Ja", + "defining_code": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "local" + }, + "code_string": "at0112" + } + }, + "archetype_node_id": "at0111" + }, + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Ortsbeschreibung" + }, + "items": [ + { + "_type": "CLUSTER", + "name": { + "_type": "DV_TEXT", + "value": "Standort" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Standortbeschreibung" + }, + "value": { + "_type": "DV_TEXT", + "value": "Norditalien" + }, + "archetype_node_id": "at0046" + } + ], + "archetype_node_id": "openEHR-EHR-CLUSTER.location.v1" + } + ], + "archetype_node_id": "at0134" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.travel_history.v0" + } + ], + "archetype_node_id": "openEHR-EHR-SECTION.adhoc.v1" + }, + { + "_type": "OBSERVATION", + "name": { + "_type": "DV_TEXT", + "value": "Geschichte/Historie" + }, + "archetype_details": { + "archetype_id": { + "value": "openEHR-EHR-OBSERVATION.story.v1" + }, + "rm_version": "1.0.4" + }, + "language": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "ISO_639-1" + }, + "code_string": "de" + }, + "encoding": { + "_type": "CODE_PHRASE", + "terminology_id": { + "_type": "TERMINOLOGY_ID", + "value": "IANA_character-sets" + }, + "code_string": "UTF-8" + }, + "subject": { + "_type": "PARTY_SELF" + }, + "data": { + "name": { + "_type": "DV_TEXT", + "value": "Event Series" + }, + "origin": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "events": [ + { + "_type": "POINT_EVENT", + "name": { + "_type": "DV_TEXT", + "value": "Beliebiges Ereignis" + }, + "time": { + "_type": "DV_DATE_TIME", + "value": "2020-05-11T22:53:12.039139+02:00" + }, + "data": { + "_type": "ITEM_TREE", + "name": { + "_type": "DV_TEXT", + "value": "Tree" + }, + "items": [ + { + "_type": "ELEMENT", + "name": { + "_type": "DV_TEXT", + "value": "Geschichte" + }, + "value": { + "_type": "DV_TEXT", + "value": "War in Ischgl" + }, + "archetype_node_id": "at0004" + } + ], + "archetype_node_id": "at0003" + }, + "archetype_node_id": "at0002" + } + ], + "archetype_node_id": "at0001" + }, + "archetype_node_id": "openEHR-EHR-OBSERVATION.story.v1" + } + ], + "archetype_node_id": "openEHR-EHR-COMPOSITION.report.v1" +} \ No newline at end of file diff --git a/base/db-setup/README.md b/base/db-setup/README.md deleted file mode 100644 index 3dce8a5bbb..0000000000 --- a/base/db-setup/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Notes on Deploying EHRBASE DB on managed PostgreSQL servers (Cloud Configuration) - -C. Chevalley 23/09/2019 - -Most, if not all, cloud providers provide PostgreSQL as managed DB server, integrating commonly supported extensions. -However, more specific extensions including 'jsquery' and 'temporal_tables' are generally not supported by these -vendors. - -EHRbase does support deployment in the cloud with the following provisions: - -1. Extension 'temporal_tables' is replaced by a compatible plpg/sql function: versioning() -2. Extension 'jsquery' is bypassed at AQL processing level by a configuration parameter - -## Deployment on a Cloud Service -Assuming a managed database for PostgreSQL (10+) is available, the DB can be configured with the following scripts: - -`# cd base/db-setup` - -`sudo -u postgres psql < cloud-db-setup.sql` - -This configures ehrbase db with the following output: - -```markdown -CREATE DATABASE -GRANT -You are now connected to database "ehrbase" as user "postgres". -CREATE SCHEMA -CREATE SCHEMA -CREATE EXTENSION -CREATE EXTENSION -ALTER DATABASE -GRANT -CREATE FUNCTION -``` - -The DB is now ready to be configured using the flyway migrations: - -`# cd ..` - -`mvn flyway:migrate` - -The process configures tables, indexes, triggers as required for ehrbase. - -## Configuring the Application to Enable SQL Querying Without Extension -To direct EHRbase to perform AQL querying without `jsquery` edit the YAML application configuration. - -For a cloud deployment, use `application-cloud.yml` (in `application/resources`) that read: - -```markdown -server: - ... - aql: - use-jsquery: false -``` - -This can be achieved during test by adding the application parameter specifying the configuration to use: - -`--spring.config.location=classpath:application-cloud.yml` - -For example: - -`java -jar application/target/application-0.8.0.jar --spring.config.location=classpath:application-cloud.yml` - -Alternatively, the parameter can be passed as an argument to the command line: - -`java -jar application/target/application-0.8.0.jar -server.aql.use-jsquery=true` - -By default, `server.aql.use-jsquery` is set to `true`, setting it to false indicate to the AQL processor to use standard -json path resolution instead of jsquery resolution in a WHERE clause. For more details on this see - -- https://github.com/postgrespro/jsquery -- https://pgxn.org/dist/jsquery/ -- https://www.postgresql.org/docs/current/functions-json.html - - diff --git a/base/db-setup/cloud-db-setup.sql b/base/db-setup/cloud-db-setup.sql deleted file mode 100644 index 6cc3c8f160..0000000000 --- a/base/db-setup/cloud-db-setup.sql +++ /dev/null @@ -1,214 +0,0 @@ --- Use this script following the creation and *migrations* of ehrbase db for CLOUD deployment --- Since most cloud service providers (AWS, AZURE, Digital Ocean) support managed PostgreSQL server instances --- they generally don't support current extensions: temporal_tables and jsquery (as of 23/09/2019). --- create database and roles (you might see an error here, these can be ignored) --- the first section of the script is similar to createdb.sql --- See README for more details re required application configuration - -CREATE ROLE ehrbase WITH LOGIN PASSWORD 'ehrbase'; -CREATE DATABASE ehrbase ENCODING 'UTF-8' TEMPLATE template0; -GRANT ALL PRIVILEGES ON DATABASE ehrbase TO ehrbase; - --- install the extensions -\c ehrbase -CREATE SCHEMA IF NOT EXISTS ehr AUTHORIZATION ehrbase; -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 ALL ON ALL FUNCTIONS IN SCHEMA ext TO ehrbase; - --- 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/cloud-db-triggers-setup.sql b/base/db-setup/cloud-db-triggers-setup.sql deleted file mode 100644 index 8b0741e793..0000000000 --- a/base/db-setup/cloud-db-triggers-setup.sql +++ /dev/null @@ -1,47 +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.audit_details - FOR EACH ROW -EXECUTE PROCEDURE ext.versioning('sys_period', 'ehr.audit_details_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/db-setup/createdb.sql b/base/db-setup/createdb.sql deleted file mode 100644 index 64938d1b38..0000000000 --- a/base/db-setup/createdb.sql +++ /dev/null @@ -1,44 +0,0 @@ --- This script needs to be run as database superuser in order to create the database --- and its required extensions. --- 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 build.gradle 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 database and roles (you might see an error here, these can be ignored) -CREATE ROLE ehrbase WITH LOGIN PASSWORD 'ehrbase'; -CREATE DATABASE ehrbase ENCODING 'UTF-8' TEMPLATE template0; -GRANT ALL PRIVILEGES ON DATABASE ehrbase TO ehrbase; - --- install the extensions -\c ehrbase -CREATE SCHEMA IF NOT EXISTS ehr AUTHORIZATION ehrbase; -CREATE SCHEMA IF NOT EXISTS ext AUTHORIZATION ehrbase; -CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA ext; -CREATE EXTENSION IF NOT EXISTS "temporal_tables" SCHEMA ext; -CREATE EXTENSION IF NOT EXISTS "jsquery" 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 ALL ON ALL FUNCTIONS IN SCHEMA ext TO ehrbase; diff --git a/base/docker-example/docker-compose.yml b/base/docker-example/docker-compose.yml deleted file mode 100644 index 33fc197645..0000000000 --- a/base/docker-example/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: "3.3" - -services: - - ethercis: - image: vitagroup-hip-docker-releases.jfrog.io/vitasystems/application:0.9.0 - depends_on: - - postgres - environment: - DB_URL: "jdbc:postgresql://postgres:5432/ehrbase" - DB_USER: postgres - DB_PASS: postgres - SERVER_NODENAME: local - OUTH2_REALM_NAME: demo - OUTH2_CLIENT_NAME: myclient - OUTH2_SERVER_URL: "http://keycloak:8080/auth" - ports: - - 8080:8080 - - postgres: - image: ehrbase/ehrbase-postgres:11.10 - - - diff --git a/base/pom.xml b/base/pom.xml deleted file mode 100644 index edda3c1428..0000000000 --- a/base/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - 4.0.0 - - - org.ehrbase.openehr - server - 0.20.0-SNAPSHOT - - - base - - - - org.flywaydb - flyway-core - - - \ No newline at end of file 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 dff5f87e2e..0000000000 --- a/base/src/main/java/db/migration/V3__terminology.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2019-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. - */ - -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(); - } - } - } -} \ No newline at end of file diff --git a/base/src/main/resources/Terminology.xsd b/base/src/main/resources/Terminology.xsd deleted file mode 100644 index 7a5ac0d3fd..0000000000 --- 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 d1ddd7a545..0000000000 --- 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 b7b395a8c9..0000000000 --- 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 050290c94d..0000000000 --- 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 bc34e430ce..0000000000 --- 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 d018df0926..0000000000 --- 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 79b4d46d78..0000000000 --- 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 2c5f220df9..0000000000 --- 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 a7cfa7765b..0000000000 --- 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 5967df2ecc..0000000000 --- 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 99f3d3f332..0000000000 --- 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 44fc22a0ea..0000000000 --- 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 443f101633..0000000000 --- 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 c62cc848df..0000000000 --- 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 44d52a4213..0000000000 --- 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 2e9665cf52..0000000000 --- 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 8b13a38a97..0000000000 --- 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 672405a29d..0000000000 --- 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 0758de1aa8..0000000000 --- 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 441d5c2232..0000000000 --- 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 65e94e9a88..0000000000 --- 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 9a02392acd..0000000000 --- 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 87dc6c0c5a..0000000000 --- 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 deb4207540..0000000000 --- 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 81f78ae407..0000000000 --- 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 cdfe5bdaf1..0000000000 --- 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 20d50fc08b..0000000000 --- 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 9a079a0f3a..0000000000 --- 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 77dd8c9e28..0000000000 --- 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 5609703843..0000000000 --- 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 1ab7874c6d..0000000000 --- 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 4a8307601d..0000000000 --- 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 4358d2c8f0..0000000000 --- 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 790c08114d..0000000000 --- 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 8fde81d962..0000000000 --- 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 4feff3db6a..0000000000 --- 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 ec1e75a127..0000000000 --- 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 520467f46b..0000000000 --- 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 235015baea..0000000000 --- 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 ec1e75a127..0000000000 --- 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 9dc2eadd25..0000000000 --- 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 49c42c1eef..0000000000 --- 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 d1a9d2003f..0000000000 --- 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 dd2ec01741..0000000000 --- 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 d9202aa05a..0000000000 --- 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 725500af51..0000000000 --- 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 baf6fe0154..0000000000 --- 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 b8ebfcbab9..0000000000 --- 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 27ed38f114..0000000000 --- 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 c021cb1dd0..0000000000 --- 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 fcd3d6f274..0000000000 --- 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 28e3edb47d..0000000000 --- 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 29680985f2..0000000000 --- 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 e205a98f27..0000000000 --- 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 4e1df58845..0000000000 --- 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 2aaee0ff6f..0000000000 --- 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 d97beb49ae..0000000000 --- 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 cbe0241a77..0000000000 --- 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 c8c483c6ab..0000000000 --- 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 e885d63418..0000000000 --- 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 d26096c5c7..0000000000 --- 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 6f6ef61db4..0000000000 --- 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/V6__aql_v1_2_0.sql b/base/src/main/resources/db/migration/V6__aql_v1_2_0.sql deleted file mode 100644 index ede2cc0985..0000000000 --- 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/V7__meta_cache.sql b/base/src/main/resources/db/migration/V7__meta_cache.sql deleted file mode 100644 index 74f841bbc2..0000000000 --- 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/V8__folders_support.sql b/base/src/main/resources/db/migration/V8__folders_support.sql deleted file mode 100644 index 49ef3e5d43..0000000000 --- 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 f0bf6ef375..0000000000 --- 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 e7a291edfd..0000000000 --- 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 4665b9e1d2..0000000000 --- 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 fa9d0dcc39..0000000000 --- a/base/src/main/resources/db/migration/beforeValidate.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Fix issue with V62__add_entry_history_missing_columns.sql - -DO -$$ -BEGIN - IF - (EXISTS - (SELECT 1 - FROM information_schema.tables - WHERE table_schema = 'ehr' - AND table_name = 'flyway_schema_history' - )) - THEN - IF - (EXISTS - (SELECT 1 - FROM ehr.flyway_schema_history - WHERE (version, checksum) = ('62', -307543225) - )) - THEN - UPDATE ehr.flyway_schema_history - SET checksum = 1440169380 - WHERE (version, checksum) = ('62', -307543225); - END IF; - END IF; -END -$$; - diff --git a/base/src/main/resources/terminology.xml b/base/src/main/resources/terminology.xml deleted file mode 100644 index 6ee9113772..0000000000 --- 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 new file mode 100644 index 0000000000..abb25df74f --- /dev/null +++ b/bom/pom.xml @@ -0,0 +1,513 @@ + + + + + + + 4.0.0 + + org.ehrbase.openehr + bom + 2.13.0-SNAPSHOT + 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.12.0 + 2.18.0 + 3.17.0 + 2.20.0 + 11.1.1 + 2.18.1 + 1.99.1 + 3.19.15 + 2.9.0 + 5.10.2 + + + 42.7.4 + + + 3.2.11 + 2.6.0 + + + 2.24.2 + 0.9.0 + 3.12.1 + 4.4 + + 1.3.2 + 4.9.10 + 3.19.4 + 1.0.5 + + + 3.13.0 + 3.4.1 + 3.8.1 + 3.5.2 + 3.2.7 + 0.8.12 + 2.43.0 + 1.7.0 + 3.5.2 + 2.17.1 + 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} + + + org.flywaydb + flyway-database-postgresql + ${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 + + + com.ethlo.cache + spring-tx-cache-decorator + ${spring-tx-cache.version} + + + + + + + + + + + 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.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + + No Snapshots Allowed! + + org.ehrbase.openehr + + + + true + + + + enforce-banned-dependencies + + enforce + + + + + + + + + + + diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 0000000000..eeb9411721 --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,54 @@ + + + + + + 4.0.0 + + + org.ehrbase.openehr + server + 2.13.0-SNAPSHOT + + + 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 0000000000..8161d1316d --- /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 0000000000..74dc75a8a6 --- /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 0000000000..0ab03b9f6b --- /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 0000000000..7becdbc301 --- /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 0000000000..ad303c7cb6 --- /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 0000000000..f6b5ffc3be --- /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 0000000000..075364c9d8 --- /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/util/ExceptionFriendlyFunction.java b/cli/src/main/java/org/ehrbase/cli/util/ExceptionFriendlyFunction.java new file mode 100644 index 0000000000..49ee426242 --- /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 0000000000..d7fc4e4535 --- /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 0000000000..1bec63c058 --- /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 0000000000..873df17772 --- /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 0000000000..1f1859d6e2 --- /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 0000000000..b542a3d630 --- /dev/null +++ b/configuration/pom.xml @@ -0,0 +1,145 @@ + + + + + + + 4.0.0 + + + org.ehrbase.openehr + server + 2.13.0-SNAPSHOT + + + 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.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 0000000000..c95985ebd6 --- /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 0000000000..39e60758f6 --- /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/configuration/src/main/java/org/ehrbase/configuration/config/HttpClientConfig.java b/configuration/src/main/java/org/ehrbase/configuration/config/HttpClientConfig.java new file mode 100644 index 0000000000..e8daa10212 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/HttpClientConfig.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.configuration.config; + +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; +import java.time.Duration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties +@ConfigurationProperties(prefix = "httpclient") +public class HttpClientConfig { + + private HttpClient client; + + private URI proxy; + private int proxyPort; + + /** + * General HTTP client with central configuration. + */ + public HttpClient getClient() { + if (this.client == null) { + var builder = HttpClient.newBuilder() + .version(Version.HTTP_2) + .followRedirects(Redirect.NEVER) + .connectTimeout(Duration.ofSeconds(20)); + + if (proxy != null && proxyPort != 0) { + builder.proxy(ProxySelector.of(new InetSocketAddress(proxy.toString(), proxyPort))); + } + + // TODO: allow configuration of authentication + // builder.authenticator(Authenticator.getDefault()); + + this.client = builder.build(); + } + return client; + } + + public URI getProxy() { + return proxy; + } + + public void setProxy(URI proxy) { + this.proxy = proxy; + } + + public int getProxyPort() { + return proxyPort; + } + + public void setProxyPort(int proxyPort) { + this.proxyPort = proxyPort; + } +} 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 ee4a62651c..ae4234edeb 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 Axel Siebert (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 * - * http://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/SwaggerConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/SwaggerConfiguration.java new file mode 100644 index 0000000000..030abb2206 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/SwaggerConfiguration.java @@ -0,0 +1,110 @@ +/* + * 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 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; + +@Configuration +public class SwaggerConfiguration { + + @Bean + public GroupedOpenApi openEhrApi() { + return GroupedOpenApi.builder() + .group("1. openEHR API") + .pathsToMatch("/rest/openehr/**") + .build(); + } + + @Bean + public GroupedOpenApi ehrScapeApi() { + return GroupedOpenApi.builder() + .group("2. EhrScape API") + .pathsToMatch("/rest/ecis/**") + .build(); + } + + @Bean + public GroupedOpenApi statusApi() { + return GroupedOpenApi.builder() + .group("3. EHRbase Status Endpoint") + .pathsToMatch("/rest/status") + .build(); + } + + @Bean + public GroupedOpenApi adminApi() { + return GroupedOpenApi.builder() + .group("4. EHRbase Admin API") + .pathsToMatch("/rest/admin/**") + .build(); + } + + @Bean + public GroupedOpenApi actuatorApi() { + return GroupedOpenApi.builder() + .group("5. Management API") + .pathsToMatch("/management/**") + .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() + .info(new Info() + .title("EHRbase API") + .description( + "EHRbase implements the [official openEHR REST API](https://specifications.openehr.org/releases/ITS-REST/latest/) and " + + "a subset of the [EhrScape API](https://www.ehrscape.com/). " + + "Additionally, EHRbase provides a custom `status` heartbeat endpoint, " + + "an [Admin API](https://docs.ehrbase.org/docs/EHRbase/Explore/Admin-REST) (if activated) " + + "and a [Status and Metrics API](https://ehrbase.readthedocs.io/en/latest/03_development/08_status_and_metrics/index.html?highlight=status) (if activated) " + + "for monitoring and maintenance. " + + "Please select the definition in the top right." + + " " + + "Note: The openEHR REST API and the EhrScape API are documented in their official documentation, not here. Please refer to their separate documentation.") + .version("v1") + .license(new License() + .name("Apache 2.0") + .url("https://github.com/ehrbase/ehrbase/blob/develop/LICENSE.md"))) + .externalDocs(new ExternalDocumentation() + .description("EHRbase Documentation") + .url("https://docs.ehrbase.org/")); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientConfiguration.java new file mode 100644 index 0000000000..b8f6da519f --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/client/HttpClientConfiguration.java @@ -0,0 +1,104 @@ +/* + * 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.client; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import javax.net.ssl.SSLContext; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.TrustAllStrategy; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ResourceUtils; + +/** + * {@link Configuration} for Apache HTTP Client. + */ +@Configuration +@EnableConfigurationProperties(HttpClientProperties.class) +@SuppressWarnings("java:S6212") +public class HttpClientConfiguration { + + @Bean + public HttpClient httpClient(HttpClientProperties properties) + throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, + IOException, KeyManagementException { + + HttpClientBuilder builder = HttpClients.custom(); + + if (properties.getSsl().isEnabled()) { + builder.setSSLContext(buildSSLContext(properties.getSsl())); + builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); + } + + if (properties.getProxy().getHost() != null && properties.getProxy().getPort() != null) { + builder.setProxy(new HttpHost( + properties.getProxy().getHost(), properties.getProxy().getPort())); + + if (properties.getProxy().getUsername() != null + && properties.getProxy().getPassword() != null) { + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( + properties.getProxy().getUsername(), + properties.getProxy().getPassword()); + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, credentials); + builder.setDefaultCredentialsProvider(credentialsProvider); + } + } + + return builder.build(); + } + + private SSLContext buildSSLContext(HttpClientProperties.Ssl properties) + throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, + IOException, KeyManagementException { + + SSLContextBuilder builder = SSLContextBuilder.create(); + + if (properties.getKeyStoreType() != null) { + builder.setKeyStoreType(properties.getKeyStoreType()); + } + builder.loadKeyMaterial( + ResourceUtils.getFile(properties.getKeyStore()), + properties.getKeyStorePassword().toCharArray(), + properties.getKeyPassword().toCharArray()); + + if (properties.getTrustStoreType() != null) { + builder.setKeyStoreType(properties.getTrustStoreType()); + } + builder.loadTrustMaterial( + ResourceUtils.getFile(properties.getTrustStore()), + properties.getTrustStorePassword().toCharArray(), + TrustAllStrategy.INSTANCE); + + return builder.build(); + } +} 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 96% 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 d4f4463126..5f567d368b 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. + * 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 * - * http://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 0000000000..ab4ca86a70 --- /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 0000000000..cbf40fd9de --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/flyway/MigrationStrategyConfig.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.configuration.config.flyway; + +import java.util.Map; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.flywaydb.core.api.pattern.ValidatePattern; +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()) + .validateOnMigrate(true) + // does not ignore *:Future migrations + // see https://documentation.red-gate.com/fd/ignore-migration-patterns-224919720.html + .ignoreMigrationPatterns(ValidatePattern.fromPattern("*:Ignored")) + .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 0000000000..1b042d5664 --- /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/configuration/src/main/java/org/ehrbase/configuration/config/jackson/JacksonConfiguration.java b/configuration/src/main/java/org/ehrbase/configuration/config/jackson/JacksonConfiguration.java new file mode 100644 index 0000000000..bd6f096f1c --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/jackson/JacksonConfiguration.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.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 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; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; + +@Configuration +public class JacksonConfiguration { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer addCustomSerialization() { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder + .serializerByType(StructuredString.class, new StructuredStringJSonSerializer()) + // RMObject support + .serializerByType(RMObject.class, new RmObjectJsonSerializer()) + .deserializerByType(Folder.class, new RmObjectJsonDeSerializer()) + // DTOs with RMObjects support + .deserializers(new DtoDeSerializer<>(EhrStatusDto.class, RmConstants.EHR_STATUS)) + .modules(new JavaTimeModule()); + } + + @Bean + @Primary + public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter( + Jackson2ObjectMapperBuilder builder) { + XmlMapper objectMapper = builder.createXmlMapper(true).build(); + objectMapper.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true); + return new MappingJackson2XmlHttpMessageConverter(objectMapper); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/plugin/EhrBasePluginManager.java b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/EhrBasePluginManager.java new file mode 100644 index 0000000000..fa26b99ccf --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/EhrBasePluginManager.java @@ -0,0 +1,135 @@ +/* + * 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.plugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.plugin.EhrBasePluginManagerInterface; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.ExtensionsInjector; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; +import org.springframework.boot.env.PropertiesPropertySourceLoader; +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.FileSystemResource; + +public class EhrBasePluginManager extends SpringPluginManager implements EhrBasePluginManagerInterface { + + private static final Map PROPERTY_SOURCE_LOADER_MAP = Stream.of( + new YamlPropertySourceLoader(), + new PropertiesPropertySourceLoader(), + new JsonPropertySourceLoader()) + .flatMap(p -> Arrays.stream(p.getFileExtensions()).map(e -> Pair.of(e, p))) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + + private final PluginManagerProperties properties; + + public EhrBasePluginManager(PluginManagerProperties properties) { + super(properties.getPluginDir()); + this.properties = properties; + } + + private boolean init = false; + + @Override + public void init() { + // Plugins will be initialised in initPlugins + } + + public void initPlugins() { + + if (!init) { + + startPlugins(); + + AbstractAutowireCapableBeanFactory beanFactory = + (AbstractAutowireCapableBeanFactory) getApplicationContext().getAutowireCapableBeanFactory(); + ExtensionsInjector extensionsInjector = new ExtensionsInjector(this, beanFactory); + extensionsInjector.injectExtensions(); + init = true; + } + } + + /** + * Create a property source from a file fileName in {@link + * PluginManagerProperties#getPluginDir()}/{@link PluginWrapper#getPluginId()} + * + * @param fileName json, yml and properties extensions are supported + * @param pluginWrapper + * @return + */ + protected PropertySource getConfig(String fileName, PluginWrapper pluginWrapper) { + + Path totalPath = Path.of(properties.getPluginConfigDir().toString(), pluginWrapper.getPluginId(), fileName); + + return Optional.of(fileName) + .map(FilenameUtils::getExtension) + .map(PROPERTY_SOURCE_LOADER_MAP::get) + .map(p -> { + try { + + return p.load(fileName, new FileSystemResource(totalPath)); + } catch (IOException e) { + throw new InternalServerException(e); + } + }) + .stream() + .flatMap(List::stream) + .findAny() + .orElseThrow( + () -> new InternalServerException(String.format("No Property Source found for %s", totalPath))); + } + + public List> loadConfig(PluginWrapper pluginWrapper) { + Path totalPath = Path.of(properties.getPluginConfigDir().toString(), pluginWrapper.getPluginId()); + + if (Files.exists(totalPath)) { + try (Stream walk = Files.walk(totalPath)) { + return walk.filter(p -> + PROPERTY_SOURCE_LOADER_MAP.keySet().contains(FilenameUtils.getExtension(p.toString()))) + .map(p -> getConfig(p.getFileName().toString(), pluginWrapper)) + .collect(Collectors.toList()); + + } catch (IOException e) { + throw new InternalServerException(e); + } + } + + return Collections.emptyList(); + } + + public static class JsonPropertySourceLoader extends YamlPropertySourceLoader { + @Override + public String[] getFileExtensions() { + return new String[] {"json"}; + } + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginConfig.java b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginConfig.java new file mode 100644 index 0000000000..74b0c30bb8 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginConfig.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.configuration.config.plugin; + +import static org.ehrbase.plugin.PluginHelper.PLUGIN_MANAGER_PREFIX; + +import java.util.HashMap; +import java.util.Map; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.plugin.WebMvcEhrBasePlugin; +import org.pf4j.PluginWrapper; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.util.UriComponentsBuilder; + +@Configuration +@EnableConfigurationProperties(PluginManagerProperties.class) +@ConditionalOnProperty(prefix = PLUGIN_MANAGER_PREFIX, name = "enable", havingValue = "true") +public class PluginConfig { + + @Bean + public EhrBasePluginManager pluginManager(Environment environment) { + + return new EhrBasePluginManager(getPluginManagerProperties(environment)); + } + // since this is used in a BeanFactoryPostProcessor the PluginManagerProperties must be bound + // manually. + private static PluginManagerProperties getPluginManagerProperties(Environment environment) { + return Binder.get(environment) + .bind(PLUGIN_MANAGER_PREFIX, PluginManagerProperties.class) + .get(); + } + + /** Register the {@link DispatcherServlet} for all {@link WebMvcEhrBasePlugin} */ + @Bean + public static BeanFactoryPostProcessor beanFactoryPostProcessor( + EhrBasePluginManager pluginManager, Environment environment) { + + PluginManagerProperties pluginManagerProperties = getPluginManagerProperties(environment); + + Map registeredUrl = new HashMap<>(); + + return beanFactory -> { + pluginManager.loadPlugins(); + + pluginManager.getPlugins().stream() + .map(PluginWrapper::getPlugin) + .filter(p -> WebMvcEhrBasePlugin.class.isAssignableFrom(p.getClass())) + .map(WebMvcEhrBasePlugin.class::cast) + .forEach(p -> register(beanFactory, pluginManagerProperties, registeredUrl, p)); + }; + } + + /** + * Register the {@link DispatcherServlet} for a {@link WebMvcEhrBasePlugin} + * + * @param beanFactory + * @param pluginManagerProperties + * @param registeredUrl + * @param p + */ + private static void register( + ConfigurableListableBeanFactory beanFactory, + PluginManagerProperties pluginManagerProperties, + Map registeredUrl, + WebMvcEhrBasePlugin p) { + + String pluginId = p.getWrapper().getPluginId(); + + final String uri = UriComponentsBuilder.newInstance() + .path(pluginManagerProperties.getPluginContextPath()) + .path(p.getContextPath()) + .path("/*") + .build() + .getPath(); + + // check for duplicate plugin uri + registeredUrl.entrySet().stream() + .filter(e -> e.getValue().equals(uri)) + .findAny() + .ifPresent(e -> { + throw new InternalServerException(String.format( + "uri %s for plugin %s already registered by plugin %s", uri, pluginId, e.getKey())); + }); + + registeredUrl.put(pluginId, uri); + + ServletRegistrationBean bean = new ServletRegistrationBean<>(p.getDispatcherServlet(), uri); + + bean.setLoadOnStartup(1); + bean.setOrder(1); + bean.setName(pluginId); + beanFactory.initializeBean(bean, pluginId); + beanFactory.autowireBean(bean); + beanFactory.registerSingleton(pluginId, bean); + } + + /** + * Create a Listener for the {@link ServletWebServerInitializedEvent } to initialise the {@link + * org.pf4j.ExtensionPoint} after all {@link DispatcherServlet} have been initialised. + * + * @param pluginManager + * @return + */ + @Bean + ApplicationListener servletWebServerInitializedEventApplicationListener( + EhrBasePluginManager pluginManager) { + + return event -> pluginManager.initPlugins(); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginManagerProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginManagerProperties.java new file mode 100644 index 0000000000..1f37978fc5 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/plugin/PluginManagerProperties.java @@ -0,0 +1,71 @@ +/* + * 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.plugin; + +import static org.ehrbase.plugin.PluginHelper.PLUGIN_MANAGER_PREFIX; + +import java.nio.file.Path; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + *

{@link ConfigurationProperties} for {@link EhrBasePluginManager}. + */ +@ConfigurationProperties(prefix = PLUGIN_MANAGER_PREFIX) +public class PluginManagerProperties { + + private Path pluginDir; + private boolean enable; + private String pluginContextPath; + private Path pluginConfigDir; + + public void setPluginDir(Path pluginDir) { + this.pluginDir = pluginDir; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public Path getPluginDir() { + return pluginDir; + } + + public void setPluginDir(String pluginDir) { + this.pluginDir = Path.of(pluginDir); + } + + public String getPluginContextPath() { + return pluginContextPath; + } + + public void setPluginContextPath(String pluginContextPath) { + this.pluginContextPath = pluginContextPath; + } + + public Path getPluginConfigDir() { + return pluginConfigDir; + } + + public void setPluginConfigDir(Path pluginConfigDir) { + this.pluginConfigDir = pluginConfigDir; + } +} 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 0000000000..d81af8464f --- /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 0000000000..e23a3358ed --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigBasicAuth.java @@ -0,0 +1,108 @@ +/* + * 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 jakarta.servlet.DispatcherType; +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 -> { + + // Permit dispatcher types forward and error + auth.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR) + .permitAll(); + + // Permit welcome page and img + auth.requestMatchers("/", "/img/**").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 0000000000..3b34dc30ab --- /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 0000000000..e834319582 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityConfigOAuth2.java @@ -0,0 +1,133 @@ +/* + * 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 jakarta.servlet.DispatcherType; +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 -> { + + // Permit dispatcher types forward and error + auth.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR) + .permitAll(); + + // Permit welcome page and img + auth.requestMatchers("/", "/img/**").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 0000000000..4d59ffcb51 --- /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/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityFilter.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityFilter.java new file mode 100644 index 0000000000..10fcf61c4a --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityFilter.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.configuration.config.security; + +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; + +public class SecurityFilter implements Filter { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + SecurityContextHolder.clearContext(); + filterChain.doFilter(servletRequest, servletResponse); + } +} diff --git a/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityProperties.java new file mode 100644 index 0000000000..5078dcf87c --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/security/SecurityProperties.java @@ -0,0 +1,130 @@ +/* + * 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 org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * Authentication type. + */ + private AuthTypes authType; + + /** + * Username. + */ + private String authUser; + + /** + * Password for the user. + */ + private String authPassword; + + /** + * Admin username. + */ + private String authAdminUser; + + /** + * Password for the admin user. + */ + private String authAdminPassword; + + /** + * User role name used with OAuth2 authentication type. + */ + private String oauth2UserRole; + + /** + * Admin role name used with OAuth2 authentication type. + */ + private String oauth2AdminRole; + + public AuthTypes getAuthType() { + return authType; + } + + public void setAuthType(AuthTypes authType) { + this.authType = authType; + } + + public String getAuthUser() { + return authUser; + } + + public void setAuthUser(String authUser) { + this.authUser = authUser; + } + + public String getAuthPassword() { + return authPassword; + } + + public void setAuthPassword(String authPassword) { + this.authPassword = authPassword; + } + + public String getAuthAdminUser() { + return authAdminUser; + } + + public void setAuthAdminUser(String authAdminUser) { + this.authAdminUser = authAdminUser; + } + + public String getAuthAdminPassword() { + return authAdminPassword; + } + + public void setAuthAdminPassword(String authAdminPassword) { + this.authAdminPassword = authAdminPassword; + } + + public String getOauth2UserRole() { + return oauth2UserRole; + } + + public void setOauth2UserRole(String oauth2UserRole) { + this.oauth2UserRole = oauth2UserRole.toUpperCase(); + } + + public String getOauth2AdminRole() { + return oauth2AdminRole; + } + + public void setOauth2AdminRole(String oauth2AdminRole) { + this.oauth2AdminRole = oauth2AdminRole.toUpperCase(); + } + + public enum AuthTypes { + NONE, + 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/ExternalValidationProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/validation/ExternalValidationProperties.java new file mode 100644 index 0000000000..7903d7d2ad --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/config/validation/ExternalValidationProperties.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.validation; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties} for external terminology validation. + */ +@ConfigurationProperties(prefix = "validation.external-terminology") +public class ExternalValidationProperties { + + private boolean enabled = false; + + private boolean authenticate = false; + + private boolean failOnError = false; + + private final Map provider = new HashMap<>(); + + public boolean isEnabled() { + return enabled; + } + + 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; + } + + public void setFailOnError(boolean failOnError) { + this.failOnError = failOnError; + } + + public Map getProvider() { + return provider; + } + + public enum ProviderType { + FHIR + } + + 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; + } + + public void setType(ProviderType type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } +} 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 0000000000..deb0810e9c --- /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 0000000000..b133a8cc32 --- /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(ExternalValidationProperties.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 ExternalValidationProperties properties; + private final CacheProvider cacheProvider; + private final OAuth2AuthorizedClientManager authorizedClientManager; + + public ValidationConfiguration( + ExternalValidationProperties 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 ExternalValidationProperties.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() == ExternalValidationProperties.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.EXTERNAL_FHIR_TERMINOLOGY_CACHE.get( + cacheProvider, 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/web/CorsProperties.java b/configuration/src/main/java/org/ehrbase/configuration/config/web/CorsProperties.java similarity index 95% 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 6e9c01d2fe..82a1e76f08 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,11 +1,13 @@ /* - * Copyright 2021 vitasystems GmbH and Hannover Medical School. + * 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 + * 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, @@ -13,20 +15,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.ehrbase.configuration.config.web; -package org.ehrbase.application.config.web; - +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.convert.DurationUnit; import org.springframework.util.CollectionUtils; import org.springframework.web.cors.CorsConfiguration; -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.List; - /** * {@link ConfigurationProperties} for CORS support. * 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 0000000000..317d0bc3ac --- /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 0000000000..f785ff178d --- /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 87% 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 f2899e97e8..cb9d87eba4 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 Axel Siebert (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 * - * http://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,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.ehrbase.application.util; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.stereotype.Component; +package org.ehrbase.configuration.util; import java.time.Instant; import java.time.ZonedDateTime; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; /** * IsoDateTimeConverter for parsing input ISO 6801 Date strings into a ZonedDateTime that contains the DateTime value diff --git a/configuration/src/main/java/org/ehrbase/configuration/web/ForwardFilter.java b/configuration/src/main/java/org/ehrbase/configuration/web/ForwardFilter.java new file mode 100644 index 0000000000..b289a36203 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/web/ForwardFilter.java @@ -0,0 +1,30 @@ +/* + * 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.web; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.ForwardedHeaderFilter; + +/** + * Handles X-Forwarded headers + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ForwardFilter extends ForwardedHeaderFilter {} diff --git a/configuration/src/main/java/org/ehrbase/configuration/web/LoggingContextFilter.java b/configuration/src/main/java/org/ehrbase/configuration/web/LoggingContextFilter.java new file mode 100644 index 0000000000..8bf7211826 --- /dev/null +++ b/configuration/src/main/java/org/ehrbase/configuration/web/LoggingContextFilter.java @@ -0,0 +1,61 @@ +/* + * 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.web; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.concurrent.ThreadLocalRandom; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Filter implementation that associates a unique traceId for logging purposes to each + * incoming request. + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class LoggingContextFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) + throws ServletException, IOException { + + try { + MDC.put("traceId", generateTraceId()); + logger.trace("Set traceId for current request"); + + filterChain.doFilter(request, response); + } finally { + MDC.remove("traceId"); + } + } + + private String generateTraceId() { + return Long.toString(ThreadLocalRandom.current().nextLong(Long.MAX_VALUE), 16); + } +} diff --git a/configuration/src/main/resources/application-cloud.yml b/configuration/src/main/resources/application-cloud.yml new file mode 100644 index 0000000000..6641a70187 --- /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 0000000000..d42797f952 --- /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 0000000000..a0fb5fb301 --- /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 0000000000..499bdbe5a1 --- /dev/null +++ b/configuration/src/main/resources/application.yml @@ -0,0 +1,248 @@ +# 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: + validation: + check-for-extra-nodes: true + validate-rm-constraints: true + aql: + pg-llj-workaround: true + experimental: + aql-on-folder: + enabled: false + template: + # Allows to override templates using POST + allow-overwrite: false + 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 + ehrscape: + enabled: false + 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: false + stored-query-init-on-startup: false + user-id-cache-config: + expire-after-access: + duration: 300 + unit: SECONDS + external-fhir-terminology-cache-config: + expire-after-write: + duration: 300 + unit: SECONDS + +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 + 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 95528ce24a..fdcac61905 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 fields allowed constraint](#422-test-case-dv_duration-fields-allowed-constraint) - - [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 - -// TBD: this DB is not used or supported by modeling tools, 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 contrained 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 contraints. 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 contraint 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) | - -TBD: more fail cases - - -## 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. So in case the SUT has an implementation decision to be considered, the developers should mention it in the Conformance Statement Document. - -### 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 | wrong ISO 8601 duration: missing duration desingator 'P' | -| P1Y | accepted | | -| P1Y3M | accepted | | -| P1W | accepted | | -| P1Y3M4D | accepted | | -| P1Y3M4DT2H | accepted | | -| P1Y3M4DT2H14M | accepted | | -| P1Y3M4DT2H14M5S | accepted | | - - -### 4.2.2. Test case DV_DURATION fields allowed constraint - -The `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 | ??? | accepted | | -| P1Y | false | true | true | true | true | true | true | ??? | rejected | C_DURATION.years_allowed | -| P1Y3M | true | true | true | true | true | true | true | ??? | accepted | | -| P1Y3M | true | false | true | true | true | true | true | ??? | rejected | C_DURATION.months_allowed | -| P1Y3M15D | true | true | true | true | true | true | true | ??? | accepted | | -| P1Y3M15D | true | true | true | false | true | true | true | ??? | rejected | C_DURATION.days_allowed | -| P1W | true | true | true | true | true | true | true | ??? | accepted | | -| P7W | true | true | false | true | true | true | true | ??? | rejected | C_DURATION.weeks_allowed | -| P1Y3M15DT23H | true | true | true | true | true | true | true | ??? | accepted | | -| P1Y3M15DT23H | true | true | true | true | false | true | true | ??? | rejected | C_DURATION.hours_allowed | -| P1Y3M15DT23H35M | true | true | true | true | true | true | true | ??? | accepted | | -| P1Y3M15DT23H35M | true | true | true | true | true | false | true | ??? | rejected | C_DURATION.minutes_allowed | -| P1Y3M15DT23H35M22S | true | true | true | true | true | true | true | ??? | accepted | | -| P1Y3M15DT23H35M22S | true | true | true | true | true | true | false | ??? | rejected | C_DURATION.seconds_allowed | - -> NOTE: the `fractional_seconds_allowed` field is not so clear since the ISO8601 would allow fractions on the lowest order component, which means if the duration lowest component is `minutes` then it's valid to have `5.23M`. Also consider in programming languages like Java, a duration string with fractions on other fields than seconds can't be parsed (for instance using https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-) - - -### 4.2.3. Test case DV_DURATION range constraint - -| 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 | ??? | TBD: it is not clear if the 20M are transformed to years to be compared with the range limits that only have years or if years in the value are compared with years in the range limits and if there are no limits for months in the range limits then the months in the value are not constrainted. | -| P2W | P0W | P3W | accepted | | -| P2W | P2W | P3W | accepted | | -| P2W | P3W | P3W | rejected | C_DURATION.range.lower | - - -### 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 | ??? | P0Y | P50Y | accepted | | -| P1Y | true | true | true | true | true | true | true | ??? | P2Y | P50Y | rejected | C_DURATION.range.lower | -| P1Y | false | true | true | true | true | true | true | ??? | P0Y | P50Y | rejected | C_DURATION.years_allowed | -| P1Y | false | true | true | true | true | true | true | ??? | P2Y | P50Y | rejected | C_DURATION.years_allowed, C_DURATION.range.lower | -| P1Y3M | true | true | true | true | true | true | Ftrue | ??? | P1Y | P50Y | accepted | | -| P1Y3M | true | false | true | true | true | true | true | ??? | P1Y | P50Y | rejected | C_DURATION.months_allowed | -| P1Y3M | true | true | true | true | true | true | true | ??? | P3Y | P50Y | rejected | C_DURATION.lower | -| P1Y3M | true | false | true | true | true | true | true | ??? | P3Y | P50Y | rejected | C_DURATION.months_allowed. C_DURATION.lower | - -> NOTE: the `fractional_seconds_allowed` field is not so clear since the ISO8601 would allow fractions on the lowest order component, which means if the duration lowest component is `minutes` then it's valid to have `5.23M`. Also consider in programming languages like Java, a duration string with fractions on other fields than seconds can't be parsed (for instance using https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-) - - -## 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