diff --git a/_build.bat b/_build.bat index 99d0e1f9..903d207a 100644 --- a/_build.bat +++ b/_build.bat @@ -25,38 +25,55 @@ IF EXIST "%input_cache_path%%publisher_jar%" ( ) ELSE ( SET "jar_location=not_found" SET "default_choice=1" + SET "default_reason=publisher not found" ECHO publisher.jar not found in input-cache or parent folder ) ) :: Handle command-line argument to bypass the menu -IF NOT "%~1"=="" ( - IF /I "%~1"=="update" SET "userChoice=1" - IF /I "%~1"=="build" SET "userChoice=2" - IF /I "%~1"=="nosushi" SET "userChoice=3" - IF /I "%~1"=="notx" SET "userChoice=4" - IF /I "%~1"=="jekyll" SET "userChoice=5" - IF /I "%~1"=="clean" SET "userChoice=6" - IF /I "%~1"=="exit" SET "userChoice=0" - GOTO executeChoice -) +:: Known first arguments select a menu option; anything else is passed through to the publisher +SET "extraArgs=" +IF "%~1"=="" GOTO showMenu +IF /I "%~1"=="update" SET "userChoice=1" & GOTO parseExtra +IF /I "%~1"=="build" SET "userChoice=2" & GOTO parseExtra +IF /I "%~1"=="nosushi" SET "userChoice=3" & GOTO parseExtra +IF /I "%~1"=="notx" SET "userChoice=4" & GOTO parseExtra +IF /I "%~1"=="jekyll" SET "userChoice=5" & GOTO parseExtra +IF /I "%~1"=="clean" SET "userChoice=6" & GOTO parseExtra +IF /I "%~1"=="exit" SET "userChoice=0" & GOTO parseExtra +:: Unknown first arg - default to build, pass all args through +SET "userChoice=2" +GOTO collectArgs +:parseExtra +SHIFT +:collectArgs +IF "%~1"=="" GOTO executeChoice +SET "extraArgs=!extraArgs! %1" +SHIFT +GOTO collectArgs +:showMenu echo --------------------------------------------------------------- ECHO Checking internet connection... -PING tx.fhir.org -4 -n 1 -w 4000 >nul 2>&1 && SET "online_status=true" || SET "online_status=false" +powershell -Command "try { $r=[System.Net.WebRequest]::Create('https://tx.fhir.org/r4/metadata'); $r.Timeout=4000; $r.GetResponse().Close(); exit 0 } catch { exit 1 }" +IF %ERRORLEVEL% EQU 0 (SET "online_status=true") ELSE (SET "online_status=false") IF "%online_status%"=="true" ( - ECHO We're online and tx.fhir.org is available. + ECHO We are online and tx.fhir.org is available. FOR /F "tokens=2 delims=:" %%a IN ('curl -s https://api.github.com/repos/HL7/fhir-ig-publisher/releases/latest ^| findstr "tag_name"') DO SET "latest_version=%%a" SET "latest_version=!latest_version:"=!" SET "latest_version=!latest_version: =!" SET "latest_version=!latest_version:~0,-1!" ) ELSE ( - ECHO We're offline or tx.fhir.org is not available, can only run the publisher without TX... + ECHO. + ECHO *** WARNING: Working offline - this is not the normal mode. + ECHO Some features including terminology rendering will not work. + ECHO. SET "txoption=-tx n/a" SET "latest_version=unknown" SET "default_choice=4" + SET "default_reason=working offline" ) echo --------------------------------------------------------------- @@ -74,14 +91,16 @@ IF NOT "%jar_location%"=="not_found" ( ECHO Publisher version: !publisher_version!; Latest is !latest_version! IF NOT "%online_status%"=="true" ( - ECHO We're offline. + ECHO We are offline. ) ELSE ( IF NOT "!publisher_version!"=="!latest_version!" ( ECHO An update is recommended. SET "default_choice=1" + SET "default_reason=newer version available" ) ELSE ( ECHO Publisher is up to date. SET "default_choice=2" + SET "default_reason=publisher is up to date" ) ) @@ -96,12 +115,9 @@ echo 4. Build IG - force no TX server echo 5. Jekyll build echo 6. Clean up temp directories echo 0. Exit -:: echo [Press Enter for default (%default_choice%) or type an option number:] echo. -:: Using CHOICE to handle input with timeout -:: ECHO [Enter=Continue, 1-7=Option, 0=Exit] -choice /C 12345670 /N /CS /D %default_choice% /T 5 /M "Choose an option number or wait 5 seconds for default (%default_choice%):" +choice /C 12345670 /N /CS /D %default_choice% /T 5 /M "Choose an option number or wait 5 seconds for default (%default_choice% - %default_reason%):" SET "userChoice=%ERRORLEVEL%" @@ -115,15 +131,12 @@ IF "%userChoice%"=="4" GOTO publish_notx IF "%userChoice%"=="5" GOTO debugjekyll IF "%userChoice%"=="6" GOTO clean IF "%userChoice%"=="0" EXIT /B - -:end - - +GOTO endscript :debugjekyll echo Running Jekyll build... jekyll build -s temp/pages -d output -GOTO end +GOTO endscript :clean @@ -152,10 +165,7 @@ GOTO end echo Removed: .\template ) -GOTO end - - - +GOTO endscript :downloadpublisher @@ -198,7 +208,7 @@ IF DEFINED FORCE ( GOTO download ) -IF "%skipPrompts%"=="y" ( +IF "%skipPrompts%"=="true" ( SET create=Y ) ELSE ( SET /p create="Download? (Y/N) " @@ -211,7 +221,7 @@ IF /I "%create%"=="Y" ( GOTO done :upgrade -IF "%skipPrompts%"=="y" ( +IF "%skipPrompts%"=="true" ( SET overwrite=Y ) ELSE ( SET /p overwrite="Overwrite %jarlocation%? (Y/N) " @@ -265,7 +275,7 @@ GOTO done ECHO. ECHO Updating scripts -IF "%skipPrompts%"=="y" ( +IF "%skipPrompts%"=="true" ( SET updateScripts=Y ) ELSE ( SET /p updateScripts="Update scripts? (Y/N) " @@ -273,7 +283,7 @@ IF "%skipPrompts%"=="y" ( IF /I "%updateScripts%"=="Y" ( GOTO scripts ) -GOTO end +GOTO endscript :scripts @@ -299,12 +309,12 @@ ECHO Updating _build.bat call POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%build_bat_url%\",\"_build.new.bat\") } else { Invoke-WebRequest -Uri "%build_bat_url%" -Outfile "_build.new.bat" } if %ERRORLEVEL% == 0 goto upd_script_2 echo "Errors encountered during download: %errorlevel%" -goto end +goto endscript :upd_script_2 start copy /y "_build.new.bat" "_build.bat" ^&^& del "_build.new.bat" ^&^& exit -GOTO end +GOTO endscript :publish_once @@ -312,14 +322,15 @@ GOTO end SET JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 :: Debugging statements before running publisher -ECHO 1jar_location is: %jar_location% +ECHO jar_location is: %jar_location% IF NOT "%jar_location%"=="not_found" ( - java %JAVA_OPTS% -jar "%jar_location%" -ig . %txoption% %* + ECHO IG Publisher FOUND, Publishing... + java %JAVA_OPTS% -jar "%jar_location%" -ig . %txoption% %extraArgs% ) ELSE ( - ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run _updatePublisher. Aborting... + ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run the script and update the publisher. Aborting... ) -GOTO end +GOTO endscript @@ -328,14 +339,14 @@ GOTO end SET JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 :: Debugging statements before running publisher -ECHO 3jar_location is: %jar_location% +ECHO jar_location is: %jar_location% IF NOT "%jar_location%"=="not_found" ( - java %JAVA_OPTS% -jar "%jar_location%" -ig . %txoption% -no-sushi %* + java %JAVA_OPTS% -jar "%jar_location%" -ig . %txoption% -no-sushi %extraArgs% ) ELSE ( - ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run _updatePublisher. Aborting... + ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run the script and update the publisher. Aborting... ) -GOTO end +GOTO endscript :publish_notx @@ -344,43 +355,33 @@ SET txoption=-tx n/a SET JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 :: Debugging statements before running publisher -ECHO 2jar_location is: %jar_location% +ECHO jar_location is: %jar_location% IF NOT "%jar_location%"=="not_found" ( - java %JAVA_OPTS% -jar "%jar_location%" -ig . %txoption% %* + java %JAVA_OPTS% -jar "%jar_location%" -ig . %txoption% %extraArgs% ) ELSE ( - ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run _updatePublisher. Aborting... + ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run the script and update the publisher. Aborting... ) -GOTO end - - +GOTO endscript :publish_continuous SET JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 -:: Debugging statements before running publisher -ECHO Checking %input_cache_path% for publisher.jar -IF EXIST "%input_cache_path%\%publisher_jar%" ( - java %JAVA_OPTS% -jar "%input_cache_path%\%publisher_jar%" -ig . %txoption% -watch %* +ECHO jar_location is: %jar_location% +IF NOT "%jar_location%"=="not_found" ( + java %JAVA_OPTS% -jar "%jar_location%" -ig . %txoption% -watch %extraArgs% ) ELSE ( - ECHO Checking %upper_path% for publisher.jar - IF EXIST "..\%publisher_jar%" ( - java %JAVA_OPTS% -jar "..\%publisher_jar%" -ig . %txoption% -watch %* - ) ELSE ( - ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run _updatePublisher. Aborting... - ) + ECHO IG Publisher NOT FOUND in input-cache or parent folder. Please run the script and update the publisher. Aborting... ) -GOTO end +GOTO endscript -:end +:endscript :: Pausing at the end - - IF NOT "%skipPrompts%"=="true" ( PAUSE ) diff --git a/_build.sh b/_build.sh new file mode 100755 index 00000000..69ac585c --- /dev/null +++ b/_build.sh @@ -0,0 +1,220 @@ +#!/bin/bash + +set -e + +# Variables +dlurl="https://github.com/HL7/fhir-ig-publisher/releases/latest/download/publisher.jar" +publisher_jar="publisher.jar" +input_cache_path="$(pwd)/input-cache/" +skipPrompts=false +upper_path="../" +scriptdlroot="https://raw.githubusercontent.com/HL7/ig-publisher-scripts/main" +build_bat_url="${scriptdlroot}/_build.bat" +build_sh_url="${scriptdlroot}/_build.sh" + +function check_jar_location() { + if [ -f "${input_cache_path}${publisher_jar}" ]; then + jar_location="${input_cache_path}${publisher_jar}" + echo "Found publisher.jar in input-cache" + elif [ -f "${upper_path}${publisher_jar}" ]; then + jar_location="${upper_path}${publisher_jar}" + echo "Found publisher.jar in parent folder" + else + jar_location="not_found" + echo "publisher.jar not found in input-cache or parent folder" + fi +} + +function check_internet_connection() { + local target="tx.fhir.org" + local reachable=false + + if command -v ping > /dev/null 2>&1; then + if ping -c 1 -W 5 "$target" > /dev/null 2>&1 \ + || ping -c 1 -w 5 "$target" > /dev/null 2>&1; then + reachable=true + fi + elif command -v curl > /dev/null 2>&1; then + if curl --silent --max-time 5 --output /dev/null "https://$target"; then + reachable=true + fi + else + echo "WARNING: Neither ping nor curl available — assuming offline." + fi + + if [ "$reachable" = "true" ]; then + online=true + echo "We're online and $target is available." + latest_version=$(curl -s https://api.github.com/repos/HL7/fhir-ig-publisher/releases/latest | grep tag_name | cut -d'"' -f4) + else + online=false + echo "" + echo "⚠️ WARNING: Working offline — this is not the normal mode." + echo " Some features (e.g. terminology rendering) will not work." + echo "" + fi +} + + + +function update_publisher() { + echo "Publisher jar location: ${input_cache_path}${publisher_jar}" + if [ "$skipPrompts" = "true" ]; then + confirm="Y" + else + read -p "Download or update publisher.jar? (Y/N): " confirm + fi + if [[ "$confirm" =~ ^[Yy]$ ]]; then + echo "Downloading latest publisher.jar (~200 MB)..." + mkdir -p "$input_cache_path" + curl -L "$dlurl" -o "${input_cache_path}${publisher_jar}" + else + echo "Skipped downloading publisher.jar" + fi + + update_scripts_prompt +} + + +function update_scripts_prompt() { + if [ "$skipPrompts" = "true" ]; then + update_confirm="Y" + else + read -p "Update scripts (_build.bat and _build.sh)? (Y/N): " update_confirm + fi + if [[ "$update_confirm" =~ ^[Yy]$ ]]; then + echo "Updating scripts..." + curl -L "$build_bat_url" -o "_build.new.bat" && mv "_build.new.bat" "_build.bat" + curl -L "$build_sh_url" -o "_build.new.sh" && mv "_build.new.sh" "_build.sh" + chmod +x _build.sh + echo "Scripts updated." + else + echo "Skipped updating scripts." + fi +} + + +function run_publisher() { + local extra_flags=("$@") + if [ "$jar_location" != "not_found" ]; then + echo "jar_location is: $jar_location" + export JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8" + java $JAVA_OPTS -jar "$jar_location" -ig . "${extra_flags[@]}" + else + echo "IG Publisher NOT FOUND in input-cache or parent folder. Please run update. Aborting..." + fi +} + +function build_ig() { + local args=() + if [ "$online" = "false" ]; then + args+=("-tx" "n/a") + fi + run_publisher "${args[@]}" "$@" +} + +function build_nosushi() { + run_publisher -no-sushi "$@" +} + +function build_notx() { + run_publisher -tx n/a "$@" +} + +function build_continuous() { + run_publisher -watch "$@" +} + +function jekyll_build() { + echo "Running Jekyll build..." + jekyll build -s temp/pages -d output +} + +function cleanup() { + echo "Cleaning up temp directories..." + if [ -f "${input_cache_path}${publisher_jar}" ]; then + mv "${input_cache_path}${publisher_jar}" ./ + rm -rf "${input_cache_path}"* + mkdir -p "$input_cache_path" + mv "$publisher_jar" "$input_cache_path" + fi + rm -rf ./output ./template ./temp + echo "Cleanup complete." +} + +check_jar_location + +# Handle command-line arguments +# Known first arguments select a menu option; anything else is passed through to the publisher +extraArgs=() +if [ $# -gt 0 ]; then + case "$1" in + update) shift; extraArgs=("$@"); update_publisher; exit 0 ;; + build) shift; extraArgs=("$@"); check_internet_connection; build_ig "${extraArgs[@]}"; exit 0 ;; + nosushi) shift; extraArgs=("$@"); check_internet_connection; build_nosushi "${extraArgs[@]}"; exit 0 ;; + notx) shift; extraArgs=("$@"); build_notx "${extraArgs[@]}"; exit 0 ;; + jekyll) jekyll_build; exit 0 ;; + clean) cleanup; exit 0 ;; + exit) exit 0 ;; + *) + # Unknown first arg - default to build, pass all args through + extraArgs=("$@") + run_publisher "${extraArgs[@]}" + exit 0 + ;; + esac +fi + +# Interactive menu +check_internet_connection + +# Compute default choice and reason +default_choice=2 +default_reason="publisher is up to date" + +if [ "$jar_location" = "not_found" ]; then + default_choice=1 + default_reason="publisher not found" +elif [ "$online" = "false" ]; then + default_choice=4 + default_reason="working offline" +elif [ -n "$latest_version" ]; then + current_version=$(java -jar "$jar_location" -v 2>/dev/null | tr -d '\r') + if [ "$current_version" != "$latest_version" ]; then + default_choice=1 + default_reason="newer version available" + fi +fi + +echo "---------------------------------------------" +echo "Publisher: ${current_version:-unknown}; Latest: ${latest_version:-unknown}" +echo "Publisher location: $jar_location" +echo "Online: $online" +echo "---------------------------------------------" +echo +echo "Please select an option:" +echo "1) Download or update publisher" +echo "2) Build IG" +echo "3) Build IG without Sushi" +echo "4) Build IG without TX server" +echo "5) Jekyll build" +echo "6) Cleanup temp directories" +echo "0) Exit" +echo + +# Read with timeout, but default if nothing entered +echo -n "Choose an option [default: $default_choice - $default_reason]: " +read -t 5 choice || choice="$default_choice" +choice="${choice:-$default_choice}" +echo "You selected: $choice" + +case "$choice" in + 1) update_publisher ;; + 2) build_ig ;; + 3) build_nosushi ;; + 4) build_notx ;; + 5) jekyll_build ;; + 6) cleanup ;; + 0) exit 0 ;; + *) echo "Invalid option." ;; +esac diff --git a/_genonce.bat b/_genonce.bat index a9864ef1..c477ba8f 100755 --- a/_genonce.bat +++ b/_genonce.bat @@ -3,7 +3,8 @@ SET publisher_jar=publisher.jar SET input_cache_path=%CD%\input-cache ECHO Checking internet connection... -PING tx.fhir.org -4 -n 1 -w 1000 | FINDSTR TTL && GOTO isonline +powershell -Command "try { $r=[System.Net.WebRequest]::Create('https://tx.fhir.org/r4/metadata'); $r.Timeout=4000; $r.GetResponse().Close(); exit 0 } catch { exit 1 }" +IF %ERRORLEVEL% EQU 0 GOTO isonline ECHO We're offline... SET txoption=-tx n/a GOTO igpublish diff --git a/_updatePublisher.bat b/_updatePublisher.bat index 10fee381..d4e1b7d1 100755 --- a/_updatePublisher.bat +++ b/_updatePublisher.bat @@ -22,7 +22,8 @@ IF "%~1"=="/f" SET skipPrompts=y ECHO. ECHO Checking internet connection... -PING tx.fhir.org -4 -n 1 -w 4000 | FINDSTR TTL && GOTO isonline +powershell -Command "try { $r=[System.Net.WebRequest]::Create('https://tx.fhir.org/r4/metadata'); $r.Timeout=4000; $r.GetResponse().Close(); exit 0 } catch { exit 1 }" +IF %ERRORLEVEL% EQU 0 GOTO isonline ECHO We're offline, nothing to do... GOTO end diff --git a/input/pagecontent/multitenancy.md b/input/pagecontent/multitenancy.md new file mode 100644 index 00000000..3b3e2fcc --- /dev/null +++ b/input/pagecontent/multitenancy.md @@ -0,0 +1,49 @@ +The eHealth Infrastructure supports a softened form of multitenancy referred to as *coexistence*: multiple solutions share the same infrastructure while each solution decides when to search generically across the shared data and when to scope queries to its own data. The full description is maintained on the [wiki site](https://ehealth-dk.atlassian.net/wiki/spaces/EDTW/pages/2355986433/Multitenancy). + +The mechanism rests on four elements: + +1. A *coexistence tag* assigned to each solution (or pair of citizen/employee solutions) by eHealth Infrastructure system administration (FUT-S). Tag values are short and neutral with respect to vendor, product name and version. Allowed values are defined in the [ehealth-system](CodeSystem-ehealth-system.html) CodeSystem. +2. A *coexistence scope* in the caller's access token, declaring which coexistence tags the solution is permitted to use. +3. *Markup* applied by the creating solution — and in some cases by the infrastructure itself — when a resource is created. +4. *Search* in which the solution chooses whether to constrain a query with one or more coexistence tags. + +### JWT Coexistence Scopes +The coexistence tags a solution is permitted to use are carried as scopes in the `scope` claim of the access token issued by the eHealth Infrastructure. Each code in the [ehealth-system](CodeSystem-ehealth-system.html) CodeSystem has a corresponding scope, formatted as `system|code`: + +| tag | scope | +|-----|-------| +| `xa` | `http://ehealth.sundhed.dk/cs/ehealth-system\|xa` | +| `xb` | `http://ehealth.sundhed.dk/cs/ehealth-system\|xb` | +| `xc` | `http://ehealth.sundhed.dk/cs/ehealth-system\|xc` | + +Solutions spanning multiple coexistence tags receive one scope entry per tag. + +### Resource Markup +Resources fall into four categories with respect to coexistence markup: + +- **Always marked** by the creating solution: `EpisodeOfCare`, `Appointment`, `CarePlan`, `ServiceRequest`. +- **Conditionally marked** by the creating solution: `Communication`, `Observation`, `QuestionnaireResponse`, `Media`. Markup applies when the resource is created in the context of a coexistence-scoped workflow. +- **Auto-marked** by the infrastructure: resources produced by infrastructure-driven processes (rule execution, measurement submission, episode creation operations, `PlanDefinition/$apply`) inherit the coexistence tag of the triggering context. +- **Not marked**: `Patient`, `RelatedPerson`, `Organization`, `Consent`, `Provenance`. + +Markup is expressed on the resource as: + +* `meta.tag.system` = `http://ehealth.sundhed.dk/cs/ehealth-system` +* `meta.tag.code` = the assigned coexistence tag value (e.g. `xa`) +* `meta.source` = the solution's source URI on the form `urn:dk:ehealth:`, subject to the same neutrality restrictions as the tag itself + +Multiple coexistence tags may be present on the same resource where a solution complex spans more than one tag. + +### Searching with Coexistence Tags +Solutions control on a per-request basis whether a search is scoped to a coexistence tag by supplying `_tag` against the `ehealth-system` CodeSystem. For example: + +``` +GET [base]/EpisodeOfCare?subject=Patient/&_tag=http://ehealth.sundhed.dk/cs/ehealth-system|xa +``` + +Multiple tag values are comma-separated within a single `_tag` parameter. Omitting `_tag` performs a generic search across all coexistence scopes that the caller is otherwise authorized to see. + +### Infrastructure Propagation +When the infrastructure creates resources on behalf of a solution — during rule execution, measurement submission, episode creation operations and `PlanDefinition/$apply` — the coexistence tag of the triggering resource or token context is carried over to the produced resources, so that downstream markup remains consistent without explicit action from the calling solution. + +Validation errors related to coexistence tagging are listed under [ehealth-system](error-messages.html#ehealth-system) in the error messages catalogue, with related episode-of-care errors such as `EPISODEOFCARE_CREATE_MISSING_COEXISTENCE_TAGS` and `EPISODEOFCARE_CREATE_INVALID_COEXISTENCE_TAGS`. diff --git a/sushi-config.yaml b/sushi-config.yaml index cb799284..00f1c283 100644 --- a/sushi-config.yaml +++ b/sushi-config.yaml @@ -44,6 +44,7 @@ menu: Table of Contents: toc.html Home: index.html General guidance: guidance.html + Multitenancy: multitenancy.html FHIR Artifacts: Profiles and extensions: profiles.html Search Parameters: searchparams.html