摘要
Day 3 已經能盤點 Bundle 裡有哪些 Resource。Day 4 建立 TW Core 驗證層的狀態模型、固定 package metadata 與降級處理訊息,確保尚未驗證的資料不會被誤顯示成通過。
Day 3 的頁面已經能顯示:
但還有一層很重要的驗證沒有真正接上:
TW Core Profile validation
在這系列中,TW Core 使用的套件為:
Package ID: tw.gov.mohw.twcore
Package version: 1.0.0
Package coordinate: tw.gov.mohw.twcore#1.0.0
FHIR version: R4 4.0.1
理想上,系統後面會針對 Bundle 內三種 Resource 分別套用 TW Core Profile:
| Resource | TW Core Profile |
|---|---|
| Patient | TW Core Patient |
| Observation | TW Core Simple Observation |
| DiagnosticReport | TW Core DiagnosticReport |
但 Day 4 還沒有要完成正式 package loading。
原因是 TW Core package 載入、HAPI validation support、terminology 設定與 profile canonical 對應都需要實測。
如果還沒有穩定載入 package,就把 TW Core validation 顯示成 PASSED,會讓使用者誤以為資料已經符合 TW Core Profile。
所以 Day 4 先建立降級處理:
尚未穩定載入
tw.gov.mohw.twcore#1.0.0,因此 TW Core validation 顯示NOT_EVALUATED。
資料品質閘門不只要知道什麼通過,也要清楚標示什麼還沒有被檢查。
NOT_EVALUATED 狀態代表什麼?Day 3 TW Core 的 NOT_EVALUATED 是頁面硬編碼;Day 4 則改由 TwCoreValidationService 回傳資料化結果,讓 TW Core package 不穩定時,優先降級處理顯示 NOT_EVALUATED。
TW Core validation result 暫時先建立這四個欄位,顯示狀態與 package metadata(中繼資料):
| 欄位 | 說明 |
|---|---|
| status | PASSED、FAILED 或 NOT_EVALUATED |
| packageId | 固定 tw.gov.mohw.twcore |
| packageVersion | 固定 1.0.0 |
| message | 成功、失敗或安全降級原因 |
因此 Day 4 目前只會回傳:
status: NOT_EVALUATED
packageId: tw.gov.mohw.twcore
packageVersion: 1.0.0
message: 尚未穩定載入 tw.gov.mohw.twcore#1.0.0,不冒充 Profile 通過。
而這裡的 NOT_EVALUATED 有兩種情境:
第一種是合法 Bundle 已經通過前面的 gate,但 TW Core package 尚未穩定載入:
TW Core validation = NOT_EVALUATED
原因:尚未穩定載入 tw.gov.mohw.twcore#1.0.0,不冒充 Profile 通過。
第二種是前面的 JSON、FHIR R4 parse 或 Bundle gate 還沒通過,因此 TW Core 層根本不應該執行:
TW Core validation = NOT_EVALUATED
原因:JSON、FHIR R4 parse 或 Bundle gate 尚未通過。
這樣可以避免在非法 JSON 或非 Bundle 輸入時,硬跑後續 Profile validation,造成錯誤層級混亂。
NOT_EVALUATED。NOT_EVALUATED 不等於錯誤,也不等於通過。Day 4 的流程如下:
使用者貼上或上傳 JSON
↓
Jackson 檢查 JSON grammar
↓
JSON 合法?
├─ 否 → JSON parse FAILED,TW Core validation NOT_EVALUATED
└─ 是
↓
HAPI FHIR R4 parser 解析 Resource
↓
FHIR Resource 可解析?
├─ 否 → FHIR R4 parse FAILED,TW Core validation NOT_EVALUATED
└─ 是
↓
是 Bundle?
├─ 否 → Resource Type Gate FAILED,TW Core validation NOT_EVALUATED
└─ 是
↓
HAPI FHIR R4 Validator
↓
整理 OperationOutcome issues
↓
盤點 Bundle.entry
↓
進入 TW Core validation shell
↓
尚未穩定載入 tw.gov.mohw.twcore#1.0.0
↓
TW Core validation NOT_EVALUATED
這裡刻意把 TW Core validation shell 放在 Resource Type Gate 後面。因為 TW Core Profile 是套用在 Bundle 內的 Patient、Observation、DiagnosticReport 上,如果輸入根本不是 Bundle,後續層就不應該執行。
今天的架構比 Day 3 多了一個 TW Core validation shell:
Browser
|
Multipart JSON
|
Spring Controller
|
BundleParseService
/ | \
Jackson HAPI FHIR R4
Parser Parser Validator
\ | /
Resource Inventory
|
TwCoreValidationService
|
ValidationResult
|
Thymeleaf UI
Day 4 TW Core 先獨立成 TwCoreValidationService,但 parser、FHIR R4 validation 與 inventory 的流程仍由 BundleParseService 串起來。等之後真正載入 package 後,再視複雜度拆出更完整的 validation support 設定。
專案目錄新增或調整如下:
src/main/java/com/twlab/qualitygate
├─ validation
│ ├─ BundleEntrySummary.java
│ ├─ BundleParseService.java
│ ├─ OperationOutcomeIssue.java
│ ├─ ParseStatus.java
│ ├─ ResourceSummary.java
│ ├─ TwCoreValidationResult.java
│ ├─ TwCoreValidationService.java
│ └─ ValidationResult.java
└─ web
└─ ParseController.java
src/main/resources/templates
└─ index.html
TwCoreValidationResult
TwCoreValidationResult 是頁面顯示用資料傳輸物件 (DTO)。Day 4 先固定 package ID 與 package version,讓結果頁可以清楚顯示目前瞄準的 TW Core 版本。
public record TwCoreValidationResult(
ParseStatus status,
String packageId,
String packageVersion,
String message
) {
public static final String PACKAGE_ID = "tw.gov.mohw.twcore";
public static final String PACKAGE_VERSION = "1.0.0";
public static TwCoreValidationResult notEvaluated(String message) {
return new TwCoreValidationResult(
ParseStatus.NOT_EVALUATED,
PACKAGE_ID,
PACKAGE_VERSION,
message
);
}
}
這裡重用既有的 ParseStatus,因為目前狀態集合剛好皆是 PASSED、FAILED、NOT_EVALUATED。如果後續需要再另外建立規則層的狀態模型。
TwCoreValidationService
Day 4 的 service 只負責回傳降級處理結果,不做真正 Profile validation。
@Service
public class TwCoreValidationService {
public TwCoreValidationResult validate(Bundle bundle) {
return TwCoreValidationResult.notEvaluated(
"尚未穩定載入 tw.gov.mohw.twcore#1.0.0,不冒充 Profile 通過。"
);
}
public TwCoreValidationResult notEvaluatedBeforeBundleGate() {
return TwCoreValidationResult.notEvaluated(
"TW Core validation 未執行:JSON、FHIR R4 parse 或 Bundle gate 尚未通過。"
);
}
}
validate(Bundle bundle) 先保留 Bundle 參數,是為了讓後續正式實作時可以直接從 Bundle 內取出 Patient、Observation 與 DiagnosticReport,再逐一套用 TW Core Profile。
ValidationResult
Day 3 的 ValidationResult 已經有 parse status、FHIR validation status、OperationOutcome issues 與 resource inventory。Day 4 新增 twCoreValidationResult:
public record ValidationResult(
ParseStatus jsonStatus,
ParseStatus fhirR4Status,
ParseStatus resourceTypeStatus,
ParseStatus fhirValidationStatus,
TwCoreValidationResult twCoreValidationResult,
List<OperationOutcomeIssue> operationOutcomeIssues,
ResourceSummary resourceSummary,
List<BundleEntrySummary> bundleEntrySummaries,
Integer resourceCount,
String resourceType,
String errorMessage
) {}
這樣頁面不用硬編碼 TW Core 狀態,也不用猜測是否有執行 Profile validation。所有狀態都從同一個結果物件讀取。
BundleParseService 只有在確定輸入是 Bundle 後,才會呼叫 TW Core validation shell:
List<BundleEntrySummary> bundleEntrySummaries = bundle.getEntry().stream()
.map(BundleEntrySummary::fromEntry)
.toList();
TwCoreValidationResult twCoreValidationResult = twCoreValidationService.validate(bundle);
return new ValidationResult(
ParseStatus.PASSED,
ParseStatus.PASSED,
ParseStatus.PASSED,
hasErrors(validationResult.getMessages()) ? ParseStatus.FAILED : ParseStatus.PASSED,
twCoreValidationResult,
issues,
ResourceSummary.fromEntries(bundleEntrySummaries),
bundleEntrySummaries,
bundle.getEntry().size(),
"Bundle",
null
);
非法 JSON、FHIR parse 失敗或不是 Bundle 時,則回傳:
twCoreValidationService.notEvaluatedBeforeBundleGate()
讓 TW Core 層的狀態與前置條件保持一致。
Day 3 的頁面原本把 TW Core validation 寫死成 NOT_EVALUATED。Day 4 改成讀取 result.twCoreValidationResult:
<dt>TW Core validation</dt>
<dd>
<span class="status"
th:classappend="${result.twCoreValidationResult.status.name() == 'PASSED'} ? ' passed' : (${result.twCoreValidationResult.status.name() == 'FAILED'} ? ' failed' : ' not-evaluated')"
th:text="${result.twCoreValidationResult.status}">NOT_EVALUATED</span>
<span class="muted"
th:text="${result.twCoreValidationResult.packageId + '#' + result.twCoreValidationResult.packageVersion}">
tw.gov.mohw.twcore#1.0.0
</span>
<br>
<span class="muted" th:text="${result.twCoreValidationResult.message}">
尚未穩定載入 tw.gov.mohw.twcore#1.0.0,不冒充 Profile 通過。
</span>
</dd>
畫面上仍然會看到 NOT_EVALUATED,但意義已經不同。Day 3 是硬編碼文字;Day 4 是正式資料模型回傳的驗證層狀態。
Day 4 沿用 Day 3 的測試資料。
valid-minimal-lab-bundle.json
這份資料通過 JSON parse、FHIR R4 parse、Resource Type Gate 與 FHIR R4 validation。Day 4 用它確認 TW Core validation shell 會顯示:
TW Core validation: NOT_EVALUATED
Package: tw.gov.mohw.twcore#1.0.0
Message: 尚未穩定載入 tw.gov.mohw.twcore#1.0.0,不冒充 Profile 通過。
貼上:
{ not-json
這個案例用來確認 TW Core 層不會在 JSON parse 失敗時執行。
貼上:
{
"resourceType": "Patient",
"id": "patient-1"
}
這個案例可以被 HAPI FHIR parser 解析,但不符合目前入口只接受 Bundle 的規則,所以 Resource Type Gate 會失敗,TW Core 層也會維持 NOT_EVALUATED。
Day 4 最重要的展示,是 TW Core validation 不再只是頁面硬編碼,而是有自己的狀態、package metadata 與降級原因。
使用首頁預設 sample,或貼上 valid-minimal-lab-bundle.json,按下 Parse Bundle。
結果會看到:
JSON parse: PASSED
FHIR R4 parse: PASSED
Resource Type Gate: PASSED
FHIR R4 validation: PASSED
TW Core validation: NOT_EVALUATED
tw.gov.mohw.twcore#1.0.0
尚未穩定載入 tw.gov.mohw.twcore#1.0.0,不冒充 Profile 通過。

這代表前面幾層已經通過,但 TW Core Profile 尚未正式驗證。系統沒有把這一層偽裝成 Passed。
Resource summary 與 Bundle entry table 仍然維持 Day 3 行為:
Patient: 1
Observation: 1
DiagnosticReport: 1
Not evaluated: 0

貼上:
{ not-json
結果停在 JSON parse layer:
JSON parse: FAILED
FHIR R4 parse: FAILED
Resource Type Gate: FAILED
FHIR R4 validation: NOT_EVALUATED
TW Core validation: NOT_EVALUATED
TW Core message 會說明:
TW Core validation 未執行:JSON、FHIR R4 parse 或 Bundle gate 尚未通過。

這表示錯誤資料不會繼續進入後面的 Profile validation shell。
貼上 Patient Resource:
{
"resourceType": "Patient",
"id": "patient-1"
}
結果會看到:
JSON parse: PASSED
FHIR R4 parse: PASSED
Resource Type Gate: FAILED
FHIR R4 validation: NOT_EVALUATED
TW Core validation: NOT_EVALUATED
錯誤訊息會保留:
FHIR R4 parse succeeded, but resourceType is not Bundle.

這個案例用來確認 TW Core 層不會在非 Bundle 輸入上執行,避免錯誤層級混在一起。
Day 3 的 TW Core validation 還只是頁面上固定顯示的 NOT_EVALUATED。
Day 4 新增 TW Core validation result 後,變成:
| 情境 | FHIR R4 Validation | Resource Inventory | TW Core Validation |
|---|---|---|---|
| minimal lab Bundle | PASS | Patient 1、Observation 1、DiagnosticReport 1 | NOT_EVALUATED,顯示 package metadata |
缺少 Bundle.type |
FAIL | 仍可盤點 Bundle entry | NOT_EVALUATED,尚未正式載入 TW Core |
| unsupported Resource | PASS 或 warning | 非 MVP Resource 標示 NOT_EVALUATED |
NOT_EVALUATED,尚未正式載入 TW Core |
| JSON 壞掉 | NOT_EVALUATED | 不執行 | NOT_EVALUATED,前置 gate 未通過 |
| Patient Resource | NOT_EVALUATED | 不執行 | NOT_EVALUATED,前置 gate 未通過 |
重點是:
TW Core
NOT_EVALUATED現在有資料來源與原因,不再只是 UI 上的一行固定文字。
指令:
./mvnw test
正常測試結果:
Tests run: 13, Failures: 0, Errors: 0, Skipped: 0
Day 4 測試重點:
NOT_EVALUATED。tw.gov.mohw.twcore#1.0.0。NOT_EVALUATED。NOT_EVALUATED。NOT_EVALUATED 當成通過Day 4 的 NOT_EVALUATED 只代表尚未完成正式 TW Core Profile validation。它不能被寫成 Passed,也不能在 README 或文章裡描述成「已符合 TW Core」。
Package ID 與 version 應該從 TwCoreValidationResult 顯示。這樣後面真正接上 package loading 時,結果頁不用重寫。
TW Core 層必須放在 JSON parse、FHIR R4 parse 與 Bundle gate 後面。前面任一層失敗時,TW Core 只能顯示 NOT_EVALUATED。
Day 4 只是新增 TW Core validation shell,不應該改變 Patient、Observation、DiagnosticReport 的 inventory 顯示,也不應該開始判斷 Reference 正確性。
LAB-REF-001、LAB-REF-002、LAB-REF-003 是第 2 週的工作。Day 4 只建立 TW Core 層的可觀測狀態。
TwCoreValidationResult DTO。TwCoreValidationService。tw.gov.mohw.twcore#1.0.0。ValidationResult,加入 TW Core validation result。BundleParseService 中接入 TW Core validation shell。NOT_EVALUATED。Day 4 尚未處理:
tw.gov.mohw.twcore#1.0.0 package。後續分層預計會變成:
quality-gate
├─ parser
├─ fhir-r4-validator
├─ resource-inventory
├─ tw-core-validator
├─ reference-checker
├─ contract-rule
└─ report
Repository:twcore-data-quality-gate