# Docker 建置具有 PAS IG 驗證的 HAPI FHIR Server

# 前言
目前 FHIR 正在持續發展,但筆者自認大家普遍都遇到官方或網路提供的驗證器驗證的很完整,結果在自己的機器建置時,卻遇到不知道如何匯入 IG (ImplementationGuide 實作指引)或 Terminology (專門術語)的問題,這份文件最終將架設出一台可以正確驗證 PAS IG 範例的 HAPI FHIR
教學涵括以下內容:
- 架設 HAPI FHIR
- 匯入 IG
- 匯入 Terminology
- 特殊案例
# 前置條件
- 具備 docker 的機器
- 基本的程式知識
- 基本的作業系統操作知識
- 擁有 JAVA 以及 postman 的機器 (與架設的機器分開)
- 認真思考的腦袋以及誠摯的心
:::info
以下教學所使用的作業系統為 debian 12
:::
# Docker Compose 架設 HAPI FHIR
## 準備 IG 檔案
在設定之前,我們需要先準備 IG 檔案以方便我們後續進行設定。
### 如何知道要準備哪些 IG 的檔案
在下載主 PAS IG 前,因為 IG 其實還會依賴其他 IG,所以我們需要先知道要準備哪些 IG 的檔案。
- 首先,打開 [PAS IG 的網站](https://nhicore.nhi.gov.tw/pas/),將頁面滾到最下方開啟 QA Report 網頁

- 找到 `Dependency Checks`區塊,旁邊會列出依賴的 IG,通常來說準備第一層的 IG 依賴檔案即可,另外 `terminology.r4` 因為實際上好像不怎麼影響,所以可以不用準備

### 下載 IG 檔案
以下是 PAS IG 需要的 IG 檔案 (`package.tgz`),如果不放心的話,可以從自行官方下載
:::info
通常 IG 檔案都會在 IG 網站的結構定義與範例檔下載或Downloads 頁面找到
:::
- PAS: https://nhicore.nhi.gov.tw/pas/package.tgz
- TWCore: [tw.gov.mohw.twcore-0.3.2.tgz](https://1drv.ms/u/c/79252f41717afb63/EZeGhVikVp9NtmvkGOO3Fu0BdupRYIrCpR0sAXEw-nGSRA?e=Iu8rm5)
- 目前`CodeSystem-icd-10-cm-2023-tw.json`與`CodeSystem-icd-10-pcs-2023-tw.json` 有點問題,這邊一樣提供自製檔給大家下載
- 更改步驟:
- 解壓縮 twcore 的 package 檔案
- 找到 `CodeSystem-icd-10-cm-2023-tw.json` 和`CodeSystem-icd-10-pcs-2023-tw.json`,把裡面的 url 數值中間的 ValueSet 改成 CodeSystem,e.g. `https://twcore.mohw.gov.tw/ig/twcore/ValueSet/icd-10-pcs-2023-tw` 改成 `https://twcore.mohw.gov.tw/ig/twcore/CodeSystem/icd-10-pcs-2023-tw`
- 更改完畢後放到原本的 package 檔案中
- davinci-pas: [hl7.fhir.us.davinci-pas-2.1.0.tgz](https://1drv.ms/u/c/79252f41717afb63/EVmFA5P8nYtFoULX4xDdnxkBn3D_o0chzyXI_p9WMK746g?e=XJGy6d)
- 不知為何,在官網找不到 package.tgz 檔案,所以這邊自製 tgz,提供大家下載
- 自製步驟如下:
- 使用 npm 下載 package 的內容
```bash
npm --registry https://packages.simplifier.net install hl7.fhir.us.davinci-pas@2.1.0
```
- 將下載的 `hl7.fhir.us.davinci-pas` 資料夾用 7z 壓縮成 tar 檔案,壓縮後再使用 gzip進行一次壓縮
:::warning
下載的檔案,請記得都放在當前目錄的 data 資料夾裡面
:::
## 準備 application.yaml
`application.yaml`檔案是 HAPI FHIR 的設定檔,因為後續要將設定檔映射到 Docker 容器內,所以我們先準備設定檔
- 建立資料夾
```bash
mkdir configs
```
- 進入 `configs` 資料夾當中
- 新增檔案 `application.yaml`
:::info
以下設定檔是精簡過的,如果要官方完整範例請參見 [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml)
:::
:::danger
注意! 以下的 `datasource.password` 為範例,生產環境請使用更有強度的密碼
:::
```yaml!
#Uncomment the "servlet" and "context-path" lines below to make the fhir endpoint available at /example/path/fhir instead of the default value of /fhir
server:
port: 8080
#Adds the option to go to eg. http://localhost:8080/actuator/health for seeing the running configuration
#see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
management:
endpoint:
endpoints:
enabled-by-default: false
web:
exposure:
include: health,prometheus
health:
enabled: true
probes:
enabled: true
livenessState:
enabled: true
readinessState:
enabled: true
prometheus:
enabled: true
metrics:
export:
enabled: true
spring:
main:
allow-circular-references: true
flyway:
enabled: false
baselineOnMigrate: true
fail-on-missing-locations: false
datasource:
#url: 'jdbc:h2:file:./target/database/h2'
#url: jdbc:h2:mem:test_mem
url: 'jdbc:postgresql://postgres:5432/hapi'
username: hapifhir
password: hapifhir
driverClassName: org.postgresql.Driver
max-active: 15
# database connection pool size
hikari:
maximum-pool-size: 10
jpa:
properties:
hibernate.format_sql: false
hibernate.show_sql: false
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
### These settings will enable fulltext search with lucene or elastic
hibernate.search.enabled: true
hapi:
fhir:
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
### Flag is false by default, can be passed as command line argument to override.
cr:
enabled: false
cdshooks:
enabled: false
### This enables the swagger-ui at /fhir/swagger-ui/index.html as well as the /fhir/api-docs (see https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html)
openapi_enabled: true
### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5
fhir_version: R4
### Flag is false by default. This flag enables runtime installation of IG's.
ig_runtime_upload_enabled: false
### enable to set the Server URL
server_address: http://localhost:8080/fhir
# 要匯入的 IG 設定區塊
implementationguides:
twcore:
name: tw.gov.mohw.twcore
version: 0.3.2
reloadExisting: false
installMode: STORE_AND_INSTALL
packageUrl: file:///data/tw.gov.mohw.twcore-0.3.2.tgz
davinci-pas:
name: hl7.fhir.us.davinci-pas
version: 2.1.0
reloadExisting: false
installMode: STORE_AND_INSTALL
packageUrl: file:///data/hl7.fhir.us.davinci-pas-2.1.0.tgz
pas:
name: tw.gov.mohw.nhi.pas
version: 1.0.5
reloadExisting: false
installMode: STORE_AND_INSTALL
packageUrl: file:///data/tw.gov.mohw.nhi.pas-1.0.5.tgz
##################################################
# Allowed Bundle Types for persistence (defaults are: COLLECTION,DOCUMENT,MESSAGE)
##################################################
allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
allow_cascading_deletes: true
allow_contains_searches: true
allow_external_references: true
allow_multiple_delete: true
allow_override_default_search_params: false
advanced_lucene_indexing: false
bulk_export_enabled: false
bulk_import_enabled: false
# language_search_parameter_enabled: true
# enforce_referential_integrity_on_delete: false
# This is an experimental feature, and does not fully support _total and other FHIR features.
# enforce_referential_integrity_on_delete: false
# enforce_referential_integrity_on_write: false
# etag_support_enabled: true
# expunge_enabled: true
client_id_strategy: ANY
# server_id_strategy: SEQUENTIAL_NUMERIC
# fhirpath_interceptor_enabled: false
# filter_search_enabled: true
# graphql_enabled: true
narrative_enabled: false
mdm_enabled: false
mdm_rules_json_location: "mdm-rules.json"
# local_base_urls:
# - https://hapi.fhir.org/baseR4
logical_urls:
- http://terminology.hl7.org/*
- https://terminology.hl7.org/*
- http://snomed.info/*
- https://snomed.info/*
- http://unitsofmeasure.org/*
- https://unitsofmeasure.org/*
- http://loinc.org/*
- https://loinc.org/*
# partitioning:
# allow_references_across_partitions: false
# partitioning_include_in_search_hashes: false
cors:
allow_Credentials: true
# These are allowed_origin patterns, see: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#setAllowedOriginPatterns-java.util.List-
allowed_origin:
- '*'
# Search coordinator thread pool sizes
search-coord-core-pool-size: 20
search-coord-max-pool-size: 100
search-coord-queue-capacity: 200
tester:
home:
name: Local Tester
server_address: 'http://localhost:8080/fhir'
refuse_to_fetch_third_party_urls: false
fhir_version: R4
global:
name: Global Tester
server_address: "http://hapi.fhir.org/baseR4"
refuse_to_fetch_third_party_urls: false
fhir_version: R4
validation:
requests_enabled: true
# responses_enabled: true
# binary_storage_enabled: true
inline_resource_storage_below_size: 4000
```
### 匯入 IG 設定說明
在上面的設定檔當中,可以找到 `hapi.fhir.implementationguides`區塊的設定,此區塊就是用於設定你的 HAPI FHIR 在啟動時需要匯入哪些 IG。如果從官方的範例參考的話你會看到這樣的設定
```yaml
# 官方範例
# 第一種
swiss:
name: swiss.mednet.fhir
version: 0.8.0
reloadExisting: false
installMode: STORE_AND_INSTALL
# 第二種 example not from registry
ips_1_0_0:
packageUrl: https://build.fhir.org/ig/HL7/fhir-ips/package.tgz
name: hl7.fhir.uv.ips
version: 1.0.0
```
第一種範例是從官方 IG registry 抓取,而第二種方式是提供網址讓 HAPI FHIR 判斷並下載。
筆者自己比較喜歡自己想到的第三種方式,**用本地檔案匯入**,這樣不只可以保持最新,且避免網路問題,更加提高使用的穩定性。
## 準備 docker-compose.yaml
檔案內容如下:
:::danger
注意! 以下的 `POSTGRES_PASSWORD` 為範例,生產環境請使用更有強度的密碼
:::
```yaml!
services:
db:
container_name: postgres
image: postgres:16-alpine
restart: always
volumes:
- ./db:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: hapifhir
POSTGRES_USER: hapifhir
POSTGRES_DB: hapi
fhir:
container_name: hapi
image: gitlab-registry.dicom.tw/a5566qq123/hapi-fhir-jpaserver-starter:v7.4.0
user: root
ports:
- "8080:8080"
volumes:
- ./configs/:/configs
- ./data/:/data
depends_on:
- db
environment:
SPRING_CONFIG_LOCATION: 'file:///configs/application.yaml'
entrypoint: ["java", "-Xmx8G", "--class-path", "/app/main.war", "-Dloader.path=main.war!/WEB-INF/classes/,main.war!/WEB-INF/,/app/extra-classes", "org.springframework.boot.loader.PropertiesLauncher"]
```
你可以發現,裡面所使用的 image 是 `gitlab-registry.dicom.tw/a5566qq123/hapi-fhir-jpaserver-starter:v7.4.0`,這是因為 PAS 有一些特殊的 ValueSet 沒辦法被 expand,而自製的 image,原因可以參考這篇「[修正 HAPI FHIR ValueSet 無法正常 Expand 特殊符號的 Code](https://hackmd.io/@chinlinblog/BJUlT_CH1x?utm_source=preview-mode&utm_medium=rec)」
## 啟動
- 進到擁有 `docker-compose.yaml` 檔案的目錄並輸入以下指令啟動 HAPI FHIR
:::info
這裡沒有使用 -d 參數是因為方便查看 log
:::
```bash!
sudo docker compose up
```
- 啟動後,觀察 log 應該就可以看到關於安裝 IG 的訊息囉

:::warning
匯入 IG 過程中,建議不要進行任何操作,在最後會有很多的 Saved 1000 deferred 的 LOG 訊息,請等待到沒看到這類的訊息之後再進行操作
:::
## 小驗證
在進行匯入 terminology 的動作前,我們先進行 HAPI FHIR 驗證功能狀態是否正常運作。
### 驗證 Patient
patient 作為最常用的 resource,當然要來驗證一下
- 首先打開 postman,網址輸入 `http://${baseUrl}/fhir/Patient/$validate`,其中`${baseUrl}` 請替換成自己的 HAPI FHIR Server 的基本網址
- Method 選擇 POST

- 下面的 tab 切換成 Body,格式改成 raw 選擇 JSON

- 下面黑框貼上 [PAS IG 提供的 Patient 範例](https://nhicore.nhi.gov.tw/pas/Patient-pat-min.json)
```json
{
"resourceType" : "Patient",
"id" : "pat-min",
"meta" : {
"profile" : ["https://nhicore.nhi.gov.tw/pas/StructureDefinition/Patient-twpas"]
},
"text" : {
"status" : "generated",
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div style=\"display: inline-block; background-color: #d9e0e7; padding: 6px; margin: 4px; border: 1px solid #8da1b4; border-radius: 5px; line-height: 60%\"><p style=\"margin-bottom: 0px\">Profile: <a href=\"StructureDefinition-Patient-twpas.html\">病人資訊-Patient TWPAS</a></p></div><blockquote><p><b>識別碼型別</b>:National Person Identifier <span style=\"background: LightGoldenRodYellow; margin: 4px; border: 1px solid khaki\">( <a href=\"http://terminology.hl7.org/CodeSystem/v2-0203\">Identifier Type Codes</a>#NNxxx)</span><br/><b>身分證字號(official)</b>:A123456789 (http://www.moi.gov.tw)</p></blockquote><blockquote><p><b>識別碼型別</b>:Medical record number <span style=\"background: LightGoldenRodYellow; margin: 4px; border: 1px solid khaki\">( <a href=\"http://terminology.hl7.org/CodeSystem/v2-0203\">Identifier Type Codes</a>#MR)</span><br/><b>病歷號(official)</b>:123456 (https://tpech.gov.taipei)</p></blockquote><p><b>姓名(usual)</b>:王大明</p><p><b>性別</b>:男</p><p><b>出生日期</b>:2001-01-01</p></div>"
},
"identifier" : [{
"use" : "official",
"type" : {
"coding" : [{
"system" : "http://terminology.hl7.org/CodeSystem/v2-0203",
"code" : "NNxxx"
}]
},
"system" : "http://www.moi.gov.tw",
"value" : "A123456789"
},
{
"use" : "official",
"type" : {
"coding" : [{
"system" : "http://terminology.hl7.org/CodeSystem/v2-0203",
"code" : "MR"
}]
},
"system" : "https://tpech.gov.taipei",
"value" : "123456"
}],
"name" : [{
"use" : "usual",
"text" : "王大明"
}],
"gender" : "male",
"birthDate" : "2001-01-01"
}
```
- 點擊 Send 送出
- 驗證完畢後,就會收到 200 OK 的訊息,就代表 Patient 沒問題,且 HAPI FHIR 有正確匯入 IG 啦

### 驗證 Claim
因為 [PAS IG 提供的 Claim 範例](https://nhicore.nhi.gov.tw/pas/Claim-cla-1.json)中,有 CodeSystem 為 `https://twcore.mohw.gov.tw/ig/twcore/CodeSystem/icd-10-pcs-2023-tw` 的資料,可以間接驗證我們匯入的 TWCore IG 的 icd-10 code 是否正常
:::warning
如果出現驗證失敗,且在錯誤訊息當中有看到 ICD-10 值集相關的內容,請確認 CodeSystem/icd-10-cm-2023-tw 與 CodeSystem/icd-10-pcs-2023-tw 的 url 是否正常,你可以透過 postman 用 GET http://${baseUrl]/fhir/CodeSystem/icd-10-cm-2023-tw 查看 url 數值
:::
- 前面步驟與驗證 Patient 一樣,所以下面只貼出驗證結果

### 驗證 Observation
以下使用的是[檢驗檢查-單項的 Observation 範例](https://nhicore.nhi.gov.tw/pas/Observation-obs-diagnostic-min.json)**,**因為它涵蓋從 LOINC 來的 Code,比較好做匯入 Terminology 之後的比較
- 與前面的步驟都相同,下面只附上驗證結果
- 你可以發現,這次的結果為驗證失敗 (422),因為我們還沒匯入 LOINC Code,HAPI FHIR 的索引認不出來導致失敗了

# 匯入 Terminology
## 下載 HAPI FHIR CLI
在匯入 Terminology 之前,我們要先去下載匯入工具 HAPI FHIR CLI
- 進到 [HAPI FHIR 的 release 頁面](https://github.com/hapifhir/hapi-fhir/releases/tag/v8.2.1)
- 下載 [**hapi-fhir-8.2.1-cli.zip](https://github.com/hapifhir/hapi-fhir/releases/download/v8.2.1/hapi-fhir-8.2.1-cli.zip)** 檔案

- 下載完畢後,進行解壓縮

## 下載 LOINC Code
:::info
請先自行創建帳號,並登入
:::
- 進到 LOINC Code [下載頁面](https://loinc.org/downloads/)
- 點擊 Download 圖示

- 點擊後,進去看規章並按下載
:::warning
本教學只涵蓋 LOINC Code 的匯入,因 ICD-10 TWCore 已經有內建在 package,而 SNOMED 正常須為會員國或收費才能使用
:::
## 準備 loincupload.properties 檔案
- 創建 `loincupload.properties` 檔案,內容如下
```json
loinc.hierarchy.file=AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv
loinc.codesystem.version=2.80
```
## 進行匯入
```json
.\hapi-fhir-cli upload-terminology -d ${loinc-zip} -d ${loinc-properties} -v r4 -t http://${baseUrl}/fhir -u http://loinc.org -s 10GB
```
- `${loinc-zip}`: 為上一步下載的 loinc zip 壓縮檔的路徑
- `${loinc-properties}`: 為上一步驟創建的 loinc.properties 檔案路徑
- `${baseUrl}`: 你的 HAPI FHIR 的基本網址,e.g. localhost:8080
- 出現以下訊息,就代表在匯入中囉

- 等到出現 Upload Complete 就代表匯入完成囉!

- 請注意,雖然 Terminology 匯入完成,但 HAPI FHIR 會在背景執行將 Code 進行索引,請等待 c.u.f.j.term.TermDeferredStorageSvcImpl 這類的處理訊息不再跳出為止在操作

:::info
索引過程需要大量時間,請耐心等待
:::
- 最後出現 Saved xxxxxxx concepts in hh:mm:ss 就代表索引完成囉

## 再度驗證
我們一樣使用[驗證 Observation](https://www.notion.so/Observation-2356d2290ebc807fa0b7c11985fde043?pvs=21) 章節的[檢驗檢查-單項的 Observation 範例](https://nhicore.nhi.gov.tw/pas/Observation-obs-diagnostic-min.json)進行驗證
- 驗證後,可以發現驗證通過囉!

- 如果把中間的 777-3 的 loinc code 改掉再進行一次驗證,就會發生失敗的狀況,這樣就代表我們 loinc code 有匯入成功,可以正常驗證 loinc code 囉

# Support Me
文件創作花費了很多心血製作,如果你覺得很有幫助
不妨贊助我一下喝杯咖啡唄,[Support Me](https://portaly.cc/Li070/support)